fix: keep the Guard order through combat and sprint guard returns
A guarding pet's engagement path (FindCombatant) rewrote ControlOrder to Attack, so guard silently converted to a plain kill order mid-fight (#2595): the (guarding)/guarded OPL tags vanished, the pet stopped scanning for the master's threats and never retargeted, recall/gate left it behind (TeleportPets only takes Guard/Follow/Come), and every engage->kill->resume cycle replayed the "is now guarding you" flourish. Return pacing also depended on stale Warmode, leaving guard returns active or passive by combat history. - FindCombatant is now FindGuardTarget: a pure selector that prefers the aggressor closest to the master (RunUO guard parity) and keeps the current combatant unless a strictly closer one exists. DoOrderGuard engages through it without ever leaving the Guard order. - HandleInvalidControlTarget resumes the persistent order first and chains the next aggressor into an explicit Attack only for non-guard fallbacks — an explicit "all attack" completes, returns to guarding, and the guard scan takes over. Resuming Guard no longer replays the sound/message flourish. - The peaceful guard branch stands down (Warmode/Combatant/FocusMob cleared) so pacing is deterministic, and guard returns sprint at the follow pace (0.1s/step, AOS) on the move clock while the think cadence is untouched. - WalkMobileRange honors the caller's run flag instead of a hardcoded distance-5 gate; follow/guard run animation now matches their actual pace. Closes #2595 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1ce2efb1ed
commit
5d00149dd8
6 changed files with 265 additions and 53 deletions
140
Projects/UOContent.Tests/Tests/Mobiles/AI/GuardOrderTests.cs
Normal file
140
Projects/UOContent.Tests/Tests/Mobiles/AI/GuardOrderTests.cs
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Server;
|
||||||
|
using Server.Mobiles;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace UOContent.Tests.Mobiles.AI;
|
||||||
|
|
||||||
|
// Pins guard-order combat semantics (issue #2595): a guarding pet engages and fights
|
||||||
|
// without ever leaving the Guard order, retargets toward the master's closest aggressor,
|
||||||
|
// and stands down deterministically when there is nothing to guard against. The scene
|
||||||
|
// sits on the proven-open (1495..1500, 1600) Trammel segment from ApproachTargetTests;
|
||||||
|
// combat targets are adjacent so no pathfinding runs.
|
||||||
|
[Collection("Sequential UOContent Tests")]
|
||||||
|
public class GuardOrderTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly List<Mobile> _created = new();
|
||||||
|
|
||||||
|
private sealed class AggressorStub : Mobile
|
||||||
|
{
|
||||||
|
public AggressorStub() => Body = 0xC9;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var m in _created)
|
||||||
|
{
|
||||||
|
m?.Delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
_created.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private (PlayerMobile master, PetTestStub pet) SpawnGuardingPet(out Map map, out int z)
|
||||||
|
{
|
||||||
|
map = Map.Maps[1];
|
||||||
|
Assert.NotNull(map);
|
||||||
|
map.GetAverageZ(1500, 1600, out _, out z, out _);
|
||||||
|
|
||||||
|
var master = new PlayerMobile(World.NewMobile);
|
||||||
|
master.DefaultMobileInit();
|
||||||
|
master.MoveToWorld(new Point3D(1500, 1600, (sbyte)z), map);
|
||||||
|
_created.Add(master);
|
||||||
|
|
||||||
|
var pet = new PetTestStub();
|
||||||
|
pet.MoveToWorld(new Point3D(1499, 1600, (sbyte)z), map);
|
||||||
|
pet.SetControlMaster(master);
|
||||||
|
_created.Add(pet);
|
||||||
|
|
||||||
|
pet.AIObject.AITimer?.Stop(); // drive manually
|
||||||
|
pet.ControlOrder = OrderType.Guard;
|
||||||
|
pet.AIObject.AITimer?.Stop(); // the order change restarts the timer
|
||||||
|
|
||||||
|
return (master, pet);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AggressorStub SpawnAggressor(PetTestStub pet, Point3D loc, Mobile attacking)
|
||||||
|
{
|
||||||
|
var aggr = new AggressorStub();
|
||||||
|
aggr.MoveToWorld(loc, pet.Map);
|
||||||
|
_created.Add(aggr);
|
||||||
|
|
||||||
|
// Guards the test setup itself: the scene must stay on open, LOS-clear terrain
|
||||||
|
// and the combatant assignment must not be vetoed.
|
||||||
|
Assert.True(pet.InLOS(aggr), $"no LOS from pet to aggressor at {loc}");
|
||||||
|
|
||||||
|
if (attacking != null)
|
||||||
|
{
|
||||||
|
aggr.Combatant = attacking;
|
||||||
|
Assert.Same(attacking, aggr.Combatant);
|
||||||
|
}
|
||||||
|
|
||||||
|
return aggr;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GuardEngage_KeepsGuardOrder()
|
||||||
|
{
|
||||||
|
var (master, pet) = SpawnGuardingPet(out _, out var z);
|
||||||
|
var aggr = SpawnAggressor(pet, new Point3D(1498, 1600, (sbyte)z), master);
|
||||||
|
|
||||||
|
pet.AIObject.Obey();
|
||||||
|
|
||||||
|
Assert.Same(aggr, pet.Combatant);
|
||||||
|
Assert.Equal(OrderType.Guard, pet.ControlOrder);
|
||||||
|
Assert.Equal(OrderType.Guard, pet.AIObject.PersistentOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Guard_RetargetsToAggressorClosestToMaster()
|
||||||
|
{
|
||||||
|
var (master, pet) = SpawnGuardingPet(out _, out var z);
|
||||||
|
var far = SpawnAggressor(pet, new Point3D(1495, 1600, (sbyte)z), master);
|
||||||
|
var near = SpawnAggressor(pet, new Point3D(1498, 1600, (sbyte)z), master);
|
||||||
|
|
||||||
|
pet.Combatant = far; // already fighting the far aggressor
|
||||||
|
|
||||||
|
pet.AIObject.Obey();
|
||||||
|
|
||||||
|
Assert.Same(near, pet.Combatant); // defends the master, not the current fight
|
||||||
|
Assert.Equal(OrderType.Guard, pet.ControlOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExplicitAttack_ResumesGuard_WithoutChainingIntoAttack()
|
||||||
|
{
|
||||||
|
var (master, pet) = SpawnGuardingPet(out _, out var z);
|
||||||
|
|
||||||
|
// Explicit kill order on a target that then becomes invalid.
|
||||||
|
var victim = SpawnAggressor(pet, new Point3D(1498, 1600, (sbyte)z), null);
|
||||||
|
pet.ControlTarget = victim;
|
||||||
|
pet.ControlOrder = OrderType.Attack;
|
||||||
|
victim.Hidden = true;
|
||||||
|
|
||||||
|
// A second aggressor is still after the master; FightMode.Closest would chain it.
|
||||||
|
var aggr2 = SpawnAggressor(pet, new Point3D(1497, 1600, (sbyte)z), master);
|
||||||
|
|
||||||
|
pet.AIObject.Obey(); // attack completes -> resume the persistent Guard
|
||||||
|
|
||||||
|
Assert.Equal(OrderType.Guard, pet.ControlOrder);
|
||||||
|
|
||||||
|
pet.AIObject.Obey(); // the guard scan engages the remaining aggressor in-order
|
||||||
|
|
||||||
|
Assert.Same(aggr2, pet.Combatant);
|
||||||
|
Assert.Equal(OrderType.Guard, pet.ControlOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PeacefulGuard_StandsDown()
|
||||||
|
{
|
||||||
|
var (_, pet) = SpawnGuardingPet(out _, out _);
|
||||||
|
Assert.True(pet.Warmode); // the guard order opens in war stance
|
||||||
|
|
||||||
|
pet.AIObject.Obey(); // nothing to guard against
|
||||||
|
|
||||||
|
Assert.False(pet.Warmode);
|
||||||
|
Assert.Null(pet.Combatant);
|
||||||
|
Assert.Null(pet.FocusMob);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -42,14 +42,50 @@ public class PetPacingTests : IDisposable
|
||||||
Assert.Equal(OrderType.Come, pet.ControlOrder);
|
Assert.Equal(OrderType.Come, pet.ControlOrder);
|
||||||
Assert.Equal(0.4, pet.CurrentMoveSpeed);
|
Assert.Equal(0.4, pet.CurrentMoveSpeed);
|
||||||
|
|
||||||
pet.ControlOrder = OrderType.Guard;
|
|
||||||
Assert.Equal(0.4, pet.CurrentMoveSpeed);
|
|
||||||
|
|
||||||
pet.ControlTarget = master;
|
pet.ControlTarget = master;
|
||||||
pet.ControlOrder = OrderType.Follow;
|
pet.ControlOrder = OrderType.Follow;
|
||||||
Assert.Equal(0.4, pet.CurrentMoveSpeed);
|
Assert.Equal(0.4, pet.CurrentMoveSpeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A guarding pet with nothing to fight returns to its master at the follow sprint
|
||||||
|
// pace (RunUO guard parity) while its think cadence stays untouched.
|
||||||
|
[Fact]
|
||||||
|
public void GuardReturn_SprintsOnMoveClock()
|
||||||
|
{
|
||||||
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
||||||
|
pet.SetMoveSpeed(0.3, 0.9);
|
||||||
|
pet.SetCurrentSpeedToPassive();
|
||||||
|
|
||||||
|
pet.ControlOrder = OrderType.Guard; // fixture era is EJ: Core.AOS is true
|
||||||
|
|
||||||
|
Assert.Equal(0.1, pet.CurrentMoveSpeed);
|
||||||
|
Assert.Equal(0.4, pet.CurrentSpeed); // think clock unaffected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boundary guard: pre-AOS eras have no sprint — guard paces on the think clock.
|
||||||
|
[Fact]
|
||||||
|
public void GuardReturn_PreAOS_PacesThinkClock()
|
||||||
|
{
|
||||||
|
var previous = Core.Expansion;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Core.Expansion = Expansion.UOR;
|
||||||
|
|
||||||
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
||||||
|
pet.SetMoveSpeed(0.3, 0.9);
|
||||||
|
pet.SetCurrentSpeedToPassive();
|
||||||
|
|
||||||
|
pet.ControlOrder = OrderType.Guard;
|
||||||
|
|
||||||
|
Assert.Equal(0.4, pet.CurrentMoveSpeed);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Core.Expansion = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Boundary guard: a pet chasing a combatant keeps the move table.
|
// Boundary guard: a pet chasing a combatant keeps the move table.
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CombatChasingPet_KeepsMoveTable()
|
public void CombatChasingPet_KeepsMoveTable()
|
||||||
|
|
|
||||||
|
|
@ -649,14 +649,12 @@ public abstract partial class BaseAI
|
||||||
{
|
{
|
||||||
var iCurrDist = (int)Mobile.GetDistanceToSqrt(m);
|
var iCurrDist = (int)Mobile.GetDistanceToSqrt(m);
|
||||||
|
|
||||||
var shouldRun = run && iCurrDist > 5;
|
|
||||||
|
|
||||||
if (iCurrDist >= iWantDistMin && iCurrDist <= iWantDistMax)
|
if (iCurrDist >= iWantDistMin && iCurrDist <= iWantDistMax)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!MoveTowardsOrAwayFrom(m, shouldRun, iCurrDist, iWantDistMax))
|
if (!MoveTowardsOrAwayFrom(m, run, iCurrDist, iWantDistMax))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -667,18 +665,19 @@ public abstract partial class BaseAI
|
||||||
return dist >= iWantDistMin && dist <= iWantDistMax;
|
return dist >= iWantDistMin && dist <= iWantDistMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The caller's run flag is honored as-is: it only sets the client-side animation
|
||||||
|
// (server pace is the move budget), and the callers that pass anything but false —
|
||||||
|
// follow, guard, clone — gate it on their own distance thresholds.
|
||||||
private bool MoveTowardsOrAwayFrom(Mobile m, bool run, int iCurrDist, int iWantDistMax)
|
private bool MoveTowardsOrAwayFrom(Mobile m, bool run, int iCurrDist, int iWantDistMax)
|
||||||
{
|
{
|
||||||
var shouldRun = run && iCurrDist > 5;
|
|
||||||
|
|
||||||
if (iCurrDist > iWantDistMax)
|
if (iCurrDist > iWantDistMax)
|
||||||
{
|
{
|
||||||
// Too far: approach via the centralized progress-based primitive.
|
// Too far: approach via the centralized progress-based primitive.
|
||||||
return ApproachTarget(m, shouldRun, iWantDistMax);
|
return ApproachTarget(m, run, iWantDistMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Too close: back away. Retreat keeps the simple greedy behavior (out of scope).
|
// Too close: back away. Retreat keeps the simple greedy behavior (out of scope).
|
||||||
if (DoMove(m.GetDirectionTo(Mobile, shouldRun), true))
|
if (DoMove(m.GetDirectionTo(Mobile, run), true))
|
||||||
{
|
{
|
||||||
Path = null;
|
Path = null;
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -163,9 +163,16 @@ public abstract partial class BaseAI
|
||||||
_commandIssuer?.RevealingAction();
|
_commandIssuer?.RevealingAction();
|
||||||
Mobile.FocusMob = null;
|
Mobile.FocusMob = null;
|
||||||
Mobile.Warmode = true;
|
Mobile.Warmode = true;
|
||||||
|
|
||||||
|
// Only a freshly issued command plays the flourish; resuming the persistent
|
||||||
|
// order after an explicit attack must not replay it after every kill.
|
||||||
|
if (!_resolvingOrder)
|
||||||
|
{
|
||||||
Mobile.PlaySound(Mobile.GetAttackSound());
|
Mobile.PlaySound(Mobile.GetAttackSound());
|
||||||
Mobile.ControlMaster?.SendLocalizedMessage(1049671, Mobile.Name);
|
Mobile.ControlMaster?.SendLocalizedMessage(1049671, Mobile.Name);
|
||||||
// ~1_NAME~ is now guarding you.
|
// ~1_NAME~ is now guarding you.
|
||||||
|
}
|
||||||
|
|
||||||
_commandIssuer = null;
|
_commandIssuer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -291,14 +291,15 @@ public abstract partial class BaseAI
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
FindCombatant();
|
var combatant = FindGuardTarget();
|
||||||
|
|
||||||
if (IsValidCombatant(Mobile.Combatant))
|
if (combatant != null)
|
||||||
{
|
{
|
||||||
var combatant = Mobile.Combatant;
|
|
||||||
|
|
||||||
this.DebugSayFormatted($"Attacking target: {combatant.Name}");
|
this.DebugSayFormatted($"Attacking target: {combatant.Name}");
|
||||||
|
|
||||||
|
// Engage without leaving the Guard order (#2595): the (guarding)/guarded
|
||||||
|
// tags persist, recall/gate keeps the pet, and the per-tick scan retargets
|
||||||
|
// toward the master's closest aggressor for the whole fight.
|
||||||
Mobile.Combatant = combatant;
|
Mobile.Combatant = combatant;
|
||||||
Mobile.FocusMob = combatant;
|
Mobile.FocusMob = combatant;
|
||||||
Action = ActionType.Combat;
|
Action = ActionType.Combat;
|
||||||
|
|
@ -309,6 +310,12 @@ public abstract partial class BaseAI
|
||||||
{
|
{
|
||||||
this.DebugSayFormatted($"Guarding my master, {controlMaster.Name}.");
|
this.DebugSayFormatted($"Guarding my master, {controlMaster.Name}.");
|
||||||
|
|
||||||
|
// Stand down deterministically: a stale Warmode otherwise leaves the return
|
||||||
|
// pace active or passive by combat history.
|
||||||
|
Mobile.FocusMob = null;
|
||||||
|
Mobile.Warmode = false;
|
||||||
|
Mobile.Combatant = null;
|
||||||
|
|
||||||
var distance = (int)Mobile.GetDistanceToSqrt(controlMaster);
|
var distance = (int)Mobile.GetDistanceToSqrt(controlMaster);
|
||||||
|
|
||||||
if (distance > 3)
|
if (distance > 3)
|
||||||
|
|
@ -359,67 +366,86 @@ public abstract partial class BaseAI
|
||||||
Mobile.ControlTarget = Mobile.ControlMaster;
|
Mobile.ControlTarget = Mobile.ControlMaster;
|
||||||
ResumePersistentOrder();
|
ResumePersistentOrder();
|
||||||
|
|
||||||
if (Mobile.FightMode is FightMode.Closest or FightMode.Aggressor)
|
// A resumed Guard engages through its own scan without ever leaving the order;
|
||||||
|
// only non-guard fallbacks chain the next aggressor through an explicit Attack.
|
||||||
|
if (Mobile.ControlOrder == OrderType.Guard ||
|
||||||
|
Mobile.FightMode is not (FightMode.Closest or FightMode.Aggressor))
|
||||||
{
|
{
|
||||||
FindCombatant();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var next = FindGuardTarget();
|
||||||
|
|
||||||
|
if (next != null)
|
||||||
|
{
|
||||||
|
Mobile.ControlTarget = next;
|
||||||
|
Mobile.ControlOrder = OrderType.Attack;
|
||||||
|
Mobile.Combatant = next;
|
||||||
|
|
||||||
|
this.DebugSayFormatted($"{next.Name} is still hostile! Engaging...");
|
||||||
|
|
||||||
|
Think();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FindCombatant()
|
/// <summary>
|
||||||
|
/// Selects the aggressor a pet should defend against, preferring whichever is
|
||||||
|
/// closest to the master (RunUO guard parity — a guarding pet retargets to protect
|
||||||
|
/// its owner). The current combatant is the baseline and is kept unless a strictly
|
||||||
|
/// closer aggressor exists. Pure selection: never mutates any order state.
|
||||||
|
/// </summary>
|
||||||
|
private Mobile FindGuardTarget()
|
||||||
{
|
{
|
||||||
var controlMaster = Mobile.ControlMaster;
|
var controlMaster = Mobile.ControlMaster;
|
||||||
|
var anchor = controlMaster ?? Mobile;
|
||||||
|
|
||||||
|
var current = Mobile.Combatant;
|
||||||
|
var best = current != controlMaster && IsValidCombatant(current) ? current : null;
|
||||||
|
var bestDist = best?.GetDistanceToSqrt(anchor) ?? double.MaxValue;
|
||||||
|
|
||||||
foreach (var aggr in Mobile.GetMobilesInRange(Mobile.RangePerception))
|
foreach (var aggr in Mobile.GetMobilesInRange(Mobile.RangePerception))
|
||||||
{
|
{
|
||||||
if (!Mobile.CanSee(aggr) || aggr.IsDeadBondedPet || !aggr.Alive)
|
if (aggr == best || aggr == Mobile || aggr == controlMaster ||
|
||||||
|
aggr.IsDeadBondedPet || !aggr.Alive ||
|
||||||
|
aggr.Combatant != Mobile && (controlMaster == null || aggr.Combatant != controlMaster))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var isAttackingPet = aggr.Combatant == Mobile;
|
var dist = aggr.GetDistanceToSqrt(anchor);
|
||||||
var isAttackingMaster = controlMaster != null && aggr.Combatant == controlMaster;
|
|
||||||
|
|
||||||
if (isAttackingPet || isAttackingMaster)
|
if (dist < bestDist && Mobile.CanSee(aggr) && Mobile.InLOS(aggr))
|
||||||
{
|
{
|
||||||
if (Mobile.InLOS(aggr))
|
best = aggr;
|
||||||
{
|
bestDist = dist;
|
||||||
Mobile.ControlTarget = aggr;
|
|
||||||
Mobile.ControlOrder = OrderType.Attack;
|
|
||||||
Mobile.Combatant = aggr;
|
|
||||||
|
|
||||||
var target = isAttackingMaster ? "master" : "me";
|
|
||||||
this.DebugSayFormatted($"{aggr.Name} is attacking my {target}! Engaging...");
|
|
||||||
|
|
||||||
Think();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (controlMaster?.Aggressors != null)
|
var aggressors = controlMaster?.Aggressors;
|
||||||
{
|
|
||||||
for (var i = 0; i < controlMaster.Aggressors.Count; i++)
|
|
||||||
{
|
|
||||||
var aggressor = controlMaster.Aggressors[i].Attacker;
|
|
||||||
|
|
||||||
if (aggressor?.Deleted != false || !aggressor.Alive || aggressor.IsDeadBondedPet)
|
if (aggressors != null)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < aggressors.Count; i++)
|
||||||
|
{
|
||||||
|
var aggressor = aggressors[i].Attacker;
|
||||||
|
|
||||||
|
if (aggressor == best || aggressor?.Deleted != false || !aggressor.Alive ||
|
||||||
|
aggressor.IsDeadBondedPet || !Mobile.InRange(aggressor, Mobile.RangePerception))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Mobile.InRange(aggressor, Mobile.RangePerception) && Mobile.CanSee(aggressor) && Mobile.InLOS(aggressor))
|
var dist = aggressor.GetDistanceToSqrt(anchor);
|
||||||
|
|
||||||
|
if (dist < bestDist && Mobile.CanSee(aggressor) && Mobile.InLOS(aggressor))
|
||||||
{
|
{
|
||||||
Mobile.ControlTarget = aggressor;
|
best = aggressor;
|
||||||
Mobile.ControlOrder = OrderType.Attack;
|
bestDist = dist;
|
||||||
Mobile.Combatant = aggressor;
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.DebugSayFormatted($"{aggressor.Name} recently attacked my master! Retaliating...");
|
return best;
|
||||||
|
|
||||||
Think();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual bool DoOrderRelease()
|
public virtual bool DoOrderRelease()
|
||||||
|
|
|
||||||
|
|
@ -765,11 +765,15 @@ namespace Server.Mobiles
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obedience is never slowed by the wild-creature move table; combat
|
// Obedience is never slowed by the wild-creature move table; combat
|
||||||
// chases (combatant set) keep it.
|
// chases (combatant set) keep it. A guarding pet returns to its master
|
||||||
|
// at the follow sprint pace (RunUO guard parity) with its think cadence
|
||||||
|
// untouched.
|
||||||
if (Controlled && Combatant == null &&
|
if (Controlled && Combatant == null &&
|
||||||
ControlOrder is OrderType.Come or OrderType.Follow or OrderType.Guard)
|
ControlOrder is OrderType.Come or OrderType.Follow or OrderType.Guard)
|
||||||
{
|
{
|
||||||
return _currentSpeed;
|
return Core.AOS && ControlOrder == OrderType.Guard
|
||||||
|
? Math.Min(_currentSpeed, 0.1)
|
||||||
|
: _currentSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _currentSpeed == _activeSpeed ? ActiveMoveSpeed
|
return _currentSpeed == _activeSpeed ? ActiveMoveSpeed
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue