using System; using System.Numerics; using AcDream.Core.Physics; using Xunit; namespace AcDream.Core.Tests.Physics; // ───────────────────────────────────────────────────────────────────────────── // RemoteMotionCombinerTests — 6 tests covering ComputeOffset (class renamed R5 // from PositionManager; see RemoteMotionCombiner's class doc). // // Mirrors retail CPhysicsObj::UpdateObjectInternal (acclient @ 0x00513730). // Pure-function combiner: CSequence root-motion delta (rotated by // body orientation) + InterpolationManager.AdjustOffset correction. // ───────────────────────────────────────────────────────────────────────────── public sealed class RemoteMotionCombinerTests { // ── helpers ─────────────────────────────────────────────────────────────── private static RemoteMotionCombiner Make() => new(); private static InterpolationManager EmptyInterp() => new(); // ========================================================================= // Test 1: stationary remote — both sources zero, no motion // ========================================================================= [Fact] public void ComputeOffset_StationaryRemote_BothSourcesZero_NoMotion() { var pm = Make(); var interp = EmptyInterp(); Vector3 offset = pm.ComputeOffset( dt: 0.1, currentBodyPosition: Vector3.Zero, rootMotionLocalDelta: Vector3.Zero, ori: Quaternion.Identity, interp: interp, maxSpeed: 4f); Assert.Equal(Vector3.Zero, offset); } // ========================================================================= // Test 2: animation only, identity orientation, forward velocity // ========================================================================= [Fact] public void ComputeOffset_AnimationOnly_Forward_BodyAdvances() { var pm = Make(); var interp = EmptyInterp(); // CSequence accumulated (0, 0.4, 0) for this 0.1-second quantum. Vector3 offset = pm.ComputeOffset( dt: 0.1, currentBodyPosition: Vector3.Zero, rootMotionLocalDelta: new Vector3(0f, 0.4f, 0f), ori: Quaternion.Identity, interp: interp, maxSpeed: 0f); Assert.Equal(0f, offset.X, precision: 4); Assert.Equal(0.4f, offset.Y, precision: 4); Assert.Equal(0f, offset.Z, precision: 4); } // ========================================================================= // Test 3: animation only, 180° yaw around Z — body moves south (-Y) // ========================================================================= [Fact] public void ComputeOffset_AnimationOnly_OrientedSouth_BodyMovesSouth() { var pm = Make(); var interp = EmptyInterp(); // 180° around Z flips +Y → -Y Quaternion ori = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI); Vector3 offset = pm.ComputeOffset( dt: 0.1, currentBodyPosition: Vector3.Zero, rootMotionLocalDelta: new Vector3(0f, 0.4f, 0f), ori: ori, interp: interp, maxSpeed: 0f); Assert.Equal(0f, offset.X, precision: 4); Assert.Equal(-0.4f, offset.Y, precision: 4); } // ========================================================================= // Test 4: interp only, no animation — body chases queue // ========================================================================= [Fact] public void ComputeOffset_InterpOnly_NoAnimation_BodyChasesQueue() { var pm = Make(); var interp = new InterpolationManager(); // Enqueue target 1m ahead on +X; body starts at origin interp.Enqueue(new Vector3(1f, 0f, 0f), heading: 0f, isMovingTo: false); // Expected catch-up: catchUpSpeed = maxSpeed × 2 = 4 × 2 = 8 m/s // step = 8 × 0.1 = 0.8m (< dist = 1m so no overshoot clamp) Vector3 offset = pm.ComputeOffset( dt: 0.1, currentBodyPosition: Vector3.Zero, rootMotionLocalDelta: Vector3.Zero, ori: Quaternion.Identity, interp: interp, maxSpeed: 4f); Assert.Equal(0.8f, offset.X, precision: 3); Assert.Equal(0f, offset.Y, precision: 3); Assert.Equal(0f, offset.Z, precision: 3); } // ========================================================================= // 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_CorrectionReplacesRootMotion() { var pm = Make(); var interp = new InterpolationManager(); // Enqueue target 1m ahead on +X interp.Enqueue(new Vector3(1f, 0f, 0f), heading: 0f, isMovingTo: false); // 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, rootMotionLocalDelta: new Vector3(0f, 0.4f, 0f), ori: Quaternion.Identity, interp: interp, maxSpeed: 4f); Assert.Equal(0.8f, offset.X, precision: 3); Assert.Equal(0f, offset.Y, precision: 3); // root motion dropped — correction replaces Assert.Equal(0f, offset.Z, precision: 3); } // ========================================================================= // Test 6: local-to-world rotation — +90° yaw around Z // ========================================================================= [Fact] public void ComputeOffset_LocalToWorldRotation_Yaw90() { var pm = Make(); var interp = EmptyInterp(); // +90° CCW around Z in right-handed coordinates: // body-local +Y → world -X Quaternion ori = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2f); // CSequence accumulated rootMotionLocal = (0, 1, 0). // after Transform by ori → (-1, 0, 0) approximately Vector3 offset = pm.ComputeOffset( dt: 1.0, currentBodyPosition: Vector3.Zero, rootMotionLocalDelta: new Vector3(0f, 1f, 0f), ori: ori, interp: interp, maxSpeed: 0f); Assert.Equal(-1f, offset.X, precision: 4); Assert.Equal(0f, offset.Y, precision: 4); Assert.Equal(0f, offset.Z, precision: 4); } // ========================================================================= // AD-10 retired 2026-08-06. Two tests lived here: // ComputeOffset_RootMotionFallback_SlopedTerrainNormal_ProjectsZOntoSlope // ComputeOffset_RootMotionFallback_FlatTerrainNormal_NoZChange // They exercised the pre-sweep terrain projection that has been deleted, so // they are gone with it. Both were also weak on their own terms: they drove // ComputeOffset, which has no production callers, and computed their // expected values by re-implementing the projection formula in a comment — // so they could detect a wrong MULTIPLY but never a wrong PLANE, which is // what the divergence actually was. The surviving coverage of the same // behaviour is geometric and runs the production tick: // AcDream.Runtime.Tests.Physics.RuntimeRemoteSlopeProjectionTests. // ========================================================================= [Fact] public void ComputeOffset_QueueHeadReached_WithLiteralZeroRootMotion_DoesNotOvershoot() { var pm = Make(); var interp = new InterpolationManager(); var target = new Vector3(0.4f, 0f, 0f); interp.Enqueue(target, heading: 0f, isMovingTo: false); Vector3 catchUp = pm.ComputeOffset( dt: 0.1, currentBodyPosition: Vector3.Zero, rootMotionLocalDelta: Vector3.Zero, ori: Quaternion.Identity, interp: interp, maxSpeed: 4f); Vector3 reachedPosition = catchUp; Vector3 afterReach = pm.ComputeOffset( dt: 0.1, currentBodyPosition: reachedPosition, rootMotionLocalDelta: Vector3.Zero, ori: Quaternion.Identity, interp: interp, maxSpeed: 4f); Assert.Equal(target, reachedPosition); Assert.Equal(Vector3.Zero, afterReach); } }