diff --git a/Projects/UOContent/Engines/Factions/Mobiles/Guards/GuardAI.cs b/Projects/UOContent/Engines/Factions/Mobiles/Guards/GuardAI.cs index c89dd6e98..91bde7a24 100644 --- a/Projects/UOContent/Engines/Factions/Mobiles/Guards/GuardAI.cs +++ b/Projects/UOContent/Engines/Factions/Mobiles/Guards/GuardAI.cs @@ -393,7 +393,7 @@ namespace Server.Factions new TeleportSpell( m_Mobile, null ).Cast(); - m_Mobile.DebugSay( "I am stuck, I'm going to try teleporting away" ); + DebugSay( "I am stuck, I'm going to try teleporting away" ); } else*/ if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true)) diff --git a/Projects/UOContent/Mobiles/AI/AIControlMobileTarget.cs b/Projects/UOContent/Mobiles/AI/AIControlMobileTarget.cs index 6e12837e4..f14b46792 100644 --- a/Projects/UOContent/Mobiles/AI/AIControlMobileTarget.cs +++ b/Projects/UOContent/Mobiles/AI/AIControlMobileTarget.cs @@ -14,10 +14,8 @@ public class AIControlMobileTarget : Target order == OrderType.Attack ? TargetFlags.Harmful : TargetFlags.None ) { - _list = new List(); + _list = [ai]; Order = order; - - AddAI(ai); } public OrderType Order { get; } diff --git a/Projects/UOContent/Mobiles/AI/BaseAI.cs b/Projects/UOContent/Mobiles/AI/BaseAI.cs deleted file mode 100644 index cc8ee14c3..000000000 --- a/Projects/UOContent/Mobiles/AI/BaseAI.cs +++ /dev/null @@ -1,3167 +0,0 @@ -using System; -using System.Collections.Generic; -using Server.Collections; -using Server.ContextMenus; -using Server.Engines.Quests.Necro; -using Server.Engines.Spawners; -using Server.Engines.Virtues; -using Server.Factions; -using Server.Gumps; -using Server.Items; -using Server.Network; -using Server.Spells; -using Server.Spells.Spellweaving; -using Server.Targets; -using MoveImpl = Server.Movement.MovementImpl; - -namespace Server.Mobiles; - -public enum AIType -{ - AI_Use_Default, - AI_Melee, - AI_Animal, - AI_Archer, - AI_Healer, - AI_Vendor, - AI_Mage, - AI_Berserk, - AI_Predator, - AI_Thief -} - -public enum ActionType -{ - Wander, - Combat, - Guard, - Flee, - Backoff, - Interact -} - -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[] _keywordTable = - { - SkillName.Parry, - SkillName.Healing, - SkillName.Hiding, - SkillName.Stealing, - SkillName.Alchemy, - SkillName.AnimalLore, - SkillName.ItemID, - SkillName.ArmsLore, - SkillName.Begging, - SkillName.Blacksmith, - SkillName.Fletching, - SkillName.Peacemaking, - SkillName.Camping, - SkillName.Carpentry, - SkillName.Cartography, - SkillName.Cooking, - SkillName.DetectHidden, - SkillName.Discordance, // ?? - SkillName.EvalInt, - SkillName.Fishing, - SkillName.Provocation, - SkillName.Lockpicking, - SkillName.Magery, - SkillName.MagicResist, - SkillName.Tactics, - SkillName.Snooping, - SkillName.RemoveTrap, - SkillName.Musicianship, - SkillName.Poisoning, - SkillName.Archery, - SkillName.SpiritSpeak, - SkillName.Tailoring, - SkillName.AnimalTaming, - SkillName.TasteID, - SkillName.Tinkering, - SkillName.Veterinary, - SkillName.Forensics, - SkillName.Herding, - SkillName.Tracking, - SkillName.Stealth, - SkillName.Inscribe, - SkillName.Swords, - SkillName.Macing, - SkillName.Fencing, - SkillName.Wrestling, - SkillName.Lumberjacking, - SkillName.Mining, - SkillName.Meditation - }; - - protected ActionType _action; - - public BaseCreature Mobile { get; } - - private long _nextDetectHidden; - private long _nextStopGuard; - public long NextDebugMessage { get; set; } - - protected PathFollower _path; - public Timer _timer; - - public BaseAI(BaseCreature m) - { - Mobile = 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; - } - - if (activate) - { - _timer.Start(); - } - - Action = ActionType.Wander; - } - - public ActionType Action - { - get => _action; - set - { - _action = value; - OnActionChanged(); - } - } - - public long NextMove { get; set; } - - public virtual bool CanDetectHidden => Mobile.Skills.DetectHidden.Value > 0; - - public virtual bool WasNamed(string speech) - { - var name = Mobile.Name; - - return name != null && speech.InsensitiveStartsWith(name); - } - - public virtual void GetContextMenuEntries(Mobile from, ref PooledRefList list) - { - if (!from.Alive || !Mobile.Controlled || !from.InRange(Mobile, 14)) - { - return; - } - - var isDeadPet = Mobile.IsDeadPet; - - if (from == 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 (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 (!Mobile.Summoned && 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 - } - else if (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 - } - } - - public virtual void BeginPickTarget(Mobile from, OrderType order) - { - if (Mobile.Deleted || !Mobile.Controlled || !from.InRange(Mobile, 14) || from.Map != Mobile.Map) - { - return; - } - - var isOwner = from == Mobile.ControlMaster; - var isFriend = !isOwner && Mobile.IsPetFriend(from); - - if (!isOwner && !isFriend) - { - return; - } - - if (isFriend && order != OrderType.Follow && order != OrderType.Stay && order != OrderType.Stop) - { - 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. - } - - from.Target = new AIControlMobileTarget(this, order); - } - else if (from.Target is AIControlMobileTarget t) - { - if (t.Order == order) - { - t.AddAI(this); - } - } - } - - public virtual void OnAggressiveAction(Mobile aggressor) - { - var currentCombat = Mobile.Combatant; - - if (currentCombat != null && !aggressor.Hidden && currentCombat != aggressor && - Mobile.GetDistanceToSqrt(currentCombat) > Mobile.GetDistanceToSqrt(aggressor)) - { - Mobile.Combatant = aggressor; - } - } - - public virtual void EndPickTarget(Mobile from, Mobile target, OrderType order) - { - if (Mobile.Deleted || !Mobile.Controlled || !from.InRange(Mobile, 14) || from.Map != Mobile.Map || - !from.CheckAlive()) - { - return; - } - - var isOwner = from == Mobile.ControlMaster; - var isFriend = !isOwner && Mobile.IsPetFriend(from); - - if (!isOwner && !isFriend) - { - return; - } - - if (isFriend && order != OrderType.Follow && order != OrderType.Stay && order != OrderType.Stop) - { - return; - } - - if (order == OrderType.Attack) - { - if (target is BaseCreature creature && creature.IsScaryToPets && Mobile.IsScaredOfScaryThings) - { - 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) - { - Mobile.SayTo(from, "Your pet refuses to attack the guard."); - return; - } - } - - if (Mobile.CheckControlChance(from)) - { - Mobile.ControlTarget = target; - Mobile.ControlOrder = order; - } - } - - public virtual bool HandlesOnSpeech(Mobile from) - { - if (from.AccessLevel >= AccessLevel.GameMaster) - { - return true; - } - - if (from.Alive && Mobile.Controlled && Mobile.Commandable && - (from == Mobile.ControlMaster || Mobile.IsPetFriend(from))) - { - return true; - } - - return from.Alive && from.InRange(Mobile.Location, 3) && Mobile.IsHumanInTown(); - } - - public virtual void OnSpeech(SpeechEventArgs e) - { - if (e.Mobile.Alive && e.Mobile.InRange(Mobile.Location, 3) && Mobile.IsHumanInTown()) - { - if (e.HasKeyword(0x9D) && WasNamed(e.Speech)) // *move* - { - if (Mobile.Combatant != null) - { - // I am too busy fighting to deal with thee! - Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 501482); - } - else - { - // Excuse me? - Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 501516); - WalkRandomInHome(2, 2, 1); - } - } - else if (e.HasKeyword(0x9E) && WasNamed(e.Speech)) // *time* - { - if (Mobile.Combatant != null) - { - // I am too busy fighting to deal with thee! - Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 501482); - } - else - { - Clock.GetTime(Mobile, out var generalNumber, out _); - - Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, generalNumber); - } - } - else if (e.HasKeyword(0x6C) && WasNamed(e.Speech)) // *train - { - if (Mobile.Combatant != null) - { - // I am too busy fighting to deal with thee! - Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 501482); - } - else - { - var foundSomething = false; - - var ourSkills = 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 && 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) - { - Mobile.Say(1043058); // I can train the following: - } - - Mobile.Say(number); - - foundSomething = true; - } - } - } - - if (!foundSomething) - { - Mobile.Say(501505); // Alas, I cannot teach thee anything. - } - } - } - else - { - 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 < _keywordTable.Length) - { - toTrain = _keywordTable[index]; - } - } - } - - if (toTrain != (SkillName)(-1) && WasNamed(e.Speech)) - { - if (Mobile.Combatant != null) - { - // I am too busy fighting to deal with thee! - Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 501482); - } - else - { - var skills = Mobile.Skills; - var skill = skills[toTrain]; - - if (skill == null || skill.Base < 60.0 || !Mobile.CheckTeach(toTrain, e.Mobile)) - { - Mobile.Say(501507); // 'Tis not something I can teach thee of. - } - else - { - Mobile.Teach(toTrain, e.Mobile, 0, false); - } - } - } - } - } - - if (Mobile.Controlled && Mobile.Commandable) - { - DebugSay("Listening..."); - - var isOwner = e.Mobile == Mobile.ControlMaster; - var isFriend = !isOwner && Mobile.IsPetFriend(e.Mobile); - - if (e.Mobile.Alive && (isOwner || isFriend)) - { - 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 (Mobile.CheckControlChance(e.Mobile)) - { - Mobile.ControlTarget = null; - 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 (Mobile.CheckControlChance(e.Mobile)) - { - Mobile.ControlTarget = null; - Mobile.ControlOrder = OrderType.Guard; - } - - return; - } - case 0x167: // all stop - { - if (Mobile.CheckControlChance(e.Mobile)) - { - Mobile.ControlTarget = null; - 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 (Mobile.CheckControlChance(e.Mobile)) - { - Mobile.ControlTarget = e.Mobile; - Mobile.ControlOrder = OrderType.Follow; - } - - return; - } - case 0x170: // all stay - { - if (Mobile.CheckControlChance(e.Mobile)) - { - Mobile.ControlTarget = null; - 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) && Mobile.CheckControlChance(e.Mobile)) - { - Mobile.ControlTarget = null; - Mobile.ControlOrder = OrderType.Come; - } - - return; - } - case 0x156: // *drop - { - if (!isOwner) - { - break; - } - - if (!Mobile.IsDeadPet && !Mobile.Summoned && WasNamed(speech) && - Mobile.CheckControlChance(e.Mobile)) - { - Mobile.ControlTarget = null; - Mobile.ControlOrder = OrderType.Drop; - } - - return; - } - case 0x15A: // *follow - { - if (WasNamed(speech) && Mobile.CheckControlChance(e.Mobile)) - { - BeginPickTarget(e.Mobile, OrderType.Follow); - } - - return; - } - case 0x15B: // *friend - { - if (!isOwner) - { - break; - } - - if (WasNamed(speech) && Mobile.CheckControlChance(e.Mobile)) - { - if (Mobile.Summoned || Mobile is GrizzledMare) - { - // Summoned creatures are loyal only to their summoners. - e.Mobile.SendLocalizedMessage(1005481); - } - else if (e.Mobile.HasTrade) - { - // You cannot friend a pet with a trade pending - e.Mobile.SendLocalizedMessage(1070947); - } - else - { - BeginPickTarget(e.Mobile, OrderType.Friend); - } - } - - return; - } - case 0x15C: // *guard - { - if (!isOwner) - { - break; - } - - if (!Mobile.IsDeadPet && WasNamed(speech) && Mobile.CheckControlChance(e.Mobile)) - { - Mobile.ControlTarget = null; - Mobile.ControlOrder = OrderType.Guard; - } - - return; - } - case 0x15D: // *kill - case 0x15E: // *attack - { - if (!isOwner) - { - break; - } - - if (!Mobile.IsDeadPet && WasNamed(speech) && Mobile.CheckControlChance(e.Mobile)) - { - BeginPickTarget(e.Mobile, OrderType.Attack); - } - - return; - } - case 0x15F: // *patrol - { - if (!isOwner) - { - break; - } - - if (WasNamed(speech) && Mobile.CheckControlChance(e.Mobile)) - { - Mobile.ControlTarget = null; - Mobile.ControlOrder = OrderType.Patrol; - } - - return; - } - case 0x161: // *stop - { - if (WasNamed(speech) && Mobile.CheckControlChance(e.Mobile)) - { - Mobile.ControlTarget = null; - Mobile.ControlOrder = OrderType.Stop; - } - - return; - } - case 0x163: // *follow me - { - if (WasNamed(speech) && Mobile.CheckControlChance(e.Mobile)) - { - Mobile.ControlTarget = e.Mobile; - Mobile.ControlOrder = OrderType.Follow; - } - - return; - } - case 0x16D: // *release - { - if (!isOwner) - { - break; - } - - if (WasNamed(speech) && Mobile.CheckControlChance(e.Mobile)) - { - if (!Mobile.Summoned) - { - e.Mobile.SendGump(new ConfirmReleaseGump(e.Mobile, Mobile)); - } - else - { - Mobile.ControlTarget = null; - Mobile.ControlOrder = OrderType.Release; - } - } - - return; - } - case 0x16E: // *transfer - { - if (!isOwner) - { - break; - } - - if (!Mobile.IsDeadPet && WasNamed(speech) && Mobile.CheckControlChance(e.Mobile)) - { - if (Mobile.Summoned || Mobile is GrizzledMare) - { - // You cannot transfer ownership of a summoned creature. - e.Mobile.SendLocalizedMessage(1005487); - } - else if (e.Mobile.HasTrade) - { - // You cannot transfer a pet with a trade pending - e.Mobile.SendLocalizedMessage(1010507); - } - else - { - BeginPickTarget(e.Mobile, OrderType.Transfer); - } - } - - return; - } - case 0x16F: // *stay - { - if (WasNamed(speech) && Mobile.CheckControlChance(e.Mobile)) - { - Mobile.ControlTarget = null; - Mobile.ControlOrder = OrderType.Stay; - } - - return; - } - } - } - } - } - else - { - if (e.Mobile.AccessLevel >= AccessLevel.GameMaster) - { - DebugSay("It's from a GM"); - - if (Mobile.FindMyName(e.Speech, true)) - { - var str = e.Speech.Split(' '); - int i; - - for (i = 0; i < str.Length; i++) - { - var word = str[i]; - - if (word.InsensitiveEquals("obey")) - { - Mobile.SetControlMaster(e.Mobile); - - if (Mobile.Summoned) - { - Mobile.SummonMaster = e.Mobile; - } - - return; - } - } - } - } - } - } - - public void DebugSay(string message, int cooldownMs = 5000) - { - if (Mobile.Debug && Core.TickCount >= NextDebugMessage) - { - Mobile.PublicOverheadMessage(MessageType.Regular, 41, false, message); - NextDebugMessage = Core.TickCount + cooldownMs; - } - } - - public virtual bool Think() - { - if (Mobile.Deleted) - { - return false; - } - - if (CheckFlee()) - { - return true; - } - - switch (Action) - { - case ActionType.Wander: - { - Mobile.OnActionWander(); - return DoActionWander(); - } - - case ActionType.Combat: - { - Mobile.OnActionCombat(); - return DoActionCombat(); - } - - case ActionType.Guard: - { - Mobile.OnActionGuard(); - return DoActionGuard(); - } - - case ActionType.Flee: - { - Mobile.OnActionFlee(); - return DoActionFlee(); - } - - case ActionType.Interact: - { - Mobile.OnActionInteract(); - return DoActionInteract(); - } - - case ActionType.Backoff: - { - Mobile.OnActionBackoff(); - return DoActionBackoff(); - } - - default: - { - return false; - } - } - } - - public virtual void OnActionChanged() - { - switch (Action) - { - case ActionType.Wander: - { - Mobile.Warmode = false; - Mobile.Combatant = null; - Mobile.FocusMob = null; - Mobile.SetCurrentSpeedToPassive(); - break; - } - - case ActionType.Combat: - { - Mobile.Warmode = true; - Mobile.FocusMob = null; - Mobile.SetCurrentSpeedToActive(); - break; - } - - case ActionType.Guard: - { - Mobile.Warmode = true; - Mobile.FocusMob = null; - Mobile.Combatant = null; - _nextStopGuard = Core.TickCount + (int)TimeSpan.FromSeconds(10).TotalMilliseconds; - Mobile.SetCurrentSpeedToActive(); - break; - } - - case ActionType.Flee: - { - Mobile.Warmode = true; - Mobile.FocusMob = null; - Mobile.SetCurrentSpeedToActive(); - break; - } - - case ActionType.Interact: - { - Mobile.Warmode = false; - Mobile.SetCurrentSpeedToPassive(); - break; - } - - case ActionType.Backoff: - { - Mobile.Warmode = false; - Mobile.SetCurrentSpeedToPassive(); - break; - } - } - } - - public virtual bool OnAtWayPoint() => true; - - public virtual bool DoActionWander() - { - if (CheckHerding()) - { - DebugSay("Praise the shepherd!"); - } - else if (Mobile.CurrentWayPoint != null) - { - var point = Mobile.CurrentWayPoint; - if ((point.X != Mobile.Location.X || point.Y != Mobile.Location.Y) && point.Map == Mobile.Map && - point.Parent == null && !point.Deleted) - { - DebugSay("I will move towards my waypoint."); - - DoMove(Mobile.GetDirectionTo(Mobile.CurrentWayPoint)); - } - else if (OnAtWayPoint()) - { - DebugSay("I will go to the next waypoint"); - - Mobile.CurrentWayPoint = point.NextPoint; - if (point.NextPoint?.Deleted == true) - { - Mobile.CurrentWayPoint = point.NextPoint = point.NextPoint.NextPoint; - } - } - } - else if (Mobile.IsAnimatedDead) - { - // animated dead follow their master - var master = Mobile.SummonMaster; - - if (master != null && master.Map == Mobile.Map && master.InRange(Mobile, Mobile.RangePerception)) - { - MoveTo(master, false, 1); - } - else - { - WalkRandomInHome(2, 2, 1); - } - } - else if (CheckMove() && CanMoveNow(out _) && !Mobile.CheckIdle()) - { - WalkRandomInHome(2, 2, 1); - } - - if (Mobile.Combatant?.Deleted == false && Mobile.Combatant.Alive && - !Mobile.Combatant.IsDeadBondedPet) - { - Mobile.Direction = Mobile.GetDirectionTo(Mobile.Combatant); - } - - return true; - } - - public virtual bool DoActionCombat() - { - if (Core.AOS && CheckHerding()) - { - DebugSay("Praise the shepherd!"); - - return true; - } - - var combatant = Mobile.Combatant; - - if (combatant == null || combatant.Deleted || combatant.Map != Mobile.Map || !combatant.Alive || - combatant.IsDeadBondedPet) - { - DebugSay("My combatant is gone!"); - - Action = ActionType.Wander; - return true; - } - - Mobile.Direction = Mobile.GetDirectionTo(combatant); - if (Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, combatant)) - { - this.DebugSayFormatted($"I used my abilities on {combatant.Name}!"); - } - - return true; - } - - public virtual bool DoActionGuard() - { - if (Core.AOS && CheckHerding()) - { - DebugSay("Praise the shepherd!"); - } - else if (Core.TickCount - _nextStopGuard < 0) - { - DebugSay("I am on guard"); - // m_Mobile.Turn( Utility.Random(0, 2) - 1 ); - } - else - { - DebugSay("I stopped being on guard"); - - Action = ActionType.Wander; - } - - return true; - } - - public virtual bool DoActionFlee() - { - var from = Mobile.FocusMob; - - if (from?.Deleted != false || from.Map != Mobile.Map) - { - DebugSay("I have lost him"); - - Action = ActionType.Guard; - return true; - } - - if (WalkMobileRange(from, 1, true, Mobile.RangePerception * 2, Mobile.RangePerception * 3)) - { - DebugSay("I have fled"); - - Action = ActionType.Guard; - return true; - } - - DebugSay("I am fleeing!"); - - return true; - } - - public virtual bool DoActionInteract() => true; - - public virtual bool DoActionBackoff() => true; - - public virtual bool Obey() => - !Mobile.Deleted && Mobile.ControlOrder switch - { - OrderType.None => DoOrderNone(), - OrderType.Come => DoOrderCome(), - OrderType.Drop => DoOrderDrop(), - OrderType.Friend => DoOrderFriend(), - OrderType.Unfriend => DoOrderUnfriend(), - OrderType.Guard => DoOrderGuard(), - OrderType.Attack => DoOrderAttack(), - OrderType.Patrol => DoOrderPatrol(), - OrderType.Release => DoOrderRelease(), - OrderType.Stay => DoOrderStay(), - OrderType.Stop => DoOrderStop(), - OrderType.Follow => DoOrderFollow(), - OrderType.Transfer => DoOrderTransfer(), - _ => false - }; - - public virtual void OnCurrentOrderChanged() - { - if (Mobile.Deleted || Mobile.ControlMaster?.Deleted != false) - { - return; - } - - switch (Mobile.ControlOrder) - { - case OrderType.None: - { - Mobile.ControlMaster.RevealingAction(); - Mobile.Home = Mobile.Location; - Mobile.SetCurrentSpeedToPassive(); - Mobile.PlaySound(Mobile.GetIdleSound()); - Mobile.Warmode = false; - Mobile.Combatant = null; - break; - } - - case OrderType.Come: - { - Mobile.ControlMaster.RevealingAction(); - Mobile.SetCurrentSpeedToActive(); - Mobile.PlaySound(Mobile.GetIdleSound()); - Mobile.Warmode = false; - Mobile.Combatant = null; - break; - } - - case OrderType.Drop: - { - Mobile.ControlMaster.RevealingAction(); - Mobile.SetCurrentSpeedToPassive(); - Mobile.PlaySound(Mobile.GetIdleSound()); - Mobile.Warmode = false; - Mobile.Combatant = null; - break; - } - - case OrderType.Friend: - case OrderType.Unfriend: - { - Mobile.ControlMaster.RevealingAction(); - break; - } - - case OrderType.Guard: - { - Mobile.ControlMaster.RevealingAction(); - Mobile.SetCurrentSpeedToActive(); - Mobile.PlaySound(Mobile.GetIdleSound()); - Mobile.Warmode = true; - Mobile.Combatant = null; - Mobile.ControlMaster.SendLocalizedMessage(1049671, Mobile.Name); // ~1_PETNAME~ is now guarding you. - break; - } - - case OrderType.Attack: - { - Mobile.ControlMaster.RevealingAction(); - Mobile.SetCurrentSpeedToActive(); - Mobile.PlaySound(Mobile.GetIdleSound()); - - Mobile.Warmode = true; - Mobile.Combatant = null; - break; - } - - case OrderType.Patrol: - { - Mobile.ControlMaster.RevealingAction(); - Mobile.SetCurrentSpeedToActive(); - Mobile.PlaySound(Mobile.GetIdleSound()); - Mobile.Warmode = false; - Mobile.Combatant = null; - break; - } - - case OrderType.Release: - { - Mobile.ControlMaster.RevealingAction(); - Mobile.SetCurrentSpeedToPassive(); - Mobile.PlaySound(Mobile.GetIdleSound()); - Mobile.Warmode = false; - Mobile.Combatant = null; - break; - } - - case OrderType.Stay: - { - Mobile.ControlMaster.RevealingAction(); - Mobile.SetCurrentSpeedToPassive(); - Mobile.PlaySound(Mobile.GetIdleSound()); - Mobile.Warmode = false; - Mobile.Combatant = null; - break; - } - - case OrderType.Stop: - { - Mobile.ControlMaster.RevealingAction(); - Mobile.Home = Mobile.Location; - Mobile.SetCurrentSpeedToPassive(); - Mobile.PlaySound(Mobile.GetIdleSound()); - Mobile.Warmode = false; - Mobile.Combatant = null; - break; - } - - case OrderType.Follow: - { - Mobile.ControlMaster.RevealingAction(); - Mobile.SetCurrentSpeedToActive(); - Mobile.PlaySound(Mobile.GetIdleSound()); - - Mobile.Warmode = false; - Mobile.Combatant = null; - break; - } - - case OrderType.Transfer: - { - Mobile.ControlMaster.RevealingAction(); - Mobile.SetCurrentSpeedToPassive(); - Mobile.PlaySound(Mobile.GetIdleSound()); - - Mobile.Warmode = false; - Mobile.Combatant = null; - break; - } - } - } - - public virtual bool DoOrderNone() - { - DebugSay("I have no order"); - - WalkRandomInHome(3, 2, 1); - - if (Mobile.Combatant?.Deleted == false && Mobile.Combatant.Alive && - !Mobile.Combatant.IsDeadBondedPet) - { - Mobile.Warmode = true; - Mobile.Direction = Mobile.GetDirectionTo(Mobile.Combatant); - } - else - { - Mobile.Warmode = false; - } - - return true; - } - - public virtual bool DoOrderCome() - { - if (Mobile.ControlMaster?.Deleted != false) - { - return true; - } - - var iCurrDist = (int)Mobile.GetDistanceToSqrt(Mobile.ControlMaster); - - if (iCurrDist > Mobile.RangePerception) - { - DebugSay("I have lost my master. I stay here"); - - Mobile.ControlTarget = null; - Mobile.ControlOrder = OrderType.None; - } - else - { - DebugSay("My master told me come"); - - // Not exactly OSI style, but better than nothing. - var bRun = iCurrDist > 5; - - if (WalkMobileRange(Mobile.ControlMaster, 1, bRun, 0, 1)) - { - if (Mobile.Combatant?.Deleted == false && Mobile.Combatant.Alive && - !Mobile.Combatant.IsDeadBondedPet) - { - Mobile.Warmode = true; - // m_Mobile.Direction = m_Mobile.GetDirectionTo(m_Mobile.Combatant, bRun); - } - else - { - Mobile.Warmode = false; - } - } - } - - return true; - } - - public virtual bool DoOrderDrop() - { - if (Mobile.IsDeadPet || !Mobile.CanDrop) - { - return true; - } - - DebugSay("I drop my stuff for my master"); - - var pack = Mobile.Backpack; - - if (pack != null) - { - var list = pack.Items; - - for (var i = list.Count - 1; i >= 0; --i) - { - if (i < list.Count) - { - list[i].MoveToWorld(Mobile.Location, Mobile.Map); - } - } - } - - Mobile.ControlTarget = null; - Mobile.ControlOrder = OrderType.None; - - return true; - } - - public virtual bool CheckHerding() - { - var target = Mobile.TargetLocation; - - if (target == null) - { - return false; // Creature is not being herded - } - - var distance = Mobile.GetDistanceToSqrt(target); - - if (distance is not (< 1 or > 15)) - { - DoMove(Mobile.GetDirectionTo(target)); - return true; - } - - if (distance < 1 && target.X == 1076 && target.Y == 450 && Mobile is HordeMinionFamiliar) - { - if (Mobile.ControlMaster is PlayerMobile pm) - { - var qs = pm.Quest; - - if (qs is DarkTidesQuest) - { - var obj = qs.FindObjective(); - - if (obj?.Completed == false) - { - Mobile.AddToBackpack(new ScrollOfAbraxus()); - obj.Complete(); - } - } - } - } - - Mobile.TargetLocation = null; - return false; // At the target or too far away - } - - public virtual bool DoOrderFollow() - { - if (CheckHerding()) - { - DebugSay("Praise the shepherd!"); - } - else if (Mobile.ControlTarget?.Deleted == false && Mobile.ControlTarget != Mobile) - { - var iCurrDist = (int)Mobile.GetDistanceToSqrt(Mobile.ControlTarget); - - if (iCurrDist > Mobile.RangePerception) - { - DebugSay("I have lost the one to follow. I stay here"); - - if (Mobile.Combatant?.Deleted == false && Mobile.Combatant.Alive && - !Mobile.Combatant.IsDeadBondedPet) - { - Mobile.Warmode = true; - Mobile.Direction = Mobile.GetDirectionTo(Mobile.Combatant); - } - else - { - Mobile.Warmode = false; - } - } - else - { - this.DebugSayFormatted($"My master told me to follow: {Mobile.ControlTarget.Name}"); - - // Not exactly OSI style, but better than nothing. - var bRun = iCurrDist > 5; - - if (WalkMobileRange(Mobile.ControlTarget, 1, bRun, 0, 1)) - { - if (Mobile.Combatant?.Deleted == false && Mobile.Combatant.Alive && - !Mobile.Combatant.IsDeadBondedPet) - { - Mobile.Warmode = true; - // m_Mobile.Direction = m_Mobile.GetDirectionTo(m_Mobile.Combatant, bRun); - } - else - { - Mobile.Warmode = false; - if (Core.AOS) - { - Mobile.CurrentSpeed = 0.1; - } - } - } - } - } - else - { - DebugSay("I have nobody to follow"); - - Mobile.ControlTarget = null; - Mobile.ControlOrder = OrderType.None; - } - - return true; - } - - public virtual bool DoOrderFriend() - { - var from = Mobile.ControlMaster; - var to = Mobile.ControlTarget; - - if (from?.Deleted != false || to?.Deleted != false || from == to || !to.Player) - { - Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 502039); // *looks confused* - } - else - { - 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 (Mobile.IsPetFriend(to)) - { - from.SendLocalizedMessage(1049691); // That person is already a friend. - } - else if (!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, $"{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{Mobile.Name}"); - - Mobile.AddPetFriend(to); - - Mobile.ControlTarget = to; - Mobile.ControlOrder = OrderType.Follow; - - return true; - } - } - } - } - - Mobile.ControlTarget = from; - Mobile.ControlOrder = OrderType.Follow; - - return true; - } - - public virtual bool DoOrderUnfriend() - { - var from = Mobile.ControlMaster; - var to = Mobile.ControlTarget; - - if (from?.Deleted != false || to?.Deleted != false || from == to || !to.Player) - { - Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 502039); // *looks confused* - } - else if (!Mobile.IsPetFriend(to)) - { - from.SendLocalizedMessage(1070953); // That person is not a friend. - } - else - { - // ~1_NAME~ will no longer accept movement commands from ~2_NAME~. - from.SendLocalizedMessage(1070951, $"{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{Mobile.Name}"); - - Mobile.RemovePetFriend(to); - } - - Mobile.ControlTarget = from; - Mobile.ControlOrder = OrderType.Follow; - - return true; - } - - public virtual bool DoOrderGuard() - { - if (Mobile.IsDeadPet) - { - return true; - } - - var controlMaster = Mobile.ControlMaster; - - if (controlMaster?.Deleted != false) - { - return true; - } - - var combatant = 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(Mobile) <= Mobile.RangePerception) - { - if (combatant == null || attacker.GetDistanceToSqrt(controlMaster) < - combatant.GetDistanceToSqrt(controlMaster)) - { - combatant = attacker; - } - } - } - - if (combatant != null) - { - DebugSay("Crap, my master has been attacked! I will attack one of those bastards!"); - } - } - - if (combatant?.Deleted == false && combatant != Mobile && combatant != Mobile.ControlMaster && - combatant.Alive && !combatant.IsDeadBondedPet && Mobile.CanSee(combatant) && - Mobile.CanBeHarmful(combatant, false) && combatant.Map == Mobile.Map) - { - DebugSay("Guarding from target..."); - - Mobile.Combatant = combatant; - 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 - { - DebugSay("Nothing to guard from"); - - Mobile.Warmode = false; - if (Core.AOS) - { - Mobile.CurrentSpeed = 0.1; - } - - WalkMobileRange(controlMaster, 1, false, 0, 1); - } - - return true; - } - - public virtual bool DoOrderAttack() - { - if (Mobile.IsDeadPet) - { - return true; - } - - if (Mobile.ControlTarget?.Deleted != false || Mobile.ControlTarget.Map != Mobile.Map || - !Mobile.ControlTarget.Alive || Mobile.ControlTarget.IsDeadBondedPet) - { - 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) - { - Mobile.ControlTarget = Mobile.ControlMaster; - Mobile.ControlOrder = OrderType.Follow; - } - else - { - Mobile.ControlTarget = null; - Mobile.ControlOrder = OrderType.None; - } - - if (Mobile.FightMode is FightMode.Closest or FightMode.Aggressor) - { - Mobile newCombatant = null; - var newScore = 0.0; - - foreach (var aggr in Mobile.GetMobilesInRange(Mobile.RangePerception)) - { - if (!Mobile.CanSee(aggr) || aggr.Combatant != Mobile) - { - continue; - } - - if (aggr.IsDeadBondedPet || !aggr.Alive) - { - continue; - } - - var aggrScore = Mobile.GetFightModeRanking(aggr, FightMode.Closest, false); - - if ((newCombatant == null || aggrScore > newScore) && Mobile.InLOS(aggr)) - { - newCombatant = aggr; - newScore = aggrScore; - } - } - - if (newCombatant != null) - { - Mobile.ControlTarget = newCombatant; - Mobile.ControlOrder = OrderType.Attack; - Mobile.Combatant = newCombatant; - DebugSay("But -that- is not dead. Here we go again..."); - - Think(); - } - } - } - else - { - DebugSay("Attacking target..."); - - Think(); - } - - return true; - } - - public virtual bool DoOrderPatrol() - { - DebugSay("This order is not yet coded"); - - return true; - } - - public virtual bool DoOrderRelease() - { - DebugSay("I have been released"); - - Mobile.PlaySound(Mobile.GetAngerSound()); - - Mobile.SetControlMaster(null); - Mobile.SummonMaster = null; - - Mobile.BondingBegin = DateTime.MinValue; - Mobile.OwnerAbandonTime = DateTime.MinValue; - Mobile.IsBonded = false; - - var spawner = Mobile.Spawner; - - if (spawner != null && spawner.HomeLocation != Point3D.Zero) - { - Mobile.Home = spawner.HomeLocation; - Mobile.RangeHome = spawner.HomeRange; - } - - if (Mobile.DeleteOnRelease || Mobile.IsDeadPet) - { - Mobile.Delete(); - } - - Mobile.BeginDeleteTimer(); - - if (Mobile.CanDrop) - { - Mobile.DropBackpack(); - } - - return true; - } - - public virtual bool DoOrderStay() - { - DebugSay(CheckHerding() ? "Praise the shepherd!" : "My master told me to stay"); - - return true; - } - - public virtual bool DoOrderStop() - { - if (Mobile.ControlMaster?.Deleted != false) - { - return true; - } - - DebugSay("My master told me to stop."); - - Mobile.Direction = Mobile.GetDirectionTo(Mobile.ControlMaster); - Mobile.Home = Mobile.Location; - - Mobile.ControlTarget = null; - - if (Core.ML) - { - WalkRandomInHome(3, 2, 1); - } - else - { - Mobile.ControlOrder = OrderType.None; - } - - return true; - } - - public virtual bool DoOrderTransfer() - { - if (Mobile.IsDeadPet) - { - return true; - } - - var from = Mobile.ControlMaster; - var to = Mobile.ControlTarget; - - if (from?.Deleted == false && to?.Deleted == false && from != to && to.Player) - { - this.DebugSayFormatted($"Begin transfer with {to.Name}"); - - var youngFrom = from is PlayerMobile mobile && mobile.Young; - var youngTo = to is PlayerMobile playerMobile && playerMobile.Young; - - if (youngFrom && !youngTo) - { - 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. - } - else if (!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~ - } - else if (!Mobile.CanBeControlledBy(from)) - { - var args = $"{to.Name}\t{from.Name}\t "; - - 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~ - } - else if (TransferItem.IsInCombat(Mobile)) - { - 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."); - } - else - { - NetState fromState = from.NetState, toState = to.NetState; - - 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 - { - fromState.AddTrade(toState).DropItem(new TransferItem(Mobile)); - } - } - } - } - - Mobile.ControlTarget = null; - Mobile.ControlOrder = OrderType.Stay; - - return true; - } - - public virtual bool DoBardPacified() - { - if (Core.Now < Mobile.BardEndTime) - { - DebugSay("I am pacified, I wait"); - - Mobile.Combatant = null; - Mobile.Warmode = false; - } - else - { - DebugSay("I'm not pacified any longer"); - - Mobile.BardPacified = false; - } - - return true; - } - - public virtual bool DoBardProvoked() - { - if (Core.Now >= Mobile.BardEndTime && - (Mobile.BardMaster?.Deleted != false || - Mobile.BardMaster.Map != Mobile.Map || Mobile.GetDistanceToSqrt(Mobile.BardMaster) > - Mobile.RangePerception)) - { - DebugSay("I have lost my provoker"); - - Mobile.BardProvoked = false; - Mobile.BardMaster = null; - Mobile.BardTarget = null; - - Mobile.Combatant = null; - Mobile.Warmode = false; - } - else if (Mobile.BardTarget?.Deleted != false || Mobile.BardTarget.Map != Mobile.Map || - Mobile.GetDistanceToSqrt(Mobile.BardTarget) > Mobile.RangePerception) - { - DebugSay("I have lost my provoke target"); - - Mobile.BardProvoked = false; - Mobile.BardMaster = null; - Mobile.BardTarget = null; - - Mobile.Combatant = null; - Mobile.Warmode = false; - } - else - { - Mobile.Combatant = Mobile.BardTarget; - _action = ActionType.Combat; - - Mobile.OnThink(); - Think(); - } - - return true; - } - - public virtual void WalkRandom(int iChanceToNotMove, int iChanceToDir, int iSteps) - { - if (Mobile.Deleted || Mobile.DisallowAllMoves) - { - return; - } - - if (iChanceToNotMove <= 0) - { - return; - } - - for (var i = 0; i < iSteps; i++) - { - if (Utility.Random(1 + iChanceToNotMove) == 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 : Mobile.Direction; - DoMove(direction); - } - } - } - - public static double TransformMoveDelay(BaseCreature bc, bool isPassive = false) - { - if (bc == null) - { - return 0.1; - } - - if (bc.MoveSpeedMod > 0) - { - return bc.MoveSpeedMod; - } - - var moveSpeed = bc.CurrentSpeed; - var isControlled = bc.Controlled || bc.Summoned; - - /* - * Movement Speed Table based on RunUO - * <0.075 -> 0.0375 - * 0.075 -> 0.05 - * 0.1 -> 0.125 - * 0.2 -> 0.3 - * 0.25 -> 0.45 - * 0.3 -> 0.6 - * 0.4 -> 0.85 - * 0.5 -> 1.05 - * 0.6 -> 1.2, - * 0.8 -> 1.5 - * 1.0 -> 1.8 - * Above 1.0 is linear - */ - - // Linear interpolated - var movementSpeed = moveSpeed switch - { - >= 1.0 => 1.8 * moveSpeed, - >= 0.5 => 1.05 + (moveSpeed - 0.5) * 1.5, - >= 0.4 => 0.85 + (moveSpeed - 0.4) * 2, - >= 0.3 => 0.6 + (moveSpeed - 0.3) * 2.5, - >= 0.2 => 0.3 + (moveSpeed - 0.2) * 3, - >= 0.1 => 0.125 + (moveSpeed - 0.1) * 1.75, - >= 0.075 => 0.05 + (moveSpeed - 0.075) * 3, - _ => 0.0375 // 30 ticks, 37.5ms - }; - - if (isPassive) - { - movementSpeed += 0.2; - } - - if (!isControlled) - { - movementSpeed += 0.1; - } - else if (!bc.Summoned) - { - if (bc.ControlOrder == OrderType.Follow && bc.ControlTarget == bc.ControlMaster) - { - movementSpeed *= 0.5; - } - - movementSpeed -= 0.075; - } - - if (!bc.IsDeadPet && (bc.ReduceSpeedWithDamage || bc.IsSubdued)) - { - int stats, statsMax; - if (Core.HS) - { - stats = bc.Stam; - statsMax = bc.StamMax; - } - else - { - stats = bc.Hits; - statsMax = bc.HitsMax; - } - - 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; - - public virtual bool CheckMove() => true; - - public virtual bool DoMove(Direction d, bool badStateOk = false) - { - var res = DoMoveImpl(d, badStateOk); - - return res is MoveResult.Success or MoveResult.SuccessAutoTurn || badStateOk && res == MoveResult.BadState; - } - - public virtual MoveResult DoMoveImpl(Direction d, bool badStateOk) - { - if (Mobile == null || Mobile.Deleted || Mobile.Frozen || Mobile.Paralyzed || - Mobile.Spell?.IsCasting == true || Mobile.DisallowAllMoves) - { - return MoveResult.BadState; - } - - if (!CheckMove()) - { - return MoveResult.BadState; - } - - var delay = (int)(TransformMoveDelay(Mobile) * 1000); - - if (!CanMoveNow(out var timeUntilMove)) - { - // When bad state is ok, we can still move if the time until move is less than the fuzzy time - if (badStateOk) - { - AIMovementTimerPool.GetTimer(TimeSpan.FromMilliseconds(timeUntilMove), this, d).Start(); - } - - return MoveResult.BadState; - } - - // This makes them always move one step, never any direction changes - // TODO: This is firing off deltas which aren't needed. Look into replacing/removing this - Mobile.Direction = d; - NextMove += delay; - - if (Core.TickCount - NextMove > 0) - { - NextMove = Core.TickCount; - } - - Mobile.Pushing = false; - var mobDirection = Mobile.Direction; - - // Do the actual move - MoveImpl.IgnoreMovableImpassables = Mobile.CanMoveOverObstacles && !Mobile.CanDestroyObstacles; - var moveResult = Mobile.Move(d); - MoveImpl.IgnoreMovableImpassables = false; - - if (moveResult) - { - // If we don't delay combat, then a direction change will happen and cause a glitchy sliding effect. - if (Mobile.Warmode && Mobile.Combatant != null) - { - var remaining = Mobile.NextCombatTime - Core.TickCount; - var maxWait = Math.Min(delay, 400); - if (remaining < maxWait) - { - Mobile.NextCombatTime = Core.TickCount + maxWait; - } - } - return MoveResult.Success; - } - - if ((mobDirection & Direction.Mask) != (d & Direction.Mask)) - { - return MoveResult.Blocked; - } - - var wasPushing = Mobile.Pushing; - - var blocked = true; - - var canOpenDoors = Mobile.CanOpenDoors; - var canDestroyObstacles = Mobile.CanDestroyObstacles; - - if (canOpenDoors || canDestroyObstacles) - { - DebugSay("My movement was blocked, I will try to clear some obstacles."); - - var map = Mobile.Map; - - if (map != null) - { - var x = Mobile.X; - var y = Mobile.Y; - Movement.Movement.Offset(d, ref x, ref y); - - 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 > Mobile.Z && - 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 > Mobile.Z && Mobile.Z + 16 > item.Z) - { - if (!Mobile.InRange(item.GetWorldLocation(), 1)) - { - continue; - } - - queue.Enqueue(item); - ++destroyables; - } - } - - if (destroyables > 0) - { - Effects.PlaySound(new Point3D(x, y, Mobile.Z), Mobile.Map, 0x3B3); - } - - if (queue.Count > 0) - { - blocked = false; // retry movement - } - - while (queue.Count > 0) - { - var item = queue.Dequeue(); - - if (item is BaseDoor door) - { - DebugSay("Little do they expect, I've learned how to open doors. Didn't they read the script??"); - - DebugSay("*twist*"); - - door.Use(Mobile); - } - else - { - this.DebugSayFormatted($"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 > Mobile.Z) - { - queue.Enqueue(check); - } - } - - cont.Destroy(); - } - else - { - item.Delete(); - } - } - } - - if (!blocked) - { - blocked = !Mobile.Move(d); - } - } - } - - if (blocked) - { - var offset = Utility.RandomDouble() < 0.4 ? 1 : -1; - - for (var i = 0; i < 2; ++i) - { - Mobile.TurnInternal(offset); - - if (Mobile.Move(Mobile.Direction)) - { - return MoveResult.SuccessAutoTurn; - } - } - - return wasPushing ? MoveResult.BadState : MoveResult.Blocked; - } - - return MoveResult.Success; - } - - public virtual void WalkRandomInHome(int iChanceToNotMove, int iChanceToDir, int iSteps) - { - if (Mobile.Deleted || Mobile.DisallowAllMoves) - { - return; - } - - if (Mobile.Home == Point3D.Zero) - { - if (Mobile.Spawner is RegionSpawner rs) - { - Region region = rs.SpawnRegion; - - if (Mobile.Region.AcceptsSpawnsFrom(region)) - { - Mobile.WalkRegion = region; - WalkRandom(iChanceToNotMove, iChanceToDir, iSteps); - Mobile.WalkRegion = null; - } - else - { - if (region.GoLocation != Point3D.Zero && Utility.RandomBool()) - { - DoMove(Mobile.GetDirectionTo(region.GoLocation)); - } - else - { - WalkRandom(iChanceToNotMove, iChanceToDir, 1); - } - } - } - else - { - WalkRandom(iChanceToNotMove, iChanceToDir, iSteps); - } - } - else - { - for (var i = 0; i < iSteps; i++) - { - if (Mobile.RangeHome != 0) - { - var iCurrDist = (int)Mobile.GetDistanceToSqrt(Mobile.Home); - - if (iCurrDist < Mobile.RangeHome * 2 / 3) - { - WalkRandom(iChanceToNotMove, iChanceToDir, 1); - } - else if (iCurrDist > Mobile.RangeHome) - { - DoMove(Mobile.GetDirectionTo(Mobile.Home)); - } - else if (Utility.Random(10) > 5) - { - DoMove(Mobile.GetDirectionTo(Mobile.Home)); - } - else - { - WalkRandom(iChanceToNotMove, iChanceToDir, 1); - } - } - else - { - if (Mobile.Location != Mobile.Home) - { - DoMove(Mobile.GetDirectionTo(Mobile.Home)); - } - } - } - } - } - - public virtual bool CheckFlee() - { - if (Mobile.CheckFlee()) - { - var combatant = Mobile.Combatant; - - if (combatant == null) - { - WalkRandom(1, 2, 1); - } - else - { - var d = combatant.GetDirectionTo(Mobile); - - d = (Direction)((int)d + Utility.RandomMinMax(-1, +1)); - - Mobile.Direction = d; - Mobile.Move(d); - } - - return true; - } - - return false; - } - - public virtual void OnTeleported() - { - if (_path != null) - { - DebugSay("Teleported; repathing"); - - _path.ForceRepath(); - } - } - - public virtual bool MoveTo(Mobile m, bool run, int range) - { - if (Mobile.Deleted || Mobile.DisallowAllMoves || m?.Deleted != false) - { - return false; - } - - if (Mobile.InRange(m, range)) - { - _path = null; - return true; - } - - if (_path?.Goal == m) - { - if (_path.Follow(run, 1)) - { - _path = null; - return true; - } - } - else if (!DoMove(Mobile.GetDirectionTo(m), true)) - { - _path = new PathFollower(Mobile, m) { Mover = DoMoveImpl }; - - if (_path.Follow(run, 1)) - { - _path = null; - return true; - } - } - else - { - _path = null; - return true; - } - - return false; - } - - /* - * Walk at range distance from mobile - * - * 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 (Mobile.Deleted || Mobile.DisallowAllMoves) - { - return false; - } - - if (m == null) - { - return false; - } - - for (var i = 0; i < iSteps; i++) - { - // Get the current distance - var iCurrDist = (int)Mobile.GetDistanceToSqrt(m); - - if (iCurrDist < iWantDistMin || iCurrDist > iWantDistMax) - { - var needCloser = iCurrDist > iWantDistMax; - - if (needCloser && _path != null && _path.Goal == m) - { - if (_path.Follow(run, 1)) - { - _path = null; - } - } - else - { - var dirTo = iCurrDist > iWantDistMax ? - Mobile.GetDirectionTo(m, run) : m.GetDirectionTo(Mobile, run); - - if (!DoMove(dirTo, true) && needCloser) - { - _path = new PathFollower(Mobile, m) { Mover = DoMoveImpl }; - - if (_path.Follow(run, 1)) - { - _path = null; - } - } - else - { - _path = null; - } - } - } - else - { - return true; - } - } - - // Get the current distance - var iNewDist = (int)Mobile.GetDistanceToSqrt(m); - - return iNewDist >= iWantDistMin && iNewDist <= iWantDistMax; - } - - /* - * Here we check to acquire a target from our surrounding - * - * iRange : The range - * acqType : A type of acquire we want (closest, strongest, etc) - * bPlayerOnly : Don't bother with other creatures or NPCs, want a player - * bFacFriend : Check people in my faction - * bFacFoe : Check people in other factions - * - */ - public virtual bool AcquireFocusMob(int iRange, FightMode acqType, bool bPlayerOnly, bool bFacFriend, bool bFacFoe) - { - if (Mobile.Deleted) - { - return false; - } - - if (Mobile.BardProvoked) - { - if (Mobile.BardTarget?.Deleted != false) - { - Mobile.FocusMob = null; - return false; - } - - Mobile.FocusMob = Mobile.BardTarget; - return Mobile.FocusMob != null; - } - - if (Mobile.Controlled) - { - if (Mobile.ControlTarget?.Deleted != false || Mobile.ControlTarget.Hidden || - !Mobile.ControlTarget.Alive || Mobile.ControlTarget.IsDeadBondedPet || - !Mobile.InRange(Mobile.ControlTarget, Mobile.RangePerception * 2)) - { - if (Mobile.ControlTarget != null && Mobile.ControlTarget != Mobile.ControlMaster) - { - Mobile.ControlTarget = null; - } - - Mobile.FocusMob = null; - return false; - } - - Mobile.FocusMob = Mobile.ControlTarget; - return Mobile.FocusMob != null; - } - - if (Mobile.ConstantFocus != null) - { - DebugSay("Acquired my constant focus"); - - Mobile.FocusMob = Mobile.ConstantFocus; - return true; - } - - if (acqType == FightMode.None) - { - Mobile.FocusMob = null; - return false; - } - - if (acqType == FightMode.Aggressor && Mobile.Aggressors.Count == 0 && Mobile.Aggressed.Count == 0 && - Mobile.FactionAllegiance == null && Mobile.EthicAllegiance == null) - { - Mobile.FocusMob = null; - return false; - } - - if (Core.TickCount - Mobile.NextReacquireTime < 0) - { - Mobile.FocusMob = null; - return false; - } - - Mobile.NextReacquireTime = Core.TickCount + (int)Mobile.ReacquireDelay.TotalMilliseconds; - - DebugSay("Acquiring..."); - - var map = Mobile.Map; - - if (map == null) - { - // TODO: Is this correct? Maybe it should return false? - return Mobile.FocusMob != null; - } - - Mobile newFocusMob = null; - var val = double.MinValue; - Mobile enemySummonMob = null; - var enemySummonVal = double.MinValue; - - foreach (var m in map.GetMobilesInRange(Mobile.Location, iRange)) - { - if (m.Deleted || m.Blessed) - { - continue; - } - - // Let's not target ourselves... - if (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 (!Mobile.CanSee(m)) - { - 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 == Mobile || !bc.SummonMaster.Player && IsHostile(bc.SummonMaster))) - { - continue; - } - - if (Mobile.Summoned && Mobile.SummonMaster != null) - { - // Animated creatures cannot attack players directly. - if (pm != null && Mobile.IsAnimatedDead) - { - continue; - } - - // Animated creatures cannot attack other animated creatures or pets of other players - if (Mobile.IsAnimatedDead && bc != null && (bc.IsAnimatedDead || bc.Controlled)) - { - continue; - } - - // If this is a summon, it can't target its controller or invalid targets. - if (Mobile.FollowsAcquireRules && (m == Mobile.SummonMaster || !SpellHelper.ValidIndirectTarget(Mobile, m))) - { - continue; - } - } - - // If we only want faction friends, make sure it's one. - if (bFacFriend && !Mobile.IsFriend(m)) - { - continue; - } - - // Ignore anyone under EtherealVoyage - if (TransformationSpellHelper.UnderTransformation(m, typeof(EtherealVoyageSpell))) - { - continue; - } - - // Ignore players with activated honor - if (Mobile.Combatant != m && VirtueSystem.GetVirtues(pm)?.HonorActive == true) - { - continue; - } - - if (acqType is FightMode.Aggressor or FightMode.Evil) - { - var bValid = IsHostile(m); - - if (!bValid) - { - bValid = Mobile.GetFactionAllegiance(m) == BaseCreature.Allegiance.Enemy || - 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 && !Mobile.IsEnemy(m)) - { - continue; - } - - // If it's an enemy factioned mobile, make sure we can be harmful to it. - if (bFacFoe && !bFacFriend && !Mobile.CanBeHarmful(m, false)) - { - continue; - } - } - - var theirVal = Mobile.GetFightModeRanking(m, acqType, bPlayerOnly); - - // Always prefer someone else over the summon master (EV/BS) - if ((theirVal > val || newFocusMob == Mobile.SummonMaster) && 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 && Mobile.InLOS(m) && bc?.Summoned == true && bc?.Controlled != true) - { - enemySummonMob = m; - enemySummonVal = theirVal; - } - } - - Mobile.FocusMob = newFocusMob ?? enemySummonMob; - return Mobile.FocusMob != null; - } - - private bool IsHostile(Mobile from) - { - if (Mobile.Combatant == from || from.Combatant == Mobile) - { - return true; - } - - var count = Math.Max(Mobile.Aggressors.Count, Mobile.Aggressed.Count); - - for (var a = 0; a < count; ++a) - { - if (a < Mobile.Aggressed.Count && Mobile.Aggressed[a].Attacker == from) - { - return true; - } - - if (a < Mobile.Aggressors.Count && Mobile.Aggressors[a].Defender == from) - { - return true; - } - } - - return false; - } - - public virtual void DetectHidden() - { - if (Mobile.Deleted || Mobile.Map == null) - { - return; - } - - DebugSay("Checking for hidden players"); - - var srcSkill = Mobile.Skills.DetectHidden.Value; - - if (srcSkill <= 0) - { - return; - } - - foreach (var trg in Mobile.GetMobilesInRange(Mobile.RangePerception)) - { - if (trg != Mobile && trg.Player && trg.Alive && trg.Hidden && trg.AccessLevel == AccessLevel.Player && - Mobile.InLOS(trg)) - { - this.DebugSayFormatted($"Trying to detect {trg.Name}"); - - var trgHiding = trg.Skills.Hiding.Value / 2.9; - var trgStealth = trg.Skills.Stealth.Value / 1.8; - - var chance = srcSkill / 1.2 - Math.Min(trgHiding, trgStealth); - - if (chance < srcSkill / 10) - { - chance = srcSkill / 10; - } - - chance /= 100; - - if (chance > Utility.RandomDouble()) - { - trg.RevealingAction(); - trg.SendLocalizedMessage(500814); // You have been revealed! - } - } - } - } - - public virtual void Deactivate() - { - if (Mobile.PlayerRangeSensitive) - { - _timer.Stop(); - - var spawner = Mobile.Spawner; - - if (spawner?.ReturnOnDeactivate == true && !Mobile.Controlled && ( - spawner.HomeLocation == Point3D.Zero && !Mobile.Region.AcceptsSpawnsFrom(spawner.Region) || - !Mobile.InRange(spawner.HomeLocation, spawner.HomeRange) - )) - { - Timer.StartTimer(ReturnToHome); - } - } - } - - private void ReturnToHome() - { - if (Mobile.Spawner != null) - { - var loc = Mobile.Spawner.GetSpawnPosition(Mobile, Mobile.Spawner.Map); - - if (loc != Point3D.Zero) - { - Mobile.MoveToWorld(loc, Mobile.Spawner.Map); - } - } - } - - public virtual void Activate() - { - if (!_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. - _timer.Delay = TimeSpan.FromMilliseconds(Utility.Random(48) * 8); - _timer.Start(); - } - } - - public virtual void OnCurrentSpeedChanged() - { - _timer.Interval = TimeSpan.FromSeconds(Math.Max(0.008, Mobile.CurrentSpeed)); - } - - private 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) - { - return; - } - - // Just in case - if (bc.IsDeadPet && _order is OrderType.Guard or OrderType.Attack or OrderType.Transfer or OrderType.Drop) - { - return; - } - - 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; - } - - switch (_order) - { - case OrderType.Follow: - case OrderType.Attack: - 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; - } - case OrderType.Release: - { - if (bc.Summoned) - { - goto default; - } - - from.SendGump(new ConfirmReleaseGump(from, bc)); - - break; - } - default: - { - if (bc.CheckControlChance(from)) - { - bc.ControlTarget = null; - bc.ControlOrder = _order; - } - - break; - } - } - } - } - - private class TransferItem : Item - { - private readonly BaseCreature _creature; - - public TransferItem(BaseCreature creature) - : base(ShrinkTable.Lookup(creature)) - { - _creature = creature; - - Movable = false; - - if (!Core.AOS) - { - Name = creature.Name; - } - else if (ItemID == ShrinkTable.DefaultItemID || - 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); - - 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(); - - 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, _creature.Name); // Pet Name: ~1_val~ - - if (_creature.ControlMaster != null) - { - list.Add(1041602, _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 || _creature?.Deleted != false || _creature.ControlMaster != from || - !from.CheckAlive() || !to.CheckAlive()) - { - return false; - } - - if (from.Map != _creature.Map || !from.InRange(_creature, 14)) - { - return false; - } - - var youngFrom = from is PlayerMobile mobile && mobile.Young; - var youngTo = to is PlayerMobile playerMobile && playerMobile.Young; - - if (accepted && youngFrom && !youngTo) - { - from.SendLocalizedMessage(502051); // As a young player, you may not transfer pets to older players. - } - else if (accepted && !youngFrom && youngTo) - { - from.SendLocalizedMessage(502052); // As an older player, you may not transfer pets to young players. - } - else if (accepted && !_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 false; - } - else if (accepted && !_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 + _creature.ControlSlots > to.FollowersMax) - { - to.SendLocalizedMessage(1049607); // You have too many followers to control that creature. - - return false; - } - else if (accepted && IsInCombat(_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 false; - } - - return true; - } - - public override void OnSecureTrade(Mobile from, Mobile to, Mobile newOwner, bool accepted) - { - if (Deleted) - { - return; - } - - Delete(); - - if (_creature?.Deleted != false || _creature.ControlMaster != from || !from.CheckAlive() || - !to.CheckAlive()) - { - return; - } - - if (from.Map != _creature.Map || !from.InRange(_creature, 14)) - { - return; - } - - if (accepted && _creature.SetControlMaster(to)) - { - if (_creature.Summoned) - { - _creature.SummonMaster = to; - } - - _creature.ControlTarget = to; - _creature.ControlOrder = OrderType.Follow; - - _creature.BondingBegin = DateTime.MinValue; - _creature.OwnerAbandonTime = DateTime.MinValue; - _creature.IsBonded = false; - - _creature.PlaySound(_creature.GetIdleSound()); - - var args = $"{from.Name}\t{_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); - } - } - } - - /* - * The Timer object - */ - private class AITimer : Timer - { - private readonly BaseAI _owner; - - public AITimer(BaseAI owner) : base( - TimeSpan.FromMilliseconds(Utility.Random(96) * 8), - TimeSpan.FromSeconds(Math.Max(0.0, owner.Mobile.CurrentSpeed)) - ) - { - _owner = owner; - _owner._nextDetectHidden = Core.TickCount; - } - - protected override void OnTick() - { - if (_owner.Mobile.Deleted) - { - Stop(); - return; - } - - if (_owner.Mobile.Map == null || _owner.Mobile.Map == Map.Internal) - { - _owner.Deactivate(); - return; - } - - if (_owner.Mobile.PlayerRangeSensitive) // have to check this in the timer.... - { - var sect = _owner.Mobile.Map.GetSector(_owner.Mobile.Location); - if (!sect.Active) - { - _owner.Deactivate(); - return; - } - } - - _owner.Mobile.OnThink(); - - if (_owner.Mobile.Deleted) - { - Stop(); - return; - } - - if (_owner.Mobile.Map == null || _owner.Mobile.Map == Map.Internal) - { - _owner.Deactivate(); - return; - } - - if (_owner.Mobile.BardPacified) - { - _owner.DoBardPacified(); - } - else if (_owner.Mobile.BardProvoked) - { - _owner.DoBardProvoked(); - } - else if (!_owner.Mobile.Controlled) - { - if (!_owner.Think()) - { - Stop(); - return; - } - } - else if (!_owner.Obey()) - { - Stop(); - return; - } - - if (_owner.CanDetectHidden && Core.TickCount - _owner._nextDetectHidden >= 0) - { - _owner.DetectHidden(); - - // Not exactly OSI style, approximation. - var delay = Math.Min(15000 / _owner.Mobile.Int, 60); - - 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 - - _owner._nextDetectHidden = Core.TickCount + Utility.RandomMinMax(min, max); - } - } - } - - public static class AIMovementTimerPool - { - private const int _poolSize = 1024; - private static readonly Queue _pool = new (_poolSize); - - public static void Configure() - { - var i = 0; - while (i++ < _poolSize) - { - _pool.Enqueue(new AIMovementTimer()); - } - } - - public static AIMovementTimer GetTimer(TimeSpan delay, BaseAI ai, Direction direction) - { - AIMovementTimer timer; - if (_pool.Count > 0) - { - timer = _pool.Dequeue(); - } - else - { - timer = new AIMovementTimer(); - } - - timer.Set(delay, ai, direction); - return timer; - } - - public class AIMovementTimer : Timer - { - public BaseAI AI { get; private set; } - public Direction Direction { get; private set; } - - public AIMovementTimer() : base(TimeSpan.Zero) - { - } - - public void Set(TimeSpan delay, BaseAI ai, Direction direction) - { - if (Running) - { - return; - } - - Delay = delay; - AI = ai; - Direction = direction; - } - - protected override void OnTick() - { - AI?.DoMove(Direction); - AI = null; - _pool.Enqueue(this); - } - } - } -} diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/AIGroupMovement.cs b/Projects/UOContent/Mobiles/AI/BaseAI/AIGroupMovement.cs new file mode 100644 index 000000000..c1564c9d7 --- /dev/null +++ b/Projects/UOContent/Mobiles/AI/BaseAI/AIGroupMovement.cs @@ -0,0 +1,215 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2025 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: AIGroupMovement.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 System.Runtime.CompilerServices; +using Server.Collections; + +namespace Server.Mobiles; + +public abstract partial class BaseAI +{ + private static readonly Dictionary _reservedPositions = new(); + private static long _lastGroupUpdateTime; + + private static void CleanupReservedPositions() + { + using var toRemove = PooledRefQueue.Create(); + + foreach (var (m, p) in _reservedPositions) + { + if (m?.Deleted != false || m.GetDistanceToSqrt(p) < 1) + { + toRemove.Enqueue(m); + } + } + + while (toRemove.Count > 0) + { + _reservedPositions.Remove(toRemove.Dequeue()); + } + } + + private bool UseGroupMovement(Mobile target) => + Mobile.Combatant == target + && !Mobile.Controlled + && CountNearbyAllies(target) > 0; + + public static bool MoveToWithGroup(BaseAI ai, Mobile target, bool run, int range) + { + if (Core.TickCount - _lastGroupUpdateTime > 1000) + { + CleanupReservedPositions(); + _lastGroupUpdateTime = Core.TickCount; + } + + var mobile = ai.Mobile; + var allies = ai.GetNearbyAllies(target); + var optimalPosition = ai.CalculateOptimalPosition(target, ref allies, range); + try + { + if (optimalPosition == Point3D.Zero) + { + return ai.MoveToWithCollisionAvoidance(target, run, range); + } + + _reservedPositions[mobile] = optimalPosition; + + var direction = mobile.GetDirectionTo(optimalPosition); + + if (Utility.Random(3) == 0) + { + direction = GetAdjustedDirection(direction); + } + + return ai.DoMove(direction, true); + } + finally + { + allies.Dispose(); + } + } + + private int CountNearbyAllies(Mobile target) + { + var allies = 0; + foreach (var m in Mobile.GetMobilesInRange(8)) + { + if (m != Mobile && m.Combatant == target && m is BaseCreature { Controlled: false } bc + && bc.Team == Mobile.Team) + { + allies++; + } + } + + return allies; + } + + private PooledRefList GetNearbyAllies(Mobile target) + { + var allies = PooledRefList.Create(); + + foreach (var m in Mobile.GetMobilesInRange(8)) + { + if (m != Mobile && m.Combatant == target && m is BaseCreature { Controlled: false } bc + && bc.Team == Mobile.Team) + { + allies.Add(bc); + } + } + + return allies; + } + + private Point3D CalculateOptimalPosition(Mobile target, ref PooledRefList allies, int range) + { + var targetLoc = target.Location; + var bestPosition = Point3D.Zero; + var bestScore = -1.0; + + for (var x = -range; x <= range; x++) + { + for (var y = -range; y <= range; y++) + { + if (Math.Abs(x) + Math.Abs(y) != range) + { + continue; + } + + var testLoc = new Point3D(targetLoc.X + x, targetLoc.Y + y, targetLoc.Z); + var distance = Mobile.GetDistanceToSqrt(testLoc); + + if (distance < range || + distance > range + 3 || !CanMoveTo(testLoc)) + { + continue; + } + + var score = ScorePosition(testLoc, distance, target, ref allies); + + if (score > bestScore) + { + bestScore = score; + bestPosition = testLoc; + + if (score > 20) + { + break; + } + } + } + } + + return bestPosition; + } + + private double ScorePosition(Point3D position, double currentDistance, Mobile target, ref PooledRefList allies) + { + var score = -(currentDistance * 2); + + for (var i = 0; i < allies.Count; i++) + { + var ally = allies[i]; + var allyDistance = ally.GetDistanceToSqrt(position); + + if (allyDistance < 2) + { + score -= 50; + } + else if (allyDistance < 3) + { + score -= 20; + } + } + + foreach (var (m, p) in _reservedPositions) + { + if (m != Mobile && p.GetDistanceToSqrt(position) < 2) + { + score -= 30; + } + } + + if (Mobile.Map != null && Mobile.Map.LineOfSight(position, target.Location)) + { + score += 10; + } + + return score + Utility.RandomDouble() * 5; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool CanMoveTo(Point3D location) => + Mobile.Map?.CanFit(location.X, location.Y, location.Z, 16, false, false) == true; + + private static Direction GetAdjustedDirection(Direction original) + { + var adjustment = Utility.Random(3) - 1; + var newDir = (int)original + adjustment; + + if (newDir < 0) + { + return (Direction)(newDir + 8); + } + + if (newDir >= 8) + { + return (Direction)(newDir - 8); + } + + return (Direction)newDir; + } +} diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/AIMovement.cs b/Projects/UOContent/Mobiles/AI/BaseAI/AIMovement.cs new file mode 100644 index 000000000..9909024bf --- /dev/null +++ b/Projects/UOContent/Mobiles/AI/BaseAI/AIMovement.cs @@ -0,0 +1,429 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2025 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: AIMovement.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.Runtime.CompilerServices; +using Server.Collections; +using Server.Items; +using MoveImpl = Server.Movement.MovementImpl; + +namespace Server.Mobiles; + +public abstract partial class BaseAI +{ + public static double BadlyHurtMoveDelay(BaseCreature bc) + { + var statMin = Core.HS ? bc.Stam : bc.Hits; + var statMax = Core.HS ? bc.StamMax : bc.HitsMax; + + if (!bc.IsDeadPet && (bc.ReduceSpeedWithDamage || bc.IsSubdued) + && statMax > 0 && statMin < statMax * 0.3) + { + var hits = (double)statMin / statMax; + + if (hits < 0.1) { return bc.CurrentSpeed + 0.15; } + if (hits < 0.2) { return bc.CurrentSpeed + 0.1; } + if (hits < 0.3) { return bc.CurrentSpeed + 0.05; } + } + + return bc.CurrentSpeed; + } + + public bool CanMoveNow(out double delay) + { + delay = 0.0; + return Core.TickCount >= NextMove; + } + + public virtual bool CheckMove() => !(Mobile.Deleted || Mobile.DisallowAllMoves); + + public virtual bool DoMove(Direction d, bool badStateOk = false) => IsMoveSuccessful(DoMoveImpl(d, badStateOk), badStateOk); + + private static bool IsMoveSuccessful(MoveResult res, bool badStateOk) => + res is MoveResult.Success or MoveResult.SuccessAutoTurn + || badStateOk && res == MoveResult.BadState; + + public virtual MoveResult DoMoveImpl(Direction d, bool badStateOk) + { + if (IsInBadState() || !CanMoveNow(out _)) + { + return MoveResult.BadState; + } + + if ((Mobile.Direction & Direction.Mask) != (d & Direction.Mask)) + { + Mobile.Direction = d; + } + + Mobile.Pushing = false; + var mobDirection = Mobile.Direction; + + if (TryMove(d)) + { + Mobile.CurrentSpeed = Mobile.Hits < Mobile.HitsMax * 0.3 + ? BadlyHurtMoveDelay(Mobile) + : Mobile.Warmode || Mobile.Combatant != null ? Mobile.ActiveSpeed : Mobile.PassiveSpeed; + + return MoveResult.Success; + } + + if ((mobDirection & Direction.Mask) != (d & Direction.Mask)) + { + Mobile.Direction = d; + return MoveResult.SuccessAutoTurn; + } + + return HandleBlockedMovement(d); + } + + private bool TryMove(Direction d) + { + MoveImpl.IgnoreMovableImpassables = Mobile.CanMoveOverObstacles && !Mobile.CanDestroyObstacles; + + var result = Mobile.Move(d); + + MoveImpl.IgnoreMovableImpassables = false; + return result; + } + + private bool IsInBadState() => + Mobile == null || Mobile.Deleted || Mobile.Frozen || Mobile.Paralyzed || + Mobile.Spell?.IsCasting == true || Mobile.DisallowAllMoves; + + private MoveResult HandleBlockedMovement(Direction d) + { + var wasPushing = Mobile.Pushing; + + if ((Mobile.CanOpenDoors || Mobile.CanDestroyObstacles) && !TryClearObstacles(d)) + { + return MoveResult.Success; + } + + return TryAlternateMovement(wasPushing); + } + + private MoveResult TryAlternateMovement(bool wasPushing) + { + var offset = Utility.Random(2) == 0 ? 1 : -1; + + for (var i = 0; i < 2; ++i) + { + Mobile.TurnInternal(offset); + + if (Mobile.Move(Mobile.Direction)) + { + return MoveResult.SuccessAutoTurn; + } + } + + return wasPushing ? MoveResult.BadState : MoveResult.Blocked; + } + + private bool TryClearObstacles(Direction d) + { + DebugSay("My movement is blocked. Trying to push through."); + + var map = Mobile.Map; + + if (map == null) { return true; } + + var (x, y) = GetOffsetLocation(d); + + var queue = GatherObstacles(x, y, out var destroyables); + + if (destroyables > 0) + { + Effects.PlaySound(new Point3D(x, y, Mobile.Z), Mobile.Map, 0x3B3); + } + + try + { + return ProcessObstacles(ref queue, d); + } + finally + { + queue.Dispose(); + } + } + + private (int x, int y) GetOffsetLocation(Direction d) + { + var x = Mobile.X; + var y = Mobile.Y; + Movement.Movement.Offset(d, ref x, ref y); + return (x, y); + } + + private PooledRefQueue GatherObstacles(int x, int y, out int destroyables) + { + var queue = PooledRefQueue.Create(); + destroyables = 0; + + foreach (var item in Mobile.Map.GetItemsInRange(new Point2D(x, y), 1)) + { + if (IsValidDoor(item, x, y) || IsValidDestroyableItem(item)) + { + queue.Enqueue(item); + if (item is not BaseDoor) + { + destroyables++; + } + } + } + + return queue; + } + + private bool IsValidDoor(Item item, int x, int y) + { + if (!Mobile.CanOpenDoors || item is not BaseDoor door) + { + return false; + } + + if (door.Z + door.ItemData.Height <= Mobile.Z || Mobile.Z + 16 <= door.Z) + { + return false; + } + + if (door.X != x || door.Y != y) + { + return false; + } + + return !door.Locked || !door.UseLocks(); + } + + private bool IsValidDestroyableItem(Item item) + { + if (!Mobile.CanDestroyObstacles || !item.Movable || !item.ItemData.Impassable) + { + return false; + } + + if (item.Z + item.ItemData.Height <= Mobile.Z || Mobile.Z + 16 <= item.Z) + { + return false; + } + + return Mobile.InRange(item.GetWorldLocation(), 1); + } + + private bool ProcessObstacles(ref PooledRefQueue queue, Direction d) + { + if (queue.Count == 0) { return true; } + + while (queue.Count > 0) + { + ProcessObstacle(queue.Dequeue(), ref queue); + } + + return !Mobile.Move(d); + } + + private void ProcessObstacle(Item item, ref PooledRefQueue queue) + { + if (item is BaseDoor door) + { + DebugSay("Opening the door."); + door.Use(Mobile); + } + else + { + this.DebugSayFormatted($"Destroying item: {item.GetType().Name}"); + + if (item is Container cont) + { + ProcessContainer(cont, ref queue); + cont.Destroy(); + } + else + { + item.Delete(); + } + } + } + + private void ProcessContainer(Container cont, ref PooledRefQueue queue) + { + foreach (var check in cont.Items) + { + if (check.Movable && check.ItemData.Impassable && cont.Z + check.ItemData.Height > Mobile.Z) + { + queue.Enqueue(check); + } + } + } + + public virtual bool MoveTo(Mobile m, bool run, int range) + { + if (Mobile.Deleted || Mobile.DisallowAllMoves || m?.Deleted != false) + { + return false; + } + + var distance = (int)Mobile.GetDistanceToSqrt(m); + var distanceThreshold = Core.AOS && IsFollowingMaster() ? 1 : 5; + + var shouldRun = run && distance > distanceThreshold; + + if (Mobile.InRange(m, range)) + { + Path = null; + return true; + } + + if (UseGroupMovement(m)) + { + return MoveToWithGroup(this, m, shouldRun, range); + } + + if (Path == null && Mobile.InLOS(m) && DoMove(Mobile.GetDirectionTo(m), true)) + { + return true; + } + + if (Path?.Goal != m) + { + Path = new PathFollower(Mobile, m) { Mover = DoMoveImpl }; + } + + if (Path.Follow(shouldRun, 1)) + { + Path = null; + return true; + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsFollowingMaster() => + Mobile.Controlled && + Mobile.ControlOrder == OrderType.Follow && + Mobile.ControlTarget == Mobile.ControlMaster && + Mobile.Combatant == null; + + private bool MoveToWithCollisionAvoidance(Mobile target, bool run, int range) + { + var distance = (int)Mobile.GetDistanceToSqrt(target); + + var shouldRun = run && distance > 5; + + var direction = Mobile.GetDirectionTo(target); + + if (DoMove(direction, true)) + { + return true; + } + + for (var i = 1; i <= 3; i++) + { + var clockwise = (Direction)(((int)direction + i) % 8); + + if (DoMove(clockwise, true)) + { + return true; + } + + var counterclockwise = (Direction)(((int)direction - i + 8) % 8); + + if (DoMove(counterclockwise, true)) + { + return true; + } + } + + if (Path?.Goal != target) + { + Path = new PathFollower(Mobile, target) { Mover = DoMoveImpl }; + } + + if (Path.Follow(shouldRun, 1)) + { + Path = null; + return true; + } + + return false; + } + + public virtual bool WalkMobileRange(Mobile m, int iSteps, bool run, int iWantDistMin, int iWantDistMax) + { + if (Mobile.Deleted || Mobile.DisallowAllMoves || m == null) + { + return false; + } + + for (var i = 0; i < iSteps; i++) + { + var iCurrDist = (int)Mobile.GetDistanceToSqrt(m); + + var shouldRun = run && iCurrDist > 5; + + if (iCurrDist >= iWantDistMin && iCurrDist <= iWantDistMax) + { + return true; + } + + if (!MoveTowardsOrAwayFrom(m, shouldRun, iCurrDist, iWantDistMax)) + { + return false; + } + } + + var dist = Mobile.GetDistanceToSqrt(m); + + return dist >= iWantDistMin && dist <= iWantDistMax; + } + + private bool MoveTowardsOrAwayFrom(Mobile m, bool run, int iCurrDist, int iWantDistMax) + { + var shouldRun = run && iCurrDist > 5; + + var needCloser = iCurrDist > iWantDistMax; + + if (needCloser && m != null && Path?.Goal == m) + { + if (Path.Follow(shouldRun, 1)) + { + Path = null; + return true; + } + } + else + { + var dirTo = needCloser ? Mobile.GetDirectionTo(m, shouldRun) : m.GetDirectionTo(Mobile, shouldRun); + + if (DoMove(dirTo, true)) + { + Path = null; + return true; + } + + if (needCloser) + { + Path = new PathFollower(Mobile, m) { Mover = DoMoveImpl }; + + if (Path.Follow(shouldRun, 1)) + { + Path = null; + return true; + } + } + } + + return false; + } +} diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/AITimer.cs b/Projects/UOContent/Mobiles/AI/BaseAI/AITimer.cs new file mode 100644 index 000000000..d42e24409 --- /dev/null +++ b/Projects/UOContent/Mobiles/AI/BaseAI/AITimer.cs @@ -0,0 +1,134 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2025 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: AITimer.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; + +namespace Server.Mobiles; + +internal sealed class AITimer : Timer +{ + private readonly BaseAI _owner; + private int _detectHiddenMinDelay; + private int _detectHiddenMaxDelay; + + public AITimer(BaseAI owner) : base(TimeSpan.FromMilliseconds(Utility.Random(3000)), + TimeSpan.FromMilliseconds(GetBaseInterval(owner))) + { + _owner = owner; + _owner._nextDetectHidden = Core.TickCount; + } + + private static double GetBaseInterval(BaseAI owner) + { + double interval; + + if (owner.IsFollowingMaster()) + { + interval = Core.AOS ? 100 : owner.Mobile.CurrentSpeed * 400; + } + else if (owner.Mobile.CurrentSpeed <= 0.4) + { + interval = owner.Mobile.CurrentSpeed * 1000; + } + else + { + interval = owner.Mobile.CurrentSpeed * 3000; + } + + return Math.Max(interval, 100); + } + + protected override void OnTick() + { + if (ShouldStop()) + { + Stop(); + return; + } + + Interval = TimeSpan.FromMilliseconds(GetBaseInterval(_owner)); + + _owner.Mobile.OnThink(); + + if (ShouldStop()) + { + Stop(); + return; + } + + HandleBardEffects(); + + if (_owner.Mobile.Controlled ? !_owner.Obey() : !_owner.Think()) + { + Stop(); + return; + } + + HandleDetectHidden(); + } + + private bool ShouldStop() + { + if (_owner.Mobile.Deleted) + { + return true; + } + + if (_owner.Mobile.Map != null && _owner.Mobile.Map != Map.Internal && + (!_owner.Mobile.PlayerRangeSensitive || _owner.Mobile.Map.GetSector(_owner.Mobile.Location).Active)) + { + return false; + } + + _owner.Deactivate(); + return true; + } + + private void HandleBardEffects() + { + if (_owner.Mobile.BardPacified) + { + _owner.DoBardPacified(); + } + else if (_owner.Mobile.BardProvoked) + { + _owner.DoBardProvoked(); + } + } + + private void CacheDetectHiddenDelays() + { + var delay = Math.Min(30000 / _owner.Mobile.Int, 120); + _detectHiddenMinDelay = delay * 900; // 26s to 108s + _detectHiddenMaxDelay = delay * 1100; // 32s to 132s + } + + private void HandleDetectHidden() + { + if (!_owner.CanDetectHidden || Core.TickCount - _owner._nextDetectHidden < 0) + { + return; + } + + _owner.DetectHidden(); + + if (_detectHiddenMinDelay == 0 || _detectHiddenMaxDelay == 0) + { + CacheDetectHiddenDelays(); + } + + _owner._nextDetectHidden = Core.TickCount + Utility.RandomMinMax(_detectHiddenMinDelay, _detectHiddenMaxDelay); + } +} diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/AIType.cs b/Projects/UOContent/Mobiles/AI/BaseAI/AIType.cs new file mode 100644 index 000000000..e04c0133d --- /dev/null +++ b/Projects/UOContent/Mobiles/AI/BaseAI/AIType.cs @@ -0,0 +1,30 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2025 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: AIType.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 . * + ************************************************************************/ + +namespace Server.Mobiles; + +public enum AIType +{ + AI_Use_Default, + AI_Melee, + AI_Animal, + AI_Archer, + AI_Healer, + AI_Vendor, + AI_Mage, + AI_Berserk, + AI_Predator, + AI_Thief +} diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/ActionType.cs b/Projects/UOContent/Mobiles/AI/BaseAI/ActionType.cs new file mode 100644 index 000000000..f34e5a63e --- /dev/null +++ b/Projects/UOContent/Mobiles/AI/BaseAI/ActionType.cs @@ -0,0 +1,26 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2025 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: ActionType.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 . * + ************************************************************************/ + +namespace Server.Mobiles; + +public enum ActionType +{ + Wander, + Combat, + Guard, + Flee, + Backoff, + Interact +} diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs b/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs new file mode 100644 index 000000000..02b178dc2 --- /dev/null +++ b/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs @@ -0,0 +1,918 @@ +/************************************************************************* + * 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 Server.Engines.Quests.Necro; +using Server.Engines.Spawners; +using Server.Engines.Virtues; +using Server.Factions; +using Server.Spells; +using Server.Spells.Spellweaving; +using Server.Targets; + +namespace Server.Mobiles; + +public abstract partial class BaseAI +{ + private ActionType _action; + public long _nextDetectHidden; + public PathFollower Path { get; protected set; } + public readonly Timer _timer; + public DateTime _lastOrder = DateTime.MinValue; + public Mobile _commandIssuer; + public long NextMove { get; set; } + + public BaseCreature Mobile { get; } + + public long NextDebugMessage { get; set; } + + public virtual bool CanDetectHidden => Mobile.Skills.DetectHidden.Value > 0; + + public BaseAI(BaseCreature m) + { + Mobile = m; + _timer = new AITimer(this); + + if (!m.PlayerRangeSensitive || !World.Loading && m.Map != null && m.Map != Map.Internal && m.Map.GetSector(m.Location).Active) + { + _timer.Start(); + } + + if (Action != ActionType.Wander) + { + Action = ActionType.Wander; + } + } + + public ActionType Action + { + get => _action; + set + { + if (_action != value) + { + _action = value; + OnActionChanged(); + } + } + } + + public virtual bool WasNamed(string speech) => !string.IsNullOrEmpty(Mobile.Name) && speech.InsensitiveStartsWith(Mobile.Name); + + public virtual void BeginPickTarget(Mobile from, OrderType order) + { + if (!IsValidTarget(from, order)) + { + return; + } + + if (from.Target == null) + { + SendOrderMessage(from, order); + from.Target = new AIControlMobileTarget(this, order); + } + else if (from.Target is AIControlMobileTarget t && t.Order == order) + { + t.AddAI(this); + } + } + + private static void SendOrderMessage(Mobile from, OrderType order) + { + switch (order) + { + case OrderType.Transfer: + { + from.SendLocalizedMessage(502038); + // Click on the person to transfer ownership to. + break; + } + case OrderType.Friend: + { + from.SendLocalizedMessage(502020); + // Click on the player whom you wish to make a co-owner. + break; + } + case OrderType.Unfriend: + { + from.SendLocalizedMessage(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 = Mobile.Combatant; + + if (currentCombat == null || currentCombat == aggressor) + { + return; + } + + if (Mobile.GetDistanceToSqrt(aggressor) < Mobile.GetDistanceToSqrt(currentCombat)) + { + Mobile.Combatant = aggressor; + } + } + + public virtual void EndPickTarget(Mobile from, Mobile target, OrderType order) + { + if (!IsValidTarget(from, order) || order == OrderType.Attack && !CanAttackTarget(from, target)) + { + return; + } + + if (Mobile.CheckControlChance(from)) + { + Mobile.ControlTarget = target; + Mobile.ControlOrder = order; + + if (order == OrderType.Attack) + { + Mobile.FocusMob = target; + Mobile.Combatant = target; + Action = ActionType.Combat; + } + } + } + + private bool IsValidTarget(Mobile from, OrderType order) + { + if (Mobile.Deleted || !Mobile.Controlled || !from.InRange(Mobile, 14) + || from.Map != Mobile.Map || !from.CheckAlive()) + { + return false; + } + + var isOwner = from == Mobile.ControlMaster; + var isFriend = !isOwner && Mobile.IsPetFriend(from); + + if (!isOwner && !isFriend) + { + return false; + } + + if (isFriend && order is not (OrderType.Follow or OrderType.Stay or OrderType.Stop)) + { + return false; + } + + return true; + } + + private bool CanAttackTarget(Mobile from, Mobile target) + { + if (target is BaseCreature creature && creature.IsScaryToPets && Mobile.IsScaredOfScaryThings) + { + 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); + // You can not force your pet to attack a creature you are protected from. + return false; + } + + if (target is BaseFactionGuard) + { + Mobile.SayTo(from, "Your pet refuses to attack the guard."); + return false; + } + + return true; + } + + public void DebugSay(string message, int cooldownMs = 5000) + { + if (Mobile.Debug && NextDebugMessage - Core.TickCount <= 0) + { + Mobile.PublicOverheadMessage(MessageType.Regular, 41, false, message); + NextDebugMessage = Core.TickCount + cooldownMs; + } + } + + public virtual bool Think() + { + if (Mobile.Deleted || Mobile.Map == null) + { + return false; + } + + if (CheckFlee()) + { + return true; + } + + switch (Action) + { + case ActionType.Wander: + { + Mobile.OnActionWander(); + return DoActionWander(); + } + case ActionType.Combat: + { + Mobile.OnActionCombat(); + return DoActionCombat(); + } + case ActionType.Guard: + { + Mobile.OnActionGuard(); + return DoActionGuard(); + } + case ActionType.Flee: + { + Mobile.OnActionFlee(); + return DoActionFlee(); + } + case ActionType.Interact: + { + Mobile.OnActionInteract(); + return DoActionInteract(); + } + case ActionType.Backoff: + { + Mobile.OnActionBackoff(); + return DoActionBackoff(); + } + } + + return false; + } + + public virtual void OnActionChanged() + { + switch (Action) + { + case ActionType.Wander: + { + HandleWanderAction(); + break; + } + case ActionType.Combat: + { + HandleCombatAction(); + break; + } + case ActionType.Guard: + { + HandleGuardAction(); + break; + } + case ActionType.Flee: + { + HandleFleeAction(); + break; + } + case ActionType.Interact: + { + HandleInteractAction(); + break; + } + case ActionType.Backoff: + { + HandleBackoffAction(); + break; + } + } + } + + private void HandleWanderAction() + { + Mobile.FocusMob = null; + Mobile.Warmode = false; + Mobile.Combatant = null; + } + + private void HandleCombatAction() + { + Mobile.Warmode = true; + } + + private void HandleGuardAction() + { + Mobile.Warmode = true; + Mobile.Combatant = null; + } + + private void HandleFleeAction() + { + Mobile.FocusMob = null; + Mobile.Warmode = true; + } + + private void HandleInteractAction() + { + Mobile.Warmode = false; + } + + private void HandleBackoffAction() + { + Mobile.Warmode = false; + } + + public virtual bool DoActionWander() + { + if (CheckHerding()) + { + this.DebugSayFormatted($"I am being herded by {Mobile.ControlTarget?.Name ?? "Unknown"}."); + } + else if (Mobile.CurrentWayPoint != null) + { + HandleWayPoint(); + } + else if (Mobile.IsAnimatedDead) + { + FollowMaster(); + } + else if (CheckMove() && CanMoveNow(out _) && !Mobile.CheckIdle()) + { + WalkRandomInHome(3, 2, 1); + } + + return true; + } + + public virtual bool OnAtWayPoint() => true; + + private void HandleWayPoint() + { + var point = Mobile.CurrentWayPoint; + + if ((point.X != Mobile.Location.X || point.Y != Mobile.Location.Y) + && point.Map == Mobile.Map && point.Parent == null && !point.Deleted) + { + this.DebugSayFormatted($"Moving towards waypoint {point.X}, {point.Y}."); + + DoMove(Mobile.GetDirectionTo(point)); + } + else if (OnAtWayPoint()) + { + this.DebugSayFormatted($"I have reached waypoint {point.X}, {point.Y}."); + + Mobile.CurrentWayPoint = point.NextPoint; + if (point.NextPoint?.Deleted == true) + { + Mobile.CurrentWayPoint = point.NextPoint = point.NextPoint.NextPoint; + } + } + } + + private void FollowMaster() + { + var master = Mobile.SummonMaster; + if (master != null && master.Map == Mobile.Map && master.InRange(Mobile, Mobile.RangePerception)) + { + MoveTo(master, false, 1); + } + else + { + WalkRandomInHome(3, 2, 1); + } + } + + public virtual bool DoActionCombat() + { + if (Core.AOS && CheckHerding()) + { + this.DebugSayFormatted($"I am being herded by {Mobile.ControlTarget?.Name ?? "Unknown"}."); + return true; + } + + var combatant = Mobile.Combatant; + if (!IsValidCombatant(combatant)) + { + DebugSay("My combatant is missing. Returning home..."); + + Mobile.FocusMob = null; + Mobile.Warmode = false; + Mobile.Combatant = null; + + WalkRandomInHome(3, 2, 1); + return true; + } + + if (Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, combatant)) + { + this.DebugSayFormatted($"I used my abilities on {combatant.Name}!"); + } + + return true; + } + + public bool IsValidCombatant(Mobile combatant) => + IsValidFocusMob(combatant) && Mobile.InLOS(combatant); + + public bool IsValidFocusMob(Mobile focusMob) => + focusMob != null + && !focusMob.Deleted + && focusMob.Map == Mobile.Map + && focusMob.Alive + && (focusMob is not BaseCreature bc || !bc.IsDeadPet) + && focusMob.AccessLevel == AccessLevel.Player + && Mobile.CanSee(focusMob) + && Mobile.InRange(focusMob, Mobile.RangePerception); + + public virtual bool DoActionGuard() + { + DebugSay("I am still on guard."); + if (Utility.Random(10) == 0) + { + Mobile.Turn(Utility.Random(0, 2) - 1); + } + + return true; + } + + public virtual bool DoActionFlee() + { + var from = Mobile.FocusMob; + + if (!IsValidFocusMob(from)) + { + DebugSay("Focus target is missing."); + + WalkRandomInHome(3, 2, 1); + return true; + } + + DebugSay("I am fleeing!"); + + DoMove(from.GetDirectionTo(Mobile)); + return true; + } + + public virtual bool DoActionInteract() => true; + + public virtual bool DoActionBackoff() => true; + + public virtual bool CheckHerding() + { + var target = Mobile.TargetLocation; + + if (target == null) + { + return false; + } + + var distance = Mobile.GetDistanceToSqrt(target); + + if (distance >= 1 && distance <= 15) + { + DoMove(Mobile.GetDirectionTo(target)); + return true; + } + + if (distance < 1 && IsSpecialHerdingCase(target)) + { + HandleSpecialHerdingCase(); + } + + Mobile.TargetLocation = null; + return false; + } + + private bool IsSpecialHerdingCase(IPoint2D target) => target.X == 1076 && target.Y == 450 && Mobile is HordeMinionFamiliar; + + private void HandleSpecialHerdingCase() + { + if (Mobile.ControlMaster is PlayerMobile pm && pm.Quest is DarkTidesQuest qs) + { + var obj = qs.FindObjective(); + + if (obj?.Completed == false) + { + Mobile.AddToBackpack(new ScrollOfAbraxus()); + obj.Complete(); + } + } + } + + public virtual void DoBardPacified() + { + if (Core.Now < Mobile.BardEndTime) + { + DebugSay("I am pacified. Can not fight."); + + Mobile.Warmode = false; + Mobile.Combatant = null; + } + else + { + DebugSay("I am free from pacification."); + + Mobile.BardPacified = false; + } + } + + public virtual void DoBardProvoked() + { + if (Core.Now >= Mobile.BardEndTime && IsProvokerLost()) + { + DebugSay("Provoker missing."); + + Mobile.BardProvoked = false; + Mobile.BardMaster = null; + Mobile.BardTarget = null; + Mobile.Warmode = false; + Mobile.Combatant = null; + } + else if (IsProvokeTargetLost()) + { + DebugSay("Provoke target missing."); + + Mobile.BardProvoked = false; + Mobile.BardMaster = null; + Mobile.BardTarget = null; + Mobile.Warmode = false; + Mobile.Combatant = null; + } + else + { + Mobile.Combatant = Mobile.BardTarget; + Action = ActionType.Combat; + } + } + + private bool IsProvokerLost() => + Mobile.BardMaster?.Deleted != false + || Mobile.BardMaster.Map != Mobile.Map + || Mobile.GetDistanceToSqrt(Mobile.BardMaster) > Mobile.RangePerception; + + private bool IsProvokeTargetLost() => + Mobile.BardTarget?.Deleted != false + || Mobile.BardTarget.Map != Mobile.Map + || Mobile.GetDistanceToSqrt(Mobile.BardTarget) > Mobile.RangePerception; + + public virtual bool CheckFlee() + { + if (!Mobile.CheckFlee()) + { + return false; + } + + if (Mobile.Combatant == null) + { + WalkRandomInHome(3, 2, 1); + } + + return true; + } + + public virtual void OnTeleported() + { + DebugSay("Teleported; recalculating path..."); + + Path?.ForceRepath(); + } + + public virtual bool AcquireFocusMob(int iRange, FightMode acqType, bool bPlayerOnly, bool bFacFriend, bool bFacFoe) + { + if (Mobile.Deleted || Mobile.Map == null) + { + return false; + } + + if (HandleBardProvoked() || HandleControlled() || HandleConstantFocus()) + { + return true; + } + + if (acqType == FightMode.None) + { + Mobile.FocusMob = null; + return false; + } + + if (HandleAggressor(acqType)) + { + return false; + } + + if (Core.TickCount - Mobile.NextReacquireTime < 0) + { + Mobile.FocusMob = null; + return false; + } + + Mobile.NextReacquireTime = Core.TickCount + (int)Mobile.ReacquireDelay.TotalMilliseconds; + + DebugSay("Acquiring new target..."); + + if (Mobile.Map == null) + { + return Mobile.FocusMob != null; + } + + return AcquireNewFocusMob(Mobile.Map, iRange, acqType, bPlayerOnly, bFacFriend, bFacFoe); + } + + private bool HandleBardProvoked() + { + if (!Mobile.BardProvoked) + { + return false; + } + + if (Mobile.BardTarget?.Deleted != false) + { + Mobile.FocusMob = null; + return false; + } + + Mobile.FocusMob = Mobile.BardTarget; + return true; + } + + private bool HandleControlled() + { + if (!Mobile.Controlled) + { + return false; + } + + if (Mobile.ControlTarget?.Deleted == false && + Mobile.ControlTarget?.Hidden != true && + Mobile.ControlTarget?.Alive == true && + Mobile.ControlTarget?.IsDeadBondedPet != true && + Mobile.InRange(Mobile.ControlTarget, Mobile.RangePerception * 2)) + { + Mobile.FocusMob = Mobile.ControlTarget; + return true; + } + + if (Mobile.ControlTarget != null && Mobile.ControlTarget != Mobile.ControlMaster) + { + Mobile.ControlTarget = null; + } + + Mobile.FocusMob = null; + return false; + } + + private bool HandleConstantFocus() + { + if (Mobile.ConstantFocus == null) + { + return false; + } + + this.DebugSayFormatted($"Acquired focused target: {Mobile.ConstantFocus.Name}."); + + Mobile.FocusMob = Mobile.ConstantFocus; + return true; + } + + private bool HandleAggressor(FightMode acqType) + { + if (acqType != FightMode.Aggressor || + Mobile.Aggressors.Count > 0 || + Mobile.Aggressed.Count > 0 || + Mobile.FactionAllegiance != null || + Mobile.EthicAllegiance != null) + { + return false; + } + + Mobile.FocusMob = null; + return true; + } + + private bool AcquireNewFocusMob(Map map, int iRange, FightMode acqType, bool bPlayerOnly, bool bFacFriend, bool bFacFoe) + { + Mobile newFocusMob = null, enemySummonMob = null; + double val = double.MinValue, enemySummonVal = double.MinValue; + + foreach (var m in map.GetMobilesInRange(Mobile.Location, iRange)) + { + if (IsInvalidTarget(m, bPlayerOnly)) + { + continue; + } + + var bc = m as BaseCreature; + var pm = m as PlayerMobile; + + if (IsInvalidSummonTarget(m, bc, pm) || IsInvalidFactionTarget(m, bFacFriend, bFacFoe) + || IsInvalidFightModeTarget(m, acqType, bc)) + { + continue; + } + + var theirVal = Mobile.GetFightModeRanking(m, acqType, bPlayerOnly); + + if (theirVal > val && Mobile.InLOS(m)) + { + newFocusMob = m; + val = theirVal; + } + else if (Core.AOS && theirVal > enemySummonVal + && Mobile.InLOS(m) && bc?.Summoned == true && bc.Controlled != true) + { + enemySummonMob = m; + enemySummonVal = theirVal; + } + } + + Mobile.FocusMob = newFocusMob ?? enemySummonMob; + return Mobile.FocusMob != null; + } + + private bool IsInvalidTarget(Mobile m, bool bPlayerOnly) => + m.Deleted || m.Blessed || m == Mobile || m is BaseFamiliar || !m.Alive || m.IsDeadBondedPet || + m.AccessLevel > AccessLevel.Player || bPlayerOnly && !m.Player || !Mobile.CanSee(m); + + private bool IsInvalidSummonTarget(Mobile m, BaseCreature bc, PlayerMobile pm) + { + if (Core.AOS && bc?.Summoned == true && + (bc.SummonMaster == Mobile || !bc.SummonMaster.Player && IsHostile(bc.SummonMaster))) + { + return true; + } + + if (!Mobile.Summoned || Mobile.SummonMaster == null) + { + return false; + } + + return m == Mobile.SummonMaster || !SpellHelper.ValidIndirectTarget(Mobile.SummonMaster, m) || + Mobile.IsAnimatedDead && (pm != null || bc?.IsAnimatedDead == true || bc?.Controlled == true); + } + + private bool IsInvalidFactionTarget(Mobile m, bool bFacFriend, bool bFacFoe) + { + if (bFacFriend && !Mobile.IsFriend(m)) + { + return true; + } + + if (TransformationSpellHelper.UnderTransformation(m, typeof(EtherealVoyageSpell)) || + Mobile.Combatant != m && VirtueSystem.GetVirtues(m as PlayerMobile)?.HonorActive == true) + { + return true; + } + + return bFacFoe && (!Mobile.IsEnemy(m) || !bFacFriend && !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 valid = IsHostile(m) || Mobile.GetFactionAllegiance(m) == BaseCreature.Allegiance.Enemy + || Mobile.GetEthicAllegiance(m) == BaseCreature.Allegiance.Enemy; + + // Valid if FightMode is Evil and the target's karma is negative + return !valid && acqType != FightMode.Evil || (bc?.GetMaster()?.Karma ?? m.Karma) >= 0; + } + + private bool IsHostile(Mobile from) => Mobile.Combatant == from || from.Combatant == Mobile || IsAggressor(from) || IsAggressed(from); + + private bool IsAggressor(Mobile from) + { + foreach (var aggressor in Mobile.Aggressors) + { + if (aggressor.Defender == from) + { + return true; + } + } + return false; + } + + private bool IsAggressed(Mobile from) + { + foreach (var aggressed in Mobile.Aggressed) + { + if (aggressed.Attacker == from) + { + return true; + } + } + + return false; + } + + public virtual void DetectHidden() + { + if (Mobile.Deleted || Mobile.Map == null || !CanDetectHidden) + { + return; + } + + DebugSay("Checking for hidden entities..."); + + var srcSkill = Mobile.Skills.DetectHidden.Value; + + if (srcSkill <= 0) + { + return; + } + + foreach (var trg in Mobile.GetMobilesInRange(Mobile.RangePerception)) + { + if (IsValidTargetCombatTarget(trg)) + { + TryDetectHidden(trg, srcSkill); + } + } + } + + private bool IsValidTargetCombatTarget(Mobile trg) => trg != Mobile && trg.Player && trg.Alive && trg.Hidden && + trg.AccessLevel == AccessLevel.Player && Mobile.InLOS(trg); + + private void TryDetectHidden(Mobile trg, double srcSkill) + { + this.DebugSayFormatted($"Trying to detect: {trg.Name}"); + + 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! + } + } + + public virtual void Deactivate() + { + if (!Mobile.PlayerRangeSensitive) + { + return; + } + + if (Mobile.Map == Map.Internal || !Mobile.Controlled && !Mobile.Map.GetSector(Mobile.Location).Active) + { + _timer.Stop(); + } + + if (ShouldReturnToHome(Mobile.Spawner as Spawner)) + { + Timer.StartTimer(ReturnToHome); + } + } + + private bool ShouldReturnToHome(Spawner spawner) => + spawner?.ReturnOnDeactivate == true && !Mobile.Controlled && + (spawner.HomeLocation == Point3D.Zero || !Mobile.InRange(spawner.HomeLocation, spawner.HomeRange)); + + private void ReturnToHome() + { + if (Mobile.Spawner is not Spawner spawner) + { + return; + } + + var loc = spawner.GetSpawnPosition(Mobile, spawner.Map); + + if (loc != Point3D.Zero) + { + Mobile.MoveToWorld(loc, spawner.Map); + } + + _timer.Start(); + } + + public virtual void Activate() + { + if (!_timer.Running) + { + _timer.Start(); + } + } + + public virtual void OnCurrentSpeedChanged() + { + _timer.Interval = TimeSpan.FromMilliseconds(Mobile.CurrentSpeed * 1000); + } +} diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/ContextMenu.cs b/Projects/UOContent/Mobiles/AI/BaseAI/ContextMenu.cs new file mode 100644 index 000000000..ca562bce7 --- /dev/null +++ b/Projects/UOContent/Mobiles/AI/BaseAI/ContextMenu.cs @@ -0,0 +1,75 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2025 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: ContextMenu.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 Server.Collections; +using Server.ContextMenus; + +namespace Server.Mobiles; + +public abstract partial class BaseAI +{ + public virtual void GetContextMenuEntries(Mobile from, ref PooledRefList list) + { + if (!from.Alive || !Mobile.Controlled || !from.InRange(Mobile, 16)) + { + return; + } + + if (from == Mobile.ControlMaster) + { + AddControlMasterEntries(ref list); + } + else if (Mobile.IsPetFriend(from)) + { + AddPetFriendEntries(ref list); + } + } + + private void AddControlMasterEntries(ref PooledRefList list) + { + var isDeadPet = Mobile.IsDeadPet; + + 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 (Mobile.CanDrop) + { + list.Add(new InternalEntry(3006109, 14, OrderType.Drop, !isDeadPet)); // Command: Drop + } + + list.Add(new InternalEntry(3006098, 14, OrderType.Rename, true)); // Rename + + if (!Mobile.Summoned && Mobile is not GrizzledMare) + { + 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 + } + + list.Add(new InternalEntry(3006118, 14, OrderType.Release, true)); // Release + } + + private void AddPetFriendEntries(ref PooledRefList list) + { + var isDeadPet = 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 + } +} diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/DebugInterpolatedStringHandler.cs b/Projects/UOContent/Mobiles/AI/BaseAI/DebugInterpolatedStringHandler.cs index c2d5dcf15..da1f80019 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/DebugInterpolatedStringHandler.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/DebugInterpolatedStringHandler.cs @@ -79,7 +79,7 @@ public ref struct DebugInterpolatedStringHandler /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. public DebugInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider? provider, Mobiles.BaseAI ai) { - _debugActive = (ai.Mobile as BaseCreature)?.Debug == true && Core.TickCount >= ai.NextDebugMessage; + _debugActive = ai.Mobile?.Debug == true && Core.TickCount >= ai.NextDebugMessage; if (_debugActive) { @@ -660,3 +660,4 @@ public ref struct DebugInterpolatedStringHandler } } } + diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/InternalEntry.cs b/Projects/UOContent/Mobiles/AI/BaseAI/InternalEntry.cs new file mode 100644 index 000000000..bf8dd029a --- /dev/null +++ b/Projects/UOContent/Mobiles/AI/BaseAI/InternalEntry.cs @@ -0,0 +1,119 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2025 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: InternalEntry.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 Server.ContextMenus; +using Server.Gumps; + +namespace Server.Mobiles; + +internal 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 (!IsValidClick(from, target, out var bc) || + IsInvalidOrderForDeadPet(bc) || + !IsOwnerOrFriend(from, bc, out var isFriend) || + IsInvalidOrderForFriend(isFriend)) + { + 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; + 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: + case OrderType.Attack: + case OrderType.Transfer: + case OrderType.Friend: + case OrderType.Unfriend: + { + HandleTargetOrder(from, bc); + break; + } + case OrderType.Release: + { + HandleReleaseOrder(from, bc); + break; + } + default: + { + 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.ControlTarget = null; + bc.ControlOrder = _order; + } + } +} diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/OnSpeech.cs b/Projects/UOContent/Mobiles/AI/BaseAI/OnSpeech.cs new file mode 100644 index 000000000..32c0356b1 --- /dev/null +++ b/Projects/UOContent/Mobiles/AI/BaseAI/OnSpeech.cs @@ -0,0 +1,457 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2025 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: OnSpeech.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 Server.Items; +using Server.Gumps; + +namespace Server.Mobiles; + +public abstract partial class BaseAI +{ + public virtual bool HandlesOnSpeech(Mobile from) + { + if (from.AccessLevel >= AccessLevel.GameMaster) + { + return true; + } + + if (from.Alive && Mobile.Controlled && Mobile.Commandable && + (from == Mobile.ControlMaster || Mobile.IsPetFriend(from))) + { + return true; + } + + return from.Alive && from.InRange(Mobile.Location, 3) && Mobile.IsHumanInTown(); + } + + public virtual void OnSpeech(SpeechEventArgs e) + { + if (WasNamed(e.Speech) && e.Mobile.Alive && + e.Mobile.InRange(Mobile.Location, 3) && Mobile.IsHumanInTown()) + { + if (HandleMoveCommand(e) || HandleTimeCommand(e) || HandleTrainCommand(e)) + { + return; + } + } + + if (Mobile.Controlled && Mobile.Commandable) + { + AllOnSpeechPet(e); + NamedOnSpeechPet(e); + return; + } + + if (e.Mobile.AccessLevel >= AccessLevel.GameMaster) + { + HandleGMCommands(e); + } + } + + private bool HandleMoveCommand(SpeechEventArgs e) + { + if (!e.HasKeyword(0x9D)) // *move* + { + return false; + } + + if ((Core.Now - _lastOrder).TotalSeconds < 5) + { + return true; + } + + _lastOrder = Core.Now; + + var map = Mobile.Map; + var currentLoc = Mobile.Location; + + var newX = currentLoc.X + Utility.RandomMinMax(-1, 1); + var newY = currentLoc.Y + Utility.RandomMinMax(-1, 1); + var newZ = currentLoc.Z; + + if (map != null && map.CanFit(newX, newY, newZ, 16, false, false)) + { + Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 501516); // Excuse me? + Mobile.Location = new Point3D(newX, newY, newZ); + } + else + { + Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 501487); + // You're standing too close, go away. + } + + return true; + } + + private bool HandleTimeCommand(SpeechEventArgs e) + { + if (!e.HasKeyword(0x9E)) // *time* + { + return false; + } + + if ((Core.Now - _lastOrder).TotalSeconds < 5) + { + return true; + } + + _lastOrder = Core.Now; + + Clock.GetTime(Mobile, out var generalNumber, out _); + Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, generalNumber); + + return true; + } + + private bool HandleTrainCommand(SpeechEventArgs e) + { + if (!e.HasKeyword(0x6C)) // *train* + { + return false; + } + + HandleTraining(e.Mobile); + return true; + } + + public virtual void AllOnSpeechPet(SpeechEventArgs e) + { + if (!e.Mobile.InRange(Mobile.Location, 14)) + { + return; + } + + var isOwner = e.Mobile == Mobile.ControlMaster; + var isPetFriend = !isOwner && Mobile.IsPetFriend(e.Mobile); + + if (!isOwner && !isPetFriend) + { + return; + } + + var keyword = e.GetFirstKeyword( + 0x164, // all come + 0x165, // all follow + 0x166, // all guard + 0x167, // all stop + 0x168, // all kill + 0x169, // all attack + 0x16B, // all guard me + 0x16C, // all follow me + 0x170 // all stay + ); + + switch (keyword) + { + case 0x164: // all come + { + HandleComeCommand(e.Mobile, true); + break; + } + case 0x165: // all follow + { + BeginPickTarget(e.Mobile, OrderType.Follow); + break; + } + case 0x166: // all guard + case 0x16B: // all guard me + { + HandleGuardCommand(e.Mobile, true); + break; + } + case 0x167: // all stop + { + HandleStayStopFollowCommand(e.Mobile, OrderType.Stop); + break; + } + case 0x168: // all kill + case 0x169: // all attack + { + HandleAttackCommand(e.Mobile, true); + break; + } + case 0x16C: // all follow me + { + HandleStayStopFollowCommand(e.Mobile, OrderType.Follow, e.Mobile); + break; + } + case 0x170: // all stay + { + HandleStayStopFollowCommand(e.Mobile, OrderType.Stay); + break; + } + } + } + + public virtual void NamedOnSpeechPet(SpeechEventArgs e) + { + if (!e.Mobile.InRange(Mobile.Location, 14)) + { + return; + } + + var isOwner = e.Mobile == Mobile.ControlMaster; + var isPetFriend = !isOwner && Mobile.IsPetFriend(e.Mobile); + + if (!isOwner && !isPetFriend) + { + return; + } + + var keyword = e.GetFirstKeyword( + 0x155, // *come + 0x156, // *drop + 0x15A, // *follow + 0x15B, // *friend + 0x15C, // *guard + 0x15D, // *kill + 0x15E, // *attack + 0x161, // *stop + 0x163, // *follow me + 0x16D, // *release + 0x16E, // *transfer + 0x16F // *stay + ); + + switch (keyword) + { + case 0x155: // *come + { + HandleComeCommand(e.Mobile, true); + break; + } + case 0x156: // *drop + { + HandleDropCommand(e.Mobile, true, e.Speech); + break; + } + case 0x15A: // *follow + { + BeginPickTarget(e.Mobile, OrderType.Follow); + break; + } + case 0x15B: // *friend + { + HandleFriendCommand(e.Mobile, true, e.Speech); + break; + } + case 0x15C: // *guard + { + HandleGuardCommand(e.Mobile, true); + break; + } + case 0x15D: // *kill + case 0x15E: // *attack + { + HandleAttackCommand(e.Mobile, true); + break; + } + case 0x161: // *stop + { + HandleStayStopFollowCommand(e.Mobile, OrderType.Stop); + break; + } + case 0x163: // *follow me + { + HandleStayStopFollowCommand(e.Mobile, OrderType.Follow, e.Mobile); + break; + } + case 0x16D: // *release + { + HandleReleaseCommand(e.Mobile, true, e.Speech); + break; + } + case 0x16E: // *transfer + { + HandleTransferCommand(e.Mobile, true, e.Speech); + break; + } + case 0x16F: // *stay + { + HandleStayStopFollowCommand(e.Mobile, OrderType.Stay); + break; + } + } + } + + private void HandleTraining(Mobile from) + { + var foundSomething = false; + + foreach (var skill in Mobile.Skills) + { + if (skill.Base < 60.0 || !Mobile.CheckTeach(skill.SkillName, from)) + { + continue; + } + + var toTeach = Math.Min(skill.Base / 3.0, 42.0); + + if (toTeach <= from.Skills[skill.SkillName].Base) + { + continue; + } + + var number = 1043059 + (int)skill.SkillName; // alchemy + if (number > 1043107) // disarming traps + { + continue; + } + + if (!foundSomething) + { + Mobile.Say(1043058); // I can train the following: + foundSomething = true; + } + + Mobile.Say(number); + } + + if (!foundSomething) + { + Mobile.Say(501505); // Alas, I cannot teach thee anything. + } + } + + private void HandleComeCommand(Mobile from, bool isOwner) + { + if (isOwner && Mobile.CheckControlChance(from)) + { + _commandIssuer = from; + Mobile.ControlTarget = null; + Mobile.ControlOrder = OrderType.Come; + } + } + + private void HandleGuardCommand(Mobile from, bool isOwner) + { + if (isOwner && Mobile.CheckControlChance(from)) + { + _commandIssuer = from; + Mobile.ControlTarget = null; + Mobile.ControlOrder = OrderType.Guard; + } + } + + private void HandleStayStopFollowCommand(Mobile from, OrderType order, Mobile target = null) + { + if (Mobile.CheckControlChance(from)) + { + _commandIssuer = from; + Mobile.ControlTarget = target; + Mobile.ControlOrder = order; + } + } + + private void HandleAttackCommand(Mobile from, bool isOwner) + { + if (isOwner) + { + _commandIssuer = from; + BeginPickTarget(from, OrderType.Attack); + } + } + + private void HandleDropCommand(Mobile from, bool isOwner, string speech) + { + if (isOwner && !Mobile.IsDeadPet && !Mobile.Summoned && WasNamed(speech) + && Mobile.CheckControlChance(from)) + { + _commandIssuer = from; + Mobile.ControlTarget = null; + Mobile.ControlOrder = OrderType.Drop; + } + } + + private void HandleFriendCommand(Mobile from, bool isOwner, string speech) + { + if (isOwner && WasNamed(speech) && Mobile.CheckControlChance(from)) + { + if (Mobile.Summoned || Mobile is GrizzledMare) + { + from.SendLocalizedMessage(1005481); + // Summoned creatures are loyal only to their summoners. + return; + } + + if (from.HasTrade) + { + from.SendLocalizedMessage(1070947); + // You cannot friend a pet with a trade pending + return; + } + + BeginPickTarget(from, OrderType.Friend); + } + } + + private void HandleReleaseCommand(Mobile from, bool isOwner, string speech) + { + if (!isOwner) + { + return; + } + + if (WasNamed(speech) && Mobile.CheckControlChance(from)) + { + if (!Mobile.Summoned) + { + from.SendGump(new ConfirmReleaseGump(from, Mobile)); + } + else + { + Mobile.ControlOrder = OrderType.Release; + } + } + } + + private void HandleTransferCommand(Mobile from, bool isOwner, string speech) + { + if (isOwner && !Mobile.IsDeadPet && WasNamed(speech) && Mobile.CheckControlChance(from)) + { + if (Mobile.Summoned || Mobile is GrizzledMare) + { + from.SendLocalizedMessage(1005487); + // You cannot transfer ownership of a summoned creature. + return; + } + + if (from.HasTrade) + { + from.SendLocalizedMessage(1010507); + // You cannot transfer a pet with a trade pending + return; + } + + BeginPickTarget(from, OrderType.Transfer); + } + } + + private void HandleGMCommands(SpeechEventArgs e) + { + this.DebugSayFormatted($"Command is from GM: {e.Mobile.Name}, Target: {Mobile.ControlTarget?.Name ?? "None or Unknown"}"); + + if (Mobile.FindMyName(e.Speech, true) && e.Speech.InsensitiveContains("obey")) + { + Mobile.SetControlMaster(e.Mobile); + + if (Mobile.Summoned) + { + Mobile.SummonMaster = e.Mobile; + } + } + } +} diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/PetOrderHandlers.cs b/Projects/UOContent/Mobiles/AI/BaseAI/PetOrderHandlers.cs new file mode 100644 index 000000000..08debb256 --- /dev/null +++ b/Projects/UOContent/Mobiles/AI/BaseAI/PetOrderHandlers.cs @@ -0,0 +1,241 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2025 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: PetOrderHandlers.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; + +namespace Server.Mobiles; + +public abstract partial class BaseAI +{ + public virtual void OnCurrentOrderChanged() + { + if (Mobile.Deleted || Mobile.ControlMaster?.Deleted != false) + { + return; + } + + switch (Mobile.ControlOrder) + { + case OrderType.None: + { + HandleNoOrder(); + break; + } + case OrderType.Come: + case OrderType.Drop: + case OrderType.Friend: + case OrderType.Unfriend: + { + break; + } + case OrderType.Release: + { + HandleReleaseOrder(); + break; + } + case OrderType.Stop: + { + HandleStopOrder(); + break; + } + case OrderType.Transfer: + { + HandleTransferOrder(); + break; + } + case OrderType.Stay: + { + HandleStayOrder(); + break; + } + case OrderType.Guard: + { + HandleGuardOrder(); + break; + } + case OrderType.Attack: + { + HandleAttackOrder(); + break; + } + case OrderType.Follow: + { + HandleFollowOrder(); + break; + } + case OrderType.Rename: + { + HandleRenameOrder(); + break; + } + } + } + + private void HandleNoOrder() + { + Mobile.ControlTarget = null; + Mobile.FocusMob = null; + Mobile.Warmode = false; + Mobile.Combatant = null; + } + + private void HandleTransferOrder() + { + if (Mobile.ControlMaster?.Alive != true) + { + return; + } + + _commandIssuer?.RevealingAction(); + Mobile.FocusMob = null; + Mobile.Warmode = false; + Mobile.Combatant = null; + Mobile.PlaySound(Mobile.GetIdleSound()); + _commandIssuer = null; + } + + private void HandleGuardOrder() + { + if (Mobile.ControlMaster?.Alive != true) + { + return; + } + + _commandIssuer?.RevealingAction(); + Mobile.FocusMob = null; + Mobile.Warmode = true; + Mobile.PlaySound(Mobile.GetAttackSound()); + Mobile.ControlMaster?.SendLocalizedMessage(1049671, Mobile.Name); + // ~1_NAME~ is now guarding you. + _commandIssuer = null; + } + + private void HandleAttackOrder() + { + if (Mobile.ControlMaster?.Alive != true) + { + return; + } + + _commandIssuer?.RevealingAction(); + + if (Mobile.ControlTarget != null && + !Mobile.ControlTarget.Deleted && + Mobile.ControlTarget.Alive) + { + Mobile.FocusMob = Mobile.ControlTarget; + Mobile.Combatant = Mobile.ControlTarget; + } + else + { + Mobile.FocusMob = null; + Mobile.Combatant = null; + } + + Mobile.Warmode = true; + Mobile.PlaySound(Mobile.GetAttackSound()); + _commandIssuer = null; + } + + private void HandleFollowOrder() + { + if (Mobile.ControlMaster?.Alive != true) + { + return; + } + + _commandIssuer?.RevealingAction(); + Mobile.FocusMob = null; + Mobile.Warmode = false; + Mobile.Combatant = null; + Mobile.PlaySound(Mobile.GetIdleSound()); + _commandIssuer = null; + } + + private void HandleStayOrder() + { + if (Mobile.ControlMaster?.Alive != true) + { + return; + } + + _commandIssuer?.RevealingAction(); + Mobile.FocusMob = null; + Mobile.Warmode = false; + Mobile.Combatant = null; + Mobile.PlaySound(Mobile.GetIdleSound()); + Mobile.Home = Mobile.Location; + _commandIssuer = null; + } + + private void HandleStopOrder() + { + if (Mobile.ControlMaster?.Alive != true) + { + return; + } + + _commandIssuer?.RevealingAction(); + Mobile.ControlTarget = null; + Mobile.FocusMob = null; + Mobile.Warmode = false; + Mobile.Combatant = null; + Mobile.PlaySound(Mobile.GetIdleSound()); + _commandIssuer = null; + } + + private void HandleReleaseOrder() + { + if (Mobile.ControlMaster?.Alive != true) + { + return; + } + + if (Mobile.Summoned) + { + Mobile.Kill(); + return; + } + + if (!string.IsNullOrEmpty(Mobile.Name)) + { + Mobile.Name = null; + } + + _commandIssuer?.RevealingAction(); + Mobile.ControlTarget = null; + Mobile.FocusMob = null; + Mobile.Warmode = false; + Mobile.Combatant = null; + Mobile.PlaySound(Mobile.GetIdleSound()); + Mobile.BondingBegin = DateTime.MinValue; + Mobile.OwnerAbandonTime = DateTime.MinValue; + Mobile.IsBonded = false; + Mobile.SetControlMaster(null); + _commandIssuer = null; + } + + public virtual void HandleRenameOrder() + { + if (Mobile.Summoned) + { + Mobile.ControlMaster?.SendMessage("You cannot rename a summoned creature."); + } + else + { + Mobile.ControlMaster?.SendMessage("Change name on pet health bar."); + } + } +} diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs b/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs new file mode 100644 index 000000000..f5919ee6b --- /dev/null +++ b/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs @@ -0,0 +1,518 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2025 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: PetOrders.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 . * + ************************************************************************/ + +namespace Server.Mobiles; + +public abstract partial class BaseAI +{ + public virtual bool Obey() => + !Mobile.Deleted && Mobile.ControlOrder switch + { + OrderType.None => DoOrderNone(), + OrderType.Come => DoOrderCome(), + OrderType.Drop => DoOrderDrop(), + OrderType.Friend => DoOrderFriend(), + OrderType.Unfriend => DoOrderUnfriend(), + OrderType.Guard => DoOrderGuard(), + OrderType.Attack => DoOrderAttack(), + OrderType.Release => DoOrderRelease(), + OrderType.Stay => DoOrderStay(), + OrderType.Stop => DoOrderStop(), + OrderType.Follow => DoOrderFollow(), + OrderType.Transfer => DoOrderTransfer(), + _ => false + }; + + public virtual bool DoOrderNone() + { + DebugSay("I currently have no orders."); + + Mobile.Warmode = IsValidCombatant(Mobile.Combatant); + + WalkRandom(3, 2, 1); + return true; + } + + public virtual bool DoOrderCome() + { + if (CheckHerding()) + { + this.DebugSayFormatted($"I am being herded by {Mobile.ControlTarget?.Name ?? "Unknown"}."); + return true; + } + + if (Mobile.ControlMaster?.Deleted != false) + { + return true; + } + + WalkMobileRange(Mobile.ControlMaster, 1, false, 1, 2); + + if (Mobile.GetDistanceToSqrt(Mobile.ControlMaster) <= 2) + { + Mobile.ControlOrder = OrderType.Stay; + } + + return true; + } + + public virtual bool DoOrderFollow() + { + if (CheckHerding()) + { + this.DebugSayFormatted($"I am being herded by {Mobile.ControlTarget?.Name ?? "Unknown"}."); + return true; + } + + if (Mobile.ControlTarget?.Deleted == false && Mobile.ControlTarget != Mobile) + { + FollowTarget(); + } + else + { + DebugSay("I have no one to follow."); + + Mobile.ControlOrder = OrderType.None; + } + + return true; + } + + private void FollowTarget() + { + var currentDistance = (int)Mobile.GetDistanceToSqrt(Mobile.ControlTarget); + + if (currentDistance > Mobile.RangePerception) + { + this.DebugSayFormatted($"Master {Mobile.ControlMaster?.Name ?? "Unknown"} is missing. Staying put."); + return; + } + + this.DebugSayFormatted($"I am ordered to follow {Mobile.ControlTarget?.Name}."); + + if (currentDistance > 1) + { + WalkMobileRange(Mobile.ControlTarget, 1, currentDistance > 2, 1, 2); + } + } + + public virtual bool DoOrderDrop() + { + if (Mobile.IsDeadPet || !Mobile.CanDrop) + { + return true; + } + + this.DebugSayFormatted($"I am ordered to drop my items by {Mobile.ControlMaster?.Name ?? "Unknown"}."); + + Mobile.ControlOrder = OrderType.None; + + DropItems(); + return true; + } + + private void DropItems() + { + var pack = Mobile.Backpack; + + if (pack == null) + { + return; + } + + var items = pack.Items; + + for (var i = items.Count - 1; i >= 0; --i) + { + if (i < items.Count) + { + items[i].MoveToWorld(Mobile.Location, Mobile.Map); + } + } + } + + public virtual bool DoOrderFriend() + { + var from = Mobile.ControlMaster; + var to = Mobile.ControlTarget; + + HandleFriendRequest(from, to); + return true; + } + + private void HandleFriendRequest(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); + // As a young player, you may not friend pets to older players. + return; + } + + if (!youngFrom && youngTo) + { + from.SendLocalizedMessage(502041); + // As an older player, you may not friend pets to young players. + return; + } + + if (!from.CanBeBeneficial(to, true)) + { + return; + } + + if (to?.Deleted != false || from == to || !to.Player) + { + Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 502039); + // *looks confused* + return; + } + + if (from.HasTrade || to.HasTrade) + { + (from.HasTrade ? from : to).SendLocalizedMessage(1070947); + // You cannot friend a pet with a trade pending + return; + } + + if (Mobile.IsPetFriend(to)) + { + from.SendLocalizedMessage(1049691); + // That person is already a friend. + Mobile.ControlOrder = OrderType.None; + return; + } + + if (!Mobile.AllowNewPetFriend) + { + from.SendLocalizedMessage(1005482); + // Your pet does not seem to be interested in making new friends right now. + return; + } + + from.SendLocalizedMessage(1049676, $"{Mobile.Name}\t{to.Name}"); + // ~1_NAME~ will now accept movement commands from ~2_NAME~. + + to.SendLocalizedMessage(1043246, $"{from.Name}\t{Mobile.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. + + Mobile.AddPetFriend(to); + + Mobile.ControlTarget = to; + Mobile.ControlOrder = OrderType.Follow; + } + + public virtual bool DoOrderUnfriend() + { + var from = Mobile.ControlMaster; + var to = Mobile.ControlTarget; + + HandleUnfriendRequest(from, to); + return true; + } + + private void HandleUnfriendRequest(Mobile from, Mobile to) + { + if (from?.Deleted != false || to?.Deleted != false || from == to || !to.Player) + { + Mobile.PublicOverheadMessage(MessageType.Regular, 0x3B2, 502039); + // *looks confused* + return; + } + + if (!Mobile.IsPetFriend(to)) + { + from.SendLocalizedMessage(1070953); + // That person is not a friend. + Mobile.ControlOrder = OrderType.None; + return; + } + + from.SendLocalizedMessage(1070951, $"{Mobile.Name}\t{to.Name}"); + // ~1_NAME~ will no longer accept movement commands from ~2_NAME~. + + to.SendLocalizedMessage(1070952, $"{from.Name}\t{Mobile.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. + + Mobile.RemovePetFriend(to); + + Mobile.ControlTarget = from; + Mobile.ControlOrder = OrderType.Follow; + } + + public virtual bool DoOrderGuard() + { + var controlMaster = Mobile.ControlMaster; + + if (Mobile.IsDeadPet || controlMaster?.Deleted != false) + { + return true; + } + + FindCombatant(); + + if (IsValidCombatant(Mobile.Combatant)) + { + var combatant = Mobile.Combatant; + + this.DebugSayFormatted($"Attacking target: {combatant.Name}"); + + Mobile.Combatant = combatant; + Mobile.FocusMob = combatant; + Action = ActionType.Combat; + + Think(); + } + else + { + this.DebugSayFormatted($"Guarding my master, {controlMaster.Name}."); + + var guardLocation = controlMaster.Location; + + var distance = (int)Mobile.GetDistanceToSqrt(guardLocation); + + if (distance > 3) + { + DoMove(Mobile.GetDirectionTo(guardLocation)); + } + else + { + WalkRandom(3, 1, 1); + } + } + + return true; + } + + public virtual bool DoOrderAttack() + { + if (Mobile.IsDeadPet) + { + return false; + } + + if (IsInvalidControlTarget(Mobile.ControlTarget)) + { + HandleInvalidControlTarget(); + } + else + { + Mobile.Combatant = Mobile.ControlTarget; + + this.DebugSayFormatted($"Attacking target: {Mobile.ControlTarget?.Name}"); + + Think(); + } + + return true; + } + + private bool IsInvalidControlTarget(Mobile target) => target?.Deleted != false || target.Map != Mobile.Map || !target.Alive || target.IsDeadBondedPet; + + private void HandleInvalidControlTarget() + { + DebugSay("Target is either dead, hidden, or out of range."); + + Mobile.ControlOrder = Core.AOS || Mobile.IsBonded ? OrderType.Follow : OrderType.None; + + if (Mobile.FightMode is FightMode.Closest or FightMode.Aggressor) + { + FindCombatant(); + } + } + + private void FindCombatant() + { + foreach (var aggr in Mobile.GetMobilesInRange(Mobile.RangePerception)) + { + if (!Mobile.CanSee(aggr) || aggr.Combatant != Mobile || aggr.IsDeadBondedPet || !aggr.Alive) + { + continue; + } + + if (Mobile.InLOS(aggr)) + { + Mobile.ControlTarget = aggr; + Mobile.ControlOrder = OrderType.Attack; + Mobile.Combatant = aggr; + + this.DebugSayFormatted($"{aggr.Name} is still alive. Resuming attacks..."); + + Think(); + break; + } + } + } + + public virtual bool DoOrderRelease() + { + DebugSay("I have been released to the wild."); + + var spawner = Mobile.Spawner; + + if (spawner != null && spawner.HomeLocation != Point3D.Zero) + { + Mobile.Home = spawner.HomeLocation; + Mobile.RangeHome = spawner.HomeRange; + } + else + { + Action = ActionType.Wander; + } + + if (Mobile.DeleteOnRelease || Mobile.IsDeadPet) + { + Mobile.Delete(); + } + else + { + Mobile.BeginDeleteTimer(); + + if (Mobile.CanDrop) + { + Mobile.DropBackpack(); + } + } + + return true; + } + + public virtual bool DoOrderStay() + { + if (CheckHerding()) + { + this.DebugSayFormatted($"I am being herded by {Mobile.ControlTarget?.Name ?? "Unknown"}."); + } + else + { + this.DebugSayFormatted($"I have been ordered to stay by {Mobile.ControlMaster?.Name ?? "Unknown"}."); + } + + WalkRandomInHome(3, 2, 1); + return true; + } + + public virtual bool DoOrderStop() + { + if (CheckHerding()) + { + this.DebugSayFormatted($"I am being herded by {Mobile.ControlTarget?.Name ?? "Unknown"}."); + } + else + { + this.DebugSayFormatted($"I have been ordered to stop by {Mobile.ControlMaster?.Name ?? "Unknown"}."); + } + + if (Core.ML) + { + WalkRandomInHome(5, 2, 1); + } + + return true; + } + + public virtual bool DoOrderTransfer() + { + if (Mobile.IsDeadPet) + { + return true; + } + + var from = Mobile.ControlMaster; + var to = Mobile.ControlTarget; + + if (from?.Deleted == false && to?.Deleted == false && from != to && to.Player) + { + this.DebugSayFormatted($"Beginning transfer with {to.Name}"); + + 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. + return true; + } + + if (!youngFrom && youngTo) + { + from.SendLocalizedMessage(502041); + // As an older player, you may not friend pets to young players. + return true; + } + + if (!Mobile.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; + } + + if (!Mobile.CanBeControlledBy(from)) + { + 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; + } + + if (Mobile.Combatant != null || Mobile.Aggressors.Count > 0 || + Mobile.Aggressed.Count > 0 || Core.TickCount < Mobile.NextCombatTime) + { + from.SendMessage("You can not transfer a pet while in combat."); + to.SendMessage("You can not transfer a pet while in combat."); + return false; + } + + var fromState = from.NetState; + var toState = to.NetState; + + if (fromState == null || toState == null) + { + return false; + } + + if (from.HasTrade || to.HasTrade) + { + from.SendLocalizedMessage(1010507); + // You cannot transfer a pet with a trade pending + to.SendLocalizedMessage(1010507); + // You cannot transfer a pet with a trade pending + return false; + } + + var container = fromState.AddTrade(toState); + container.DropItem(new TransferItem(Mobile)); + } + + Mobile.ControlOrder = OrderType.Stay; + 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); + } +} diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/SpeechEventArgsExt.cs b/Projects/UOContent/Mobiles/AI/BaseAI/SpeechEventArgsExt.cs new file mode 100644 index 000000000..5090f235a --- /dev/null +++ b/Projects/UOContent/Mobiles/AI/BaseAI/SpeechEventArgsExt.cs @@ -0,0 +1,35 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2025 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SpeechEventArgsExt.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; + +namespace Server; + +public static class SpeechEventArgsExt +{ + public static int GetFirstKeyword(this SpeechEventArgs e, params ReadOnlySpan keywords) + { + for (var i = 0; i < keywords.Length; i++) + { + var keyword = keywords[i]; + if (e.HasKeyword(keyword)) + { + return keyword; + } + } + + return 0; + } +} diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/TransferItem.cs b/Projects/UOContent/Mobiles/AI/BaseAI/TransferItem.cs new file mode 100644 index 000000000..1a8f0bd74 --- /dev/null +++ b/Projects/UOContent/Mobiles/AI/BaseAI/TransferItem.cs @@ -0,0 +1,190 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2025 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: TransferItem.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.Runtime.CompilerServices; + +namespace Server.Mobiles; + +internal sealed class TransferItem : Item +{ + private readonly BaseCreature _creature; + + public override string DefaultName => _creature.GetType().Name; + + public TransferItem(BaseCreature creature) : base(ShrinkTable.Lookup(creature)) + { + _creature = creature; + Movable = false; + + Hue = creature.Hue & 0x0FFF; + } + + public TransferItem(Serial serial) : base(serial) + { + } + + public static bool IsInCombat(BaseCreature creature) => 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); + 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, _creature.Name); // Pet Name: ~1_val~ + + if (_creature.ControlMaster != null) + { + list.Add(1041602, _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) || IsInvalidTrade(from, to)) + { + return false; + } + + return !accepted || HandleAcceptedTrade(from, to); + } + + private bool IsInvalidTrade(Mobile from, Mobile to) => + Deleted + || _creature?.Deleted != false + || _creature.ControlMaster != from + || !from.CheckAlive() + || !to.CheckAlive() + || from.Map != _creature.Map + || !from.InRange(_creature, 14); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool HandleAcceptedTrade(Mobile from, Mobile to) => + ValidateYoungStatus(from, to) && ValidateControlStatus(from, to) && ValidateFollowerLimit(to) && + !IsInCombat(_creature); + + private bool ValidateFollowerLimit(Mobile to) + { + if (to.Followers + _creature.ControlSlots > to.FollowersMax) + { + to.SendLocalizedMessage(1049607); + // You have too many followers to control that 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 (youngFrom && !youngTo) + { + from.SendLocalizedMessage(502051); + // As a young player, you may not transfer pets to older players. + return false; + } + + if (!youngFrom && youngTo) + { + from.SendLocalizedMessage(502052); + // As an older player, you may not transfer pets to young players. + return false; + } + + return true; + } + + private bool ValidateControlStatus(Mobile from, Mobile to) + { + if (!_creature.CanBeControlledBy(to)) + { + SendTransferRefusalMessages(from, to, 1043248, 1043249); + // The pet refuses to be transferred because it will not obey ~1_NAME~.~3_BLANK~ + // The pet will not accept you as a master because it does not trust you.~3_BLANK~ + return false; + } + + if (!_creature.CanBeControlledBy(from)) + { + SendTransferRefusalMessages(from, to, 1043250, 1043251); + // The pet refuses to be transferred because it will not obey you sufficiently.~3_BLANK~ + // The pet will not accept you as a master because it does not trust ~2_NAME~.~3_BLANK~ + 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 || IsInvalidTrade(from, to)) + { + Delete(); + return; + } + + Delete(); + + if (!accepted || !_creature.SetControlMaster(to)) + { + return; + } + + TransferPetOwnership(from, to); + } + + private void TransferPetOwnership(Mobile from, Mobile to) + { + if (_creature.Summoned) + { + _creature.SummonMaster = to; + } + + _creature.ControlTarget = to; + _creature.ControlOrder = OrderType.Follow; + _creature.BondingBegin = DateTime.MinValue; + _creature.OwnerAbandonTime = DateTime.MinValue; + _creature.IsBonded = false; + _creature.PlaySound(_creature.GetIdleSound()); + + var args = $"{from.Name}\t{_creature.Name}\t{to.Name}"; + from.SendLocalizedMessage(1043253, args); + // You have transferred your pet to ~3_GETTER~. + to.SendLocalizedMessage(1043252, args); + // ~1_NAME~ has transferred the allegiance of ~2_PET_NAME~ to you. + } +} diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/WalkRandomLogic.cs b/Projects/UOContent/Mobiles/AI/BaseAI/WalkRandomLogic.cs new file mode 100644 index 000000000..e7d1af42f --- /dev/null +++ b/Projects/UOContent/Mobiles/AI/BaseAI/WalkRandomLogic.cs @@ -0,0 +1,125 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2025 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: WalkRandomLogic.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 Server.Engines.Spawners; + +namespace Server.Mobiles; + +public abstract partial class BaseAI +{ + public virtual void WalkRandom(int chanceToNotMove, int chanceToDir, int steps) + { + if (Mobile.Deleted || Mobile.DisallowAllMoves || chanceToNotMove <= 0) + { + return; + } + + var maxSteps = Math.Min(steps, 3); + + for (var i = 0; i < maxSteps; i++) + { + if (Utility.Random(1 + chanceToNotMove) == 0) + { + DoMove(GetRandomDirection(chanceToDir)); + } + } + } + + public virtual void WalkRandomInHome(int chanceToNotMove, int chanceToDir, int steps) + { + if (Mobile.Deleted || Mobile.DisallowAllMoves) + { + return; + } + + if (Mobile.Home == Point3D.Zero) + { + WalkRandomNoHome(chanceToNotMove, chanceToDir, steps); + } + else + { + WalkRandomWithHome(chanceToNotMove, chanceToDir, steps); + } + } + + private void WalkRandomNoHome(int chanceToNotMove, int chanceToDir, int steps) + { + if (Mobile.Spawner is RegionSpawner rs) + { + var region = rs.SpawnRegion; + + if (Mobile.Region.AcceptsSpawnsFrom(region)) + { + Mobile.WalkRegion = region; + + WalkRandom(chanceToNotMove, chanceToDir, steps); + + Mobile.WalkRegion = null; + } + else if (region.GoLocation != Point3D.Zero && Utility.RandomBool()) + { + DoMove(Mobile.GetDirectionTo(region.GoLocation)); + } + else + { + WalkRandom(chanceToNotMove, chanceToDir, 1); + } + } + else + { + WalkRandom(chanceToNotMove, chanceToDir, steps); + } + } + + private void WalkRandomWithHome(int chanceToNotMove, int chanceToDir, int steps) + { + if (Mobile.RangeHome == 0 && Mobile.Location != Mobile.Home) + { + DoMove(Mobile.GetDirectionTo(Mobile.Home)); + return; + } + + for (var i = 0; i < steps; i++) + { + var currDist = (int)Mobile.GetDistanceToSqrt(Mobile.Home); + + if (currDist > Mobile.RangeHome) + { + DoMove(Mobile.GetDirectionTo(Mobile.Home)); + } + else if (currDist < Mobile.RangeHome * 2 / 3 || Utility.Random(10) <= 5) + { + WalkRandom(chanceToNotMove, chanceToDir, 1); + } + else + { + DoMove(Mobile.GetDirectionTo(Mobile.Home)); + } + } + } + + private Direction GetRandomDirection(int chanceToDir) + { + var randomMove = Utility.Random(8 * (chanceToDir + 1)); + + if (randomMove < 8) + { + return (Direction)randomMove; + } + + return Mobile.Direction; + } +} diff --git a/Projects/UOContent/Mobiles/AI/HealerAI.cs b/Projects/UOContent/Mobiles/AI/HealerAI.cs index e6189e033..2a1127b43 100644 --- a/Projects/UOContent/Mobiles/AI/HealerAI.cs +++ b/Projects/UOContent/Mobiles/AI/HealerAI.cs @@ -31,23 +31,15 @@ public class HealerAI : BaseAI { var spellTarg = targ as ISpellTarget; - if (spellTarg?.Spell is CureSpell) + var funcs = spellTarg?.Spell switch { - ProcessTarget(targ, Cure); - } - else if (spellTarg?.Spell is GreaterHealSpell) - { - ProcessTarget(targ, GHeal); - } - else if (spellTarg?.Spell is HealSpell) - { - ProcessTarget(targ, LHeal); - } - else - { - targ.Cancel(Mobile, TargetCancelType.Canceled); - } + CureSpell => Cure, + GreaterHealSpell => GHeal, + HealSpell => LHeal, + _ => null + }; + ProcessTarget(targ, funcs); return true; } @@ -102,6 +94,12 @@ public class HealerAI : BaseAI private void ProcessTarget(Target targ, NeedDelegate[] func) { + if (func == null || func.Length == 0) + { + targ.Cancel(Mobile, TargetCancelType.Canceled); + return; + } + var toHelp = Find(func); if (toHelp == null) diff --git a/Projects/UOContent/Mobiles/AI/MageAI.cs b/Projects/UOContent/Mobiles/AI/MageAI.cs index f54326baf..03fe60a6a 100644 --- a/Projects/UOContent/Mobiles/AI/MageAI.cs +++ b/Projects/UOContent/Mobiles/AI/MageAI.cs @@ -20,8 +20,8 @@ public class MageAI : BaseAI private const double DispelChance = 0.75; // 75% chance to dispel at gm magery private const double InvisChance = 0.50; // 50% chance to invis at gm magery - private static readonly int[] _offsets = - { + private static readonly int[] Offsets = + [ -1, -1, -1, 0, -1, 1, @@ -47,7 +47,7 @@ public class MageAI : BaseAI 2, 0, 2, 1, 2, 2 - }; + ]; protected int _combo = -1; @@ -58,8 +58,7 @@ public class MageAI : BaseAI private LandTarget _revealTarget; - public MageAI(BaseCreature m) - : base(m) + public MageAI(BaseCreature m) : base(m) { } @@ -518,7 +517,15 @@ public class MageAI : BaseAI } } - DebugSay(spell != null ? $"Casting {spell.Name}" : "I don't have a spell to use!"); + if (spell != null) + { + this.DebugSayFormatted($"Casting {spell.Name}"); + } + else + { + DebugSay("I don't have a spell to use!"); + } + return spell; } @@ -628,6 +635,7 @@ public class MageAI : BaseAI Mobile.Combatant = c = Mobile.FocusMob!; this.DebugSayFormatted($"Something happened to my combatant, so I am going to fight {c.Name}"); + Mobile.FocusMob = null; } else @@ -766,7 +774,7 @@ public class MageAI : BaseAI RunTo(c); } - if (Mobile.Spell != null || !Mobile.InRange(c, 1) || Core.TickCount - Mobile.LastMoveTime > 800) + if (Mobile.InRange(c, 1) || Mobile.Spell?.IsCasting == true || Core.TickCount - Mobile.LastMoveTime > 400) { Mobile.Direction = Mobile.GetDirectionTo(c); } @@ -829,8 +837,6 @@ public class MageAI : BaseAI public override bool DoActionFlee() { - // Mobile c = m_Mobile.Combatant; - if ((Mobile.Mana > 20 || Mobile.Mana == Mobile.ManaMax) && Mobile.Hits > Mobile.HitsMax / 2) { DebugSay("I am stronger now, my guard is up"); @@ -1098,9 +1104,9 @@ public class MageAI : BaseAI py = toTarget.Y; } - for (var i = 0; i < _offsets.Length; i += 2) + for (var i = 0; i < Offsets.Length; i += 2) { - int x = _offsets[i], y = _offsets[i + 1]; + int x = Offsets[i], y = Offsets[i + 1]; var p = new Point3D(px + x, py + y, 0); diff --git a/Projects/UOContent/Mobiles/AI/MeleeAI.cs b/Projects/UOContent/Mobiles/AI/MeleeAI.cs index adc8d566b..edf0b108e 100644 --- a/Projects/UOContent/Mobiles/AI/MeleeAI.cs +++ b/Projects/UOContent/Mobiles/AI/MeleeAI.cs @@ -69,11 +69,6 @@ public class MeleeAI : BaseAI if (!MoveTo(combatant, true, Mobile.RangeFight)) { - if (Mobile.InRange(combatant, 1)) - { - Mobile.Direction = Mobile.GetDirectionTo(combatant); - } - if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true)) { this.DebugSayFormatted($"My move is blocked, so I am going to attack {Mobile.FocusMob!.Name}"); @@ -93,25 +88,21 @@ public class MeleeAI : BaseAI this.DebugSayFormatted($"I cannot find {combatant.Name}, so my guard is up"); } - else if (Core.TickCount - Mobile.LastMoveTime > 400) + else if (Core.TickCount - Mobile.LastMoveTime > 200) { Mobile.Direction = Mobile.GetDirectionTo(combatant); } - if (!Mobile.Controlled && !Mobile.Summoned && Mobile.CanFlee) + // We are low on health, should we flee? + if (!Mobile.Controlled && !Mobile.Summoned && Mobile.CanFlee && Mobile.Hits < Mobile.HitsMax * 20 / 100) { - if (Mobile.Hits < Mobile.HitsMax * 20 / 100) + var fleeChance = 10 + Math.Max(0, combatant.Hits - Mobile.Hits); // (10 + diff)% chance to flee; + if (Utility.Random(0, 100) < fleeChance) { - // We are low on health, should we flee? + this.DebugSayFormatted($"I am going to flee from {combatant.Name}"); - var fleeChance = 10 + Math.Max(0, combatant.Hits - Mobile.Hits); // (10 + diff)% chance to flee; - if (Utility.Random(0, 100) < fleeChance) - { - this.DebugSayFormatted($"I am going to flee from {combatant.Name}"); - - Action = ActionType.Flee; - return true; - } + Action = ActionType.Flee; + return true; } } @@ -146,8 +137,6 @@ public class MeleeAI : BaseAI { DebugSay("I am stronger now, so I will continue fighting"); - Mobile.PlaySound(Mobile.GetAttackSound()); - Mobile.CurrentSpeed = Mobile.ActiveSpeed; Action = ActionType.Combat; } else diff --git a/Projects/UOContent/Mobiles/AI/OppositionGroup.cs b/Projects/UOContent/Mobiles/AI/OppositionGroup.cs index c087112db..344b65eba 100644 --- a/Projects/UOContent/Mobiles/AI/OppositionGroup.cs +++ b/Projects/UOContent/Mobiles/AI/OppositionGroup.cs @@ -1,135 +1,125 @@ using System; using Server.Mobiles; -namespace Server +namespace Server; + +public class OppositionGroup { - public class OppositionGroup + private readonly Type[][] _types; + + public OppositionGroup(Type[][] types) => _types = types; + + public static OppositionGroup TerathansAndOphidians { get; } = new( + [ + [ + typeof(TerathanAvenger), + typeof(TerathanDrone), + typeof(TerathanMatriarch), + typeof(TerathanWarrior) + ], + [ + typeof(OphidianArchmage), + typeof(OphidianKnight), + typeof(OphidianMage), + typeof(OphidianMatriarch), + typeof(OphidianWarrior) + ] + ] + ); + + public static OppositionGroup SavagesAndOrcs { get; } = new( + [ + [ + typeof(Orc), + typeof(OrcBomber), + typeof(OrcBrute), + typeof(OrcCaptain), + typeof(OrcishLord), + typeof(OrcishMage), + typeof(SpawnedOrcishLord) + ], + [ + typeof(Savage), + typeof(SavageRider), + typeof(SavageRidgeback), + typeof(SavageShaman) + ] + ] + ); + + public static OppositionGroup FeyAndUndead { get; } = new( + [ + [ + typeof(Centaur), + typeof(EtherealWarrior), + typeof(Kirin), + typeof(LordOaks), + typeof(Pixie), + typeof(Silvani), + typeof(Unicorn), + typeof(Wisp), + typeof(Treefellow), + typeof(MLDryad), + typeof(Satyr) + ], + [ + typeof(AncientLich), + typeof(Bogle), + typeof(LichLord), + typeof(Shade), + typeof(Spectre), + typeof(Wraith), + typeof(BoneKnight), + typeof(Ghoul), + typeof(Mummy), + typeof(SkeletalKnight), + typeof(Skeleton), + typeof(Zombie), + typeof(ShadowKnight), + typeof(DarknightCreeper), + typeof(RevenantLion), + typeof(LadyOfTheSnow), + typeof(RottingCorpse), + typeof(SkeletalDragon), + typeof(Lich) + ] + ] + ); + + public bool IsEnemy(object from, object target) { - private readonly Type[][] _types; + var fromGroup = IndexOf(from); + var targGroup = IndexOf(target); - public OppositionGroup(Type[][] types) => _types = types; + return fromGroup != -1 && targGroup != -1 && fromGroup != targGroup; + } - public static OppositionGroup TerathansAndOphidians { get; } = new( - new[] - { - new[] - { - typeof(TerathanAvenger), - typeof(TerathanDrone), - typeof(TerathanMatriarch), - typeof(TerathanWarrior) - }, - new[] - { - typeof(OphidianArchmage), - typeof(OphidianKnight), - typeof(OphidianMage), - typeof(OphidianMatriarch), - typeof(OphidianWarrior) - } - } - ); - - public static OppositionGroup SavagesAndOrcs { get; } = new( - new[] - { - new[] - { - typeof(Orc), - typeof(OrcBomber), - typeof(OrcBrute), - typeof(OrcCaptain), - typeof(OrcishLord), - typeof(OrcishMage), - typeof(SpawnedOrcishLord) - }, - new[] - { - typeof(Savage), - typeof(SavageRider), - typeof(SavageRidgeback), - typeof(SavageShaman) - } - } - ); - - public static OppositionGroup FeyAndUndead { get; } = new( - new[] - { - new[] - { - typeof(Centaur), - typeof(EtherealWarrior), - typeof(Kirin), - typeof(LordOaks), - typeof(Pixie), - typeof(Silvani), - typeof(Unicorn), - typeof(Wisp), - typeof(Treefellow), - typeof(MLDryad), - typeof(Satyr) - }, - new[] - { - typeof(AncientLich), - typeof(Bogle), - typeof(LichLord), - typeof(Shade), - typeof(Spectre), - typeof(Wraith), - typeof(BoneKnight), - typeof(Ghoul), - typeof(Mummy), - typeof(SkeletalKnight), - typeof(Skeleton), - typeof(Zombie), - typeof(ShadowKnight), - typeof(DarknightCreeper), - typeof(RevenantLion), - typeof(LadyOfTheSnow), - typeof(RottingCorpse), - typeof(SkeletalDragon), - typeof(Lich) - } - } - ); - - public bool IsEnemy(object from, object target) + public int IndexOf(object obj) + { + if (obj == null) { - var fromGroup = IndexOf(from); - var targGroup = IndexOf(target); - - return fromGroup != -1 && targGroup != -1 && fromGroup != targGroup; - } - - public int IndexOf(object obj) - { - if (obj == null) - { - return -1; - } - - var type = obj.GetType(); - - for (var i = 0; i < _types.Length; ++i) - { - var group = _types[i]; - - var contains = false; - - for (var j = 0; !contains && j < group.Length; ++j) - { - contains = group[j].IsAssignableFrom(type); - } - - if (contains) - { - return i; - } - } - return -1; } + + var type = obj.GetType(); + + for (var i = 0; i < _types.Length; ++i) + { + var group = _types[i]; + + var contains = false; + + for (var j = 0; !contains && j < group.Length; ++j) + { + contains = group[j].IsAssignableFrom(type); + } + + if (contains) + { + return i; + } + } + + return -1; } } diff --git a/Projects/UOContent/Mobiles/AI/VendorAI.cs b/Projects/UOContent/Mobiles/AI/VendorAI.cs index 30f63dd29..a835ef61f 100644 --- a/Projects/UOContent/Mobiles/AI/VendorAI.cs +++ b/Projects/UOContent/Mobiles/AI/VendorAI.cs @@ -24,20 +24,17 @@ public class VendorAI : BaseAI Mobile.Say(GetRandomGuardMessage()); Action = ActionType.Flee; } + else if (Mobile.FocusMob != null) + { + this.DebugSayFormatted($"{Mobile.FocusMob.Name} has talked to me"); + + Action = ActionType.Interact; + } else { - if (Mobile.FocusMob != null) - { - this.DebugSayFormatted($"{Mobile.FocusMob.Name} has talked to me"); + Mobile.Warmode = false; - Action = ActionType.Interact; - } - else - { - Mobile.Warmode = false; - - base.DoActionWander(); - } + base.DoActionWander(); } return true; @@ -60,7 +57,7 @@ public class VendorAI : BaseAI if (customer?.Deleted != false || customer.Map != Mobile.Map) { - DebugSay("My customer have disapeared"); + DebugSay("My customer has disappeared"); Mobile.FocusMob = null; diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index ecfbba73f..c8aeeea18 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -56,11 +56,12 @@ namespace Server.Mobiles Attack, // "(All/Name) kill", // "(All/Name) attack" All or the specified pet(s) currently under your control attack the target. - Patrol, // "(Name) patrol" Roves between two or more guarded targets. - 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. + Patrol, // "(Name) patrol" Roves between two or more guarded targets. + 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. + Rename // "(Name) rename" Changes the name of the pet. } [Flags] @@ -132,30 +133,6 @@ namespace Server.Mobiles public int CompareTo(DamageStore ds) => (ds?.m_Damage ?? 0).CompareTo(m_Damage); } - [AttributeUsage(AttributeTargets.Class)] - public class FriendlyNameAttribute : Attribute - { - public FriendlyNameAttribute(TextDefinition friendlyName) => FriendlyName = friendlyName; - // future use: Talisman 'Protection/Bonus vs. Specific Creature - - public TextDefinition FriendlyName { get; } - - public static TextDefinition GetFriendlyNameFor(Type t) - { - if (t.IsDefined(typeof(FriendlyNameAttribute), false)) - { - var objs = t.GetCustomAttributes(typeof(FriendlyNameAttribute), false); - - if (objs.Length > 0) - { - return (objs[0] as FriendlyNameAttribute)?.FriendlyName ?? ""; - } - } - - return t.Name; - } - } - public abstract partial class BaseCreature : Mobile, IHonorTarget, IQuestGiver { public enum Allegiance @@ -364,7 +341,11 @@ namespace Server.Mobiles FightMode = mode; - ResetSpeeds(); + GetSpeeds(out var activeSpeed, out var passiveSpeed); + + ActiveSpeed = activeSpeed; + PassiveSpeed = passiveSpeed; + CurrentSpeed = passiveSpeed; m_Team = 0; @@ -926,8 +907,6 @@ namespace Server.Mobiles public virtual bool ReturnsToHome => SeeksHome && Home != Point3D.Zero && !m_ReturnQueued && !Controlled && !Summoned; - public virtual bool ScaleSpeedByDex => NPCSpeeds.ScaleSpeedByDex && !IsMonster; - // used for deleting untamed creatures [in houses] [CommandProperty(AccessLevel.GameMaster)] public bool RemoveIfUntamed { get; set; } @@ -1517,15 +1496,6 @@ namespace Server.Mobiles } } - public override void OnRawDexChange(int oldValue) - { - // This only really happens for pets or when a GM modifies a mob. - if (oldValue != RawDex && ScaleSpeedByDex) - { - ResetSpeeds(); - } - } - public override void OnBeforeSpawn(Point3D location, Map m) { if (Paragon.CheckConvert(this, location, m)) @@ -3536,7 +3506,6 @@ namespace Server.Mobiles } Guild = null; - ResetSpeeds(); Delta(MobileDelta.Noto); @@ -4926,15 +4895,6 @@ namespace Server.Mobiles NPCSpeeds.GetSpeeds(this, out activeSpeed, out passiveSpeed); } - public void ResetSpeeds(bool currentUseActive = false) - { - GetSpeeds(out var activeSpeed, out var passiveSpeed); - - ActiveSpeed = activeSpeed; - PassiveSpeed = passiveSpeed; - CurrentSpeed = currentUseActive ? activeSpeed : passiveSpeed; - } - public virtual void DropBackpack() { if (Backpack?.Items.Count > 0) diff --git a/Projects/UOContent/Mobiles/NPCSpeeds.cs b/Projects/UOContent/Mobiles/NPCSpeeds.cs index 3a2bc201e..a61ce70c6 100644 --- a/Projects/UOContent/Mobiles/NPCSpeeds.cs +++ b/Projects/UOContent/Mobiles/NPCSpeeds.cs @@ -22,32 +22,12 @@ public static class NPCSpeeds private static readonly Dictionary _speedsByType = new(); private static readonly Dictionary _speedsByLevel = new(); - // Enabled for pets on HS+ - public static bool ScaleSpeedByDex { get; private set; } - public static double MinDelay { get; private set; } - public static double MaxDelay { get; private set; } - public static int MinDex { get; private set; } - public static int MaxDex { get; private set; } - // Time period to lock NPCs into idling public static int MinIdleSeconds { get; private set; } public static int MaxIdleSeconds { get; private set; } public static void GetSpeeds(BaseCreature bc, out double activeSpeed, out double passiveSpeed) { - // Used for scaling pet's speed by dex in HS+ - if (bc.ScaleSpeedByDex) - { - var maxDex = MaxDex; - double min = MinDelay; - double max = MaxDelay; - var dex = Math.Clamp(bc.Dex, MinDex, maxDex); - - activeSpeed = Math.Max(max - (max - min) * ((double)dex / maxDex), min); - passiveSpeed = activeSpeed * 2; - return; - } - if ((bc.SpeedClass == SpeedLevel.None || !_speedsByLevel.TryGetValue(bc.SpeedClass, out var sp)) && !_speedsByType.TryGetValue(bc.GetType(), out sp)) { @@ -70,11 +50,6 @@ public static class NPCSpeeds public static void Configure() { - ScaleSpeedByDex = ServerConfiguration.GetSetting("movement.delay.scaleSpeedByDex", Core.HS); - MinDelay = ServerConfiguration.GetSetting("movement.delay.npcMinDelay", 0.1); - MaxDelay = ServerConfiguration.GetSetting("movement.delay.npcMaxDelay", 0.4); - MaxDex = ServerConfiguration.GetSetting("movement.delay.npcMinDex", 50); - MaxDex = ServerConfiguration.GetSetting("movement.delay.npcMaxDex", 200); MinIdleSeconds = ServerConfiguration.GetSetting("movement.delay.npcMinIdle", 15); MaxIdleSeconds = ServerConfiguration.GetSetting("movement.delay.npcMaxIdle", 25); diff --git a/version.json b/version.json index 3bb3f304b..227ebd3dd 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.14.0" + "version": "0.15.0" }