diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs index 91acee5d9..ca30ee7d9 100644 --- a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs +++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs @@ -133,8 +133,10 @@ public class PetOrderTests : IDisposable Assert.Equal(pet.Location, pet.Home); // idle anchor = where stopped } + // Publish 51: "Stop: ... may wander." A stay is a standing order like follow or guard; + // stop cancels it and the pet idles, anchored where it stands. [Fact] - public void Stop_WhileStaying_RemainsStayingAtOriginalPost() + public void Stop_WhileStaying_CancelsToIdleNone() { var post = new Point3D(1005, 1005, 0); var (_, pet) = Spawn(new Point3D(1000, 1000, 0), post); @@ -143,9 +145,26 @@ public class PetOrderTests : IDisposable pet.ControlOrder = OrderType.Stop; + Assert.Equal(OrderType.None, pet.ControlOrder); + Assert.Equal(OrderType.None, pet.AIObject.PersistentOrder); + Assert.Equal(pet.Location, pet.Home); // idle anchor = where stopped + } + + // "all come" rests into Stay on arrival; "all stop" must not leave the pet at that post. + [Fact] + public void Come_ThenArrive_ThenStop_IdlesRatherThanStaying() + { + var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0)); + Assert.Equal(OrderType.Come, pet.ControlOrder); + + pet.AIObject.Obey(); // within 2 tiles -> Stay Assert.Equal(OrderType.Stay, pet.ControlOrder); - Assert.Equal(OrderType.Stay, pet.AIObject.PersistentOrder); - Assert.Equal(post, pet.Home); // post unchanged + + pet.ControlOrder = OrderType.Stop; + + Assert.Equal(OrderType.None, pet.ControlOrder); + Assert.Equal(OrderType.None, pet.AIObject.PersistentOrder); + Assert.Equal(pet.Location, pet.Home); } [Fact] diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetRetaliationTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetRetaliationTests.cs index 00316a351..d5074704c 100644 --- a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetRetaliationTests.cs +++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetRetaliationTests.cs @@ -161,4 +161,66 @@ public class PetRetaliationTests : IDisposable Assert.Same(attacker, pet.Combatant); Assert.True(pet.Warmode); } + + private sealed class UncommandablePet : PetTestStub + { + public override bool StandsDownOnCommand => true; + public override bool Commandable => false; + } + + // The publish speaks of commanded pets. A creature nobody can give an order to was never + // told anything, yet it rests on None (wild, uncontrolled summon) or a system-issued Follow + // (familiar, escortee) - the same orders a pet stands down on. It must still fight back. + [Fact] + public void WildCreature_Retaliates_UnderStandDown() + { + var creature = new StandDownPet(); + creature.MoveToWorld(new Point3D(1001, 1000, 0), Map.Felucca); + creature.AIObject.AITimer?.Stop(); + _created.Add(creature); + + Assert.Equal(OrderType.None, creature.ControlOrder); + + var attacker = Attack(creature); + + Assert.Same(attacker, creature.Combatant); + Assert.True(creature.Warmode); + } + + [Fact] // energy vortex, blade spirits: Summoned with a SummonMaster, never Controlled + public void UncontrolledSummon_Retaliates_UnderStandDown() + { + var caster = new PlayerMobile(World.NewMobile); + caster.DefaultMobileInit(); + caster.MoveToWorld(new Point3D(1000, 1000, 0), Map.Felucca); + _created.Add(caster); + + var summon = new StandDownPet(); + summon.Summoned = true; + summon.SummonMaster = caster; + summon.MoveToWorld(new Point3D(1001, 1000, 0), Map.Felucca); + summon.AIObject.AITimer?.Stop(); + _created.Add(summon); + + Assert.False(summon.Controlled); + Assert.Equal(OrderType.None, summon.ControlOrder); + + var attacker = Attack(summon); + + Assert.Same(attacker, summon.Combatant); + Assert.True(summon.Warmode); + } + + [Fact] // familiar, escortee: Controlled with a master, but not Commandable + public void UncommandableCreature_Retaliates_UnderStandDown() + { + var (master, familiar) = Spawn(); + familiar.ControlTarget = master; + familiar.ControlOrder = OrderType.Follow; // system-issued, not a command + + var attacker = Attack(familiar); + + Assert.Same(attacker, familiar.Combatant); + Assert.True(familiar.Warmode); + } } diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs b/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs index 1e751e9ab..1a166d493 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs @@ -218,19 +218,16 @@ public abstract partial class BaseAI return OrderType.Attack; } - // Stop: Follow/Guard -> idle here; Stay -> keep the post; anything transient -> the standing order. + // Stop: Follow/Guard/Stay -> idle here; anything transient -> the standing order. private OrderType IssueStop(OrderType previous) { Mobile.ControlTarget = null; switch (previous) { - case OrderType.Stay: - { - return OrderType.Stay; // resumed: anchor untouched - } case OrderType.Follow: case OrderType.Guard: + case OrderType.Stay: { SetPersistentOrder(OrderType.None); // cancel the standing order; idle anchor = here return OrderType.None; diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index be4eea9e9..6aa05c1f7 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -2736,22 +2736,18 @@ namespace Server.Mobiles aggressor.Aggressors.Add(AggressorInfo.Create(this, aggressor, true)); } - var ct = _controlOrder; + var toldToStandDown = _controlled && _controlMaster != null && Commandable && + BaseAI.IsStandDownOrder(_controlOrder); - if (AIObject != null) + if (StandsDownOnCommand && toldToStandDown) { - if (!StandsDownOnCommand || !BaseAI.IsStandDownOrder(ct)) - { - AIObject.OnAggressiveAction(aggressor); - } - else - { - AIObject.DebugSay("I'm being attacked but my master told me not to fight."); - Warmode = false; - return; - } + AIObject?.DebugSay("I'm being attacked but my master told me not to fight."); + Warmode = false; + return; } + AIObject?.OnAggressiveAction(aggressor); + StopFlee(); ForceReacquire(); @@ -2767,7 +2763,7 @@ namespace Server.Mobiles } // Only reachable when the pet does not stand down: the orders above returned early. - if (aggressor.ChangingCombatant && (_controlled || _summoned) && BaseAI.IsStandDownOrder(ct)) + if (aggressor.ChangingCombatant && toldToStandDown) { IssueOrder(OrderType.Attack, null, aggressor); }