/ GUNRPG.Tests / AwarenessCoverIntegrationTests.cs
AwarenessCoverIntegrationTests.cs
  1  using GUNRPG.Core;
  2  using GUNRPG.Core.Combat;
  3  using GUNRPG.Core.Events;
  4  using GUNRPG.Core.Intents;
  5  using GUNRPG.Core.Operators;
  6  using GUNRPG.Core.Weapons;
  7  using Xunit;
  8  
  9  namespace GUNRPG.Tests;
 10  
 11  /// <summary>
 12  /// Integration tests for awareness, cover transitions, and suppressive fire mechanics.
 13  /// Tests the complete flow of these systems working together in combat.
 14  /// </summary>
 15  public class AwarenessCoverIntegrationTests
 16  {
 17      private static Operator CreateTestOperator(string name, float accuracy = 0.8f, float accuracyProficiency = 0.7f)
 18      {
 19          var op = new Operator(name)
 20          {
 21              Health = 100,
 22              MaxHealth = 100,
 23              Accuracy = accuracy,
 24              AccuracyProficiency = accuracyProficiency,
 25              CurrentAmmo = 30,
 26              DistanceToOpponent = 15f,
 27              CurrentMovement = MovementState.Stationary,
 28              CurrentCover = CoverState.None,
 29              EquippedWeapon = WeaponFactory.CreateSturmwolf45()
 30          };
 31          return op;
 32      }
 33  
 34      #region Cover Transition Integration Tests
 35  
 36      [Fact]
 37      public void CoverTransition_CreatesExposureWindow()
 38      {
 39          // Arrange
 40          var player = CreateTestOperator("Player");
 41          var enemy = CreateTestOperator("Enemy");
 42  
 43          var combat = new CombatSystemV2(player, enemy, seed: 42);
 44  
 45          // Act: Enter partial cover (should have transition delay)
 46          var intents = new SimultaneousIntents(player.Id) { Cover = CoverAction.EnterPartial };
 47          combat.SubmitIntents(player, intents);
 48          combat.BeginExecution();
 49          combat.ExecuteUntilReactionWindow();
 50  
 51          // Assert: Should have cover transition events
 52          var transitionStartEvents = combat.ExecutedEvents
 53              .OfType<CoverTransitionStartedEvent>()
 54              .ToList();
 55          var transitionCompleteEvents = combat.ExecutedEvents
 56              .OfType<CoverTransitionCompletedEvent>()
 57              .ToList();
 58  
 59          Assert.NotEmpty(transitionStartEvents);
 60          Assert.NotEmpty(transitionCompleteEvents);
 61  
 62          // Verify transition state was correctly tracked
 63          var startEvent = transitionStartEvents.First();
 64          var completeEvent = transitionCompleteEvents.First();
 65  
 66          Assert.Equal(CoverState.None, startEvent.FromCover);
 67          Assert.Equal(CoverState.Partial, startEvent.ToCover);
 68          Assert.Equal(CoverState.Partial, completeEvent.ToCover);
 69      }
 70  
 71      [Fact]
 72      public void CoverTransition_DuringTransition_TreatedAsPartialCover()
 73      {
 74          // Arrange
 75          var op = CreateTestOperator("Test");
 76          op.CurrentCover = CoverState.None;
 77  
 78          // Simulate starting a transition
 79          op.IsCoverTransitioning = true;
 80          op.CoverTransitionFromState = CoverState.None;
 81          op.CoverTransitionToState = CoverState.Full;
 82          op.CoverTransitionStartMs = 0;
 83          op.CoverTransitionEndMs = 200;
 84  
 85          // Act & Assert: Mid-transition should be treated as partial cover
 86          var effectiveCover = op.GetEffectiveCoverState(currentTimeMs: 100);
 87          Assert.Equal(CoverState.Partial, effectiveCover);
 88  
 89          // After transition end time but before completion event clears the flag,
 90          // we remain consistent with CurrentCover (None in this case)
 91          effectiveCover = op.GetEffectiveCoverState(currentTimeMs: 250);
 92          Assert.Equal(CoverState.None, effectiveCover);
 93  
 94          // Simulate completion event clearing the transition flag and updating cover
 95          op.IsCoverTransitioning = false;
 96          op.CurrentCover = CoverState.Full;
 97          
 98          // Now should return the new cover state
 99          effectiveCover = op.GetEffectiveCoverState(currentTimeMs: 300);
100          Assert.Equal(CoverState.Full, effectiveCover);
101      }
102  
103      #endregion
104  
105      #region Suppressive Fire Integration Tests
106  
107      [Fact]
108      public void SuppressiveFire_AgainstFullCover_DoesNotMagDump()
109      {
110          // Arrange
111          var player = CreateTestOperator("Player");
112          var enemy = CreateTestOperator("Enemy");
113          enemy.CurrentCover = CoverState.Full;
114  
115          // Set up player to have seen enemy recently
116          player.LastTargetVisibleMs = 0;
117  
118          var combat = new CombatSystemV2(player, enemy, seed: 42);
119  
120          // Act: Fire at enemy in full cover
121          var intents = new SimultaneousIntents(player.Id) { Primary = PrimaryAction.Fire };
122          combat.SubmitIntents(player, intents);
123          combat.BeginExecution();
124          combat.ExecuteUntilReactionWindow();
125  
126          // Assert: Should have used suppressive fire (limited ammo consumption)
127          int expectedMaxConsumption = SuppressiveFireModel.MaxSuppressiveBurstSize;
128          int ammoConsumed = 30 - player.CurrentAmmo;
129  
130          Assert.True(ammoConsumed <= expectedMaxConsumption,
131              $"Suppressive fire should not mag-dump. Expected max {expectedMaxConsumption} rounds, consumed {ammoConsumed}");
132  
133          // Should have suppressive fire events
134          var suppressiveStartEvents = combat.ExecutedEvents
135              .OfType<SuppressiveFireStartedEvent>()
136              .ToList();
137          Assert.NotEmpty(suppressiveStartEvents);
138      }
139  
140      [Fact]
141      public void SuppressiveFire_AppliesSuppression_NoDirectDamage()
142      {
143          // Arrange
144          var player = CreateTestOperator("Player");
145          var enemy = CreateTestOperator("Enemy");
146          enemy.CurrentCover = CoverState.Full;
147          player.LastTargetVisibleMs = 0;
148  
149          var combat = new CombatSystemV2(player, enemy, seed: 42);
150  
151          // Act
152          var intents = new SimultaneousIntents(player.Id) { Primary = PrimaryAction.Fire };
153          combat.SubmitIntents(player, intents);
154          combat.BeginExecution();
155          combat.ExecuteUntilReactionWindow();
156  
157          // Assert: Enemy should have suppression but no damage
158          Assert.Equal(100f, enemy.Health); // No damage
159          
160          // Should have suppression (from suppressive fire completion event)
161          var suppressiveCompletedEvents = combat.ExecutedEvents
162              .OfType<SuppressiveFireCompletedEvent>()
163              .ToList();
164          Assert.NotEmpty(suppressiveCompletedEvents);
165      }
166  
167      [Fact]
168      public void SuppressiveFire_EndsRoundEarly()
169      {
170          // Arrange
171          var player = CreateTestOperator("Player");
172          var enemy = CreateTestOperator("Enemy");
173          enemy.CurrentCover = CoverState.Full;
174          player.LastTargetVisibleMs = 0;
175  
176          var combat = new CombatSystemV2(player, enemy, seed: 42);
177  
178          // Act
179          var intents = new SimultaneousIntents(player.Id) { Primary = PrimaryAction.Fire };
180          combat.SubmitIntents(player, intents);
181          combat.BeginExecution();
182          bool roundEnded = combat.ExecuteUntilReactionWindow();
183  
184          // Assert: Round should have ended
185          Assert.True(roundEnded, "Suppressive fire should end the round");
186  
187          // Player should no longer be actively firing
188          Assert.False(player.IsActivelyFiring, "Player should stop firing after suppressive burst");
189      }
190  
191      #endregion
192  
193      #region Awareness / Visibility Tests
194  
195      [Fact]
196      public void FullCover_BlocksVisibility()
197      {
198          // Arrange
199          var op = CreateTestOperator("Test");
200          op.CurrentCover = CoverState.Full;
201  
202          // Act & Assert
203          bool isVisible = op.IsVisibleToOpponents(currentTimeMs: 100);
204          Assert.False(isVisible);
205      }
206  
207      [Fact]
208      public void PartialCover_AllowsVisibility()
209      {
210          // Arrange
211          var op = CreateTestOperator("Test");
212          op.CurrentCover = CoverState.Partial;
213  
214          // Act & Assert
215          bool isVisible = op.IsVisibleToOpponents(currentTimeMs: 100);
216          Assert.True(isVisible);
217      }
218  
219      [Fact]
220      public void NoCover_FullyVisible()
221      {
222          // Arrange
223          var op = CreateTestOperator("Test");
224          op.CurrentCover = CoverState.None;
225  
226          // Act & Assert
227          bool isVisible = op.IsVisibleToOpponents(currentTimeMs: 100);
228          Assert.True(isVisible);
229      }
230  
231      #endregion
232  
233      #region Recognition Delay Tests
234  
235      [Fact]
236      public void RecognitionDelay_HighProficiency_FastRecognition()
237      {
238          float delay = AwarenessModel.CalculateRecognitionDelayMs(
239              observerAccuracyProficiency: 0.9f,
240              observerSuppressionLevel: 0f);
241  
242          // High proficiency should result in fast recognition (close to minimum)
243          Assert.True(delay < AwarenessModel.BaseRecognitionDelayMs,
244              $"High proficiency recognition ({delay}ms) should be faster than base ({AwarenessModel.BaseRecognitionDelayMs}ms)");
245      }
246  
247      [Fact]
248      public void RecognitionDelay_Suppressed_SlowRecognition()
249      {
250          float unsuppressedDelay = AwarenessModel.CalculateRecognitionDelayMs(
251              observerAccuracyProficiency: 0.5f,
252              observerSuppressionLevel: 0f);
253  
254          float suppressedDelay = AwarenessModel.CalculateRecognitionDelayMs(
255              observerAccuracyProficiency: 0.5f,
256              observerSuppressionLevel: 0.8f);
257  
258          Assert.True(suppressedDelay > unsuppressedDelay,
259              $"Suppressed recognition ({suppressedDelay}ms) should be slower than unsuppressed ({unsuppressedDelay}ms)");
260      }
261  
262      [Fact]
263      public void RecognitionProgress_AffectsAccuracy()
264      {
265          // At start of recognition (0 progress)
266          float earlyAccuracy = AwarenessModel.GetRecognitionAccuracyMultiplier(0f);
267          // After recognition complete (full progress)
268          float fullAccuracy = AwarenessModel.GetRecognitionAccuracyMultiplier(1f);
269  
270          Assert.True(earlyAccuracy < fullAccuracy,
271              $"Early recognition accuracy ({earlyAccuracy}) should be worse than full ({fullAccuracy})");
272          Assert.True(earlyAccuracy < 0.5f, "Early recognition should have severe accuracy penalty");
273          Assert.Equal(1.0f, fullAccuracy);
274      }
275  
276      #endregion
277  
278      #region AI Behavior Tests
279  
280      [Fact]
281      public void AI_DoesNotMagDump_AgainstFullCover()
282      {
283          // This is tested implicitly by the suppressive fire tests,
284          // but we verify explicitly that sustained firing doesn't occur
285  
286          var player = CreateTestOperator("Player");
287          var enemy = CreateTestOperator("Enemy");
288          player.CurrentCover = CoverState.Full; // Player is concealed
289          enemy.LastTargetVisibleMs = 0; // Enemy saw player before
290  
291          var combat = new CombatSystemV2(player, enemy, seed: 42);
292  
293          // Enemy fires at concealed player
294          var intents = new SimultaneousIntents(enemy.Id) { Primary = PrimaryAction.Fire };
295          combat.SubmitIntents(enemy, intents);
296          combat.BeginExecution();
297          combat.ExecuteUntilReactionWindow();
298  
299          // Enemy should use controlled suppressive fire
300          int maxExpectedConsumption = SuppressiveFireModel.MaxSuppressiveBurstSize;
301          int actualConsumption = 30 - enemy.CurrentAmmo;
302  
303          Assert.True(actualConsumption <= maxExpectedConsumption,
304              $"AI should not mag-dump. Max expected: {maxExpectedConsumption}, Actual: {actualConsumption}");
305      }
306  
307      [Fact]
308      public void FullCover_StillReceivesSuppression()
309      {
310          // Verify that full cover blocks damage but not suppression
311          var player = CreateTestOperator("Player");
312          var enemy = CreateTestOperator("Enemy");
313          player.CurrentCover = CoverState.Full;
314          enemy.LastTargetVisibleMs = 0;
315  
316          var combat = new CombatSystemV2(player, enemy, seed: 123);
317  
318          var intents = new SimultaneousIntents(enemy.Id) { Primary = PrimaryAction.Fire };
319          combat.SubmitIntents(enemy, intents);
320          combat.BeginExecution();
321          combat.ExecuteUntilReactionWindow();
322  
323          // Health should be full (no damage through full cover)
324          Assert.Equal(100f, player.Health);
325  
326          // Should have suppressive fire completed events (suppression applied)
327          var suppressiveCompletedEvents = combat.ExecutedEvents
328              .OfType<SuppressiveFireCompletedEvent>()
329              .ToList();
330  
331          if (suppressiveCompletedEvents.Count > 0)
332          {
333              // Suppression was applied
334              Assert.True(suppressiveCompletedEvents[0].SuppressionApplied > 0,
335                  "Suppressive fire should apply suppression even through full cover");
336          }
337      }
338  
339      #endregion
340  }