diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs index e5d7760ed..fbc6a2f81 100644 --- a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs +++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs @@ -67,6 +67,59 @@ public class PetOrderTests : IDisposable Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder); } + [Fact] + public void Stop_WhileAttacking_ResumedFollowTargetsTheMaster() + { + var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0)); + pet.ControlTarget = master; + pet.ControlOrder = OrderType.Follow; // persistent = Follow + pet.ControlOrder = OrderType.Attack; // transient + Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder); + + pet.ControlOrder = OrderType.Stop; + + Assert.Equal(OrderType.Follow, pet.ControlOrder); + Assert.Equal(master, pet.ControlTarget); + } + + [Fact] + public void Stop_WhileAttacking_ResumedFollowKeepsAPetFriendAsItsTarget() + { + var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0)); + var friend = new PlayerMobile(World.NewMobile); + friend.DefaultMobileInit(); + friend.MoveToWorld(new Point3D(1002, 1000, 0), pet.Map); + _created.Add(friend); + + var victim = new PetTestStub(); + victim.MoveToWorld(new Point3D(1003, 1000, 0), pet.Map); + _created.Add(victim); + + pet.ControlTarget = friend; // a pet friend said "all follow me" + pet.ControlOrder = OrderType.Follow; + pet.ControlTarget = victim; // then "all kill" — the attack order takes the target + pet.ControlOrder = OrderType.Attack; + + pet.ControlOrder = OrderType.Stop; + + Assert.Equal(OrderType.Follow, pet.ControlOrder); + Assert.Equal(friend, pet.ControlTarget); // still the friend, not the owner + } + + [Fact] + public void Stop_WhileAttacking_ResumedFollowSurvivesTheNextThink() + { + var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0)); + pet.ControlOrder = OrderType.Follow; + pet.ControlOrder = OrderType.Attack; + pet.ControlOrder = OrderType.Stop; // resumes Follow + + pet.AIObject.Obey(); + + Assert.Equal(OrderType.Follow, pet.ControlOrder); // not dropped to idle + Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder); + } + [Fact] public void Stop_WhileFollowing_CancelsToIdleNone() { diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs b/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs index e09424328..3282493b8 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs @@ -28,16 +28,30 @@ public abstract partial class BaseAI // never re-derives the persistent command or re-anchors Home. See OnCurrentOrderChanged. private bool _resolvingOrder; + // Who a standing Follow follows: usually the master, but a pet friend can point it + // elsewhere ("all follow me"). Runtime-only, like PersistentOrder. + private Mobile _persistentFollowTarget; + // The controlled-pet wander anchor (Home) is a pure function of the persistent command. internal void SetPersistentOrder(OrderType order) { PersistentOrder = order; + _persistentFollowTarget = order == OrderType.Follow ? Mobile.ControlTarget ?? Mobile.ControlMaster : null; Mobile.Home = order is OrderType.Follow or OrderType.Guard ? Point3D.Zero : Mobile.Location; } // Resume the persistent command without re-deriving the persistent order or anchor. private void ResumePersistentOrder() { + // The ending order owns ControlTarget and leaves it cleared or pointing elsewhere; + // without a target DoOrderFollow cancels itself to idle on the next think. + if (PersistentOrder == OrderType.Follow) + { + Mobile.ControlTarget = _persistentFollowTarget?.Deleted == false + ? _persistentFollowTarget + : Mobile.ControlMaster; + } + _resolvingOrder = true; Mobile.ControlOrder = PersistentOrder; _resolvingOrder = false;