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 <noreply@anthropic.com>
This commit is contained in:
Kamron Batman 2026-08-23 10:08:16 -07:00
parent 41ae2d8ffe
commit 5429465d64
No known key found for this signature in database
GPG key ID: 7D81DF26D9A5D94A
3 changed files with 41 additions and 19 deletions

View file

@ -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()
{

View file

@ -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;
}

View file

@ -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
/// <summary>
/// Resolved seconds per step: a verbatim active/passive <see cref="CurrentSpeed"/>
/// maps to the matching movement value; a bespoke pace stays fused to both clocks.
/// A herded creature is always driven at <see cref="HerdingMoveSpeed"/>.
/// </summary>
[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;
}
}