ModernUO/Projects/UOContent/Mobiles/AI/MageAI.cs
Kamron Batman 7da4171bbe
fix: creatures track and chase targets reliably around corners
Fixes the long-standing "monster loses track of a player who runs
around a corner" reports. Root causes and fixes:

1. Movement contract: MoveTo/ApproachTarget returned false on every
   healthy mid-chase tick, so MeleeAI's inherited "move failed and
   >RangePerception+1 -> Guard" clause (RunUO only ran it on real
   blockage) fired every tick of every chase. They now report failure
   only on genuine movement failure (no step with no working path, or
   approach give-up). ArcherAI's equivalent clause moves to the hard
   leash.

2. Last-known-position pursuit: while a combatant is in LOS its
   position is recorded each think tick. When the target vanishes
   (corner, hiding, recall) the creature investigates the last-seen
   spot, stands guard there ~10s (restoring RunUO's guard grace that
   had decayed to a single tick), and re-engages instantly if the same
   target re-enters view - bypassing the 10s reacquire throttle.

3. ChaseLeashRange (virtual, default RangePerception * 2 = 32 tiles)
   replaces the inline RangePerception * 3; per-creature tunable.

4. Group movement is now a crowding refinement, not a movement mode:
   it only engages near the target when allies contest the ring, and
   blocked/wall-slid steps escalate to the pathfinder instead of
   reporting success - previously any creature with one ally on the
   same target greedy-stepped for the whole chase and never pathfound.

5. Mages close distance when geometry (not hiding) blocks line of
   sight instead of standing at a wall holding a spell until the 60s
   combatant expiry.

6. Move budget: one actual step per AI think tick (half-step NextMove
   charge). Code paths that attempted several moves in one tick could
   cross multiple tiles at once - visible as "warping" when crowded
   creatures jockey for position. Blocked attempts consume nothing, so
   retry ladders are unaffected. CanMoveNow is wraparound-safe now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 01:07:39 -07:00

1138 lines
32 KiB
C#

using System;
using Server.Items;
using Server.Spells;
using Server.Spells.Fifth;
using Server.Spells.First;
using Server.Spells.Fourth;
using Server.Spells.Necromancy;
using Server.Spells.Second;
using Server.Spells.Seventh;
using Server.Spells.Sixth;
using Server.Spells.Third;
using Server.Targeting;
namespace Server.Mobiles;
public class MageAI : BaseAI
{
private const double HealChance = 0.10; // 10% chance to heal at gm magery
private const double TeleportChance = 0.05; // 5% chance to teleport at gm magery
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,
-1, 0,
-1, 1,
0, -1,
0, 1,
1, -1,
1, 0,
1, 1,
-2, -2,
-2, -1,
-2, 0,
-2, 1,
-2, 2,
-1, -2,
-1, 2,
0, -2,
0, 2,
1, -2,
1, 2,
2, -2,
2, -1,
2, 0,
2, 1,
2, 2
];
protected int _combo = -1;
private Mobile _lastTarget;
private Point3D _lastTargetLoc;
private long _nextCastTime;
private long _nextHealTime;
private LandTarget _revealTarget;
public MageAI(BaseCreature m) : base(m)
{
}
public virtual bool SmartAI => Mobile is BaseVendor or BaseEscortable or Changeling;
public virtual bool IsNecromancer => Core.AOS && Mobile.Skills.Necromancy.Value > 50;
public override bool Think()
{
if (Mobile.Deleted)
{
return false;
}
return ProcessTarget() || base.Think();
}
public virtual double ScaleBySkill(double v, SkillName skill) => v * Mobile.Skills[skill].Value / 100;
public override bool DoActionWander()
{
if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true))
{
this.DebugSayFormatted($"I am going to attack {Mobile.FocusMob.Name}");
Mobile.Combatant = Mobile.FocusMob;
Action = ActionType.Combat;
_nextCastTime = Core.TickCount;
}
else if (SmartAI && Mobile.Mana < Mobile.ManaMax && !Mobile.Meditating)
{
DebugSay("I am going to meditate");
Mobile.UseSkill(SkillName.Meditation);
}
else
{
DebugSay("I am wandering");
Mobile.Warmode = false;
base.DoActionWander();
if (Utility.RandomDouble() < 0.05)
{
var spell = CheckCastHealingSpell();
spell?.Cast();
}
}
return true;
}
private Spell CheckCastHealingSpell()
{
// If I'm poisoned, always attempt to cure.
if (Mobile.Poisoned)
{
return new CureSpell(Mobile);
}
// Summoned creatures never heal themselves.
if (Mobile.Summoned || Mobile.Controlled && Core.TickCount - _nextHealTime < 0)
{
return null;
}
if (!SmartAI)
{
if (HealChance > 0 && ScaleBySkill(HealChance, SkillName.Magery) < Utility.RandomDouble())
{
return null;
}
}
else if (Utility.Random(0, 4 + (Mobile.Hits == 0 ? Mobile.HitsMax : Mobile.HitsMax / Mobile.Hits)) < 3)
{
return null;
}
Spell spell = null;
if (Mobile.Hits < Mobile.HitsMax - 50)
{
if (UseNecromancy())
{
Mobile.UseSkill(SkillName.SpiritSpeak);
}
else
{
spell = new GreaterHealSpell(Mobile);
}
}
else if (Mobile.Hits < Mobile.HitsMax - 10)
{
spell = new HealSpell(Mobile);
}
var delay = Mobile.Int >= 500 ? Utility.RandomMinMax(7, 10) : Math.Sqrt(600 - Mobile.Int);
_nextHealTime = Core.TickCount + (int)TimeSpan.FromSeconds(delay).TotalMilliseconds;
return spell;
}
public void RunTo(Mobile m)
{
if (!SmartAI)
{
if (!MoveTo(m, false, Mobile.RangeFight))
{
OnFailedMove();
}
return;
}
if (m.Paralyzed || m.Frozen)
{
if (Mobile.InRange(m, 1))
{
RunFrom(m);
}
else if (!Mobile.InRange(m, Math.Max(Mobile.RangeFight, 2)) && !MoveTo(m, false, 1))
{
OnFailedMove();
}
}
else if (!Mobile.InRange(m, Mobile.RangeFight))
{
if (!MoveTo(m, false, 1))
{
OnFailedMove();
}
}
else if (Mobile.InRange(m, Mobile.RangeFight - 1))
{
RunFrom(m);
}
}
public void RunFrom(Mobile m)
{
Run((Mobile.GetDirectionTo(m) - 4) & Direction.Mask);
}
public void OnFailedMove()
{
if (!Mobile.DisallowAllMoves && (SmartAI
? Utility.Random(4) == 0
: TeleportChance > 0 && ScaleBySkill(TeleportChance, SkillName.Magery) > Utility.RandomDouble()))
{
Mobile.Target?.Cancel(Mobile, TargetCancelType.Canceled);
new TeleportSpell(Mobile).Cast();
DebugSay("I am stuck, I'm going to try teleporting away");
}
else 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;
}
else
{
DebugSay("I am stuck");
}
}
public void Run(Direction d)
{
if (Mobile.Spell?.IsCasting == true || Mobile.Paralyzed || Mobile.Frozen ||
Mobile.DisallowAllMoves)
{
return;
}
Mobile.Direction = d | Direction.Running;
if (!DoMove(Mobile.Direction, true))
{
OnFailedMove();
}
}
public virtual bool UseNecromancy()
{
var magery = Mobile.Skills.Magery.BaseFixedPoint;
var necro = Mobile.Skills.Necromancy.BaseFixedPoint;
return IsNecromancer && Utility.Random(magery + necro) >= magery;
}
public virtual Spell GetRandomDamageSpell() =>
UseNecromancy() ? GetRandomDamageSpellNecro() : GetRandomDamageSpellMage();
public virtual Spell GetRandomDamageSpellNecro()
{
var bound = Mobile.Skills.Necromancy.Value >= 100 ? 5 : 3;
return Utility.Random(bound) switch
{
0 => new PainSpikeSpell(Mobile),
1 => new PoisonStrikeSpell(Mobile),
2 => new StrangleSpell(Mobile),
3 => new WitherSpell(Mobile),
_ => new VengefulSpiritSpell(Mobile)
};
}
public virtual Spell GetRandomDamageSpellMage()
{
var maxCircle = Math.Clamp((int)((Mobile.Skills.Magery.Value + 20.0) / (100.0 / 7.0)), 1, 8);
return Utility.Random(maxCircle * 2) switch
{
0 => new MagicArrowSpell(Mobile),
1 => new MagicArrowSpell(Mobile),
2 => new HarmSpell(Mobile),
3 => new HarmSpell(Mobile),
4 => new FireballSpell(Mobile),
5 => new FireballSpell(Mobile),
6 => new LightningSpell(Mobile),
7 => new LightningSpell(Mobile),
8 => new MindBlastSpell(Mobile),
9 => new MindBlastSpell(Mobile),
10 => new EnergyBoltSpell(Mobile),
11 => new ExplosionSpell(Mobile),
_ => new FlameStrikeSpell(Mobile)
};
}
public virtual Spell GetRandomCurseSpell() =>
UseNecromancy() ? GetRandomCurseSpellNecro() : GetRandomCurseSpellMage();
public virtual Spell GetRandomCurseSpellNecro()
{
return Utility.Random(4) switch
{
0 => new BloodOathSpell(Mobile),
1 => new CorpseSkinSpell(Mobile),
2 => new EvilOmenSpell(Mobile),
_ => new MindRotSpell(Mobile)
};
}
public virtual Spell GetRandomCurseSpellMage()
{
if (Mobile.Skills.Magery.Value >= 40.0 && Utility.Random(4) == 0)
{
return new CurseSpell(Mobile);
}
return Utility.Random(3) switch
{
0 => new WeakenSpell(Mobile),
1 => new ClumsySpell(Mobile),
_ => new FeeblemindSpell(Mobile)
};
}
public virtual Spell GetRandomManaDrainSpell()
{
if (Mobile.Skills.Magery.Value >= 80.0 && Utility.RandomBool())
{
return new ManaVampireSpell(Mobile);
}
return new ManaDrainSpell(Mobile);
}
public virtual Spell DoDispel(Mobile toDispel)
{
if (!SmartAI)
{
if (DispelChance > 0 && ScaleBySkill(DispelChance, SkillName.Magery) > Utility.RandomDouble())
{
return new DispelSpell(Mobile);
}
return null;
}
var spell = CheckCastHealingSpell();
if (spell != null)
{
return spell;
}
var distance = (int)Mobile.GetDistanceToSqrt(toDispel);
if (!Mobile.DisallowAllMoves && distance > 0 && Utility.Random(distance) == 0)
{
return new TeleportSpell(Mobile);
}
if (Utility.Random(3) == 0 && !Mobile.InRange(toDispel, 3) && !toDispel.Paralyzed && !toDispel.Frozen)
{
return new ParalyzeSpell(Mobile);
}
return new DispelSpell(Mobile);
}
public virtual Spell ChooseSpell(Mobile c)
{
Spell spell;
if (!SmartAI)
{
spell = CheckCastHealingSpell();
if (spell != null)
{
return spell;
}
if (IsNecromancer)
{
var psDamage =
(Mobile.Skills.SpiritSpeak.Value - c.Skills.MagicResist.Value) / 10 +
(c.Player ? 18 : 30);
if (psDamage > c.Hits)
{
return new PainSpikeSpell(Mobile);
}
}
switch (Utility.Random(16))
{
case 0:
case 1: // Poison them
{
if (c.Poisoned)
{
goto default;
}
DebugSay("Attempting to poison");
spell = new PoisonSpell(Mobile);
break;
}
case 2: // Bless ourselves
{
DebugSay("Blessing myself");
spell = new BlessSpell(Mobile);
break;
}
case 3:
case 4: // Curse them
{
DebugSay("Attempting to curse");
spell = GetRandomCurseSpell();
break;
}
case 5: // Paralyze them
{
if (c.Paralyzed || Mobile.Skills.Magery.Value <= 50.0)
{
goto default;
}
DebugSay("Attempting to paralyze");
spell = new ParalyzeSpell(Mobile);
break;
}
case 6: // Drain mana
{
DebugSay("Attempting to drain mana");
spell = GetRandomManaDrainSpell();
break;
}
case 7: // Invis ourselves
{
if (InvisChance <= 0 || ScaleBySkill(InvisChance, SkillName.Magery) <= Utility.RandomDouble())
{
goto default;
}
DebugSay("Attempting to invis myself");
spell = new InvisibilitySpell(Mobile);
break;
}
default: // Damage them
{
DebugSay("Just doing damage");
spell = GetRandomDamageSpell();
break;
}
}
}
else
{
spell = CheckCastHealingSpell();
if (spell == null)
{
switch (Utility.Random(3))
{
case 0: // Poison them
{
if (c.Poisoned)
{
goto case 1;
}
spell = new PoisonSpell(Mobile);
break;
}
case 1: // Deal some damage
{
spell = GetRandomDamageSpell();
break;
}
default: // Set up a combo
{
if (Mobile.Mana is > 15 and < 40)
{
if (c.Paralyzed && !c.Poisoned && !Mobile.Meditating)
{
DebugSay("I am going to meditate");
Mobile.UseSkill(SkillName.Meditation);
}
else if (!c.Poisoned)
{
spell = new ParalyzeSpell(Mobile);
}
}
else if (Mobile.Mana > 60)
{
if (Utility.RandomBool() && !c.Paralyzed && !c.Frozen && !c.Poisoned)
{
_combo = 0;
spell = new ParalyzeSpell(Mobile);
}
else
{
_combo = 1;
spell = new ExplosionSpell(Mobile);
}
}
break;
}
}
}
}
if (spell != null)
{
this.DebugSayFormatted($"Casting {spell.Name}");
}
else
{
DebugSay("I don't have a spell to use!");
}
return spell;
}
public virtual Spell DoCombo(Mobile c)
{
Spell spell = null;
if (_combo == 0)
{
spell = new ExplosionSpell(Mobile);
++_combo; // Move to next spell
}
else if (_combo == 1)
{
spell = new WeakenSpell(Mobile);
++_combo; // Move to next spell
}
else if (_combo == 2)
{
if (!c.Poisoned)
{
spell = new PoisonSpell(Mobile);
}
else if (IsNecromancer)
{
spell = new StrangleSpell(Mobile);
}
++_combo; // Move to next spell
}
if (_combo == 3 && spell == null)
{
switch (Utility.Random(IsNecromancer ? 4 : 3))
{
case 0:
{
if (c.Int < c.Dex)
{
spell = new FeeblemindSpell(Mobile);
}
else
{
spell = new ClumsySpell(Mobile);
}
++_combo; // Move to next spell
break;
}
case 1:
{
spell = new EnergyBoltSpell(Mobile);
_combo = -1; // Reset combo state
break;
}
case 2:
{
spell = new FlameStrikeSpell(Mobile);
_combo = -1; // Reset combo state
break;
}
default:
{
spell = new PainSpikeSpell(Mobile);
_combo = -1; // Reset combo state
break;
}
}
}
else if (_combo == 4 && spell == null)
{
spell = new MindBlastSpell(Mobile);
_combo = -1;
}
return spell;
}
private TimeSpan GetDelay(Spell spell)
{
if (SmartAI || spell is DispelSpell)
{
return TimeSpan.FromSeconds(Mobile.ActiveSpeed);
}
var del = ScaleBySkill(3.0, SkillName.Magery);
var min = 6.0 - del * 0.75;
var max = 6.0 - del * 1.25;
return TimeSpan.FromSeconds(min + (max - min) * Utility.RandomDouble());
}
public override bool DoActionCombat()
{
var c = Mobile.Combatant;
Mobile.Warmode = true;
if (c?.Deleted != false || !c.Alive || c.IsDeadBondedPet || !Mobile.CanSee(c) ||
!Mobile.CanBeHarmful(c, false) || c.Map != Mobile.Map)
{
// Our combatant is deleted, dead, hidden, or we cannot hurt them
// Try to find another combatant
if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true))
{
Mobile.Combatant = c = Mobile.FocusMob!;
this.DebugSayFormatted($"Something happened to my combatant, so I am going to fight {c.Name}");
Mobile.FocusMob = null;
}
else
{
DebugSay("Something happened to my combatant, and nothing is around. I am on guard.");
Action = ActionType.Guard;
return true;
}
}
if (!Mobile.InLOS(c))
{
DebugSay("I can't see my target");
if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true))
{
Mobile.Combatant = c = Mobile.FocusMob!;
this.DebugSayFormatted($"I will switch to {c.Name}");
Mobile.FocusMob = null;
}
}
if (!Core.AOS && SmartAI && !Mobile.StunReady && Mobile.Skills.Wrestling.Value >= 80.0 &&
Mobile.Skills.Anatomy.Value >= 80.0)
{
Fists.StunRequest(Mobile);
}
if (!Mobile.InRange(c, Mobile.RangePerception))
{
// They are somewhat far away, can we find something else?
if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true))
{
Mobile.Combatant = Mobile.FocusMob;
Mobile.FocusMob = null;
}
else if (!Mobile.InRange(c, Mobile.ChaseLeashRange))
{
Mobile.Combatant = null;
}
c = Mobile.Combatant;
if (c == null)
{
DebugSay("My combatant has fled, so I am on guard");
Action = ActionType.Guard;
return true;
}
}
// Geometry (not hiding — CanSee passed above) is blocking the shot: close in until
// line of sight returns. Poisoned mages still fall through to cure.
if (!Mobile.Poisoned && Mobile.Spell?.IsCasting != true && !Mobile.InLOS(c))
{
DebugSay("I cannot see my target, moving to regain line of sight");
if (!MoveTo(c, false, 1))
{
OnFailedMove();
}
_lastTarget = c;
_lastTargetLoc = c.Location;
return true;
}
if (Mobile.TriggerAbility(MonsterAbilityTrigger.CombatAction, c))
{
DebugSay("I used my abilities!");
}
else if (Mobile.Spell == null && Core.TickCount - _nextCastTime >= 0)
{
if (Mobile.Controlled && c == Mobile)
{
DebugSay("I should not attack myself!");
Mobile.Combatant = null;
Action = ActionType.Guard;
return true;
}
// We are ready to cast a spell
Spell spell;
var toDispel = FindDispelTarget(true);
if (Mobile.Poisoned) // Top cast priority is cure
{
DebugSay("I am going to cure myself");
spell = new CureSpell(Mobile);
}
else if (toDispel != null) // Something dispellable is attacking us
{
this.DebugSayFormatted($"I am going to dispel {toDispel}");
spell = DoDispel(toDispel); // May return null if dumb AI and doesn't have enough skill
}
else
{
spell = null;
}
spell ??= SmartAI switch
{
// We are doing a spell combo
true when _combo != -1 => DoCombo(c),
// They have a heal spell out
true when !c.Poisoned && c.Spell is HealSpell or GreaterHealSpell => new PoisonSpell(Mobile),
_ => ChooseSpell(c)
};
// Now we have a spell picked
var range = (spell as IRangedSpell)?.TargetRange ?? Mobile.RangePerception;
// Move first before casting
if (!SmartAI || toDispel == null)
{
RunTo(c);
}
else if (Mobile.InRange(toDispel, Math.Min(10, range)))
{
RunFrom(toDispel);
}
else if (!Mobile.InRange(toDispel, range))
{
RunTo(toDispel);
}
// After running, make sure we are still in range
if (spell == null || Mobile.InRange(c, range))
{
spell?.Cast();
// Even if we don't have a spell, delay the next potential cast
_nextCastTime = Core.TickCount + (int)GetDelay(spell).TotalMilliseconds;
}
}
else if (Mobile.Spell?.IsCasting != true)
{
RunTo(c);
}
if (Mobile.InRange(c, 1) || Mobile.Spell?.IsCasting == true || Core.TickCount - Mobile.LastMoveTime > 400)
{
Mobile.Direction = Mobile.GetDirectionTo(c);
}
_lastTarget = c;
_lastTargetLoc = c.Location;
return true;
}
public override bool DoActionGuard()
{
if (_lastTarget?.Hidden == true)
{
var map = Mobile.Map;
if (map == null || !Mobile.InRange(_lastTargetLoc, Mobile.RangePerception))
{
_lastTarget = null;
}
else if (Mobile.Spell == null && Core.TickCount - _nextCastTime >= 0)
{
DebugSay("I am going to reveal my last target");
_revealTarget = new LandTarget(_lastTargetLoc, map);
Spell spell = new RevealSpell(Mobile);
if (spell.Cast())
{
_lastTarget = null; // only do it once
}
_nextCastTime = Core.TickCount + (int)GetDelay(spell).TotalMilliseconds;
}
}
if (AcquireFocusMob(Mobile.RangePerception, Mobile.FightMode, false, false, true))
{
this.DebugSayFormatted($"I am going to attack {Mobile.FocusMob.Name}");
Mobile.Combatant = Mobile.FocusMob;
Action = ActionType.Combat;
}
else
{
if (!Mobile.Controlled)
{
ProcessTarget();
var spell = CheckCastHealingSpell();
spell?.Cast();
}
base.DoActionGuard();
}
return true;
}
public Mobile FindDispelTarget(bool activeOnly)
{
if (Mobile.Deleted || Mobile.Int < 95 || CanDispel(Mobile) || Mobile.AutoDispel)
{
return null;
}
if (activeOnly)
{
var aggressed = Mobile.Aggressed;
var aggressors = Mobile.Aggressors;
Mobile active = null;
var activePrio = 0.0;
var comb = Mobile.Combatant;
if (comb?.Deleted == false && comb.Alive && !comb.IsDeadBondedPet &&
Mobile.InRange(comb, Mobile.RangePerception) && CanDispel(comb))
{
active = comb;
activePrio = Mobile.GetDistanceToSqrt(comb);
if (activePrio <= 2)
{
return active;
}
}
for (var i = 0; i < aggressed.Count; ++i)
{
var info = aggressed[i];
var m = info.Defender;
if (m != comb && m.Combatant == Mobile && Mobile.InRange(m, Mobile.RangePerception) && CanDispel(m))
{
var prio = Mobile.GetDistanceToSqrt(m);
if (active == null || prio < activePrio)
{
active = m;
activePrio = prio;
if (activePrio <= 2)
{
return active;
}
}
}
}
for (var i = 0; i < aggressors.Count; ++i)
{
var info = aggressors[i];
var m = info.Attacker;
if (m != comb && m.Combatant == Mobile && Mobile.InRange(m, Mobile.RangePerception) && CanDispel(m))
{
var prio = Mobile.GetDistanceToSqrt(m);
if (active == null || prio < activePrio)
{
active = m;
activePrio = prio;
if (activePrio <= 2)
{
return active;
}
}
}
}
return active;
}
var map = Mobile.Map;
if (map != null)
{
Mobile active = null, inactive = null;
double actPrio = 0.0, inactPrio = 0.0;
var comb = Mobile.Combatant;
if (comb?.Deleted == false && comb.Alive && !comb.IsDeadBondedPet && CanDispel(comb))
{
active = inactive = comb;
actPrio = inactPrio = Mobile.GetDistanceToSqrt(comb);
}
foreach (var m in Mobile.GetMobilesInRange(Mobile.RangePerception))
{
if (m != Mobile && CanDispel(m))
{
var prio = Mobile.GetDistanceToSqrt(m);
if (inactive == null || prio < inactPrio)
{
inactive = m;
inactPrio = prio;
}
if ((Mobile.Combatant == m || m.Combatant == Mobile) && (active == null || prio < actPrio))
{
active = m;
actPrio = prio;
}
}
}
return active ?? inactive;
}
return null;
}
public bool CanDispel(Mobile m) =>
m is BaseCreature creature && creature.Summoned && creature.SummonMaster != Mobile &&
Mobile.CanBeHarmful(creature, false) && !creature.IsAnimatedDead;
private bool ProcessTarget()
{
var targ = Mobile.Target;
if (targ == null)
{
return false;
}
var spellTarg = targ as ISpellTarget<Mobile>;
var isReveal = spellTarg?.Spell is RevealSpell;
var isDispel = spellTarg?.Spell is DispelSpell;
var isParalyze = spellTarg?.Spell is ParalyzeSpell;
var isTeleport = spellTarg?.Spell is TeleportSpell;
var isInvisible = spellTarg?.Spell is InvisibilitySpell;
var teleportAway = false;
Mobile toTarget;
if (isInvisible)
{
toTarget = Mobile;
}
else if (isDispel)
{
toTarget = FindDispelTarget(false);
if (!SmartAI && toTarget != null)
{
RunTo(toTarget);
}
else if (toTarget != null && Mobile.InRange(toTarget, 10))
{
RunFrom(toTarget);
}
}
else if (SmartAI && (isParalyze || isTeleport))
{
toTarget = FindDispelTarget(true);
if (toTarget == null)
{
toTarget = Mobile.Combatant;
if (toTarget != null)
{
RunTo(toTarget);
}
}
else if (Mobile.InRange(toTarget, 10))
{
RunFrom(toTarget);
teleportAway = true;
}
else
{
teleportAway = true;
}
}
else
{
toTarget = Mobile.Combatant;
if (toTarget != null)
{
// Without line of sight the stand-off is pointless — close in so the held
// target can be invoked.
if (!Mobile.InLOS(toTarget))
{
MoveTo(toTarget, true, 1);
}
else
{
RunTo(toTarget);
}
}
}
if ((targ.Flags & TargetFlags.Harmful) != 0 && toTarget != null)
{
if ((targ.Range == -1 || Mobile.InRange(toTarget, targ.Range)) && Mobile.CanSee(toTarget) &&
Mobile.InLOS(toTarget))
{
targ.Invoke(Mobile, toTarget);
}
else if (isDispel)
{
targ.Cancel(Mobile, TargetCancelType.Canceled);
}
}
else if ((targ.Flags & TargetFlags.Beneficial) != 0)
{
targ.Invoke(Mobile, Mobile);
}
else if (isReveal && _revealTarget != null)
{
targ.Invoke(Mobile, _revealTarget);
}
else
{
var map = Mobile.Map;
if (map != null && isTeleport && toTarget != null)
{
var teleRange = targ.Range >= 0 ? targ.Range :
Core.ML ? 11 : 12;
int px, py;
if (teleportAway)
{
var rx = Mobile.X - toTarget.X;
var ry = Mobile.Y - toTarget.Y;
var d = Mobile.GetDistanceToSqrt(toTarget);
px = toTarget.X + (int)(rx * (10 / d));
py = toTarget.Y + (int)(ry * (10 / d));
}
else
{
px = toTarget.X;
py = toTarget.Y;
}
for (var i = 0; i < Offsets.Length; i += 2)
{
int x = Offsets[i], y = Offsets[i + 1];
var p = new Point3D(px + x, py + y, 0);
var lt = new LandTarget(p, map);
if ((targ.Range == -1 || Mobile.InRange(p, targ.Range)) && Mobile.InLOS(lt) &&
map.CanSpawnMobile(px + x, py + y, lt.Z) && !SpellHelper.CheckMulti(p, map))
{
targ.Invoke(Mobile, lt);
return true;
}
}
for (var i = 0; i < 10; ++i)
{
var randomPoint = new Point3D(
Mobile.X - teleRange + Utility.Random(teleRange * 2 + 1),
Mobile.Y - teleRange + Utility.Random(teleRange * 2 + 1),
0
);
var lt = new LandTarget(randomPoint, map);
if (Mobile.InLOS(lt) && map.CanSpawnMobile(lt.X, lt.Y, lt.Z) &&
!SpellHelper.CheckMulti(randomPoint, map))
{
targ.Invoke(Mobile, new LandTarget(randomPoint, map));
return true;
}
}
}
targ.Cancel(Mobile, TargetCancelType.Canceled);
}
return true;
}
}