/ GUNRPG.Core / Combat / SuppressiveFireModel.cs
SuppressiveFireModel.cs
  1  using GUNRPG.Core.Operators;
  2  using GUNRPG.Core.Weapons;
  3  
  4  namespace GUNRPG.Core.Combat;
  5  
  6  /// <summary>
  7  /// Models suppressive fire behavior when engaging targets in full cover.
  8  /// Suppressive fire is a controlled burst that applies suppression without attempting hit resolution.
  9  /// This prevents unrealistic mag-dumping into cover and maintains tactical realism.
 10  /// </summary>
 11  public static class SuppressiveFireModel
 12  {
 13      /// <summary>
 14      /// Default burst size for suppressive fire (rounds).
 15      /// Short controlled bursts, not full magazine dumps.
 16      /// </summary>
 17      public const int DefaultSuppressiveBurstSize = 3;
 18  
 19      /// <summary>
 20      /// Minimum burst size for suppressive fire.
 21      /// </summary>
 22      public const int MinSuppressiveBurstSize = 2;
 23  
 24      /// <summary>
 25      /// Maximum burst size for suppressive fire.
 26      /// </summary>
 27      public const int MaxSuppressiveBurstSize = 6;
 28  
 29      /// <summary>
 30      /// Base suppression amount applied per suppressive fire burst.
 31      /// Higher than individual shot suppression due to sustained fire intent.
 32      /// </summary>
 33      public const float BaseSuppressiveFireSuppression = 0.25f;
 34  
 35      /// <summary>
 36      /// Multiplier for suppression when target is in full cover.
 37      /// Full cover amplifies psychological pressure even if no damage is possible.
 38      /// </summary>
 39      public const float FullCoverSuppressionMultiplier = 0.8f;
 40  
 41      /// <summary>
 42      /// Time window (ms) to remember that a target was recently visible.
 43      /// After this time, AI should not continue suppressive fire.
 44      /// </summary>
 45      public const long TargetLastSeenWindowMs = 3000;
 46  
 47      /// <summary>
 48      /// Cooldown after suppressive fire before next action (ms).
 49      /// Represents tactical pause to assess effect.
 50      /// </summary>
 51      public const int PostSuppressiveCooldownMs = 200;
 52  
 53      /// <summary>
 54      /// Calculates the burst size for suppressive fire based on weapon and situation.
 55      /// </summary>
 56      /// <param name="weapon">The weapon being fired</param>
 57      /// <param name="availableAmmo">Current ammo count</param>
 58      /// <returns>Number of rounds to fire in suppressive burst</returns>
 59      public static int CalculateSuppressiveBurstSize(Weapon weapon, int availableAmmo)
 60      {
 61          // Base burst size
 62          int burstSize = DefaultSuppressiveBurstSize;
 63  
 64          // LMGs and weapons with high suppression factors get slightly larger bursts
 65          if (weapon.SuppressionFactor >= 1.3f)
 66          {
 67              burstSize = Math.Min(MaxSuppressiveBurstSize, burstSize + 2);
 68          }
 69          else if (weapon.SuppressionFactor >= 1.0f)
 70          {
 71              burstSize = Math.Min(MaxSuppressiveBurstSize, burstSize + 1);
 72          }
 73  
 74          // Clamp to available ammo (this is the hard constraint)
 75          burstSize = Math.Min(burstSize, availableAmmo);
 76  
 77          // Return the clamped burst size (may be less than minimum if ammo is low)
 78          // Caller should check ShouldUseSuppressiveFire which validates minimum ammo
 79          return burstSize;
 80      }
 81  
 82      /// <summary>
 83      /// Calculates suppression severity from a suppressive fire burst.
 84      /// </summary>
 85      /// <param name="weapon">Weapon used for suppressive fire</param>
 86      /// <param name="burstSize">Number of rounds in the burst</param>
 87      /// <param name="distanceMeters">Distance to target</param>
 88      /// <param name="targetMovementState">Target's movement state (optional)</param>
 89      /// <returns>Total suppression severity to apply</returns>
 90      public static float CalculateSuppressiveBurstSeverity(
 91          Weapon weapon,
 92          int burstSize,
 93          float distanceMeters,
 94          MovementState? targetMovementState = null)
 95      {
 96          // Base suppression from burst
 97          float baseSuppression = BaseSuppressiveFireSuppression;
 98  
 99          // Scale by weapon suppression factor
100          baseSuppression *= weapon.SuppressionFactor;
101  
102          // Scale by burst size (diminishing returns)
103          float burstMultiplier = 1.0f + (burstSize - 1) * 0.2f; // Each additional round adds 20%
104          baseSuppression *= burstMultiplier;
105  
106          // Distance factor (closer = more suppressive)
107          float distanceFactor = CalculateDistanceFactor(distanceMeters);
108          baseSuppression *= distanceFactor;
109  
110          // Apply full cover multiplier (reduced effectiveness since not visible)
111          baseSuppression *= FullCoverSuppressionMultiplier;
112  
113          // Apply target movement modifier
114          if (targetMovementState.HasValue)
115          {
116              float movementMultiplier = MovementModel.GetSuppressionBuildupMultiplier(targetMovementState.Value);
117              baseSuppression *= movementMultiplier;
118          }
119  
120          return Math.Clamp(baseSuppression, 0f, SuppressionModel.MaxSuppressionLevel);
121      }
122  
123      /// <summary>
124      /// Calculates the duration of a suppressive fire burst in milliseconds.
125      /// </summary>
126      /// <param name="weapon">Weapon being fired</param>
127      /// <param name="burstSize">Number of rounds in burst</param>
128      /// <returns>Duration in milliseconds</returns>
129      public static long CalculateBurstDurationMs(Weapon weapon, int burstSize)
130      {
131          float timeBetweenShots = weapon.GetTimeBetweenShotsMs();
132          // Duration = time for (burstSize - 1) intervals between shots
133          return (long)((burstSize - 1) * timeBetweenShots);
134      }
135  
136      /// <summary>
137      /// Determines if suppressive fire should be used given the combat situation.
138      /// </summary>
139      /// <param name="attackerAmmo">Attacker's current ammo</param>
140      /// <param name="targetCoverState">Target's cover state</param>
141      /// <param name="targetLastVisibleMs">When the target was last visible (null if never seen)</param>
142      /// <param name="currentTimeMs">Current simulation time</param>
143      /// <returns>True if suppressive fire should be used</returns>
144      public static bool ShouldUseSuppressiveFire(
145          int attackerAmmo,
146          CoverState targetCoverState,
147          long? targetLastVisibleMs,
148          long currentTimeMs)
149      {
150          // Must have ammo
151          if (attackerAmmo < MinSuppressiveBurstSize)
152              return false;
153  
154          // Target must be in full cover
155          if (targetCoverState != CoverState.Full)
156              return false;
157  
158          // Target must have been recently visible (we know they're there)
159          if (!targetLastVisibleMs.HasValue)
160              return false;
161  
162          // Check if target was seen within the memory window
163          long timeSinceVisible = currentTimeMs - targetLastVisibleMs.Value;
164          return timeSinceVisible <= TargetLastSeenWindowMs;
165      }
166  
167      /// <summary>
168      /// Distance factor for suppressive fire (closer = more effective).
169      /// Similar to SuppressionModel but with different curve for sustained fire.
170      /// </summary>
171      private static float CalculateDistanceFactor(float distanceMeters)
172      {
173          // Full suppression up to 15m, then linear falloff
174          if (distanceMeters <= 15f)
175              return 1.0f;
176          if (distanceMeters <= 40f)
177              return 1.0f - (distanceMeters - 15f) / 50f; // 0.5 at 40m
178          return 0.3f; // Minimum floor at long range
179      }
180  }