/ 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 }