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