/ GUNRPG.Tests / MovementIntegrationTests.cs
MovementIntegrationTests.cs
1 using GUNRPG.Core; 2 using GUNRPG.Core.Combat; 3 using GUNRPG.Core.Events; 4 using GUNRPG.Core.Operators; 5 using GUNRPG.Core.Weapons; 6 using Xunit; 7 8 namespace GUNRPG.Tests; 9 10 public class MovementIntegrationTests 11 { 12 [Fact] 13 public void Movement_AffectsAccuracy() 14 { 15 var random = new Random(42); 16 var shooter = new Operator("Shooter") 17 { 18 Accuracy = 0.7f, 19 AccuracyProficiency = 0.5f, 20 EquippedWeapon = WeaponFactory.CreateM15Mod0(), 21 DistanceToOpponent = 15f 22 }; 23 24 // Stationary baseline 25 shooter.CurrentMovement = MovementState.Stationary; 26 var stationaryResult = HitResolution.ResolveShotWithProficiency( 27 BodyPart.UpperTorso, 28 shooter.Accuracy, 29 shooter.AccuracyProficiency, 30 0f, 0f, 0f, 31 new Random(42), 32 movementState: shooter.CurrentMovement); 33 34 // Sprinting penalty 35 shooter.CurrentMovement = MovementState.Sprinting; 36 var sprintingResult = HitResolution.ResolveShotWithProficiency( 37 BodyPart.UpperTorso, 38 shooter.Accuracy, 39 shooter.AccuracyProficiency, 40 0f, 0f, 0f, 41 new Random(42), 42 movementState: shooter.CurrentMovement); 43 44 // Verify that sprinting affects accuracy (results should differ) 45 // Note: Due to randomness, we can't assert exact values, but we verify the system runs 46 Assert.NotNull(stationaryResult); 47 Assert.NotNull(sprintingResult); 48 } 49 50 [Fact] 51 public void Crouching_ReducesSuppression() 52 { 53 var target = new Operator("Target") 54 { 55 CurrentMovement = MovementState.Crouching 56 }; 57 58 // Apply suppression while crouching 59 float suppressionWithCrouch = SuppressionModel.CalculateSuppressionSeverity( 60 weaponSuppressionFactor: 1.0f, 61 weaponFireRateRPM: 600f, 62 distanceMeters: 15f, 63 angularDeviationDegrees: 0.3f, 64 targetMovementState: MovementState.Crouching); 65 66 // Apply suppression while standing 67 float suppressionStanding = SuppressionModel.CalculateSuppressionSeverity( 68 weaponSuppressionFactor: 1.0f, 69 weaponFireRateRPM: 600f, 70 distanceMeters: 15f, 71 angularDeviationDegrees: 0.3f, 72 targetMovementState: MovementState.Stationary); 73 74 // Crouching should reduce suppression 75 Assert.True(suppressionWithCrouch < suppressionStanding); 76 } 77 78 [Fact] 79 public void Sprinting_IncreasesSuppression() 80 { 81 var target = new Operator("Target") 82 { 83 CurrentMovement = MovementState.Sprinting 84 }; 85 86 float suppressionSprinting = SuppressionModel.CalculateSuppressionSeverity( 87 weaponSuppressionFactor: 1.0f, 88 weaponFireRateRPM: 600f, 89 distanceMeters: 15f, 90 angularDeviationDegrees: 0.3f, 91 targetMovementState: MovementState.Sprinting); 92 93 float suppressionStationary = SuppressionModel.CalculateSuppressionSeverity( 94 weaponSuppressionFactor: 1.0f, 95 weaponFireRateRPM: 600f, 96 distanceMeters: 15f, 97 angularDeviationDegrees: 0.3f, 98 targetMovementState: MovementState.Stationary); 99 100 // Sprinting should increase suppression 101 Assert.True(suppressionSprinting > suppressionStationary); 102 } 103 104 [Fact] 105 public void Crouching_IncreasesSuppressionDecay() 106 { 107 var op = new Operator("Test") 108 { 109 CurrentMovement = MovementState.Crouching 110 }; 111 112 float initialSuppression = 0.5f; 113 long deltaMs = 1000; 114 115 float decayedCrouching = SuppressionModel.ApplyDecay( 116 initialSuppression, 117 deltaMs, 118 isUnderFire: false, 119 movementState: MovementState.Crouching); 120 121 float decayedStanding = SuppressionModel.ApplyDecay( 122 initialSuppression, 123 deltaMs, 124 isUnderFire: false, 125 movementState: MovementState.Stationary); 126 127 // Crouching should decay faster (lower final value) 128 Assert.True(decayedCrouching < decayedStanding); 129 } 130 131 [Fact] 132 public void MovementCancellation_UpdatesStateImmediately() 133 { 134 var op = new Operator("Test"); 135 var eventQueue = new EventQueue(); 136 long currentTime = 1000; 137 138 // Start walking 139 op.StartMovement(MovementState.Walking, 1000, currentTime, eventQueue); 140 Assert.Equal(MovementState.Walking, op.CurrentMovement); 141 Assert.True(op.IsMoving); 142 143 // Cancel after 300ms 144 currentTime = 1300; 145 op.CancelMovement(currentTime, eventQueue); 146 147 // State should be updated immediately 148 Assert.Equal(MovementState.Stationary, op.CurrentMovement); 149 Assert.False(op.IsMoving); 150 151 // Verify cancellation event was emitted with correct remaining duration 152 var events = new List<ISimulationEvent>(); 153 while (eventQueue.Count > 0) 154 { 155 events.Add(eventQueue.DequeueNext()!); 156 } 157 158 var cancelEvent = events.OfType<MovementCancelledEvent>().FirstOrDefault(); 159 Assert.NotNull(cancelEvent); 160 Assert.Equal(700, cancelEvent!.RemainingDurationMs); 161 } 162 163 [Fact] 164 public void Sprint_ThenCover_ThenADS_WorkflowTest() 165 { 166 var op = new Operator("Operator"); 167 var eventQueue = new EventQueue(); 168 long currentTime = 0; 169 170 // 1. Sprint for 2 seconds 171 op.StartMovement(MovementState.Sprinting, 2000, currentTime, eventQueue); 172 Assert.Equal(MovementState.Sprinting, op.CurrentMovement); 173 174 // 2. Complete sprint 175 currentTime = 2000; 176 op.UpdateMovement(currentTime); 177 Assert.Equal(MovementState.Stationary, op.CurrentMovement); 178 179 // 3. Enter cover (should succeed now that stationary) 180 bool enterSuccess = op.EnterCover(CoverState.Full, currentTime, eventQueue); 181 Assert.True(enterSuccess); 182 Assert.Equal(CoverState.Full, op.CurrentCover); 183 184 // 4. Verify operator has better survivability in cover 185 float coverMultiplier = MovementModel.GetCoverHitProbabilityMultiplier(op.CurrentCover); 186 Assert.Equal(0.0f, coverMultiplier); // Full cover blocks hits 187 } 188 189 [Fact] 190 public void MovementEndedEvent_ClearsMovementState() 191 { 192 var op = new Operator("Test"); 193 var eventQueue = new EventQueue(); 194 long currentTime = 1000; 195 196 // Start movement 197 op.StartMovement(MovementState.Walking, 500, currentTime, eventQueue); 198 199 // Peek at events without removing them 200 var endedEvent = eventQueue.PeekNext(); 201 while (endedEvent != null && !(endedEvent is MovementEndedEvent)) 202 { 203 eventQueue.DequeueNext(); 204 endedEvent = eventQueue.PeekNext(); 205 } 206 207 Assert.NotNull(endedEvent); 208 Assert.IsType<MovementEndedEvent>(endedEvent); 209 210 // Execute the ended event 211 endedEvent!.Execute(); 212 213 // Verify state was cleared 214 Assert.Equal(MovementState.Stationary, op.CurrentMovement); 215 Assert.Null(op.MovementEndTimeMs); 216 Assert.False(op.IsMoving); 217 } 218 219 [Fact] 220 public void WeaponSway_AppliedDuringMovement() 221 { 222 // Test that weapon sway is applied correctly during different movement states 223 var random = new Random(42); 224 225 // No sway when stationary 226 float stationarySway = MovementModel.GetWeaponSwayDegrees(MovementState.Stationary); 227 Assert.Equal(0.0f, stationarySway); 228 229 // Sway when sprinting 230 float sprintSway = MovementModel.GetWeaponSwayDegrees(MovementState.Sprinting); 231 Assert.True(sprintSway > 0f); 232 Assert.Equal(0.15f, sprintSway, precision: 2); 233 234 // Minimal sway when crouching 235 float crouchSway = MovementModel.GetWeaponSwayDegrees(MovementState.Crouching); 236 Assert.True(crouchSway < sprintSway); 237 } 238 }