/ GUNRPG.Tests / DirectionalSuppressionTests.cs
DirectionalSuppressionTests.cs
  1  using GUNRPG.Core;
  2  using GUNRPG.Core.Combat;
  3  using GUNRPG.Core.Operators;
  4  using GUNRPG.Core.Weapons;
  5  using Xunit;
  6  
  7  namespace GUNRPG.Tests;
  8  
  9  /// <summary>
 10  /// Tests for directional movement affecting suppression buildup and hit probability.
 11  /// </summary>
 12  public class DirectionalSuppressionTests
 13  {
 14      [Theory]
 15      [InlineData(MovementDirection.Holding, 1.0f)]
 16      [InlineData(MovementDirection.Advancing, 1.2f)]
 17      [InlineData(MovementDirection.Retreating, 0.85f)]
 18      public void GetDirectionalSuppressionMultiplier_ReturnsCorrectValues(MovementDirection direction, float expected)
 19      {
 20          float actual = MovementModel.GetDirectionalSuppressionMultiplier(direction);
 21          Assert.Equal(expected, actual, precision: 2);
 22      }
 23  
 24      [Theory]
 25      [InlineData(MovementDirection.Holding, 1.0f)]
 26      [InlineData(MovementDirection.Advancing, 1.15f)]
 27      [InlineData(MovementDirection.Retreating, 0.9f)]
 28      public void GetDirectionalHitProbabilityMultiplier_ReturnsCorrectValues(MovementDirection direction, float expected)
 29      {
 30          float actual = MovementModel.GetDirectionalHitProbabilityMultiplier(direction);
 31          Assert.Equal(expected, actual, precision: 2);
 32      }
 33  
 34      [Fact]
 35      public void AdvancingDirection_IncreasesSuppression()
 36      {
 37          // Arrange
 38          var weapon = WeaponFactory.CreateM15Mod0();
 39  
 40          // Act
 41          float suppressionAdvancing = SuppressionModel.CalculateSuppressionSeverity(
 42              weapon.SuppressionFactor,
 43              weapon.RoundsPerMinute,
 44              distanceMeters: 15f,
 45              angularDeviationDegrees: 0.3f,
 46              targetMovementState: MovementState.Stationary,
 47              targetDirection: MovementDirection.Advancing);
 48  
 49          float suppressionHolding = SuppressionModel.CalculateSuppressionSeverity(
 50              weapon.SuppressionFactor,
 51              weapon.RoundsPerMinute,
 52              distanceMeters: 15f,
 53              angularDeviationDegrees: 0.3f,
 54              targetMovementState: MovementState.Stationary,
 55              targetDirection: MovementDirection.Holding);
 56  
 57          // Assert
 58          Assert.True(suppressionAdvancing > suppressionHolding,
 59              $"Advancing should increase suppression. Advancing={suppressionAdvancing}, Holding={suppressionHolding}");
 60      }
 61  
 62      [Fact]
 63      public void RetreatingDirection_ReducesSuppression()
 64      {
 65          // Arrange
 66          var weapon = WeaponFactory.CreateM15Mod0();
 67  
 68          // Act
 69          float suppressionRetreating = SuppressionModel.CalculateSuppressionSeverity(
 70              weapon.SuppressionFactor,
 71              weapon.RoundsPerMinute,
 72              distanceMeters: 15f,
 73              angularDeviationDegrees: 0.3f,
 74              targetMovementState: MovementState.Stationary,
 75              targetDirection: MovementDirection.Retreating);
 76  
 77          float suppressionHolding = SuppressionModel.CalculateSuppressionSeverity(
 78              weapon.SuppressionFactor,
 79              weapon.RoundsPerMinute,
 80              distanceMeters: 15f,
 81              angularDeviationDegrees: 0.3f,
 82              targetMovementState: MovementState.Stationary,
 83              targetDirection: MovementDirection.Holding);
 84  
 85          // Assert
 86          Assert.True(suppressionRetreating < suppressionHolding,
 87              $"Retreating should reduce suppression. Retreating={suppressionRetreating}, Holding={suppressionHolding}");
 88      }
 89  
 90      [Fact]
 91      public void DirectionalModifier_StacksWithMovementState()
 92      {
 93          // Arrange
 94          var weapon = WeaponFactory.CreateM15Mod0();
 95  
 96          // Act - Advancing + Sprinting (both increase suppression)
 97          float suppressionAdvancingSprinting = SuppressionModel.CalculateSuppressionSeverity(
 98              weapon.SuppressionFactor,
 99              weapon.RoundsPerMinute,
100              distanceMeters: 15f,
101              angularDeviationDegrees: 0.3f,
102              targetMovementState: MovementState.Sprinting,
103              targetDirection: MovementDirection.Advancing);
104  
105          // Baseline - Holding + Stationary
106          float suppressionBaseline = SuppressionModel.CalculateSuppressionSeverity(
107              weapon.SuppressionFactor,
108              weapon.RoundsPerMinute,
109              distanceMeters: 15f,
110              angularDeviationDegrees: 0.3f,
111              targetMovementState: MovementState.Stationary,
112              targetDirection: MovementDirection.Holding);
113  
114          // Assert - Both modifiers should stack
115          Assert.True(suppressionAdvancingSprinting > suppressionBaseline,
116              $"Advancing + Sprinting should significantly increase suppression. Combined={suppressionAdvancingSprinting}, Baseline={suppressionBaseline}");
117  
118          // The combined effect should be multiplicative
119          float expectedMultiplier = MovementModel.GetSuppressionBuildupMultiplier(MovementState.Sprinting) *
120                                     MovementModel.GetDirectionalSuppressionMultiplier(MovementDirection.Advancing);
121          
122          float actualMultiplier = suppressionAdvancingSprinting / suppressionBaseline;
123          
124          Assert.Equal(expectedMultiplier, actualMultiplier, precision: 1);
125      }
126  
127      [Fact]
128      public void OperatorDefaultDirection_IsHolding()
129      {
130          // Arrange & Act
131          var op = new Operator("Test");
132  
133          // Assert
134          Assert.Equal(MovementDirection.Holding, op.CurrentDirection);
135      }
136  
137      [Fact]
138      public void MovementDirection_DoesNotAffectDistance()
139      {
140          // This is an architectural test - ensure direction doesn't have distance side effects
141          // Arrange
142          var op = new Operator("Test")
143          {
144              DistanceToOpponent = 15f,
145              CurrentDirection = MovementDirection.Holding
146          };
147  
148          float initialDistance = op.DistanceToOpponent;
149  
150          // Act - Change direction
151          op.CurrentDirection = MovementDirection.Advancing;
152  
153          // Assert - Distance should remain unchanged
154          Assert.Equal(initialDistance, op.DistanceToOpponent);
155  
156          // Act - Change direction again
157          op.CurrentDirection = MovementDirection.Retreating;
158  
159          // Assert - Distance still unchanged
160          Assert.Equal(initialDistance, op.DistanceToOpponent);
161      }
162  
163      [Fact]
164      public void DirectionalHitProbability_IntegratedIntoHitResolution()
165      {
166          // This test verifies that directional movement affects hit probability
167          // by comparing overall hit rates with different directions
168          
169          // Arrange
170          var weapon = WeaponFactory.CreateM15Mod0();
171          
172          // Act - Resolve multiple shots and count ANY hit (not just head)
173          int advancingHits = 0;
174          int holdingHits = 0;
175          int retreatingHits = 0;
176          int iterations = 500;
177          
178          for (int i = 0; i < iterations; i++)
179          {
180              // Use lower accuracy to make the directional modifier more visible
181              float accuracy = 0.55f;
182              float proficiency = 0.5f;
183              float recoil = weapon.VerticalRecoil * 0.4f;
184              
185              // Test advancing target (easier to hit) - 15% bonus = 1.15x multiplier
186              var randomAdv = new Random(42 + i);
187              var resultAdvancing = HitResolution.ResolveShotWithProficiency(
188                  BodyPart.UpperTorso, accuracy, proficiency, weapon.VerticalRecoil, recoil, 0.08f,
189                  randomAdv, targetDirection: MovementDirection.Advancing);
190              if (resultAdvancing.HitLocation != BodyPart.Miss) advancingHits++;
191              
192              // Test holding target (baseline) - 1.0x multiplier
193              var randomHold = new Random(42 + i);
194              var resultHolding = HitResolution.ResolveShotWithProficiency(
195                  BodyPart.UpperTorso, accuracy, proficiency, weapon.VerticalRecoil, recoil, 0.08f,
196                  randomHold, targetDirection: MovementDirection.Holding);
197              if (resultHolding.HitLocation != BodyPart.Miss) holdingHits++;
198              
199              // Test retreating target (harder to hit) - 10% penalty = 0.9x multiplier
200              var randomRetr = new Random(42 + i);
201              var resultRetreating = HitResolution.ResolveShotWithProficiency(
202                  BodyPart.UpperTorso, accuracy, proficiency, weapon.VerticalRecoil, recoil, 0.08f,
203                  randomRetr, targetDirection: MovementDirection.Retreating);
204              if (resultRetreating.HitLocation != BodyPart.Miss) retreatingHits++;
205          }
206          
207          float advancingRate = advancingHits / (float)iterations;
208          float holdingRate = holdingHits / (float)iterations;
209          float retreatingRate = retreatingHits / (float)iterations;
210          
211          // Assert - The directional modifier should create visible differences in hit rates
212          // Advancing (1.15x) should have higher hit rate than holding (1.0x)
213          // Holding (1.0x) should have higher hit rate than retreating (0.9x)
214          Assert.True(advancingHits >= holdingHits,
215              $"Advancing targets should be at least as easy to hit as holding. Advancing={advancingHits} ({advancingRate:P}), Holding={holdingHits} ({holdingRate:P})");
216          Assert.True(holdingHits >= retreatingHits,
217              $"Holding targets should be at least as easy to hit as retreating. Holding={holdingHits} ({holdingRate:P}), Retreating={retreatingHits} ({retreatingRate:P})");
218          
219          // At least verify that the integration is working (not all zeros or all same)
220          int totalHits = advancingHits + holdingHits + retreatingHits;
221          Assert.True(totalHits > 0, "At least some shots should hit to validate the test");
222          Assert.True(advancingHits != holdingHits || holdingHits != retreatingHits,
223              $"Directional modifiers should create some variation. Advancing={advancingHits}, Holding={holdingHits}, Retreating={retreatingHits}");
224      }
225  }