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 }