feat: Overhauls AI (Speech/Movement) (#2232)

### Summary

* Refactors AI so it is easier to read and maintain
* Fixes NPC speed issues
* Fixes pet sector AI issue that was causing stuttering
* Fixes direction snapping for Melee/Mage AI
* Refactors pet orders
* Refactors speech commands
* Removes scale speed by dex for HS+ (it was a stupid feature anyways)
This commit is contained in:
Kamron Batman 2025-07-18 22:39:57 -07:00 committed by GitHub
parent f89150735e
commit aab731ed96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 3688 additions and 3429 deletions

View file

@ -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))

View file

@ -14,10 +14,8 @@ public class AIControlMobileTarget : Target
order == OrderType.Attack ? TargetFlags.Harmful : TargetFlags.None
)
{
_list = new List<BaseAI>();
_list = [ai];
Order = order;
AddAI(ai);
}
public OrderType Order { get; }

File diff suppressed because it is too large Load diff

View file

@ -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 <http://www.gnu.org/licenses/>. *
************************************************************************/
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<BaseCreature, Point3D> _reservedPositions = new();
private static long _lastGroupUpdateTime;
private static void CleanupReservedPositions()
{
using var toRemove = PooledRefQueue<BaseCreature>.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<BaseCreature> GetNearbyAllies(Mobile target)
{
var allies = PooledRefList<BaseCreature>.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<BaseCreature> 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<BaseCreature> 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;
}
}

View file

@ -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 <http://www.gnu.org/licenses/>. *
************************************************************************/
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<Item> GatherObstacles(int x, int y, out int destroyables)
{
var queue = PooledRefQueue<Item>.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<Item> 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<Item> 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<Item> 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;
}
}

View file

@ -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 <http://www.gnu.org/licenses/>. *
************************************************************************/
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);
}
}

View file

@ -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 <http://www.gnu.org/licenses/>. *
************************************************************************/
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
}

View file

@ -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 <http://www.gnu.org/licenses/>. *
************************************************************************/
namespace Server.Mobiles;
public enum ActionType
{
Wander,
Combat,
Guard,
Flee,
Backoff,
Interact
}

View file

@ -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 <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 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<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 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);
}
}

View file

@ -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 <http://www.gnu.org/licenses/>. *
************************************************************************/
using Server.Collections;
using Server.ContextMenus;
namespace Server.Mobiles;
public abstract partial class BaseAI
{
public virtual void GetContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> 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<ContextMenuEntry> 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<ContextMenuEntry> 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
}
}

View file

@ -79,7 +79,7 @@ public ref struct DebugInterpolatedStringHandler
/// <remarks>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.</remarks>
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
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>. *
************************************************************************/
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;
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>. *
************************************************************************/
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;
}
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>. *
************************************************************************/
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.");
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>. *
************************************************************************/
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);
}
}

View file

@ -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 <http://www.gnu.org/licenses/>. *
************************************************************************/
using System;
namespace Server;
public static class SpeechEventArgsExt
{
public static int GetFirstKeyword(this SpeechEventArgs e, params ReadOnlySpan<int> keywords)
{
for (var i = 0; i < keywords.Length; i++)
{
var keyword = keywords[i];
if (e.HasKeyword(keyword))
{
return keyword;
}
}
return 0;
}
}

View file

@ -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 <http://www.gnu.org/licenses/>. *
************************************************************************/
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.
}
}

View file

@ -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 <http://www.gnu.org/licenses/>. *
************************************************************************/
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;
}
}

View file

@ -31,23 +31,15 @@ public class HealerAI : BaseAI
{
var spellTarg = targ as ISpellTarget<Mobile>;
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)

View file

@ -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);

View file

@ -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

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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)

View file

@ -22,32 +22,12 @@ public static class NPCSpeeds
private static readonly Dictionary<Type, SpeedClassEntry> _speedsByType = new();
private static readonly Dictionary<SpeedLevel, SpeedClassEntry> _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);