diff --git a/tests/AcDream.Core.Tests/Input/DispatcherToMovementIntegrationTests.cs b/tests/AcDream.Core.Tests/Input/DispatcherToMovementIntegrationTests.cs index dcb2c1f..0e12417 100644 --- a/tests/AcDream.Core.Tests/Input/DispatcherToMovementIntegrationTests.cs +++ b/tests/AcDream.Core.Tests/Input/DispatcherToMovementIntegrationTests.cs @@ -100,7 +100,14 @@ public class DispatcherToMovementIntegrationTests Assert.True(input.Forward); Assert.False(input.Run); - var result = controller.Update(1.0f, input); + // L.5 physics-tick gate (235de33, 2026-04-30): Update() integrates + // only one MaxQuantum (~0.1s) physics step per call, matching + // retail's 30Hz physics gate. Drive the controller one MaxQuantum + // at a time for ~1s to accumulate real forward motion. + MovementResult result = default; + int ticks = (int)MathF.Ceiling(1.0f / PhysicsBody.MaxQuantum) + 1; // ~11 ticks + for (int i = 0; i < ticks; i++) + result = controller.Update(PhysicsBody.MaxQuantum, input); Assert.True(result.Position.X > 96f + 2f, $"X={result.Position.X} should have moved forward"); } diff --git a/tests/AcDream.Core.Tests/Input/PlayerMovementControllerTests.cs b/tests/AcDream.Core.Tests/Input/PlayerMovementControllerTests.cs index 8a09b79..add3dae 100644 --- a/tests/AcDream.Core.Tests/Input/PlayerMovementControllerTests.cs +++ b/tests/AcDream.Core.Tests/Input/PlayerMovementControllerTests.cs @@ -42,10 +42,19 @@ public class PlayerMovementControllerTests controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001); controller.Yaw = 0f; // facing +X + // L.5 physics-tick gate (235de33, 2026-04-30): Update() integrates + // only one MinQuantum (~0.033s) per MaxQuantum (~0.1s) tick, matching + // retail's 30Hz physics. A single Update(1.0f) only advances one + // MaxQuantum step (~0.312m at walk speed 3.12 m/s). Drive the + // controller one MaxQuantum at a time for ~1s to accumulate real + // forward motion (8 × 0.1s = 0.8s × 3.12 m/s ≈ 2.5m). var input = new MovementInput { Forward = true }; - var result = controller.Update(1.0f, input); // 1 second + MovementResult result = default; + int ticks = (int)MathF.Ceiling(1.0f / PhysicsBody.MaxQuantum) + 1; // ~11 ticks + for (int i = 0; i < ticks; i++) + result = controller.Update(PhysicsBody.MaxQuantum, input); - // Should have moved ~4 units in +X (walk speed). + // Should have moved >2 units in +X (walk speed over ~1s). Assert.True(result.Position.X > 96f + 2f, $"X={result.Position.X} should have moved forward"); } diff --git a/tests/AcDream.Core.Tests/Physics/BSPStepUpTests.cs b/tests/AcDream.Core.Tests/Physics/BSPStepUpTests.cs index 47bdfb8..93ffbbf 100644 --- a/tests/AcDream.Core.Tests/Physics/BSPStepUpTests.cs +++ b/tests/AcDream.Core.Tests/Physics/BSPStepUpTests.cs @@ -396,14 +396,18 @@ public class BSPStepUpTests /// /// Airborne mover descending toward a steep slope (normal.Z < FloorZ): - /// Path 6 should still set the Collide flag (it fires for any polygon hit, - /// walkable or not). + /// Path 6 returns and does NOT set + /// the Collide flag — the steep-normal slide-tangent branch (L.4, + /// commit b1af56e, 2026-04-30) intercepts the hit before SetCollide is + /// called and projects the move along the steep face instead, keeping the + /// body airborne with the falling animation. /// - /// Retail: set_collide fires unconditionally when sphere_intersects_poly - /// hits; the walkable check happens later in the Collide-flag handler. + /// This is a documented intentional deviation from retail (retail calls + /// set_collide unconditionally; our interim port uses slide-tangent while + /// the retail step_up_slide / cliff_slide chain port is completed). /// [Fact] - public void C3_Path6_AirborneMoverHitsSteepSlope_SetsCollide() + public void C3_Path6_AirborneMoverHitsSteepSlope_ReturnsSlid() { var (root, resolved) = BSPStepUpFixtures.SlopedUnwalkable(); @@ -423,11 +427,13 @@ public class BSPStepUpTests root, resolved, t, localSphere, null, currPos, Vector3.UnitZ, 1.0f); - // After L.2.2: Collide flag set, Adjusted returned. - // Currently: Slid (wall-slide). - Assert.Equal(TransitionState.Adjusted, result); - Assert.True(t.SpherePath.Collide, - "Expected Collide flag set when airborne sphere hits slope (L.2.2)"); + // L.4 slide-tangent (b1af56e, 2026-04-30): steep polygon hit by + // airborne sphere returns Slid (not Adjusted) and does NOT set + // the Collide flag — the into-wall displacement is removed and + // CollisionNormal/SlidingNormal are set instead. + Assert.Equal(TransitionState.Slid, result); + Assert.False(t.SpherePath.Collide, + "Collide must NOT be set when the L.4 steep-slope slide-tangent fires"); } // ========================================================================= diff --git a/tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs b/tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs index 0242c2a..2920d65 100644 --- a/tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs +++ b/tests/AcDream.Core.Tests/Physics/PositionManagerTests.cs @@ -120,11 +120,18 @@ public sealed class PositionManagerTests } // ========================================================================= - // Test 5: both sources active — combined delta + // Test 5: both sources active — correction REPLACES root motion + // + // retail-faithful semantics (842dfcd, L.3.2, 2026-05-03): + // when InterpolationManager.AdjustOffset returns a non-zero correction, + // ComputeOffset returns the correction alone — it does NOT add root + // motion on top. Mirrors retail's PositionManager::adjust_offset + // (acclient @ 0x00555190) which calls Frame::operator= to OVERWRITE + // the rootOffset frame when catch-up engages. // ========================================================================= [Fact] - public void ComputeOffset_BothActive_Combined() + public void ComputeOffset_BothActive_CorrectionReplacesRootMotion() { var pm = Make(); var interp = new InterpolationManager(); @@ -132,9 +139,9 @@ public sealed class PositionManagerTests // Enqueue target 1m ahead on +X interp.Enqueue(new Vector3(1f, 0f, 0f), heading: 0f, isMovingTo: false); - // rootMotion = (0, 4, 0) × 0.1 = (0, 0.4, 0) - // correction ≈ (0.8, 0, 0) - // combined ≈ (0.8, 0.4, 0) + // correction ≈ (0.8, 0, 0) — replaces root motion (0, 0.4, 0). + // retail-faithful: correction overwrites root motion, Y is dropped. + // (842dfcd, 2026-05-03: switched from additive to replace semantics) Vector3 offset = pm.ComputeOffset( dt: 0.1, currentBodyPosition: Vector3.Zero, @@ -144,7 +151,7 @@ public sealed class PositionManagerTests maxSpeed: 4f); Assert.Equal(0.8f, offset.X, precision: 3); - Assert.Equal(0.4f, offset.Y, precision: 3); + Assert.Equal(0f, offset.Y, precision: 3); // root motion dropped — correction replaces Assert.Equal(0f, offset.Z, precision: 3); }