/ GUNRPG.Tests / CoverSemanticsTests.cs
CoverSemanticsTests.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  /// Tests for clarified cover semantics:
 13  /// - Partial Cover = Peeking (only upper body exposed)
 14  /// - Full Cover = Complete Concealment (no damage possible)
 15  /// </summary>
 16  public class CoverSemanticsTests
 17  {
 18      private static Operator CreateTestOperator(string name, float accuracy = 1.0f, float accuracyProficiency = 0.8f)
 19      {
 20          var op = new Operator(name)
 21          {
 22              Health = 100,
 23              MaxHealth = 100,
 24              Accuracy = accuracy,
 25              AccuracyProficiency = accuracyProficiency,
 26              CurrentAmmo = 30,
 27              DistanceToOpponent = 10f,
 28              CurrentMovement = MovementState.Stationary,
 29              CurrentCover = CoverState.None,
 30              EquippedWeapon = WeaponFactory.CreateSturmwolf45()
 31          };
 32          return op;
 33      }
 34  
 35      [Fact]
 36      public void PartialCover_BlocksLowerTorsoHits()
 37      {
 38          // Fire many shots to test hit distribution
 39          int lowerTorsoHits = 0;
 40          int upperBodyHits = 0;
 41          int totalShots = 100;
 42  
 43          for (int i = 0; i < totalShots; i++)
 44          {
 45              var shooter2 = CreateTestOperator("Shooter");
 46              var target2 = CreateTestOperator("Target");
 47              target2.CurrentCover = CoverState.Partial;
 48  
 49              var combat2 = new CombatSystemV2(shooter2, target2, seed: 42 + i);
 50              
 51              var intents = new SimultaneousIntents(shooter2.Id) { Primary = PrimaryAction.Fire };
 52              combat2.SubmitIntents(shooter2, intents);
 53              combat2.BeginExecution();
 54              combat2.ExecuteUntilReactionWindow();
 55  
 56              // Check if target was hit and where
 57              float healthLost = 100 - target2.Health;
 58              if (healthLost > 0)
 59              {
 60                  // Hit occurred - check that it wasn't lower torso
 61                  // We can't directly check the hit location from health alone,
 62                  // but we can verify lower torso hits are filtered by checking events
 63                  var damageEvents = combat2.ExecutedEvents.OfType<DamageAppliedEvent>().ToList();
 64                  foreach (var evt in damageEvents)
 65                  {
 66                      if (evt.BodyPart == BodyPart.LowerTorso)
 67                      {
 68                          lowerTorsoHits++;
 69                      }
 70                      else if (evt.BodyPart == BodyPart.UpperTorso || evt.BodyPart == BodyPart.Neck || evt.BodyPart == BodyPart.Head)
 71                      {
 72                          upperBodyHits++;
 73                      }
 74                  }
 75              }
 76          }
 77  
 78          // Assert: No lower torso hits should occur in partial cover
 79          Assert.Equal(0, lowerTorsoHits);
 80          // Assert: Some upper body hits should occur (to verify test is working)
 81          Assert.True(upperBodyHits > 0, "Expected some upper body hits to occur");
 82      }
 83  
 84      [Fact]
 85      public void FullCover_BlocksAllDamage()
 86      {
 87          // Arrange
 88          var shooter = CreateTestOperator("Shooter", accuracy: 1.0f);
 89          var target = CreateTestOperator("Target");
 90          target.CurrentCover = CoverState.Full; // Completely concealed
 91  
 92          var combat = new CombatSystemV2(shooter, target, seed: 42);
 93  
 94          // Act: Shoot multiple times
 95          int shotsToFire = 10;
 96          for (int i = 0; i < shotsToFire; i++)
 97          {
 98              var intents = new SimultaneousIntents(shooter.Id) { Primary = PrimaryAction.Fire };
 99              combat.SubmitIntents(shooter, intents);
100              combat.BeginExecution();
101              combat.ExecuteUntilReactionWindow();
102              
103              // Reset for next shot
104              shooter.CurrentAmmo = 30;
105          }
106  
107          // Assert: Target should have taken no damage
108          Assert.Equal(100f, target.Health);
109          
110          // Assert: No damage events should have been generated
111          var damageEvents = combat.ExecutedEvents.OfType<DamageAppliedEvent>().ToList();
112          Assert.Empty(damageEvents);
113      }
114  
115      [Fact]
116      public void FullCover_StillAppliesSuppression()
117      {
118          // Arrange
119          var shooter = CreateTestOperator("Shooter", accuracy: 0.5f); // Lower accuracy for more misses
120          var target = CreateTestOperator("Target");
121          target.CurrentCover = CoverState.Full; // Completely concealed
122  
123          var combat = new CombatSystemV2(shooter, target, seed: 123);
124  
125          // Act: Fire shots that should miss but still suppress
126          var intents = new SimultaneousIntents(shooter.Id) { Primary = PrimaryAction.Fire };
127          combat.SubmitIntents(shooter, intents);
128          combat.BeginExecution();
129          combat.ExecuteUntilReactionWindow();
130  
131          // Assert: Target should have some suppression level (shots are passing by)
132          // Note: Suppression may or may not apply depending on shot accuracy and proximity
133          // The key is that the system allows suppression events for full cover
134          var suppressionEvents = combat.ExecutedEvents
135              .Where(e => e is SuppressionStartedEvent || e is SuppressionUpdatedEvent)
136              .ToList();
137          
138          // We're verifying that suppression events CAN be generated for full cover targets
139          // (deterministically, given the fixed seed and parameters)
140          Assert.NotEmpty(suppressionEvents);
141          
142          // And full cover should still block all damage
143          Assert.InRange(target.Health, 99.9f, 100.1f);
144      }
145  
146      [Fact]
147      public void FullCover_BlocksShooting()
148      {
149          // Arrange
150          var shooter = CreateTestOperator("Shooter");
151          shooter.CurrentCover = CoverState.Full; // Shooter is in full cover
152          var target = CreateTestOperator("Target");
153  
154          var combat = new CombatSystemV2(shooter, target, seed: 42);
155  
156          // Act: Try to shoot from full cover
157          var intents = new SimultaneousIntents(shooter.Id) { Primary = PrimaryAction.Fire };
158          var result = combat.SubmitIntents(shooter, intents);
159          
160          // Submit should succeed (intent is valid), but execution should block it
161          Assert.True(result.success);
162          
163          combat.BeginExecution();
164          combat.ExecuteUntilReactionWindow();
165  
166          // Assert: No shots should have been fired
167          var shotEvents = combat.ExecutedEvents.OfType<ShotFiredEvent>().ToList();
168          Assert.Empty(shotEvents);
169          
170          // Assert: Ammo should not have been consumed
171          Assert.Equal(30, shooter.CurrentAmmo);
172      }
173  
174      [Fact]
175      public void FullCover_BlocksAdvancing()
176      {
177          // Arrange
178          var shooter = CreateTestOperator("Shooter");
179          shooter.CurrentCover = CoverState.Full; // In full cover
180          var target = CreateTestOperator("Target");
181  
182          var combat = new CombatSystemV2(shooter, target, seed: 42);
183  
184          float initialDistance = shooter.DistanceToOpponent;
185  
186          // Act: Try to advance from full cover
187          var intents = new SimultaneousIntents(shooter.Id) { Movement = MovementAction.WalkToward };
188          var result = combat.SubmitIntents(shooter, intents);
189          
190          Assert.True(result.success);
191          
192          combat.BeginExecution();
193          combat.ExecuteUntilReactionWindow();
194  
195          // Assert: Distance should not have changed (movement blocked)
196          Assert.Equal(initialDistance, shooter.DistanceToOpponent);
197      }
198  
199      [Fact]
200      public void FullCover_AllowsRetreating()
201      {
202          // Arrange
203          var shooter = CreateTestOperator("Shooter");
204          shooter.CurrentCover = CoverState.Full; // In full cover
205          var target = CreateTestOperator("Target");
206  
207          var combat = new CombatSystemV2(shooter, target, seed: 42);
208  
209          // Act: Try to retreat from full cover (should be allowed)
210          var intents = new SimultaneousIntents(shooter.Id) { Movement = MovementAction.WalkAway };
211          var result = combat.SubmitIntents(shooter, intents);
212          
213          // Assert: retreat intent is accepted from full cover
214          Assert.True(result.success);
215      }
216  
217      [Fact]
218      public void PartialCover_AllowsShooting()
219      {
220          // Arrange
221          var shooter = CreateTestOperator("Shooter");
222          shooter.CurrentCover = CoverState.Partial; // Peeking
223          var target = CreateTestOperator("Target");
224  
225          var combat = new CombatSystemV2(shooter, target, seed: 42);
226  
227          // Act: Try to shoot from partial cover (should be allowed)
228          var intents = new SimultaneousIntents(shooter.Id) { Primary = PrimaryAction.Fire };
229          var result = combat.SubmitIntents(shooter, intents);
230          
231          Assert.True(result.success);
232          
233          combat.BeginExecution();
234          combat.ExecuteUntilReactionWindow();
235  
236          // Assert: Shot should have been fired
237          var shotEvents = combat.ExecutedEvents.OfType<ShotFiredEvent>().ToList();
238          Assert.NotEmpty(shotEvents);
239          
240          // Assert: Ammo should have been consumed
241          Assert.Equal(29, shooter.CurrentAmmo);
242      }
243  
244      [Fact]
245      public void PartialCover_AllowsAdvancing()
246      {
247          // Arrange
248          var shooter = CreateTestOperator("Shooter");
249          shooter.CurrentCover = CoverState.Partial; // Peeking
250          var target = CreateTestOperator("Target");
251  
252          var combat = new CombatSystemV2(shooter, target, seed: 42);
253  
254          // Act: Try to advance from partial cover (should be allowed)
255          var intents = new SimultaneousIntents(shooter.Id) { Movement = MovementAction.WalkToward };
256          var result = combat.SubmitIntents(shooter, intents);
257          
258          // Assert: Intent should be accepted
259          Assert.True(result.success);
260      }
261  
262      [Fact]
263      public void CanShoot_ReturnsCorrectValues()
264      {
265          var op = CreateTestOperator("Test");
266  
267          // None: Can shoot
268          op.CurrentCover = CoverState.None;
269          Assert.True(op.CanShoot());
270  
271          // Partial: Can shoot (peeking)
272          op.CurrentCover = CoverState.Partial;
273          Assert.True(op.CanShoot());
274  
275          // Full: Cannot shoot (concealed)
276          op.CurrentCover = CoverState.Full;
277          Assert.False(op.CanShoot());
278      }
279  
280      [Fact]
281      public void CanAdvance_ReturnsCorrectValues()
282      {
283          var op = CreateTestOperator("Test");
284  
285          // None: Can advance
286          op.CurrentCover = CoverState.None;
287          Assert.True(op.CanAdvance());
288  
289          // Partial: Can advance
290          op.CurrentCover = CoverState.Partial;
291          Assert.True(op.CanAdvance());
292  
293          // Full: Cannot advance (must exit cover first)
294          op.CurrentCover = CoverState.Full;
295          Assert.False(op.CanAdvance());
296      }
297  
298      [Fact]
299      public void NoCover_CanBeHit()
300      {
301          // This test verifies that without cover, hits can occur
302          // The implicit verification is that the PartialCover test shows lower torso IS filtered
303          // Therefore, the filtering logic is working (it wouldn't make sense to filter if hits weren't possible)
304          
305          int hitsLanded = 0;
306          
307          for (int i = 0; i < 10; i++)
308          {
309              var shooter2 = CreateTestOperator("Shooter", accuracy: 0.9f);
310              var target2 = CreateTestOperator("Target");
311              target2.CurrentCover = CoverState.None;
312  
313              var combat = new CombatSystemV2(shooter2, target2, seed: 100 + i);
314              
315              var intents = new SimultaneousIntents(shooter2.Id) { Primary = PrimaryAction.Fire };
316              combat.SubmitIntents(shooter2, intents);
317              combat.BeginExecution();
318              combat.ExecuteUntilReactionWindow();
319  
320              if (target2.Health < target2.MaxHealth)
321                  hitsLanded++;
322          }
323  
324          // Assert: With high accuracy and no cover, some hits should land
325          Assert.True(hitsLanded > 0, "Expected some hits to land with no cover and high accuracy");
326      }
327  }