/ GUNRPG.Core / AI / SimpleAIV2.cs
SimpleAIV2.cs
  1  using GUNRPG.Core.Combat;
  2  using GUNRPG.Core.Intents;
  3  using GUNRPG.Core.Operators;
  4  
  5  namespace GUNRPG.Core.AI;
  6  
  7  /// <summary>
  8  /// AI for enemy decision making with simultaneous intent support.
  9  /// Makes tactical decisions based on operator state and opponent state.
 10  /// </summary>
 11  public class SimpleAIV2
 12  {
 13      private readonly Random _random;
 14  
 15      // AI decision constants
 16      private const int LOW_AMMO_THRESHOLD = 5;
 17      private const int LOW_HEALTH_THRESHOLD = 30;
 18      private const int REGEN_WAIT_HEALTH_THRESHOLD = 40;
 19  
 20      public SimpleAIV2(int? seed = null)
 21      {
 22          _random = seed.HasValue ? new Random(seed.Value) : new Random();
 23      }
 24  
 25      /// <summary>
 26      /// Decides the next simultaneous intents for the AI operator.
 27      /// </summary>
 28      public SimultaneousIntents DecideIntents(Operator self, Operator opponent, CombatSystemV2 combat)
 29      {
 30          var intents = new SimultaneousIntents(self.Id);
 31  
 32          // Decide primary action
 33          intents.Primary = DecidePrimaryAction(self, opponent, combat);
 34  
 35          // Decide movement
 36          intents.Movement = DecideMovement(self, opponent, combat);
 37  
 38          // Decide stance
 39          intents.Stance = DecideStance(self, opponent, combat);
 40  
 41          return intents;
 42      }
 43  
 44      private PrimaryAction DecidePrimaryAction(Operator self, Operator opponent, CombatSystemV2 combat)
 45      {
 46          // Priority 1: Reload if out of ammo
 47          if (self.CurrentAmmo == 0 && self.WeaponState == WeaponState.Ready)
 48          {
 49              return PrimaryAction.Reload;
 50          }
 51  
 52          // Priority 2: Reload if low on ammo and opponent is far or reloading
 53          if (self.CurrentAmmo < LOW_AMMO_THRESHOLD &&
 54              self.WeaponState == WeaponState.Ready &&
 55              (self.DistanceToOpponent > 15 || opponent.WeaponState == WeaponState.Reloading))
 56          {
 57              return PrimaryAction.Reload;
 58          }
 59  
 60          // Priority 3: Fire if in range and have ammo
 61          if (self.CurrentAmmo > 0 &&
 62              self.WeaponState == WeaponState.Ready &&
 63              self.DistanceToOpponent < 30)
 64          {
 65              // Fire if opponent is visible and in reasonable range
 66              return PrimaryAction.Fire;
 67          }
 68  
 69          return PrimaryAction.None;
 70      }
 71  
 72      private MovementAction DecideMovement(Operator self, Operator opponent, CombatSystemV2 combat)
 73      {
 74          float optimalRange = 15f;
 75          float currentDistance = self.DistanceToOpponent;
 76  
 77          // Priority 1: Survive - if low health and not regenerating, create distance
 78          if (self.Health < LOW_HEALTH_THRESHOLD && !self.CanRegenerateHealth(combat.CurrentTimeMs))
 79          {
 80              return self.Stamina > 50 ? MovementAction.SprintAway : MovementAction.WalkAway;
 81          }
 82  
 83          // Priority 2: If health is low and can regenerate, stop and wait
 84          if (self.Health < REGEN_WAIT_HEALTH_THRESHOLD && self.CanRegenerateHealth(combat.CurrentTimeMs))
 85          {
 86              return MovementAction.Stand;
 87          }
 88  
 89          // Priority 3: Adjust position based on optimal range
 90          if (currentDistance > optimalRange + 5)
 91          {
 92              // Too far, move closer
 93              return (self.Stamina > 30 && opponent.Health > 50)
 94                  ? MovementAction.SprintToward
 95                  : MovementAction.WalkToward;
 96          }
 97          else if (currentDistance < optimalRange - 5)
 98          {
 99              // Too close, back up
100              return MovementAction.WalkAway;
101          }
102  
103          // At optimal range, no movement needed
104          return MovementAction.Stand;
105      }
106  
107      private StanceAction DecideStance(Operator self, Operator opponent, CombatSystemV2 combat)
108      {
109          // If we're firing and not in ADS, enter ADS for better accuracy
110          if (self.CurrentAmmo > 0 && self.WeaponState == WeaponState.Ready)
111          {
112              // Check current ADS progress
113              float adsProgress = self.GetADSProgress(combat.CurrentTimeMs);
114              
115              // If not in ADS or transitioning, and not actively firing yet, start ADS
116              if (adsProgress < 0.5f && !self.IsActivelyFiring)
117              {
118                  return StanceAction.EnterADS;
119              }
120          }
121  
122          // Exit ADS if moving fast or low health (need mobility)
123          if (self.MovementState == MovementState.Sprinting || self.Health < 30)
124          {
125              float adsProgress = self.GetADSProgress(combat.CurrentTimeMs);
126              if (adsProgress > 0.1f)
127              {
128                  return StanceAction.ExitADS;
129              }
130          }
131  
132          return StanceAction.None;
133      }
134  
135      /// <summary>
136      /// Decides reaction at a reaction window.
137      /// Returns true if the AI wants to change its intent.
138      /// </summary>
139      public bool ShouldReact(Operator self, Operator opponent, CombatSystemV2 combat, out SimultaneousIntents? newIntents)
140      {
141          newIntents = null;
142  
143          // React if took significant damage recently
144          if (self.LastDamageTimeMs.HasValue && 
145              combat.CurrentTimeMs - self.LastDamageTimeMs.Value < 200) // Very recent damage
146          {
147              // Reassess situation
148              newIntents = DecideIntents(self, opponent, combat);
149              return true;
150          }
151  
152          // React if ammo empty
153          if (self.CurrentAmmo == 0 && self.WeaponState == WeaponState.Ready)
154          {
155              var intents = new SimultaneousIntents(self.Id);
156              intents.Primary = PrimaryAction.Reload;
157              newIntents = intents;
158              return true;
159          }
160  
161          // React if opponent finished reloading (opportunity to push)
162          if (opponent.WeaponState == WeaponState.Ready &&
163              self.DistanceToOpponent > 10 &&
164              self.Stamina > 50 &&
165              _random.NextDouble() < 0.3)
166          {
167              var intents = new SimultaneousIntents(self.Id);
168              intents.Movement = MovementAction.SprintToward;
169              intents.Primary = PrimaryAction.Fire;
170              newIntents = intents;
171              return true;
172          }
173  
174          // Otherwise, continue current intent
175          return false;
176      }
177  }