ModernUO/Projects/UOContent/Mobiles/Familiars/BaseFamiliar.cs
Kamron Batman 3332fabc39
fix: Reverts to RunUO speeds, fixes direction glitching, adds movement speed interpolation (#1695)
> [!IMPORTANT]  
> Please read through these changes, as they changed certain expectations for how the internal AI thinking/movement work.
> Note that some mobs still don't have smooth movement on ClassicUO due to how the client handles animations/movement.

### Summary
- **Important Change**: Mobs will now move at a more regular pace. If the OnThink results in a move, but the cooldown would otherwise prevent the move, then the movement is scheduled on another timer.
- Added a 400ms delay to mobs turning to face a player to attack in order to avoid glitching between moving and turning.
- Reverted AI thinking speeds back to RunUO specific speeds.
- Reverted the AI thinking to moving conversion delays back to RunUO.
- For thinking speeds that are not exactly the predefined speeds from RunUO, there is a new calculation to determine the correct conversion to movement speed. This stops speeds like `0.35` from being faster than `0.3`

> [!NOTE]  
> **Developer Note**
> BaseAI.CheckMove() no longer contains the check for whether or not a mob is on movement cooldown. This function can now be overwritten without causing issues to figuring out that cooldown.
2024-03-18 14:13:41 -07:00

191 lines
4.4 KiB
C#

using System.Collections.Generic;
using ModernUO.Serialization;
using Server.ContextMenus;
using Server.Items;
namespace Server.Mobiles;
[SerializationGenerator(0, false)]
public abstract partial class BaseFamiliar : BaseCreature
{
private bool m_LastHidden;
public BaseFamiliar() : base(AIType.AI_Melee)
{
SetSpeed(0.1, 0.11);
}
public override bool BardImmune => true;
public override Poison PoisonImmune => Poison.Lethal;
public override bool Commandable => false;
public override bool IgnoreMobiles => true;
public override bool PlayerRangeSensitive => false;
public virtual void RangeCheck()
{
if (Deleted || ControlMaster?.Deleted != false)
{
return;
}
var range = RangeHome - 2;
if (InRange(ControlMaster.Location, RangeHome))
{
return;
}
var master = ControlMaster;
var m_Loc = Point3D.Zero;
if (Map != master.Map)
{
return;
}
var x = X > master.X ? master.X + range : master.X - range;
var y = Y > master.Y ? master.Y + range : master.Y - range;
for (var i = 0; i < 10; i++)
{
m_Loc.X = x + Utility.RandomMinMax(-1, 1);
m_Loc.Y = y + Utility.RandomMinMax(-1, 1);
m_Loc.Z = Map.GetAverageZ(m_Loc.X, m_Loc.Y);
if (Map.CanSpawnMobile(m_Loc))
{
break;
}
m_Loc = master.Location;
}
if (!Deleted)
{
SetLocation(m_Loc, true);
}
}
public override void OnThink()
{
var master = ControlMaster;
if (Deleted)
{
return;
}
if (master?.Deleted != false)
{
DropPackContents();
EndRelease(null);
return;
}
RangeCheck();
if (m_LastHidden != master.Hidden)
{
Hidden = m_LastHidden = master.Hidden;
}
if (AIObject?.WalkMobileRange(master, 5, true, 1, 1) == true)
{
Warmode = master.Warmode;
Combatant = master.Combatant;
CurrentSpeed = 0.1;
}
else
{
Warmode = false;
FocusMob = Combatant = null;
CurrentSpeed = 0.01;
}
}
public override void GetContextMenuEntries(Mobile from, List<ContextMenuEntry> list)
{
base.GetContextMenuEntries(from, list);
if (from.Alive && Controlled && from == ControlMaster && from.InRange(this, 14))
{
list.Add(new ReleaseEntry(from, this));
}
}
public virtual void BeginRelease(Mobile from)
{
if (!Deleted && Controlled && from == ControlMaster && from.CheckAlive())
{
EndRelease(from);
}
}
public virtual void EndRelease(Mobile from)
{
if (from?.CheckAlive() != false && !Deleted && Controlled && from == ControlMaster)
{
Effects.SendLocationParticles(
EffectItem.Create(Location, Map, EffectItem.DefaultDuration),
0x3728,
1,
13,
2100,
3,
5042,
0
);
PlaySound(0x201);
Delete();
}
}
public virtual void DropPackContents()
{
var map = Map;
var pack = Backpack;
if (map != null && map != Map.Internal && pack != null)
{
var list = new List<Item>(pack.Items);
for (var i = 0; i < list.Count; ++i)
{
list[i].MoveToWorld(Location, map);
}
}
}
[AfterDeserialization(false)]
private void AfterDeserialization()
{
DropPackContents();
Delete();
}
private class ReleaseEntry : ContextMenuEntry
{
private readonly BaseFamiliar m_Familiar;
private readonly Mobile m_From;
public ReleaseEntry(Mobile from, BaseFamiliar familiar) : base(6118, 14)
{
m_From = from;
m_Familiar = familiar;
}
public override void OnClick()
{
if (!m_Familiar.Deleted && m_Familiar.Controlled && m_From == m_Familiar.ControlMaster &&
m_From.CheckAlive())
{
m_Familiar.BeginRelease(m_From);
}
}
}
}