ModernUO/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs
Kamron Batman 36e09bae13
fix: Pets stop permanently, simplifies pet movement speed (#2401)
## Summary
- Fixes pets falling behind mounted masters in AOS+ by setting `CurrentSpeed = 0.1` when following master
- Fixes AI timer permanently stopping when `Obey()`/`Think()` returns `false` for transient conditions
- Fixes controlled pets losing AI in inactive sectors (pet follows owner across sector boundary, sector deactivates, AI dies)
- Adds defense-in-depth: AI timer restarts on pet resurrection and order changes

## AI Timer Permanent Stop (Bug Fix)

`AITimer.OnTick()` called `Stop()` when `Obey()` or `Think()` returned `false`. By that point, `ShouldStop()` had already validated the creature is alive, on a valid map, and in an active sector — so any `false` return was a **transient** condition, not terminal. The timer stopped permanently with no mechanism to restart it.

**Scenarios that triggered permanent AI death:**
- Dead bonded pet with attack order (`DoOrderAttack` returned `false` for `IsDeadPet`)
- Failed pet transfer — loyalty refusal, combat, disconnected player, or pending trade (`DoOrderTransfer` returned `false` for 5 different transient conditions)
- Unknown `OrderType` or `ActionType` (defensive defaults)

**Fixes:**
- Removed `Stop()` from the `Obey()`/`Think()` failure path — timer skips the tick and fires again next interval
- Changed `DoOrderAttack()` and all five `DoOrderTransfer()` failure paths to return `true` (correct semantics: these are recoverable states, not "stop AI forever" signals)
- Added `Activate()` call in `ResurrectPet()` — ensures dead bonded pets have AI running after resurrection
- Added `Activate()` call in `OnCurrentOrderChanged()` — self-heals timer if any voice command is issued to a pet with a stopped timer

## Controlled Pet Sector Deactivation (Bug Fix)

`ShouldStop()` stopped the AI timer for **all** `PlayerRangeSensitive` creatures in inactive sectors, including controlled pets. But `Deactivate()` intentionally exempted controlled pets. The exemption was dead code — `ShouldStop()` bypassed it.

This matters when a pet follows its owner across a sector boundary: the owner enters the next sector (active), the pet's old sector deactivates (no more players), and the pet's AI dies. The pet stops following and stands there until the player backtracks far enough to reactivate the sector.

**Fix:** Added `Controlled` check to `ShouldStop()` to match `Deactivate()`. Controlled pets now keep their AI running in inactive sectors. The overhead is negligible — controlled pets are bounded by follower slots.

## Movement Speed Simplification

- Simplifies `AITimer` to use `CurrentSpeed` directly as the tick interval (in seconds), removing the complex multiplier/floor logic in `GetBaseInterval`
- Refactors `DoMoveImpl` speed assignment into explicit if/else for clarity
- AOS+ pets following master use `CurrentSpeed = 0.1` (100ms), matching `RunMountDelay`

## Files Changed
- `AITimer.cs` — removed `Stop()` on Obey/Think failure, added `Controlled` exemption to `ShouldStop()`, simplified interval logic
- `BaseAI.cs` — renamed `_timer` to `AITimer` (public), simplified `Deactivate()`, fixed `ReturnToHome` to use `Activate()`
- `PetOrders.cs` — `DoOrderAttack` and `DoOrderTransfer` return `true` for transient failures
- `PetOrderHandlers.cs` — `OnCurrentOrderChanged()` calls `Activate()` to self-heal stopped timers
- `BaseCreature.cs` — `ResurrectPet()` calls `Activate()`, fixed `GoHome_Callback` PlayerRangeSensitive check
- `AIMovement.cs` — refactored speed assignment, AOS+ follow-master speed fix
2026-04-25 11:10:30 -07:00

1004 lines
27 KiB
C#

/*************************************************************************
* ModernUO *
* Copyright 2019-2026 - 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 <http://www.gnu.org/licenses/>. *
************************************************************************/
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 DateTime _lastOrder = DateTime.MinValue;
public Mobile _commandIssuer;
public PathFollower Path { get; protected set; }
public AITimer AITimer { get; }
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;
AITimer = new AITimer(this);
if (!m.PlayerRangeSensitive || !World.Loading && m.Map != null && m.Map != Map.Internal && m.Map.GetSector(m.Location).Active)
{
AITimer.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.Hidden)
{
return false;
}
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 (ShouldBackoff())
{
DebugSay("Detected threat, backing off!");
Action = ActionType.Backoff;
return true;
}
else if (Mobile.CurrentWayPoint != null)
{
HandleWayPoint();
}
else if (Mobile.IsAnimatedDead)
{
FollowMaster();
if (CheckMove() && CanMoveNow(out _) && !Mobile.CheckIdle())
{
WalkRandomInHome(3, 2, 1);
}
}
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);
}
}
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. Wandering...");
Mobile.FocusMob = null;
Mobile.Warmode = false;
Mobile.Combatant = null;
Action = ActionType.Wander;
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) || IsHostile(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()
{
if (Mobile.Combatant == null)
{
DebugSay("No threats found. Going home...");
Action = ActionType.Wander;
}
DebugSay("I stopped being on guard.");
Action = ActionType.Wander;
return true;
}
public virtual bool DoActionFlee()
{
var from = Mobile.Combatant;
if (!IsValidCombatant(from))
{
DebugSay("Combatant 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 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<FetchAbraxusScrollObjective>();
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 double FleeHealthThreshold => 0.0;
public virtual double FleeChance => 0.0;
public virtual bool 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;
}
var hitPercent = (double)Mobile.Hits / Mobile.HitsMax;
if (hitPercent > FleeHealthThreshold)
{
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;
}
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 (Mobile.BardPacified)
{
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.Map == Map.Internal || !Mobile.Controlled && !Mobile.Map.GetSector(Mobile.Location).Active)
{
AITimer.Stop();
}
if (ShouldReturnToHome(Mobile.Spawner))
{
Timer.StartTimer(ReturnToHome);
}
}
private bool ShouldReturnToHome(ISpawner spawner) =>
spawner?.ReturnOnDeactivate == true && !Mobile.Controlled &&
!spawner.IsInSpawnBounds(Mobile.Location);
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);
}
Activate();
}
public virtual void Activate()
{
if (!AITimer.Running)
{
AITimer.Activate();
}
}
public virtual void OnCurrentSpeedChanged()
{
AITimer.Interval = TimeSpan.FromSeconds(Mobile.CurrentSpeed);
}
}