diff --git a/Projects/UOContent/Mobiles/AI/BaseAI.cs b/Projects/UOContent/Mobiles/AI/BaseAI.cs index 5ce7ba5a7d..78149f9c97 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI.cs @@ -1,3 +1,19 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2025 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: BaseAI.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + ************************************************************************/ + + using System; using System.Collections.Generic; using Server.Collections; @@ -42,8 +58,6 @@ 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, @@ -63,7 +77,7 @@ public abstract class BaseAI SkillName.Cartography, SkillName.Cooking, SkillName.DetectHidden, - SkillName.Discordance, // ?? + SkillName.Discordance, // ?? > was Enticement i tihnk for pre-UOTD SkillName.EvalInt, SkillName.Fishing, SkillName.Provocation, @@ -97,46 +111,30 @@ public abstract class BaseAI }; protected ActionType m_Action; - public readonly BaseCreature m_Mobile; - private long m_NextDetectHidden; private long m_NextStopGuard; - protected PathFollower m_Path; public Timer m_Timer; public BaseAI(BaseCreature m) { m_Mobile = m; - m_Timer = new AITimer(this); - bool activate; - - if (!m.PlayerRangeSensitive) - { - activate = true; - } - else if (World.Loading) - { - activate = false; - } - else if (m.Map == null || m.Map == Map.Internal || !m.Map.GetSector(m.Location).Active) - { - activate = false; - } - else - { - activate = true; - } + var activate = !m.PlayerRangeSensitive || + (!World.Loading && m.Map != null && m.Map != Map.Internal && + m.Map.GetSector(m.Location).Active); if (activate) - { + { m_Timer.Start(); } - Action = ActionType.Wander; + if (Action != ActionType.Wander) + { + Action = ActionType.Wander; + } } public ActionType Action @@ -144,115 +142,138 @@ public ActionType Action get => m_Action; set { - m_Action = value; - OnActionChanged(); + if (m_Action != value) + { + m_Action = value; + OnActionChanged(); + } } } public long NextMove { get; set; } + // OSI, DetectHidden >= 50.0? public virtual bool CanDetectHidden => m_Mobile.Skills.DetectHidden.Value > 0; - + public virtual bool WasNamed(string speech) { var name = m_Mobile.Name; - - return name != null && speech.InsensitiveStartsWith(name); + return !string.IsNullOrEmpty(name) && speech.InsensitiveStartsWith(name); } public virtual void GetContextMenuEntries(Mobile from, ref PooledRefList list) - { - if (!from.Alive || !m_Mobile.Controlled || !from.InRange(m_Mobile, 14)) + { // OSI has 16 tiles for opening the context menu. + if (!from.Alive || !m_Mobile.Controlled || !from.InRange(m_Mobile, 16)) { return; } - - var isDeadPet = m_Mobile.IsDeadPet; - + if (from == m_Mobile.ControlMaster) { - list.Add(new InternalEntry(6107, 14, OrderType.Guard, !isDeadPet)); // Command: Guard - list.Add(new InternalEntry(6108, 14, OrderType.Follow, true)); // Command: Follow - - if (m_Mobile.CanDrop) - { - list.Add(new InternalEntry(6109, 14, OrderType.Drop, !isDeadPet)); // Command: Drop - } - - list.Add(new InternalEntry(6111, 14, OrderType.Attack, !isDeadPet)); // Command: Kill - - list.Add(new InternalEntry(6112, 14, OrderType.Stop, true)); // Command: Stop - list.Add(new InternalEntry(6114, 14, OrderType.Stay, true)); // Command: Stay - - if (!m_Mobile.Summoned && m_Mobile is not GrizzledMare) - { - list.Add(new InternalEntry(6110, 14, OrderType.Friend, true)); // Add Friend - list.Add(new InternalEntry(6099, 14, OrderType.Unfriend, true)); // Remove Friend - list.Add(new InternalEntry(6113, 14, OrderType.Transfer, !isDeadPet)); // Transfer - } - - list.Add(new InternalEntry(6118, 14, OrderType.Release, true)); // Release + AddControlMasterEntries(ref list); } else if (m_Mobile.IsPetFriend(from)) { - list.Add(new InternalEntry(6108, 14, OrderType.Follow, true)); // Command: Follow - list.Add(new InternalEntry(6112, 14, OrderType.Stop, !isDeadPet)); // Command: Stop - list.Add(new InternalEntry(6114, 14, OrderType.Stay, true)); // Command: Stay + AddPetFriendEntries(ref list); } } - - public virtual void BeginPickTarget(Mobile from, OrderType order) + + private void AddControlMasterEntries(ref PooledRefList list) { - if (m_Mobile.Deleted || !m_Mobile.Controlled || !from.InRange(m_Mobile, 14) || from.Map != m_Mobile.Map) + var isDeadPet = m_Mobile.IsDeadPet; // Ordered list to match OSI. + + // OSI has 14 tiles for context menu commands to be available. + // On OSI, commands grey out. Here, they do when out of range which is better, imo. + list.Add(new InternalEntry(3006111, 14, OrderType.Attack, !isDeadPet)); // Command: Kill + list.Add(new InternalEntry(3006108, 14, OrderType.Follow, true)); // Command: Follow + list.Add(new InternalEntry(3006107, 14, OrderType.Guard, !isDeadPet)); // Command: Guard + list.Add(new InternalEntry(3006112, 14, OrderType.Stop, true)); // Command: Stop + list.Add(new InternalEntry(3006114, 14, OrderType.Stay, true)); // Command: Stay + + if (m_Mobile.CanDrop) { - return; + list.Add(new InternalEntry(3006109, 14, OrderType.Drop, !isDeadPet)); // Command: Drop } - var isOwner = from == m_Mobile.ControlMaster; - var isFriend = !isOwner && m_Mobile.IsPetFriend(from); - - if (!isOwner && !isFriend) + // This command is on OSI. However, the cliloc must be manually added to client files. + // simply returns a string giving instruction on how to rename pets. + list.Add(new InternalEntry(3006098, 14, OrderType.Rename, true)); // Rename + + if (!m_Mobile.Summoned && m_Mobile is not GrizzledMare) { - return; + list.Add(new InternalEntry(3006110, 14, OrderType.Friend, true)); // Add Friend + list.Add(new InternalEntry(3006099, 14, OrderType.Unfriend, true)); // Remove Friend + list.Add(new InternalEntry(3006113, 14, OrderType.Transfer, !isDeadPet)); // Transfer } - if (isFriend && order != OrderType.Follow && order != OrderType.Stay && order != OrderType.Stop) + // OSI has release command set to 13 tiles for no apparent reason. + list.Add(new InternalEntry(3006118, 13, OrderType.Release, true)); // Release + } + + private void AddPetFriendEntries(ref PooledRefList list) + { + var isDeadPet = m_Mobile.IsDeadPet; + + list.Add(new InternalEntry(3006108, 14, OrderType.Follow, true)); // Command: Follow + list.Add(new InternalEntry(3006112, 14, OrderType.Stop, !isDeadPet)); // Command: Stop + list.Add(new InternalEntry(3006114, 14, OrderType.Stay, true)); // Command: Stay + } + + public virtual void BeginPickTarget(Mobile from, OrderType order) + { + if (!IsValidTarget(from, order)) { return; } - + if (from.Target == null) { - if (order == OrderType.Transfer) - { - from.SendLocalizedMessage(502038); // Click on the person to transfer ownership to. - } - else if (order == OrderType.Friend) - { - from.SendLocalizedMessage(502020); // Click on the player whom you wish to make a co-owner. - } - else if (order == OrderType.Unfriend) - { - from.SendLocalizedMessage(1070948); // Click on the player whom you wish to remove as a co-owner. - } - + SendOrderMessage(from, order); from.Target = new AIControlMobileTarget(this, order); } - else if (from.Target is AIControlMobileTarget t) + else if (from.Target is AIControlMobileTarget t && t.Order == order) { - if (t.Order == order) - { - t.AddAI(this); - } + t.AddAI(this); + } + } + + private static void SendOrderMessage(Mobile from, OrderType order) + { + switch (order) + { + case OrderType.Transfer: + from.SendLocalizedMessage(502038); + // 502038: Click on the person to transfer ownership to. + break; + case OrderType.Friend: + from.SendLocalizedMessage(502020); + // 502020: Click on the player whom you wish to make a co-owner. + break; + case OrderType.Unfriend: + from.SendLocalizedMessage(1070948); + // 1070948: Click on the player whom you wish to remove as a co-owner. + break; } } public virtual void OnAggressiveAction(Mobile aggressor) { + if (aggressor.Hidden) + { + return; + } + var currentCombat = m_Mobile.Combatant; - - if (currentCombat != null && !aggressor.Hidden && currentCombat != aggressor && - m_Mobile.GetDistanceToSqrt(currentCombat) > m_Mobile.GetDistanceToSqrt(aggressor)) + + if (currentCombat == null || currentCombat == aggressor) + { + return; + } + + var currentDistance = m_Mobile.GetDistanceToSqrt(currentCombat); + var aggressorDistance = m_Mobile.GetDistanceToSqrt(aggressor); + + if (aggressorDistance < currentDistance) { m_Mobile.Combatant = aggressor; } @@ -260,54 +281,70 @@ public virtual void OnAggressiveAction(Mobile aggressor) public virtual void EndPickTarget(Mobile from, Mobile target, OrderType order) { - if (m_Mobile.Deleted || !m_Mobile.Controlled || !from.InRange(m_Mobile, 14) || from.Map != m_Mobile.Map || - !from.CheckAlive()) + if (!IsValidTarget(from, order) || + (order == OrderType.Attack && !CanAttackTarget(from, target))) { return; } + if (m_Mobile.CheckControlChance(from)) + { + m_Mobile.ControlTarget = target; + m_Mobile.ControlOrder = order; + } + } + + private bool IsValidTarget(Mobile from, OrderType order) + { + if (m_Mobile.Deleted || !m_Mobile.Controlled || !from.InRange(m_Mobile, 14) + || from.Map != m_Mobile.Map || !from.CheckAlive()) + { + return false; + } + var isOwner = from == m_Mobile.ControlMaster; var isFriend = !isOwner && m_Mobile.IsPetFriend(from); if (!isOwner && !isFriend) { - return; + return false; } if (isFriend && order != OrderType.Follow && order != OrderType.Stay && order != OrderType.Stop) { - return; + return false; } + + return true; + } - if (order == OrderType.Attack) + private bool CanAttackTarget(Mobile from, Mobile target) + { + if (target is BaseCreature creature && creature.IsScaryToPets && m_Mobile.IsScaredOfScaryThings) { - if (target is BaseCreature creature && creature.IsScaryToPets && m_Mobile.IsScaredOfScaryThings) - { - m_Mobile.SayTo(from, "Your pet refuses to attack this creature!"); - return; - } - - if (SolenHelper.CheckRedFriendship(from) && - target is RedSolenInfiltratorQueen or RedSolenInfiltratorWarrior or RedSolenQueen or RedSolenWarrior or RedSolenWorker - || SolenHelper.CheckBlackFriendship(from) && - target is BlackSolenInfiltratorQueen or BlackSolenInfiltratorWarrior or BlackSolenQueen or BlackSolenWarrior or BlackSolenWorker) - { - from.SendAsciiMessage("You can not force your pet to attack a creature you are protected from."); - return; - } - - if (target is BaseFactionGuard) - { - m_Mobile.SayTo(from, "Your pet refuses to attack the guard."); - return; - } + m_Mobile.SayTo(from, "Your pet refuses to attack this creature!"); + return false; + } + + if (SolenHelper.CheckRedFriendship(from) && + target is RedSolenInfiltratorQueen or RedSolenInfiltratorWarrior + or RedSolenQueen or RedSolenWarrior or RedSolenWorker || + SolenHelper.CheckBlackFriendship(from) && + target is BlackSolenInfiltratorQueen or BlackSolenInfiltratorWarrior + or BlackSolenQueen or BlackSolenWarrior or BlackSolenWorker) + { + from.SendLocalizedMessage(1063106); + // 1063106: You can not force your pet to attack a creature you are protected from. + return false; } - if (m_Mobile.CheckControlChance(from)) + if (target is BaseFactionGuard) { - m_Mobile.ControlTarget = target; - m_Mobile.ControlOrder = order; + m_Mobile.SayTo(from, "Your pet refuses to attack the guard."); + return false; } + + return true; } public virtual bool HandlesOnSpeech(Mobile from) @@ -328,596 +365,450 @@ public virtual bool HandlesOnSpeech(Mobile from) public virtual void OnSpeech(SpeechEventArgs e) { - if (e.Mobile.Alive && e.Mobile.InRange(m_Mobile.Location, 3) && m_Mobile.IsHumanInTown()) + if (WasNamed(e.Speech) && e.Mobile.Alive && + e.Mobile.InRange(m_Mobile.Location, 3) && m_Mobile.IsHumanInTown()) { - if (e.HasKeyword(0x9D) && WasNamed(e.Speech)) // *move* + if (e.HasKeyword(0x9D)) // *move* { - if (m_Mobile.Combatant != null) - { - // I am too busy fighting to deal with thee! - m_Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 501482); - } - else - { - // Excuse me? - m_Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 501516); - WalkRandomInHome(2, 2, 1); - } + m_Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 501516); + // 501516: Excuse me? + m_Mobile.Location = new Point3D(m_Mobile.Location.X + + Utility.RandomMinMax(-1, 1), m_Mobile.Location.Y + + Utility.RandomMinMax(-1, 1), m_Mobile.Location.Z); + WalkRandomInHome(4, 3, 1); + return; } - else if (e.HasKeyword(0x9E) && WasNamed(e.Speech)) // *time* + else if (e.HasKeyword(0x9E)) // *time* { - if (m_Mobile.Combatant != null) - { - // I am too busy fighting to deal with thee! - m_Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 501482); - } - else - { - Clock.GetTime(m_Mobile, out var generalNumber, out _); - - m_Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, generalNumber); - } + Clock.GetTime(m_Mobile, out var generalNumber, out _); + m_Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, generalNumber); + return; } - else if (e.HasKeyword(0x6C) && WasNamed(e.Speech)) // *train + else if (e.HasKeyword(0x6C)) // *train* { - if (m_Mobile.Combatant != null) - { - // I am too busy fighting to deal with thee! - m_Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 501482); - } - else - { - var foundSomething = false; - - var ourSkills = m_Mobile.Skills; - var theirSkills = e.Mobile.Skills; - - for (var i = 0; i < ourSkills.Length && i < theirSkills.Length; ++i) - { - var skill = ourSkills[i]; - var theirSkill = theirSkills[i]; - - if (skill?.Base >= 60.0 && m_Mobile.CheckTeach(skill.SkillName, e.Mobile)) - { - var toTeach = skill.Base / 3.0; - - if (toTeach > 42.0) - { - toTeach = 42.0; - } - - if (toTeach > theirSkill.Base) - { - var number = 1043059 + i; - - if (number > 1043107) - { - continue; - } - - if (!foundSomething) - { - m_Mobile.Say(1043058); // I can train the following: - } + HandleTraining(e.Mobile); + return; + } + } - m_Mobile.Say(number); + if (m_Mobile.Controlled && m_Mobile.Commandable) + { + AllOnSpeechPet(e); + NamedOnSpeechPet(e); + return; + } - foundSomething = true; - } - } - } + if (e.Mobile.AccessLevel >= AccessLevel.GameMaster) + { + HandleGMCommands(e); + } + } - if (!foundSomething) - { - m_Mobile.Say(501505); // Alas, I cannot teach thee anything. - } - } + public virtual void AllOnSpeechPet(SpeechEventArgs e) + { + if (e.Mobile.InRange(m_Mobile.Location, 14)) + { + if (e.HasKeyword(0x164)) // all come + { + HandleComeCommand(e.Mobile, true); } - else + else if (e.HasKeyword(0x165)) // all follow { - var toTrain = (SkillName)(-1); - - for (var i = 0; toTrain == (SkillName)(-1) && i < e.Keywords.Length; ++i) - { - var keyword = e.Keywords[i]; - - if (keyword == 0x154) - { - toTrain = SkillName.Anatomy; - } - else if (keyword >= 0x6D && keyword <= 0x9C) - { - var index = keyword - 0x6D; - - if (index >= 0 && index < m_KeywordTable.Length) - { - toTrain = m_KeywordTable[index]; - } - } - } - - if (toTrain != (SkillName)(-1) && WasNamed(e.Speech)) - { - if (m_Mobile.Combatant != null) - { - // I am too busy fighting to deal with thee! - m_Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 501482); - } - else - { - var skills = m_Mobile.Skills; - var skill = skills[toTrain]; - - if (skill == null || skill.Base < 60.0 || !m_Mobile.CheckTeach(toTrain, e.Mobile)) - { - m_Mobile.Say(501507); // 'Tis not something I can teach thee of. - } - else - { - m_Mobile.Teach(toTrain, e.Mobile, 0, false); - } - } - } + BeginPickTarget(e.Mobile, OrderType.Follow); + } + else if (e.HasKeyword(0x166)) // all guard + { + HandleGuardCommand(e.Mobile, true); + } + else if (e.HasKeyword(0x16B)) // all guard me + { + HandleGuardCommand(e.Mobile, true); + } + else if (e.HasKeyword(0x167)) // all stop + { + HandleSimpleCommand(e.Mobile, OrderType.Stop); + } + else if (e.HasKeyword(0x168)) // all kill + { + HandleAttackCommand(e.Mobile, true); + } + else if (e.HasKeyword(0x169)) // all attack + { + HandleAttackCommand(e.Mobile, true); + } + else if (e.HasKeyword(0x16C)) // all follow me + { + HandleSimpleCommand(e.Mobile, OrderType.Follow, e.Mobile); + } + else if (e.HasKeyword(0x170)) // all stay + { + HandleSimpleCommand(e.Mobile, OrderType.Stay); } } + } - if (m_Mobile.Controlled && m_Mobile.Commandable) + public virtual void NamedOnSpeechPet(SpeechEventArgs e) + { + if (e.Mobile.InRange(m_Mobile.Location, 14)) { - if (m_Mobile.Debug) + if (e.HasKeyword(0x155)) // *come { - m_Mobile.DebugSay("Listening..."); + HandleComeCommand(e.Mobile, true); } - - var isOwner = e.Mobile == m_Mobile.ControlMaster; - var isFriend = !isOwner && m_Mobile.IsPetFriend(e.Mobile); - - if (e.Mobile.Alive && (isOwner || isFriend)) + else if (e.HasKeyword(0x156)) // *drop { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("It's from my master"); - } - - var keywords = e.Keywords; - var speech = e.Speech; - - // First, check the all* - for (var i = 0; i < keywords.Length; ++i) - { - var keyword = keywords[i]; - - switch (keyword) - { - case 0x164: // all come - { - if (!isOwner) - { - break; - } - - if (m_Mobile.CheckControlChance(e.Mobile)) - { - m_Mobile.ControlTarget = null; - m_Mobile.ControlOrder = OrderType.Come; - } - - return; - } - case 0x165: // all follow - { - BeginPickTarget(e.Mobile, OrderType.Follow); - return; - } - case 0x166: // all guard - case 0x16B: // all guard me - { - if (!isOwner) - { - break; - } - - if (m_Mobile.CheckControlChance(e.Mobile)) - { - m_Mobile.ControlTarget = null; - m_Mobile.ControlOrder = OrderType.Guard; - } - - return; - } - case 0x167: // all stop - { - if (m_Mobile.CheckControlChance(e.Mobile)) - { - m_Mobile.ControlTarget = null; - m_Mobile.ControlOrder = OrderType.Stop; - } - - return; - } - case 0x168: // all kill - case 0x169: // all attack - { - if (!isOwner) - { - break; - } - - BeginPickTarget(e.Mobile, OrderType.Attack); - return; - } - case 0x16C: // all follow me - { - if (m_Mobile.CheckControlChance(e.Mobile)) - { - m_Mobile.ControlTarget = e.Mobile; - m_Mobile.ControlOrder = OrderType.Follow; - } - - return; - } - case 0x170: // all stay - { - if (m_Mobile.CheckControlChance(e.Mobile)) - { - m_Mobile.ControlTarget = null; - m_Mobile.ControlOrder = OrderType.Stay; - } - - return; - } - } - } - - // No all*, so check *command - for (var i = 0; i < keywords.Length; ++i) - { - var keyword = keywords[i]; - - switch (keyword) - { - case 0x155: // *come - { - if (!isOwner) - { - break; - } - - if (WasNamed(speech) && m_Mobile.CheckControlChance(e.Mobile)) - { - m_Mobile.ControlTarget = null; - m_Mobile.ControlOrder = OrderType.Come; - } - - return; - } - case 0x156: // *drop - { - if (!isOwner) - { - break; - } - - if (!m_Mobile.IsDeadPet && !m_Mobile.Summoned && WasNamed(speech) && - m_Mobile.CheckControlChance(e.Mobile)) - { - m_Mobile.ControlTarget = null; - m_Mobile.ControlOrder = OrderType.Drop; - } - - return; - } - case 0x15A: // *follow - { - if (WasNamed(speech) && m_Mobile.CheckControlChance(e.Mobile)) - { - BeginPickTarget(e.Mobile, OrderType.Follow); - } - - return; - } - case 0x15B: // *friend - { - if (!isOwner) - { - break; - } - - if (WasNamed(speech) && m_Mobile.CheckControlChance(e.Mobile)) - { - if (m_Mobile.Summoned || m_Mobile is GrizzledMare) - { - e.Mobile.SendLocalizedMessage( - 1005481 - ); // Summoned creatures are loyal only to their summoners. - } - else if (e.Mobile.HasTrade) - { - e.Mobile.SendLocalizedMessage( - 1070947 - ); // You cannot friend a pet with a trade pending - } - else - { - BeginPickTarget(e.Mobile, OrderType.Friend); - } - } - - return; - } - case 0x15C: // *guard - { - if (!isOwner) - { - break; - } - - if (!m_Mobile.IsDeadPet && WasNamed(speech) && m_Mobile.CheckControlChance(e.Mobile)) - { - m_Mobile.ControlTarget = null; - m_Mobile.ControlOrder = OrderType.Guard; - } - - return; - } - case 0x15D: // *kill - case 0x15E: // *attack - { - if (!isOwner) - { - break; - } - - if (!m_Mobile.IsDeadPet && WasNamed(speech) && m_Mobile.CheckControlChance(e.Mobile)) - { - BeginPickTarget(e.Mobile, OrderType.Attack); - } - - return; - } - case 0x15F: // *patrol - { - if (!isOwner) - { - break; - } - - if (WasNamed(speech) && m_Mobile.CheckControlChance(e.Mobile)) - { - m_Mobile.ControlTarget = null; - m_Mobile.ControlOrder = OrderType.Patrol; - } - - return; - } - case 0x161: // *stop - { - if (WasNamed(speech) && m_Mobile.CheckControlChance(e.Mobile)) - { - m_Mobile.ControlTarget = null; - m_Mobile.ControlOrder = OrderType.Stop; - } - - return; - } - case 0x163: // *follow me - { - if (WasNamed(speech) && m_Mobile.CheckControlChance(e.Mobile)) - { - m_Mobile.ControlTarget = e.Mobile; - m_Mobile.ControlOrder = OrderType.Follow; - } - - return; - } - case 0x16D: // *release - { - if (!isOwner) - { - break; - } - - if (WasNamed(speech) && m_Mobile.CheckControlChance(e.Mobile)) - { - if (!m_Mobile.Summoned) - { - e.Mobile.SendGump(new ConfirmReleaseGump(e.Mobile, m_Mobile)); - } - else - { - m_Mobile.ControlTarget = null; - m_Mobile.ControlOrder = OrderType.Release; - } - } - - return; - } - case 0x16E: // *transfer - { - if (!isOwner) - { - break; - } - - if (!m_Mobile.IsDeadPet && WasNamed(speech) && m_Mobile.CheckControlChance(e.Mobile)) - { - if (m_Mobile.Summoned || m_Mobile is GrizzledMare) - { - e.Mobile.SendLocalizedMessage( - 1005487 - ); // You cannot transfer ownership of a summoned creature. - } - else if (e.Mobile.HasTrade) - { - e.Mobile.SendLocalizedMessage( - 1010507 - ); // You cannot transfer a pet with a trade pending - } - else - { - BeginPickTarget(e.Mobile, OrderType.Transfer); - } - } - - return; - } - case 0x16F: // *stay - { - if (WasNamed(speech) && m_Mobile.CheckControlChance(e.Mobile)) - { - m_Mobile.ControlTarget = null; - m_Mobile.ControlOrder = OrderType.Stay; - } - - return; - } - } - } + HandleDropCommand(e.Mobile, true, e.Speech); } - } - else + else if (e.HasKeyword(0x15A)) // *follow + { + BeginPickTarget(e.Mobile, OrderType.Follow); + } + else if (e.HasKeyword(0x15B)) // *friend + { + HandleFriendCommand(e.Mobile, true, e.Speech); + } + else if (e.HasKeyword(0x15C)) // *guard + { + HandleGuardCommand(e.Mobile, true); + } + else if (e.HasKeyword(0x15D)) // *kill + { + HandleAttackCommand(e.Mobile, true); + } + else if (e.HasKeyword(0x15E)) // *attack + { + HandleAttackCommand(e.Mobile, true); + } + else if (e.HasKeyword(0x161)) // *stop + { + HandleSimpleCommand(e.Mobile, OrderType.Stop); + } + else if (e.HasKeyword(0x163)) // *follow me + { + HandleSimpleCommand(e.Mobile, OrderType.Follow, e.Mobile); + } + else if (e.HasKeyword(0x16D)) // *release + { + HandleReleaseCommand(e.Mobile, true, e.Speech); + } + else if (e.HasKeyword(0x16E)) // *transfer + { + HandleTransferCommand(e.Mobile, true, e.Speech); + } + else if (e.HasKeyword(0x16F)) // *stay + { + HandleSimpleCommand(e.Mobile, OrderType.Stay); + } + } + } + + private void HandleTraining(Mobile from) + { + var foundSomething = false; + + foreach (var skill in m_Mobile.Skills) { - if (e.Mobile.AccessLevel >= AccessLevel.GameMaster) + if (skill.Base < 60.0 || !m_Mobile.CheckTeach(skill.SkillName, from)) { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("It's from a GM"); - } + continue; + } - if (m_Mobile.FindMyName(e.Speech, true)) - { - var str = e.Speech.Split(' '); - int i; + var toTeach = Math.Min(skill.Base / 3.0, 42.0); + + if (toTeach <= from.Skills[skill.SkillName].Base) + { + continue; + } - for (i = 0; i < str.Length; i++) - { - var word = str[i]; + var number = 1043059 + (int)skill.SkillName; + // 1043059: alchemy + if (number > 1043107) + // 1043107: disarming traps + { + continue; + } - if (word.InsensitiveEquals("obey")) - { - m_Mobile.SetControlMaster(e.Mobile); + if (!foundSomething) + { + m_Mobile.Say(1043058); + // 1043058: I can train the following: + foundSomething = true; + } - if (m_Mobile.Summoned) - { - m_Mobile.SummonMaster = e.Mobile; - } + m_Mobile.Say(number); + } - return; - } - } - } - } + if (!foundSomething) + { + m_Mobile.Say(501505); + // 501505: Alas, I cannot teach thee anything. } } - - public virtual bool Think() + + private void HandleComeCommand(Mobile from, bool isOwner) { - if (m_Mobile.Deleted) + if (isOwner && m_Mobile.CheckControlChance(from)) { - return false; + m_Mobile.ControlTarget = null; + m_Mobile.ControlOrder = OrderType.Come; } - - if (CheckFlee()) + } + + private void HandleGuardCommand(Mobile from, bool isOwner) + { + if (isOwner && m_Mobile.CheckControlChance(from)) { - return true; + m_Mobile.ControlTarget = null; + m_Mobile.ControlOrder = OrderType.Guard; } - - switch (Action) + } + + private void HandleSimpleCommand(Mobile from, OrderType order, Mobile target = null) + { + if (m_Mobile.CheckControlChance(from)) { - case ActionType.Wander: - { - m_Mobile.OnActionWander(); - return DoActionWander(); - } - - case ActionType.Combat: - { - m_Mobile.OnActionCombat(); - return DoActionCombat(); - } - - case ActionType.Guard: - { - m_Mobile.OnActionGuard(); - return DoActionGuard(); - } - - case ActionType.Flee: - { - m_Mobile.OnActionFlee(); - return DoActionFlee(); - } - - case ActionType.Interact: + m_Mobile.ControlTarget = target; + m_Mobile.ControlOrder = order; + } + } + + private void HandleAttackCommand(Mobile from, bool isOwner) + { + if (isOwner) + { + BeginPickTarget(from, OrderType.Attack); + } + } + + private void HandleDropCommand(Mobile from, bool isOwner, string speech) + { + if (isOwner && !m_Mobile.IsDeadPet && !m_Mobile.Summoned && WasNamed(speech) && m_Mobile.CheckControlChance(from)) + { + m_Mobile.ControlTarget = null; + m_Mobile.ControlOrder = OrderType.Drop; + } + } + + private void HandleFriendCommand(Mobile from, bool isOwner, string speech) + { + if (isOwner && WasNamed(speech) && m_Mobile.CheckControlChance(from)) + { + if (m_Mobile.Summoned || m_Mobile is GrizzledMare) + { + from.SendLocalizedMessage(1005481); + // 1005481: Summoned creatures are loyal only to their summoners. + } + else if (from.HasTrade) + { + from.SendLocalizedMessage(1070947); + // 1070947: You cannot friend a pet with a trade pending + } + else + { + BeginPickTarget(from, OrderType.Friend); + } + } + } + + private void HandleReleaseCommand(Mobile from, bool isOwner, string speech) + { + if (isOwner && WasNamed(speech) && m_Mobile.CheckControlChance(from)) + { + if (!m_Mobile.Summoned) + { + from.SendGump(new ConfirmReleaseGump(from, m_Mobile)); + } + else + { + m_Mobile.ControlTarget = null; + m_Mobile.ControlOrder = OrderType.Release; + } + } + } + + private void HandleTransferCommand(Mobile from, bool isOwner, string speech) + { + if (isOwner && !m_Mobile.IsDeadPet && WasNamed(speech) && m_Mobile.CheckControlChance(from)) + { + if (m_Mobile.Summoned || m_Mobile is GrizzledMare) + { + from.SendLocalizedMessage(1005487); + // 1005487: You cannot transfer ownership of a summoned creature. + } + else if (from.HasTrade) + { + from.SendLocalizedMessage(1010507); + // 1010507: You cannot transfer a pet with a trade pending + } + else + { + BeginPickTarget(from, OrderType.Transfer); + } + } + } + + private void HandleGMCommands(SpeechEventArgs e) + { + if (m_Mobile.Debug) + { + m_Mobile.DebugSay("It is from a GM"); + } + + if (m_Mobile.FindMyName(e.Speech, true)) + { + foreach (var word in e.Speech.Split(' ')) + { + if (word.InsensitiveEquals("obey")) { - m_Mobile.OnActionInteract(); - return DoActionInteract(); + m_Mobile.SetControlMaster(e.Mobile); + + if (m_Mobile.Summoned) + { + m_Mobile.SummonMaster = e.Mobile; + } + + return; } + } + } + } - case ActionType.Backoff: - { - m_Mobile.OnActionBackoff(); - return DoActionBackoff(); - } + public virtual bool Think() + { + if (m_Mobile?.Deleted != false || m_Mobile.Map == null) + { + return false; + } - default: - { - return false; - } + if (CheckFlee()) + { + return true; } + + var actionHandlers = new Dictionary> + { + { ActionType.Wander, () => { m_Mobile.OnActionWander(); return DoActionWander(); } }, + { ActionType.Combat, () => { m_Mobile.OnActionCombat(); return DoActionCombat(); } }, + { ActionType.Guard, () => { m_Mobile.OnActionGuard(); return DoActionGuard(); } }, + { ActionType.Flee, () => { m_Mobile.OnActionFlee(); return DoActionFlee(); } }, + { ActionType.Interact, () => { m_Mobile.OnActionInteract(); return DoActionInteract(); } }, + { ActionType.Backoff, () => { m_Mobile.OnActionBackoff(); return DoActionBackoff(); } } + }; + + return actionHandlers.TryGetValue(Action, out var handler) && handler(); } public virtual void OnActionChanged() { - switch (Action) + var actionHandlers = new Dictionary + { + { ActionType.Wander, HandleWanderAction }, + { ActionType.Combat, HandleCombatAction }, + { ActionType.Guard, HandleGuardAction }, + { ActionType.Flee, HandleFleeAction }, + { ActionType.Interact, HandleInteractAction }, + { ActionType.Backoff, HandleBackoffAction } + }; + + if (actionHandlers.TryGetValue(Action, out var handler)) { - case ActionType.Wander: - { - m_Mobile.Warmode = false; - m_Mobile.Combatant = null; - m_Mobile.FocusMob = null; - m_Mobile.SetCurrentSpeedToPassive(); - break; - } - - case ActionType.Combat: - { - m_Mobile.Warmode = true; - m_Mobile.FocusMob = null; - m_Mobile.SetCurrentSpeedToActive(); - break; - } - - case ActionType.Guard: - { - m_Mobile.Warmode = true; - m_Mobile.FocusMob = null; - m_Mobile.Combatant = null; - m_NextStopGuard = Core.TickCount + (int)TimeSpan.FromSeconds(10).TotalMilliseconds; - m_Mobile.SetCurrentSpeedToActive(); - break; - } - - case ActionType.Flee: - { - m_Mobile.Warmode = true; - m_Mobile.FocusMob = null; - m_Mobile.SetCurrentSpeedToActive(); - break; - } - - case ActionType.Interact: - { - m_Mobile.Warmode = false; - m_Mobile.SetCurrentSpeedToPassive(); - break; - } - - case ActionType.Backoff: - { - m_Mobile.Warmode = false; - m_Mobile.SetCurrentSpeedToPassive(); - break; - } + handler(); + } + } + + private void HandleWanderAction() + { + if (m_Mobile.Warmode) + { + m_Mobile.Warmode = false; + } + if (m_Mobile.Combatant != null) + { + m_Mobile.Combatant = null; + } + if (m_Mobile.FocusMob != null) + { + m_Mobile.FocusMob = null; + } + if (m_Mobile.CurrentSpeed != m_Mobile.ActiveSpeed) + { + m_Mobile.SetCurrentSpeedToActive(); + } + } + + private void HandleCombatAction() + { + if (!m_Mobile.Warmode) + { + m_Mobile.Warmode = true; + } + if (m_Mobile.FocusMob != null) + { + m_Mobile.FocusMob = null; + } + if (m_Mobile.CurrentSpeed != m_Mobile.ActiveSpeed) + { + m_Mobile.SetCurrentSpeedToActive(); + } + } + + private void HandleGuardAction() + { + if (!m_Mobile.Warmode) + { + m_Mobile.Warmode = true; + } + if (m_Mobile.FocusMob != null) + { + m_Mobile.FocusMob = null; + } + if (m_Mobile.Combatant != null) + { + m_Mobile.Combatant = null; + } + if (m_Mobile.CurrentSpeed != m_Mobile.ActiveSpeed) + { + m_Mobile.SetCurrentSpeedToActive(); + } + } + + private void HandleFleeAction() + { + if (!m_Mobile.Warmode) + { + m_Mobile.Warmode = true; + } + if (m_Mobile.FocusMob != null) + { + m_Mobile.FocusMob = null; + } + if (m_Mobile.CurrentSpeed != m_Mobile.ActiveSpeed) + { + m_Mobile.SetCurrentSpeedToActive(); + } + } + + private void HandleInteractAction() + { + if (m_Mobile.Warmode) + { + m_Mobile.Warmode = false; + } + if (m_Mobile.CurrentSpeed != m_Mobile.PassiveSpeed) + { + m_Mobile.SetCurrentSpeedToPassive(); + } + } + + private void HandleBackoffAction() + { + if (m_Mobile.Warmode) + { + m_Mobile.Warmode = false; + } + if (m_Mobile.CurrentSpeed != m_Mobile.PassiveSpeed) + { + m_Mobile.SetCurrentSpeedToPassive(); } } @@ -929,62 +820,77 @@ public virtual bool DoActionWander() { if (m_Mobile.Debug) { - m_Mobile.DebugSay("Praise the shepherd!"); + m_Mobile.DebugSay("I am being herded."); } } else if (m_Mobile.CurrentWayPoint != null) { - var point = m_Mobile.CurrentWayPoint; - if ((point.X != m_Mobile.Location.X || point.Y != m_Mobile.Location.Y) && point.Map == m_Mobile.Map && - point.Parent == null && !point.Deleted) - { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("I will move towards my waypoint."); - } + HandleWayPoint(); + } + else if (m_Mobile.IsAnimatedDead) + { + FollowMaster(); + } + else if (CheckMove() && CanMoveNow(out _) && !m_Mobile.CheckIdle()) + { + WalkRandomInHome(4, 3, 1); + } + + UpdateCombatantDirection(); + + return true; + } - DoMove(m_Mobile.GetDirectionTo(m_Mobile.CurrentWayPoint)); - } - else if (OnAtWayPoint()) + private void HandleWayPoint() + { + var point = m_Mobile.CurrentWayPoint; + + if ((point.X != m_Mobile.Location.X || point.Y != m_Mobile.Location.Y) + && point.Map == m_Mobile.Map && point.Parent == null && !point.Deleted) + { + if (m_Mobile.Debug) { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("I will go to the next waypoint"); - } - - m_Mobile.CurrentWayPoint = point.NextPoint; - if (point.NextPoint?.Deleted == true) - { - m_Mobile.CurrentWayPoint = point.NextPoint = point.NextPoint.NextPoint; - } + m_Mobile.DebugSay("I will move towards my waypoint."); } + + DoMove(m_Mobile.GetDirectionTo(point)); } - else if (m_Mobile.IsAnimatedDead) + else if (OnAtWayPoint()) { - // animated dead follow their master - var master = m_Mobile.SummonMaster; - - if (master != null && master.Map == m_Mobile.Map && master.InRange(m_Mobile, m_Mobile.RangePerception)) + if (m_Mobile.Debug) { - MoveTo(master, false, 1); + m_Mobile.DebugSay("I have reached my waypoint."); } - else + + m_Mobile.CurrentWayPoint = point.NextPoint; + + if (point.NextPoint?.Deleted == true) { - WalkRandomInHome(2, 2, 1); + m_Mobile.CurrentWayPoint = point.NextPoint = point.NextPoint.NextPoint; } } - else if (CheckMove() && CanMoveNow(out _) && !m_Mobile.CheckIdle()) + } + + private void FollowMaster() + { + var master = m_Mobile.SummonMaster; + + if (master != null && master.Map == m_Mobile.Map && master.InRange(m_Mobile, m_Mobile.RangePerception)) { - WalkRandomInHome(2, 2, 1); + MoveTo(master, false, 1); } - - if (m_Mobile.Combatant?.Deleted == false && m_Mobile.Combatant.Alive && - !m_Mobile.Combatant.IsDeadBondedPet) + else + { + WalkRandomInHome(4, 3, 1); + } + } + + private void UpdateCombatantDirection() + { + if (m_Mobile.Combatant?.Deleted == false && m_Mobile.Combatant.Alive && !m_Mobile.Combatant.IsDeadBondedPet) { m_Mobile.Direction = m_Mobile.GetDirectionTo(m_Mobile.Combatant); } - - return true; } public virtual bool DoActionCombat() @@ -993,94 +899,106 @@ public virtual bool DoActionCombat() { if (m_Mobile.Debug) { - m_Mobile.DebugSay("Praise the shepherd!"); + m_Mobile.DebugSay("I am being herded."); } return true; } - + var combatant = m_Mobile.Combatant; - - if (combatant == null || combatant.Deleted || combatant.Map != m_Mobile.Map || !combatant.Alive || - combatant.IsDeadBondedPet) + + if (!IsValidCombatant(combatant)) { if (m_Mobile.Debug) { - m_Mobile.DebugSay("My combatant is gone!"); + m_Mobile.DebugSay("My combatant is missing."); } - Action = ActionType.Wander; + if (Action != ActionType.Wander) + { + Action = ActionType.Wander; + } return true; } - + m_Mobile.Direction = m_Mobile.GetDirectionTo(combatant); + if (m_Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, combatant)) { if (m_Mobile.Debug) { - m_Mobile.DebugSay($"I used my abilities on {combatant.Name}!"); + m_Mobile.DebugSay(string.Format("I used my abilities on: {0}", combatant.Name)); } } - + return true; } + + private bool IsValidCombatant(Mobile combatant) + { + return combatant != null && !combatant.Deleted && combatant.Map == m_Mobile.Map + && combatant.Alive && !combatant.IsDeadBondedPet; + } public virtual bool DoActionGuard() { - if (Core.AOS && CheckHerding()) - { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("Praise the shepherd!"); - } - } - else if (Core.TickCount - m_NextStopGuard < 0) + if (Core.TickCount - m_NextStopGuard < 0) { if (m_Mobile.Debug) { - m_Mobile.DebugSay("I am on guard"); + m_Mobile.DebugSay("I am still on guard."); } - // m_Mobile.Turn( Utility.Random(0, 2) - 1 ); + + m_Mobile.Turn(Utility.Random(0, 2) - 1); // added for immersion } else { if (m_Mobile.Debug) { - m_Mobile.DebugSay("I stopped being on guard"); + m_Mobile.DebugSay("I stopped being on guard."); } - Action = ActionType.Wander; + if (Action != ActionType.Wander) + { + Action = ActionType.Wander; + } } - + return true; } public virtual bool DoActionFlee() { var from = m_Mobile.FocusMob; - - if (from?.Deleted != false || from.Map != m_Mobile.Map) + + if (!IsValidFocusMob(from)) { if (m_Mobile.Debug) { - m_Mobile.DebugSay("I have lost him"); + m_Mobile.DebugSay("Focused target is missing."); } - Action = ActionType.Guard; + if (Action != ActionType.Guard) + { + Action = ActionType.Guard; + } return true; } - - if (WalkMobileRange(from, 1, true, m_Mobile.RangePerception * 2, m_Mobile.RangePerception * 3)) + + if (WalkMobileRange(from, 1, true, m_Mobile.RangePerception * 2, m_Mobile.RangePerception * 1)) { if (m_Mobile.Debug) { - m_Mobile.DebugSay("I have fled"); + m_Mobile.DebugSay("I am fleeing."); } - Action = ActionType.Guard; + if (Action != ActionType.Guard) + { + Action = ActionType.Guard; + } return true; } - + if (m_Mobile.Debug) { m_Mobile.DebugSay("I am fleeing!"); @@ -1088,11 +1006,24 @@ public virtual bool DoActionFlee() return true; } + + private bool IsValidFocusMob(Mobile focusMob) + { + return focusMob != null && !focusMob.Deleted && focusMob.Map == m_Mobile.Map; + } - public virtual bool DoActionInteract() => true; + private void DebugSay(string message) + { + if (m_Mobile.Debug) + { + m_Mobile.DebugSay(message); + } + } + public virtual bool DoActionInteract() => true; + public virtual bool DoActionBackoff() => true; - + public virtual bool Obey() => !m_Mobile.Deleted && m_Mobile.ControlOrder switch { @@ -1103,7 +1034,6 @@ public virtual bool Obey() => OrderType.Unfriend => DoOrderUnfriend(), OrderType.Guard => DoOrderGuard(), OrderType.Attack => DoOrderAttack(), - OrderType.Patrol => DoOrderPatrol(), OrderType.Release => DoOrderRelease(), OrderType.Stay => DoOrderStay(), OrderType.Stop => DoOrderStop(), @@ -1111,152 +1041,175 @@ public virtual bool Obey() => OrderType.Transfer => DoOrderTransfer(), _ => false }; - + public virtual void OnCurrentOrderChanged() { if (m_Mobile.Deleted || m_Mobile.ControlMaster?.Deleted != false) { return; } - + + m_Mobile.ControlMaster.RevealingAction(); + switch (m_Mobile.ControlOrder) { case OrderType.None: - { - m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.Home = m_Mobile.Location; - m_Mobile.SetCurrentSpeedToPassive(); - m_Mobile.PlaySound(m_Mobile.GetIdleSound()); - m_Mobile.Warmode = false; - m_Mobile.Combatant = null; - break; - } - case OrderType.Come: - { - m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.SetCurrentSpeedToActive(); - m_Mobile.PlaySound(m_Mobile.GetIdleSound()); - m_Mobile.Warmode = false; - m_Mobile.Combatant = null; - break; - } - case OrderType.Drop: - { - m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.SetCurrentSpeedToPassive(); - m_Mobile.PlaySound(m_Mobile.GetIdleSound()); - m_Mobile.Warmode = false; - m_Mobile.Combatant = null; - break; - } - + case OrderType.Release: + case OrderType.Stay: + case OrderType.Stop: + case OrderType.Transfer: + HandlePassiveOrder(); + break; + case OrderType.Friend: case OrderType.Unfriend: - { - m_Mobile.ControlMaster.RevealingAction(); - break; - } - + break; + case OrderType.Guard: - { - m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.SetCurrentSpeedToActive(); - m_Mobile.PlaySound(m_Mobile.GetIdleSound()); - m_Mobile.Warmode = true; - m_Mobile.Combatant = null; - m_Mobile.ControlMaster.SendLocalizedMessage(1049671, m_Mobile.Name); // ~1_PETNAME~ is now guarding you. - break; - } - + HandleGuardOrder(); + break; + case OrderType.Attack: - { - m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.SetCurrentSpeedToActive(); - m_Mobile.PlaySound(m_Mobile.GetIdleSound()); + HandleAttackOrder(); + break; + + case OrderType.Follow: + HandleFollowOrder(); + break; - m_Mobile.Warmode = true; - m_Mobile.Combatant = null; - break; - } + case OrderType.Rename: + HandleRenameOrder(); + break; + } + } + + private void HandlePassiveOrder() + { + if (m_Mobile.Warmode) + { + m_Mobile.Warmode = false; + } + if (m_Mobile.Combatant != null) + { + m_Mobile.Combatant = null; + } + if (m_Mobile.FocusMob != null) + { + m_Mobile.FocusMob = null; + } + if (m_Mobile.CurrentSpeed != m_Mobile.PassiveSpeed) + { + m_Mobile.CurrentSpeed = m_Mobile.PassiveSpeed; + } - case OrderType.Patrol: - { - m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.SetCurrentSpeedToActive(); - m_Mobile.PlaySound(m_Mobile.GetIdleSound()); - m_Mobile.Warmode = false; - m_Mobile.Combatant = null; - break; - } + m_Mobile.PlaySound(m_Mobile.GetIdleSound()); + } + + private void HandleGuardOrder() + { + if (m_Mobile.ControlMaster == null || !m_Mobile.ControlMaster.Alive) + { + return; + } - case OrderType.Release: - { - m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.SetCurrentSpeedToPassive(); - m_Mobile.PlaySound(m_Mobile.GetIdleSound()); - m_Mobile.Warmode = false; - m_Mobile.Combatant = null; - break; - } + if (!m_Mobile.Warmode) + { + m_Mobile.Warmode = true; + } + if (m_Mobile.FocusMob != null) + { + m_Mobile.FocusMob = null; + } + if (m_Mobile.CurrentSpeed != m_Mobile.PassiveSpeed) + { + m_Mobile.CurrentSpeed = m_Mobile.PassiveSpeed; + } - case OrderType.Stay: - { - m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.SetCurrentSpeedToPassive(); - m_Mobile.PlaySound(m_Mobile.GetIdleSound()); - m_Mobile.Warmode = false; - m_Mobile.Combatant = null; - break; - } + m_Mobile.Home = m_Mobile.ControlMaster.Location; - case OrderType.Stop: - { - m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.Home = m_Mobile.Location; - m_Mobile.SetCurrentSpeedToPassive(); - m_Mobile.PlaySound(m_Mobile.GetIdleSound()); - m_Mobile.Warmode = false; - m_Mobile.Combatant = null; - break; - } + if (m_Mobile.ControlOrder != OrderType.None) + { + m_Mobile.ControlOrder = OrderType.None; + } - case OrderType.Follow: - { - m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.SetCurrentSpeedToActive(); - m_Mobile.PlaySound(m_Mobile.GetIdleSound()); + m_Mobile.PlaySound(m_Mobile.GetAttackSound()); + m_Mobile.ControlMaster.SendLocalizedMessage(1049671, m_Mobile.Name); + // 1049671: ~1_NAME~ is now guarding you. + } + + private void HandleAttackOrder() + { + if (m_Mobile.ControlMaster == null || !m_Mobile.ControlMaster.Alive) + { + return; + } - m_Mobile.Warmode = false; - m_Mobile.Combatant = null; - break; - } + if (!m_Mobile.Warmode) + { + m_Mobile.Warmode = true; + } + if (m_Mobile.FocusMob != null) + { + m_Mobile.FocusMob = null; + } + if (m_Mobile.CurrentSpeed != m_Mobile.ActiveSpeed) + { + m_Mobile.CurrentSpeed = m_Mobile.ActiveSpeed; + } - case OrderType.Transfer: - { - m_Mobile.ControlMaster.RevealingAction(); - m_Mobile.SetCurrentSpeedToPassive(); - m_Mobile.PlaySound(m_Mobile.GetIdleSound()); + m_Mobile.PlaySound(m_Mobile.GetIdleSound()); + } + + private void HandleFollowOrder() + { + if (m_Mobile.ControlMaster == null || !m_Mobile.ControlMaster.Alive) + { + return; + } - m_Mobile.Warmode = false; - m_Mobile.Combatant = null; - break; - } + if (m_Mobile.Warmode) + { + m_Mobile.Warmode = false; + } + if (m_Mobile.Combatant != null) + { + m_Mobile.Combatant = null; + } + if (m_Mobile.FocusMob != null) + { + m_Mobile.FocusMob = null; + } + if (m_Mobile.CurrentSpeed != m_Mobile.ActiveSpeed) + { + m_Mobile.CurrentSpeed = m_Mobile.ActiveSpeed; } + + m_Mobile.PlaySound(m_Mobile.GetIdleSound()); + } + + public virtual void HandleRenameOrder() + { + m_Mobile.ControlMaster.SendMessage("Change name on pet health bar."); } public virtual bool DoOrderNone() { if (m_Mobile.Debug) { - m_Mobile.DebugSay("I have no order"); + m_Mobile.DebugSay("I currently have no orders."); } + + WalkRandomInHome(4, 3, 1); + UpdateCombatantState(); + + return true; + } - WalkRandomInHome(3, 2, 1); - - if (m_Mobile.Combatant?.Deleted == false && m_Mobile.Combatant.Alive && - !m_Mobile.Combatant.IsDeadBondedPet) + private void UpdateCombatantState() + { + if (IsValidCombatant(m_Mobile.Combatant)) { m_Mobile.Warmode = true; m_Mobile.Direction = m_Mobile.GetDirectionTo(m_Mobile.Combatant); @@ -1265,8 +1218,6 @@ public virtual bool DoOrderNone() { m_Mobile.Warmode = false; } - - return true; } public virtual bool DoOrderCome() @@ -1276,44 +1227,44 @@ public virtual bool DoOrderCome() return true; } - var iCurrDist = (int)m_Mobile.GetDistanceToSqrt(m_Mobile.ControlMaster); + var currentDistance = (int)m_Mobile.GetDistanceToSqrt(m_Mobile.ControlMaster); - if (iCurrDist > m_Mobile.RangePerception) + if (currentDistance > m_Mobile.RangePerception) { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("I have lost my master. I stay here"); - } - - m_Mobile.ControlTarget = null; - m_Mobile.ControlOrder = OrderType.None; + HandleLostMaster(); } else { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("My master told me come"); - } + HandleComeOrder(currentDistance); + } - // Not exactly OSI style, but better than nothing. - var bRun = iCurrDist > 5; + return true; + } - if (WalkMobileRange(m_Mobile.ControlMaster, 1, bRun, 0, 1)) - { - if (m_Mobile.Combatant?.Deleted == false && m_Mobile.Combatant.Alive && - !m_Mobile.Combatant.IsDeadBondedPet) - { - m_Mobile.Warmode = true; - // m_Mobile.Direction = m_Mobile.GetDirectionTo(m_Mobile.Combatant, bRun); - } - else - { - m_Mobile.Warmode = false; - } - } + private void HandleLostMaster() + { + if (m_Mobile.Debug) + { + m_Mobile.DebugSay("Master is missing. Staying put."); } + + m_Mobile.ControlTarget = null; + m_Mobile.ControlOrder = OrderType.None; + } - return true; + private void HandleComeOrder(int currentDistance) + { + if (m_Mobile.Debug) + { + m_Mobile.DebugSay("I am ordered to come."); + } + + var shouldRun = currentDistance > 5; + + if (WalkMobileRange(m_Mobile.ControlMaster, 1, shouldRun, 0, 1)) + { + UpdateCombatantState(); + } } public virtual bool DoOrderDrop() @@ -1322,71 +1273,83 @@ public virtual bool DoOrderDrop() { return true; } - + if (m_Mobile.Debug) { - m_Mobile.DebugSay("I drop my stuff for my master"); + m_Mobile.DebugSay("I am ordered to drop my items."); } + + DropItems(); + + m_Mobile.ControlTarget = null; + m_Mobile.ControlOrder = OrderType.None; + + return true; + } + private void DropItems() + { var pack = m_Mobile.Backpack; - - if (pack != null) + + if (pack == null) { - var list = pack.Items; - - for (var i = list.Count - 1; i >= 0; --i) + return; + } + + var items = pack.Items; + + for (var i = items.Count - 1; i >= 0; --i) + { + if (i < items.Count) { - if (i < list.Count) - { - list[i].MoveToWorld(m_Mobile.Location, m_Mobile.Map); - } + items[i].MoveToWorld(m_Mobile.Location, m_Mobile.Map); } } - - m_Mobile.ControlTarget = null; - m_Mobile.ControlOrder = OrderType.None; - - return true; } public virtual bool CheckHerding() { var target = m_Mobile.TargetLocation; - + if (target == null) { - return false; // Creature is not being herded + return false; } - + var distance = m_Mobile.GetDistanceToSqrt(target); - - if (distance is not (< 1 or > 15)) + + if (distance >= 1 && distance <= 15) { DoMove(m_Mobile.GetDirectionTo(target)); return true; } - - if (distance < 1 && target.X == 1076 && target.Y == 450 && m_Mobile is HordeMinionFamiliar) + + if (distance < 1 && IsSpecialHerdingCase(target)) + { + HandleSpecialHerdingCase(); + } + + m_Mobile.TargetLocation = null; + return false; + } + + private bool IsSpecialHerdingCase(IPoint2D target) + { + return target.X == 1076 && target.Y == 450 && m_Mobile is HordeMinionFamiliar; + } + + private void HandleSpecialHerdingCase() + { + if (m_Mobile.ControlMaster is PlayerMobile pm && pm.Quest is DarkTidesQuest qs) { - if (m_Mobile.ControlMaster is PlayerMobile pm) + var obj = qs.FindObjective(); + + if (obj?.Completed == false) { - var qs = pm.Quest; - - if (qs is DarkTidesQuest) - { - var obj = qs.FindObjective(); - - if (obj?.Completed == false) - { - m_Mobile.AddToBackpack(new ScrollOfAbraxus()); - obj.Complete(); - } - } + m_Mobile.AddToBackpack(new ScrollOfAbraxus()); + obj.Complete(); } } - - m_Mobile.TargetLocation = null; - return false; // At the target or too far away } public virtual bool DoOrderFollow() @@ -1395,72 +1358,90 @@ public virtual bool DoOrderFollow() { if (m_Mobile.Debug) { - m_Mobile.DebugSay("Praise the shepherd!"); + DebugSay("I am being herded."); } + + return true; } - else if (m_Mobile.ControlTarget?.Deleted == false && m_Mobile.ControlTarget != m_Mobile) + + if (m_Mobile.ControlTarget?.Deleted == false && m_Mobile.ControlTarget != m_Mobile) { - var iCurrDist = (int)m_Mobile.GetDistanceToSqrt(m_Mobile.ControlTarget); - - if (iCurrDist > m_Mobile.RangePerception) + HandleFollowTarget(); + } + else + { + if (m_Mobile.Debug) { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("I have lost the one to follow. I stay here"); - } + DebugSay("I have no one to follow."); + } - if (m_Mobile.Combatant?.Deleted == false && m_Mobile.Combatant.Alive && - !m_Mobile.Combatant.IsDeadBondedPet) - { - m_Mobile.Warmode = true; - m_Mobile.Direction = m_Mobile.GetDirectionTo(m_Mobile.Combatant); - } - else - { - m_Mobile.Warmode = false; - } + if (m_Mobile.ControlTarget != null) + { + m_Mobile.ControlTarget = null; } - else + if (m_Mobile.Warmode) { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay($"My master told me to follow: {m_Mobile.ControlTarget.Name}"); - } - - // Not exactly OSI style, but better than nothing. - var bRun = iCurrDist > 5; - - if (WalkMobileRange(m_Mobile.ControlTarget, 1, bRun, 0, 1)) - { - if (m_Mobile.Combatant?.Deleted == false && m_Mobile.Combatant.Alive && - !m_Mobile.Combatant.IsDeadBondedPet) - { - m_Mobile.Warmode = true; - // m_Mobile.Direction = m_Mobile.GetDirectionTo(m_Mobile.Combatant, bRun); - } - else - { - m_Mobile.Warmode = false; - if (Core.AOS) - { - m_Mobile.CurrentSpeed = 0.1; - } - } - } + m_Mobile.Warmode = false; + } + if (m_Mobile.Combatant != null) + { + m_Mobile.Combatant = null; + } + if (m_Mobile.FocusMob != null) + { + m_Mobile.FocusMob = null; + } + if (m_Mobile.ControlOrder != OrderType.None) + { + m_Mobile.ControlOrder = OrderType.None; } } - else + + return true; + } + + private void HandleFollowTarget() + { + var currentDistance = (int)m_Mobile.GetDistanceToSqrt(m_Mobile.ControlTarget); + + if (currentDistance > m_Mobile.RangePerception) { if (m_Mobile.Debug) { - m_Mobile.DebugSay("I have nobody to follow"); + DebugSay("Target is missing. Staying put."); } - m_Mobile.ControlTarget = null; - m_Mobile.ControlOrder = OrderType.None; + UpdateCombatantState(); + } + else + { + if (m_Mobile.Debug) + { + DebugSay($"I am ordered to follow: {m_Mobile.ControlTarget.Name}"); + } + + var minFollowDist = 2; + var shouldRun = currentDistance > 6; + var maxFollowDist = shouldRun ? 4 : 3; + + if (currentDistance <= maxFollowDist + 1) + { + m_Mobile.CurrentSpeed = currentDistance <= minFollowDist ? + m_Mobile.PassiveSpeed : + (m_Mobile.PassiveSpeed + m_Mobile.ActiveSpeed) / 2; + + m_Mobile.Direction = m_Mobile.GetDirectionTo(m_Mobile.ControlTarget); + } + else + { + m_Mobile.CurrentSpeed = m_Mobile.ActiveSpeed; + if (WalkMobileRange(m_Mobile.ControlTarget, minFollowDist, + shouldRun, minFollowDist, maxFollowDist)) + { + UpdateCombatantState(); + } + } } - - return true; } public virtual bool DoOrderFriend() @@ -1468,105 +1449,123 @@ public virtual bool DoOrderFriend() var from = m_Mobile.ControlMaster; var to = m_Mobile.ControlTarget; - if (from?.Deleted != false || to?.Deleted != false || from == to || !to.Player) + + if (IsYoungPlayer(from, to)) + { + return true; + } + else if (from?.Deleted != false || to?.Deleted != false || from == to || !to.Player) { m_Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 502039); // *looks confused* } - else + else if (from.CanBeBeneficial(to, true)) { - var youngFrom = from is PlayerMobile mobile && mobile.Young; - var youngTo = to is PlayerMobile playerMobile && playerMobile.Young; - - if (youngFrom && !youngTo) - { - from.SendLocalizedMessage(502040); // As a young player, you may not friend pets to older players. - } - else if (!youngFrom && youngTo) - { - from.SendLocalizedMessage(502041); // As an older player, you may not friend pets to young players. - } - else if (from.CanBeBeneficial(to, true)) - { - NetState fromState = from.NetState, toState = to.NetState; - - if (fromState != null && toState != null) - { - if (from.HasTrade) - { - from.SendLocalizedMessage(1070947); // You cannot friend a pet with a trade pending - } - else if (to.HasTrade) - { - to.SendLocalizedMessage(1070947); // You cannot friend a pet with a trade pending - } - else if (m_Mobile.IsPetFriend(to)) - { - from.SendLocalizedMessage(1049691); // That person is already a friend. - } - else if (!m_Mobile.AllowNewPetFriend) - { - from.SendLocalizedMessage( - 1005482 - ); // Your pet does not seem to be interested in making new friends right now. - } - else - { - // ~1_NAME~ will now accept movement commands from ~2_NAME~. - from.SendLocalizedMessage(1049676, $"{m_Mobile.Name}\t{to.Name}"); - - /* ~1_NAME~ has granted you the ability to give orders to their pet ~2_PET_NAME~. - * This creature will now consider you as a friend. - */ - to.SendLocalizedMessage(1043246, $"{from.Name}\t{m_Mobile.Name}"); - - m_Mobile.AddPetFriend(to); - - m_Mobile.ControlTarget = to; - m_Mobile.ControlOrder = OrderType.Follow; - - return true; - } - } - } + HandleFriendRequest(from, to); } - + m_Mobile.ControlTarget = from; m_Mobile.ControlOrder = OrderType.Follow; - + return true; } + + private static bool IsYoungPlayer(Mobile from, Mobile to) + { + var youngFrom = from is PlayerMobile mobile && mobile.Young; + var youngTo = to is PlayerMobile playerMobile && playerMobile.Young; + + if (youngFrom && !youngTo) + { + from.SendLocalizedMessage(502040); + // 502040: As a young player, you may not friend pets to older players. + return true; + } + else if (!youngFrom && youngTo) + { + from.SendLocalizedMessage(502041); + // 502041: As an older player, you may not friend pets to young players. + return true; + } + + return false; + } + + private void HandleFriendRequest(Mobile from, Mobile to) + { + if (from.HasTrade) + { + from.SendLocalizedMessage(1070947); + // 1070947: You cannot friend a pet with a trade pending + } + else if (to.HasTrade) + { + to.SendLocalizedMessage(1070947); + // 1070947: You cannot friend a pet with a trade pending + } + else if (m_Mobile.IsPetFriend(to)) + { + from.SendLocalizedMessage(1049691); + // 1049691: That person is already a friend. + } + else if (!m_Mobile.AllowNewPetFriend) + { + from.SendLocalizedMessage(1005482); + // 1005482: Your pet does not seem to be interested in making new friends right now. + } + else + { + from.SendLocalizedMessage(1049676, $"{m_Mobile.Name}\t{to.Name}"); + to.SendLocalizedMessage(1043246, $"{from.Name}\t{m_Mobile.Name}"); + // 1049676: ~1_NAME~ will now accept movement commands from ~2_NAME~. + // 1043246: ~1_NAME~ has granted you the ability to give orders to their pet ~2_PET_NAME~. + + m_Mobile.AddPetFriend(to); + + m_Mobile.ControlTarget = to; + m_Mobile.ControlOrder = OrderType.Follow; + } + } public virtual bool DoOrderUnfriend() { var from = m_Mobile.ControlMaster; var to = m_Mobile.ControlTarget; - - if (from?.Deleted != false || to?.Deleted != false || from == to || !to.Player) + + if (IsInvalidUnfriendRequest(from, to)) { - m_Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 502039); // *looks confused* + m_Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 502039); + // 502039: *looks confused* } else if (!m_Mobile.IsPetFriend(to)) { - from.SendLocalizedMessage(1070953); // That person is not a friend. + from.SendLocalizedMessage(1070953); + // 1070953: That person is not a friend. } else { - // ~1_NAME~ will no longer accept movement commands from ~2_NAME~. - from.SendLocalizedMessage(1070951, $"{m_Mobile.Name}\t{to.Name}"); - - /* ~1_NAME~ has no longer granted you the ability to give orders to their pet ~2_PET_NAME~. - * This creature will no longer consider you as a friend. - */ - to.SendLocalizedMessage(1070952, $"{from.Name}\t{m_Mobile.Name}"); - - m_Mobile.RemovePetFriend(to); + HandleUnfriendRequest(from, to); } - + m_Mobile.ControlTarget = from; m_Mobile.ControlOrder = OrderType.Follow; - + return true; } + + private static bool IsInvalidUnfriendRequest(Mobile from, Mobile to) + { + return from?.Deleted != false || to?.Deleted != false || from == to || !to.Player; + } + + private void HandleUnfriendRequest(Mobile from, Mobile to) + { + from.SendLocalizedMessage(1070951, $"{m_Mobile.Name}\t{to.Name}"); + to.SendLocalizedMessage(1070952, $"{from.Name}\t{m_Mobile.Name}"); + // 1070951: ~1_NAME~ will no longer accept movement commands from ~2_NAME~. + // 1070952: ~1_NAME~ has no longer granted you the ability to give orders to their pet ~2_PET_NAME~. + + m_Mobile.RemovePetFriend(to); + } public virtual bool DoOrderGuard() { @@ -1574,25 +1573,40 @@ public virtual bool DoOrderGuard() { return true; } - + var controlMaster = m_Mobile.ControlMaster; - + if (controlMaster?.Deleted != false) { return true; } + + var combatant = FindCombatant(controlMaster); + + if (IsValidCombatant(combatant)) + { + GuardFromTarget(combatant); + } + else + { + GuardNothing(controlMaster); + } + + return true; + } + private Mobile FindCombatant(Mobile controlMaster) + { var combatant = m_Mobile.Combatant; - var aggressors = controlMaster.Aggressors; - + if (aggressors.Count > 0) { for (var i = 0; i < aggressors.Count; ++i) { var info = aggressors[i]; var attacker = info.Attacker; - + if (attacker?.Deleted == false && attacker.GetDistanceToSqrt(m_Mobile) <= m_Mobile.RangePerception) { @@ -1603,49 +1617,50 @@ public virtual bool DoOrderGuard() } } } - + if (combatant != null && m_Mobile.Debug) { - m_Mobile.DebugSay("Crap, my master has been attacked! I will attack one of those bastards!"); + m_Mobile.DebugSay("Master is under attack. Assisting..."); } } - - if (combatant?.Deleted == false && combatant != m_Mobile && combatant != m_Mobile.ControlMaster && - combatant.Alive && !combatant.IsDeadBondedPet && m_Mobile.CanSee(combatant) && - m_Mobile.CanBeHarmful(combatant, false) && combatant.Map == m_Mobile.Map) + + return combatant; + } + + private void GuardFromTarget(Mobile combatant) + { + if (m_Mobile.Debug) + { + m_Mobile.DebugSay("Guarding target."); + } + + m_Mobile.Combatant = combatant; + m_Mobile.FocusMob = combatant; + + if (Action != ActionType.Combat) { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("Guarding from target..."); - } - - m_Mobile.Combatant = combatant; - m_Mobile.FocusMob = combatant; Action = ActionType.Combat; - - /* - * We need to call Think() here or spell casting monsters will not use - * spells when guarding because their target is never processed. - */ - Think(); } - else + + // used to update spell caster combat states + Think(); + } + + private void GuardNothing(Mobile controlMaster) + { + if (m_Mobile.Debug) { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("Nothing to guard from"); - } - - m_Mobile.Warmode = false; - if (Core.AOS) - { - m_Mobile.CurrentSpeed = 0.1; - } - - WalkMobileRange(controlMaster, 1, false, 0, 1); + m_Mobile.DebugSay("There is nothing to guard."); } - - return true; + + m_Mobile.Warmode = false; + + if (Core.AOS && m_Mobile.CurrentSpeed != 0.1) + { + m_Mobile.CurrentSpeed = 0.1; + } + + WalkMobileRange(controlMaster, 1, false, 0, 1); } public virtual bool DoOrderAttack() @@ -1654,67 +1669,10 @@ public virtual bool DoOrderAttack() { return true; } - - if (m_Mobile.ControlTarget?.Deleted != false || m_Mobile.ControlTarget.Map != m_Mobile.Map || - !m_Mobile.ControlTarget.Alive || m_Mobile.ControlTarget.IsDeadBondedPet) + + if (IsInvalidControlTarget(m_Mobile.ControlTarget)) { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay( - "I think he might be dead. He's not anywhere around here at least. That's cool. I'm glad he's dead." - ); - } - - if (Core.AOS) - { - m_Mobile.ControlTarget = m_Mobile.ControlMaster; - m_Mobile.ControlOrder = OrderType.Follow; - } - else - { - m_Mobile.ControlTarget = null; - m_Mobile.ControlOrder = OrderType.None; - } - - if (m_Mobile.FightMode is FightMode.Closest or FightMode.Aggressor) - { - Mobile newCombatant = null; - var newScore = 0.0; - - foreach (var aggr in m_Mobile.GetMobilesInRange(m_Mobile.RangePerception)) - { - if (!m_Mobile.CanSee(aggr) || aggr.Combatant != m_Mobile) - { - continue; - } - - if (aggr.IsDeadBondedPet || !aggr.Alive) - { - continue; - } - - var aggrScore = m_Mobile.GetFightModeRanking(aggr, FightMode.Closest, false); - - if ((newCombatant == null || aggrScore > newScore) && m_Mobile.InLOS(aggr)) - { - newCombatant = aggr; - newScore = aggrScore; - } - } - - if (newCombatant != null) - { - m_Mobile.ControlTarget = newCombatant; - m_Mobile.ControlOrder = OrderType.Attack; - m_Mobile.Combatant = newCombatant; - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("But -that- is not dead. Here we go again..."); - } - - Think(); - } - } + HandleInvalidControlTarget(); } else { @@ -1722,78 +1680,185 @@ public virtual bool DoOrderAttack() { m_Mobile.DebugSay("Attacking target..."); } - + Think(); } - + return true; } - - public virtual bool DoOrderPatrol() + + private bool IsInvalidControlTarget(Mobile target) + { + return target?.Deleted != false || target.Map != m_Mobile.Map || !target.Alive || target.IsDeadBondedPet; + } + + private void HandleInvalidControlTarget() { if (m_Mobile.Debug) { - m_Mobile.DebugSay("This order is not yet coded"); + m_Mobile.DebugSay("Target missing. Either dead, hidden, or out of range."); + } + + if (Core.AOS) + { + m_Mobile.ControlTarget = m_Mobile.ControlMaster; + m_Mobile.ControlOrder = OrderType.Follow; + } + else + { + m_Mobile.ControlTarget = null; + m_Mobile.ControlOrder = OrderType.None; + } + + if (m_Mobile.FightMode is FightMode.Closest or FightMode.Aggressor) + { + FindNewCombatant(); + } + } + + private void FindNewCombatant() + { + Mobile newCombatant = null; + var newScore = 0.0; + + foreach (var aggr in m_Mobile.GetMobilesInRange(m_Mobile.RangePerception)) + { + if (!m_Mobile.CanSee(aggr) || aggr.Combatant != m_Mobile || aggr.IsDeadBondedPet || !aggr.Alive) + { + continue; + } + + var aggrScore = m_Mobile.GetFightModeRanking(aggr, FightMode.Closest, false); + + if ((newCombatant == null || aggrScore > newScore) && m_Mobile.InLOS(aggr)) + { + newCombatant = aggr; + newScore = aggrScore; + } + } + + if (newCombatant != null) + { + m_Mobile.ControlTarget = newCombatant; + m_Mobile.ControlOrder = OrderType.Attack; + m_Mobile.Combatant = newCombatant; + + if (m_Mobile.Debug) + { + m_Mobile.DebugSay("Target is still alive. Resuming attacks..."); + } + + Think(); } - - return true; } public virtual bool DoOrderRelease() { if (m_Mobile.Debug) { - m_Mobile.DebugSay("I have been released"); + m_Mobile.DebugSay("I have been released to the wild."); } - + m_Mobile.PlaySound(m_Mobile.GetAngerSound()); - + + ReleaseControl(); + ResetBonding(); + SetHomeLocation(); + HandleDeletion(); + + return true; + } + + private void ReleaseControl() + { m_Mobile.SetControlMaster(null); m_Mobile.SummonMaster = null; - + } + + private void ResetBonding() + { m_Mobile.BondingBegin = DateTime.MinValue; m_Mobile.OwnerAbandonTime = DateTime.MinValue; m_Mobile.IsBonded = false; - + } + + private void SetHomeLocation() + { var spawner = m_Mobile.Spawner; - + if (spawner != null && spawner.HomeLocation != Point3D.Zero) { m_Mobile.Home = spawner.HomeLocation; m_Mobile.RangeHome = spawner.HomeRange; } - + } + + private void HandleDeletion() + { if (m_Mobile.DeleteOnRelease || m_Mobile.IsDeadPet) { m_Mobile.Delete(); } - - m_Mobile.BeginDeleteTimer(); - - if (m_Mobile.CanDrop) + else { - m_Mobile.DropBackpack(); + m_Mobile.BeginDeleteTimer(); + + if (m_Mobile.CanDrop) + { + m_Mobile.DropBackpack(); + } } - - return true; } public virtual bool DoOrderStay() { - if (m_Mobile.Debug) + if (CheckHerding()) { - if (CheckHerding()) + if (m_Mobile.Debug) { - m_Mobile.DebugSay("Praise the shepherd!"); + m_Mobile.DebugSay("I am being herded."); } - else + } + else + { + if (m_Mobile.Debug) { - m_Mobile.DebugSay("My master told me to stay"); + m_Mobile.DebugSay("I have been ordered to stay."); } } - + + HandleStayOrder(); + return true; } + + private void HandleStayOrder() + { + if (m_Mobile.ControlOrder != OrderType.None) + { + m_Mobile.Direction = m_Mobile.GetDirectionTo(m_Mobile.ControlMaster); + m_Mobile.Home = m_Mobile.Location; + + if (m_Mobile.ControlTarget != null) + { + m_Mobile.ControlTarget = null; + } + if (m_Mobile.Warmode) + { + m_Mobile.Warmode = false; + } + if (m_Mobile.Combatant != null) + { + m_Mobile.Combatant = null; + } + if (m_Mobile.FocusMob != null) + { + m_Mobile.FocusMob = null; + } + + m_Mobile.ControlOrder = OrderType.None; + } + } public virtual bool DoOrderStop() { @@ -1801,27 +1866,43 @@ public virtual bool DoOrderStop() { return true; } - + if (m_Mobile.Debug) { - m_Mobile.DebugSay("My master told me to stop."); + m_Mobile.DebugSay("I have been ordered to stop."); } + + HandleStopOrder(); + + return true; + } + + private void HandleStopOrder() + { + if (m_Mobile.ControlOrder != OrderType.Stop) + { + m_Mobile.Direction = m_Mobile.GetDirectionTo(m_Mobile.ControlMaster); + m_Mobile.Home = m_Mobile.Location; - m_Mobile.Direction = m_Mobile.GetDirectionTo(m_Mobile.ControlMaster); - m_Mobile.Home = m_Mobile.Location; - - m_Mobile.ControlTarget = null; + if (m_Mobile.ControlTarget != null) + { + m_Mobile.ControlTarget = null; + } + if (m_Mobile.Warmode) + { + m_Mobile.Warmode = false; + } + if (m_Mobile.Combatant != null) + { + m_Mobile.Combatant = null; + } + if (m_Mobile.FocusMob != null) + { + m_Mobile.FocusMob = null; + } - if (Core.ML) - { - WalkRandomInHome(3, 2, 1); - } - else - { - m_Mobile.ControlOrder = OrderType.None; + m_Mobile.ControlOrder = OrderType.Stop; } - - return true; } public virtual bool DoOrderTransfer() @@ -1834,483 +1915,559 @@ public virtual bool DoOrderTransfer() var from = m_Mobile.ControlMaster; var to = m_Mobile.ControlTarget; - if (from?.Deleted == false && to?.Deleted == false && from != to && to.Player) + if (IsValidTransferRequest(from, to)) { if (m_Mobile.Debug) { - m_Mobile.DebugSay($"Begin transfer with {to.Name}"); + m_Mobile.DebugSay($"Beginning transfer with {to.Name}"); } - var youngFrom = from is PlayerMobile mobile && mobile.Young; - var youngTo = to is PlayerMobile playerMobile && playerMobile.Young; - - if (youngFrom && !youngTo) + if (IsYoungPlayer(from, to)) { - from.SendLocalizedMessage(502051); // As a young player, you may not transfer pets to older players. - } - else if (!youngFrom && youngTo) - { - from.SendLocalizedMessage(502052); // As an older player, you may not transfer pets to young players. + return true; } else if (!m_Mobile.CanBeControlledBy(to)) { - var args = $"{to.Name}\t{from.Name}\t "; - - from.SendLocalizedMessage( - 1043248, - args - ); // The pet refuses to be transferred because it will not obey ~1_NAME~.~3_BLANK~ - to.SendLocalizedMessage( - 1043249, - args - ); // The pet will not accept you as a master because it does not trust you.~3_BLANK~ + SendTransferRefusalMessages(from, to, 1043248, 1043249); + // 1043248: The pet refuses to be transferred because it will not obey ~1_NAME~.~3_BLANK~ + // 1043249: The pet will not accept you as a master because it does not trust you.~3_BLANK~ } else if (!m_Mobile.CanBeControlledBy(from)) { - var args = $"{to.Name}\t{from.Name}\t "; + SendTransferRefusalMessages(from, to, 1043250, 1043251); + // 1043250: The pet refuses to be transferred because it will not obey you sufficiently.~3_BLANK~ + // 1043251: The pet will not accept you as a master because it does not trust ~2_NAME~.~3_BLANK~ + } + else if (IsInCombatState()) + { + from.SendMessage("You can not transfer a pet while in combat."); + to.SendMessage("You can not transfer a pet while in combat."); + } + + var fromState = from.NetState; + var toState = to.NetState; - from.SendLocalizedMessage( - 1043250, - args - ); // The pet refuses to be transferred because it will not obey you sufficiently.~3_BLANK~ - to.SendLocalizedMessage( - 1043251, - args - ); // The pet will not accept you as a master because it does not trust ~2_NAME~.~3_BLANK~ + if (fromState == null || toState == null) + { + return false; } - else if (TransferItem.IsInCombat(m_Mobile)) + + if (from.HasTrade || to.HasTrade) { - from.SendMessage("You may not transfer a pet that has recently been in combat."); - to.SendMessage("The pet may not be transferred to you because it has recently been in combat."); + from.SendLocalizedMessage(1010507); + // 1010507: You cannot transfer a pet with a trade pending + to.SendLocalizedMessage(1010507); + // 1010507: You cannot transfer a pet with a trade pending + return false; } else { - NetState fromState = from.NetState, toState = to.NetState; + var container = fromState.AddTrade(toState); + container.DropItem(new TransferItem(m_Mobile)); + } + } - if (fromState != null && toState != null) - { - if (from.HasTrade) - { - from.SendLocalizedMessage(1010507); // You cannot transfer a pet with a trade pending - } - else if (to.HasTrade) - { - to.SendLocalizedMessage(1010507); // You cannot transfer a pet with a trade pending - } - else - { - Container c = fromState.AddTrade(toState); - c.DropItem(new TransferItem(m_Mobile)); - } - } - } + if (m_Mobile.ControlTarget != null) + { + m_Mobile.ControlTarget = null; + } + if (m_Mobile.ControlOrder != OrderType.Stay) + { + m_Mobile.ControlOrder = OrderType.Stay; } - m_Mobile.ControlTarget = null; - m_Mobile.ControlOrder = OrderType.Stay; + m_Mobile.PlaySound(m_Mobile.GetIdleSound()); return true; } + private bool IsInCombatState() + { + return m_Mobile.Combatant != null || m_Mobile.Aggressors.Count > 0 || + m_Mobile.Aggressed.Count > 0 || Core.TickCount < m_Mobile.NextCombatTime; + } + + private static bool IsValidTransferRequest(Mobile from, Mobile to) + { + return from?.Deleted == false && to?.Deleted == false && from != to && to.Player; + } + + private static void SendTransferRefusalMessages(Mobile from, Mobile to, int fromMessage, int toMessage) + { + var args = $"{to.Name}\t{from.Name}\t "; + from.SendLocalizedMessage(fromMessage, args); + to.SendLocalizedMessage(toMessage, args); + } + public virtual bool DoBardPacified() { if (Core.Now < m_Mobile.BardEndTime) { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("I am pacified, I wait"); - } - - m_Mobile.Combatant = null; - m_Mobile.Warmode = false; + HandlePacifiedState(); } else { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("I'm not pacified any longer"); - } - - m_Mobile.BardPacified = false; + HandleEndOfPacification(); } - + return true; } + + private void HandlePacifiedState() + { + if (m_Mobile.Debug) + { + m_Mobile.DebugSay("I am pacified. Can not fight."); + } + + if (m_Mobile.Combatant != null) + { + m_Mobile.Combatant = null; + } + if (m_Mobile.Warmode) + { + m_Mobile.Warmode = false; + } + } + + private void HandleEndOfPacification() + { + if (m_Mobile.Debug) + { + m_Mobile.DebugSay("I am free from pacification."); + } + + m_Mobile.BardPacified = false; + } public virtual bool DoBardProvoked() { - if (Core.Now >= m_Mobile.BardEndTime && - (m_Mobile.BardMaster?.Deleted != false || - m_Mobile.BardMaster.Map != m_Mobile.Map || m_Mobile.GetDistanceToSqrt(m_Mobile.BardMaster) > - m_Mobile.RangePerception)) + if (Core.Now >= m_Mobile.BardEndTime && IsProvokerLost()) + { + HandleLostProvoker(); + } + else if (IsProvokeTargetLost()) + { + HandleLostProvokeTarget(); + } + else + { + HandleProvokeCombat(); + } + + return true; + } + + private bool IsProvokerLost() + { + return m_Mobile.BardMaster?.Deleted != false || + m_Mobile.BardMaster.Map != m_Mobile.Map || + m_Mobile.GetDistanceToSqrt(m_Mobile.BardMaster) > m_Mobile.RangePerception; + } + + private bool IsProvokeTargetLost() + { + return m_Mobile.BardTarget?.Deleted != false || + m_Mobile.BardTarget.Map != m_Mobile.Map || + m_Mobile.GetDistanceToSqrt(m_Mobile.BardTarget) > m_Mobile.RangePerception; + } + + private void HandleLostProvoker() + { + if (m_Mobile.Debug) + { + m_Mobile.DebugSay("Provoker missing."); + } + + if (!m_Mobile.BardProvoked) { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("I have lost my provoker"); - } - m_Mobile.BardProvoked = false; + } + if (m_Mobile.BardMaster != null) + { m_Mobile.BardMaster = null; + } + if (m_Mobile.BardTarget != null) + { m_Mobile.BardTarget = null; - + } + if (m_Mobile.Combatant != null) + { m_Mobile.Combatant = null; + } + if (m_Mobile.Warmode) + { m_Mobile.Warmode = false; } - else if (m_Mobile.BardTarget?.Deleted != false || m_Mobile.BardTarget.Map != m_Mobile.Map || - m_Mobile.GetDistanceToSqrt(m_Mobile.BardTarget) > m_Mobile.RangePerception) + } + + private void HandleLostProvokeTarget() + { + if (m_Mobile.Debug) + { + m_Mobile.DebugSay("Provoke target missing."); + } + + if (m_Mobile.BardProvoked) { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("I have lost my provoke target"); - } - m_Mobile.BardProvoked = false; + } + if (m_Mobile.BardMaster != null) + { m_Mobile.BardMaster = null; + } + if (m_Mobile.BardTarget != null) + { m_Mobile.BardTarget = null; - + } + if (m_Mobile.Combatant != null) + { m_Mobile.Combatant = null; - m_Mobile.Warmode = false; } - else + if (m_Mobile.Warmode) { - m_Mobile.Combatant = m_Mobile.BardTarget; - m_Action = ActionType.Combat; - - m_Mobile.OnThink(); - Think(); + m_Mobile.Warmode = false; } - - return true; } - - public virtual void WalkRandom(int iChanceToNotMove, int iChanceToDir, int iSteps) + + private void HandleProvokeCombat() { - if (m_Mobile.Deleted || m_Mobile.DisallowAllMoves) + m_Mobile.Combatant = m_Mobile.BardTarget; + + if (Action != ActionType.Combat) { - return; + Action = ActionType.Combat; } + + m_Mobile.OnThink(); + + Think(); + } - if (iChanceToNotMove <= 0) + public virtual void WalkRandom(int chanceToNotMove, int chanceToDir, int steps) + { + if (ShouldNotMove(chanceToNotMove)) { return; } - - for (var i = 0; i < iSteps; i++) + + for (var i = 0; i < steps; i++) { - if (Utility.Random(1 + iChanceToNotMove) == 0) + if (Utility.Random(1 + chanceToNotMove) == 0) { - // 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; + var direction = GetRandomDirection(chanceToDir); DoMove(direction); } } } + + private bool ShouldNotMove(int chanceToNotMove) + { + return m_Mobile.Deleted || m_Mobile.DisallowAllMoves || chanceToNotMove <= 0; + } + + private Direction GetRandomDirection(int chanceToDir) + { + var randomMove = Utility.Random(8 * (chanceToDir + 1)); + return randomMove < 8 ? (Direction)randomMove : m_Mobile.Direction; + } - public static double TransformMoveDelay(BaseCreature bc, bool isPassive = false) + public static double TransformMoveDelay(BaseCreature bc, double maxReduction = 0.4) { if (bc == null) { return 0.1; } - - if (bc.MoveSpeedMod > 0) + + var moveSpeed = bc.CurrentSpeed; + + if (!bc.IsDeadPet && (bc.ReduceSpeedWithDamage || bc.IsSubdued)) { - return bc.MoveSpeedMod; + int stats = Core.HS ? bc.Stam : bc.Hits; + int statsMax = Core.HS ? bc.StamMax : bc.HitsMax; + + var statRatio = statsMax <= 0 ? 0.0 : Math.Max(0, stats) / (double)statsMax; + moveSpeed += (1.0 - statRatio) * maxReduction; } + + return Math.Max(0.1, moveSpeed); + } + + // this needs to match ai interval + private const int MovementTimingTolerance = 50; - 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 - }; + public bool CanMoveNow(out long delay) + { + delay = NextMove - Core.TickCount; + return delay <= MovementTimingTolerance; + } - if (isPassive) + public virtual bool CheckMove() + { + if (m_Mobile.Deleted || m_Mobile.DisallowAllMoves) { - movementSpeed += 0.2; + return false; } + return true; + } + + public virtual bool DoMove(Direction d, bool badStateOk = false) + { + var res = DoMoveImpl(d, badStateOk); + return IsMoveSuccessful(res, badStateOk); + } + + private static bool IsMoveSuccessful(MoveResult res, bool badStateOk) + { + return res is MoveResult.Success or MoveResult.SuccessAutoTurn ||(badStateOk && res == MoveResult.BadState); + } - if (!isControlled) + public virtual MoveResult DoMoveImpl(Direction d, bool badStateOk) + { + var isInBadState = IsInBadState(); + if (isInBadState) { - movementSpeed += 0.1; + return MoveResult.BadState; } - else if (!bc.Summoned) + + if (!CanMoveNow(out var timeUntilMove)) { - if (bc.ControlOrder == OrderType.Follow && bc.ControlTarget == bc.ControlMaster) + if (badStateOk) { - movementSpeed *= 0.5; + AIMovementTimerPool.GetTimer(TimeSpan.FromMilliseconds(timeUntilMove), this, d).Start(); } + return MoveResult.BadState; + } + + if ((m_Mobile.Direction & Direction.Mask) != (d & Direction.Mask)) + { + m_Mobile.Direction = d; + } + + m_Mobile.Pushing = false; + var mobDirection = m_Mobile.Direction; + + var moveResult = TryMove(d); + + if (moveResult) + { + TransformMoveDelay(m_Mobile); + HandleCombatDelay(); + return MoveResult.Success; + } + + if ((mobDirection & Direction.Mask) != (d & Direction.Mask)) + { + m_Mobile.Direction = d; + return MoveResult.SuccessAutoTurn; + } + + return HandleBlockedMovement(d, mobDirection); + } - movementSpeed -= 0.075; + private void HandleCombatDelay() + { + if (m_Mobile == null) + { + return; } - if (!bc.IsDeadPet && (bc.ReduceSpeedWithDamage || bc.IsSubdued)) + if (m_Mobile.Warmode || m_Mobile.Combatant != null) { - int stats, statsMax; - if (Core.HS) - { - stats = bc.Stam; - statsMax = bc.StamMax; - } - else + var maxWait = Utility.RandomMinMax(1000, 2000); + var remaining = Math.Max(0, m_Mobile.NextCombatTime - Core.TickCount); + + if (remaining < maxWait) { - stats = bc.Hits; - statsMax = bc.HitsMax; + m_Mobile.NextCombatTime = Core.TickCount + maxWait; } - - var offset = statsMax <= 0 ? 1.0 : Math.Max(0, stats) / (double)statsMax; - - movementSpeed += (1.0 - offset) * 0.8; } - - return movementSpeed; } - public bool CanMoveNow(out long delay) => (delay = NextMove - Core.TickCount) <= FuzzyTimeUntilNextMove; + private bool TryMove(Direction d) + { + MoveImpl.IgnoreMovableImpassables = m_Mobile.CanMoveOverObstacles && !m_Mobile.CanDestroyObstacles; + var result = m_Mobile.Move(d); + MoveImpl.IgnoreMovableImpassables = false; + return result; + } - public virtual bool CheckMove() => true; + private bool IsInBadState() => + m_Mobile == null || m_Mobile.Deleted || m_Mobile.Frozen || m_Mobile.Paralyzed || + m_Mobile.Spell?.IsCasting == true || m_Mobile.DisallowAllMoves; - public virtual bool DoMove(Direction d, bool badStateOk = false) + private MoveResult HandleBlockedMovement(Direction d, Direction mobDirection) { - var res = DoMoveImpl(d, badStateOk); + var wasPushing = m_Mobile.Pushing; - return res is MoveResult.Success or MoveResult.SuccessAutoTurn || badStateOk && res == MoveResult.BadState; + if ((m_Mobile.CanOpenDoors || m_Mobile.CanDestroyObstacles) && !TryClearObstacles(d)) + { + return MoveResult.Success; + } + + return TryAlternateMovement(wasPushing); } - public virtual MoveResult DoMoveImpl(Direction d, bool badStateOk) + private MoveResult TryAlternateMovement(bool wasPushing) { - if (m_Mobile == null || m_Mobile.Deleted || m_Mobile.Frozen || m_Mobile.Paralyzed || - m_Mobile.Spell?.IsCasting == true || m_Mobile.DisallowAllMoves) - { - return MoveResult.BadState; - } + var offset = Utility.RandomDouble() < 0.4 ? 1 : -1; - if (!CheckMove()) + for (var i = 0; i < 2; ++i) { - return MoveResult.BadState; + m_Mobile.TurnInternal(offset); + + if (m_Mobile.Move(m_Mobile.Direction)) + { + return MoveResult.SuccessAutoTurn; + } } - var delay = (int)(TransformMoveDelay(m_Mobile) * 1000); + return wasPushing ? MoveResult.BadState : MoveResult.Blocked; + } - if (!CanMoveNow(out var timeUntilMove)) + private bool TryClearObstacles(Direction d) + { + if (m_Mobile.Debug) { - // 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(); - } + m_Mobile.DebugSay("My movement is blocked. Trying to push through."); + } - return MoveResult.BadState; + var map = m_Mobile.Map; + if (map == null) + { + return true; } - // 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; - NextMove += delay; + var (x, y) = GetOffsetLocation(d); + using var queue = PooledRefQueue.Create(); + var destroyables = GatherObstacles(x, y, queue); - if (Core.TickCount - NextMove > 0) + if (destroyables > 0) { - NextMove = Core.TickCount; + Effects.PlaySound(new Point3D(x, y, m_Mobile.Z), m_Mobile.Map, 0x3B3); } - m_Mobile.Pushing = false; - var mobDirection = m_Mobile.Direction; + return ProcessObstacles(queue, d); + } - // Do the actual move - MoveImpl.IgnoreMovableImpassables = m_Mobile.CanMoveOverObstacles && !m_Mobile.CanDestroyObstacles; - var moveResult = m_Mobile.Move(d); - MoveImpl.IgnoreMovableImpassables = false; + private (int x, int y) GetOffsetLocation(Direction d) + { + var x = m_Mobile.X; + var y = m_Mobile.Y; + Movement.Movement.Offset(d, ref x, ref y); + return (x, y); + } - if (moveResult) + private int GatherObstacles(int x, int y, PooledRefQueue queue) + { + var destroyables = 0; + + foreach (var item in m_Mobile.Map.GetItemsInRange(new Point2D(x, y), 1)) { - // 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) + if (IsValidDoor(item, x, y) || IsValidDestroyableItem(item)) { - var remaining = m_Mobile.NextCombatTime - Core.TickCount; - var maxWait = Math.Min(delay, 400); - if (remaining < maxWait) + queue.Enqueue(item); + if (item is not BaseDoor) { - m_Mobile.NextCombatTime = Core.TickCount + maxWait; + destroyables++; } } - return MoveResult.Success; } - if ((mobDirection & Direction.Mask) != (d & Direction.Mask)) + return destroyables; + } + + private bool IsValidDoor(Item item, int x, int y) + { + if (!m_Mobile.CanOpenDoors || item is not BaseDoor door) { - return MoveResult.Blocked; + return false; } - var wasPushing = m_Mobile.Pushing; - - var blocked = true; - - var canOpenDoors = m_Mobile.CanOpenDoors; - var canDestroyObstacles = m_Mobile.CanDestroyObstacles; + if (door.Z + door.ItemData.Height <= m_Mobile.Z || m_Mobile.Z + 16 <= door.Z) + { + return false; + } - if (canOpenDoors || canDestroyObstacles) + if (door.X != x || door.Y != y) { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("My movement was blocked, I will try to clear some obstacles."); - } + return false; + } - var map = m_Mobile.Map; + return !door.Locked || !door.UseLocks(); + } - if (map != null) - { - var x = m_Mobile.X; - var y = m_Mobile.Y; - Movement.Movement.Offset(d, ref x, ref y); + private bool IsValidDestroyableItem(Item item) + { + if (!m_Mobile.CanDestroyObstacles || !item.Movable || !item.ItemData.Impassable) + { + return false; + } - using var queue = PooledRefQueue.Create(); - var destroyables = 0; - foreach (var item in map.GetItemsInRange(new Point2D(x, y), 1)) - { - if (canOpenDoors && item is BaseDoor door && door.Z + door.ItemData.Height > m_Mobile.Z && - m_Mobile.Z + 16 > door.Z) - { - if (door.X != x || door.Y != y) - { - continue; - } - - if (!door.Locked || !door.UseLocks()) - { - queue.Enqueue(door); - } - - if (!canDestroyObstacles) - { - break; - } - } - else if (canDestroyObstacles && item.Movable && item.ItemData.Impassable && - item.Z + item.ItemData.Height > m_Mobile.Z && m_Mobile.Z + 16 > item.Z) - { - if (!m_Mobile.InRange(item.GetWorldLocation(), 1)) - { - continue; - } + if (item.Z + item.ItemData.Height <= m_Mobile.Z || m_Mobile.Z + 16 <= item.Z) + { + return false; + } + + return m_Mobile.InRange(item.GetWorldLocation(), 1); + } - queue.Enqueue(item); - ++destroyables; - } - } + private bool ProcessObstacles(PooledRefQueue queue, Direction d) + { + if (queue.Count == 0) + { + return true; + } - if (destroyables > 0) - { - Effects.PlaySound(new Point3D(x, y, m_Mobile.Z), m_Mobile.Map, 0x3B3); - } + while (queue.Count > 0) + { + var item = queue.Dequeue(); + ProcessObstacle(item, queue); + } - if (queue.Count > 0) - { - blocked = false; // retry movement - } + return !m_Mobile.Move(d); + } - while (queue.Count > 0) - { - var item = queue.Dequeue(); + private void ProcessObstacle(Item item, PooledRefQueue queue) + { + if (item is BaseDoor door) + { + if (m_Mobile.Debug) + { + m_Mobile.DebugSay("Opening the door."); + } - if (item is BaseDoor door) - { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay( - "Little do they expect, I've learned how to open doors. Didn't they read the script??" - ); - } - - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("*twist*"); - } - - door.Use(m_Mobile); - } - else - { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay( - $"Ugabooga. I'm so big and tough I can destroy it: {item.GetType().Name}" - ); - } - - if (item is Container cont) - { - for (var i = 0; i < cont.Items.Count; ++i) - { - var check = cont.Items[i]; - - if (check.Movable && check.ItemData.Impassable && - cont.Z + check.ItemData.Height > m_Mobile.Z) - { - queue.Enqueue(check); - } - } - - cont.Destroy(); - } - else - { - item.Delete(); - } - } - } + door.Use(m_Mobile); + } + else + { + if (m_Mobile.Debug) + { + m_Mobile.DebugSay($"Destroying item: {item.GetType().Name}"); + } - if (!blocked) - { - blocked = !m_Mobile.Move(d); - } + if (item is Container cont) + { + ProcessContainer(cont, queue); + cont.Destroy(); + } + else + { + item.Delete(); } } + } - if (blocked) + private void ProcessContainer(Container cont, PooledRefQueue queue) + { + for (var i = 0; i < cont.Items.Count; ++i) { - var offset = Utility.RandomDouble() < 0.4 ? 1 : -1; - - for (var i = 0; i < 2; ++i) + var check = cont.Items[i]; + if (check.Movable && check.ItemData.Impassable && cont.Z + check.ItemData.Height > m_Mobile.Z) { - m_Mobile.TurnInternal(offset); - - if (m_Mobile.Move(m_Mobile.Direction)) - { - return MoveResult.SuccessAutoTurn; - } + queue.Enqueue(check); } - - return wasPushing ? MoveResult.BadState : MoveResult.Blocked; } - - return MoveResult.Success; } public virtual void WalkRandomInHome(int iChanceToNotMove, int iChanceToDir, int iSteps) @@ -2322,106 +2479,104 @@ public virtual void WalkRandomInHome(int iChanceToNotMove, int iChanceToDir, int if (m_Mobile.Home == Point3D.Zero) { - if (m_Mobile.Spawner is RegionSpawner rs) - { - Region region = rs.SpawnRegion; + HandleNoHome(iChanceToNotMove, iChanceToDir, iSteps); + } + else + { + HandleHomeMovement(iChanceToNotMove, iChanceToDir, iSteps); + } + } - if (m_Mobile.Region.AcceptsSpawnsFrom(region)) - { - m_Mobile.WalkRegion = region; - WalkRandom(iChanceToNotMove, iChanceToDir, iSteps); - m_Mobile.WalkRegion = null; - } - else - { - if (region.GoLocation != Point3D.Zero && Utility.RandomBool()) - { - DoMove(m_Mobile.GetDirectionTo(region.GoLocation)); - } - else - { - WalkRandom(iChanceToNotMove, iChanceToDir, 1); - } - } + private void HandleNoHome(int iChanceToNotMove, int iChanceToDir, int iSteps) + { + if (m_Mobile.Spawner is RegionSpawner rs) + { + Region region = rs.SpawnRegion; + + if (m_Mobile.Region.AcceptsSpawnsFrom(region)) + { + m_Mobile.WalkRegion = region; + WalkRandom(iChanceToNotMove, iChanceToDir, iSteps); + m_Mobile.WalkRegion = null; + } + else if (region.GoLocation != Point3D.Zero && Utility.RandomBool()) + { + DoMove(m_Mobile.GetDirectionTo(region.GoLocation)); } else { - WalkRandom(iChanceToNotMove, iChanceToDir, iSteps); + WalkRandom(iChanceToNotMove, iChanceToDir, 1); } } else { - for (var i = 0; i < iSteps; i++) - { - if (m_Mobile.RangeHome != 0) - { - var iCurrDist = (int)m_Mobile.GetDistanceToSqrt(m_Mobile.Home); - - if (iCurrDist < m_Mobile.RangeHome * 2 / 3) - { - WalkRandom(iChanceToNotMove, iChanceToDir, 1); - } - else if (iCurrDist > m_Mobile.RangeHome) - { - 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); - } - } - else - { - if (m_Mobile.Location != m_Mobile.Home) - { - DoMove(m_Mobile.GetDirectionTo(m_Mobile.Home)); - } - } - } + WalkRandom(iChanceToNotMove, iChanceToDir, iSteps); } } - public virtual bool CheckFlee() + private void HandleHomeMovement(int iChanceToNotMove, int iChanceToDir, int iSteps) { - if (m_Mobile.CheckFlee()) + if (m_Mobile.RangeHome == 0) + { + if (m_Mobile.Location != m_Mobile.Home) + { + DoMove(m_Mobile.GetDirectionTo(m_Mobile.Home)); + } + return; + } + + for (var i = 0; i < iSteps; i++) { - var combatant = m_Mobile.Combatant; + var iCurrDist = (int)m_Mobile.GetDistanceToSqrt(m_Mobile.Home); - if (combatant == null) + if (iCurrDist > m_Mobile.RangeHome) { - WalkRandom(1, 2, 1); + DoMove(m_Mobile.GetDirectionTo(m_Mobile.Home)); + } + else if (iCurrDist < m_Mobile.RangeHome * 2 / 3 || Utility.Random(10) <= 5) + { + WalkRandom(iChanceToNotMove, iChanceToDir, 1); } else { - var d = combatant.GetDirectionTo(m_Mobile); - - d = (Direction)((int)d + Utility.RandomMinMax(-1, +1)); - - m_Mobile.Direction = d; - m_Mobile.Move(d); + DoMove(m_Mobile.GetDirectionTo(m_Mobile.Home)); } - - return true; } + } - return false; + public virtual bool CheckFlee() + { + if (!m_Mobile.CheckFlee()) + { + return false; + } + + var combatant = m_Mobile.Combatant; + + if (combatant == null) + { + WalkRandom(1, 2, 1); + } + else + { + var direction = combatant.GetDirectionTo(m_Mobile); + direction = (Direction)((int)direction + Utility.RandomMinMax(-1, +1)); + + m_Mobile.Direction = direction; + m_Mobile.Move(direction); + } + + return true; } public virtual void OnTeleported() { - if (m_Path != null) + if (m_Mobile.Debug && m_Path != null) { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("Teleported; repathing"); - } - - m_Path.ForceRepath(); + m_Mobile.DebugSay("Teleported; recalculating path."); } + + m_Path?.ForceRepath(); } public virtual bool MoveTo(Mobile m, bool run, int range) @@ -2437,25 +2592,17 @@ public virtual bool MoveTo(Mobile m, bool run, int range) return true; } - if (m_Path?.Goal == m) + if (m_Path == null && DoMove(m_Mobile.GetDirectionTo(m), true)) { - if (m_Path.Follow(run, 1)) - { - m_Path = null; - return true; - } + return true; } - else if (!DoMove(m_Mobile.GetDirectionTo(m), true)) + + if (m_Path?.Goal != m) { m_Path = new PathFollower(m_Mobile, m) { Mover = DoMoveImpl }; - - if (m_Path.Follow(run, 1)) - { - m_Path = null; - return true; - } } - else + + if (m_Path.Follow(run, 1)) { m_Path = null; return true; @@ -2467,58 +2614,28 @@ public virtual bool MoveTo(Mobile m, bool run, int range) /* * Walk at range distance from mobile * - * iSteps : Number of steps - * bRun : Do we run - * iWantDistMin : The minimum distance we want to be + * iSteps : Number of steps + * bRun : Do we run + * iWantDistMin : The minimum distance we want to be * iWantDistMax : The maximum distance we want to be * */ public virtual bool WalkMobileRange(Mobile m, int iSteps, bool run, int iWantDistMin, int iWantDistMax) { - if (m_Mobile.Deleted || m_Mobile.DisallowAllMoves) - { - return false; - } - - if (m == null) + if (m_Mobile.Deleted || m_Mobile.DisallowAllMoves || m == null) { return false; } for (var i = 0; i < iSteps; i++) { - // Get the current distance var iCurrDist = (int)m_Mobile.GetDistanceToSqrt(m); if (iCurrDist < iWantDistMin || iCurrDist > iWantDistMax) { - var needCloser = iCurrDist > iWantDistMax; - - if (needCloser && m_Path != null && m_Path.Goal == m) - { - if (m_Path.Follow(run, 1)) - { - m_Path = null; - } - } - else + if (!MoveTowardsOrAwayFrom(m, run, iCurrDist, iWantDistMax)) { - var dirTo = iCurrDist > iWantDistMax ? - 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(run, 1)) - { - m_Path = null; - } - } - else - { - m_Path = null; - } + return false; } } else @@ -2527,10 +2644,44 @@ public virtual bool WalkMobileRange(Mobile m, int iSteps, bool run, int iWantDis } } - // Get the current distance - var iNewDist = (int)m_Mobile.GetDistanceToSqrt(m); + return m_Mobile.GetDistanceToSqrt(m) is var dist && + dist >= iWantDistMin && dist <= iWantDistMax; + } + + private bool MoveTowardsOrAwayFrom(Mobile m, bool run, int iCurrDist, int iWantDistMax) + { + var needCloser = iCurrDist > iWantDistMax; + + if (needCloser && m_Path?.Goal == m) + { + if (m_Path.Follow(run, 1)) + { + m_Path = null; + return true; + } + } + else + { + var dirTo = needCloser ? m_Mobile.GetDirectionTo(m, run) : m.GetDirectionTo(m_Mobile, run); + + if (DoMove(dirTo, true)) + { + m_Path = null; + return true; + } + + if (needCloser) + { + m_Path = new PathFollower(m_Mobile, m) { Mover = DoMoveImpl }; + if (m_Path.Follow(run, 1)) + { + m_Path = null; + return true; + } + } + } - return iNewDist >= iWantDistMin && iNewDist <= iWantDistMax; + return false; } /* @@ -2545,409 +2696,414 @@ public virtual bool WalkMobileRange(Mobile m, int iSteps, bool run, int iWantDis */ public virtual bool AcquireFocusMob(int iRange, FightMode acqType, bool bPlayerOnly, bool bFacFriend, bool bFacFoe) { - if (m_Mobile.Deleted) + if (m_Mobile?.Deleted != false || m_Mobile.Map == null || acqType == FightMode.None) { return false; } - if (m_Mobile.BardProvoked) + if (HandleBardProvoked() || HandleControlled() || HandleConstantFocus()) { - if (m_Mobile.BardTarget?.Deleted != false) - { - m_Mobile.FocusMob = null; - return false; - } + return true; + } - m_Mobile.FocusMob = m_Mobile.BardTarget; - return m_Mobile.FocusMob != null; + if (acqType == FightMode.None) + { + m_Mobile.FocusMob = null; + return false; } - if (m_Mobile.Controlled) + if (HandleAggressor(acqType)) { - if (m_Mobile.ControlTarget?.Deleted != false || m_Mobile.ControlTarget.Hidden || - !m_Mobile.ControlTarget.Alive || m_Mobile.ControlTarget.IsDeadBondedPet || - !m_Mobile.InRange(m_Mobile.ControlTarget, m_Mobile.RangePerception * 2)) - { - if (m_Mobile.ControlTarget != null && m_Mobile.ControlTarget != m_Mobile.ControlMaster) - { - m_Mobile.ControlTarget = null; - } + return false; + } - m_Mobile.FocusMob = null; - return false; - } + if (Core.TickCount - m_Mobile.NextReacquireTime < 0) + { + m_Mobile.FocusMob = null; + return false; + } - m_Mobile.FocusMob = m_Mobile.ControlTarget; - return m_Mobile.FocusMob != null; + m_Mobile.NextReacquireTime = Core.TickCount + (int)m_Mobile.ReacquireDelay.TotalMilliseconds; + + if (m_Mobile.Debug) + { + m_Mobile.DebugSay("Acquiring new target..."); } - if (m_Mobile.ConstantFocus != null) + if (m_Mobile.Map == null) { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay("Acquired my constant focus"); - } + return m_Mobile.FocusMob != null; + } - m_Mobile.FocusMob = m_Mobile.ConstantFocus; - return true; + return AcquireNewFocusMob(m_Mobile.Map, iRange, acqType, bPlayerOnly, bFacFriend, bFacFoe); + } + + private bool HandleBardProvoked() + { + if (!m_Mobile.BardProvoked) + { + return false; } - if (acqType == FightMode.None) + if (m_Mobile.BardTarget?.Deleted != false) { m_Mobile.FocusMob = null; return false; } - if (acqType == FightMode.Aggressor && m_Mobile.Aggressors.Count == 0 && m_Mobile.Aggressed.Count == 0 && - m_Mobile.FactionAllegiance == null && m_Mobile.EthicAllegiance == null) + m_Mobile.FocusMob = m_Mobile.BardTarget; + return true; + } + + private bool HandleControlled() + { + if (!m_Mobile.Controlled) { - m_Mobile.FocusMob = null; return false; } - if (Core.TickCount - m_Mobile.NextReacquireTime < 0) + if (m_Mobile.ControlTarget?.Deleted != false || m_Mobile.ControlTarget.Hidden || + !m_Mobile.ControlTarget.Alive || m_Mobile.ControlTarget.IsDeadBondedPet || + !m_Mobile.InRange(m_Mobile.ControlTarget, m_Mobile.RangePerception * 2)) { + if (m_Mobile.ControlTarget != null && m_Mobile.ControlTarget != m_Mobile.ControlMaster) + { + m_Mobile.ControlTarget = null; + } + m_Mobile.FocusMob = null; return false; } - m_Mobile.NextReacquireTime = Core.TickCount + (int)m_Mobile.ReacquireDelay.TotalMilliseconds; + m_Mobile.FocusMob = m_Mobile.ControlTarget; + return true; + } + + private bool HandleConstantFocus() + { + if (m_Mobile.ConstantFocus == null) + { + return false; + } if (m_Mobile.Debug) { - m_Mobile.DebugSay("Acquiring..."); + m_Mobile.DebugSay("Acquired focused target."); } - var map = m_Mobile.Map; - - if (map == null) + m_Mobile.FocusMob = m_Mobile.ConstantFocus; + return true; + } + + private bool HandleAggressor(FightMode acqType) + { + if (acqType != FightMode.Aggressor || + m_Mobile.Aggressors.Count > 0 || + m_Mobile.Aggressed.Count > 0 || + m_Mobile.FactionAllegiance != null || + m_Mobile.EthicAllegiance != null) { - // TODO: Is this correct? Maybe it should return false? - return m_Mobile.FocusMob != null; + return false; } + m_Mobile.FocusMob = null; + return true; + } + + private bool AcquireNewFocusMob(Map map, int iRange, FightMode acqType, bool bPlayerOnly, bool bFacFriend, bool bFacFoe) + { Mobile newFocusMob = null; var val = double.MinValue; Mobile enemySummonMob = null; var enemySummonVal = double.MinValue; - + foreach (var m in map.GetMobilesInRange(m_Mobile.Location, iRange)) { - if (m.Deleted || m.Blessed) - { - continue; - } - - // Let's not target ourselves... - if (m == m_Mobile || m is BaseFamiliar) - { - continue; - } - - // Dead targets are invalid. - if (!m.Alive || m.IsDeadBondedPet) - { - continue; - } - - // Staff members cannot be targeted. - if (m.AccessLevel > AccessLevel.Player) - { - continue; - } - - // Does it have to be a player? - if (bPlayerOnly && !m.Player) - { - continue; - } - - // Can't acquire a target we can't see. - if (!m_Mobile.CanSee(m)) + var isInvalidTarget = IsInvalidTarget(m, bPlayerOnly); + if (isInvalidTarget) { continue; } - + var bc = m as BaseCreature; var pm = m as PlayerMobile; - - // Monster don't attack it's own summon or the summon of another monster - if (Core.AOS && bc != null && bc.Summoned && (bc.SummonMaster == m_Mobile || (!bc.SummonMaster.Player && IsHostile(bc.SummonMaster)))) - { - continue; - } - - if (m_Mobile.Summoned && m_Mobile.SummonMaster != null) - { - // If this is a summon, it can't target its controller. - if (m == m_Mobile.SummonMaster) - { - continue; - } - - // It also must abide by harmful spell rules. - if (!SpellHelper.ValidIndirectTarget(m_Mobile.SummonMaster, m)) - { - continue; - } - - // Animated creatures cannot attack players directly. - if (pm != null && m_Mobile.IsAnimatedDead) - { - continue; - } - - // Animated creatures cannot attack other animated creatures - if (m_Mobile.IsAnimatedDead && bc?.IsAnimatedDead == true) - { - continue; - } - - // Animated creatures cannot attack pets of other players - if (m_Mobile.IsAnimatedDead && bc?.Controlled == true) - { - continue; - } - } - - // If we only want faction friends, make sure it's one. - if (bFacFriend && !m_Mobile.IsFriend(m)) - { - continue; - } - - // Ignore anyone under EtherealVoyage - if (TransformationSpellHelper.UnderTransformation(m, typeof(EtherealVoyageSpell))) - { - continue; - } - - // Ignore players with activated honor - if (m_Mobile.Combatant != m && VirtueSystem.GetVirtues(pm)?.HonorActive == true) - { - continue; - } - - if (acqType is FightMode.Aggressor or FightMode.Evil) - { - var bValid = IsHostile(m); - - if (!bValid) - { - bValid = m_Mobile.GetFactionAllegiance(m) == BaseCreature.Allegiance.Enemy || - m_Mobile.GetEthicAllegiance(m) == BaseCreature.Allegiance.Enemy; - } - - if (acqType == FightMode.Evil && !bValid) - { - if (bc?.Controlled == true && bc?.ControlMaster != null) - { - bValid = bc.ControlMaster.Karma < 0; - } - else - { - bValid = m.Karma < 0; - } - } - - if (!bValid) - { - continue; - } - } - else - { - // Same goes for faction enemies. - if (bFacFoe && !m_Mobile.IsEnemy(m)) - { - continue; - } - - // If it's an enemy factioned mobile, make sure we can be harmful to it. - if (bFacFoe && !bFacFriend && !m_Mobile.CanBeHarmful(m, false)) - { - continue; - } + + var isInvalidSummonTarget = IsInvalidSummonTarget(m, bc, pm); + var isInvalidFactionTarget = IsInvalidFactionTarget(m, bFacFriend, bFacFoe); + var isInvalidFightModeTarget = IsInvalidFightModeTarget(m, acqType, bc); + + if (isInvalidSummonTarget || isInvalidFactionTarget || isInvalidFightModeTarget) + { + continue; } - + var theirVal = m_Mobile.GetFightModeRanking(m, acqType, bPlayerOnly); if (theirVal > val && m_Mobile.InLOS(m)) { newFocusMob = m; val = theirVal; } - // The summon is targeted when nothing else around. Otherwise this monster enters idle mode. - // Do a check for this edge case so players cannot abuse by casting EVs offscreen to kill an idle monster. else if (Core.AOS && theirVal > enemySummonVal && m_Mobile.InLOS(m) && bc?.Summoned == true && bc?.Controlled != true) { enemySummonMob = m; enemySummonVal = theirVal; } } - + m_Mobile.FocusMob = newFocusMob ?? enemySummonMob; return m_Mobile.FocusMob != null; } + + private bool IsInvalidTarget(Mobile m, bool bPlayerOnly) + { + return m.Deleted || m.Blessed || m == m_Mobile || m is BaseFamiliar || !m.Alive || m.IsDeadBondedPet || + m.AccessLevel > AccessLevel.Player || (bPlayerOnly && !m.Player) || !m_Mobile.CanSee(m); + } + + private bool IsInvalidSummonTarget(Mobile m, BaseCreature bc, PlayerMobile pm) + { + if (Core.AOS && bc?.Summoned == true && + (bc.SummonMaster == m_Mobile || (!bc.SummonMaster.Player && IsHostile(bc.SummonMaster)))) + { + return true; + } - private bool IsHostile(Mobile from) + if (m_Mobile.Summoned && m_Mobile.SummonMaster != null) + { + return m == m_Mobile.SummonMaster || + !SpellHelper.ValidIndirectTarget(m_Mobile.SummonMaster, m) || + (pm != null && m_Mobile.IsAnimatedDead) || + (m_Mobile.IsAnimatedDead && (bc?.IsAnimatedDead == true) || bc?.Controlled == true); + } + + return false; + } + + private bool IsInvalidFactionTarget(Mobile m, bool bFacFriend, bool bFacFoe) { - if (m_Mobile.Combatant == from || from.Combatant == m_Mobile) + if (bFacFriend && !m_Mobile.IsFriend(m)) { return true; } - var count = Math.Max(m_Mobile.Aggressors.Count, m_Mobile.Aggressed.Count); + if (TransformationSpellHelper.UnderTransformation(m, typeof(EtherealVoyageSpell)) || + (m_Mobile.Combatant != m && VirtueSystem.GetVirtues(m as PlayerMobile)?.HonorActive == true)) + { + return true; + } + + return bFacFoe && (!m_Mobile.IsEnemy(m) || (!bFacFriend && !m_Mobile.CanBeHarmful(m, false))); + } + + private bool IsInvalidFightModeTarget(Mobile m, FightMode acqType, BaseCreature bc) + { + if (acqType is not (FightMode.Aggressor or FightMode.Evil)) + { + return false; + } + + var bValid = IsHostile(m) || + m_Mobile.GetFactionAllegiance(m) == BaseCreature.Allegiance.Enemy || + m_Mobile.GetEthicAllegiance(m) == BaseCreature.Allegiance.Enemy; + + if (!bValid && acqType == FightMode.Evil) + { + bValid = bc?.Controlled == true && bc?.ControlMaster != null + ? bc.ControlMaster.Karma < 0 + : m.Karma < 0; + } - for (var a = 0; a < count; ++a) + return !bValid; + } + + private bool IsHostile(Mobile from) + { + return m_Mobile.Combatant == from || + from.Combatant == m_Mobile || + IsAggressor(from) || + IsAggressed(from); + } + + private bool IsAggressor(Mobile from) + { + foreach (var aggressor in m_Mobile.Aggressors) { - if (a < m_Mobile.Aggressed.Count && m_Mobile.Aggressed[a].Attacker == from) + if (aggressor.Defender == from) { return true; } - - if (a < m_Mobile.Aggressors.Count && m_Mobile.Aggressors[a].Defender == from) + } + return false; + } + + private bool IsAggressed(Mobile from) + { + foreach (var aggressed in m_Mobile.Aggressed) + { + if (aggressed.Attacker == from) { return true; } } - return false; } public virtual void DetectHidden() { - if (m_Mobile.Deleted || m_Mobile.Map == null) + if (m_Mobile?.Deleted != false || m_Mobile.Map == null || !CanDetectHidden) { return; } - + if (m_Mobile.Debug) { - m_Mobile.DebugSay("Checking for hidden players"); + m_Mobile.DebugSay("Checking for hidden entities..."); } - + var srcSkill = m_Mobile.Skills.DetectHidden.Value; - if (srcSkill <= 0) { return; } - + foreach (var trg in m_Mobile.GetMobilesInRange(m_Mobile.RangePerception)) { - if (trg != m_Mobile && trg.Player && trg.Alive && trg.Hidden && trg.AccessLevel == AccessLevel.Player && - m_Mobile.InLOS(trg)) + if (IsValidTargetCombatTarget(trg)) { - if (m_Mobile.Debug) - { - m_Mobile.DebugSay($"Trying to detect {trg.Name}"); - } - - var trgHiding = trg.Skills.Hiding.Value / 2.9; - var trgStealth = trg.Skills.Stealth.Value / 1.8; + TryDetectHidden(trg, srcSkill); + } + } + } - var chance = srcSkill / 1.2 - Math.Min(trgHiding, trgStealth); + private bool IsValidTargetCombatTarget(Mobile trg) => + trg != m_Mobile && + trg.Player && + trg.Alive && + trg.Hidden && + trg.AccessLevel == AccessLevel.Player && + m_Mobile.InLOS(trg); - if (chance < srcSkill / 10) - { - chance = srcSkill / 10; - } + private void TryDetectHidden(Mobile trg, double srcSkill) + { + if (m_Mobile.Debug) + { + m_Mobile.DebugSay($"Trying to detect: {trg.Name}"); + } - chance /= 100; + var trgHiding = trg.Skills.Hiding.Value / 2.9; + var trgStealth = trg.Skills.Stealth.Value / 1.8; + var chance = Math.Max(srcSkill / 10, srcSkill / 1.2 - Math.Min(trgHiding, trgStealth)) / 100; - if (chance > Utility.RandomDouble()) - { - trg.RevealingAction(); - trg.SendLocalizedMessage(500814); // You have been revealed! - } - } + if (chance > Utility.RandomDouble()) + { + trg.RevealingAction(); + trg.SendLocalizedMessage(500814); + // 500814: You have been revealed! } } public virtual void Deactivate() { - if (m_Mobile.PlayerRangeSensitive) + if (!m_Mobile.PlayerRangeSensitive) { - m_Timer.Stop(); + return; + } - var spawner = m_Mobile.Spawner; + m_Timer.Stop(); - if (spawner?.ReturnOnDeactivate == true && !m_Mobile.Controlled && ( - spawner.HomeLocation == Point3D.Zero && !m_Mobile.Region.AcceptsSpawnsFrom(spawner.Region) || - !m_Mobile.InRange(spawner.HomeLocation, spawner.HomeRange) - )) - { - Timer.StartTimer(ReturnToHome); - } + var spawner = m_Mobile.Spawner; + + if (ShouldReturnToHome((Server.Engines.Spawners.Spawner)spawner)) + { + Timer.StartTimer(ReturnToHome); } } + private bool ShouldReturnToHome(Spawner spawner) + { + return spawner?.ReturnOnDeactivate == true && + !m_Mobile.Controlled && ( + spawner.HomeLocation == Point3D.Zero || + !m_Mobile.InRange(spawner.HomeLocation, spawner.HomeRange) + ); + } + private void ReturnToHome() { - if (m_Mobile.Spawner != null) + if (m_Mobile.Spawner is not { } spawner) { - var loc = m_Mobile.Spawner.GetSpawnPosition(m_Mobile, m_Mobile.Spawner.Map); + return; + } - if (loc != Point3D.Zero) - { - m_Mobile.MoveToWorld(loc, m_Mobile.Spawner.Map); - } + var loc = spawner.GetSpawnPosition(m_Mobile, spawner.Map); + + if (loc != Point3D.Zero) + { + m_Mobile.MoveToWorld(loc, spawner.Map); } + + m_Timer.Start(); } public virtual void Activate() { if (!m_Timer.Running) { - // We want to randomize the time at which the AI activates. - // This triggers when a mob is first created since it moves from the internal map to it's added location - // If we spawn lots of mobs, we don't want their AI synchronized exactly. - m_Timer.Delay = TimeSpan.FromMilliseconds(Utility.Random(48) * 8); m_Timer.Start(); } } - + public virtual void OnCurrentSpeedChanged() { - m_Timer.Interval = TimeSpan.FromSeconds(Math.Max(0.008, m_Mobile.CurrentSpeed)); + // changed to 50ms, because at 8ms, AI updates 125 times a second + // at 50ms, AI updates 20 times a second which is better for performance + m_Timer.Interval = TimeSpan.FromMilliseconds(Math.Max(50, m_Mobile.CurrentSpeed * 1000)); } - private class InternalEntry : ContextMenuEntry + private sealed class InternalEntry : ContextMenuEntry { private readonly OrderType _order; - + public InternalEntry(int number, int range, OrderType order, bool enabled) : base(number, range) { _order = order; Enabled = enabled; } - + public override void OnClick(Mobile from, IEntity target) { - if (!from.CheckAlive() || target is not BaseCreature { Deleted: not true, Controlled: true } bc) + if (!IsValidClick(from, target, out var bc) || + IsInvalidOrderForDeadPet(bc) || + !IsOwnerOrFriend(from, bc, out var isFriend) || + IsInvalidOrderForFriend(isFriend)) { return; } - - // Just in case - if (bc.IsDeadPet && _order is OrderType.Guard or OrderType.Attack or OrderType.Transfer or OrderType.Drop) - { - return; - } - + + HandleOrder(from, bc); + } + + private static bool IsValidClick(Mobile from, IEntity target, out BaseCreature bc) + { + bc = target as BaseCreature; + return from.CheckAlive() && bc != null && !bc.Deleted && bc.Controlled; + } + + private bool IsInvalidOrderForDeadPet(BaseCreature bc) => + bc.IsDeadPet && _order is OrderType.Guard or OrderType.Attack or OrderType.Transfer or OrderType.Drop; + + private static bool IsOwnerOrFriend(Mobile from, BaseCreature bc, out bool isFriend) + { var isOwner = from == bc.ControlMaster; - var isFriend = !isOwner && bc.IsPetFriend(from); - - if (!isOwner && !isFriend) - { - return; - } - - if (isFriend && _order != OrderType.Follow && _order != OrderType.Stay && _order != OrderType.Stop) - { - return; - } - + isFriend = !isOwner && bc.IsPetFriend(from); + return isOwner || isFriend; + } + + private bool IsInvalidOrderForFriend(bool isFriend) => + isFriend && _order is not (OrderType.Follow or OrderType.Stay or OrderType.Stop); + + private void HandleOrder(Mobile from, BaseCreature bc) + { switch (_order) { case OrderType.Follow: @@ -2955,274 +3111,324 @@ public override void OnClick(Mobile from, IEntity target) case OrderType.Transfer: case OrderType.Friend: case OrderType.Unfriend: - { - if (_order == OrderType.Transfer && from.HasTrade) - { - from.SendLocalizedMessage(1010507); // You cannot transfer a pet with a trade pending - } - else if (_order == OrderType.Friend && from.HasTrade) - { - from.SendLocalizedMessage(1070947); // You cannot friend a pet with a trade pending - } - else - { - bc.AIObject.BeginPickTarget(from, _order); - } - - break; - } + HandleTargetOrder(from, bc); + break; case OrderType.Release: - { - if (bc.Summoned) - { - goto default; - } - - from.SendGump(new ConfirmReleaseGump(from, bc)); - - break; - } + HandleReleaseOrder(from, bc); + break; default: - { - if (bc.CheckControlChance(from)) - { - bc.ControlOrder = _order; - } - - break; - } + HandleDefaultOrder(from, bc); + break; + } + } + + private void HandleTargetOrder(Mobile from, BaseCreature bc) + { + if (_order is OrderType.Transfer or OrderType.Friend && from.HasTrade) + { + from.SendLocalizedMessage(_order == OrderType.Transfer ? 1010507 : 1070947); + // 1010507: You cannot transfer a pet with a trade pending + // 1070947: You cannot friend a pet with a trade pending + return; + } + + bc.AIObject.BeginPickTarget(from, _order); + } + + private void HandleReleaseOrder(Mobile from, BaseCreature bc) + { + if (bc.Summoned) + { + HandleDefaultOrder(from, bc); + return; + } + + from.SendGump(new ConfirmReleaseGump(from, bc)); + } + + private void HandleDefaultOrder(Mobile from, BaseCreature bc) + { + if (bc.CheckControlChance(from)) + { + bc.ControlOrder = _order; } } } - private class TransferItem : Item + private sealed class TransferItem : Item { private readonly BaseCreature m_Creature; - + public TransferItem(BaseCreature creature) : base(ShrinkTable.Lookup(creature)) { m_Creature = creature; - Movable = false; - + if (!Core.AOS) { Name = creature.Name; } else if (ItemID == ShrinkTable.DefaultItemID || - creature.GetType().IsDefined(typeof(FriendlyNameAttribute), false) || creature is Reptalon) + creature.GetType().IsDefined(typeof(FriendlyNameAttribute), false) || + creature is Reptalon) { Name = FriendlyNameAttribute.GetFriendlyNameFor(creature.GetType()).ToString(); } - - // (As Per OSI)No name. Normally, set by the ItemID of the Shrink Item unless we either explicitly set it with an Attribute, or, no lookup found - + Hue = creature.Hue & 0x0FFF; } - + public TransferItem(Serial serial) : base(serial) { } - + public static bool IsInCombat(BaseCreature creature) => - creature != null && (creature.Aggressors.Count > 0 || creature.Aggressed.Count > 0); - + creature?.Aggressors.Count > 0 || creature?.Aggressed.Count > 0; + public override void Serialize(IGenericWriter writer) { base.Serialize(writer); - writer.Write(0); // version } - + public override void Deserialize(IGenericReader reader) { base.Deserialize(reader); - - var version = reader.ReadInt(); - + reader.ReadInt(); // version Delete(); } - + public override void GetProperties(IPropertyList list) { base.GetProperties(list); - - list.Add(1041603); // This item represents a pet currently in consideration for trade - list.Add(1041601, m_Creature.Name); // Pet Name: ~1_val~ - + list.Add(1041603); + // 1041603: This item represents a pet currently in consideration for trade + list.Add(1041601, m_Creature.Name); + // 1041601: Pet Name: ~1_val~ + if (m_Creature.ControlMaster != null) { list.Add(1041602, m_Creature.ControlMaster.Name); // Owner: ~1_val~ } } - + public override bool AllowSecureTrade(Mobile from, Mobile to, Mobile newOwner, bool accepted) { - if (!base.AllowSecureTrade(from, to, newOwner, accepted)) - { - return false; - } - - if (Deleted || m_Creature?.Deleted != false || m_Creature.ControlMaster != from || - !from.CheckAlive() || !to.CheckAlive()) + if (!base.AllowSecureTrade(from, to, newOwner, accepted) || IsInvalidTrade(from, to)) { return false; } - if (from.Map != m_Creature.Map || !from.InRange(m_Creature, 14)) + return !accepted || HandleAcceptedTrade(from, to); + } + + private bool IsInvalidTrade(Mobile from, Mobile to) => + Deleted || m_Creature?.Deleted != false || m_Creature.ControlMaster != from || + !from.CheckAlive() || !to.CheckAlive() || from.Map != m_Creature.Map || !from.InRange(m_Creature, 14); + + private bool HandleAcceptedTrade(Mobile from, Mobile to) + { + if (!ValidateYoungStatus(from, to) || + !ValidateControlStatus(from, to) || + !ValidateFollowerLimit(to) || + IsInCombat(m_Creature)) { return false; } + return true; + } + + private static bool ValidateYoungStatus(Mobile from, Mobile to) + { var youngFrom = from is PlayerMobile mobile && mobile.Young; var youngTo = to is PlayerMobile playerMobile && playerMobile.Young; - if (accepted && youngFrom && !youngTo) + if (youngFrom && !youngTo) { - from.SendLocalizedMessage(502051); // As a young player, you may not transfer pets to older players. + from.SendLocalizedMessage(502051); + // 502051: As a young player, you may not transfer pets to older players. + return false; } - else if (accepted && !youngFrom && youngTo) + + if (!youngFrom && youngTo) { - from.SendLocalizedMessage(502052); // As an older player, you may not transfer pets to young players. + from.SendLocalizedMessage(502052); + // 502052: As an older player, you may not transfer pets to young players. + return false; } - else if (accepted && !m_Creature.CanBeControlledBy(to)) - { - var args = $"{to.Name}\t{from.Name}\t "; - - // The pet refuses to be transferred because it will not obey ~1_NAME~.~3_BLANK~ - from.SendLocalizedMessage(1043248, args); - // The pet will not accept you as a master because it does not trust you.~3_BLANK~ - to.SendLocalizedMessage(1043249, args); + return true; + } + + private bool ValidateControlStatus(Mobile from, Mobile to) + { + if (!m_Creature.CanBeControlledBy(to)) + { + SendTransferRefusalMessages(from, to, 1043248, 1043249); + // 1043248: The pet refuses to be transferred because it will not obey ~1_NAME~.~3_BLANK~ + // 1043249: The pet will not accept you as a master because it does not trust you.~3_BLANK~ return false; } - else if (accepted && !m_Creature.CanBeControlledBy(from)) - { - var args = $"{to.Name}\t{from.Name}\t "; - // The pet refuses to be transferred because it will not obey you sufficiently.~3_BLANK~ - from.SendLocalizedMessage(1043250, args); - // The pet will not accept you as a master because it does not trust ~2_NAME~.~3_BLANK~ - to.SendLocalizedMessage(1043251, args); - } - else if (accepted && to.Followers + m_Creature.ControlSlots > to.FollowersMax) + if (!m_Creature.CanBeControlledBy(from)) { - to.SendLocalizedMessage(1049607); // You have too many followers to control that creature. - + SendTransferRefusalMessages(from, to, 1043250, 1043251); + // 1043250: The pet refuses to be transferred because it will not obey you sufficiently.~3_BLANK~ + // 1043251: The pet will not accept you as a master because it does not trust ~2_NAME~.~3_BLANK~ return false; } - else if (accepted && IsInCombat(m_Creature)) - { - from.SendMessage("You may not transfer a pet that has recently been in combat."); - to.SendMessage("The pet may not be transferred to you because it has recently been in combat."); + return true; + } + + private bool ValidateFollowerLimit(Mobile to) + { + if (to.Followers + m_Creature.ControlSlots > to.FollowersMax) + { + to.SendLocalizedMessage(1049607); + // 1049607: You cannot have more followers than your follower limit. return false; } - return true; } - + + private static void SendTransferRefusalMessages(Mobile from, Mobile to, int fromMessage, int toMessage) + { + var args = $"{to.Name}\t{from.Name}\t "; + from.SendLocalizedMessage(fromMessage, args); + to.SendLocalizedMessage(toMessage, args); + } + public override void OnSecureTrade(Mobile from, Mobile to, Mobile newOwner, bool accepted) { - if (Deleted) + if (Deleted || IsInvalidTrade(from, to)) { + Delete(); return; } Delete(); - if (m_Creature?.Deleted != false || m_Creature.ControlMaster != from || !from.CheckAlive() || - !to.CheckAlive()) + if (!accepted || !m_Creature.SetControlMaster(to)) { return; } - if (from.Map != m_Creature.Map || !from.InRange(m_Creature, 14)) + TransferPetOwnership(from, to); + } + + private void TransferPetOwnership(Mobile from, Mobile to) + { + if (m_Creature.Summoned) { - return; + m_Creature.SummonMaster = to; } - if (accepted && m_Creature.SetControlMaster(to)) - { - if (m_Creature.Summoned) - { - m_Creature.SummonMaster = to; - } - - m_Creature.ControlTarget = to; - m_Creature.ControlOrder = OrderType.Follow; + m_Creature.ControlTarget = to; + m_Creature.ControlOrder = OrderType.Follow; - m_Creature.BondingBegin = DateTime.MinValue; - m_Creature.OwnerAbandonTime = DateTime.MinValue; - m_Creature.IsBonded = false; + m_Creature.BondingBegin = DateTime.MinValue; + m_Creature.OwnerAbandonTime = DateTime.MinValue; + m_Creature.IsBonded = false; - m_Creature.PlaySound(m_Creature.GetIdleSound()); + m_Creature.PlaySound(m_Creature.GetIdleSound()); - var args = $"{from.Name}\t{m_Creature.Name}\t{to.Name}"; - - from.SendLocalizedMessage(1043253, args); // You have transferred your pet to ~3_GETTER~. - // ~1_NAME~ has transferred the allegiance of ~2_PET_NAME~ to you. - to.SendLocalizedMessage(1043252, args); - } + var args = $"{from.Name}\t{m_Creature.Name}\t{to.Name}"; + from.SendLocalizedMessage(1043253, args); + // 1043253: You have transferred your pet to ~3_GETTER~. + to.SendLocalizedMessage(1043252, args); + // 1043252: ~1_NAME~ has transferred the allegiance of ~2_PET_NAME~ to you. } } - /* - * The Timer object - */ - private class AITimer : Timer + private sealed class AITimer : Timer { private readonly BaseAI m_Owner; + private int _detectHiddenMinDelay; + private int _detectHiddenMaxDelay; public AITimer(BaseAI owner) : base( - TimeSpan.FromMilliseconds(Utility.Random(96) * 8), - TimeSpan.FromSeconds(Math.Max(0.0, owner.m_Mobile.CurrentSpeed)) - ) + TimeSpan.FromMilliseconds(Utility.RandomMinMax(100, 200)), + TimeSpan.FromMilliseconds(GetBaseInterval(owner))) { m_Owner = owner; m_Owner.m_NextDetectHidden = Core.TickCount; } + private static double GetBaseInterval(BaseAI owner) + { + var baseInterval = Math.Max(100.0, owner.m_Mobile.CurrentSpeed * 1000); + + if (owner.m_Mobile.Warmode || owner.m_Mobile.Combatant != null) + { + return Math.Max(200.0, baseInterval * 0.5); + } + + return Math.Max(baseInterval, owner.m_Mobile.Controlled ? 100.0 : 200.0); + } + protected override void OnTick() { - if (m_Owner.m_Mobile.Deleted) + if (ShouldStop()) { Stop(); return; } - if (m_Owner.m_Mobile.Map == null || m_Owner.m_Mobile.Map == Map.Internal) + var newInterval = TimeSpan.FromMilliseconds(GetBaseInterval(m_Owner)); + + if (Interval != newInterval) { - m_Owner.Deactivate(); + Interval = newInterval; + } + + m_Owner.m_Mobile.OnThink(); + + if (ShouldStop()) + { + Stop(); return; } - - if (m_Owner.m_Mobile.PlayerRangeSensitive) // have to check this in the timer.... + + HandleBardEffects(); + + if (m_Owner.m_Mobile.Controlled ? !m_Owner.Obey() : !m_Owner.Think()) { - var sect = m_Owner.m_Mobile.Map.GetSector(m_Owner.m_Mobile.Location); - if (!sect.Active) - { - m_Owner.Deactivate(); - return; - } + Stop(); + return; } + + HandleDetectHidden(); + } - m_Owner.m_Mobile.OnThink(); - + private bool ShouldStop() + { if (m_Owner.m_Mobile.Deleted) { - Stop(); - return; + return true; } if (m_Owner.m_Mobile.Map == null || m_Owner.m_Mobile.Map == Map.Internal) { m_Owner.Deactivate(); - return; + return true; + } + + if (m_Owner.m_Mobile.PlayerRangeSensitive && + !m_Owner.m_Mobile.Map.GetSector(m_Owner.m_Mobile.Location).Active) + { + m_Owner.Deactivate(); + return true; } + return false; + } + + private void HandleBardEffects() + { if (m_Owner.m_Mobile.BardPacified) { m_Owner.DoBardPacified(); @@ -3231,91 +3437,86 @@ protected override void OnTick() { m_Owner.DoBardProvoked(); } - else if (!m_Owner.m_Mobile.Controlled) - { - if (!m_Owner.Think()) - { - Stop(); - return; - } - } - else if (!m_Owner.Obey()) + } + + private void CacheDetectHiddenDelays() + { + var delay = Math.Min(30000 / m_Owner.m_Mobile.Int, 120); + _detectHiddenMinDelay = delay * 900; // 26s to 108s + _detectHiddenMaxDelay = delay * 1100; // 32s to 132s + } + + private void HandleDetectHidden() + { + if (!m_Owner.CanDetectHidden || Core.TickCount - m_Owner.m_NextDetectHidden < 0) { - Stop(); return; } - if (m_Owner.CanDetectHidden && Core.TickCount - m_Owner.m_NextDetectHidden >= 0) - { - m_Owner.DetectHidden(); - - // Not exactly OSI style, approximation. - var delay = Math.Min(15000 / m_Owner.m_Mobile.Int, 60); + m_Owner.DetectHidden(); - var min = delay * 900; // 13s at 1000 int, 33s at 400 int, 54s at <250 int - var max = delay * 1100; // 16s at 1000 int, 41s at 400 int, 66s at <250 int - - m_Owner.m_NextDetectHidden = Core.TickCount + Utility.RandomMinMax(min, max); + if (_detectHiddenMinDelay == 0 || _detectHiddenMaxDelay == 0) + { + CacheDetectHiddenDelays(); } + + m_Owner.m_NextDetectHidden = Core.TickCount + + Utility.RandomMinMax(_detectHiddenMinDelay, _detectHiddenMaxDelay); } } public static class AIMovementTimerPool { - private const int _poolSize = 1024; - private static readonly Queue _pool = new (_poolSize); - + private const int _poolSize = 2048; + private static readonly Queue _pool = new(_poolSize); + public static void Configure() { - var i = 0; - while (i++ < _poolSize) + for (var i = 0; i < _poolSize; i++) { _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(); - } - + var timer = _pool.TryDequeue(out var pooledTimer) ? pooledTimer : 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); + try + { + AI?.DoMove(Direction); + } + finally + { + AI = null; + _pool.Enqueue(this); + } } } } diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index dc7e91ceb5..1cbb31d0c4 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -60,7 +60,8 @@ public enum OrderType Release, // "(Name) release" Releases pet back into the wild (removes "tame" status). Stay, // "(All/Name) stay" All or the specified pet(s) will stop and stay in current spot. Stop, // "(All/Name) stop Cancels any current orders to attack, guard or follow. - Transfer // "(Name) transfer" Transfers complete ownership to targeted player. + Transfer, // "(Name) transfer" Transfers complete ownership to targeted player. + Rename // "(Name) rename" Changes the name of the pet. } [Flags]