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:
parent
f89150735e
commit
aab731ed96
26 changed files with 3688 additions and 3429 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
215
Projects/UOContent/Mobiles/AI/BaseAI/AIGroupMovement.cs
Normal file
215
Projects/UOContent/Mobiles/AI/BaseAI/AIGroupMovement.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
429
Projects/UOContent/Mobiles/AI/BaseAI/AIMovement.cs
Normal file
429
Projects/UOContent/Mobiles/AI/BaseAI/AIMovement.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
134
Projects/UOContent/Mobiles/AI/BaseAI/AITimer.cs
Normal file
134
Projects/UOContent/Mobiles/AI/BaseAI/AITimer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
30
Projects/UOContent/Mobiles/AI/BaseAI/AIType.cs
Normal file
30
Projects/UOContent/Mobiles/AI/BaseAI/AIType.cs
Normal 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
|
||||
}
|
||||
26
Projects/UOContent/Mobiles/AI/BaseAI/ActionType.cs
Normal file
26
Projects/UOContent/Mobiles/AI/BaseAI/ActionType.cs
Normal 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
|
||||
}
|
||||
918
Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs
Normal file
918
Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
75
Projects/UOContent/Mobiles/AI/BaseAI/ContextMenu.cs
Normal file
75
Projects/UOContent/Mobiles/AI/BaseAI/ContextMenu.cs
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
119
Projects/UOContent/Mobiles/AI/BaseAI/InternalEntry.cs
Normal file
119
Projects/UOContent/Mobiles/AI/BaseAI/InternalEntry.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
457
Projects/UOContent/Mobiles/AI/BaseAI/OnSpeech.cs
Normal file
457
Projects/UOContent/Mobiles/AI/BaseAI/OnSpeech.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
241
Projects/UOContent/Mobiles/AI/BaseAI/PetOrderHandlers.cs
Normal file
241
Projects/UOContent/Mobiles/AI/BaseAI/PetOrderHandlers.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
518
Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs
Normal file
518
Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
35
Projects/UOContent/Mobiles/AI/BaseAI/SpeechEventArgsExt.cs
Normal file
35
Projects/UOContent/Mobiles/AI/BaseAI/SpeechEventArgsExt.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
190
Projects/UOContent/Mobiles/AI/BaseAI/TransferItem.cs
Normal file
190
Projects/UOContent/Mobiles/AI/BaseAI/TransferItem.cs
Normal 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.
|
||||
}
|
||||
}
|
||||
125
Projects/UOContent/Mobiles/AI/BaseAI/WalkRandomLogic.cs
Normal file
125
Projects/UOContent/Mobiles/AI/BaseAI/WalkRandomLogic.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue