### Summary - [X] Fixed a bug where entity persistence was serialized out of order, causing world corruption. - [X] Fixed LastSerialized not being utilized properly and dangling references still becoming an issue. - [X] Added a new `GenericEntityPersistence<T>` type to encapsulate `ISerializable` serialization. - [X] Removing the custom logic and moved Items, Mobiles, Guilds, and Accounts to GenericEntityPersistence. - [X] Changed serialization to use the singleton pattern to reduce calling methods from stored variables.
465 lines
19 KiB
C#
465 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Server.Engines.Virtues;
|
|
using Server.Factions;
|
|
using Server.Mobiles;
|
|
using Server.Spells;
|
|
using Server.Spells.Necromancy;
|
|
using Server.Spells.Spellweaving;
|
|
using Server.Targeting;
|
|
|
|
namespace Server.SkillHandlers
|
|
{
|
|
public static class AnimalTaming
|
|
{
|
|
private static readonly HashSet<Mobile> m_BeingTamed = new();
|
|
|
|
public static bool DisableMessage { get; set; }
|
|
|
|
public static void Initialize()
|
|
{
|
|
SkillInfo.Table[(int)SkillName.AnimalTaming].Callback = OnUse;
|
|
}
|
|
|
|
public static TimeSpan OnUse(Mobile m)
|
|
{
|
|
m.RevealingAction();
|
|
|
|
m.Target = new InternalTarget();
|
|
m.RevealingAction();
|
|
|
|
if (!DisableMessage)
|
|
{
|
|
m.SendLocalizedMessage(502789); // Tame which animal?
|
|
}
|
|
|
|
return TimeSpan.FromSeconds(30);
|
|
}
|
|
|
|
public static bool CheckMastery(Mobile tamer, BaseCreature creature) =>
|
|
SummonFamiliarSpell.Table.TryGetValue(tamer, out var bc)
|
|
&& bc is DarkWolfFamiliar { Deleted: false }
|
|
&& creature is DireWolf or GreyWolf or TimberWolf or WhiteWolf or BakeKitsune;
|
|
|
|
public static bool MustBeSubdued(BaseCreature bc) =>
|
|
bc.Owners.Count <= 0 && bc.SubdueBeforeTame && bc.Hits > bc.HitsMax / 10;
|
|
|
|
public static void ScaleStats(BaseCreature bc, double scalar)
|
|
{
|
|
if (bc.RawStr > 0)
|
|
{
|
|
bc.RawStr = (int)Math.Max(1, bc.RawStr * scalar);
|
|
}
|
|
|
|
if (bc.RawDex > 0)
|
|
{
|
|
bc.RawDex = (int)Math.Max(1, bc.RawDex * scalar);
|
|
}
|
|
|
|
if (bc.RawInt > 0)
|
|
{
|
|
bc.RawInt = (int)Math.Max(1, bc.RawInt * scalar);
|
|
}
|
|
|
|
if (bc.HitsMaxSeed > 0)
|
|
{
|
|
bc.HitsMaxSeed = (int)Math.Max(1, bc.HitsMaxSeed * scalar);
|
|
bc.Hits = bc.Hits;
|
|
}
|
|
|
|
if (bc.StamMaxSeed > 0)
|
|
{
|
|
bc.StamMaxSeed = (int)Math.Max(1, bc.StamMaxSeed * scalar);
|
|
bc.Stam = bc.Stam;
|
|
}
|
|
}
|
|
|
|
public static void ScaleSkills(BaseCreature bc, double scalar)
|
|
{
|
|
ScaleSkills(bc, scalar, scalar);
|
|
}
|
|
|
|
public static void ScaleSkills(BaseCreature bc, double scalar, double capScalar)
|
|
{
|
|
for (var i = 0; i < bc.Skills.Length; ++i)
|
|
{
|
|
bc.Skills[i].Base *= scalar;
|
|
|
|
bc.Skills[i].Cap = Math.Max(100.0, bc.Skills[i].Cap * capScalar);
|
|
|
|
if (bc.Skills[i].Base > bc.Skills[i].Cap)
|
|
{
|
|
bc.Skills[i].Cap = bc.Skills[i].Base;
|
|
}
|
|
}
|
|
}
|
|
|
|
private class InternalTarget : Target
|
|
{
|
|
private bool m_SetSkillTime = true;
|
|
|
|
public InternalTarget() : base(Core.AOS ? 3 : 2, false, TargetFlags.None)
|
|
{
|
|
}
|
|
|
|
protected override void OnTargetFinish(Mobile from)
|
|
{
|
|
if (m_SetSkillTime)
|
|
{
|
|
from.NextSkillTime = Core.TickCount;
|
|
}
|
|
}
|
|
|
|
protected override void OnTarget(Mobile from, object targeted)
|
|
{
|
|
from.RevealingAction();
|
|
|
|
if (targeted is not Mobile mobile)
|
|
{
|
|
from.SendLocalizedMessage(502801); // You can't tame that!
|
|
return;
|
|
}
|
|
|
|
if (mobile is not BaseCreature creature)
|
|
{
|
|
// That being cannot be tamed.
|
|
mobile.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 502469, from.NetState);
|
|
return;
|
|
}
|
|
|
|
if (!creature.Tamable)
|
|
{
|
|
// That creature cannot be tamed.
|
|
creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 1049655, from.NetState);
|
|
return;
|
|
}
|
|
|
|
if (creature.Controlled)
|
|
{
|
|
// That animal looks tame already.
|
|
creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 502804, from.NetState);
|
|
return;
|
|
}
|
|
|
|
if (from.Female && !creature.AllowFemaleTamer)
|
|
{
|
|
// That creature can only be tamed by males.
|
|
creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 1049653, from.NetState);
|
|
return;
|
|
}
|
|
|
|
if (!from.Female && !creature.AllowMaleTamer)
|
|
{
|
|
// That creature can only be tamed by females.
|
|
creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 1049652, from.NetState);
|
|
return;
|
|
}
|
|
|
|
if (creature is CuSidhe && from.Race != Race.Elf)
|
|
{
|
|
// You can't tame that!
|
|
creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 502801, from.NetState);
|
|
return;
|
|
}
|
|
|
|
if (from.Followers + creature.ControlSlots > from.FollowersMax)
|
|
{
|
|
from.SendLocalizedMessage(1049611); // You have too many followers to tame that creature.
|
|
return;
|
|
}
|
|
|
|
if (creature.Owners.Count >= BaseCreature.MaxOwners && !creature.Owners.Contains(from))
|
|
{
|
|
// This animal has had too many owners and is too upset for you to tame.
|
|
creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 1005615, from.NetState);
|
|
return;
|
|
}
|
|
|
|
if (MustBeSubdued(creature))
|
|
{
|
|
// You must subdue this creature before you can tame it!
|
|
creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 1054025, from.NetState);
|
|
return;
|
|
}
|
|
|
|
if (!(CheckMastery(from, creature) || from.Skills.AnimalTaming.Value >= creature.MinTameSkill))
|
|
{
|
|
// You have no chance of taming this creature.
|
|
creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 502806, from.NetState);
|
|
return;
|
|
}
|
|
|
|
if (creature is FactionWarHorse warHorse)
|
|
{
|
|
var faction = Faction.Find(from);
|
|
|
|
if (faction == null || faction != warHorse.Faction)
|
|
{
|
|
// You cannot tame this creature.
|
|
creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 1042590, from.NetState);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (m_BeingTamed.Contains(creature))
|
|
{
|
|
// Someone else is already taming this.
|
|
creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 502802, from.NetState);
|
|
}
|
|
else if (creature.CanAngerOnTame && Utility.RandomDouble() < 0.95)
|
|
{
|
|
// You seem to anger the beast!
|
|
creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 502805, from.NetState);
|
|
|
|
creature.PlaySound(creature.GetAngerSound());
|
|
creature.Direction = creature.GetDirectionTo(from);
|
|
|
|
if (creature.BardPacified && Utility.RandomDouble() < 0.75)
|
|
{
|
|
Timer.StartTimer(TimeSpan.FromSeconds(2.0), () => Pacify(creature));
|
|
}
|
|
else
|
|
{
|
|
creature.BardEndTime = Core.Now;
|
|
}
|
|
|
|
creature.BardPacified = false;
|
|
|
|
creature.AIObject?.DoMove(creature.Direction);
|
|
|
|
if (from is PlayerMobile pm &&
|
|
!(VirtueSystem.GetVirtues(pm)?.HonorActive == true ||
|
|
TransformationSpellHelper.UnderTransformation(pm, typeof(EtherealVoyageSpell))))
|
|
{
|
|
creature.Combatant = pm;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_BeingTamed.Add(creature);
|
|
|
|
// You start to tame the creature.
|
|
from.LocalOverheadMessage(MessageType.Emote, 0x59, 1010597);
|
|
|
|
// *begins taming a creature.*
|
|
from.NonlocalOverheadMessage(MessageType.Emote, 0x59, 1010598);
|
|
|
|
new InternalTimer(from, creature, Utility.Random(3, 2)).Start();
|
|
|
|
m_SetSkillTime = false;
|
|
}
|
|
}
|
|
|
|
private static void Pacify(BaseCreature bc) => bc.BardPacified = true; // Should use bc.Pacify with an end time?
|
|
|
|
private class InternalTimer : Timer
|
|
{
|
|
private readonly BaseCreature m_Creature;
|
|
private readonly int m_MaxCount;
|
|
private readonly DateTime m_StartTime;
|
|
private readonly Mobile m_Tamer;
|
|
private int m_Count;
|
|
private bool m_Paralyzed;
|
|
|
|
public InternalTimer(Mobile tamer, BaseCreature creature, int count) : base(
|
|
TimeSpan.FromSeconds(3.0),
|
|
TimeSpan.FromSeconds(3.0),
|
|
count
|
|
)
|
|
{
|
|
m_Tamer = tamer;
|
|
m_Creature = creature;
|
|
m_MaxCount = count;
|
|
m_Paralyzed = creature.Paralyzed;
|
|
m_StartTime = Core.Now;
|
|
}
|
|
|
|
protected override void OnTick()
|
|
{
|
|
m_Count++;
|
|
|
|
var de = m_Creature.FindMostRecentDamageEntry(false);
|
|
var alreadyOwned = m_Creature.Owners.Contains(m_Tamer);
|
|
|
|
if (!m_Tamer.InRange(m_Creature, Core.AOS ? 7 : 6))
|
|
{
|
|
m_BeingTamed.Remove(m_Creature);
|
|
m_Tamer.NextSkillTime = Core.TickCount;
|
|
// You are too far away to continue taming.
|
|
m_Creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 502795, m_Tamer.NetState);
|
|
Stop();
|
|
}
|
|
else if (!m_Tamer.CheckAlive())
|
|
{
|
|
m_BeingTamed.Remove(m_Creature);
|
|
m_Tamer.NextSkillTime = Core.TickCount;
|
|
// You are dead, and cannot continue taming.
|
|
m_Creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 502796, m_Tamer.NetState);
|
|
Stop();
|
|
}
|
|
else if (!m_Tamer.CanSee(m_Creature) || !m_Tamer.InLOS(m_Creature) || !CanPath())
|
|
{
|
|
m_BeingTamed.Remove(m_Creature);
|
|
m_Tamer.NextSkillTime = Core.TickCount;
|
|
// You do not have a clear path to the animal you are taming, and must cease your attempt.
|
|
m_Tamer.SendLocalizedMessage(1049654);
|
|
Stop();
|
|
}
|
|
else if (!m_Creature.Tamable)
|
|
{
|
|
m_BeingTamed.Remove(m_Creature);
|
|
m_Tamer.NextSkillTime = Core.TickCount;
|
|
// That creature cannot be tamed.
|
|
m_Creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 1049655, m_Tamer.NetState);
|
|
Stop();
|
|
}
|
|
else if (m_Creature.Controlled)
|
|
{
|
|
m_BeingTamed.Remove(m_Creature);
|
|
m_Tamer.NextSkillTime = Core.TickCount;
|
|
// That animal looks tame already.
|
|
m_Creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 502804, m_Tamer.NetState);
|
|
Stop();
|
|
}
|
|
else if (m_Creature.Owners.Count >= BaseCreature.MaxOwners && !m_Creature.Owners.Contains(m_Tamer))
|
|
{
|
|
m_BeingTamed.Remove(m_Creature);
|
|
m_Tamer.NextSkillTime = Core.TickCount;
|
|
// This animal has had too many owners and is too upset for you to tame.
|
|
m_Creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 1005615, m_Tamer.NetState);
|
|
Stop();
|
|
}
|
|
else if (MustBeSubdued(m_Creature))
|
|
{
|
|
m_BeingTamed.Remove(m_Creature);
|
|
m_Tamer.NextSkillTime = Core.TickCount;
|
|
// You must subdue this creature before you can tame it!
|
|
m_Creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 1054025, m_Tamer.NetState);
|
|
Stop();
|
|
}
|
|
else if (de?.LastDamage > m_StartTime)
|
|
{
|
|
m_BeingTamed.Remove(m_Creature);
|
|
m_Tamer.NextSkillTime = Core.TickCount;
|
|
// The animal is too angry to continue taming.
|
|
m_Creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 502794, m_Tamer.NetState);
|
|
Stop();
|
|
}
|
|
else if (m_Count < m_MaxCount)
|
|
{
|
|
m_Tamer.RevealingAction();
|
|
|
|
switch (Utility.Random(3))
|
|
{
|
|
case 0:
|
|
{
|
|
m_Tamer.PublicOverheadMessage(MessageType.Regular, 0x3B2, Utility.Random(502790, 4));
|
|
break;
|
|
}
|
|
case 1:
|
|
{
|
|
m_Tamer.PublicOverheadMessage(MessageType.Regular, 0x3B2, Utility.Random(1005608, 6));
|
|
break;
|
|
}
|
|
case 2:
|
|
{
|
|
m_Tamer.PublicOverheadMessage(MessageType.Regular, 0x3B2, Utility.Random(1010593, 4));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!alreadyOwned) // Passively check animal lore for gain
|
|
{
|
|
m_Tamer.CheckTargetSkill(SkillName.AnimalLore, m_Creature, 0.0, 120.0);
|
|
}
|
|
|
|
if (m_Creature.Paralyzed)
|
|
{
|
|
m_Paralyzed = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_Tamer.RevealingAction();
|
|
m_Tamer.NextSkillTime = Core.TickCount;
|
|
m_BeingTamed.Remove(m_Creature);
|
|
|
|
if (m_Creature.Paralyzed)
|
|
{
|
|
m_Paralyzed = true;
|
|
}
|
|
|
|
if (!alreadyOwned) // Passively check animal lore for gain
|
|
{
|
|
m_Tamer.CheckTargetSkill(SkillName.AnimalLore, m_Creature, 0.0, 120.0);
|
|
}
|
|
|
|
var minSkill = m_Creature.MinTameSkill + m_Creature.Owners.Count * 6.0;
|
|
|
|
if (minSkill > -24.9 && CheckMastery(m_Tamer, m_Creature))
|
|
{
|
|
minSkill = -24.9; // 50% at 0.0?
|
|
}
|
|
|
|
minSkill += 24.9;
|
|
|
|
if (CheckMastery(m_Tamer, m_Creature) || alreadyOwned ||
|
|
m_Tamer.CheckTargetSkill(SkillName.AnimalTaming, m_Creature, minSkill - 25.0, minSkill + 25.0))
|
|
{
|
|
if (m_Creature.Owners.Count == 0) // First tame
|
|
{
|
|
if (m_Creature is GreaterDragon)
|
|
{
|
|
ScaleSkills(m_Creature, 0.72, 0.90); // 72% of original skills trainable to 90%
|
|
// Greater dragons have a 90% cap reduction and 90% skill reduction on magery
|
|
m_Creature.Skills.Magery.Base = m_Creature.Skills.Magery.Cap;
|
|
}
|
|
else if (m_Paralyzed)
|
|
{
|
|
// 86% of original skills if they were paralyzed during the taming
|
|
ScaleSkills(m_Creature, 0.86);
|
|
}
|
|
else
|
|
{
|
|
ScaleSkills(m_Creature, 0.90); // 90% of original skills
|
|
}
|
|
|
|
if (m_Creature.StatLossAfterTame)
|
|
{
|
|
ScaleStats(m_Creature, 0.50);
|
|
}
|
|
}
|
|
|
|
if (alreadyOwned)
|
|
{
|
|
m_Tamer.SendLocalizedMessage(502797); // That wasn't even challenging.
|
|
}
|
|
else
|
|
{
|
|
// It seems to accept you as master.
|
|
m_Creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 502799, m_Tamer.NetState);
|
|
m_Creature.Owners.Add(m_Tamer);
|
|
}
|
|
|
|
m_Creature.SetControlMaster(m_Tamer);
|
|
m_Creature.IsBonded = false;
|
|
}
|
|
else
|
|
{
|
|
// You fail to tame the creature.
|
|
m_Creature.PrivateOverheadMessage(MessageType.Regular, 0x3B2, 502798, m_Tamer.NetState);
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool CanPath()
|
|
{
|
|
IPoint3D p = m_Tamer;
|
|
|
|
return p != null && (m_Creature.InRange(new Point3D(p), 1) ||
|
|
new MovementPath(m_Creature, new Point3D(p)).Success);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|