From 5429465d64f90c945f1279f4579ea98047f8c1d3 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 23 Aug 2026 10:08:16 -0700 Subject: [PATCH] feat: herding paces the movement clock, not the think clock Herding is a movement scenario: the old CurrentSpeed getter hack forced 0.3 on the fused clock, which under the split misclassified - a herded mob whose active think happens to be 0.3 walked at its (slow) move value while any other mob got a bespoke 0.3. Now CurrentMoveSpeed drives a herded creature at a fixed 0.3s per step - RunUO's forced pace, without its TransformMoveDelay inflation to 0.6 - so herding is never penalized by a slow creature. Thinking is untouched. CheckHerding walks through MoveToPoint (cached boxed goal) instead of a greedy per-think DoMove, so the movement clock actually governs pace between think ticks - and herded creatures path around obstacles instead of walking into them. Co-Authored-By: Claude Fable 5 --- .../Tests/Mobiles/AI/MoveSpeedTests.cs | 15 +++++++++ .../UOContent/Mobiles/AI/BaseAI/BaseAI.cs | 13 +++++++- Projects/UOContent/Mobiles/BaseCreature.cs | 32 ++++++++----------- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/MoveSpeedTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/MoveSpeedTests.cs index f22383cf3..7d9243a29 100644 --- a/Projects/UOContent.Tests/Tests/Mobiles/AI/MoveSpeedTests.cs +++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/MoveSpeedTests.cs @@ -124,6 +124,21 @@ public class MoveSpeedTests : IDisposable Assert.Equal(bc.PassiveSpeed, bc.PassiveMoveSpeed); // still inheriting, not 0 * scalar } + [Fact] + public void Herding_DrivesMoveClock_ThinkUntouched() + { + var bc = NewCreature(); // think 0.3/0.6, passive + bc.SetMoveSpeed(0.45, 1.05); + + bc.TargetLocation = new Point2D(10, 10); + + Assert.Equal(0.6, bc.CurrentSpeed); // think clock unaffected by herding + Assert.Equal(0.3, bc.CurrentMoveSpeed); // fixed herding pace, not 1.05 + + bc.TargetLocation = null; + Assert.Equal(1.05, bc.CurrentMoveSpeed); + } + [Fact] public void SnapSpeedsToTable_UndoesScalingDrift_KeepsTunedValues() { diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs b/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs index 57e96b3fd..cf64cae17 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs @@ -40,6 +40,7 @@ public abstract partial class BaseAI private Mobile _lkpTarget; private Point3D _lkpLocation; private IPoint3D _lkpGoal; // boxed _lkpLocation handed to the PathFollower + private IPoint3D _herdGoal; // boxed herding goal handed to the PathFollower private long _lkpExpireTick; private long _guardStopTick; private long _investigateStopTick; @@ -627,6 +628,7 @@ public abstract partial class BaseAI if (target == null) { + _herdGoal = null; return false; } @@ -634,7 +636,15 @@ public abstract partial class BaseAI if (distance >= 1 && distance <= 15) { - DoMove(Mobile.GetDirectionTo(target)); + // A cached boxed goal keeps the PathFollower persistent across ticks; walking + // through MoveToPoint paces herding on the movement clock and paths around + // obstacles. + if (_herdGoal == null || _herdGoal.X != target.X || _herdGoal.Y != target.Y) + { + _herdGoal = new Point3D(target.X, target.Y, Mobile.Map?.GetAverageZ(target.X, target.Y) ?? Mobile.Z); + } + + MoveToPoint(_herdGoal, false); return true; } @@ -644,6 +654,7 @@ public abstract partial class BaseAI } Mobile.TargetLocation = null; + _herdGoal = null; return false; } diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index 016fcaa27..f64a289f4 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -269,8 +269,8 @@ namespace Server.Mobiles private double _activeMoveSpeed; private double _passiveMoveSpeed; - // Herding - Overrides the AI to force the mob to move to a specific location - // Thinking: 0.3s, Movement: 0.6s. + // Herding - forces the mob to walk to a specific location, paced by the movement + // clock at HerdingMoveSpeed. Thinking is unaffected. private IPoint2D _targetLocation; private int m_DamageMax = -1; @@ -723,21 +723,21 @@ namespace Server.Mobiles set => _passiveMoveSpeed = value > 0 ? value : 0; } + // Herded creatures walk at a fixed standard pace regardless of their own speed + // (RunUO's forced 0.3, without its TransformMoveDelay inflation to 0.6). + private const double HerdingMoveSpeed = 0.3; + [CommandProperty(AccessLevel.GameMaster)] public IPoint2D TargetLocation { get => _targetLocation; - set - { - _targetLocation = value; - AIObject?.OnCurrentSpeedChanged(); - } + set => _targetLocation = value; } [CommandProperty(AccessLevel.GameMaster)] public double CurrentSpeed { - get => _targetLocation != null ? 0.3 : _currentSpeed; + get => _currentSpeed; set { if (Math.Abs(_currentSpeed - value) > 0.0001) @@ -751,25 +751,21 @@ namespace Server.Mobiles /// /// Resolved seconds per step: a verbatim active/passive /// maps to the matching movement value; a bespoke pace stays fused to both clocks. + /// A herded creature is always driven at . /// [CommandProperty(AccessLevel.GameMaster)] public double CurrentMoveSpeed { get { - var current = CurrentSpeed; - - if (current == _activeSpeed) + if (_targetLocation != null) { - return ActiveMoveSpeed; + return HerdingMoveSpeed; } - if (current == _passiveSpeed) - { - return PassiveMoveSpeed; - } - - return current; + return _currentSpeed == _activeSpeed ? ActiveMoveSpeed + : _currentSpeed == _passiveSpeed ? PassiveMoveSpeed + : _currentSpeed; } }