diff --git a/Projects/UOContent/Mobiles/AI/AnimalAI.cs b/Projects/UOContent/Mobiles/AI/AnimalAI.cs index 5107b57fe..a9d410483 100644 --- a/Projects/UOContent/Mobiles/AI/AnimalAI.cs +++ b/Projects/UOContent/Mobiles/AI/AnimalAI.cs @@ -1,12 +1,3 @@ -// Ideas -// When you run on animals the panic -// When if (distance < 8 && Utility.RandomDouble() * Math.Sqrt( (8 - distance) / 6 ) >= incoming.Skills.AnimalTaming.Value) -// More your close, the more it can panic -/* - * AnimalHunterAI, AnimalHidingAI, AnimalDomesticAI... - * - */ - namespace Server.Mobiles; public class AnimalAI : BaseAI @@ -15,22 +6,15 @@ public class AnimalAI : BaseAI { } + public override double FleeHealthThreshold => 0.1; // 10% is default + public override double FleeChance => 0.1; // 10% is default + public override double BackoffChance => 0.5; // 50% is default + public override bool DoActionWander() { - // New, only flee @ 10% - - var hitPercent = (double)Mobile.Hits / Mobile.HitsMax; - - if (!Mobile.Summoned && !Mobile.Controlled && hitPercent < 0.1 && Mobile.CanFlee) // Less than 10% health - { - DebugSay("I am low on health!"); - - Action = ActionType.Flee; - } - else if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true)) + if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true)) { this.DebugSayFormatted($"I have detected {Mobile.FocusMob.Name}, attacking"); - Mobile.Combatant = Mobile.FocusMob; Action = ActionType.Combat; } @@ -46,8 +30,7 @@ public class AnimalAI : BaseAI { var combatant = Mobile.Combatant; - if (combatant == null || combatant.Deleted || combatant.Map != Mobile.Map || !combatant.Alive || - combatant.IsDeadBondedPet) + if (!IsValidCombatant(combatant)) { DebugSay("My combatant is gone!"); @@ -64,27 +47,12 @@ public class AnimalAI : BaseAI Action = ActionType.Wander; return true; } - - this.DebugSayFormatted($"I should be closer to {combatant.Name}"); } else if (Core.TickCount - Mobile.LastMoveTime > 400) { Mobile.Direction = Mobile.GetDirectionTo(combatant); } - if (!Mobile.Controlled && !Mobile.Summoned && Mobile.CanFlee) - { - var hitPercent = (double)Mobile.Hits / Mobile.HitsMax; - - if (hitPercent <= 0.1) - { - DebugSay("I am low on health!"); - - Action = ActionType.Flee; - return true; - } - } - if (Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, combatant)) { this.DebugSayFormatted($"I used my abilities on {combatant.Name}!"); @@ -93,33 +61,6 @@ public class AnimalAI : BaseAI return true; } - public override bool DoActionBackoff() - { - var hitPercent = (double)Mobile.Hits / Mobile.HitsMax; - - if (!Mobile.Summoned && !Mobile.Controlled && hitPercent < 0.1 && Mobile.CanFlee) // Less than 10% health - { - Action = ActionType.Flee; - } - else if (AcquireFocusMob(Mobile.RangePerception * 2, FightMode.Closest, true, false, true)) - { - if (WalkMobileRange(Mobile.FocusMob, 1, false, Mobile.RangePerception, Mobile.RangePerception * 2)) - { - DebugSay("Well, here I am safe"); - - Action = ActionType.Wander; - } - } - else - { - DebugSay("I have lost my focus, lets relax"); - - Action = ActionType.Wander; - } - - return true; - } - public override bool DoActionFlee() { AcquireFocusMob(Mobile.RangePerception * 2, Mobile.FightMode, true, false, true); diff --git a/Projects/UOContent/Mobiles/AI/ArcherAI.cs b/Projects/UOContent/Mobiles/AI/ArcherAI.cs index 19e0375c4..dd461a03f 100644 --- a/Projects/UOContent/Mobiles/AI/ArcherAI.cs +++ b/Projects/UOContent/Mobiles/AI/ArcherAI.cs @@ -1,4 +1,3 @@ -using System; using Server.Items; namespace Server.Mobiles; @@ -9,6 +8,9 @@ public class ArcherAI : BaseAI { } + public override double FleeHealthThreshold => 0.2; // 20% is default + public override double FleeChance => 0.1; // 10% is default + public override bool DoActionWander() { DebugSay("I have no combatant"); @@ -75,18 +77,6 @@ public class ArcherAI : BaseAI return true; } - // At 20% we should check if we must leave - if (Mobile.Combatant != null && Mobile.Hits < Mobile.HitsMax * 20 / 100 && Mobile.CanFlee) - { - // 10% to flee + the diff of hits - var fleeChance = 10 + Math.Max(0, Mobile.Combatant.Hits - Mobile.Hits); - - if (Utility.Random(0, 100) > fleeChance) - { - Action = ActionType.Flee; - } - } - return true; } diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs b/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs index b76a8a131..d849354d9 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs @@ -181,6 +181,11 @@ public abstract partial class BaseAI private bool CanAttackTarget(Mobile from, Mobile target) { + if (target.Hidden) + { + return false; + } + if (target is BaseCreature creature && creature.IsScaryToPets && Mobile.IsScaredOfScaryThings) { Mobile.SayTo(from, "Your pet refuses to attack this creature!"); @@ -341,6 +346,13 @@ public abstract partial class BaseAI { this.DebugSayFormatted($"I am being herded by {Mobile.ControlTarget?.Name ?? "Unknown"}."); } + else if (ShouldBackoff()) + { + DebugSay("Detected threat, backing off!"); + + Action = ActionType.Backoff; + return true; + } else if (Mobile.CurrentWayPoint != null) { HandleWayPoint(); @@ -348,6 +360,11 @@ public abstract partial class BaseAI else if (Mobile.IsAnimatedDead) { FollowMaster(); + + if (CheckMove() && CanMoveNow(out _) && !Mobile.CheckIdle()) + { + WalkRandomInHome(3, 2, 1); + } } else if (CheckMove() && CanMoveNow(out _) && !Mobile.CheckIdle()) { @@ -389,10 +406,6 @@ public abstract partial class BaseAI { MoveTo(master, false, 1); } - else - { - WalkRandomInHome(3, 2, 1); - } } public virtual bool DoActionCombat() @@ -406,13 +419,13 @@ public abstract partial class BaseAI var combatant = Mobile.Combatant; if (!IsValidCombatant(combatant)) { - DebugSay("My combatant is missing. Returning home..."); + DebugSay("My combatant is missing. Wandering..."); Mobile.FocusMob = null; Mobile.Warmode = false; Mobile.Combatant = null; - WalkRandomInHome(3, 2, 1); + Action = ActionType.Wander; return true; } @@ -424,8 +437,8 @@ public abstract partial class BaseAI return true; } - public bool IsValidCombatant(Mobile combatant) => - IsValidFocusMob(combatant) && Mobile.InLOS(combatant); + public bool IsValidCombatant(Mobile combatant) => IsValidFocusMob(combatant) + && (Mobile.InLOS(combatant) || IsHostile(combatant)); public bool IsValidFocusMob(Mobile focusMob) => focusMob != null @@ -453,11 +466,11 @@ public abstract partial class BaseAI public virtual bool DoActionFlee() { - var from = Mobile.FocusMob; + var from = Mobile.Combatant; - if (!IsValidFocusMob(from)) + if (!IsValidCombatant(from)) { - DebugSay("Focus target is missing."); + DebugSay("Combatant is missing."); WalkRandomInHome(3, 2, 1); return true; @@ -471,8 +484,6 @@ public abstract partial class BaseAI public virtual bool DoActionInteract() => true; - public virtual bool DoActionBackoff() => true; - public virtual bool CheckHerding() { var target = Mobile.TargetLocation; @@ -571,16 +582,86 @@ public abstract partial class BaseAI || Mobile.BardTarget.Map != Mobile.Map || Mobile.GetDistanceToSqrt(Mobile.BardTarget) > Mobile.RangePerception; + public virtual double FleeHealthThreshold => 0.0; + public virtual double FleeChance => 0.0; + public virtual bool CheckFlee() { - if (!Mobile.CheckFlee()) + if (Mobile.CheckFlee()) + { + if (Mobile.Combatant == null) + { + Action = ActionType.Wander; + return false; + } + + Action = ActionType.Flee; + return true; + } + + if (StartFlee()) + { + var duration = TimeSpan.FromSeconds(Utility.Random(10, 30)); + Mobile.BeginFlee(duration); + + Action = ActionType.Flee; + + return true; + } + + return false; + } + + protected virtual bool StartFlee() + { + if (!Mobile.CanFlee) { return false; } - - if (Mobile.Combatant == null) + + var hitPercent = (double)Mobile.Hits / Mobile.HitsMax; + + if (hitPercent > FleeHealthThreshold) { - WalkRandomInHome(3, 2, 1); + return false; + } + + return Utility.RandomDouble() < FleeChance; + } + + public virtual double BackoffChance => 0.0; + + protected virtual bool ShouldBackoff() + { + if (Mobile.Combatant != null || BackoffChance <= 0.0) + { + return false; + } + + if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true)) + { + return Utility.RandomDouble() < BackoffChance; + } + + return false; + } + + public virtual bool DoActionBackoff() + { + if (AcquireFocusMob(Mobile.RangePerception * 2, FightMode.Closest, true, false, true)) + { + if (WalkMobileRange(Mobile.FocusMob, 1, false, Mobile.RangePerception, Mobile.RangePerception * 2)) + { + DebugSay("I backed off to safety. Wandering..."); + + Action = ActionType.Wander; + } + } + else + { + DebugSay("Focus target missing. Wandering..."); + + Action = ActionType.Wander; } return true; @@ -801,7 +882,8 @@ public abstract partial class BaseAI 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 IsHostile(Mobile from) => Mobile.Combatant == from || from.Combatant == Mobile + || IsAggressor(from) || IsAggressed(from); private bool IsAggressor(Mobile from) { diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs b/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs index 1d3d7bd22..61787dbf6 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs @@ -357,13 +357,14 @@ public abstract partial class BaseAI } private bool IsInvalidControlTarget(Mobile target) => target?.Deleted != false || target.Map != Mobile.Map - || !target.Alive || target.IsDeadBondedPet; + || !target.Alive || target.IsDeadBondedPet || target.Hidden; private void HandleInvalidControlTarget() { DebugSay("Target is either dead, hidden, or out of range."); - Mobile.ControlOrder = Core.AOS || Mobile.IsBonded ? OrderType.Follow : OrderType.None; + Mobile.ControlTarget = Mobile.ControlMaster; + Mobile.ControlOrder = OrderType.None; if (Mobile.FightMode is FightMode.Closest or FightMode.Aggressor) { diff --git a/Projects/UOContent/Mobiles/AI/MageAI.cs b/Projects/UOContent/Mobiles/AI/MageAI.cs index 0ca4893e5..e9450db98 100644 --- a/Projects/UOContent/Mobiles/AI/MageAI.cs +++ b/Projects/UOContent/Mobiles/AI/MageAI.cs @@ -20,6 +20,9 @@ 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 + public override double FleeHealthThreshold => 0.2; // 20% is default + public override double FleeChance => 0.1; // 10% is default + private static readonly int[] Offsets = [ -1, -1, @@ -692,21 +695,6 @@ public class MageAI : BaseAI } } - if (!Mobile.Controlled && !Mobile.Summoned && Mobile.CanFlee && Mobile.Hits < Mobile.HitsMax * 20 / 100) - { - // We are low on health, should we flee? - // (10 + diff)% chance to flee - var fleeChance = 10 + Math.Max(0, c.Hits - Mobile.Hits); - - if (Utility.Random(0, 100) > fleeChance) - { - this.DebugSayFormatted($"I am going to flee from {c.Name}"); - - Action = ActionType.Flee; - return true; - } - } - if (Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, c)) { DebugSay("I used my abilities!"); @@ -843,37 +831,6 @@ public class MageAI : BaseAI return true; } - public override bool DoActionFlee() - { - if ((Mobile.Mana > 20 || Mobile.Mana == Mobile.ManaMax) && Mobile.Hits > Mobile.HitsMax / 2) - { - DebugSay("I am stronger now, my guard is up"); - - Action = ActionType.Guard; - } - else if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true)) - { - this.DebugSayFormatted($"I am scared of {Mobile.FocusMob.Name}"); - - RunFrom(Mobile.FocusMob); - Mobile.FocusMob = null; - - if (Mobile.Poisoned && Utility.Random(0, 5) == 0) - { - new CureSpell(Mobile).Cast(); - } - } - else - { - DebugSay("Area seems clear, but my guard is up"); - - Action = ActionType.Guard; - Mobile.Warmode = true; - } - - return true; - } - public Mobile FindDispelTarget(bool activeOnly) { if (Mobile.Deleted || Mobile.Int < 95 || CanDispel(Mobile) || Mobile.AutoDispel) diff --git a/Projects/UOContent/Mobiles/AI/MeleeAI.cs b/Projects/UOContent/Mobiles/AI/MeleeAI.cs index 01ef3c06f..aa262caee 100644 --- a/Projects/UOContent/Mobiles/AI/MeleeAI.cs +++ b/Projects/UOContent/Mobiles/AI/MeleeAI.cs @@ -1,5 +1,3 @@ -using System; - namespace Server.Mobiles; public class MeleeAI : BaseAI @@ -8,21 +6,21 @@ public class MeleeAI : BaseAI { } + public override double FleeHealthThreshold => 0.2; // 20% is default + public override double FleeChance => 0.1; // 10% is default + public override bool DoActionWander() { if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true)) { this.DebugSayFormatted($"I have detected {Mobile.FocusMob.Name}, attacking"); - Mobile.Combatant = Mobile.FocusMob; Action = ActionType.Combat; } else { DebugSay("I am wandering"); - Mobile.Warmode = false; - base.DoActionWander(); } @@ -33,79 +31,32 @@ public class MeleeAI : BaseAI { var combatant = Mobile.Combatant; - if (combatant == null || combatant.Deleted || combatant.Map != Mobile.Map || !combatant.Alive || - combatant.IsDeadBondedPet) + if (!IsValidCombatant(combatant)) { DebugSay("My combatant is gone, so my guard is up"); - Action = ActionType.Guard; return true; } if (!Mobile.InRange(combatant, Mobile.RangePerception)) { - // They are somewhat far away, can we find something else? - - if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true)) + if (!HandleOutOfRangeCombatant(combatant)) { - Mobile.Combatant = Mobile.FocusMob; - Mobile.FocusMob = null; + return true; } - else if (!Mobile.InRange(combatant, Mobile.RangePerception * 3)) - { - Mobile.Combatant = null; - } - combatant = Mobile.Combatant; - - if (combatant == null) - { - DebugSay("My combatant has fled, so I am on guard"); - - Action = ActionType.Guard; - return true; - } } - if (!MoveTo(combatant, false, Mobile.RangeFight)) + if (!AttemptMoveToCombatant(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}"); - - Mobile.Combatant = Mobile.FocusMob; - Action = ActionType.Combat; - return true; - } - - if (Mobile.GetDistanceToSqrt(combatant) > Mobile.RangePerception + 1) - { - this.DebugSayFormatted($"I cannot find {combatant.Name}, so my guard is up"); - - Action = ActionType.Guard; - return true; - } - - this.DebugSayFormatted($"I cannot find {combatant.Name}, so my guard is up"); + return true; } - else if (Core.TickCount - Mobile.LastMoveTime > 200) + + if (Core.TickCount - Mobile.LastMoveTime > 400) { Mobile.Direction = Mobile.GetDirectionTo(combatant); } - // We are low on health, should we flee? - if (!Mobile.Controlled && !Mobile.Summoned && Mobile.CanFlee && 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) - { - this.DebugSayFormatted($"I am going to flee from {combatant.Name}"); - - Action = ActionType.Flee; - return true; - } - } - if (Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, combatant)) { DebugSay("I used my abilities!"); @@ -114,12 +65,69 @@ public class MeleeAI : BaseAI return true; } + private bool IsValidCombatant(Mobile combatant) + { + return combatant?.Deleted == false + && combatant.Map == Mobile.Map + && combatant.Alive + && !combatant.IsDeadBondedPet; + } + + private bool HandleOutOfRangeCombatant(Mobile combatant) + { + if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true)) + { + Mobile.Combatant = Mobile.FocusMob; + Mobile.FocusMob = null; + return true; + } + + if (!Mobile.InRange(combatant, Mobile.RangePerception * 3)) + { + Mobile.Combatant = null; + } + + if (Mobile.Combatant == null) + { + DebugSay("My combatant has fled, so I am on guard."); + Action = ActionType.Guard; + return false; + } + + return true; + } + + private bool AttemptMoveToCombatant(Mobile combatant) + { + if (MoveTo(combatant, false, Mobile.RangeFight)) + { + return true; + } + + if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true)) + { + this.DebugSayFormatted($"My move is blocked, so I am going to attack {Mobile.FocusMob.Name}."); + Mobile.Combatant = Mobile.FocusMob; + Action = ActionType.Combat; + return true; + } + + if (Mobile.GetDistanceToSqrt(combatant) > Mobile.RangePerception + 1) + { + this.DebugSayFormatted($"I cannot find {combatant.Name}, so my guard is up."); + Action = ActionType.Guard; + return false; + } + + this.DebugSayFormatted($"I cannot reach {combatant.Name} but continuing to try."); + return true; + } + public override bool DoActionGuard() { if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true)) { - this.DebugSayFormatted($"I have detected {Mobile.FocusMob.Name}, attacking"); - + this.DebugSayFormatted($"I have detected {Mobile.FocusMob.Name}, attacking."); Mobile.Combatant = Mobile.FocusMob; Action = ActionType.Combat; } @@ -133,10 +141,9 @@ public class MeleeAI : BaseAI public override bool DoActionFlee() { - if (Mobile.Hits > Mobile.HitsMax / 2) + if (Mobile.Hits > Mobile.HitsMax * FleeHealthThreshold) { - DebugSay("I am stronger now, so I will continue fighting"); - + DebugSay("I am stronger now, so I will continue fighting."); Action = ActionType.Combat; } else