using System.Numerics; using AcDream.Core.Physics; using AcDream.Core.Physics.Motion; using Xunit; namespace AcDream.Core.Tests.Physics.Motion; /// /// R4-V5 — the MoveToComplete CLIENT-ADDITION seam contract (see the /// seam's doc on ): fires with /// on NATURAL COMPLETION (the /// empty-queue exits, both sticky /// and non-sticky, plus CleanUpAndCallWeenie's instant-success path) /// and NEVER on . The App layer's /// AD-27 re-anchor (deferred Use/PickUp re-send on arrival) depends on /// exactly this split: arrival fires the deferred action once; a user-input /// cancel must not. /// public sealed class MoveToManagerCompletionSeamTests { /// Arms a position move 5 m dead ahead (heading 90 = +X, /// facing it) and drives the scripted body to arrival via UseTime. private static MoveToManagerHarness DriveToArrival() { var h = new MoveToManagerHarness(); h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity); h.Heading = 90f; h.Manager.MoveToPosition( new Position(1u, new Vector3(5f, 0f, 0f), Quaternion.Identity), new MovementParameters { UseSpheres = false, DistanceToObject = 0.6f }); h.DrainPendingMotions(); for (int i = 0; i < 200 && h.Manager.IsMovingTo(); i++) { h.Tick(); var cur = h.WorldPosition.Frame.Origin; h.WorldPosition = new Position( 1u, cur + new Vector3(0.2f, 0f, 0f), Quaternion.Identity); h.Manager.UseTime(); h.DrainPendingMotions(); } Assert.False(h.Manager.IsMovingTo(), "scripted drive must reach arrival within the tick budget"); return h; } [Fact] public void NaturalArrival_FiresMoveToComplete_OnceWithNone() { var h = DriveToArrival(); Assert.Single(h.MoveToCompleteCalls); Assert.Equal(WeenieError.None, h.MoveToCompleteCalls[0]); } [Fact] public void AfterArrival_ExtraUseTimeTicks_DoNotRefire() { var h = DriveToArrival(); for (int i = 0; i < 10; i++) { h.Tick(); h.Manager.UseTime(); } Assert.Single(h.MoveToCompleteCalls); } [Fact] public void CancelMoveTo_DoesNotFireMoveToComplete() { var h = new MoveToManagerHarness(); h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity); h.Heading = 90f; h.Manager.MoveToPosition( new Position(1u, new Vector3(20f, 0f, 0f), Quaternion.Identity), new MovementParameters { UseSpheres = false }); Assert.True(h.Manager.IsMovingTo()); h.Manager.CancelMoveTo(WeenieError.ActionCancelled); Assert.False(h.Manager.IsMovingTo()); Assert.Empty(h.MoveToCompleteCalls); } [Fact] public void StickyCompletion_FiresMoveToComplete_AfterStickToHandoff() { // In-range sticky object move: MoveToObject arms the tracker; the // first Ok callback finds the target already inside // DistanceToObject, so no motion nodes are needed and BeginNextNode // lands straight on the empty-queue STICKY exit — StickTo gets the // pre-CleanUp tlid/radius/height, then the completion seam fires. var h = new MoveToManagerHarness(); h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity); h.Heading = 90f; h.Manager.MoveToObject( objectId: 0x1000u, topLevelId: 0x1000u, radius: 0.5f, height: 1f, new MovementParameters { Sticky = true, DistanceToObject = 5f }); h.DrainPendingMotions(); var target = new Position(1u, new Vector3(1f, 0f, 0f), Quaternion.Identity); h.Manager.HandleUpdateTarget( new TargetInfo(0x1000u, TargetStatus.Ok, target, target)); h.DrainPendingMotions(); // Some builds need the turn/settle nodes ticked through first. for (int i = 0; i < 50 && h.Manager.IsMovingTo(); i++) { h.Tick(); h.Manager.UseTime(); h.DrainPendingMotions(); } Assert.False(h.Manager.IsMovingTo()); Assert.Single(h.StickToCalls); Assert.Equal((0x1000u, 0.5f, 1f), h.StickToCalls[0]); Assert.Single(h.MoveToCompleteCalls); Assert.Equal(WeenieError.None, h.MoveToCompleteCalls[0]); } }