ModernUO/Projects/UOContent/Mobiles/Townfolk/BaseEscortable.cs
2020-08-21 19:50:36 -07:00

727 lines
19 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using Server.ContextMenus;
using Server.Engines.MLQuests;
using Server.Engines.MLQuests.Definitions;
using Server.Engines.MLQuests.Objectives;
using Server.Items;
using Server.Misc;
using Server.Regions;
using EDI = Server.Mobiles.EscortDestinationInfo;
namespace Server.Mobiles
{
public class BaseEscortable : BaseCreature
{
public static readonly TimeSpan EscortDelay = TimeSpan.FromMinutes(5.0);
public static readonly TimeSpan AbandonDelay =
MLQuestSystem.Enabled ? TimeSpan.FromMinutes(1.0) : TimeSpan.FromMinutes(2.0);
public static readonly TimeSpan DeleteTime =
MLQuestSystem.Enabled ? TimeSpan.FromSeconds(100) : TimeSpan.FromSeconds(30);
// Classic list
// Used when: !MLQuestSystem.Enabled && !Core.ML
private static readonly string[] m_TownNames =
{
"Cove", "Britain", "Jhelom",
"Minoc", "Ocllo", "Trinsic",
"Vesper", "Yew", "Skara Brae",
"Nujel'm", "Moonglow", "Magincia"
};
// ML list, pre-ML quest system
// Used when: !MLQuestSystem.Enabled && Core.ML
private static readonly string[] m_MLTownNames =
{
"Cove", "Serpent's Hold", "Jhelom",
"Nujel'm"
};
// ML quest system general list
// Used when: MLQuestSystem.Enabled && !Region.IsPartOf( "Haven Island" )
private static readonly Type[] m_MLQuestTypes =
{
typeof(EscortToYew),
typeof(EscortToVesper),
typeof(EscortToTrinsic),
typeof(EscortToSkaraBrae),
typeof(EscortToSerpentsHold),
typeof(EscortToNujelm),
typeof(EscortToMoonglow),
typeof(EscortToMinoc),
typeof(EscortToMagincia),
typeof(EscortToJhelom),
typeof(EscortToCove),
typeof(EscortToBritain)
// Ocllo was removed in pub 56
// typeof( EscortToOcllo )
};
// ML quest system New Haven list
// Used when: MLQuestSystem.Enabled && Region.IsPartOf( "Haven Island" )
private static readonly Type[] m_MLQuestTypesNH =
{
typeof(EscortToNHAlchemist),
typeof(EscortToNHBard),
typeof(EscortToNHWarrior),
typeof(EscortToNHTailor),
typeof(EscortToNHCarpenter),
typeof(EscortToNHMapmaker),
typeof(EscortToNHMage),
typeof(EscortToNHInn),
// Farm destination was removed
// typeof( EscortToNHFarm ),
typeof(EscortToNHDocks),
typeof(EscortToNHBowyer),
typeof(EscortToNHBank)
};
private bool m_DeleteCorpse;
private DateTime m_DeleteTime;
private Timer m_DeleteTimer;
private EDI m_Destination;
private string m_DestinationString;
private DateTime m_LastSeenEscorter;
private MLQuest m_MLQuest;
[Constructible]
public BaseEscortable()
: base(AIType.AI_Melee, FightMode.Aggressor, 22, 1, 0.2, 1.0)
{
InitBody();
InitOutfit();
Fame = 200;
Karma = 4000;
}
public BaseEscortable(Serial serial)
: base(serial)
{
}
public override bool StaticMLQuester => false; // Suppress automatic quest registration on creation/deserialization
public override bool CanShout => !Controlled && !IsBeingDeleted;
public bool IsBeingDeleted => m_DeleteTimer != null;
public override bool Commandable => false; // Our master cannot boss us around!
public override bool DeleteCorpseOnDeath => m_DeleteCorpse;
[CommandProperty(AccessLevel.GameMaster)]
public string Destination
{
get => m_Destination?.Name;
set
{
m_DestinationString = value;
m_Destination = EDI.Find(value);
}
}
public static Dictionary<Mobile, BaseEscortable> EscortTable { get; } = new Dictionary<Mobile, BaseEscortable>();
protected override List<MLQuest> ConstructQuestList()
{
if (m_MLQuest == null)
{
Region reg = Region;
Type[] list = reg.IsPartOf("Haven Island") ? m_MLQuestTypesNH : m_MLQuestTypes;
int randomIdx = Utility.Random(list.Length);
for (int i = 0; i < list.Length; ++i)
{
Type questType = list[randomIdx];
MLQuest quest = MLQuestSystem.FindQuest(questType);
if (quest != null)
{
bool okay = true;
foreach (BaseObjective obj in quest.Objectives)
if (obj is EscortObjective objective && objective.Destination.Contains(reg))
{
okay = false; // We're already there!
break;
}
if (okay)
{
m_MLQuest = quest;
break;
}
}
else if (MLQuestSystem.Debug)
{
Console.WriteLine("Warning: Escortable cannot be assigned quest type '{0}', it is not registered",
questType.Name);
}
randomIdx = (randomIdx + 1) % list.Length;
}
if (m_MLQuest == null)
{
if (MLQuestSystem.Debug)
Console.WriteLine("Warning: No suitable quest found for escort {0}", Serial);
return null;
}
}
List<MLQuest> result = new List<MLQuest> { m_MLQuest };
return result;
}
public override void Shout(PlayerMobile pm)
{
/*
* 1072301 - You there! Care to hear how to earn some easy gold?
* 1072302 - Adventurer! I have an offer for you.
* 1072303 - Wait! I have an opportunity for you to make some gold!
*/
MLQuestSystem.Tell(this, pm, Utility.Random(1072301, 3));
}
public virtual void InitBody()
{
SetStr(90, 100);
SetDex(90, 100);
SetInt(15, 25);
Hue = Race.Human.RandomSkinHue();
if (Female = Utility.RandomBool())
{
Body = 401;
Name = NameList.RandomName("female");
}
else
{
Body = 400;
Name = NameList.RandomName("male");
}
}
public virtual void InitOutfit()
{
AddItem(new FancyShirt(Utility.RandomNeutralHue()));
AddItem(new ShortPants(Utility.RandomNeutralHue()));
AddItem(new Boots(Utility.RandomNeutralHue()));
Utility.AssignRandomHair(this);
PackGold(200, 250);
}
public virtual bool SayDestinationTo(Mobile m)
{
EDI dest = GetDestination();
if (dest == null || !m.Alive)
return false;
Mobile escorter = GetEscorter();
if (escorter == null)
{
Say("I am looking to go to {0}, will you take me?",
dest.Name == "Ocllo" && m.Map == Map.Trammel ? "Haven" : dest.Name);
return true;
}
if (escorter == m)
{
Say("Lead on! Payment will be made when we arrive in {0}.",
dest.Name == "Ocllo" && m.Map == Map.Trammel ? "Haven" : dest.Name);
return true;
}
return false;
}
public virtual bool AcceptEscorter(Mobile m)
{
EDI dest = GetDestination();
if (dest == null)
return false;
if (GetEscorter() != null || !m.Alive)
return false;
if (EscortTable.TryGetValue(m, out BaseEscortable escortable) && escortable?.Deleted == false && escortable.GetEscorter() == m)
{
Say("I see you already have an escort.");
return false;
}
if (m is PlayerMobile mobile && mobile.LastEscortTime + EscortDelay >= DateTime.UtcNow)
{
int minutes =
(int)Math.Ceiling((mobile.LastEscortTime + EscortDelay - DateTime.UtcNow).TotalMinutes);
Say("You must rest {0} minute{1} before we set out on this journey.", minutes, minutes == 1 ? "" : "s");
return false;
}
if (SetControlMaster(m))
{
m_LastSeenEscorter = DateTime.UtcNow;
if (m is PlayerMobile playerMobile)
playerMobile.LastEscortTime = DateTime.UtcNow;
Say("Lead on! Payment will be made when we arrive in {0}.",
dest.Name == "Ocllo" && m.Map == Map.Trammel ? "Haven" : dest.Name);
EscortTable[m] = this;
StartFollow();
return true;
}
return false;
}
public override bool HandlesOnSpeech(Mobile from) => !MLQuestSystem.Enabled && (from.InRange(Location, 3) || base.HandlesOnSpeech(from));
public override void OnSpeech(SpeechEventArgs e)
{
base.OnSpeech(e);
if (GetDestination() == null || e.Handled || !e.Mobile.InRange(Location, 3))
return;
if (e.HasKeyword(0x1D)) // *destination*
e.Handled = SayDestinationTo(e.Mobile);
else if (e.HasKeyword(0x1E)) // *i will take thee*
e.Handled = AcceptEscorter(e.Mobile);
}
public override void OnAfterDelete()
{
m_DeleteTimer?.Stop();
m_DeleteTimer = null;
base.OnAfterDelete();
}
public override void OnThink()
{
base.OnThink();
CheckAtDestination();
}
protected override bool OnMove(Direction d)
{
if (!base.OnMove(d))
return false;
CheckAtDestination();
return true;
}
// TODO: Pre-ML methods below, might be mergeable with the ML methods in EscortObjective
public virtual void StartFollow()
{
StartFollow(GetEscorter());
}
public virtual void StartFollow(Mobile escorter)
{
if (escorter == null)
return;
ActiveSpeed = 0.1;
PassiveSpeed = 0.2;
ControlOrder = OrderType.Follow;
ControlTarget = escorter;
if (IsPrisoner && CantWalk) CantWalk = false;
CurrentSpeed = 0.1;
}
public virtual void StopFollow()
{
ActiveSpeed = 0.2;
PassiveSpeed = 1.0;
ControlOrder = OrderType.None;
ControlTarget = null;
CurrentSpeed = 1.0;
}
public virtual Mobile GetEscorter()
{
if (!Controlled)
return null;
Mobile master = ControlMaster;
if (MLQuestSystem.Enabled || master == null)
return master;
if (master.Deleted || master.Map != Map || !master.InRange(Location, 30) || !master.Alive)
{
StopFollow();
TimeSpan lastSeenDelay = DateTime.UtcNow - m_LastSeenEscorter;
if (lastSeenDelay >= AbandonDelay)
{
master.SendLocalizedMessage(1042473); // You have lost the person you were escorting.
Say(1005653); // Hmmm. I seem to have lost my master.
SetControlMaster(null);
EscortTable.Remove(master);
Timer.DelayCall(TimeSpan.FromSeconds(5.0), Delete);
return null;
}
ControlOrder = OrderType.Stay;
return master;
}
if (ControlOrder != OrderType.Follow)
StartFollow(master);
m_LastSeenEscorter = DateTime.UtcNow;
return master;
}
public virtual void BeginDelete()
{
m_DeleteTimer?.Stop();
m_DeleteTime = DateTime.UtcNow + DeleteTime;
m_DeleteTimer = new DeleteTimer(this, m_DeleteTime - DateTime.UtcNow);
m_DeleteTimer.Start();
}
public virtual bool CheckAtDestination()
{
if (MLQuestSystem.Enabled)
return false;
EDI dest = GetDestination();
if (dest == null)
return false;
Mobile escorter = GetEscorter();
if (escorter == null)
return false;
if (dest.Contains(Location))
{
Say(1042809,
escorter.Name); // We have arrived! I thank thee, ~1_PLAYER_NAME~! I have no further need of thy services. Here is thy pay.
// not going anywhere
m_Destination = null;
m_DestinationString = null;
Container cont = escorter.Backpack ?? escorter.BankBox;
Gold gold = new Gold(500, 1000);
if (!cont.TryDropItem(escorter, gold, false))
gold.MoveToWorld(escorter.Location, escorter.Map);
StopFollow();
SetControlMaster(null);
EscortTable.Remove(escorter);
BeginDelete();
Titles.AwardFame(escorter, 10, true);
bool gainedPath = false;
if (escorter is PlayerMobile pm)
{
if (pm.CompassionGains > 0 && DateTime.UtcNow > pm.NextCompassionDay)
{
pm.NextCompassionDay = DateTime.MinValue;
pm.CompassionGains = 0;
}
if (pm.CompassionGains >= 5) // have already gained 5 times in one day, can gain no more
{
pm.SendLocalizedMessage(
1053004); // You must wait about a day before you can gain in compassion again.
}
else if (VirtueHelper.Award(pm, VirtueName.Compassion, IsPrisoner ? 400 : 200, ref gainedPath))
{
if (gainedPath)
pm.SendLocalizedMessage(1053005); // You have achieved a path in compassion!
else
pm.SendLocalizedMessage(1053002); // You have gained in compassion.
pm.NextCompassionDay =
DateTime.UtcNow + TimeSpan.FromDays(1.0); // in one day CompassionGains gets reset to 0
++pm.CompassionGains;
if (pm.CompassionGains >= 5)
pm.SendLocalizedMessage(
1053004); // You must wait about a day before you can gain in compassion again.
}
else
{
pm.SendLocalizedMessage(
1053003); // You have achieved the highest path of compassion and can no longer gain any further.
}
}
return true;
}
return false;
}
public override bool OnBeforeDeath()
{
m_DeleteCorpse = Controlled || IsBeingDeleted;
return base.OnBeforeDeath();
}
public override void Serialize(IGenericWriter writer)
{
base.Serialize(writer);
writer.Write(1); // version
EDI dest = GetDestination();
writer.Write(dest != null);
if (dest != null)
writer.Write(dest.Name);
writer.Write(m_DeleteTimer != null);
if (m_DeleteTimer != null)
writer.WriteDeltaTime(m_DeleteTime);
MLQuestSystem.WriteQuestRef(writer, StaticMLQuester ? null : m_MLQuest);
}
public override void Deserialize(IGenericReader reader)
{
base.Deserialize(reader);
int version = reader.ReadInt();
if (reader.ReadBool())
m_DestinationString =
reader.ReadString(); // NOTE: We cannot EDI.Find here, regions have not yet been loaded :-(
if (reader.ReadBool())
{
m_DeleteTime = reader.ReadDeltaTime();
m_DeleteTimer = new DeleteTimer(this, m_DeleteTime - DateTime.UtcNow);
m_DeleteTimer.Start();
}
if (version >= 1)
{
MLQuest quest = MLQuestSystem.ReadQuestRef(reader);
if (MLQuestSystem.Enabled && quest != null && !StaticMLQuester)
m_MLQuest = quest;
}
}
public override bool CanBeRenamedBy(Mobile from) => from.AccessLevel >= AccessLevel.GameMaster;
public override void AddCustomContextEntries(Mobile from, List<ContextMenuEntry> list)
{
if (from.Alive)
{
Mobile escorter = GetEscorter();
if (!MLQuestSystem.Enabled && GetDestination() != null)
{
if (escorter == null || escorter == from)
list.Add(new AskDestinationEntry(this, from));
if (escorter == null)
list.Add(new AcceptEscortEntry(this, from));
}
if (escorter == from)
list.Add(new AbandonEscortEntry(this));
}
base.AddCustomContextEntries(from, list);
}
public virtual string[] GetPossibleDestinations() => Core.ML ? m_MLTownNames : m_TownNames;
public virtual string PickRandomDestination()
{
if (Map.Felucca.Regions.Count == 0 || Map == null || Map == Map.Internal || Location == Point3D.Zero)
return null; // Not yet fully initialized
string[] possible = GetPossibleDestinations();
string picked = null;
while (picked == null)
{
picked = possible.RandomElement();
EDI test = EDI.Find(picked);
if (test.Contains(Location))
picked = null;
}
return picked;
}
public EDI GetDestination()
{
if (MLQuestSystem.Enabled)
return null;
if (m_DestinationString == null && m_DeleteTimer == null)
m_DestinationString = PickRandomDestination();
if (m_Destination != null && m_Destination.Name == m_DestinationString)
return m_Destination;
if (Map.Felucca.Regions.Count > 0)
return m_Destination = EDI.Find(m_DestinationString);
return m_Destination = null;
}
private class DeleteTimer : Timer
{
private readonly Mobile m_Mobile;
public DeleteTimer(Mobile m, TimeSpan delay)
: base(delay)
{
m_Mobile = m;
Priority = TimerPriority.OneSecond;
}
protected override void OnTick()
{
m_Mobile.Delete();
}
}
}
public class EscortDestinationInfo
{
private static Dictionary<string, EscortDestinationInfo> m_Table;
public EscortDestinationInfo(string name, Region region)
{
Name = name;
Region = region;
}
public string Name { get; }
public Region Region { get; }
public bool Contains(Point3D p) => Region.Contains(p);
public static void LoadTable()
{
ICollection list = Map.Felucca.Regions.Values;
if (list.Count == 0)
return;
m_Table = new Dictionary<string, EscortDestinationInfo>();
foreach (Region r in list)
if (r.Name != null && (r is DungeonRegion || r is TownRegion))
m_Table[r.Name] = new EscortDestinationInfo(r.Name, r);
}
public static EDI Find(string name)
{
if (m_Table == null)
LoadTable();
if (name == null || m_Table == null)
return null;
m_Table.TryGetValue(name, out EscortDestinationInfo info);
return info;
}
}
public class AskDestinationEntry : ContextMenuEntry
{
private readonly Mobile m_From;
private readonly BaseEscortable m_Mobile;
public AskDestinationEntry(BaseEscortable m, Mobile from)
: base(6100, 3)
{
m_Mobile = m;
m_From = from;
}
public override void OnClick()
{
m_Mobile.SayDestinationTo(m_From);
}
}
public class AcceptEscortEntry : ContextMenuEntry
{
private readonly Mobile m_From;
private readonly BaseEscortable m_Mobile;
public AcceptEscortEntry(BaseEscortable m, Mobile from)
: base(6101, 3)
{
m_Mobile = m;
m_From = from;
}
public override void OnClick()
{
m_Mobile.AcceptEscorter(m_From);
}
}
public class AbandonEscortEntry : ContextMenuEntry
{
private readonly BaseEscortable m_Mobile;
public AbandonEscortEntry(BaseEscortable m)
: base(6102, 3) =>
m_Mobile = m;
public override void OnClick()
{
m_Mobile.Delete(); // OSI just seems to delete instantly
}
}
}