diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/MoveSpeedTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/MoveSpeedTests.cs index 7d9243a29..7ad149eba 100644 --- a/Projects/UOContent.Tests/Tests/Mobiles/AI/MoveSpeedTests.cs +++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/MoveSpeedTests.cs @@ -57,8 +57,9 @@ public class MoveSpeedTests : IDisposable { var bc = NewCreature(); - Assert.Equal(0.3, bc.ActiveMoveSpeed); - Assert.Equal(0.6, bc.PassiveMoveSpeed); + // 0 = no override; the resolved pace comes from CurrentMoveSpeed. + Assert.Equal(0, bc.ActiveMoveSpeed); + Assert.Equal(0, bc.PassiveMoveSpeed); Assert.Equal(bc.CurrentSpeed, bc.CurrentMoveSpeed); } @@ -96,8 +97,8 @@ public class MoveSpeedTests : IDisposable bc.SetSpeed(0.2, 0.4); - Assert.Equal(0.2, bc.ActiveMoveSpeed); - Assert.Equal(0.4, bc.PassiveMoveSpeed); + Assert.Equal(0, bc.ActiveMoveSpeed); + Assert.Equal(0, bc.PassiveMoveSpeed); } [Fact] @@ -108,8 +109,10 @@ public class MoveSpeedTests : IDisposable bc.ActiveMoveSpeed = 0; - Assert.Equal(0.3, bc.ActiveMoveSpeed); // inheriting again + Assert.Equal(0, bc.ActiveMoveSpeed); // inheriting again Assert.Equal(0.9, bc.PassiveMoveSpeed); // other override untouched + bc.SetCurrentSpeedToActive(); + Assert.Equal(0.3, bc.CurrentMoveSpeed); // resolves to the think clock } [Fact] @@ -121,7 +124,7 @@ public class MoveSpeedTests : IDisposable bc.ScaleMoveSpeed(1.0 / 1.2); Assert.Equal(0.5, bc.ActiveMoveSpeed); - Assert.Equal(bc.PassiveSpeed, bc.PassiveMoveSpeed); // still inheriting, not 0 * scalar + Assert.Equal(0, bc.PassiveMoveSpeed); // still inheriting, not 0 * scalar } [Fact] @@ -185,8 +188,8 @@ public class MoveSpeedTests : IDisposable bc.MigrateMoveSpeeds(); - Assert.Equal(0.35, bc.ActiveMoveSpeed); - Assert.Equal(0.6, bc.PassiveMoveSpeed); + Assert.Equal(0, bc.ActiveMoveSpeed); // still inheriting the (tuned) think clock + Assert.Equal(0, bc.PassiveMoveSpeed); } [Theory] @@ -213,7 +216,7 @@ public class MoveSpeedTests : IDisposable // The v22 tail is the last block; exact consumption catches any offset mistake. Assert.Equal(buffer.Length, reader.Position); - Assert.Equal(overridden ? 0.45 : 0.3, copy.ActiveMoveSpeed); - Assert.Equal(overridden ? 0.9 : 0.6, copy.PassiveMoveSpeed); + Assert.Equal(overridden ? 0.45 : 0, copy.ActiveMoveSpeed); + Assert.Equal(overridden ? 0.9 : 0, copy.PassiveMoveSpeed); } } diff --git a/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs index 7425cdab7..6a60f12d3 100644 --- a/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs +++ b/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs @@ -269,7 +269,8 @@ public class BaseCreatureSerializationTests : IDisposable Assert.Equal(10, copy.FireResistSeed); Assert.Equal(0.3, copy.ActiveSpeed); // v22 wrote explicit zeros for the move overrides ("inherit"), so the resolved - // pace falls back to the think clock through the resolving getters. - Assert.Equal(0.3, copy.ActiveMoveSpeed); + // pace falls back to the think clock. + Assert.Equal(0, copy.ActiveMoveSpeed); + Assert.Equal(0.6, copy.CurrentMoveSpeed); // passive mode, inheriting } } diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index 3953916fc..88a03a8a7 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -363,11 +363,30 @@ namespace Server.Mobiles private void OnCurrentSpeedChange(double oldValue, double newValue) => AIObject?.OnCurrentSpeedChanged(); - // Movement clock (seconds per step); 0 = inherit the matching think value. - // Serialized through the hand-written resolving properties (fields 10 and 11). + /// + /// Movement clock (seconds per step) while engaged; 0 = inherit + /// . resolves the pace. + /// + [SerializableField(10, allowFieldChange: nameof(CoerceMoveSpeed))] + [SaveFlag(nameof(ShouldSerializeActiveMoveSpeed), nameof(ActiveMoveSpeedDefaultValue))] + [SerializedCommandProperty(AccessLevel.GameMaster)] private double _activeMoveSpeed; + + /// + /// Movement clock (seconds per step) while idle; 0 = inherit + /// . resolves the pace. + /// + [SerializableField(11, allowFieldChange: nameof(CoerceMoveSpeed))] + [SaveFlag(nameof(ShouldSerializePassiveMoveSpeed), nameof(PassiveMoveSpeedDefaultValue))] + [SerializedCommandProperty(AccessLevel.GameMaster)] private double _passiveMoveSpeed; + private bool CoerceMoveSpeed(ref double value) + { + value = Math.Max(0, value); // anything non-positive means "inherit" + return true; + } + private bool ShouldSerializeActiveMoveSpeed() { GetMoveSpeeds(out var activeMoveSpeed, out _); @@ -1075,34 +1094,6 @@ namespace Server.Mobiles [CommandProperty(AccessLevel.GameMaster)] public virtual int ChaseLeashRange => RangePerception * 2; - /// Seconds per step while engaged. Inherits ; set 0 to re-inherit. - [SerializableProperty(10, useField: nameof(_activeMoveSpeed))] - [SaveFlag(nameof(ShouldSerializeActiveMoveSpeed), nameof(ActiveMoveSpeedDefaultValue))] - [CommandProperty(AccessLevel.GameMaster)] - public virtual double ActiveMoveSpeed - { - get => _activeMoveSpeed > 0 ? _activeMoveSpeed : _activeSpeed; - set - { - _activeMoveSpeed = value > 0 ? value : 0; - this.MarkDirty(); - } - } - - /// Seconds per step while idle. Inherits ; set 0 to re-inherit. - [SerializableProperty(11, useField: nameof(_passiveMoveSpeed))] - [SaveFlag(nameof(ShouldSerializePassiveMoveSpeed), nameof(PassiveMoveSpeedDefaultValue))] - [CommandProperty(AccessLevel.GameMaster)] - public virtual double PassiveMoveSpeed - { - get => _passiveMoveSpeed > 0 ? _passiveMoveSpeed : _passiveSpeed; - set - { - _passiveMoveSpeed = value > 0 ? value : 0; - this.MarkDirty(); - } - } - // 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; @@ -1129,9 +1120,17 @@ namespace Server.Mobiles return HerdingMoveSpeed; } - return _currentSpeed == _activeSpeed ? ActiveMoveSpeed - : _currentSpeed == _passiveSpeed ? PassiveMoveSpeed - : _currentSpeed; + if (_currentSpeed == _activeSpeed) + { + return _activeMoveSpeed > 0 ? _activeMoveSpeed : _activeSpeed; + } + + if (_currentSpeed == _passiveSpeed) + { + return _passiveMoveSpeed > 0 ? _passiveMoveSpeed : _passiveSpeed; + } + + return _currentSpeed; } } diff --git a/dev-docs/content-patterns.md b/dev-docs/content-patterns.md index ec56e1bb3..2c78fcd67 100644 --- a/dev-docs/content-patterns.md +++ b/dev-docs/content-patterns.md @@ -262,8 +262,9 @@ All "speed" values are **delays in seconds** (smaller = faster). A creature runs (combat decisions, target acquisition, spell timing). - **Move clock** — `ActiveMoveSpeed`/`PassiveMoveSpeed`/`CurrentMoveSpeed`: seconds per step. Inherits the matching think value until overridden, so a creature configured with - only think speeds behaves as one clock. Any value is legal — steps are scheduled - independently of think ticks, so the two need not divide evenly. + only think speeds behaves as one clock. The properties read the raw override (`0` = + inheriting); `CurrentMoveSpeed` is the resolved pace. Any value is legal — steps are + scheduled independently of think ticks, so the two need not divide evenly. Speeds normally come from `Distribution/Data/npc-speeds.json` (via `SpeedClass` or type lists); `activeMove`/`passiveMove` are optional per bucket. Prefer data over code: