From 3332fabc39c5e95c00a6bdf73eb8d7033c2d6113 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Mon, 18 Mar 2024 14:13:41 -0700 Subject: [PATCH] fix: Reverts to RunUO speeds, fixes direction glitching, adds movement speed interpolation (#1695) > [!IMPORTANT] > Please read through these changes, as they changed certain expectations for how the internal AI thinking/movement work. > Note that some mobs still don't have smooth movement on ClassicUO due to how the client handles animations/movement. ### Summary - **Important Change**: Mobs will now move at a more regular pace. If the OnThink results in a move, but the cooldown would otherwise prevent the move, then the movement is scheduled on another timer. - Added a 400ms delay to mobs turning to face a player to attack in order to avoid glitching between moving and turning. - Reverted AI thinking speeds back to RunUO specific speeds. - Reverted the AI thinking to moving conversion delays back to RunUO. - For thinking speeds that are not exactly the predefined speeds from RunUO, there is a new calculation to determine the correct conversion to movement speed. This stops speeds like `0.35` from being faster than `0.3` > [!NOTE] > **Developer Note** > BaseAI.CheckMove() no longer contains the check for whether or not a mob is on movement cooldown. This function can now be overwritten without causing issues to figuring out that cooldown. --- Distribution/Data/npc-speeds.json | 16 +- Projects/Server/Mobiles/Mobile.cs | 24 +- .../Factions/Mobiles/Guards/GuardAI.cs | 12 +- .../ML Quests/Objectives/EscortObjective.cs | 16 +- .../UOContent/Engines/Pathing/MoveResult.cs | 2 +- .../UOContent/Engines/Pathing/PathFollower.cs | 14 +- .../UOContent/Items/Weapons/BaseWeapon.cs | 2 + Projects/UOContent/Mobiles/AI/AnimalAI.cs | 5 +- Projects/UOContent/Mobiles/AI/ArcherAI.cs | 10 +- Projects/UOContent/Mobiles/AI/BaseAI.cs | 335 +++++++++++------- Projects/UOContent/Mobiles/AI/BerserkAI.cs | 5 +- Projects/UOContent/Mobiles/AI/MageAI.cs | 6 +- Projects/UOContent/Mobiles/AI/MeleeAI.cs | 18 +- Projects/UOContent/Mobiles/BaseCreature.cs | 84 +++-- .../Mobiles/Familiars/BaseFamiliar.cs | 2 +- .../UOContent/Mobiles/Healers/BaseHealer.cs | 6 +- Projects/UOContent/Mobiles/NPCSpeeds.cs | 5 +- Projects/UOContent/Mobiles/Special/Neira.cs | 3 + .../Mobiles/Townfolk/BaseEscortable.cs | 10 +- .../UOContent/Spells/Ninjitsu/MirrorImage.cs | 2 +- 20 files changed, 354 insertions(+), 223 deletions(-) diff --git a/Distribution/Data/npc-speeds.json b/Distribution/Data/npc-speeds.json index af77f1826..6e07859d5 100644 --- a/Distribution/Data/npc-speeds.json +++ b/Distribution/Data/npc-speeds.json @@ -1,8 +1,14 @@ [ + { + "level": "VerySlow", + "active": 0.4, + "passive": 0.8, + "types": [] + }, { "level": "Slow", - "active": 0.35, - "passive": 0.5, + "active": 0.3, + "passive": 0.6, "types": [ "AntLion", "ArcticOgreLord", "BogThing", "Bogle", "BoneKnight", "EarthElemental", @@ -20,8 +26,8 @@ }, { "level": "Medium", - "active": 0.275, - "passive": 0.45, + "active": 0.25, + "passive": 0.5, "types": [ "AcidElemental", "AgapiteElemental", "Alligator", "AncientLich", "Betrayer", "Bird", @@ -132,7 +138,7 @@ { "level": "VeryFast", "active": 0.125, - "passive": 0.3, + "passive": 0.30, "types": [ "Barracoon", "Mephitis", "Neira", "Rikktor", "Semidar", "EnergyVortex", diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index 8bd8ef926..5f373105c 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -4339,10 +4339,9 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro if (m_NetState != null) { m_NetState._nextMovementTime += ComputeMovementSpeed(d); + m_NetState.SendMovementAck(m_NetState.Sequence, this); } - m_NetState?.SendMovementAck(m_NetState.Sequence, this); - SetLocation(newLocation, false); SetDirection(d); @@ -7282,12 +7281,15 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro box.Close(); } - m_NetState?.ValidateAllTrades(); - - if (isTeleport && m_NetState != null && (!m_NetState.HighSeas || !NoMoveHS)) + if (m_NetState != null) { - m_NetState.Sequence = 0; - m_NetState.SendMobileUpdate(this); + m_NetState.ValidateAllTrades(); + + if (isTeleport && (!m_NetState.HighSeas || !NoMoveHS)) + { + m_NetState.Sequence = 0; + m_NetState.SendMobileUpdate(this); + } } var map = m_Map; @@ -7367,6 +7369,7 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro m.SendOPLPacketTo(ourState); } + foreach (var item in map.GetItemsInRange(newLocation, Core.GlobalMaxUpdateRange)) { var range = item.GetUpdateRange(this); @@ -8916,7 +8919,12 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro }; } - return ret | (run ? Direction.Running : 0); + if (run) + { + ret |= Direction.Running; + } + + return ret; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Projects/UOContent/Engines/Factions/Mobiles/Guards/GuardAI.cs b/Projects/UOContent/Engines/Factions/Mobiles/Guards/GuardAI.cs index d13a1bda1..83bc0150a 100644 --- a/Projects/UOContent/Engines/Factions/Mobiles/Guards/GuardAI.cs +++ b/Projects/UOContent/Engines/Factions/Mobiles/Guards/GuardAI.cs @@ -545,11 +545,7 @@ namespace Server.Factions Action = ActionType.Combat; } - if (m_Mobile.CurrentSpeed != m_Mobile.ActiveSpeed) - { - m_Mobile.CurrentSpeed = m_Mobile.ActiveSpeed; - } - + m_Mobile.SetCurrentSpeedToActive(); m_Guard.Warmode = true; RunTo(toFollow); @@ -561,11 +557,7 @@ namespace Server.Factions Action = ActionType.Wander; } - if (m_Mobile.CurrentSpeed != m_Mobile.PassiveSpeed) - { - m_Mobile.CurrentSpeed = m_Mobile.PassiveSpeed; - } - + m_Mobile.SetCurrentSpeedToPassive(); m_Guard.Warmode = false; WalkRandomInHome(2, 2, 1); diff --git a/Projects/UOContent/Engines/ML Quests/Objectives/EscortObjective.cs b/Projects/UOContent/Engines/ML Quests/Objectives/EscortObjective.cs index f33e9137c..a2a105fbe 100644 --- a/Projects/UOContent/Engines/ML Quests/Objectives/EscortObjective.cs +++ b/Projects/UOContent/Engines/ML Quests/Objectives/EscortObjective.cs @@ -186,35 +186,29 @@ namespace Server.Engines.MLQuests.Objectives quester.ControlSlots = 0; quester.SetControlMaster(pm); - quester.ActiveSpeed = 0.1; - quester.PassiveSpeed = 0.2; - quester.ControlOrder = OrderType.Follow; quester.ControlTarget = pm; quester.CantWalk = false; - quester.CurrentSpeed = 0.1; + + quester.SetSpeed(0.1, 0.2, false); } public static void EndFollow(BaseCreature quester) { - quester.ActiveSpeed = 0.2; - quester.PassiveSpeed = 1.0; - quester.ControlOrder = OrderType.None; quester.ControlTarget = null; - quester.CurrentSpeed = 1.0; - quester.SetControlMaster(null); + quester.SetSpeed(0.1, 0.2); + (quester as BaseEscortable)?.BeginDelete(); } public override void OnQuestAccepted() { - var instance = Instance; - var pm = instance.Player; + var pm = Instance.Player; pm.LastEscortTime = Core.Now; diff --git a/Projects/UOContent/Engines/Pathing/MoveResult.cs b/Projects/UOContent/Engines/Pathing/MoveResult.cs index 643f4f19b..cd9b91ddd 100644 --- a/Projects/UOContent/Engines/Pathing/MoveResult.cs +++ b/Projects/UOContent/Engines/Pathing/MoveResult.cs @@ -1,6 +1,6 @@ namespace Server { - public delegate MoveResult MoveMethod(Direction d); + public delegate MoveResult MoveMethod(Direction d, bool badStateOk); public enum MoveResult { diff --git a/Projects/UOContent/Engines/Pathing/PathFollower.cs b/Projects/UOContent/Engines/Pathing/PathFollower.cs index 20a285a2e..1bce5e6d8 100644 --- a/Projects/UOContent/Engines/Pathing/PathFollower.cs +++ b/Projects/UOContent/Engines/Pathing/PathFollower.cs @@ -30,7 +30,7 @@ namespace Server } public MoveResult Move(Direction d) => - Mover?.Invoke(d) ?? (m_From.Move(d) ? MoveResult.Success : MoveResult.Blocked); + Mover?.Invoke(d, true) ?? (m_From.Move(d) ? MoveResult.Success : MoveResult.Blocked); public Point3D GetGoalLocation() => (Goal as Item)?.GetWorldLocation() ?? new Point3D(Goal); @@ -102,14 +102,14 @@ namespace Server if (!(Enabled && m_Path.Success)) { - d = m_From.GetDirectionTo(goal); + d = m_From.GetDirectionTo(goal, run); m_From.SetDirection(d); - Move(d); - return Check(m_From.Location, goal, range); + return Move(d) is MoveResult.Success or MoveResult.SuccessAutoTurn + && Check(m_From.Location, goal, range); } - d = m_From.GetDirectionTo(m_Next); + d = m_From.GetDirectionTo(m_Next, run); m_From.SetDirection(d); var res = Move(d); @@ -127,9 +127,9 @@ namespace Server { d = m_From.GetDirectionTo(goal); m_From.SetDirection(d); - Move(d); - return Check(m_From.Location, goal, range); + return Move(d) is MoveResult.Success or MoveResult.SuccessAutoTurn + && Check(m_From.Location, goal, range); } d = m_From.GetDirectionTo(m_Next); diff --git a/Projects/UOContent/Items/Weapons/BaseWeapon.cs b/Projects/UOContent/Items/Weapons/BaseWeapon.cs index 13232f808..dbd9a6b8f 100644 --- a/Projects/UOContent/Items/Weapons/BaseWeapon.cs +++ b/Projects/UOContent/Items/Weapons/BaseWeapon.cs @@ -888,6 +888,8 @@ public abstract partial class BaseWeapon : Item, IWeapon, IFactionItem, ICraftab if (attacker is BaseCreature bc) { + // Only change direction if they are not a player. + attacker.Direction = attacker.GetDirectionTo(defender); var ab = bc.GetWeaponAbility(); if (ab != null) diff --git a/Projects/UOContent/Mobiles/AI/AnimalAI.cs b/Projects/UOContent/Mobiles/AI/AnimalAI.cs index dbfceda60..8e3da75df 100644 --- a/Projects/UOContent/Mobiles/AI/AnimalAI.cs +++ b/Projects/UOContent/Mobiles/AI/AnimalAI.cs @@ -82,6 +82,10 @@ public class AnimalAI : BaseAI m_Mobile.DebugSay($"I should be closer to {combatant.Name}"); } } + else if (Core.TickCount - m_Mobile.LastMoveTime > 400) + { + m_Mobile.Direction = m_Mobile.GetDirectionTo(combatant); + } if (!m_Mobile.Controlled && !m_Mobile.Summoned && m_Mobile.CanFlee) { @@ -99,7 +103,6 @@ public class AnimalAI : BaseAI } } - m_Mobile.Direction = m_Mobile.GetDirectionTo(combatant); if (m_Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, combatant)) { if (m_Mobile.Debug) diff --git a/Projects/UOContent/Mobiles/AI/ArcherAI.cs b/Projects/UOContent/Mobiles/AI/ArcherAI.cs index 304799cab..ad6788952 100644 --- a/Projects/UOContent/Mobiles/AI/ArcherAI.cs +++ b/Projects/UOContent/Mobiles/AI/ArcherAI.cs @@ -50,9 +50,8 @@ public class ArcherAI : BaseAI return true; } - if (Core.TickCount - m_Mobile.LastMoveTime > 1000 && - !WalkMobileRange( - m_Mobile.Combatant, + if (Core.TickCount - m_Mobile.LastMoveTime > 1000 && !WalkMobileRange( + combatant, 1, true, m_Mobile.RangeFight, @@ -76,8 +75,11 @@ public class ArcherAI : BaseAI return true; } } + else if (Core.TickCount - m_Mobile.LastMoveTime > 400) + { + m_Mobile.Direction = m_Mobile.GetDirectionTo(combatant); + } - m_Mobile.Direction = m_Mobile.GetDirectionTo(combatant); if (m_Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, combatant)) { if (m_Mobile.Debug) diff --git a/Projects/UOContent/Mobiles/AI/BaseAI.cs b/Projects/UOContent/Mobiles/AI/BaseAI.cs index ab077f724..530e5e70d 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI.cs @@ -42,6 +42,8 @@ public enum ActionType public abstract class BaseAI { + // How many milliseconds until our next move can we consider it ok to move without deferring/blocking. + private const int FuzzyTimeUntilNextMove = 24; private static readonly SkillName[] m_KeywordTable = { SkillName.Parry, @@ -96,7 +98,7 @@ public abstract class BaseAI protected ActionType m_Action; - public BaseCreature m_Mobile; + public readonly BaseCreature m_Mobile; private long m_NextDetectHidden; private long m_NextStopGuard; @@ -869,7 +871,7 @@ public abstract class BaseAI m_Mobile.Warmode = false; m_Mobile.Combatant = null; m_Mobile.FocusMob = null; - m_Mobile.CurrentSpeed = m_Mobile.PassiveSpeed; + m_Mobile.SetCurrentSpeedToPassive(); break; } @@ -877,7 +879,7 @@ public abstract class BaseAI { m_Mobile.Warmode = true; m_Mobile.FocusMob = null; - m_Mobile.CurrentSpeed = m_Mobile.ActiveSpeed; + m_Mobile.SetCurrentSpeedToActive(); break; } @@ -886,9 +888,8 @@ public abstract class BaseAI m_Mobile.Warmode = true; m_Mobile.FocusMob = null; m_Mobile.Combatant = null; - m_Mobile.CurrentSpeed = m_Mobile.ActiveSpeed; m_NextStopGuard = Core.TickCount + (int)TimeSpan.FromSeconds(10).TotalMilliseconds; - m_Mobile.CurrentSpeed = m_Mobile.ActiveSpeed; + m_Mobile.SetCurrentSpeedToActive(); break; } @@ -896,21 +897,21 @@ public abstract class BaseAI { m_Mobile.Warmode = true; m_Mobile.FocusMob = null; - m_Mobile.CurrentSpeed = m_Mobile.ActiveSpeed; + m_Mobile.SetCurrentSpeedToActive(); break; } case ActionType.Interact: { m_Mobile.Warmode = false; - m_Mobile.CurrentSpeed = m_Mobile.PassiveSpeed; + m_Mobile.SetCurrentSpeedToPassive(); break; } case ActionType.Backoff: { m_Mobile.Warmode = false; - m_Mobile.CurrentSpeed = m_Mobile.PassiveSpeed; + m_Mobile.SetCurrentSpeedToPassive(); break; } } @@ -968,12 +969,9 @@ public abstract class BaseAI WalkRandomInHome(2, 2, 1); } } - else if (CheckMove()) + else if (CheckMove() && CanMoveNow(out _) && !m_Mobile.CheckIdle()) { - if (!m_Mobile.CheckIdle()) - { - WalkRandomInHome(2, 2, 1); - } + WalkRandomInHome(2, 2, 1); } if (m_Mobile.Combatant?.Deleted == false && m_Mobile.Combatant.Alive && @@ -1123,7 +1121,7 @@ public abstract class BaseAI { m_Mobile.ControlMaster.RevealingAction(); m_Mobile.Home = m_Mobile.Location; - m_Mobile.CurrentSpeed = m_Mobile.PassiveSpeed; + m_Mobile.SetCurrentSpeedToPassive(); m_Mobile.PlaySound(m_Mobile.GetIdleSound()); m_Mobile.Warmode = false; m_Mobile.Combatant = null; @@ -1133,7 +1131,7 @@ public abstract class BaseAI case OrderType.Come: { m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.CurrentSpeed = m_Mobile.ActiveSpeed; + m_Mobile.SetCurrentSpeedToActive(); m_Mobile.PlaySound(m_Mobile.GetIdleSound()); m_Mobile.Warmode = false; m_Mobile.Combatant = null; @@ -1143,7 +1141,7 @@ public abstract class BaseAI case OrderType.Drop: { m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.CurrentSpeed = m_Mobile.PassiveSpeed; + m_Mobile.SetCurrentSpeedToPassive(); m_Mobile.PlaySound(m_Mobile.GetIdleSound()); m_Mobile.Warmode = false; m_Mobile.Combatant = null; @@ -1160,7 +1158,7 @@ public abstract class BaseAI case OrderType.Guard: { m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.CurrentSpeed = m_Mobile.ActiveSpeed; + m_Mobile.SetCurrentSpeedToActive(); m_Mobile.PlaySound(m_Mobile.GetIdleSound()); m_Mobile.Warmode = true; m_Mobile.Combatant = null; @@ -1171,7 +1169,7 @@ public abstract class BaseAI case OrderType.Attack: { m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.CurrentSpeed = m_Mobile.ActiveSpeed; + m_Mobile.SetCurrentSpeedToActive(); m_Mobile.PlaySound(m_Mobile.GetIdleSound()); m_Mobile.Warmode = true; @@ -1182,7 +1180,7 @@ public abstract class BaseAI case OrderType.Patrol: { m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.CurrentSpeed = m_Mobile.ActiveSpeed; + m_Mobile.SetCurrentSpeedToActive(); m_Mobile.PlaySound(m_Mobile.GetIdleSound()); m_Mobile.Warmode = false; m_Mobile.Combatant = null; @@ -1192,7 +1190,7 @@ public abstract class BaseAI case OrderType.Release: { m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.CurrentSpeed = m_Mobile.PassiveSpeed; + m_Mobile.SetCurrentSpeedToPassive(); m_Mobile.PlaySound(m_Mobile.GetIdleSound()); m_Mobile.Warmode = false; m_Mobile.Combatant = null; @@ -1202,7 +1200,7 @@ public abstract class BaseAI case OrderType.Stay: { m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.CurrentSpeed = m_Mobile.PassiveSpeed; + m_Mobile.SetCurrentSpeedToPassive(); m_Mobile.PlaySound(m_Mobile.GetIdleSound()); m_Mobile.Warmode = false; m_Mobile.Combatant = null; @@ -1213,7 +1211,7 @@ public abstract class BaseAI { m_Mobile.ControlMaster.RevealingAction(); m_Mobile.Home = m_Mobile.Location; - m_Mobile.CurrentSpeed = m_Mobile.PassiveSpeed; + m_Mobile.SetCurrentSpeedToPassive(); m_Mobile.PlaySound(m_Mobile.GetIdleSound()); m_Mobile.Warmode = false; m_Mobile.Combatant = null; @@ -1223,7 +1221,7 @@ public abstract class BaseAI case OrderType.Follow: { m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.CurrentSpeed = m_Mobile.ActiveSpeed; + m_Mobile.SetCurrentSpeedToActive(); m_Mobile.PlaySound(m_Mobile.GetIdleSound()); m_Mobile.Warmode = false; @@ -1234,7 +1232,7 @@ public abstract class BaseAI case OrderType.Transfer: { m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.CurrentSpeed = m_Mobile.PassiveSpeed; + m_Mobile.SetCurrentSpeedToPassive(); m_Mobile.PlaySound(m_Mobile.GetIdleSound()); m_Mobile.Warmode = false; @@ -1988,115 +1986,123 @@ public abstract class BaseAI return; } + if (iChanceToNotMove <= 0) + { + return; + } + for (var i = 0; i < iSteps; i++) { - if (Utility.Random(8 * iChanceToNotMove) <= 8) + if (Utility.Random(1 + iChanceToNotMove) == 0) { - var iRndMove = Utility.Random(0, 8 + 9 * iChanceToDir); - - switch (iRndMove) - { - case 0: - { - DoMove(Direction.Up); - break; - } - case 1: - { - DoMove(Direction.North); - break; - } - case 2: - { - DoMove(Direction.Left); - break; - } - case 3: - { - DoMove(Direction.West); - break; - } - case 5: - { - DoMove(Direction.Down); - break; - } - case 6: - { - DoMove(Direction.South); - break; - } - case 7: - { - DoMove(Direction.Right); - break; - } - case 8: - { - DoMove(Direction.East); - break; - } - default: - { - DoMove(m_Mobile.Direction); - break; - } - } + // iChanceToDir = 2, total weight is 18 + // 7/26 chance of other direction + var iRndMove = Utility.Random(8 * (iChanceToDir + 1)); + Direction direction = iRndMove < 8 ? (Direction)iRndMove : m_Mobile.Direction; + DoMove(direction); } } } - public double TransformMoveDelay(double thinkingSpeed) + public static double TransformMoveDelay(BaseCreature bc, bool isPassive = false) { - var isControlled = m_Mobile.Controlled || m_Mobile.Summoned; - - // Monster is passive - if (!isControlled && Math.Abs(thinkingSpeed - m_Mobile.PassiveSpeed) < 0.0001) + if (bc == null) { - thinkingSpeed *= 3; // Monster passive is 3x slower than thinking - } - else if (!isControlled || m_Mobile.ControlOrder != OrderType.Follow || m_Mobile.ControlTarget != m_Mobile.ControlMaster) - { - thinkingSpeed *= 2; // Monster active speed is 2x slower than thinking + return 0.1; } - if (!m_Mobile.IsDeadPet && (m_Mobile.ReduceSpeedWithDamage || m_Mobile.IsSubdued)) + if (bc.MoveSpeedMod > 0) + { + return bc.MoveSpeedMod; + } + + var moveSpeed = bc.CurrentSpeed; + var isControlled = bc.Controlled || bc.Summoned; + + /* + * Movement Speed Table based on RunUO + * <0.075 -> 0.0375 + * 0.075 -> 0.05 + * 0.1 -> 0.125 + * 0.2 -> 0.3 + * 0.25 -> 0.45 + * 0.3 -> 0.6 + * 0.4 -> 0.85 + * 0.5 -> 1.05 + * 0.6 -> 1.2, + * 0.8 -> 1.5 + * 1.0 -> 1.8 + * Above 1.0 is linear + */ + + // Linear interpolated + var movementSpeed = moveSpeed switch + { + >= 1.0 => 1.8 * moveSpeed, + >= 0.5 => 1.05 + (moveSpeed - 0.5) * 1.5, + >= 0.4 => 0.85 + (moveSpeed - 0.4) * 2, + >= 0.3 => 0.6 + (moveSpeed - 0.3) * 2.5, + >= 0.2 => 0.3 + (moveSpeed - 0.2) * 3, + >= 0.1 => 0.125 + (moveSpeed - 0.1) * 1.75, + >= 0.075 => 0.05 + (moveSpeed - 0.075) * 3, + _ => 0.0375 // 30 ticks, 37.5ms + }; + + if (isPassive) + { + movementSpeed += 0.2; + } + + if (!isControlled) + { + movementSpeed += 0.1; + } + else if (!bc.Summoned) + { + if (bc.ControlOrder == OrderType.Follow && bc.ControlTarget == bc.ControlMaster) + { + movementSpeed *= 0.5; + } + + movementSpeed -= 0.075; + } + + if (!bc.IsDeadPet && (bc.ReduceSpeedWithDamage || bc.IsSubdued)) { int stats, statsMax; if (Core.HS) { - stats = m_Mobile.Stam; - statsMax = m_Mobile.StamMax; + stats = bc.Stam; + statsMax = bc.StamMax; } else { - stats = m_Mobile.Hits; - statsMax = m_Mobile.HitsMax; + stats = bc.Hits; + statsMax = bc.HitsMax; } var offset = statsMax <= 0 ? 1.0 : Math.Max(0, stats) / (double)statsMax; - if (offset < 1.0) - { - thinkingSpeed += m_Mobile.PassiveSpeed * (1.0 - offset); - } + movementSpeed += (1.0 - offset) * 0.8; } - return thinkingSpeed; + return movementSpeed; } - public virtual bool CheckMove() => Core.TickCount - NextMove >= 0; + public bool CanMoveNow(out long delay) => (delay = NextMove - Core.TickCount) <= FuzzyTimeUntilNextMove; + + public virtual bool CheckMove() => true; public virtual bool DoMove(Direction d, bool badStateOk = false) { - var res = DoMoveImpl(d); + var res = DoMoveImpl(d, badStateOk); return res is MoveResult.Success or MoveResult.SuccessAutoTurn || badStateOk && res == MoveResult.BadState; } - public virtual MoveResult DoMoveImpl(Direction d) + public virtual MoveResult DoMoveImpl(Direction d, bool badStateOk) { - if (m_Mobile.Deleted || m_Mobile.Frozen || m_Mobile.Paralyzed || + if (m_Mobile == null || m_Mobile.Deleted || m_Mobile.Frozen || m_Mobile.Paralyzed || m_Mobile.Spell?.IsCasting == true || m_Mobile.DisallowAllMoves) { return MoveResult.BadState; @@ -2107,11 +2113,22 @@ public abstract class BaseAI return MoveResult.BadState; } + var delay = (int)(TransformMoveDelay(m_Mobile) * 1000); + + if (!CanMoveNow(out var timeUntilMove)) + { + // When bad state is ok, we can still move if the time until move is less than the fuzzy time + if (badStateOk) + { + AIMovementTimerPool.GetTimer(TimeSpan.FromMilliseconds(timeUntilMove), this, d).Start(); + } + + return MoveResult.BadState; + } + // This makes them always move one step, never any direction changes // TODO: This is firing off deltas which aren't needed. Look into replacing/removing this m_Mobile.Direction = d; - - var delay = (int)(TransformMoveDelay(m_Mobile.CurrentSpeed) * 1000); NextMove += delay; if (Core.TickCount - NextMove > 0) @@ -2120,21 +2137,31 @@ public abstract class BaseAI } m_Mobile.Pushing = false; + var mobDirection = m_Mobile.Direction; + // Do the actual move MoveImpl.IgnoreMovableImpassables = m_Mobile.CanMoveOverObstacles && !m_Mobile.CanDestroyObstacles; + var moveResult = m_Mobile.Move(d); + MoveImpl.IgnoreMovableImpassables = false; - if ((m_Mobile.Direction & Direction.Mask) != (d & Direction.Mask)) + if (moveResult) { - var v = m_Mobile.Move(d); - - MoveImpl.IgnoreMovableImpassables = false; - return v ? MoveResult.Success : MoveResult.Blocked; + // If we don't delay combat, then a direction change will happen and cause a glitchy sliding effect. + if (m_Mobile.Warmode && m_Mobile.Combatant != null) + { + var remaining = m_Mobile.NextCombatTime - Core.TickCount; + var maxWait = Math.Min(delay, 400); + if (remaining < maxWait) + { + m_Mobile.NextCombatTime = Core.TickCount + maxWait; + } + } + return MoveResult.Success; } - if (m_Mobile.Move(d)) + if ((mobDirection & Direction.Mask) != (d & Direction.Mask)) { - MoveImpl.IgnoreMovableImpassables = false; - return MoveResult.Success; + return MoveResult.Blocked; } var wasPushing = m_Mobile.Pushing; @@ -2271,16 +2298,13 @@ public abstract class BaseAI if (m_Mobile.Move(m_Mobile.Direction)) { - MoveImpl.IgnoreMovableImpassables = false; return MoveResult.SuccessAutoTurn; } } - MoveImpl.IgnoreMovableImpassables = false; return wasPushing ? MoveResult.BadState : MoveResult.Blocked; } - MoveImpl.IgnoreMovableImpassables = false; return MoveResult.Success; } @@ -2305,7 +2329,7 @@ public abstract class BaseAI } else { - if (region.GoLocation != Point3D.Zero && Utility.Random(10) > 5) + if (region.GoLocation != Point3D.Zero && Utility.RandomBool()) { DoMove(m_Mobile.GetDirectionTo(region.GoLocation)); } @@ -2336,16 +2360,13 @@ public abstract class BaseAI { DoMove(m_Mobile.GetDirectionTo(m_Mobile.Home)); } + else if (Utility.Random(10) > 5) + { + DoMove(m_Mobile.GetDirectionTo(m_Mobile.Home)); + } else { - if (Utility.Random(10) > 5) - { - DoMove(m_Mobile.GetDirectionTo(m_Mobile.Home)); - } - else - { - WalkRandom(iChanceToNotMove, iChanceToDir, 1); - } + WalkRandom(iChanceToNotMove, iChanceToDir, 1); } } else @@ -2447,7 +2468,7 @@ public abstract class BaseAI * iWantDistMax : The maximum distance we want to be * */ - public virtual bool WalkMobileRange(Mobile m, int iSteps, bool bRun, int iWantDistMin, int iWantDistMax) + public virtual bool WalkMobileRange(Mobile m, int iSteps, bool run, int iWantDistMin, int iWantDistMax) { if (m_Mobile.Deleted || m_Mobile.DisallowAllMoves) { @@ -2467,11 +2488,10 @@ public abstract class BaseAI if (iCurrDist < iWantDistMin || iCurrDist > iWantDistMax) { var needCloser = iCurrDist > iWantDistMax; - var needFurther = !needCloser; if (needCloser && m_Path != null && m_Path.Goal == m) { - if (m_Path.Follow(bRun, 1)) + if (m_Path.Follow(run, 1)) { m_Path = null; } @@ -2479,13 +2499,13 @@ public abstract class BaseAI else { var dirTo = iCurrDist > iWantDistMax ? - m_Mobile.GetDirectionTo(m, bRun) : m.GetDirectionTo(m_Mobile, bRun); + m_Mobile.GetDirectionTo(m, run) : m.GetDirectionTo(m_Mobile, run); if (!DoMove(dirTo, true) && needCloser) { m_Path = new PathFollower(m_Mobile, m) { Mover = DoMoveImpl }; - if (m_Path.Follow(bRun, 1)) + if (m_Path.Follow(run, 1)) { m_Path = null; } @@ -2882,9 +2902,6 @@ public abstract class BaseAI } } - /* - * The mobile changed speeds, we must adjust the timer - */ public virtual void OnCurrentSpeedChanged() { m_Timer.Interval = TimeSpan.FromSeconds(Math.Max(0.008, m_Mobile.CurrentSpeed)); @@ -3247,4 +3264,64 @@ public abstract class BaseAI } } } + + public static class AIMovementTimerPool + { + private const int _poolSize = 1024; + private static readonly Queue _pool = new (_poolSize); + + public static void Configure() + { + var i = 0; + while (i++ < _poolSize) + { + _pool.Enqueue(new AIMovementTimer()); + } + } + + public static AIMovementTimer GetTimer(TimeSpan delay, BaseAI ai, Direction direction) + { + AIMovementTimer timer; + if (_pool.Count > 0) + { + timer = _pool.Dequeue(); + } + else + { + timer = new AIMovementTimer(); + } + + timer.Set(delay, ai, direction); + return timer; + } + + public class AIMovementTimer : Timer + { + public BaseAI AI { get; private set; } + public Direction Direction { get; private set; } + + public AIMovementTimer() : base(TimeSpan.Zero) + { + } + + public void Set(TimeSpan delay, BaseAI ai, Direction direction) + { + if (Running) + { + return; + } + + Delay = delay; + AI = ai; + Direction = direction; + } + + protected override void OnTick() + { + AI?.DoMove(Direction); + AI = null; + _pool.Enqueue(this); + } + } + } } diff --git a/Projects/UOContent/Mobiles/AI/BerserkAI.cs b/Projects/UOContent/Mobiles/AI/BerserkAI.cs index 18228909a..db7a994ba 100644 --- a/Projects/UOContent/Mobiles/AI/BerserkAI.cs +++ b/Projects/UOContent/Mobiles/AI/BerserkAI.cs @@ -65,8 +65,11 @@ public class BerserkAI : BaseAI return true; } } + else if (Core.TickCount - m_Mobile.LastMoveTime > 400) + { + m_Mobile.Direction = m_Mobile.GetDirectionTo(combatant); + } - m_Mobile.Direction = m_Mobile.GetDirectionTo(combatant); if (m_Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, combatant)) { if (m_Mobile.Debug) diff --git a/Projects/UOContent/Mobiles/AI/MageAI.cs b/Projects/UOContent/Mobiles/AI/MageAI.cs index 294a3ba5a..f16410538 100644 --- a/Projects/UOContent/Mobiles/AI/MageAI.cs +++ b/Projects/UOContent/Mobiles/AI/MageAI.cs @@ -769,7 +769,6 @@ public class MageAI : BaseAI } } - m_Mobile.Direction = m_Mobile.GetDirectionTo(c); if (m_Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, c)) { if (m_Mobile.Debug) @@ -840,6 +839,11 @@ public class MageAI : BaseAI RunTo(c); } + if (m_Mobile.Spell != null || !m_Mobile.InRange(c, 1) || Core.TickCount - m_Mobile.LastMoveTime > 800) + { + m_Mobile.Direction = m_Mobile.GetDirectionTo(c); + } + m_LastTarget = c; m_LastTargetLoc = c.Location; diff --git a/Projects/UOContent/Mobiles/AI/MeleeAI.cs b/Projects/UOContent/Mobiles/AI/MeleeAI.cs index fd8bc0fda..5bb697a1b 100644 --- a/Projects/UOContent/Mobiles/AI/MeleeAI.cs +++ b/Projects/UOContent/Mobiles/AI/MeleeAI.cs @@ -10,11 +10,6 @@ public class MeleeAI : BaseAI public override bool DoActionWander() { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("I have no combatant"); - } - if (AcquireFocusMob(m_Mobile.RangePerception, m_Mobile.FightMode, false, false, true)) { if (m_Mobile.Debug) @@ -27,6 +22,13 @@ public class MeleeAI : BaseAI } else { + if (m_Mobile.Debug) + { + m_Mobile.DebugSay("I am wandering"); + } + + m_Mobile.Warmode = false; + base.DoActionWander(); } @@ -79,6 +81,7 @@ public class MeleeAI : BaseAI if (!MoveTo(combatant, true, m_Mobile.RangeFight)) { + m_Mobile.Direction = m_Mobile.GetDirectionTo(combatant); if (AcquireFocusMob(m_Mobile.RangePerception, m_Mobile.FightMode, false, false, true)) { if (m_Mobile.Debug) @@ -107,6 +110,10 @@ public class MeleeAI : BaseAI m_Mobile.DebugSay($"I cannot find {combatant.Name}, so my guard is up"); } } + else if (Core.TickCount - m_Mobile.LastMoveTime > 400) + { + m_Mobile.Direction = m_Mobile.GetDirectionTo(combatant); + } if (!m_Mobile.Controlled && !m_Mobile.Summoned && m_Mobile.CanFlee) { @@ -128,7 +135,6 @@ public class MeleeAI : BaseAI } } - m_Mobile.Direction = m_Mobile.GetDirectionTo(combatant); if (m_Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, combatant)) { if (m_Mobile.Debug) diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index 97cff366b..03cf7e43c 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -261,7 +261,14 @@ namespace Server.Mobiles private AIType m_CurrentAI; // The current AI - private double m_CurrentSpeed; // The current speed, lets say it could be changed by something; + private double _activeSpeed; + private double _passiveSpeed; + private double _currentSpeed; + + // Herding - Overrides the AI to force the mob to move to a specific location + // Thinking: 0.3s, Movement: 0.6s. + private IPoint2D _targetLocation; + private int m_DamageMax = -1; private int m_DamageMin = -1; @@ -541,9 +548,6 @@ namespace Server.Mobiles [CommandProperty(AccessLevel.GameMaster)] public WayPoint CurrentWayPoint { get; set; } - [CommandProperty(AccessLevel.GameMaster)] - public IPoint2D TargetLocation { get; set; } - public virtual Mobile ConstantFocus => null; public virtual bool DisallowAllMoves => false; @@ -660,32 +664,60 @@ namespace Server.Mobiles public int RangeHome { get; set; } = 10; [CommandProperty(AccessLevel.GameMaster)] - public double ActiveSpeed { get; set; } + public virtual double ActiveSpeed + { + get => _activeSpeed; + set + { + if (Math.Abs(_activeSpeed - value) > .0001) + { + _activeSpeed = value; + } + } + } [CommandProperty(AccessLevel.GameMaster)] - public double PassiveSpeed { get; set; } + public virtual double PassiveSpeed + { + get => _passiveSpeed; + set + { + _passiveSpeed = value; + if (Math.Abs(_passiveSpeed - value) > .0001) + { + _passiveSpeed = value; + } + } + } [CommandProperty(AccessLevel.GameMaster)] - public double SpeedMod { get; set; } + public IPoint2D TargetLocation + { + get => _targetLocation; + set + { + _targetLocation = value; + AIObject?.OnCurrentSpeedChanged(); + } + } [CommandProperty(AccessLevel.GameMaster)] public double CurrentSpeed { - get => TargetLocation != null ? 0.3 : SpeedMod <= 0 ? m_CurrentSpeed : SpeedMod; + get => _targetLocation != null ? 0.3 : _currentSpeed; set { - if (m_CurrentSpeed != value) + if (Math.Abs(_currentSpeed - value) > 0.0001) { - m_CurrentSpeed = value; - - if (SpeedMod <= 0) - { - AIObject?.OnCurrentSpeedChanged(); - } + _currentSpeed = value; + AIObject?.OnCurrentSpeedChanged(); } } } + [CommandProperty(AccessLevel.GameMaster)] + public double MoveSpeedMod { get; set; } + [CommandProperty(AccessLevel.GameMaster)] public Point3D Home { @@ -1795,9 +1827,9 @@ namespace Server.Mobiles writer.Write(m_Team); - writer.Write(ActiveSpeed); - writer.Write(PassiveSpeed); - writer.Write(m_CurrentSpeed); + writer.Write(_activeSpeed); + writer.Write(_passiveSpeed); + writer.Write(_currentSpeed); writer.Write(m_Home.X); writer.Write(m_Home.Y); @@ -1921,9 +1953,9 @@ namespace Server.Mobiles m_Team = reader.ReadInt(); - ActiveSpeed = reader.ReadDouble(); - PassiveSpeed = reader.ReadDouble(); - m_CurrentSpeed = reader.ReadDouble(); + _activeSpeed = reader.ReadDouble(); + _passiveSpeed = reader.ReadDouble(); + _currentSpeed = reader.ReadDouble(); if (RangePerception == OldRangePerception) { @@ -4478,13 +4510,19 @@ namespace Server.Mobiles return false; } - public void SetSpeed(double active, double passive) + public void SetSpeed(double active, double passive, bool isPassive = true) { ActiveSpeed = active; PassiveSpeed = passive; - CurrentSpeed = PassiveSpeed; + CurrentSpeed = isPassive ? PassiveSpeed : ActiveSpeed; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetCurrentSpeedToActive() => CurrentSpeed = ActiveSpeed; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetCurrentSpeedToPassive() => CurrentSpeed = PassiveSpeed; + public void SetDamage(int val) { m_DamageMin = val; diff --git a/Projects/UOContent/Mobiles/Familiars/BaseFamiliar.cs b/Projects/UOContent/Mobiles/Familiars/BaseFamiliar.cs index 321333f89..7990663af 100644 --- a/Projects/UOContent/Mobiles/Familiars/BaseFamiliar.cs +++ b/Projects/UOContent/Mobiles/Familiars/BaseFamiliar.cs @@ -97,7 +97,7 @@ public abstract partial class BaseFamiliar : BaseCreature Warmode = master.Warmode; Combatant = master.Combatant; - CurrentSpeed = 0.10; + CurrentSpeed = 0.1; } else { diff --git a/Projects/UOContent/Mobiles/Healers/BaseHealer.cs b/Projects/UOContent/Mobiles/Healers/BaseHealer.cs index 3dde1e15d..54e41cf82 100644 --- a/Projects/UOContent/Mobiles/Healers/BaseHealer.cs +++ b/Projects/UOContent/Mobiles/Healers/BaseHealer.cs @@ -19,8 +19,7 @@ public abstract partial class BaseHealer : BaseVendor if (!IsInvulnerable) { AI = AIType.AI_Mage; - ActiveSpeed = 0.2; - PassiveSpeed = 0.8; + SetSpeed(0.2, 0.8); RangePerception = DefaultRangePerception; FightMode = FightMode.Aggressor; } @@ -148,8 +147,7 @@ public abstract partial class BaseHealer : BaseVendor if (!IsInvulnerable) { AI = AIType.AI_Mage; - ActiveSpeed = 0.2; - PassiveSpeed = 0.8; + SetSpeed(0.2, 0.8); RangePerception = DefaultRangePerception; FightMode = FightMode.Aggressor; } diff --git a/Projects/UOContent/Mobiles/NPCSpeeds.cs b/Projects/UOContent/Mobiles/NPCSpeeds.cs index b052c7af7..3a2bc201e 100644 --- a/Projects/UOContent/Mobiles/NPCSpeeds.cs +++ b/Projects/UOContent/Mobiles/NPCSpeeds.cs @@ -9,6 +9,7 @@ namespace Server.Mobiles; public enum SpeedLevel { None, + VerySlow, Slow, Medium, Fast, @@ -18,8 +19,8 @@ public enum SpeedLevel public static class NPCSpeeds { private const string _tablePath = "Data/npc-speeds.json"; - private static Dictionary _speedsByType = new(); - private static Dictionary _speedsByLevel = new(); + private static readonly Dictionary _speedsByType = new(); + private static readonly Dictionary _speedsByLevel = new(); // Enabled for pets on HS+ public static bool ScaleSpeedByDex { get; private set; } diff --git a/Projects/UOContent/Mobiles/Special/Neira.cs b/Projects/UOContent/Mobiles/Special/Neira.cs index 80d2a947c..4cbcbb5c9 100644 --- a/Projects/UOContent/Mobiles/Special/Neira.cs +++ b/Projects/UOContent/Mobiles/Special/Neira.cs @@ -136,6 +136,9 @@ public partial class Neira : BaseChampion PassiveSpeed *= SpeedBoostScalar; _speedBoost = false; } + + // Assume active + CurrentSpeed = ActiveSpeed; } public override void OnGaveMeleeAttack(Mobile defender, int damage) diff --git a/Projects/UOContent/Mobiles/Townfolk/BaseEscortable.cs b/Projects/UOContent/Mobiles/Townfolk/BaseEscortable.cs index 1b9851c31..51e8390a0 100644 --- a/Projects/UOContent/Mobiles/Townfolk/BaseEscortable.cs +++ b/Projects/UOContent/Mobiles/Townfolk/BaseEscortable.cs @@ -509,9 +509,6 @@ public partial class BaseEscortable : BaseCreature return; } - ActiveSpeed = 0.1; - PassiveSpeed = 0.2; - ControlOrder = OrderType.Follow; ControlTarget = escorter; @@ -520,18 +517,15 @@ public partial class BaseEscortable : BaseCreature CantWalk = false; } - CurrentSpeed = 0.1; + SetSpeed(0.1, 0.2, false); } public virtual void StopFollow() { - ActiveSpeed = 0.2; - PassiveSpeed = 1.0; - ControlOrder = OrderType.None; ControlTarget = null; - CurrentSpeed = 1.0; + SetSpeed(0.2, 1.0); } public virtual Mobile GetEscorter() diff --git a/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs b/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs index 120980f87..6cde043a2 100644 --- a/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs +++ b/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs @@ -223,7 +223,7 @@ namespace Server.Mobiles { public class CloneAI : BaseAI { - public CloneAI(Clone m) : base(m) => m.CurrentSpeed = m.ActiveSpeed; + public CloneAI(Clone m) : base(m) => m.SetCurrentSpeedToActive(); public override bool CanDetectHidden => false;