ModernUO/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFollowTests.cs
Kamron Batman 28c5ea2f68
refactor: pet order speeds flow organically through the order handlers
Streamlines the two sprint mechanisms (the CurrentMoveSpeed guard carve-out
and DoMoveImpl's follow-master 0.1 write) into one RunUO-parity model:

- Order handlers own obedience speed, mirroring RunUO's OnCurrentOrderChanged
  and DoOrder* writes: issuing a movement order (Come/Follow/Guard/Attack)
  sets the active think clock, resting orders (Stay/None/Transfer) set
  passive, and the guard/follow peaceful branches write RunUO's AOS 0.1
  sprint (guard's else-branch had the identical `if (Core.AOS)
  CurrentSpeed = 0.1` as follow). Pre-AOS guard returns run active.
- CurrentMoveSpeed reverts to pure herding + classification — the bespoke
  0.1 fuses to both clocks through the existing rule, so the sprint needs no
  special case and the obedience branch is deleted.
- DoMoveImpl's per-step speed flip skips obeying pets (their handler owns the
  pace; per-step passive flips would fight it) and loses its 0.1 write.
  Combat still re-derives organically via warmode/combatant.

Net pacing (Medium bucket): guard/follow AOS returns sprint 0.1 fused (RunUO
parity, guard was previously move-clock-only), Come and friend-follow pace at
activeMove (0.45, ~= the pre-#2591 feel), and the stale-Warmode active/
passive lottery is gone everywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 09:43:02 -07:00

99 lines
3.2 KiB
C#

using System.Collections.Generic;
using Server;
using Server.Mobiles;
using Xunit;
namespace UOContent.Tests.Mobiles.AI;
// Guard-order following drives real movement primitives (and may pathfind), so it shares
// the pathfinding sequential collection like ApproachTargetTests.
[Collection("Sequential Pathfinding Tests")]
public class GuardFollowTests
{
[Fact]
public void GuardFollow_StepsTowardMaster_AndRegistersMoveIntent()
{
var map = Map.Maps[1];
Assert.NotNull(map);
map.GetAverageZ(1500, 1600, out _, out var z, out _);
var master = new PlayerMobile(World.NewMobile);
master.DefaultMobileInit();
master.MoveToWorld(new Point3D(1494, 1600, (sbyte)z), map);
var pet = new PetTestStub();
pet.MoveToWorld(new Point3D(1500, 1600, (sbyte)z), map); // 6 tiles east, open terrain
pet.SetControlMaster(master);
var ai = pet.AIObject;
ai.AITimer?.Stop(); // drive manually
pet.ControlOrder = OrderType.Guard;
ai.AITimer?.Stop(); // the order change may restart the timer
var start = pet.Location;
ai.NextMove = 0;
ai.Obey();
var moved = pet.Location != start;
var hasIntent = ai.TryGetMoveWake(out _);
var currentSpeed = pet.CurrentSpeed;
var currentMoveSpeed = pet.CurrentMoveSpeed;
pet.Delete();
master.Delete();
Assert.True(moved, "a guarding pet beyond guard range must step toward its master");
// Between-think move wakes require a registered move intent; bare greedy stepping
// quantizes guard-following to the think grid (issue #2593).
Assert.True(hasIntent, "guard-following must register a move intent");
// RunUO AOS parity: the guard return sprints at the bespoke 0.1, fused to both
// clocks, and the per-step speed flip must not undo it (fixture era is EJ).
Assert.Equal(0.1, currentSpeed);
Assert.Equal(0.1, currentMoveSpeed);
}
[Fact]
public void GuardReturn_PreAOS_RunsActive()
{
var previous = Core.Expansion;
try
{
Core.Expansion = Expansion.UOR;
var map = Map.Maps[1];
Assert.NotNull(map);
map.GetAverageZ(1500, 1600, out _, out var z, out _);
var master = new PlayerMobile(World.NewMobile);
master.DefaultMobileInit();
master.MoveToWorld(new Point3D(1494, 1600, (sbyte)z), map);
var pet = new PetTestStub();
pet.MoveToWorld(new Point3D(1500, 1600, (sbyte)z), map);
pet.SetControlMaster(master);
var ai = pet.AIObject;
ai.AITimer?.Stop();
pet.ControlOrder = OrderType.Guard;
ai.AITimer?.Stop();
pet.SetCurrentSpeedToPassive(); // a stale passive state must not persist
ai.NextMove = 0;
ai.Obey();
var currentSpeed = pet.CurrentSpeed;
pet.Delete();
master.Delete();
// No sprint pre-AOS: the return runs organically active.
Assert.Equal(0.2, currentSpeed);
}
finally
{
Core.Expansion = previous;
}
}
}