From 51ecee6caa043b4c28303eab7b1639c5ef7610cf Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 27 Jul 2023 21:44:06 -0700 Subject: [PATCH] fix: Overhauls champion titles & codegen champion system (#1430) ## MAJOR CHANGE Added a champion title system to facilitate the existing champion titles. This should make it easier to extend or create other related game content. Champion titles will be saved in a folder called _ChampionTitles_. ### Motivation The motivation to refactor was two-folder, but mostly related to performance in two ways. First, every player had a ChampionTitleInfo object with an array of ChamptionTitleInfo. We want to eliminate the need for this information unless a player actually uses it. This should save a considerable amount of memory. Second, to facilitate the atrophy mechanic, the champion titles would run atrophy post-world save, adding to the time that the server is frozen. Eliminating this post-world save side effect unlocks our ability to further optimize the world save process since there are no direct side effects. ### Bugs fixed - [X] Fixed titles getting cut off on the paperdoll - [X] Fixed champion title not displaying overhead (OPL) ### Screenshots image --- Projects/Server/Guild.cs | 3 - Projects/Server/IEntity.cs | 6 - Projects/Server/Items/Item.cs | 6 - Projects/Server/Main.cs | 2 - Projects/Server/Mobiles/Mobile.cs | 6 - .../Serialization/GenericPersistence.cs | 3 +- .../Server/Serialization/ISerializable.cs | 13 - Projects/Server/Serialization/Persistence.cs | 9 - Projects/Server/World/World.cs | 14 +- Projects/UOContent/Accounting/Account.cs | 6 - .../Engines/CannedEvil/ChampionAltar.cs | 63 +- .../Engines/CannedEvil/ChampionPlatform.cs | 119 +- .../Engines/CannedEvil/ChampionSkull.cs | 103 +- .../CannedEvil/ChampionSkullBrazier.cs | 266 +- .../CannedEvil/ChampionSkullPlatform.cs | 197 +- .../Engines/CannedEvil/ChampionSpawn.cs | 2776 ++++++++--------- .../Engines/CannedEvil/ChampionSpawnInfo.cs | 150 + .../Engines/CannedEvil/ChampionSpawnType.cs | 134 - .../Engines/CannedEvil/ChampionTitle.cs | 30 + .../CannedEvil/ChampionTitleContext.cs | 379 +++ .../Engines/CannedEvil/ChampionTitleSystem.cs | 172 + .../CannedEvil/DungeonChampionSpawn.cs | 52 +- .../Engines/CannedEvil/HarrowerGate.cs | 78 +- .../Engines/CannedEvil/LLChampionSpawn.cs | 54 +- .../Engines/CannedEvil/StarRoomGate.cs | 136 +- Projects/UOContent/Engines/Virtues/Valor.cs | 4 +- ...r.Engines.CannedEvil.ChampionAltar.v0.json | 11 + ...ngines.CannedEvil.ChampionPlatform.v0.json | 11 + ...es.CannedEvil.ChampionSkullBrazier.v1.json | 21 + ...s.CannedEvil.ChampionSkullPlatform.v0.json | 36 + ....Engines.CannedEvil.ChampionSpawn.v10.json | 186 ++ ...r.Engines.CannedEvil.ChampionTitle.v0.json | 22 + ...es.CannedEvil.ChampionTitleContext.v1.json | 95 + ...es.CannedEvil.DungeonChampionSpawn.v0.json | 4 + ...gines.CannedEvil.IdolOfTheChampion.v0.json | 11 + ...Engines.CannedEvil.LLChampionSpawn.v0.json | 4 + .../Server.Items.ChampionSkull.v2.json | 11 + .../Server.Items.HarrowerGate.v0.json | 11 + .../Server.Items.StarRoomGate.v1.json | 22 + Projects/UOContent/Misc/Guild.cs | 6 - Projects/UOContent/Misc/Titles.cs | 173 +- .../UOContent/Mobiles/ChampionTitleInfo.cs | 339 -- Projects/UOContent/Mobiles/PlayerMobile.cs | 66 +- .../UOContent/Mobiles/Special/Harrower.cs | 3 +- 44 files changed, 3028 insertions(+), 2785 deletions(-) create mode 100644 Projects/UOContent/Engines/CannedEvil/ChampionSpawnInfo.cs delete mode 100755 Projects/UOContent/Engines/CannedEvil/ChampionSpawnType.cs create mode 100644 Projects/UOContent/Engines/CannedEvil/ChampionTitle.cs create mode 100644 Projects/UOContent/Engines/CannedEvil/ChampionTitleContext.cs create mode 100644 Projects/UOContent/Engines/CannedEvil/ChampionTitleSystem.cs create mode 100644 Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionAltar.v0.json create mode 100644 Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionPlatform.v0.json create mode 100644 Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSkullBrazier.v1.json create mode 100644 Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSkullPlatform.v0.json create mode 100644 Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSpawn.v10.json create mode 100644 Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionTitle.v0.json create mode 100644 Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionTitleContext.v1.json create mode 100644 Projects/UOContent/Migrations/Server.Engines.CannedEvil.DungeonChampionSpawn.v0.json create mode 100644 Projects/UOContent/Migrations/Server.Engines.CannedEvil.IdolOfTheChampion.v0.json create mode 100644 Projects/UOContent/Migrations/Server.Engines.CannedEvil.LLChampionSpawn.v0.json create mode 100644 Projects/UOContent/Migrations/Server.Items.ChampionSkull.v2.json create mode 100644 Projects/UOContent/Migrations/Server.Items.HarrowerGate.v0.json create mode 100644 Projects/UOContent/Migrations/Server.Items.StarRoomGate.v1.json delete mode 100644 Projects/UOContent/Mobiles/ChampionTitleInfo.cs diff --git a/Projects/Server/Guild.cs b/Projects/Server/Guild.cs index eca1d305f..2732cf473 100644 --- a/Projects/Server/Guild.cs +++ b/Projects/Server/Guild.cs @@ -40,9 +40,6 @@ public abstract class BaseGuild : ISerializable public abstract GuildType Type { get; set; } public abstract bool Disbanded { get; } - public abstract bool ShouldExecuteAfterSerialize { get; } - public abstract void AfterSerialize(); - public abstract void Delete(); public bool Deleted => Disbanded; diff --git a/Projects/Server/IEntity.cs b/Projects/Server/IEntity.cs index f10c372ec..4825d7294 100644 --- a/Projects/Server/IEntity.cs +++ b/Projects/Server/IEntity.cs @@ -112,10 +112,4 @@ public class Entity : IEntity public void Serialize(IGenericWriter writer) { } - - public bool ShouldExecuteAfterSerialize => false; - - public void AfterSerialize() - { - } } diff --git a/Projects/Server/Items/Item.cs b/Projects/Server/Items/Item.cs index dfdc5e8f4..b319b65c4 100644 --- a/Projects/Server/Items/Item.cs +++ b/Projects/Server/Items/Item.cs @@ -1041,12 +1041,6 @@ public class Item : IHued, IComparable, ISpawnable, IObjectPropertyListEnt } } - public virtual bool ShouldExecuteAfterSerialize => false; - - public virtual void AfterSerialize() - { - } - public void MoveToWorld(WorldLocation worldLocation) { MoveToWorld(worldLocation.Location, worldLocation.Map); diff --git a/Projects/Server/Main.cs b/Projects/Server/Main.cs index 8b83355f8..cd60c0f6d 100644 --- a/Projects/Server/Main.cs +++ b/Projects/Server/Main.cs @@ -126,8 +126,6 @@ public static class Core private static long _tickCount; // Don't access this from other threads than the game thread. - // Persistence accesses this via AfterSerialize or Serialize in other threads, but the value is set and won't change - // since the game loop is frozen at that moment. private static DateTime _now; // For Unix Stopwatch.Frequency is normalized to 1ns diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index 1b1c12b9c..f7ae20bbe 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -2380,12 +2380,6 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro writer.Write((byte)m_IntLock); } - public virtual bool ShouldExecuteAfterSerialize => false; - - public virtual void AfterSerialize() - { - } - public bool Deleted { get; private set; } public virtual void Delete() diff --git a/Projects/Server/Serialization/GenericPersistence.cs b/Projects/Server/Serialization/GenericPersistence.cs index b567edba0..20ff8899f 100644 --- a/Projects/Server/Serialization/GenericPersistence.cs +++ b/Projects/Server/Serialization/GenericPersistence.cs @@ -25,7 +25,6 @@ public static class GenericPersistence string name, Action serializer, Action deserializer, - Action afterSerialize = null, int priority = Persistence.DefaultPriority ) { @@ -49,6 +48,6 @@ public static class GenericPersistence void Deserialize(string savePath, Dictionary typesDb) => AdhocPersistence.Deserialize(Path.Combine(savePath, name, $"{name}.bin"), deserializer); - Persistence.Register(name, Serialize, WriteSnapshot, Deserialize, afterSerialize, priority); + Persistence.Register(name, Serialize, WriteSnapshot, Deserialize, priority); } } diff --git a/Projects/Server/Serialization/ISerializable.cs b/Projects/Server/Serialization/ISerializable.cs index fce6c8af2..e569d435c 100644 --- a/Projects/Server/Serialization/ISerializable.cs +++ b/Projects/Server/Serialization/ISerializable.cs @@ -34,12 +34,6 @@ public interface ISerializable void Deserialize(IGenericReader reader); void Serialize(IGenericWriter writer); - // Determines if AfterSerialize should execute. This is checked on a worker thread. - bool ShouldExecuteAfterSerialize { get; } - - // Executes after serialization if ShouldExecuteAfterSerialize is true. This is run on the game thread synchronously. - void AfterSerialize(); - bool Deleted { get; } void Delete(); @@ -60,13 +54,6 @@ public interface ISerializable { SaveBuffer ??= new BufferWriter(true, types); - // Queue for post serialization if this entity has it enabled - // This will run AfterSerialize in the main game thread after the world is done saving - if (ShouldExecuteAfterSerialize) - { - World.EnqueueAfterSerialization(this); - } - // Clean, don't bother serializing if (SavePosition > -1) { diff --git a/Projects/Server/Serialization/Persistence.cs b/Projects/Server/Serialization/Persistence.cs index d8faf0aaf..c9aacd9cc 100644 --- a/Projects/Server/Serialization/Persistence.cs +++ b/Projects/Server/Serialization/Persistence.cs @@ -32,7 +32,6 @@ public static class Persistence Action serializer, Action snapshotWriter, Action> deserializer, - Action afterSerialize = null, int priority = DefaultPriority ) { @@ -42,7 +41,6 @@ public static class Persistence Name = name, Priority = priority, Serialize = serializer, - AfterSerialize = afterSerialize, WriteSnapshot = snapshotWriter, Deserialize = deserializer } @@ -91,12 +89,6 @@ public static class Persistence public static void Serialize() { Parallel.ForEach(_registry, entry => entry.Serialize()); - - // Synchronously run the AfterSerialize on the main game thread - foreach (var entry in _registry) - { - entry.AfterSerialize?.Invoke(); - } } public static void WriteSnapshot(string path, ConcurrentQueue types) @@ -141,7 +133,6 @@ public static class Persistence // Serializes to memory buffers and run in parallel public Action Serialize { get; init; } - public Action AfterSerialize { get; init; } public Action WriteSnapshot { get; init; } public Action> Deserialize { get; init; } } diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs index 6a5639fe8..8e3faa085 100644 --- a/Projects/Server/World/World.cs +++ b/Projects/Server/World/World.cs @@ -44,7 +44,6 @@ public static class World private static Dictionary _pendingAdd = new(); private static Dictionary _pendingDelete = new(); private static ConcurrentQueue _decayQueue = new(); - private static ConcurrentQueue _afterSerializeEntities = new(); private static string _tempSavePath; // Path to the temporary folder for the save private static bool _enableSaveStats; @@ -166,7 +165,7 @@ public static class World _enableSaveStats = ServerConfiguration.GetOrUpdateSetting("world.enableSaveStats", false); // Mobiles & Items - Persistence.Register("Mobiles & Items", SaveEntities, WriteEntities, LoadEntities, AfterSerialize, 1); + Persistence.Register("Mobiles & Items", SaveEntities, WriteEntities, LoadEntities, 1); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -187,9 +186,6 @@ public static class World _decayQueue.Enqueue(item); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void EnqueueAfterSerialization(ISerializable entity) => _afterSerializeEntities.Enqueue(entity); - public static void Broadcast(int hue, bool ascii, string text) { var length = OutgoingMessagePackets.GetMaxMessageLength(text); @@ -519,14 +515,6 @@ public static class World */ public static ConcurrentQueue SerializedTypes { get; } = new(); - private static void AfterSerialize() - { - while (_afterSerializeEntities.TryDequeue(out var entity)) - { - entity.AfterSerialize(); - } - } - private static void SaveEntities() { _serializationStart = DateTime.UtcNow; diff --git a/Projects/UOContent/Accounting/Account.cs b/Projects/UOContent/Accounting/Account.cs index b0fdfe9ee..88b6643f4 100644 --- a/Projects/UOContent/Accounting/Account.cs +++ b/Projects/UOContent/Accounting/Account.cs @@ -333,12 +333,6 @@ namespace Server.Accounting } } - public bool ShouldExecuteAfterSerialize => false; - - public void AfterSerialize() - { - } - /// /// Deletes the account, all characters of the account, and all houses of those characters /// diff --git a/Projects/UOContent/Engines/CannedEvil/ChampionAltar.cs b/Projects/UOContent/Engines/CannedEvil/ChampionAltar.cs index 3e41ff33c..40d20ac81 100755 --- a/Projects/UOContent/Engines/CannedEvil/ChampionAltar.cs +++ b/Projects/UOContent/Engines/CannedEvil/ChampionAltar.cs @@ -13,56 +13,31 @@ * along with this program. If not, see . * *************************************************************************/ +using ModernUO.Serialization; using Server.Items; -namespace Server.Engines.CannedEvil +namespace Server.Engines.CannedEvil; + +[SerializationGenerator(0, false)] +public partial class ChampionAltar : PentagramAddon { - public class ChampionAltar : PentagramAddon + [SerializableField(0, setter: "private")] + private ChampionSpawn _spawn; + + public ChampionAltar(ChampionSpawn spawn) => _spawn = spawn; + + public override void OnAfterDelete() { - public ChampionSpawn Spawn { get; private set; } + base.OnAfterDelete(); + Spawn?.Delete(); + } - public ChampionAltar(ChampionSpawn spawn) => Spawn = spawn; - - public override void OnAfterDelete() + [AfterDeserialization(false)] + private void AfterDeserialization() + { + if (Spawn == null) { - base.OnAfterDelete(); - - Spawn?.Delete(); - } - - public ChampionAltar(Serial serial) : base(serial) - { - } - - public override void Serialize(IGenericWriter writer) - { - base.Serialize(writer); - - writer.Write(0); // version - - writer.Write(Spawn); - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - - var version = reader.ReadInt(); - - switch (version) - { - case 0: - { - Spawn = reader.ReadEntity(); - - if (Spawn == null) - { - Delete(); - } - - break; - } - } + Delete(); } } } diff --git a/Projects/UOContent/Engines/CannedEvil/ChampionPlatform.cs b/Projects/UOContent/Engines/CannedEvil/ChampionPlatform.cs index 0ef33dda8..7212e8fc7 100755 --- a/Projects/UOContent/Engines/CannedEvil/ChampionPlatform.cs +++ b/Projects/UOContent/Engines/CannedEvil/ChampionPlatform.cs @@ -13,95 +13,70 @@ * along with this program. If not, see . * *************************************************************************/ +using ModernUO.Serialization; using Server.Items; -namespace Server.Engines.CannedEvil +namespace Server.Engines.CannedEvil; + +[SerializationGenerator(0, false)] +public partial class ChampionPlatform : BaseAddon { - public class ChampionPlatform : BaseAddon + [SerializableField(0, setter: "private")] + private ChampionSpawn _spawn; + + public ChampionPlatform(ChampionSpawn spawn) { - public ChampionSpawn Spawn { get; private set; } + _spawn = spawn; - public ChampionPlatform(ChampionSpawn spawn) + for (var x = -2; x <= 2; ++x) { - Spawn = spawn; - - for (var x = -2; x <= 2; ++x) + for (var y = -2; y <= 2; ++y) { - for (var y = -2; y <= 2; ++y) - { - AddComponent(0x750, x, y, -5); - } + AddComponent(0x750, x, y, -5); } + } - for (var x = -1; x <= 1; ++x) + for (var x = -1; x <= 1; ++x) + { + for (var y = -1; y <= 1; ++y) { - for (var y = -1; y <= 1; ++y) - { - AddComponent(0x750, x, y, 0); - } + AddComponent(0x750, x, y, 0); } - - for (var i = -1; i <= 1; ++i) - { - AddComponent(0x751, i, 2, 0); - AddComponent(0x752, 2, i, 0); - - AddComponent(0x753, i, -2, 0); - AddComponent(0x754, -2, i, 0); - } - - AddComponent(0x759, -2, -2, 0); - AddComponent(0x75A, 2, 2, 0); - AddComponent(0x75B, -2, 2, 0); - AddComponent(0x75C, 2, -2, 0); } - public void AddComponent(int id, int x, int y, int z) + for (var i = -1; i <= 1; ++i) { - AddonComponent ac = new AddonComponent(id) { Hue = 0x497 }; - AddComponent(ac, x, y, z); + AddComponent(0x751, i, 2, 0); + AddComponent(0x752, 2, i, 0); + + AddComponent(0x753, i, -2, 0); + AddComponent(0x754, -2, i, 0); } - public override void OnAfterDelete() + AddComponent(0x759, -2, -2, 0); + AddComponent(0x75A, 2, 2, 0); + AddComponent(0x75B, -2, 2, 0); + AddComponent(0x75C, 2, -2, 0); + } + + public void AddComponent(int id, int x, int y, int z) + { + AddonComponent ac = new AddonComponent(id) { Hue = 0x497 }; + AddComponent(ac, x, y, z); + } + + public override void OnAfterDelete() + { + base.OnAfterDelete(); + Spawn?.Delete(); + } + + [AfterDeserialization(false)] + private void AfterDeserialization() + { + if (Spawn == null) { - base.OnAfterDelete(); - - Spawn?.Delete(); - } - - public ChampionPlatform(Serial serial) : base(serial) - { - } - - public override void Serialize(IGenericWriter writer) - { - base.Serialize(writer); - - writer.Write(0); // version - - writer.Write(Spawn); - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - - var version = reader.ReadInt(); - - switch (version) - { - case 0: - { - Spawn = reader.ReadEntity(); - - if (Spawn == null) - { - Delete(); - } - - break; - } - } + Delete(); } } } diff --git a/Projects/UOContent/Engines/CannedEvil/ChampionSkull.cs b/Projects/UOContent/Engines/CannedEvil/ChampionSkull.cs index 72e634e8f..9834a40c1 100755 --- a/Projects/UOContent/Engines/CannedEvil/ChampionSkull.cs +++ b/Projects/UOContent/Engines/CannedEvil/ChampionSkull.cs @@ -13,86 +13,41 @@ * along with this program. If not, see . * *************************************************************************/ +using ModernUO.Serialization; using Server.Engines.CannedEvil; -namespace Server.Items +namespace Server.Items; + +[SerializationGenerator(2, false)] +public partial class ChampionSkull : Item { - public class ChampionSkull : Item + [InvalidateProperties] + [SerializableField(0)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private ChampionSkullType _type; + + public override int LabelNumber => 1049479 + (int)_type; + + [Constructible] + public ChampionSkull(ChampionSkullType type) : base(0x1AE1) { - private ChampionSkullType m_Type; + _type = type; + LootType = LootType.Cursed; - [CommandProperty(AccessLevel.GameMaster)] - public ChampionSkullType Type + // TODO: All hue values + Hue = type switch { - get => m_Type; - set - { - m_Type = value; - InvalidateProperties(); - } - } + ChampionSkullType.Power => 0x159, + ChampionSkullType.Venom => 0x172, + ChampionSkullType.Greed => 0x1EE, + ChampionSkullType.Death => 0x025, + ChampionSkullType.Pain => 0x035, + _ => Hue + }; + } - public override int LabelNumber => 1049479 + (int)m_Type; - - [Constructible] - public ChampionSkull(ChampionSkullType type) : base(0x1AE1) - { - m_Type = type; - LootType = LootType.Cursed; - - // TODO: All hue values - Hue = type switch - { - ChampionSkullType.Power => 0x159, - ChampionSkullType.Venom => 0x172, - ChampionSkullType.Greed => 0x1EE, - ChampionSkullType.Death => 0x025, - ChampionSkullType.Pain => 0x035, - _ => Hue - }; - } - - public ChampionSkull(Serial serial) : base(serial) - { - } - - public override void Serialize(IGenericWriter writer) - { - base.Serialize(writer); - - writer.Write(1); // version - - writer.Write((int)m_Type); - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - - var version = reader.ReadInt(); - - switch (version) - { - case 1: - case 0: - { - m_Type = (ChampionSkullType)reader.ReadInt(); - break; - } - } - - if (version == 0) - { - if (LootType != LootType.Cursed) - { - LootType = LootType.Cursed; - } - - if (Insured) - { - Insured = false; - } - } - } + private void Deserialize(IGenericReader reader, int version) + { + _type = (ChampionSkullType)reader.ReadInt(); } } diff --git a/Projects/UOContent/Engines/CannedEvil/ChampionSkullBrazier.cs b/Projects/UOContent/Engines/CannedEvil/ChampionSkullBrazier.cs index ed0db3277..39b1c5fe5 100755 --- a/Projects/UOContent/Engines/CannedEvil/ChampionSkullBrazier.cs +++ b/Projects/UOContent/Engines/CannedEvil/ChampionSkullBrazier.cs @@ -13,194 +13,156 @@ * along with this program. If not, see . * *************************************************************************/ +using ModernUO.Serialization; using Server.Items; using Server.Targeting; using Server.Mobiles; -namespace Server.Engines.CannedEvil +namespace Server.Engines.CannedEvil; + +[SerializationGenerator(1, false)] +public partial class ChampionSkullBrazier : AddonComponent { - public class ChampionSkullBrazier : AddonComponent + [InvalidateProperties] + [SerializableField(0)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private ChampionSkullType _type; + + [InvalidateProperties] + [SerializableField(1)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private ChampionSkullPlatform _platform; + + [SerializableProperty(2)] + [CommandProperty(AccessLevel.GameMaster)] + public Item Skull { - private ChampionSkullType m_Type; - private Item m_Skull; - - [CommandProperty(AccessLevel.GameMaster)] - public ChampionSkullPlatform Platform { get; private set; } - - [CommandProperty(AccessLevel.GameMaster)] - public ChampionSkullType Type + get => _skull; + set { - get => m_Type; - set - { - m_Type = value; - InvalidateProperties(); - } + _skull = value; + this.MarkDirty(); + _platform?.Validate(); + } + } + + public override int LabelNumber => 1049489 + (int)_type; + + public ChampionSkullBrazier(ChampionSkullPlatform platform, ChampionSkullType type) : base(0x19BB) + { + Hue = 0x455; + Light = LightType.Circle300; + + _platform = platform; + _type = type; + } + + public override void OnDoubleClick(Mobile from) + { + _platform?.Validate(); + BeginSacrifice(from); + } + + public void BeginSacrifice(Mobile from) + { + if (Deleted) + { + return; } - [CommandProperty(AccessLevel.GameMaster)] - public Item Skull + if (_skull is { Deleted: true }) { - get => m_Skull; - set - { - m_Skull = value; - Platform?.Validate(); - } + Skull = null; } - public override int LabelNumber => 1049489 + (int)m_Type; - - public ChampionSkullBrazier(ChampionSkullPlatform platform, ChampionSkullType type) : base(0x19BB) + if (from.Map != Map || !from.InRange(GetWorldLocation(), 3)) { - Hue = 0x455; - Light = LightType.Circle300; + from.SendLocalizedMessage(500446); // That is too far away. + } + else if (!Harrower.CanSpawn) + { + from.SendMessage("The harrower has already been spawned."); + } + else if (_skull == null) + { + from.SendLocalizedMessage(1049485); // What would you like to sacrifice? + from.Target = new SacrificeTarget(this); + } + else + { + SendLocalizedMessageTo(from, 1049487); // I already have my champions awakening skull! + } + } - Platform = platform; - m_Type = type; + public void EndSacrifice(Mobile from, ChampionSkull skull) + { + if (Deleted) + { + return; } - public ChampionSkullBrazier(Serial serial) : base(serial) + if (_skull is { Deleted: true }) { + Skull = null; } - public override void OnDoubleClick(Mobile from) + if (from.Map != Map || !from.InRange(GetWorldLocation(), 3)) { - Platform?.Validate(); - - BeginSacrifice(from); + from.SendLocalizedMessage(500446); // That is too far away. } - - public void BeginSacrifice(Mobile from) + else if (!Harrower.CanSpawn) { - if (Deleted) - { - return; - } - - if (m_Skull is { Deleted: true }) - { - Skull = null; - } - - if (from.Map != Map || !from.InRange(GetWorldLocation(), 3)) - { - from.SendLocalizedMessage(500446); // That is too far away. - } - else if (!Harrower.CanSpawn) - { - from.SendMessage("The harrower has already been spawned."); - } - else if (m_Skull == null) - { - from.SendLocalizedMessage(1049485); // What would you like to sacrifice? - from.Target = new SacrificeTarget(this); - } - else - { - SendLocalizedMessageTo(from, 1049487); // I already have my champions awakening skull! - } + from.SendMessage("The harrower has already been spawned."); } - - public void EndSacrifice(Mobile from, ChampionSkull skull) + else if (skull == null) { - if (Deleted) - { - return; - } - - if (m_Skull is { Deleted: true }) - { - Skull = null; - } - - if (from.Map != Map || !from.InRange(GetWorldLocation(), 3)) - { - from.SendLocalizedMessage(500446); // That is too far away. - } - else if (!Harrower.CanSpawn) - { - from.SendMessage("The harrower has already been spawned."); - } - else if (skull == null) - { - SendLocalizedMessageTo(from, 1049488); // That is not my champions awakening skull! - } - else if (m_Skull != null) - { - SendLocalizedMessageTo(from, 1049487); // I already have my champions awakening skull! - } - else if (!skull.IsChildOf(from.Backpack)) - { - from.SendLocalizedMessage(1049486); // You can only sacrifice items that are in your backpack! - } - else if (skull.Type == Type) - { - skull.Movable = false; - skull.MoveToWorld(GetWorldTop(), Map); - - Skull = skull; - } - else - { - SendLocalizedMessageTo(from, 1049488); // That is not my champions awakening skull! - } + SendLocalizedMessageTo(from, 1049488); // That is not my champions awakening skull! } - - private class SacrificeTarget : Target + else if (_skull != null) { - private readonly ChampionSkullBrazier m_Brazier; - - public SacrificeTarget(ChampionSkullBrazier brazier) : base(12, false, TargetFlags.None) => - m_Brazier = brazier; - - protected override void OnTarget(Mobile from, object targeted) => - m_Brazier.EndSacrifice(from, targeted as ChampionSkull); + SendLocalizedMessageTo(from, 1049487); // I already have my champions awakening skull! } - - public override void Serialize(IGenericWriter writer) + else if (!skull.IsChildOf(from.Backpack)) { - base.Serialize(writer); - - writer.Write(0); // version - - writer.Write((int)m_Type); - writer.Write(Platform); - writer.Write(m_Skull); + from.SendLocalizedMessage(1049486); // You can only sacrifice items that are in your backpack! } - - public override void Deserialize(IGenericReader reader) + else if (skull.Type == Type) { - base.Deserialize(reader); + skull.Movable = false; + skull.MoveToWorld(GetWorldTop(), Map); - var version = reader.ReadInt(); + Skull = skull; + } + else + { + SendLocalizedMessageTo(from, 1049488); // That is not my champions awakening skull! + } + } - switch (version) - { - case 0: - { - m_Type = (ChampionSkullType)reader.ReadInt(); - Platform = reader.ReadEntity(); - m_Skull = reader.ReadEntity(); + private class SacrificeTarget : Target + { + private readonly ChampionSkullBrazier _brazier; - if (Platform == null) - { - Delete(); - } + public SacrificeTarget(ChampionSkullBrazier brazier) : base(12, false, TargetFlags.None) => + _brazier = brazier; - break; - } - } + protected override void OnTarget(Mobile from, object targeted) => + _brazier.EndSacrifice(from, targeted as ChampionSkull); + } - if (Hue == 0x497) - { - Hue = 0x455; - } + private void Deserialize(IGenericReader reader, int version) + { + _type = (ChampionSkullType)reader.ReadInt(); + _platform = reader.ReadEntity(); + _skull = reader.ReadEntity(); + } - if (Light != LightType.Circle300) - { - Light = LightType.Circle300; - } + [AfterDeserialization(false)] + private void AfterDeserialization() + { + if (_platform == null) + { + Delete(); } } } diff --git a/Projects/UOContent/Engines/CannedEvil/ChampionSkullPlatform.cs b/Projects/UOContent/Engines/CannedEvil/ChampionSkullPlatform.cs index c60f6747d..fb4f51c4f 100755 --- a/Projects/UOContent/Engines/CannedEvil/ChampionSkullPlatform.cs +++ b/Projects/UOContent/Engines/CannedEvil/ChampionSkullPlatform.cs @@ -13,127 +13,104 @@ * along with this program. If not, see . * *************************************************************************/ +using ModernUO.Serialization; using Server.Items; using Server.Mobiles; -namespace Server.Engines.CannedEvil +namespace Server.Engines.CannedEvil; + +[SerializationGenerator(0, false)] +public partial class ChampionSkullPlatform : BaseAddon { - public class ChampionSkullPlatform : BaseAddon + [SerializableField(0)] + private ChampionSkullBrazier _power; + + [SerializableField(1)] + private ChampionSkullBrazier _enlightenment; + + [SerializableField(2)] + private ChampionSkullBrazier _venom; + + [SerializableField(3)] + private ChampionSkullBrazier _pain; + + [SerializableField(4)] + private ChampionSkullBrazier _greed; + + [SerializableField(5)] + private ChampionSkullBrazier _death; + + [Constructible] + public ChampionSkullPlatform() { - private ChampionSkullBrazier m_Power, m_Enlightenment, m_Venom, m_Pain, m_Greed, m_Death; + AddComponent(new AddonComponent(0x71A), -1, -1, -1); + AddComponent(new AddonComponent(0x709), 0, -1, -1); + AddComponent(new AddonComponent(0x709), 1, -1, -1); + AddComponent(new AddonComponent(0x709), -1, 0, -1); + AddComponent(new AddonComponent(0x709), 0, 0, -1); + AddComponent(new AddonComponent(0x709), 1, 0, -1); + AddComponent(new AddonComponent(0x709), -1, 1, -1); + AddComponent(new AddonComponent(0x709), 0, 1, -1); + AddComponent(new AddonComponent(0x71B), 1, 1, -1); - [Constructible] - public ChampionSkullPlatform() + AddComponent(new AddonComponent(0x50F), 0, -1, 4); + AddComponent(_power = new ChampionSkullBrazier(this, ChampionSkullType.Power), 0, -1, 5); + + AddComponent(new AddonComponent(0x50F), 1, -1, 4); + AddComponent(_enlightenment = new ChampionSkullBrazier(this, ChampionSkullType.Enlightenment), 1, -1, 5); + + AddComponent(new AddonComponent(0x50F), -1, 0, 4); + AddComponent(_venom = new ChampionSkullBrazier(this, ChampionSkullType.Venom), -1, 0, 5); + + AddComponent(new AddonComponent(0x50F), 1, 0, 4); + AddComponent(_pain = new ChampionSkullBrazier(this, ChampionSkullType.Pain), 1, 0, 5); + + AddComponent(new AddonComponent(0x50F), -1, 1, 4); + AddComponent(_greed = new ChampionSkullBrazier(this, ChampionSkullType.Greed), -1, 1, 5); + + AddComponent(new AddonComponent(0x50F), 0, 1, 4); + AddComponent(_death = new ChampionSkullBrazier(this, ChampionSkullType.Death), 0, 1, 5); + + AddonComponent comp = new LocalizedAddonComponent(0x20D2, 1049495) { Hue = 0x482 }; + AddComponent(comp, 0, 0, 5); + + comp = new LocalizedAddonComponent(0x0BCF, 1049496) { Hue = 0x482 }; + AddComponent(comp, 0, 2, -7); + + comp = new LocalizedAddonComponent(0x0BD0, 1049497) { Hue = 0x482 }; + AddComponent(comp, 2, 0, -7); + } + + public void Validate() + { + if (Validate(_power) && Validate(_enlightenment) && Validate(_venom) && Validate(_pain) && + Validate(_greed) && Validate(_death)) { - AddComponent(new AddonComponent(0x71A), -1, -1, -1); - AddComponent(new AddonComponent(0x709), 0, -1, -1); - AddComponent(new AddonComponent(0x709), 1, -1, -1); - AddComponent(new AddonComponent(0x709), -1, 0, -1); - AddComponent(new AddonComponent(0x709), 0, 0, -1); - AddComponent(new AddonComponent(0x709), 1, 0, -1); - AddComponent(new AddonComponent(0x709), -1, 1, -1); - AddComponent(new AddonComponent(0x709), 0, 1, -1); - AddComponent(new AddonComponent(0x71B), 1, 1, -1); + Mobile harrower = Harrower.Spawn(new Point3D(X, Y, Z + 6), Map); - AddComponent(new AddonComponent(0x50F), 0, -1, 4); - AddComponent(m_Power = new ChampionSkullBrazier(this, ChampionSkullType.Power), 0, -1, 5); - - AddComponent(new AddonComponent(0x50F), 1, -1, 4); - AddComponent(m_Enlightenment = new ChampionSkullBrazier(this, ChampionSkullType.Enlightenment), 1, -1, 5); - - AddComponent(new AddonComponent(0x50F), -1, 0, 4); - AddComponent(m_Venom = new ChampionSkullBrazier(this, ChampionSkullType.Venom), -1, 0, 5); - - AddComponent(new AddonComponent(0x50F), 1, 0, 4); - AddComponent(m_Pain = new ChampionSkullBrazier(this, ChampionSkullType.Pain), 1, 0, 5); - - AddComponent(new AddonComponent(0x50F), -1, 1, 4); - AddComponent(m_Greed = new ChampionSkullBrazier(this, ChampionSkullType.Greed), -1, 1, 5); - - AddComponent(new AddonComponent(0x50F), 0, 1, 4); - AddComponent(m_Death = new ChampionSkullBrazier(this, ChampionSkullType.Death), 0, 1, 5); - - AddonComponent comp = new LocalizedAddonComponent(0x20D2, 1049495) { Hue = 0x482 }; - AddComponent(comp, 0, 0, 5); - - comp = new LocalizedAddonComponent(0x0BCF, 1049496) { Hue = 0x482 }; - AddComponent(comp, 0, 2, -7); - - comp = new LocalizedAddonComponent(0x0BD0, 1049497) { Hue = 0x482 }; - AddComponent(comp, 2, 0, -7); - } - - public void Validate() - { - if (Validate(m_Power) && Validate(m_Enlightenment) && Validate(m_Venom) && Validate(m_Pain) && - Validate(m_Greed) && Validate(m_Death)) + if (harrower == null) { - Mobile harrower = Harrower.Spawn(new Point3D(X, Y, Z + 6), Map); - - if (harrower == null) - { - return; - } - - Clear(m_Power); - Clear(m_Enlightenment); - Clear(m_Venom); - Clear(m_Pain); - Clear(m_Greed); - Clear(m_Death); + return; } - } - public void Clear(ChampionSkullBrazier brazier) - { - if (brazier != null) - { - Effects.SendBoltEffect(brazier); - - brazier.Skull?.Delete(); - } - } - - public bool Validate(ChampionSkullBrazier brazier) => brazier is { Skull: { Deleted: false } }; - - public ChampionSkullPlatform(Serial serial) : base(serial) - { - } - - public override void Serialize(IGenericWriter writer) - { - base.Serialize(writer); - - writer.Write(0); // version - - writer.Write(m_Power); - writer.Write(m_Enlightenment); - writer.Write(m_Venom); - writer.Write(m_Pain); - writer.Write(m_Greed); - writer.Write(m_Death); - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - - var version = reader.ReadInt(); - - switch (version) - { - case 0: - { - m_Power = reader.ReadEntity(); - m_Enlightenment = reader.ReadEntity(); - m_Venom = reader.ReadEntity(); - m_Pain = reader.ReadEntity(); - m_Greed = reader.ReadEntity(); - m_Death = reader.ReadEntity(); - - break; - } - } + Clear(_power); + Clear(_enlightenment); + Clear(_venom); + Clear(_pain); + Clear(_greed); + Clear(_death); } } + + public static void Clear(ChampionSkullBrazier brazier) + { + if (brazier != null) + { + Effects.SendBoltEffect(brazier); + + brazier.Skull?.Delete(); + } + } + + public static bool Validate(ChampionSkullBrazier brazier) => brazier is { Skull: { Deleted: false } }; } diff --git a/Projects/UOContent/Engines/CannedEvil/ChampionSpawn.cs b/Projects/UOContent/Engines/CannedEvil/ChampionSpawn.cs index 36f830c8c..9e95df3b7 100755 --- a/Projects/UOContent/Engines/CannedEvil/ChampionSpawn.cs +++ b/Projects/UOContent/Engines/CannedEvil/ChampionSpawn.cs @@ -16,6 +16,8 @@ using System; using System.Net; using System.Collections.Generic; +using System.Runtime.InteropServices; +using ModernUO.Serialization; using Server.Engines.Virtues; using Server.Gumps; using Server.Items; @@ -24,1621 +26,1497 @@ using Server.Regions; using Server.Logging; using Server.Utilities; -namespace Server.Engines.CannedEvil +namespace Server.Engines.CannedEvil; + +[SerializationGenerator(10, false)] +public partial class ChampionSpawn : Item { - public class ChampionSpawn : Item + private static readonly ILogger logger = LogFactory.GetLogger(typeof(ChampionSpawn)); + + [SerializableField(1)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private bool _activatedByProximity; + + [DeltaDateTime] + [SerializableField(2)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private DateTime _nextProximityTime; + + [SerializableField(4)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private bool _activatedByValor; + + [SerializableField(5)] + private Dictionary_damageEntries; + + [SerializableField(6)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private bool _confinedRoaming; + + [SerializableField(7)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private IdolOfTheChampion _idol; + + [SerializableField(8)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private bool _hasBeenAdvanced; + + [SerializableField(10)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private bool _randomizeType; + + [InvalidateProperties] + [SerializableField(13)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private ChampionSpawnType _type; + + [SerializableField(14)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private List _creatures; + + [SerializableField(15)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private List _redSkulls; + + [SerializableField(16)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private List _whiteSkulls; + + [SerializableField(17)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private ChampionPlatform _platform; + + [SerializableField(18)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private ChampionAltar _altar; + + [SerializableField(19)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private TimeSpan _expireDelay; + + [DeltaDateTime] + [SerializableField(20)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private DateTime _expireTime; + + [SerializableField(21)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private Mobile _champion; + + [SerializableField(22)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private TimeSpan _restartDelay; + + [DeltaDateTime] + [SerializableField(23, setter: "private")] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private DateTime _restartTime; + + private ChampionSpawnRegion m_Region; + + //Goes back each level, below level 0 and it goes off! + + private TimerExecutionToken _timerToken; + private TimerExecutionToken _restartTimerToken; + + public virtual string BroadcastMessage => "The Champion has sensed your presence! Beware its wrath!"; + public virtual bool ProximitySpawn => false; + public virtual bool AlwaysActive => false; + + public override TimeSpan DecayTime => TimeSpan.FromSeconds(180.0); + + public virtual bool HasStarRoomGate => true; + + [CommandProperty(AccessLevel.GameMaster)] + public Point3D EjectLocation { get; set; } + + [CommandProperty(AccessLevel.GameMaster)] + public Map EjectMap { get; set; } + + public override int LabelNumber => 1041030; // Evil in a Can: Don't delete me! + + [Constructible] + public ChampionSpawn() : base(0xBD2) { - private static readonly ILogger logger = LogFactory.GetLogger(typeof(ChampionSpawn)); - private bool m_Active; - private ChampionSpawnType m_Type; - private List m_Creatures; - private List m_RedSkulls; - private List m_WhiteSkulls; - private ChampionPlatform m_Platform; - private ChampionAltar m_Altar; - private int m_Kills; - private int m_MaxLevel; - private int m_Level; + Movable = false; + Visible = false; - //private int m_SpawnRange; - private Rectangle2D m_SpawnArea; - private ChampionSpawnRegion m_Region; + _creatures = new List(); + _redSkulls = new List(); + _whiteSkulls = new List(); - //Goes back each level, below level 0 and it goes off! + _platform = new ChampionPlatform(this); + _altar = new ChampionAltar(this); + _idol = new IdolOfTheChampion(this); - private TimerExecutionToken _timerToken; + ExpireDelay = TimeSpan.FromMinutes(30.0); + RestartDelay = TimeSpan.FromMinutes(30.0); + _damageEntries = new Dictionary(); - private IdolOfTheChampion m_Idol; - private TimerExecutionToken _restartTimerToken; + Timer.StartTimer(TimeSpan.Zero, SetInitialSpawnArea); + } - public virtual string BroadcastMessage => "The Champion has sensed your presence! Beware its wrath!"; - public virtual bool ProximitySpawn => false; - public virtual bool CanAdvanceByValor => true; - public virtual bool CanActivateByValor => true; - public virtual bool AlwaysActive => false; + public void SetInitialSpawnArea() + { + //Previous default used to be 24; + SpawnArea = new Rectangle2D(new Point2D(X - 24, Y - 24), new Point2D(X + 24, Y + 24)); + } - public override TimeSpan DecayTime => TimeSpan.FromSeconds(180.0); + public virtual ChampionSpawnRegion GetRegion() => new(this); - public virtual bool HasStarRoomGate => true; + public void UpdateRegion() + { + m_Region?.Unregister(); - public Dictionary DamageEntries { get; private set; } - - [CommandProperty(AccessLevel.GameMaster)] - public bool ConfinedRoaming { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public bool HasBeenAdvanced { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public Point3D EjectLocation { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public Map EjectMap { get; set; } - - public override int LabelNumber => 1041030; // Evil in a Can: Don't delete me! - - [Constructible] - public ChampionSpawn() : base(0xBD2) + if (!Deleted && Map != Map.Internal) { - Movable = false; - Visible = false; - - m_Creatures = new List(); - m_RedSkulls = new List(); - m_WhiteSkulls = new List(); - - m_Platform = new ChampionPlatform(this); - m_Altar = new ChampionAltar(this); - m_Idol = new IdolOfTheChampion(this); - - ExpireDelay = TimeSpan.FromMinutes(30.0); - RestartDelay = TimeSpan.FromMinutes(30.0); - DamageEntries = new Dictionary(); - - Timer.StartTimer(TimeSpan.Zero, SetInitialSpawnArea); + m_Region = GetRegion(); + m_Region.Register(); } + } - public void SetInitialSpawnArea() + [SerializableProperty(0)] + [CommandProperty(AccessLevel.GameMaster)] + public int Level + { + get => _level; + set { - //Previous default used to be 24; - SpawnArea = new Rectangle2D(new Point2D(X - 24, Y - 24), new Point2D(X + 24, Y + 24)); + for (int i = _redSkulls.Count - 1; i >= value; --i) + { + _redSkulls[i].Delete(); + _redSkulls.RemoveAt(i); + } + + for (int i = _redSkulls.Count; i < Math.Min(value, 16); ++i) + { + Item skull = new Item(0x1854) { Hue = 0x26, Movable = false, Light = LightType.Circle150 }; + skull.MoveToWorld(GetRedSkullLocation(i), Map); + _redSkulls.Add(skull); + } + + _level = value; + this.MarkDirty(); + InvalidateProperties(); } + } - public virtual ChampionSpawnRegion GetRegion() => new(this); + [SerializableProperty(3)] + [CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)] + public int MaxLevel + { + get => _maxLevel; + set => _maxLevel = Math.Clamp(value, 0, 18); + } - public void UpdateRegion() + [SerializableProperty(9)] + [CommandProperty(AccessLevel.GameMaster)] + public Rectangle2D SpawnArea + { + get => _spawnArea; + set { - m_Region?.Unregister(); - - if (!Deleted && Map != Map.Internal) - { - m_Region = GetRegion(); - m_Region.Register(); - } + _spawnArea = value; + this.MarkDirty(); + InvalidateProperties(); + UpdateRegion(); } + } - [CommandProperty(AccessLevel.GameMaster)] - public bool RandomizeType { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public int Kills + [SerializableProperty(11)] + [CommandProperty(AccessLevel.GameMaster)] + public int Kills + { + get => _kills; + set { - get => m_Kills; - set + _kills = value; + this.MarkDirty(); + + double n = _kills / (double)MaxKills; + int p = (int)(n * 100); + + if (p < 90) { - m_Kills = value; - - double n = m_Kills / (double)MaxKills; - int p = (int)(n * 100); - - if (p < 90) - { - SetWhiteSkullCount(p / 20); - } - - InvalidateProperties(); + SetWhiteSkullCount(p / 20); } - } - [CommandProperty(AccessLevel.GameMaster)] - public Rectangle2D SpawnArea + InvalidateProperties(); + } + } + + [SerializableProperty(12)] + [CommandProperty(AccessLevel.GameMaster)] + public bool Active + { + get => _active; + set { - get => m_SpawnArea; - set + this.MarkDirty(); + + if (value) { - m_SpawnArea = value; - InvalidateProperties(); - UpdateRegion(); - } - } - - [CommandProperty(AccessLevel.GameMaster)] - public TimeSpan RestartDelay { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public DateTime RestartTime { get; private set; } - - [CommandProperty(AccessLevel.GameMaster)] - public TimeSpan ExpireDelay { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public DateTime ExpireTime { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public ChampionSpawnType Type - { - get => m_Type; - set - { - m_Type = value; - InvalidateProperties(); - } - } - - [CommandProperty(AccessLevel.GameMaster)] - public bool Active - { - get => m_Active; - set - { - if (value) - { - Start(); - } - else - { - Stop(); - } - - InvalidateProperties(); - } - } - - [CommandProperty(AccessLevel.GameMaster)] - public bool ReadyToActivate { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public bool ActivatedByValor { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public bool ActivatedByProximity { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public DateTime NextProximityTime { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public Mobile Champion { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public int Level - { - get => m_Level; - set - { - for (int i = m_RedSkulls.Count - 1; i >= value; --i) - { - m_RedSkulls[i].Delete(); - m_RedSkulls.RemoveAt(i); - } - - for (int i = m_RedSkulls.Count; i < Math.Min(value, 16); ++i) - { - Item skull = new Item(0x1854) { Hue = 0x26, Movable = false, Light = LightType.Circle150 }; - skull.MoveToWorld(GetRedSkullLocation(i), Map); - m_RedSkulls.Add(skull); - } - - m_Level = value; - - InvalidateProperties(); - } - } - - [CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)] - public int MaxLevel{ get => m_MaxLevel; - set => m_MaxLevel = Math.Max(Math.Min(value, 18), 0); - } - - public bool IsChampionSpawn(Mobile m) => m_Creatures.Contains(m); - - [CommandProperty(AccessLevel.GameMaster)] - public virtual int MaxKills - { - get - { - return Level switch - { - >= 16 => 16, - >= 12 => 32, - >= 8 => 64, - >= 4 => 128, - _ => 256 - }; - } - } - - public void SetWhiteSkullCount(int val) - { - for (int i = m_WhiteSkulls.Count - 1; i >= val; --i) - { - m_WhiteSkulls[i].Delete(); - m_WhiteSkulls.RemoveAt(i); - } - - for (int i = m_WhiteSkulls.Count; i < val; ++i) - { - Item skull = new Item(0x1854); - - skull.Movable = false; - skull.Light = LightType.Circle150; - - skull.MoveToWorld(GetWhiteSkullLocation(i), Map); - - m_WhiteSkulls.Add(skull); - - Effects.PlaySound(skull.Location, skull.Map, 0x29); - Effects.SendLocationEffect(new Point3D(skull.X + 1, skull.Y + 1, skull.Z), skull.Map, 0x3728, 10); - } - } - - public void Start() - { - if (m_Active || Deleted) - { - return; - } - - if (RandomizeType) - { - Type = Utility.Random(5) switch - { - 0 => ChampionSpawnType.VerminHorde, - 1 => ChampionSpawnType.UnholyTerror, - 2 => ChampionSpawnType.ColdBlood, - 3 => ChampionSpawnType.Abyss, - 4 => ChampionSpawnType.Arachnid, - _ => Type - }; - } - - m_Active = true; - ReadyToActivate = false; - HasBeenAdvanced = false; - m_MaxLevel = 16 + Utility.Random(3); - - _timerToken.Cancel(); - Timer.StartTimer(TimeSpan.FromSeconds(1.0), TimeSpan.FromSeconds(1.0), OnSlice, out _timerToken); - - _restartTimerToken.Cancel(); - - if (m_Altar != null) - { - m_Altar.Hue = Champion != null ? 0x26 : 0; - } - - if (m_Platform != null) - { - m_Platform.Hue = 0x452; - } - - ExpireTime = Core.Now + ExpireDelay; - } - - public void Stop() - { - if (!m_Active || Deleted) - { - return; - } - - m_Active = false; - ActivatedByValor = false; - HasBeenAdvanced = false; - m_MaxLevel = 0; - - _timerToken.Cancel(); - _restartTimerToken.Cancel(); - - if (m_Altar != null) - { - m_Altar.Hue = 0; - } - - if (m_Platform != null) - { - m_Platform.Hue = 0x497; - } - - if (AlwaysActive) - { - BeginRestart(RestartDelay); - } - else if (ActivatedByProximity) - { - ActivatedByProximity = false; - NextProximityTime = Core.Now + TimeSpan.FromHours(6.0); - } - - Timer.StartTimer(TimeSpan.FromMinutes(10.0), ExpireCreatures); - } - - public void BeginRestart(TimeSpan ts) - { - RestartTime = Core.Now + ts; - _restartTimerToken.Cancel(); - Timer.StartTimer(ts, EndRestart, out _restartTimerToken); - } - - public void EndRestart() - { - HasBeenAdvanced = false; - ReadyToActivate = true; - } - - private ScrollofTranscendence CreateRandomTramSoT() - { - int level = Utility.Random(5) + 1; - return ScrollofTranscendence.CreateRandom(level, level); - } - - private ScrollofTranscendence CreateRandomFelSoT() - { - int level = Utility.Random(5) + 1; - return ScrollofTranscendence.CreateRandom(level, level); - } - - private static PowerScroll CreateRandomFelPS() => PowerScroll.CreateRandomNoCraft(5, 5); - - public static void GiveScrollOfTranscendenceFelTo(Mobile killer, ScrollofTranscendence SoTF) - { - if (SoTF == null || killer == null) //sanity - { - return; - } - - killer.SendLocalizedMessage(1094936); // You have received a Scroll of Transcendence! - - if (killer.Alive) - { - killer.AddToBackpack(SoTF); - } - else if (killer.Corpse is { Deleted: false }) - { - killer.Corpse.DropItem(SoTF); + Start(); } else { - killer.AddToBackpack(SoTF); + Stop(); } - // Justice reward - var pm = (PlayerMobile)killer; - var prot = JusticeVirtue.GetProtector(pm); - if (prot == null || prot.Map != killer.Map || prot.Kills >= 5 || prot.Criminal || - !JusticeVirtue.CheckMapRegion(killer, prot)) - { - return; - } + InvalidateProperties(); + } + } - var chance = VirtueSystem.GetLevel(prot, VirtueName.Justice) switch + [CommandProperty(AccessLevel.GameMaster)] + public bool ReadyToActivate { get; set; } + + public bool IsChampionSpawn(Mobile m) => _creatures.Contains(m); + + [CommandProperty(AccessLevel.GameMaster)] + public virtual int MaxKills + { + get + { + return Level switch { - VirtueLevel.Seeker => 60, - VirtueLevel.Follower => 80, - VirtueLevel.Knight => 100, - _ => 0 + >= 16 => 16, + >= 12 => 32, + >= 8 => 64, + >= 4 => 128, + _ => 256 + }; + } + } + + public void SetWhiteSkullCount(int val) + { + for (int i = _whiteSkulls.Count - 1; i >= val; --i) + { + _whiteSkulls[i].Delete(); + _whiteSkulls.RemoveAt(i); + } + + for (int i = _whiteSkulls.Count; i < val; ++i) + { + Item skull = new Item(0x1854) + { + Movable = false, + Light = LightType.Circle150 }; - if (chance > 0 && chance > Utility.Random(100)) - { - prot.SendLocalizedMessage(1049368); // You have been rewarded for your dedication to Justice! - ScrollofTranscendence SoTFduplicate = new ScrollofTranscendence (SoTF.Skill, SoTF.Value); - prot.AddToBackpack(SoTFduplicate); - } + skull.MoveToWorld(GetWhiteSkullLocation(i), Map); + + _whiteSkulls.Add(skull); + + Effects.PlaySound(skull.Location, skull.Map, 0x29); + Effects.SendLocationEffect(new Point3D(skull.X + 1, skull.Y + 1, skull.Z), skull.Map, 0x3728, 10); } - public static void GivePowerScrollFelTo (Mobile killer, PowerScroll PS) + this.MarkDirty(); + } + + public void Start() + { + if (_active || Deleted) { - if (PS == null || killer == null) //sanity - { - return; - } + return; + } - killer.SendLocalizedMessage(1049524); // You have received a scroll of power! - - if (killer.Alive) + if (RandomizeType) + { + Type = Utility.Random(5) switch { - killer.AddToBackpack(PS); - } - else if (killer.Corpse is { Deleted: false }) - { - killer.Corpse.DropItem(PS); - } - else - { - killer.AddToBackpack(PS); - } - - // Justice reward - var pm = (PlayerMobile)killer; - var prot = JusticeVirtue.GetProtector(pm); - if (prot == null || prot.Map != killer.Map || prot.Kills >= 5 || prot.Criminal || - !JusticeVirtue.CheckMapRegion(killer, prot)) - { - return; - } - - var chance = VirtueSystem.GetLevel(prot, VirtueName.Justice) switch - { - VirtueLevel.Seeker => 60, - VirtueLevel.Follower => 80, - VirtueLevel.Knight => 100, - _ => 0 + 0 => ChampionSpawnType.VerminHorde, + 1 => ChampionSpawnType.UnholyTerror, + 2 => ChampionSpawnType.ColdBlood, + 3 => ChampionSpawnType.Abyss, + 4 => ChampionSpawnType.Arachnid, + _ => Type }; - - if (chance > 0 && chance > Utility.Random(100)) - { - prot.SendLocalizedMessage(1049368); // You have been rewarded for your dedication to Justice! - prot.AddToBackpack(CreateRandomFelPS()); - } } - public void OnSlice() + Active = true; + ReadyToActivate = false; + HasBeenAdvanced = false; + MaxLevel = 16 + Utility.Random(3); + + _timerToken.Cancel(); + Timer.StartTimer(TimeSpan.FromSeconds(1.0), TimeSpan.FromSeconds(1.0), OnSlice, out _timerToken); + + _restartTimerToken.Cancel(); + + if (_altar != null) { - if (!m_Active || Deleted) - { - return; - } + _altar.Hue = Champion != null ? 0x26 : 0; + } - if (Champion != null) + if (_platform != null) + { + _platform.Hue = 0x452; + } + + ExpireTime = Core.Now + ExpireDelay; + } + + public void Stop() + { + if (!_active || Deleted) + { + return; + } + + Active = false; + ActivatedByValor = false; + HasBeenAdvanced = false; + MaxLevel = 0; + + _timerToken.Cancel(); + _restartTimerToken.Cancel(); + + if (_altar != null) + { + _altar.Hue = 0; + } + + if (_platform != null) + { + _platform.Hue = 0x497; + } + + if (AlwaysActive) + { + BeginRestart(RestartDelay); + } + else if (ActivatedByProximity) + { + ActivatedByProximity = false; + NextProximityTime = Core.Now + TimeSpan.FromHours(6.0); + } + + Timer.StartTimer(TimeSpan.FromMinutes(10.0), ExpireCreatures); + } + + public void BeginRestart(TimeSpan ts) + { + RestartTime = Core.Now + ts; + _restartTimerToken.Cancel(); + Timer.StartTimer(ts, EndRestart, out _restartTimerToken); + } + + public void EndRestart() + { + HasBeenAdvanced = false; + ReadyToActivate = true; + } + + private ScrollofTranscendence CreateRandomTramSoT() + { + int level = Utility.Random(5) + 1; + return ScrollofTranscendence.CreateRandom(level, level); + } + + private ScrollofTranscendence CreateRandomFelSoT() + { + int level = Utility.Random(5) + 1; + return ScrollofTranscendence.CreateRandom(level, level); + } + + private static PowerScroll CreateRandomFelPS() => PowerScroll.CreateRandomNoCraft(5, 5); + + public static void GiveScrollOfTranscendenceFelTo(Mobile killer, ScrollofTranscendence SoTF) + { + if (SoTF == null || killer == null) //sanity + { + return; + } + + killer.SendLocalizedMessage(1094936); // You have received a Scroll of Transcendence! + + if (killer.Alive) + { + killer.AddToBackpack(SoTF); + } + else if (killer.Corpse is { Deleted: false }) + { + killer.Corpse.DropItem(SoTF); + } + else + { + killer.AddToBackpack(SoTF); + } + + // Justice reward + var pm = (PlayerMobile)killer; + var prot = JusticeVirtue.GetProtector(pm); + if (prot == null || prot.Map != killer.Map || prot.Kills >= 5 || prot.Criminal || + !JusticeVirtue.CheckMapRegion(killer, prot)) + { + return; + } + + var chance = VirtueSystem.GetLevel(prot, VirtueName.Justice) switch + { + VirtueLevel.Seeker => 60, + VirtueLevel.Follower => 80, + VirtueLevel.Knight => 100, + _ => 0 + }; + + if (chance > 0 && chance > Utility.Random(100)) + { + prot.SendLocalizedMessage(1049368); // You have been rewarded for your dedication to Justice! + ScrollofTranscendence SoTFduplicate = new ScrollofTranscendence (SoTF.Skill, SoTF.Value); + prot.AddToBackpack(SoTFduplicate); + } + } + + public static void GivePowerScrollFelTo (Mobile killer, PowerScroll PS) + { + if (PS == null || killer == null) //sanity + { + return; + } + + killer.SendLocalizedMessage(1049524); // You have received a scroll of power! + + if (killer.Alive) + { + killer.AddToBackpack(PS); + } + else if (killer.Corpse is { Deleted: false }) + { + killer.Corpse.DropItem(PS); + } + else + { + killer.AddToBackpack(PS); + } + + // Justice reward + var pm = (PlayerMobile)killer; + var prot = JusticeVirtue.GetProtector(pm); + if (prot == null || prot.Map != killer.Map || prot.Kills >= 5 || prot.Criminal || + !JusticeVirtue.CheckMapRegion(killer, prot)) + { + return; + } + + var chance = VirtueSystem.GetLevel(prot, VirtueName.Justice) switch + { + VirtueLevel.Seeker => 60, + VirtueLevel.Follower => 80, + VirtueLevel.Knight => 100, + _ => 0 + }; + + if (chance > 0 && chance > Utility.Random(100)) + { + prot.SendLocalizedMessage(1049368); // You have been rewarded for your dedication to Justice! + prot.AddToBackpack(CreateRandomFelPS()); + } + } + + public void OnSlice() + { + if (!_active || Deleted) + { + return; + } + + if (Champion != null) + { + if (Champion.Deleted) { - if (Champion.Deleted) + RegisterDamageTo(Champion); + + if (Core.ML) { - RegisterDamageTo(Champion); - - if (Core.ML) - { - AwardArtifact((Champion as BaseChampion)?.GetArtifact()); - } - - DamageEntries.Clear(); - - if (m_Platform != null) - { - m_Platform.Hue = 0x497; - } - - if (m_Altar != null) - { - m_Altar.Hue = 0; - - if (HasStarRoomGate && (!Core.ML || Map == Map.Felucca)) - { - new StarRoomGate(m_Altar.Location, m_Altar.Map, true); - } - } - - Champion = null; - Stop(); + AwardArtifact((Champion as BaseChampion)?.GetArtifact()); } - } - else - { - int kills = m_Kills; - for (var i = 0; i < m_Creatures.Count; ++i) + this.Clear(_damageEntries); + + if (_platform != null) { - Mobile m = m_Creatures[i]; + _platform.Hue = 0x497; + } - if (m.Deleted) + if (_altar != null) + { + _altar.Hue = 0; + + if (HasStarRoomGate && (!Core.ML || Map == Map.Felucca)) { - if (m.Corpse is { Deleted: false }) + new StarRoomGate(_altar.Location, _altar.Map, true); + } + } + + Champion = null; + Stop(); + } + } + else + { + int kills = _kills; + + for (var i = 0; i < _creatures.Count; ++i) + { + Mobile m = _creatures[i]; + + if (m.Deleted) + { + if (m.Corpse is { Deleted: false }) + { + ((Corpse)m.Corpse).BeginDecay(TimeSpan.FromMinutes(1)); + } + + this.RemoveAt(_creatures, i); + --i; + ++_kills; + + Mobile killer = m.FindMostRecentDamager(false); + + RegisterDamageTo(m); + + if (killer is BaseCreature creature) + { + killer = creature.GetMaster(); + } + + if (killer is PlayerMobile pm) + { + if (Core.ML) { - ((Corpse)m.Corpse).BeginDecay(TimeSpan.FromMinutes(1)); - } - - m_Creatures.RemoveAt(i); - --i; - ++m_Kills; - - Mobile killer = m.FindMostRecentDamager(false); - - RegisterDamageTo(m); - - if (killer is BaseCreature creature) - { - killer = creature.GetMaster(); - } - - if (killer is PlayerMobile pm) - { - if (Core.ML) + if (Map == Map.Felucca) { - if (Map == Map.Felucca) + // 1 in 1000 you get either a scroll of transcendence or a powerscroll + var random = Utility.Random(2000); + if (random == 0) { - // 1 in 1000 you get either a scroll of transcendence or a powerscroll - var random = Utility.Random(2000); - if (random == 0) - { - GiveScrollOfTranscendenceFelTo(pm, CreateRandomFelSoT()); - } - else if (random == 1) - { - GivePowerScrollFelTo(pm, CreateRandomFelPS()); - } + GiveScrollOfTranscendenceFelTo(pm, CreateRandomFelSoT()); } - - if ((Map == Map.Ilshenar || Map == Map.Tokuno) && Utility.Random(10000) < 15) + else if (random == 1) { - pm.SendLocalizedMessage(1094936); // You have received a Scroll of Transcendence! - pm.AddToBackpack(CreateRandomTramSoT()); + GivePowerScrollFelTo(pm, CreateRandomFelPS()); } } - int mobSubLevel = GetSubLevelfor(m) + 1; - - if (mobSubLevel >= 0) + if ((Map == Map.Ilshenar || Map == Map.Tokuno) && Utility.Random(10000) < 15) { - bool gainedPath = false; + pm.SendLocalizedMessage(1094936); // You have received a Scroll of Transcendence! + pm.AddToBackpack(CreateRandomTramSoT()); + } + } - int pointsToGain = mobSubLevel * 40; + int mobSubLevel = GetSubLevelfor(m) + 1; - if (VirtueSystem.Award(pm, VirtueName.Valor, pointsToGain, ref gainedPath)) + if (mobSubLevel >= 0) + { + bool gainedPath = false; + + int pointsToGain = mobSubLevel * 40; + + if (VirtueSystem.Award(pm, VirtueName.Valor, pointsToGain, ref gainedPath)) + { + if (gainedPath) { - if (gainedPath) - { - pm.SendLocalizedMessage(1054032); // You have gained a path in Valor! - } - else - { - pm.SendLocalizedMessage(1054030); // You have gained in Valor! - } - - // No delay on Valor gains + pm.SendLocalizedMessage(1054032); // You have gained a path in Valor! + } + else + { + pm.SendLocalizedMessage(1054030); // You have gained in Valor! } - pm.ChampionTitles.Award(m_Type, mobSubLevel); + // No delay on Valor gains } + + pm.ChampionTitles.Award(_type, mobSubLevel); } } } - - // Only really needed once. - if (m_Kills > kills) - { - InvalidateProperties(); - } - - double n = m_Kills / (double)MaxKills; - int p = (int)(n * 100); - - if (p >= 99) - { - AdvanceLevel(); - } - else if (p > 0) - { - SetWhiteSkullCount(p / 20); - } - - if (Core.Now >= ExpireTime) - { - Expire(); - } - - Respawn(); } - } - public void AdvanceLevel() - { - ExpireTime = Core.Now + ExpireDelay; - - if (Level < m_MaxLevel) + // Only really needed once. + if (_kills > kills) { - m_Kills = 0; - ++Level; InvalidateProperties(); - SetWhiteSkullCount(0); + } - if (m_Altar != null) - { - Effects.PlaySound(m_Altar.Location, m_Altar.Map, 0x29); - Effects.SendLocationEffect(new Point3D(m_Altar.X + 1, m_Altar.Y + 1, m_Altar.Z), m_Altar.Map, 0x3728, 10); - } - } - else + double n = _kills / (double)MaxKills; + int p = (int)(n * 100); + + if (p >= 99) { - SpawnChampion(); + AdvanceLevel(); } + else if (p > 0) + { + SetWhiteSkullCount(p / 20); + } + + if (Core.Now >= ExpireTime) + { + Expire(); + } + + Respawn(); } + } - public void SpawnChampion() + public void AdvanceLevel() + { + ExpireTime = Core.Now + ExpireDelay; + + if (Level < _maxLevel) { - if (m_Altar != null) - { - m_Altar.Hue = 0x26; - } - - if (m_Platform != null) - { - m_Platform.Hue = 0x452; - } - - m_Kills = 0; - Level = 0; + _kills = 0; + ++Level; InvalidateProperties(); SetWhiteSkullCount(0); - try + if (_altar != null) { - Champion = ChampionSpawnInfo.GetInfo(m_Type).Champion.CreateInstance(); - } - catch (Exception e) - { - logger.Error( - e, - "Failed to spawn champion \"{MobileType}\" at {Location} ({Map}).", - m_Type, - Location, - Map - ); + Effects.PlaySound(_altar.Location, _altar.Map, 0x29); + Effects.SendLocationEffect(new Point3D(_altar.X + 1, _altar.Y + 1, _altar.Z), _altar.Map, 0x3728, 10); } + } + else + { + SpawnChampion(); + } + } - if (Champion != null) - { - Champion.MoveToWorld(new Point3D(X, Y, Z - 15), Map); + public void SpawnChampion() + { + if (_altar != null) + { + _altar.Hue = 0x26; + } - if (Champion is BaseCreature bc) + if (_platform != null) + { + _platform.Hue = 0x452; + } + + _kills = 0; + Level = 0; + InvalidateProperties(); + SetWhiteSkullCount(0); + + try + { + Champion = ChampionSpawnInfo.GetInfo(_type).Champion.CreateInstance(); + } + catch (Exception e) + { + logger.Error( + e, + "Failed to spawn champion \"{MobileType}\" at {Location} ({Map}).", + _type, + Location, + Map + ); + } + + if (Champion != null) + { + Champion.MoveToWorld(new Point3D(X, Y, Z - 15), Map); + + if (Champion is BaseCreature bc) + { + if (ConfinedRoaming) { - if (ConfinedRoaming) - { - bc.Home = Location; - bc.HomeMap = Map; - bc.RangeHome = Math.Min(m_SpawnArea.Width / 2, m_SpawnArea.Height / 2); - } - else - { - bc.Home = bc.Location; - bc.HomeMap = bc.Map; - - Point2D xWall1 = new Point2D(m_SpawnArea.X, bc.Y); - Point2D xWall2 = new Point2D(m_SpawnArea.X + m_SpawnArea.Width, bc.Y); - Point2D yWall1 = new Point2D(bc.X, m_SpawnArea.Y); - Point2D yWall2 = new Point2D(bc.X, m_SpawnArea.Y + m_SpawnArea.Height); - - double minXDist = Math.Min(bc.GetDistanceToSqrt(xWall1), bc.GetDistanceToSqrt(xWall2)); - double minYDist = Math.Min(bc.GetDistanceToSqrt(yWall1), bc.GetDistanceToSqrt(yWall2)); - - bc.RangeHome = (int)Math.Min(minXDist, minYDist); - } + bc.Home = Location; + bc.HomeMap = Map; + bc.RangeHome = Math.Min(_spawnArea.Width / 2, _spawnArea.Height / 2); } else { - throw new Exception("Champion Spawn is not inherited from BaseCreature"); + bc.Home = bc.Location; + bc.HomeMap = bc.Map; + + Point2D xWall1 = new Point2D(_spawnArea.X, bc.Y); + Point2D xWall2 = new Point2D(_spawnArea.X + _spawnArea.Width, bc.Y); + Point2D yWall1 = new Point2D(bc.X, _spawnArea.Y); + Point2D yWall2 = new Point2D(bc.X, _spawnArea.Y + _spawnArea.Height); + + double minXDist = Math.Min(bc.GetDistanceToSqrt(xWall1), bc.GetDistanceToSqrt(xWall2)); + double minYDist = Math.Min(bc.GetDistanceToSqrt(yWall1), bc.GetDistanceToSqrt(yWall2)); + + bc.RangeHome = (int)Math.Min(minXDist, minYDist); } } - } - - public virtual int MaxSpawn => 250 - GetSubLevel() * 40; - - public void Respawn() - { - if (!m_Active || Deleted || Champion != null) - { - return; - } - - while (m_Creatures.Count < MaxSpawn) - { - Mobile m = Spawn(); - - if (m == null) - { - return; - } - - Point3D loc = GetSpawnLocation(); - - // Allow creatures to turn into Paragons at Ilshenar champions. - m.OnBeforeSpawn(loc, Map); - - m_Creatures.Add(m); - m.MoveToWorld(loc, Map); - - if (m is BaseCreature bc) - { - bc.Tamable = false; - - if (ConfinedRoaming) - { - bc.Home = Location; - bc.HomeMap = Map; - bc.RangeHome = Math.Min(m_SpawnArea.Width / 2, m_SpawnArea.Height / 2); - } - else - { - bc.Home = bc.Location; - bc.HomeMap = bc.Map; - - Point2D xWall1 = new Point2D(m_SpawnArea.X, bc.Y); - Point2D xWall2 = new Point2D(m_SpawnArea.X + m_SpawnArea.Width, bc.Y); - Point2D yWall1 = new Point2D(bc.X, m_SpawnArea.Y); - Point2D yWall2 = new Point2D(bc.X, m_SpawnArea.Y + m_SpawnArea.Height); - - double minXDist = Math.Min(bc.GetDistanceToSqrt(xWall1), bc.GetDistanceToSqrt(xWall2)); - double minYDist = Math.Min(bc.GetDistanceToSqrt(yWall1), bc.GetDistanceToSqrt(yWall2)); - - bc.RangeHome = (int)Math.Min(minXDist, minYDist); - } - } - } - } - - public Point3D GetSpawnLocation() - { - Map map = Map; - - if (map == null) - { - return Location; - } - - // Try 20 times to find a spawnable location. - for (int i = 0; i < 20; i++) - { - int x = Utility.Random(m_SpawnArea.X, m_SpawnArea.Width); - int y = Utility.Random(m_SpawnArea.Y, m_SpawnArea.Height); - - int z = Map.GetAverageZ(x, y); - - if (Map.CanSpawnMobile(new Point2D(x, y), z)) - { - return new Point3D(x, y, z); - } - } - - return Location; - } - - public int Level1 => 4; - public int Level2 => 8; - public int Level3 => 12; - - public int GetSubLevel() - { - int level = Level; - - return level <= Level1 ? 0 : level <= Level2 ? 1 : level <= Level3 ? 2 : 3; - } - - public int GetSubLevelfor (Mobile m) - { - Type[][] types = ChampionSpawnInfo.GetInfo(m_Type).SpawnTypes; - Type t = m.GetType(); - - for (int i = 0; i < types.GetLength(0); i++) - { - Type[] individualTypes = types[i]; - - for (int j = 0; j < individualTypes.Length; j++) - { - if (t == individualTypes[j]) - { - return i; - } - } - } - - return -1; - } - - public Mobile Spawn() - { - Type[][] types = ChampionSpawnInfo.GetInfo(m_Type).SpawnTypes; - - int v = GetSubLevel(); - - if (v >= 0 && v < types.Length) - { - return Spawn(types[v]); - } - - return null; - } - - public Mobile Spawn(params Type[] types) - { - var type = types[Utility.Random(types.Length)]; - try - { - return type.CreateInstance(); - } - catch (Exception e) - { - logger.Error( - e, - "Failed to spawn minion \"{Type}\" for champion {ChampionSpawnType} at {Location} ({Map}).", - type, - m_Type, - Location, - Map - ); - return null; - } - } - - public void Expire() - { - m_Kills = 0; - - if (m_WhiteSkulls.Count == 0) - { - // They didn't even get 20%, go back a level - - if (Level > 0) - { - --Level; - } - - if (!AlwaysActive && Level == 0) - { - Stop(); - } - - InvalidateProperties(); - } else { - SetWhiteSkullCount(0); - } - - ExpireTime = Core.Now + ExpireDelay; - } - - public Point3D GetRedSkullLocation(int index) - { - int x, y; - - if (index < 5) - { - x = index - 2; - y = -2; - } - else if (index < 9) - { - x = 2; - y = index - 6; - } - else if (index < 13) - { - x = 10 - index; - y = 2; - } - else - { - x = -2; - y = 14 - index; - } - - return new Point3D(X + x, Y + y, Z - 15); - } - - public Point3D GetWhiteSkullLocation(int index) - { - int x, y; - - switch(index) - { - default: x = -1; y = -1; break; - case 1: x = 1; y = -1; break; - case 2: x = 1; y = 1; break; - case 3: x = -1; y = 1; break; - } - - return new Point3D(X + x, Y + y, Z - 15); - } - - public override void AddNameProperty(IPropertyList list) - { - list.Add("champion spawn"); - } - - public override void GetProperties(IPropertyList list) - { - base.GetProperties(list); - - if (m_Active) - { - list.Add(1060742); // active - list.Add(1060658, $"{"Type"}\t{m_Type}"); // ~1_val~: ~2_val~ - list.Add(1060659, $"{"Level"}\t{Level}"); // ~1_val~: ~2_val~ - var killRatio = 100.0 * ((double)m_Kills / MaxKills); - list.Add(1060660, $"{"Kills"}\t{m_Kills} of {MaxKills} ({killRatio:F1}%)"); // ~1_val~: ~2_val~ - } - else - { - list.Add(1060743); // inactive + throw new Exception("Champion Spawn is not inherited from BaseCreature"); } } + } - public override void OnSingleClick(Mobile from) + public virtual int MaxSpawn => 250 - GetSubLevel() * 40; + + public void Respawn() + { + if (!_active || Deleted || Champion != null) { - if (m_Active) - { - LabelTo(from, $"{m_Type} (Active; Level: {Level}; Kills: {m_Kills}/{MaxKills})"); - } - else - { - LabelTo(from, $"{m_Type} (Inactive)"); - } + return; } - public override void OnDoubleClick(Mobile from) + while (_creatures.Count < MaxSpawn) { - from.SendGump(new PropertiesGump(from, this)); - } + Mobile m = Spawn(); - public override void OnLocationChange(Point3D oldLoc) - { - if (Deleted) - { - return; - } - - if (m_Platform != null) - { - m_Platform.Location = new Point3D(X, Y, Z - 20); - } - - if (m_Altar != null) - { - m_Altar.Location = new Point3D(X, Y, Z - 15); - } - - if (m_Idol != null) - { - m_Idol.Location = new Point3D(X, Y, Z - 15); - } - - if (m_RedSkulls != null) - { - for (var i = 0; i < Math.Min(m_RedSkulls.Count, 16); ++i) - { - m_RedSkulls[i].Location = GetRedSkullLocation(i); - } - } - - if (m_WhiteSkulls != null) - { - for (int i = 0; i < m_WhiteSkulls.Count; ++i) - { - m_WhiteSkulls[i].Location = GetWhiteSkullLocation(i); - } - } - - m_SpawnArea.X += Location.X - oldLoc.X; - m_SpawnArea.Y += Location.Y - oldLoc.Y; - - UpdateRegion(); - } - - public override void OnMapChange() - { - if (Deleted) - { - return; - } - - if (m_Platform != null) - { - m_Platform.Map = Map; - } - - if (m_Altar != null) - { - m_Altar.Map = Map; - } - - if (m_Idol != null) - { - m_Idol.Map = Map; - } - - if (m_RedSkulls != null) - { - for (int i = 0; i < m_RedSkulls.Count; ++i) - { - m_RedSkulls[i].Map = Map; - } - } - - if (m_WhiteSkulls != null) - { - for (int i = 0; i < m_WhiteSkulls.Count; ++i) - { - m_WhiteSkulls[i].Map = Map; - } - } - - UpdateRegion(); - } - - public override void OnAfterDelete() - { - base.OnAfterDelete(); - - m_Platform?.Delete(); - - m_Altar?.Delete(); - - m_Idol?.Delete(); - - if (m_RedSkulls != null) - { - for (int i = 0; i < m_RedSkulls.Count; ++i) - { - m_RedSkulls[i].Delete(); - } - - m_RedSkulls.Clear(); - } - - if (m_WhiteSkulls != null) - { - for (int i = 0; i < m_WhiteSkulls.Count; ++i) - { - m_WhiteSkulls[i].Delete(); - } - - m_WhiteSkulls.Clear(); - } - - DeleteCreatures(); - - if (Champion is { Player: false }) - { - Champion.Delete(); - } - - Stop(); - - UpdateRegion(); - } - - public void ExpireCreatures() - { - if (!m_Active && !ReadyToActivate && !AlwaysActive) - { - DeleteCreatures(); - } - } - - public void DeleteCreatures() - { - if (m_Creatures != null) - { - for (int i = 0; i < m_Creatures.Count; ++i) - { - Mobile mob = m_Creatures[i]; - - if (!mob.Player) - { - mob.Delete(); - } - } - - m_Creatures.Clear(); - } - } - - public ChampionSpawn(Serial serial) : base(serial) - { - } - - public virtual void RegisterDamageTo(Mobile m) - { if (m == null) { return; } - foreach (DamageEntry de in m.DamageEntries) + Point3D loc = GetSpawnLocation(); + + // Allow creatures to turn into Paragons at Ilshenar champions. + m.OnBeforeSpawn(loc, Map); + + this.Add(_creatures, m); + m.MoveToWorld(loc, Map); + + if (m is BaseCreature bc) { - if (de.HasExpired) + bc.Tamable = false; + + if (ConfinedRoaming) { - continue; + bc.Home = Location; + bc.HomeMap = Map; + bc.RangeHome = Math.Min(_spawnArea.Width / 2, _spawnArea.Height / 2); } - - Mobile damager = de.Damager; - Mobile master = damager.GetDamageMaster(m); - - if (master != null) + else { - damager = master; + bc.Home = bc.Location; + bc.HomeMap = bc.Map; + + Point2D xWall1 = new Point2D(_spawnArea.X, bc.Y); + Point2D xWall2 = new Point2D(_spawnArea.X + _spawnArea.Width, bc.Y); + Point2D yWall1 = new Point2D(bc.X, _spawnArea.Y); + Point2D yWall2 = new Point2D(bc.X, _spawnArea.Y + _spawnArea.Height); + + double minXDist = Math.Min(bc.GetDistanceToSqrt(xWall1), bc.GetDistanceToSqrt(xWall2)); + double minYDist = Math.Min(bc.GetDistanceToSqrt(yWall1), bc.GetDistanceToSqrt(yWall2)); + + bc.RangeHome = (int)Math.Min(minXDist, minYDist); } - - RegisterDamage(damager, de.DamageGiven); } } - - public void RegisterDamage(Mobile from, int amount) - { - if (from?.Player != true) - { - return; - } - - if (DamageEntries.ContainsKey(from)) - { - DamageEntries[from] += amount; - } - else - { - DamageEntries.Add(from, amount); - } - } - - public virtual void AwardArtifact(Item artifact) - { - if (artifact == null) - { - return; - } - - if (DamageEntries.Count > 0) - { - int totalDamage = 0; - - Dictionary validEntries = new Dictionary(); - - foreach (var (key, value) in DamageEntries) - { - if (IsEligible(key, artifact)) - { - validEntries.Add(key, value); - totalDamage += value; - } - } - - bool artifactGiven = false; - - do - { - int randomDamage = Utility.RandomMinMax(1, totalDamage); - - int checkDamage = 0; - - foreach (var (key, value) in validEntries) - { - checkDamage += value; - - if (checkDamage > randomDamage) - { - if (GiveArtifact(key, artifact)) - { - artifactGiven = true; - } - else - { - validEntries.Remove(key); - } - - break; - } - } - - if (validEntries.Count == 0) //EVERYONE has a full backpack?!@ - { - artifact.Delete(); - break; - } - } - while (!artifactGiven); - } - else - { - artifact.Delete(); - } - } - - public bool GiveArtifact(Mobile to, Item artifact) - { - if (to == null || artifact == null) - { - return false; - } - - Container pack = to.Backpack; - - if (pack?.TryDropItem(to, artifact, false) != true) - { - return false; - } - - to.SendLocalizedMessage(1062317); // For your valor in combating the fallen beast, a special artifact has been bestowed on you. - return true; - } - - public bool IsEligible(Mobile m, Item Artifact) => - m.Player && m.Alive && m.Region != null && m.Region == m_Region && - m.Backpack?.CheckHold(m, Artifact, false) == true; - - public override void Serialize(IGenericWriter writer) - { - base.Serialize(writer); - - writer.Write(9); // version - - writer.Write(m_Level); - - writer.Write(ActivatedByProximity); - writer.WriteDeltaTime(NextProximityTime); - - writer.Write(m_MaxLevel); //This can change, based on how you use the champion spawn - - writer.Write(ActivatedByValor); - - writer.Write(DamageEntries.Count); - foreach (KeyValuePair kvp in DamageEntries) - { - writer.Write(kvp.Key); - writer.Write(kvp.Value); - } - - writer.Write(ConfinedRoaming); - writer.Write(m_Idol); - writer.Write(HasBeenAdvanced); - writer.Write(m_SpawnArea); - - writer.Write(RandomizeType); - - writer.Write(m_Kills); - - writer.Write(m_Active); - writer.Write((int)m_Type); - writer.Write(m_Creatures); - writer.Write(m_RedSkulls); - writer.Write(m_WhiteSkulls); - writer.Write(m_Platform); - writer.Write(m_Altar); - writer.Write(ExpireDelay); - writer.WriteDeltaTime(ExpireTime); - writer.Write(Champion); - writer.Write(RestartDelay); - - if (_restartTimerToken.Running) - { - writer.Write(true); - writer.WriteDeltaTime(RestartTime); - } - else - { - writer.Write(false); - } - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - - DamageEntries = new Dictionary(); - - int version = reader.ReadInt(); - - switch(version) - { - case 9: - { - m_Level = reader.ReadInt(); - - goto case 8; - } - case 8: - { - ActivatedByProximity = reader.ReadBool(); - NextProximityTime = reader.ReadDeltaTime(); - goto case 7; - } - case 7: - { - m_MaxLevel = reader.ReadInt(); - goto case 6; - } - case 6: - { - if (version < 7) - { - m_MaxLevel = 16 + Utility.Random(3); //full levels - } - - ActivatedByValor = reader.ReadBool(); - goto case 5; - } - case 5: - { - int entries = reader.ReadInt(); - Mobile m; - int damage; - for (int i = 0; i < entries; ++i) - { - m = reader.ReadEntity(); - damage = reader.ReadInt(); - if (m != null) - { - DamageEntries.Add(m, damage); - } - } - goto case 4; - } - case 4: - { - ConfinedRoaming = reader.ReadBool(); - m_Idol = reader.ReadEntity(); - HasBeenAdvanced = reader.ReadBool(); - - goto case 3; - } - case 3: - { - m_SpawnArea = reader.ReadRect2D(); - - goto case 2; - } - case 2: - { - RandomizeType = reader.ReadBool(); - - goto case 1; - } - case 1: - { - if (version < 3) - { - int oldRange = reader.ReadInt(); - - m_SpawnArea = new Rectangle2D(new Point2D(X - oldRange, Y - oldRange), new Point2D(X + oldRange, Y + oldRange)); - } - - m_Kills = reader.ReadInt(); - - goto case 0; - } - case 0: - { - if (version < 1) - { - m_SpawnArea = new Rectangle2D(new Point2D(X - 24, Y - 24), new Point2D(X + 24, Y + 24)); //Default was 24 - } - - bool active = reader.ReadBool(); - m_Type = (ChampionSpawnType)reader.ReadInt(); - m_Creatures = reader.ReadEntityList(); - m_RedSkulls = reader.ReadEntityList(); - m_WhiteSkulls = reader.ReadEntityList(); - m_Platform = reader.ReadEntity(); - m_Altar = reader.ReadEntity(); - ExpireDelay = reader.ReadTimeSpan(); - ExpireTime = reader.ReadDeltaTime(); - Champion = reader.ReadEntity(); - RestartDelay = reader.ReadTimeSpan(); - - if (reader.ReadBool()) - { - RestartTime = reader.ReadDeltaTime(); - BeginRestart(RestartTime - Core.Now); - } - - if (version < 4) - { - m_Idol = new IdolOfTheChampion(this); - m_Idol.MoveToWorld(new Point3D(X, Y, Z - 15), Map); - } - - if (m_Platform == null || m_Altar == null || m_Idol == null) - { - Delete(); - } - else if (active) - { - Start(); - } - else if (AlwaysActive) - { - ReadyToActivate = true; - } - - break; - } - } - - Timer.StartTimer(TimeSpan.Zero, UpdateRegion); - } } - public class ChampionSpawnRegion : BaseRegion + public Point3D GetSpawnLocation() { - public ChampionSpawn Spawn { get; } + Map map = Map; - public ChampionSpawnRegion(ChampionSpawn spawn) : - base(null, spawn.Map, Find(spawn.Location, spawn.Map), spawn.SpawnArea) => Spawn = spawn; - - public override bool AllowHousing(Mobile from, Point3D p) => false; - - public bool CanSpawn() => Spawn.EjectLocation != new Point3D(0, 0, 0) && Spawn.EjectMap != null; - - public override void AlterLightLevel(Mobile m, ref int global, ref int personal) + if (map == null) { - base.AlterLightLevel(m, ref global, ref personal); - global = Math.Max(global, 1 + Spawn.Level); //This is a guesstimate. TODO: Verify & get exact values // OSI testing: at 2 red skulls, light = 0x3 ; 1 red = 0x3.; 3 = 8; 9 = 0xD 8 = 0xD 12 = 0x12 10 = 0xD + return Location; } - public override void OnEnter(Mobile m) + // Try 20 times to find a spawnable location. + for (int i = 0; i < 20; i++) { - if (m.Player && m.AccessLevel == AccessLevel.Player && !Spawn.Active) + int x = Utility.Random(_spawnArea.X, _spawnArea.Width); + int y = Utility.Random(_spawnArea.Y, _spawnArea.Height); + + int z = Map.GetAverageZ(x, y); + + if (Map.CanSpawnMobile(new Point2D(x, y), z)) { - Region parent = Parent ?? this; + return new Point3D(x, y, z); + } + } - if (Spawn.ReadyToActivate) + return Location; + } + + public int Level1 => 4; + public int Level2 => 8; + public int Level3 => 12; + + public int GetSubLevel() + { + int level = Level; + + return level <= Level1 ? 0 : level <= Level2 ? 1 : level <= Level3 ? 2 : 3; + } + + public int GetSubLevelfor (Mobile m) + { + Type[][] types = ChampionSpawnInfo.GetInfo(_type).SpawnTypes; + Type t = m.GetType(); + + for (int i = 0; i < types.GetLength(0); i++) + { + Type[] individualTypes = types[i]; + + for (int j = 0; j < individualTypes.Length; j++) + { + if (t == individualTypes[j]) { - Spawn.Start(); - } - else if (Spawn.ProximitySpawn && !Spawn.ActivatedByProximity && Core.Now >= Spawn.NextProximityTime) - { - List players = parent.GetPlayers(); - List addresses = new List(); - for (var i = 0; i < players.Count; i++) - { - if (players[i].AccessLevel == AccessLevel.Player && players[i].NetState != null && - !addresses.Contains(players[i].NetState.Address) && !((PlayerMobile)players[i]).Young) - { - addresses.Add(players[i].NetState.Address); - } - } - - if (addresses.Count >= 15) - { - foreach (Mobile player in players) - { - player.SendMessage(0x20, Spawn.BroadcastMessage); - } - - Spawn.ActivatedByProximity = true; - Spawn.BeginRestart(TimeSpan.FromMinutes(5.0)); - } + return i; } } } - public override bool OnMoveInto(Mobile m, Direction d, Point3D newLocation, Point3D oldLocation) + return -1; + } + + public Mobile Spawn() + { + Type[][] types = ChampionSpawnInfo.GetInfo(_type).SpawnTypes; + + int v = GetSubLevel(); + + if (v >= 0 && v < types.Length) { - if (base.OnMoveInto(m, d, newLocation, oldLocation)) + return Spawn(types[v]); + } + + return null; + } + + public Mobile Spawn(params Type[] types) + { + var type = types[Utility.Random(types.Length)]; + try + { + return type.CreateInstance(); + } + catch (Exception e) + { + logger.Error( + e, + "Failed to spawn minion \"{Type}\" for champion {ChampionSpawnType} at {Location} ({Map}).", + type, + _type, + Location, + Map + ); + return null; + } + } + + public void Expire() + { + _kills = 0; + + if (_whiteSkulls.Count == 0) + { + // They didn't even get 20%, go back a level + + if (Level > 0) { - if (m.Player) + --Level; + } + + if (!AlwaysActive && Level == 0) + { + Stop(); + } + + InvalidateProperties(); + } + else + { + SetWhiteSkullCount(0); + } + + ExpireTime = Core.Now + ExpireDelay; + } + + public Point3D GetRedSkullLocation(int index) + { + int x, y; + + if (index < 5) + { + x = index - 2; + y = -2; + } + else if (index < 9) + { + x = 2; + y = index - 6; + } + else if (index < 13) + { + x = 10 - index; + y = 2; + } + else + { + x = -2; + y = 14 - index; + } + + return new Point3D(X + x, Y + y, Z - 15); + } + + public Point3D GetWhiteSkullLocation(int index) + { + int x, y; + + switch(index) + { + default: x = -1; y = -1; break; + case 1: x = 1; y = -1; break; + case 2: x = 1; y = 1; break; + case 3: x = -1; y = 1; break; + } + + return new Point3D(X + x, Y + y, Z - 15); + } + + public override void AddNameProperty(IPropertyList list) + { + list.Add("champion spawn"); + } + + public override void GetProperties(IPropertyList list) + { + base.GetProperties(list); + + if (_active) + { + list.Add(1060742); // active + list.Add(1060658, $"{"Type"}\t{_type}"); // ~1_val~: ~2_val~ + list.Add(1060659, $"{"Level"}\t{Level}"); // ~1_val~: ~2_val~ + var killRatio = 100.0 * ((double)_kills / MaxKills); + list.Add(1060660, $"{"Kills"}\t{_kills} of {MaxKills} ({killRatio:F1}%)"); // ~1_val~: ~2_val~ + } + else + { + list.Add(1060743); // inactive + } + } + + public override void OnSingleClick(Mobile from) + { + if (_active) + { + LabelTo(from, $"{_type} (Active; Level: {Level}; Kills: {_kills}/{MaxKills})"); + } + else + { + LabelTo(from, $"{_type} (Inactive)"); + } + } + + public override void OnDoubleClick(Mobile from) + { + from.SendGump(new PropertiesGump(from, this)); + } + + public override void OnLocationChange(Point3D oldLoc) + { + if (Deleted) + { + return; + } + + if (_platform != null) + { + _platform.Location = new Point3D(X, Y, Z - 20); + } + + if (_altar != null) + { + _altar.Location = new Point3D(X, Y, Z - 15); + } + + if (_idol != null) + { + _idol.Location = new Point3D(X, Y, Z - 15); + } + + if (_redSkulls != null) + { + for (var i = 0; i < Math.Min(_redSkulls.Count, 16); ++i) + { + _redSkulls[i].Location = GetRedSkullLocation(i); + } + } + + if (_whiteSkulls != null) + { + for (int i = 0; i < _whiteSkulls.Count; ++i) + { + _whiteSkulls[i].Location = GetWhiteSkullLocation(i); + } + } + + _spawnArea.X += Location.X - oldLoc.X; + _spawnArea.Y += Location.Y - oldLoc.Y; + + UpdateRegion(); + } + + public override void OnMapChange() + { + if (Deleted) + { + return; + } + + if (_platform != null) + { + _platform.Map = Map; + } + + if (_altar != null) + { + _altar.Map = Map; + } + + if (_idol != null) + { + _idol.Map = Map; + } + + if (_redSkulls != null) + { + for (int i = 0; i < _redSkulls.Count; ++i) + { + _redSkulls[i].Map = Map; + } + } + + if (_whiteSkulls != null) + { + for (int i = 0; i < _whiteSkulls.Count; ++i) + { + _whiteSkulls[i].Map = Map; + } + } + + UpdateRegion(); + } + + public override void OnAfterDelete() + { + base.OnAfterDelete(); + + _platform?.Delete(); + + _altar?.Delete(); + + _idol?.Delete(); + + if (_redSkulls != null) + { + for (int i = 0; i < _redSkulls.Count; ++i) + { + _redSkulls[i].Delete(); + } + + this.Clear(_redSkulls); + } + + if (_whiteSkulls != null) + { + for (int i = 0; i < _whiteSkulls.Count; ++i) + { + _whiteSkulls[i].Delete(); + } + + this.Clear(_whiteSkulls); + } + + DeleteCreatures(); + + if (Champion is { Player: false }) + { + Champion.Delete(); + } + + Stop(); + + UpdateRegion(); + } + + public void ExpireCreatures() + { + if (!_active && !ReadyToActivate && !AlwaysActive) + { + DeleteCreatures(); + } + } + + public void DeleteCreatures() + { + if (_creatures != null) + { + for (int i = 0; i < _creatures.Count; ++i) + { + Mobile mob = _creatures[i]; + + if (!mob.Player) { - if (((PlayerMobile)m).Young) + mob.Delete(); + } + } + + this.Clear(_creatures); + } + } + + public virtual void RegisterDamageTo(Mobile m) + { + if (m == null) + { + return; + } + + foreach (DamageEntry de in m.DamageEntries) + { + if (de.HasExpired) + { + continue; + } + + Mobile damager = de.Damager; + Mobile master = damager.GetDamageMaster(m); + + if (master != null) + { + damager = master; + } + + RegisterDamage(damager, de.DamageGiven); + } + } + + public void RegisterDamage(Mobile from, int amount) + { + if (from?.Player != true) + { + return; + } + + ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(_damageEntries, from, out _); + value += amount; + + this.MarkDirty(); + } + + public virtual void AwardArtifact(Item artifact) + { + if (artifact == null) + { + return; + } + + if (DamageEntries.Count <= 0) + { + artifact.Delete(); + return; + } + + int totalDamage = 0; + + Dictionary validEntries = new Dictionary(); + + foreach (var (key, value) in DamageEntries) + { + if (IsEligible(key, artifact)) + { + validEntries.Add(key, value); + totalDamage += value; + } + } + + bool artifactGiven = false; + + do + { + int randomDamage = Utility.RandomMinMax(1, totalDamage); + + int checkDamage = 0; + + foreach (var (key, value) in validEntries) + { + checkDamage += value; + + if (checkDamage > randomDamage) + { + if (GiveArtifact(key, artifact)) { - m.SendMessage("You decide against going here because of the danger."); - } - else if (!m.Alive) - { - m.SendMessage("A magical force prevents ghosts from entering this region."); + artifactGiven = true; } else { - return true; + validEntries.Remove(key); } + + break; + } + } + + if (validEntries.Count == 0) // EVERYONE has a full backpack?!@ + { + artifact.Delete(); + break; + } + } while (!artifactGiven); + } + + public static bool GiveArtifact(Mobile to, Item artifact) + { + if (to == null || artifact == null) + { + return false; + } + + Container pack = to.Backpack; + + if (pack?.TryDropItem(to, artifact, false) != true) + { + return false; + } + + to.SendLocalizedMessage(1062317); // For your valor in combating the fallen beast, a special artifact has been bestowed on you. + return true; + } + + public bool IsEligible(Mobile m, Item Artifact) => + m.Player && m.Alive && m.Region != null && m.Region == m_Region && + m.Backpack?.CheckHold(m, Artifact, false) == true; + + private void Deserialize(IGenericReader reader, int version) + { + _damageEntries = new Dictionary(); + + _level = reader.ReadInt(); + _activatedByProximity = reader.ReadBool(); + _nextProximityTime = reader.ReadDeltaTime(); + _maxLevel = reader.ReadInt(); + _activatedByValor = reader.ReadBool(); + + int entries = reader.ReadInt(); + for (int i = 0; i < entries; ++i) + { + var m = reader.ReadEntity(); + var damage = reader.ReadInt(); + if (m != null) + { + _damageEntries.Add(m, damage); + } + } + + _confinedRoaming = reader.ReadBool(); + _idol = reader.ReadEntity(); + _hasBeenAdvanced = reader.ReadBool(); + _spawnArea = reader.ReadRect2D(); + _randomizeType = reader.ReadBool(); + _kills = reader.ReadInt(); + + _active = reader.ReadBool(); + _type = (ChampionSpawnType)reader.ReadInt(); + _creatures = reader.ReadEntityList(); + _redSkulls = reader.ReadEntityList(); + _whiteSkulls = reader.ReadEntityList(); + _platform = reader.ReadEntity(); + _altar = reader.ReadEntity(); + _expireDelay = reader.ReadTimeSpan(); + _expireTime = reader.ReadDeltaTime(); + _champion = reader.ReadEntity(); + _restartDelay = reader.ReadTimeSpan(); + + if (reader.ReadBool()) + { + _restartTime = reader.ReadDeltaTime(); + } + } + + [AfterDeserialization(false)] + private void AfterDeserialization() + { + if (_restartTime < Core.Now) + { + BeginRestart(_restartTime - Core.Now); + } + + if (_platform == null || _altar == null || _idol == null) + { + Delete(); + } + else if (_active) + { + _active = false; // Set back to false so we can properly start + Start(); + } + else if (AlwaysActive) + { + ReadyToActivate = true; + } + + Timer.StartTimer(TimeSpan.Zero, UpdateRegion); + } +} + +public class ChampionSpawnRegion : BaseRegion +{ + public ChampionSpawn Spawn { get; } + + public ChampionSpawnRegion(ChampionSpawn spawn) : + base(null, spawn.Map, Find(spawn.Location, spawn.Map), spawn.SpawnArea) => Spawn = spawn; + + public override bool AllowHousing(Mobile from, Point3D p) => false; + + public bool CanSpawn() => Spawn.EjectLocation != new Point3D(0, 0, 0) && Spawn.EjectMap != null; + + public override void AlterLightLevel(Mobile m, ref int global, ref int personal) + { + base.AlterLightLevel(m, ref global, ref personal); + global = Math.Max(global, 1 + Spawn.Level); //This is a guesstimate. TODO: Verify & get exact values // OSI testing: at 2 red skulls, light = 0x3 ; 1 red = 0x3.; 3 = 8; 9 = 0xD 8 = 0xD 12 = 0x12 10 = 0xD + } + + public override void OnEnter(Mobile m) + { + if (m.Player && m.AccessLevel == AccessLevel.Player && !Spawn.Active) + { + Region parent = Parent ?? this; + + if (Spawn.ReadyToActivate) + { + Spawn.Start(); + } + else if (Spawn.ProximitySpawn && !Spawn.ActivatedByProximity && Core.Now >= Spawn.NextProximityTime) + { + List players = parent.GetPlayers(); + List addresses = new List(); + for (var i = 0; i < players.Count; i++) + { + if (players[i].AccessLevel == AccessLevel.Player && players[i].NetState != null && + !addresses.Contains(players[i].NetState.Address) && !((PlayerMobile)players[i]).Young) + { + addresses.Add(players[i].NetState.Address); + } + } + + if (addresses.Count >= 15) + { + foreach (Mobile player in players) + { + player.SendMessage(0x20, Spawn.BroadcastMessage); + } + + Spawn.ActivatedByProximity = true; + Spawn.BeginRestart(TimeSpan.FromMinutes(5.0)); + } + } + } + } + + public override bool OnMoveInto(Mobile m, Direction d, Point3D newLocation, Point3D oldLocation) + { + if (base.OnMoveInto(m, d, newLocation, oldLocation)) + { + if (m.Player) + { + if (((PlayerMobile)m).Young) + { + m.SendMessage("You decide against going here because of the danger."); + } + else if (!m.Alive) + { + m.SendMessage("A magical force prevents ghosts from entering this region."); } else { return true; } } + else + { + return true; + } + } + return false; + } + + public override bool OnBeforeDeath(Mobile m) + { + if (Parent?.OnBeforeDeath(m) == false) + { return false; } - public override bool OnBeforeDeath(Mobile m) + if (m.Player) //Give them 5 minutes to resurrect, then they are booted. { - if (Parent?.OnBeforeDeath(m) == false) - { - return false; - } - - if (m.Player) //Give them 5 minutes to resurrect, then they are booted. - { - m.SendMessage("A magical force encompasses you, attempting to force you out of the area."); - new EjectTimer(m, this).Start(); - } - - return true; + m.SendMessage("A magical force encompasses you, attempting to force you out of the area."); + new EjectTimer(m, this).Start(); } - private class EjectTimer : Timer + return true; + } + + private class EjectTimer : Timer + { + private readonly Mobile m_From; + private readonly ChampionSpawnRegion m_Region; + + public EjectTimer(Mobile from, ChampionSpawnRegion region) : base(TimeSpan.FromMinutes(5.0)) { - private readonly Mobile m_From; - private readonly ChampionSpawnRegion m_Region; + m_From = from; + m_Region = region; + } - public EjectTimer(Mobile from, ChampionSpawnRegion region) : base(TimeSpan.FromMinutes(5.0)) + protected override void OnTick() + { + //See if they are dead, or logged out! + if (m_Region.Spawn != null && m_Region.CanSpawn() && !m_From.Alive) { - m_From = from; - m_Region = region; - } - - protected override void OnTick() - { - //See if they are dead, or logged out! - if (m_Region.Spawn != null && m_Region.CanSpawn() && !m_From.Alive) + if (m_From.NetState != null) { - if (m_From.NetState != null) + if (m_From.Region.IsPartOf(m_Region)) { - if (m_From.Region.IsPartOf(m_Region)) - { - m_From.MoveToWorld(m_Region.Spawn.EjectLocation, m_Region.Spawn.EjectMap); - m_From.SendMessage("A magical force forces you out of the area."); - } - } - else if (Find(m_From.LogoutLocation, m_From.LogoutMap).IsPartOf(m_Region)) - { - m_From.LogoutLocation = m_Region.Spawn.EjectLocation; - m_From.LogoutMap = m_Region.Spawn.EjectMap; + m_From.MoveToWorld(m_Region.Spawn.EjectLocation, m_Region.Spawn.EjectMap); + m_From.SendMessage("A magical force forces you out of the area."); } } + else if (Find(m_From.LogoutLocation, m_From.LogoutMap).IsPartOf(m_Region)) + { + m_From.LogoutLocation = m_Region.Spawn.EjectLocation; + m_From.LogoutMap = m_Region.Spawn.EjectMap; + } } } } +} - public class IdolOfTheChampion : Item +[SerializationGenerator(0, false)] +public partial class IdolOfTheChampion : Item +{ + [SerializableField(0)] + private ChampionSpawn _spawn; + + public override string DefaultName => "Idol of the Champion"; + + public IdolOfTheChampion(ChampionSpawn spawn): base(0x1F18) { - public ChampionSpawn Spawn { get; private set; } + _spawn = spawn; + Movable = false; + } - public override string DefaultName => "Idol of the Champion"; + public override void OnAfterDelete() + { + base.OnAfterDelete(); + _spawn?.Delete(); + } - public IdolOfTheChampion(ChampionSpawn spawn): base(0x1F18) + [AfterDeserialization(false)] + private void AfterDeserialization() + { + if (_spawn == null) { - Spawn = spawn; - Movable = false; - } - - public override void OnAfterDelete() - { - base.OnAfterDelete(); - - Spawn?.Delete(); - } - - public IdolOfTheChampion(Serial serial) : base(serial) - { - } - - public override void Serialize(IGenericWriter writer) - { - base.Serialize(writer); - - writer.Write(0); // version - - writer.Write(Spawn); - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - - int version = reader.ReadInt(); - - switch (version) - { - case 0: - { - Spawn = reader.ReadEntity(); - - if (Spawn == null) - { - Delete(); - } - - break; - } - } + Delete(); } } } diff --git a/Projects/UOContent/Engines/CannedEvil/ChampionSpawnInfo.cs b/Projects/UOContent/Engines/CannedEvil/ChampionSpawnInfo.cs new file mode 100644 index 000000000..0754076d9 --- /dev/null +++ b/Projects/UOContent/Engines/CannedEvil/ChampionSpawnInfo.cs @@ -0,0 +1,150 @@ +using System; +using Server.Mobiles; + +namespace Server.Engines.CannedEvil; + +public enum ChampionSpawnType +{ + Abyss, + Arachnid, + ColdBlood, + ForestLord, + VerminHorde, + UnholyTerror, + SleepingDragon, + Glade, + Corrupt, + // Unliving, + // Pit, + // DragonTurtle, + // Khaldun +} + +public class ChampionSpawnInfo +{ + public ChampionSpawnType Type { get; } + + public Type Champion { get; } + + public Type[][] SpawnTypes { get; } + + public TextDefinition[] LevelNames { get; } + + public ChampionSpawnInfo(ChampionSpawnType type, Type champion, TextDefinition[] levelNames, Type[][] spawnTypes) + { + Type = type; + Champion = champion; + LevelNames = levelNames; + SpawnTypes = spawnTypes; + } + + // After updating this table, make sure to update ChampionTitleContext. + public static ChampionSpawnInfo[] Table { get; } = { + new(ChampionSpawnType.Abyss, typeof(Semidar), new TextDefinition[]{ 1113118, 1113107, 1113096 }, new[] + { + new[]{ typeof(GreaterMongbat), typeof(Imp) }, // Level 1 + new[]{ typeof(Gargoyle), typeof(Harpy) }, // Level 2 + new[]{ typeof(FireGargoyle), typeof(StoneGargoyle) }, // Level 3 + new[]{ typeof(Daemon), typeof(Succubus) } // Level 4 + }), + new(ChampionSpawnType.Arachnid, typeof(Mephitis), new TextDefinition[]{ 1113117, 1113106, 1113095 }, new[] + { + new[]{ typeof(Scorpion), typeof(GiantSpider) }, // Level 1 + new[]{ typeof(TerathanDrone), typeof(TerathanWarrior) }, // Level 2 + new[]{ typeof(DreadSpider), typeof(TerathanMatriarch) }, // Level 3 + new[]{ typeof(PoisonElemental), typeof(TerathanAvenger) } // Level 4 + }), + new(ChampionSpawnType.ColdBlood, typeof(Rikktor), new TextDefinition[]{ 1113115, 1113104, 1113093 }, new[] + { + new[]{ typeof(Lizardman), typeof(GiantSerpent) }, // Level 1 + new[]{ typeof(LavaLizard), typeof(OphidianWarrior) }, // Level 2 + new[]{ typeof(Drake), typeof(OphidianArchmage) }, // Level 3 + new[]{ typeof(Dragon), typeof(OphidianKnight) } // Level 4 + }), + new(ChampionSpawnType.ForestLord, typeof(LordOaks), new TextDefinition[]{ 1113116, 1113105, 1113094 }, new[] + { + new[]{ typeof(Pixie), typeof(ShadowWisp) }, // Level 1 + new[]{ typeof(Kirin), typeof(Wisp) }, // Level 2 + new[]{ typeof(Centaur), typeof(Unicorn) }, // Level 3 + new[]{ typeof(EtherealWarrior), typeof(SerpentineDragon) } // Level 4 + }), + new(ChampionSpawnType.VerminHorde, typeof(Barracoon), new TextDefinition[]{ 1113119, 1113108, 1113097 }, new[] + { + new[]{ typeof(GiantRat), typeof(Slime) }, // Level 1 + new[]{ typeof(DireWolf), typeof(Ratman) }, // Level 2 + new[]{ typeof(HellHound), typeof(RatmanMage) }, // Level 3 + new[]{ typeof(RatmanArcher), typeof(SilverSerpent) } // Level 4 + }), + new(ChampionSpawnType.UnholyTerror, typeof(Neira), new TextDefinition[]{ 1113120, 1113109, 1113098 }, new[] + { + Core.AOS ? // Level 1 + new[]{ typeof(Bogle), typeof(Ghoul), typeof(Shade), typeof(Spectre), typeof(Wraith) } + : new[]{ typeof(Ghoul), typeof(Shade), typeof(Spectre), typeof(Wraith) }, + + new[]{ typeof(BoneMagi), typeof(Mummy), typeof(SkeletalMage) }, // Level 2 + new[]{ typeof(BoneKnight), typeof(Lich), typeof(SkeletalKnight) }, // Level 3 + new[]{ typeof(LichLord), typeof(RottingCorpse) } // Level 4 + }), + new(ChampionSpawnType.SleepingDragon, typeof(Serado), new TextDefinition[]{ 1113121, 1113110, 1113099 }, new[] + { + new[]{ typeof(DeathwatchBeetleHatchling), typeof(Lizardman) }, // Level 1 + new[]{ typeof(DeathwatchBeetle), typeof(Kappa) }, // Level 2 + new[]{ typeof(LesserHiryu), typeof(RevenantLion) }, // Level 3 + new[]{ typeof(Hiryu), typeof(Oni) } // Level 4 + }), + new(ChampionSpawnType.Glade, typeof(Twaulo), new TextDefinition[]{ 1113123, 1113112, 1113101 }, new[] + { + new[]{ typeof(Pixie), typeof(ShadowWisp) }, // Level 1 + new[]{ typeof(Centaur), typeof(MLDryad) }, // Level 2 + new[]{ typeof(Satyr), typeof(CuSidhe) }, // Level 3 + new[]{ typeof(FeralTreefellow), typeof(RagingGrizzlyBear) } // Level 4 + }), + new(ChampionSpawnType.Corrupt, typeof(Ilhenir), new TextDefinition[]{ 1113122, 1113111, 1113100 }, new[] + { + new[]{ typeof(PlagueSpawn), typeof(Bogling) }, // Level 1 + new[]{ typeof(PlagueBeast), typeof(BogThing) }, // Level 2 + new[]{ typeof(PlagueBeastLord), typeof(InterredGrizzle) }, // Level 3 + new[]{ typeof(FetidEssence), typeof(PestilentBandage) } // Level 4 + }), + // new(ChampionSpawnType.Unliving, typeof(PrimevalLich), new TextDefinition[]{ 1113124, 1113113, 1113102 }, new[] + // { + // new[]{ typeof(GoreFiend), typeof(VampireBat) }, + // new[]{ typeof(FleshGolem), typeof(DarkWisp) }, + // new[]{ typeof(UndeadGargoyle), typeof(Wight) }, + // new[]{ typeof(SkeletalDrake), typeof(DreamWraith) } + // }), + // new(ChampionSpawnType.Pit, typeof(AbyssalInfernal), new TextDefinition[]{ 1113125, 1113114, 1113103 }, new[] + // { + // new[]{ typeof(HordeMinion), typeof(ChaosDaemon) }, + // new[]{ typeof(StoneHarpy), typeof(ArcaneDaemon) }, + // new[]{ typeof(PitFiend), typeof(Moloch) }, + // new[]{ typeof(ArchDaemon), typeof(AbyssalAbomination) } + // }), + // new(ChampionSpawnType.Valley, typeof(DragonTurtle), new TextDefinition[]{ "Explorer", "Huntsman", "Msafiri" }, new[] + // { + // new[]{ typeof(MyrmidexDrone), typeof(MyrmidexLarvae) }, + // new[]{ typeof(SilverbackGorilla), typeof(WildTiger) }, + // new[]{ typeof(GreaterPhoenix ), typeof(Infernus) }, + // new[]{ typeof(Dimetrosaur), typeof(Allosaurus) } + // }), + // new(ChampionSpawnType.Khaldun, typeof(KhalAnkur), new TextDefinition[]{ "Banisher", "Enforcer", "Eradicator" }, new[] + // { + // new[]{ typeof(SkelementalKnight), typeof(KhaldunBlood) }, + // new[]{ typeof(SkelementalMage), typeof(Viscera) }, + // new[]{ typeof(CultistAmbusher), typeof(ShadowFiend) }, + // new[]{ typeof(KhalAnkurWarriors) } + // }) + }; + + public static ChampionSpawnInfo GetInfo(ChampionSpawnType type) + { + var v = (int)type; + + if (v < 0 || v >= Table.Length) + { + v = 0; + } + + return Table[v]; + } +} diff --git a/Projects/UOContent/Engines/CannedEvil/ChampionSpawnType.cs b/Projects/UOContent/Engines/CannedEvil/ChampionSpawnType.cs deleted file mode 100755 index 546fc46ca..000000000 --- a/Projects/UOContent/Engines/CannedEvil/ChampionSpawnType.cs +++ /dev/null @@ -1,134 +0,0 @@ -/************************************************************************* - * ModernUO * - * Copyright 2019-2022 - ModernUO Development Team * - * Email: hi@modernuo.com * - * File: ChampionSpawnType.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 . * - *************************************************************************/ - -using System; -using Server.Mobiles; - -namespace Server.Engines.CannedEvil -{ - public enum ChampionSpawnType - { - Abyss, - Arachnid, - ColdBlood, - ForestLord, - VerminHorde, - UnholyTerror, - SleepingDragon, - Glade, - Pestilence, - Graveyard - } - - public class ChampionSpawnInfo - { - public string Name { get; } - - public Type Champion { get; } - - public Type[][] SpawnTypes { get; } - - public string[] LevelNames { get; } - - public ChampionSpawnInfo(string name, Type champion, string[] levelNames, Type[][] spawnTypes) - { - Name = name; - Champion = champion; - LevelNames = levelNames; - SpawnTypes = spawnTypes; - } - - public static ChampionSpawnInfo[] Table { get; } = { - new("Abyss", typeof(Semidar), new[]{ "Foe", "Assassin", "Conqueror" }, new[] - { - new[]{ typeof(GreaterMongbat), typeof(Imp) }, // Level 1 - new[]{ typeof(Gargoyle), typeof(Harpy) }, // Level 2 - new[]{ typeof(FireGargoyle), typeof(StoneGargoyle) }, // Level 3 - new[]{ typeof(Daemon), typeof(Succubus) } // Level 4 - }), - new("Arachnid", typeof(Mephitis), new[]{ "Bane", "Killer", "Vanquisher" }, new[] - { - new[]{ typeof(Scorpion), typeof(GiantSpider) }, // Level 1 - new[]{ typeof(TerathanDrone), typeof(TerathanWarrior) }, // Level 2 - new[]{ typeof(DreadSpider), typeof(TerathanMatriarch) }, // Level 3 - new[]{ typeof(PoisonElemental), typeof(TerathanAvenger) } // Level 4 - }), - new("Cold Blood", typeof(Rikktor), new[]{ "Blight", "Slayer", "Destroyer" }, new[] - { - new[]{ typeof(Lizardman), typeof(GiantSerpent) }, // Level 1 - new[]{ typeof(LavaLizard), typeof(OphidianWarrior) }, // Level 2 - new[]{ typeof(Drake), typeof(OphidianArchmage) }, // Level 3 - new[]{ typeof(Dragon), typeof(OphidianKnight) } // Level 4 - }), - new("Forest Lord", typeof(LordOaks), new[]{ "Enemy", "Curse", "Slaughterer" }, new[] - { - new[]{ typeof(Pixie), typeof(ShadowWisp) }, // Level 1 - new[]{ typeof(Kirin), typeof(Wisp) }, // Level 2 - new[]{ typeof(Centaur), typeof(Unicorn) }, // Level 3 - new[]{ typeof(EtherealWarrior), typeof(SerpentineDragon) } // Level 4 - }), - new("Vermin Horde", typeof(Barracoon), new[]{ "Adversary", "Subjugator", "Eradicator" }, new[] - { - new[]{ typeof(GiantRat), typeof(Slime) }, // Level 1 - new[]{ typeof(DireWolf), typeof(Ratman) }, // Level 2 - new[]{ typeof(HellHound), typeof(RatmanMage) }, // Level 3 - new[]{ typeof(RatmanArcher), typeof(SilverSerpent) } // Level 4 - }), - new("Unholy Terror", typeof(Neira), new[]{ "Scourge", "Punisher", "Nemesis" }, new[] - { - Core.AOS ? // Level 1 - new[]{ typeof(Bogle), typeof(Ghoul), typeof(Shade), typeof(Spectre), typeof(Wraith) } - : new[]{ typeof(Ghoul), typeof(Shade), typeof(Spectre), typeof(Wraith) }, - - new[]{ typeof(BoneMagi), typeof(Mummy), typeof(SkeletalMage) }, // Level 2 - new[]{ typeof(BoneKnight), typeof(Lich), typeof(SkeletalKnight) }, // Level 3 - new[]{ typeof(LichLord), typeof(RottingCorpse) } // Level 4 - }), - new("Sleeping Dragon", typeof(Serado), new[]{ "Rival", "Challenger", "Antagonist" } , new[] - { - new[]{ typeof(DeathwatchBeetleHatchling), typeof(Lizardman) }, // Level 1 - new[]{ typeof(DeathwatchBeetle), typeof(Kappa) }, // Level 2 - new[]{ typeof(LesserHiryu), typeof(RevenantLion) }, // Level 3 - new[]{ typeof(Hiryu), typeof(Oni) } // Level 4 - }), - new("Glade", typeof(Twaulo), new[]{ "Banisher", "Enforcer", "Eradicator" } , new[] - { - new[]{ typeof(Pixie), typeof(ShadowWisp) }, // Level 1 - new[]{ typeof(Centaur), typeof(MLDryad) }, // Level 2 - new[]{ typeof(Satyr), typeof(CuSidhe) }, // Level 3 - new[]{ typeof(FeralTreefellow), typeof(RagingGrizzlyBear) } // Level 4 - }), - new("The Corrupt", typeof(Ilhenir), new[]{ "Cleanser", "Expunger", "Depurator" } , new[] - { - new[]{ typeof(PlagueSpawn), typeof(Bogling) }, // Level 1 - new[]{ typeof(PlagueBeast), typeof(BogThing) }, // Level 2 - new[]{ typeof(PlagueBeastLord), typeof(InterredGrizzle) }, // Level 3 - new[]{ typeof(FetidEssence), typeof(PestilentBandage) } // Level 4 - }), - }; - - public static ChampionSpawnInfo GetInfo(ChampionSpawnType type) - { - var v = (int)type; - - if (v < 0 || v >= Table.Length) - { - v = 0; - } - - return Table[v]; - } - } -} diff --git a/Projects/UOContent/Engines/CannedEvil/ChampionTitle.cs b/Projects/UOContent/Engines/CannedEvil/ChampionTitle.cs new file mode 100644 index 000000000..c1970a449 --- /dev/null +++ b/Projects/UOContent/Engines/CannedEvil/ChampionTitle.cs @@ -0,0 +1,30 @@ +using System; +using ModernUO.Serialization; + +namespace Server.Engines.CannedEvil; + +[SerializationGenerator(0)] +public partial class ChampionTitle +{ + [EncodedInt] + [SerializableField(0)] + private int _value; + + // TODO: Change to delta time + [SerializableField(1)] + private DateTime _lastDecay; + + public bool Atrophy(int value) + { + var before = Value; + + Value -= Math.Min(value, before); + + if (before != Value) + { + LastDecay = Core.Now; + } + + return Value > 0; + } +} diff --git a/Projects/UOContent/Engines/CannedEvil/ChampionTitleContext.cs b/Projects/UOContent/Engines/CannedEvil/ChampionTitleContext.cs new file mode 100644 index 000000000..27dd32213 --- /dev/null +++ b/Projects/UOContent/Engines/CannedEvil/ChampionTitleContext.cs @@ -0,0 +1,379 @@ +using System; +using System.Runtime.CompilerServices; +using ModernUO.Serialization; +using Server.Mobiles; + +namespace Server.Engines.CannedEvil; + +[PropertyObject] +[SerializationGenerator(1)] +public partial class ChampionTitleContext +{ + private const int LossAmount = 90; + private static readonly TimeSpan LossDelay = TimeSpan.FromDays(1.0); + + [SerializableField(0)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _harrower; + + private PlayerMobile _player; + + public PlayerMobile Player => _player; + + public ChampionTitleContext(PlayerMobile player) => _player = player; + + // Visible to PublicMobile for migrations + internal void Deserialize(IGenericReader reader, int version) + { + _harrower = reader.ReadEncodedInt(); + + var length = reader.ReadEncodedInt(); + + if (length == 0) + { + return; + } + + // If the list/enum for ChampionSpawnInfo changes, then this has to change for migrations + for (var i = 0; i < length; i++) + { + var type = (ChampionSpawnType)i; + ref var title = ref GetTitleValueRef(type); + + if (Unsafe.IsNullRef(ref title)) + { + throw new NotImplementedException($"Cannot find ChampionSpawnType value {type}."); + } + + title = new ChampionTitle(); + title.Deserialize(reader); + } + } + + [SerializableField(1)] + private ChampionTitle _abyss; + + [SerializableFieldSaveFlag(1)] + private bool ShouldSerializeAbyss() => _abyss != null; + + [CommandProperty(AccessLevel.GameMaster)] + public int AbyssValue + { + get => GetTitle(ChampionSpawnType.Abyss)?.Value ?? 0; + set => SetValue(ChampionSpawnType.Abyss, value); + } + + [CommandProperty(AccessLevel.GameMaster)] + public DateTime AbyssLastDecay + { + get => GetTitle(ChampionSpawnType.Abyss)?.LastDecay ?? DateTime.MinValue; + set => SetLastDecay(ChampionSpawnType.Abyss, value); + } + + [SerializableField(2)] + private ChampionTitle _arachnid; + + [SerializableFieldSaveFlag(2)] + private bool ShouldSerializeArachnid() => _arachnid != null; + + [CommandProperty(AccessLevel.GameMaster)] + public int ArachnidValue + { + get => GetTitle(ChampionSpawnType.Arachnid)?.Value ?? 0; + set => SetValue(ChampionSpawnType.Arachnid, value); + } + + [CommandProperty(AccessLevel.GameMaster)] + public DateTime ArachnidLastDecay + { + get => GetTitle(ChampionSpawnType.Arachnid)?.LastDecay ?? DateTime.MinValue; + set => SetLastDecay(ChampionSpawnType.Arachnid, value); + } + + [SerializableField(3)] + private ChampionTitle _coldBlood; + + [SerializableFieldSaveFlag(3)] + private bool ShouldSerializeColdBlood() => _coldBlood != null; + + [CommandProperty(AccessLevel.GameMaster)] + public int ColdBloodValue + { + get => GetTitle(ChampionSpawnType.ColdBlood)?.Value ?? 0; + set => SetValue(ChampionSpawnType.ColdBlood, value); + } + + [CommandProperty(AccessLevel.GameMaster)] + public DateTime ColdBloodLastDecay + { + get => GetTitle(ChampionSpawnType.ColdBlood)?.LastDecay ?? DateTime.MinValue; + set => SetLastDecay(ChampionSpawnType.ColdBlood, value); + } + + [SerializableField(4)] + private ChampionTitle _forestLord; + + [SerializableFieldSaveFlag(4)] + private bool ShouldSerializeForestLord() => _forestLord != null; + + [CommandProperty(AccessLevel.GameMaster)] + public int ForestLordValue + { + get => GetTitle(ChampionSpawnType.ForestLord)?.Value ?? 0; + set => SetValue(ChampionSpawnType.ForestLord, value); + } + + [CommandProperty(AccessLevel.GameMaster)] + public DateTime ForestLordLastDecay + { + get => GetTitle(ChampionSpawnType.ForestLord)?.LastDecay ?? DateTime.MinValue; + set => SetLastDecay(ChampionSpawnType.ForestLord, value); + } + + [SerializableField(5)] + private ChampionTitle _verminHorde; + + [SerializableFieldSaveFlag(5)] + private bool ShouldSerializeVerminHorde() => _verminHorde != null; + + [CommandProperty(AccessLevel.GameMaster)] + public int VerminHordeValue + { + get => GetTitle(ChampionSpawnType.VerminHorde)?.Value ?? 0; + set => SetValue(ChampionSpawnType.VerminHorde, value); + } + + [CommandProperty(AccessLevel.GameMaster)] + public DateTime VerminHordeLastDecay + { + get => GetTitle(ChampionSpawnType.VerminHorde)?.LastDecay ?? DateTime.MinValue; + set => SetLastDecay(ChampionSpawnType.VerminHorde, value); + } + + [SerializableField(6)] + private ChampionTitle _unholyTerror; + + [SerializableFieldSaveFlag(6)] + private bool ShouldSerializeUnholyTerror() => _unholyTerror != null; + + [CommandProperty(AccessLevel.GameMaster)] + public int UnholyTerrorValue + { + get => GetTitle(ChampionSpawnType.UnholyTerror)?.Value ?? 0; + set => SetValue(ChampionSpawnType.UnholyTerror, value); + } + + [CommandProperty(AccessLevel.GameMaster)] + public DateTime UnholyTerrorLastDecay + { + get => GetTitle(ChampionSpawnType.UnholyTerror)?.LastDecay ?? DateTime.MinValue; + set => SetLastDecay(ChampionSpawnType.UnholyTerror, value); + } + + [SerializableField(7)] + private ChampionTitle _sleepingDragon; + + [SerializableFieldSaveFlag(7)] + private bool ShouldSerializeSleepingDragon() => _sleepingDragon != null; + + [CommandProperty(AccessLevel.GameMaster)] + public int SleepingDragonValue + { + get => GetTitle(ChampionSpawnType.SleepingDragon)?.Value ?? 0; + set => SetValue(ChampionSpawnType.SleepingDragon, value); + } + + [CommandProperty(AccessLevel.GameMaster)] + public DateTime SleepingDragonLastDecay + { + get => GetTitle(ChampionSpawnType.SleepingDragon)?.LastDecay ?? DateTime.MinValue; + set => SetLastDecay(ChampionSpawnType.SleepingDragon, value); + } + + [SerializableField(8)] + private ChampionTitle _corrupt; + + [SerializableFieldSaveFlag(8)] + private bool ShouldSerializeCorrupt() => _corrupt != null; + + [CommandProperty(AccessLevel.GameMaster)] + public int CorruptValue + { + get => GetTitle(ChampionSpawnType.Corrupt)?.Value ?? 0; + set => SetValue(ChampionSpawnType.Corrupt, value); + } + + [CommandProperty(AccessLevel.GameMaster)] + public DateTime CorruptLastDecay + { + get => GetTitle(ChampionSpawnType.Corrupt)?.LastDecay ?? DateTime.MinValue; + set => SetLastDecay(ChampionSpawnType.Corrupt, value); + } + + [SerializableField(9)] + private ChampionTitle _glade; + + [SerializableFieldSaveFlag(9)] + private bool ShouldSerializeGlade() => _glade != null; + + [CommandProperty(AccessLevel.GameMaster)] + public int GladeValue + { + get => GetTitle(ChampionSpawnType.Glade)?.Value ?? 0; + set => SetValue(ChampionSpawnType.Glade, value); + } + + [CommandProperty(AccessLevel.GameMaster)] + public DateTime GladeLastDecay + { + get => GetTitle(ChampionSpawnType.Glade)?.LastDecay ?? DateTime.MinValue; + set => SetLastDecay(ChampionSpawnType.Glade, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ChampionTitle GetTitle(ChampionSpawnType type) => GetTitleValueRef(type); + + // Get a pointer to the title so we can manipulate it internally such as nullifying it. + // Using this technique instead of an array decouples the context from changes to ChampionSpawnInfo.Table indexes + private ref ChampionTitle GetTitleValueRef(ChampionSpawnType type) + { + switch (type) + { + case ChampionSpawnType.Abyss: + { + return ref _abyss; + } + case ChampionSpawnType.Arachnid: + { + return ref _arachnid; + } + case ChampionSpawnType.ColdBlood: + { + return ref _coldBlood; + } + case ChampionSpawnType.ForestLord: + { + return ref _forestLord; + } + case ChampionSpawnType.VerminHorde: + { + return ref _verminHorde; + } + case ChampionSpawnType.UnholyTerror: + { + return ref _unholyTerror; + } + case ChampionSpawnType.SleepingDragon: + { + return ref _sleepingDragon; + } + case ChampionSpawnType.Glade: + { + return ref _glade; + } + case ChampionSpawnType.Corrupt: + { + return ref _corrupt; + } + default: + { + return ref Unsafe.NullRef(); + } + } + } + + public ChampionTitle TryGetOrCreateTitle(ChampionSpawnType type) + { + ref var title = ref GetTitleValueRef(type); + if (Unsafe.IsNullRef(ref title)) + { + return null; + } + + return title ??= new ChampionTitle(); + } + + public void SetValue(ChampionSpawnType type, int value) + { + ref var title = ref GetTitleValueRef(type); + if (Unsafe.IsNullRef(ref title)) + { + return; + } + + if (title != null) + { + if (value <= 0) + { + title = null; + + _player.InvalidateProperties(); + return; + } + } + else + { + title = new ChampionTitle(); + } + + title.Value = value; + title.LastDecay = Core.Now; + _player.InvalidateProperties(); + } + + public void SetLastDecay(ChampionSpawnType type, DateTime value) + { + var title = TryGetOrCreateTitle(type); + if (title != null) + { + title.LastDecay = value; + } + } + + public void Award(ChampionSpawnType type, int value) + { + var title = TryGetOrCreateTitle(type); + if (title == null) + { + return; + } + + title.Value += value; + if (title.LastDecay == DateTime.MinValue) + { + title.LastDecay = Core.Now; + } + } + + public bool CheckAtrophy() + { + var valuesUsed = _harrower > 0; + + for (var i = 0; i < ChampionSpawnInfo.Table.Length; i++) + { + ref var title = ref GetTitleValueRef(ChampionSpawnInfo.Table[i].Type); + if (Unsafe.IsNullRef(ref title)) + { + continue; + } + + var decay = title.LastDecay; + if (decay > DateTime.MinValue && decay + LossDelay < Core.Now) + { + // Use bitwise-or to force-execute the Atrophy function + var isUsed = title.Atrophy(LossAmount); + + if (!isUsed) + { + title = null; + } + + valuesUsed |= isUsed; + } + } + + return valuesUsed; + } + + public override string ToString() => "..."; +} diff --git a/Projects/UOContent/Engines/CannedEvil/ChampionTitleSystem.cs b/Projects/UOContent/Engines/CannedEvil/ChampionTitleSystem.cs new file mode 100644 index 000000000..bbb8f92bc --- /dev/null +++ b/Projects/UOContent/Engines/CannedEvil/ChampionTitleSystem.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Server.Collections; +using Server.Mobiles; + +namespace Server.Engines.CannedEvil; + +public static class ChampionTitleSystem +{ + // All of the players with murders + private static readonly Dictionary _championTitleContexts = new(); + + private static readonly Timer _championTitleTimer = new ChampionTitleTimer(); + + public static void Configure() + { + GenericPersistence.Register("ChampionTitles", Serialize, Deserialize); + } + + public static void Initialize() + { + EventSink.PlayerDeleted += OnPlayerDeleted; + + _championTitleTimer.Start(); + } + + private static void OnPlayerDeleted(Mobile m) + { + if (m is PlayerMobile pm) + { + _championTitleContexts.Remove(pm); + } + } + + private static void Deserialize(IGenericReader reader) + { + var version = reader.ReadEncodedInt(); + + var count = reader.ReadEncodedInt(); + for (var i = 0; i < count; ++i) + { + var context = new ChampionTitleContext(reader.ReadEntity()); + context.Deserialize(reader); + + _championTitleContexts.Add(context.Player, context); + } + } + + private static void Serialize(IGenericWriter writer) + { + writer.WriteEncodedInt(0); // version + + writer.WriteEncodedInt(_championTitleContexts.Count); + foreach (var (m, context) in _championTitleContexts) + { + writer.Write(m); + context.Serialize(writer); + } + } + + public static bool GetChampionTitleContext(this PlayerMobile player, out ChampionTitleContext context) => + _championTitleContexts.TryGetValue(player, out context); + + public static ChampionTitleContext GetOrCreateChampionTitleContext(this PlayerMobile player) + { + ref var context = ref CollectionsMarshal.GetValueRefOrAddDefault(_championTitleContexts, player, out var exists); + if (!exists) + { + context = new ChampionTitleContext(player); + } + + return context; + } + + // Called when killing a harrower. Will give a minimum of 1 point. + public static void AwardHarrowerTitle(PlayerMobile pm) + { + var context = pm.GetOrCreateChampionTitleContext(); + + var count = 1; + for (var i = 0; i < ChampionSpawnInfo.Table.Length; i++) + { + var title = context.GetTitle(ChampionSpawnInfo.Table[i].Type); + if (title?.Value > 900) + { + count++; + } + } + + context.Harrower = Math.Max(count, context.Harrower); // Harrower titles never decay. + } + + public static int GetChampionTitleLabel(this PlayerMobile player) + { + if (!player.GetChampionTitleContext(out var context)) + { + return 0; + } + + if (context.Harrower > 0) + { + return 1113082 + Math.Min(context.Harrower, 10); + } + + var highestValue = 0; + var highestType = 0; + + for (var i = 0; i < ChampionSpawnInfo.Table.Length; i++) + { + var t = context.GetTitle(ChampionSpawnInfo.Table[i].Type); + if (t == null) + { + continue; + } + + var v = t.Value; + + if (v > highestValue) + { + highestValue = v; + highestType = i; + } + } + + var offset = highestValue switch + { + > 800 => 3, + > 300 => highestValue / 300, + _ => 0 + }; + + if (offset > 0) + { + var champInfo = ChampionSpawnInfo.Table[highestType]; + var championLevelName = champInfo.LevelNames[Math.Min(offset, champInfo.LevelNames.Length) - 1]; + return championLevelName.Number; + } + + return 0; + } + + private class ChampionTitleTimer : Timer + { + public ChampionTitleTimer() : base(TimeSpan.FromMinutes(5.0), TimeSpan.FromMinutes(5.0)) + { + } + + protected override void OnTick() + { + if (_championTitleContexts.Count == 0) + { + return; + } + + using var queue = PooledRefQueue.Create(); + + foreach (var context in _championTitleContexts.Values) + { + if (!context.CheckAtrophy()) + { + queue.Enqueue(context.Player); + } + } + + while (queue.Count > 0) + { + _championTitleContexts.Remove((PlayerMobile)queue.Dequeue()); + } + } + } +} diff --git a/Projects/UOContent/Engines/CannedEvil/DungeonChampionSpawn.cs b/Projects/UOContent/Engines/CannedEvil/DungeonChampionSpawn.cs index f1bd4a0fe..eeff89c3c 100644 --- a/Projects/UOContent/Engines/CannedEvil/DungeonChampionSpawn.cs +++ b/Projects/UOContent/Engines/CannedEvil/DungeonChampionSpawn.cs @@ -13,42 +13,30 @@ * along with this program. If not, see . * *************************************************************************/ -namespace Server.Engines.CannedEvil +using ModernUO.Serialization; + +namespace Server.Engines.CannedEvil; + +[SerializationGenerator(0)] +public partial class DungeonChampionSpawn : ChampionSpawn { - public class DungeonChampionSpawn : ChampionSpawn + [Constructible] + public DungeonChampionSpawn() { - [Constructible] - public DungeonChampionSpawn() - { - CannedEvilTimer.AddSpawn(this); - } + CannedEvilTimer.AddSpawn(this); + } - public DungeonChampionSpawn(Serial serial) : base(serial) - { - CannedEvilTimer.AddSpawn(this); - } + public DungeonChampionSpawn(Serial serial) + { + CannedEvilTimer.AddSpawn(this); + } - public override bool ProximitySpawn => true; - public override bool AlwaysActive => false; + public override bool ProximitySpawn => true; + public override bool AlwaysActive => false; - public override void Serialize(IGenericWriter writer) - { - base.Serialize(writer); - - writer.WriteEncodedInt(0); //version - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - - int version = reader.ReadEncodedInt(); - } - - public override void OnAfterDelete() - { - base.OnAfterDelete(); - CannedEvilTimer.RemoveSpawn(this); - } + public override void OnAfterDelete() + { + base.OnAfterDelete(); + CannedEvilTimer.RemoveSpawn(this); } } diff --git a/Projects/UOContent/Engines/CannedEvil/HarrowerGate.cs b/Projects/UOContent/Engines/CannedEvil/HarrowerGate.cs index aa11225df..fc862cab8 100644 --- a/Projects/UOContent/Engines/CannedEvil/HarrowerGate.cs +++ b/Projects/UOContent/Engines/CannedEvil/HarrowerGate.cs @@ -1,60 +1,32 @@ -namespace Server.Items +using ModernUO.Serialization; + +namespace Server.Items; + +[SerializationGenerator(0, false)] +public partial class HarrowerGate : Moongate { - public class HarrowerGate : Moongate + [SerializableField(0)] + private Mobile _harrower; + + public HarrowerGate(Mobile harrower, Point3D loc, Map map, Point3D targLoc, Map targMap) : base(targLoc, targMap) { - private Mobile m_Harrower; + _harrower = harrower; - public HarrowerGate(Mobile harrower, Point3D loc, Map map, Point3D targLoc, Map targMap) : base(targLoc, targMap) + Dispellable = false; + ItemID = 0x1FD4; + Light = LightType.Circle300; + + MoveToWorld(loc, map); + } + + public override int LabelNumber => 1049498; // dark moongate + + [AfterDeserialization(false)] + private void AfterDeserialization() + { + if (_harrower == null) { - m_Harrower = harrower; - - Dispellable = false; - ItemID = 0x1FD4; - Light = LightType.Circle300; - - MoveToWorld(loc, map); - } - - public HarrowerGate(Serial serial) : base(serial) - { - } - - public override int LabelNumber => 1049498; // dark moongate - - public override void Serialize(IGenericWriter writer) - { - base.Serialize(writer); - - writer.Write(0); // version - - writer.Write(m_Harrower); - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - - var version = reader.ReadInt(); - - switch (version) - { - case 0: - { - m_Harrower = reader.ReadEntity(); - - if (m_Harrower == null) - { - Delete(); - } - - break; - } - } - - if (Light != LightType.Circle300) - { - Light = LightType.Circle300; - } + Delete(); } } } diff --git a/Projects/UOContent/Engines/CannedEvil/LLChampionSpawn.cs b/Projects/UOContent/Engines/CannedEvil/LLChampionSpawn.cs index 6ce233709..51d7fe462 100644 --- a/Projects/UOContent/Engines/CannedEvil/LLChampionSpawn.cs +++ b/Projects/UOContent/Engines/CannedEvil/LLChampionSpawn.cs @@ -13,43 +13,31 @@ * along with this program. If not, see . * *************************************************************************/ -namespace Server.Engines.CannedEvil +using ModernUO.Serialization; + +namespace Server.Engines.CannedEvil; + +[SerializationGenerator(0)] +public partial class LLChampionSpawn : ChampionSpawn { - public class LLChampionSpawn : ChampionSpawn + public override bool HasStarRoomGate => false; + + [Constructible] + public LLChampionSpawn() { - public override bool HasStarRoomGate => false; + CannedEvilTimer.AddSpawn(this); + } - [Constructible] - public LLChampionSpawn() - { - CannedEvilTimer.AddSpawn(this); - } + public LLChampionSpawn(Serial serial) + { + CannedEvilTimer.AddSpawn(this); + } - public LLChampionSpawn(Serial serial) : base(serial) - { - } + public override bool AlwaysActive => false; - public override bool AlwaysActive => false; - - public override void Serialize(IGenericWriter writer) - { - base.Serialize(writer); - - writer.WriteEncodedInt(0); //version - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - - int version = reader.ReadEncodedInt(); - CannedEvilTimer.AddSpawn(this); - } - - public override void OnAfterDelete() - { - base.OnAfterDelete(); - CannedEvilTimer.RemoveSpawn(this); - } + public override void OnAfterDelete() + { + base.OnAfterDelete(); + CannedEvilTimer.RemoveSpawn(this); } } diff --git a/Projects/UOContent/Engines/CannedEvil/StarRoomGate.cs b/Projects/UOContent/Engines/CannedEvil/StarRoomGate.cs index 0eb74b62f..a5426e7a5 100644 --- a/Projects/UOContent/Engines/CannedEvil/StarRoomGate.cs +++ b/Projects/UOContent/Engines/CannedEvil/StarRoomGate.cs @@ -1,98 +1,68 @@ using System; +using ModernUO.Serialization; -namespace Server.Items +namespace Server.Items; + +[SerializationGenerator(1, false)] +public partial class StarRoomGate : Moongate { - public class StarRoomGate : Moongate + private static TimeSpan GateDuration = TimeSpan.FromMinutes(2.0); + + [SerializableField(0)] + private bool _decays; + + [DeltaDateTime] + [SerializableField(1)] + private DateTime _decayTime; + + private Timer _timer; + + [Constructible] + public StarRoomGate(Point3D loc, Map map, bool decays) : this(decays) { - private bool m_Decays; - private DateTime m_DecayTime; - private Timer m_Timer; + MoveToWorld(loc, map); + Effects.PlaySound(loc, map, 0x20E); + } - [Constructible] - public StarRoomGate(Point3D loc, Map map, bool decays) : this(decays) + [Constructible] + public StarRoomGate(bool decays = false) : base(new Point3D(5143, 1774, 0), Map.Felucca) + { + Dispellable = false; + ItemID = 0x1FD4; + + if (decays) { - MoveToWorld(loc, map); - Effects.PlaySound(loc, map, 0x20E); + _decays = true; + _decayTime = Core.Now + GateDuration; + + _timer = Timer.DelayCall(GateDuration, Delete); } + } - [Constructible] - public StarRoomGate(bool decays = false) : base(new Point3D(5143, 1774, 0), Map.Felucca) + public override int LabelNumber => 1049498; // dark moongate + + public override void OnAfterDelete() + { + _timer?.Stop(); + base.OnAfterDelete(); + } + + private void Deserialize(IGenericReader reader, int version) + { + _decays = reader.ReadBool(); + + if (_decays) { - Dispellable = false; - ItemID = 0x1FD4; - - if (decays) - { - m_Decays = true; - m_DecayTime = Core.Now + TimeSpan.FromMinutes(2.0); - - m_Timer = new InternalTimer(this, m_DecayTime); - m_Timer.Start(); - } + _decayTime = reader.ReadDeltaTime(); } + } - public StarRoomGate(Serial serial) : base(serial) + [AfterDeserialization] + private void AfterDeserialization() + { + if (_decays) { - } - - public override int LabelNumber => 1049498; // dark moongate - - public override void OnAfterDelete() - { - m_Timer?.Stop(); - - base.OnAfterDelete(); - } - - public override void Serialize(IGenericWriter writer) - { - base.Serialize(writer); - - writer.Write(0); // version - - writer.Write(m_Decays); - - if (m_Decays) - { - writer.WriteDeltaTime(m_DecayTime); - } - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - - var version = reader.ReadInt(); - - switch (version) - { - case 0: - { - m_Decays = reader.ReadBool(); - - if (m_Decays) - { - m_DecayTime = reader.ReadDeltaTime(); - - m_Timer = new InternalTimer(this, m_DecayTime); - m_Timer.Start(); - } - - break; - } - } - } - - private class InternalTimer : Timer - { - private readonly Item m_Item; - - public InternalTimer(Item item, DateTime end) : base(end - Core.Now) => m_Item = item; - - protected override void OnTick() - { - m_Item.Delete(); - } + _timer = Timer.DelayCall(_decayTime - Core.Now, Delete); } } } diff --git a/Projects/UOContent/Engines/Virtues/Valor.cs b/Projects/UOContent/Engines/Virtues/Valor.cs index e8a2e520b..4c9d58b10 100644 --- a/Projects/UOContent/Engines/Virtues/Valor.cs +++ b/Projects/UOContent/Engines/Virtues/Valor.cs @@ -58,9 +58,9 @@ public static class ValorVirtue } else if (idol.Spawn.Active) { - if (idol.Spawn.Champion != null) // TODO: Message? + if (idol.Spawn.Champion != null) { - return; + from.SendLocalizedMessage(1112470); // You may not use Valor on this Champion Idol. The Champion has already spawned. } int needed, consumed; diff --git a/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionAltar.v0.json b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionAltar.v0.json new file mode 100644 index 000000000..5954cfce7 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionAltar.v0.json @@ -0,0 +1,11 @@ +{ + "version": 0, + "type": "Server.Engines.CannedEvil.ChampionAltar", + "properties": [ + { + "name": "Spawn", + "type": "Server.Engines.CannedEvil.ChampionSpawn", + "rule": "SerializableInterfaceMigrationRule" + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionPlatform.v0.json b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionPlatform.v0.json new file mode 100644 index 000000000..92931e758 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionPlatform.v0.json @@ -0,0 +1,11 @@ +{ + "version": 0, + "type": "Server.Engines.CannedEvil.ChampionPlatform", + "properties": [ + { + "name": "Spawn", + "type": "Server.Engines.CannedEvil.ChampionSpawn", + "rule": "SerializableInterfaceMigrationRule" + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSkullBrazier.v1.json b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSkullBrazier.v1.json new file mode 100644 index 000000000..b3e997c01 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSkullBrazier.v1.json @@ -0,0 +1,21 @@ +{ + "version": 1, + "type": "Server.Engines.CannedEvil.ChampionSkullBrazier", + "properties": [ + { + "name": "Type", + "type": "Server.Engines.CannedEvil.ChampionSkullType", + "rule": "EnumMigrationRule" + }, + { + "name": "Platform", + "type": "Server.Engines.CannedEvil.ChampionSkullPlatform", + "rule": "SerializableInterfaceMigrationRule" + }, + { + "name": "Skull", + "type": "Server.Item", + "rule": "SerializableInterfaceMigrationRule" + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSkullPlatform.v0.json b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSkullPlatform.v0.json new file mode 100644 index 000000000..78176295c --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSkullPlatform.v0.json @@ -0,0 +1,36 @@ +{ + "version": 0, + "type": "Server.Engines.CannedEvil.ChampionSkullPlatform", + "properties": [ + { + "name": "Power", + "type": "Server.Engines.CannedEvil.ChampionSkullBrazier", + "rule": "SerializableInterfaceMigrationRule" + }, + { + "name": "Enlightenment", + "type": "Server.Engines.CannedEvil.ChampionSkullBrazier", + "rule": "SerializableInterfaceMigrationRule" + }, + { + "name": "Venom", + "type": "Server.Engines.CannedEvil.ChampionSkullBrazier", + "rule": "SerializableInterfaceMigrationRule" + }, + { + "name": "Pain", + "type": "Server.Engines.CannedEvil.ChampionSkullBrazier", + "rule": "SerializableInterfaceMigrationRule" + }, + { + "name": "Greed", + "type": "Server.Engines.CannedEvil.ChampionSkullBrazier", + "rule": "SerializableInterfaceMigrationRule" + }, + { + "name": "Death", + "type": "Server.Engines.CannedEvil.ChampionSkullBrazier", + "rule": "SerializableInterfaceMigrationRule" + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSpawn.v10.json b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSpawn.v10.json new file mode 100644 index 000000000..b04ef31d0 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionSpawn.v10.json @@ -0,0 +1,186 @@ +{ + "version": 10, + "type": "Server.Engines.CannedEvil.ChampionSpawn", + "properties": [ + { + "name": "Level", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "ActivatedByProximity", + "type": "bool", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "NextProximityTime", + "type": "System.DateTime", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "DeltaTime" + ] + }, + { + "name": "MaxLevel", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "ActivatedByValor", + "type": "bool", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "DamageEntries", + "type": "System.Collections.Generic.Dictionary\u003CServer.Mobile, int\u003E", + "rule": "DictionaryMigrationRule", + "ruleArguments": [ + "Server.Mobile", + "SerializableInterfaceMigrationRule", + "0", + "int", + "PrimitiveTypeMigrationRule", + "1", + "" + ] + }, + { + "name": "ConfinedRoaming", + "type": "bool", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "Idol", + "type": "Server.Engines.CannedEvil.IdolOfTheChampion", + "rule": "SerializableInterfaceMigrationRule" + }, + { + "name": "HasBeenAdvanced", + "type": "bool", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "SpawnArea", + "type": "Server.Rectangle2D", + "rule": "PrimitiveUOTypeMigrationRule", + "ruleArguments": [ + "Rect2D" + ] + }, + { + "name": "RandomizeType", + "type": "bool", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "Kills", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "Active", + "type": "bool", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "Type", + "type": "Server.Engines.CannedEvil.ChampionSpawnType", + "rule": "EnumMigrationRule" + }, + { + "name": "Creatures", + "type": "System.Collections.Generic.List\u003CServer.Mobile\u003E", + "rule": "ListMigrationRule", + "ruleArguments": [ + "Server.Mobile", + "SerializableInterfaceMigrationRule" + ] + }, + { + "name": "RedSkulls", + "type": "System.Collections.Generic.List\u003CServer.Item\u003E", + "rule": "ListMigrationRule", + "ruleArguments": [ + "Server.Item", + "SerializableInterfaceMigrationRule" + ] + }, + { + "name": "WhiteSkulls", + "type": "System.Collections.Generic.List\u003CServer.Item\u003E", + "rule": "ListMigrationRule", + "ruleArguments": [ + "Server.Item", + "SerializableInterfaceMigrationRule" + ] + }, + { + "name": "Platform", + "type": "Server.Engines.CannedEvil.ChampionPlatform", + "rule": "SerializableInterfaceMigrationRule" + }, + { + "name": "Altar", + "type": "Server.Engines.CannedEvil.ChampionAltar", + "rule": "SerializableInterfaceMigrationRule" + }, + { + "name": "ExpireDelay", + "type": "System.TimeSpan", + "rule": "PrimitiveTypeMigrationRule" + }, + { + "name": "ExpireTime", + "type": "System.DateTime", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "DeltaTime" + ] + }, + { + "name": "Champion", + "type": "Server.Mobile", + "rule": "SerializableInterfaceMigrationRule" + }, + { + "name": "RestartDelay", + "type": "System.TimeSpan", + "rule": "PrimitiveTypeMigrationRule" + }, + { + "name": "RestartTime", + "type": "System.DateTime", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "DeltaTime" + ] + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionTitle.v0.json b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionTitle.v0.json new file mode 100644 index 000000000..93699d32d --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionTitle.v0.json @@ -0,0 +1,22 @@ +{ + "version": 0, + "type": "Server.Engines.CannedEvil.ChampionTitle", + "properties": [ + { + "name": "Value", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "LastDecay", + "type": "System.DateTime", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionTitleContext.v1.json b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionTitleContext.v1.json new file mode 100644 index 000000000..af6658d06 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionTitleContext.v1.json @@ -0,0 +1,95 @@ +{ + "version": 1, + "type": "Server.Engines.CannedEvil.ChampionTitleContext", + "properties": [ + { + "name": "Harrower", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "Abyss", + "type": "Server.Engines.CannedEvil.ChampionTitle", + "usesSaveFlag": true, + "rule": "RawSerializableMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "Arachnid", + "type": "Server.Engines.CannedEvil.ChampionTitle", + "usesSaveFlag": true, + "rule": "RawSerializableMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "ColdBlood", + "type": "Server.Engines.CannedEvil.ChampionTitle", + "usesSaveFlag": true, + "rule": "RawSerializableMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "ForestLord", + "type": "Server.Engines.CannedEvil.ChampionTitle", + "usesSaveFlag": true, + "rule": "RawSerializableMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "VerminHorde", + "type": "Server.Engines.CannedEvil.ChampionTitle", + "usesSaveFlag": true, + "rule": "RawSerializableMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "UnholyTerror", + "type": "Server.Engines.CannedEvil.ChampionTitle", + "usesSaveFlag": true, + "rule": "RawSerializableMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "SleepingDragon", + "type": "Server.Engines.CannedEvil.ChampionTitle", + "usesSaveFlag": true, + "rule": "RawSerializableMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "Corrupt", + "type": "Server.Engines.CannedEvil.ChampionTitle", + "usesSaveFlag": true, + "rule": "RawSerializableMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "Glade", + "type": "Server.Engines.CannedEvil.ChampionTitle", + "usesSaveFlag": true, + "rule": "RawSerializableMigrationRule", + "ruleArguments": [ + "" + ] + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Engines.CannedEvil.DungeonChampionSpawn.v0.json b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.DungeonChampionSpawn.v0.json new file mode 100644 index 000000000..a175e33dc --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.DungeonChampionSpawn.v0.json @@ -0,0 +1,4 @@ +{ + "version": 0, + "type": "Server.Engines.CannedEvil.DungeonChampionSpawn" +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Engines.CannedEvil.IdolOfTheChampion.v0.json b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.IdolOfTheChampion.v0.json new file mode 100644 index 000000000..d4f713358 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.IdolOfTheChampion.v0.json @@ -0,0 +1,11 @@ +{ + "version": 0, + "type": "Server.Engines.CannedEvil.IdolOfTheChampion", + "properties": [ + { + "name": "Spawn", + "type": "Server.Engines.CannedEvil.ChampionSpawn", + "rule": "SerializableInterfaceMigrationRule" + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Engines.CannedEvil.LLChampionSpawn.v0.json b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.LLChampionSpawn.v0.json new file mode 100644 index 000000000..75993331f --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.LLChampionSpawn.v0.json @@ -0,0 +1,4 @@ +{ + "version": 0, + "type": "Server.Engines.CannedEvil.LLChampionSpawn" +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.ChampionSkull.v2.json b/Projects/UOContent/Migrations/Server.Items.ChampionSkull.v2.json new file mode 100644 index 000000000..48cf1246f --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Items.ChampionSkull.v2.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "type": "Server.Items.ChampionSkull", + "properties": [ + { + "name": "Type", + "type": "Server.Engines.CannedEvil.ChampionSkullType", + "rule": "EnumMigrationRule" + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.HarrowerGate.v0.json b/Projects/UOContent/Migrations/Server.Items.HarrowerGate.v0.json new file mode 100644 index 000000000..6f5d139b9 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Items.HarrowerGate.v0.json @@ -0,0 +1,11 @@ +{ + "version": 0, + "type": "Server.Items.HarrowerGate", + "properties": [ + { + "name": "Harrower", + "type": "Server.Mobile", + "rule": "SerializableInterfaceMigrationRule" + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.StarRoomGate.v1.json b/Projects/UOContent/Migrations/Server.Items.StarRoomGate.v1.json new file mode 100644 index 000000000..544069802 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Items.StarRoomGate.v1.json @@ -0,0 +1,22 @@ +{ + "version": 1, + "type": "Server.Items.StarRoomGate", + "properties": [ + { + "name": "Decays", + "type": "bool", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "DecayTime", + "type": "System.DateTime", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "DeltaTime" + ] + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Misc/Guild.cs b/Projects/UOContent/Misc/Guild.cs index c8c39d931..f9e30c56b 100644 --- a/Projects/UOContent/Misc/Guild.cs +++ b/Projects/UOContent/Misc/Guild.cs @@ -1207,12 +1207,6 @@ namespace Server.Guilds writer.Write(Website); } - public override bool ShouldExecuteAfterSerialize => false; - - public override void AfterSerialize() - { - } - public override void Delete() { World.RemoveGuild(this); diff --git a/Projects/UOContent/Misc/Titles.cs b/Projects/UOContent/Misc/Titles.cs index 98ada37ee..7b293356e 100644 --- a/Projects/UOContent/Misc/Titles.cs +++ b/Projects/UOContent/Misc/Titles.cs @@ -1,5 +1,6 @@ using System; using System.Text; +using Server.Buffers; using Server.Engines.CannedEvil; using Server.Mobiles; @@ -13,12 +14,6 @@ namespace Server.Misc public const int MinKarma = -15000; public const int MaxKarma = 15000; - public static string[] HarrowerTitles = - { - "Spite", "Opponent", "Hunter", "Venom", "Executioner", - "Annihilator", "Champion", "Assailant", "Purifier", "Nullifier" - }; - private static readonly string[,] m_Levels = { { "Neophyte", "Neophyte", "Neophyte" }, @@ -39,85 +34,85 @@ namespace Server.Misc 1249, new[] { - new KarmaEntry(-10000, "The Outcast {0}"), - new KarmaEntry(-5000, "The Despicable {0}"), - new KarmaEntry(-2500, "The Scoundrel {0}"), - new KarmaEntry(-1250, "The Unsavory {0}"), - new KarmaEntry(-625, "The Rude {0}"), - new KarmaEntry(624, "{0}"), - new KarmaEntry(1249, "The Fair {0}"), - new KarmaEntry(2499, "The Kind {0}"), - new KarmaEntry(4999, "The Good {0}"), - new KarmaEntry(9999, "The Honest {0}"), - new KarmaEntry(10000, "The Trustworthy {0}") + new KarmaEntry(-10000, "The Outcast "), + new KarmaEntry(-5000, "The Despicable "), + new KarmaEntry(-2500, "The Scoundrel "), + new KarmaEntry(-1250, "The Unsavory "), + new KarmaEntry(-625, "The Rude "), + new KarmaEntry(624, ""), + new KarmaEntry(1249, "The Fair "), + new KarmaEntry(2499, "The Kind "), + new KarmaEntry(4999, "The Good "), + new KarmaEntry(9999, "The Honest "), + new KarmaEntry(10000, "The Trustworthy ") } ), new( 2499, new[] { - new KarmaEntry(-10000, "The Wretched {0}"), - new KarmaEntry(-5000, "The Dastardly {0}"), - new KarmaEntry(-2500, "The Malicious {0}"), - new KarmaEntry(-1250, "The Dishonorable {0}"), - new KarmaEntry(-625, "The Disreputable {0}"), - new KarmaEntry(624, "The Notable {0}"), - new KarmaEntry(1249, "The Upstanding {0}"), - new KarmaEntry(2499, "The Respectable {0}"), - new KarmaEntry(4999, "The Honorable {0}"), - new KarmaEntry(9999, "The Commendable {0}"), - new KarmaEntry(10000, "The Estimable {0}") + new KarmaEntry(-10000, "The Wretched "), + new KarmaEntry(-5000, "The Dastardly "), + new KarmaEntry(-2500, "The Malicious "), + new KarmaEntry(-1250, "The Dishonorable "), + new KarmaEntry(-625, "The Disreputable "), + new KarmaEntry(624, "The Notable "), + new KarmaEntry(1249, "The Upstanding "), + new KarmaEntry(2499, "The Respectable "), + new KarmaEntry(4999, "The Honorable "), + new KarmaEntry(9999, "The Commendable "), + new KarmaEntry(10000, "The Estimable ") } ), new( 4999, new[] { - new KarmaEntry(-10000, "The Nefarious {0}"), - new KarmaEntry(-5000, "The Wicked {0}"), - new KarmaEntry(-2500, "The Vile {0}"), - new KarmaEntry(-1250, "The Ignoble {0}"), - new KarmaEntry(-625, "The Notorious {0}"), - new KarmaEntry(624, "The Prominent {0}"), - new KarmaEntry(1249, "The Reputable {0}"), - new KarmaEntry(2499, "The Proper {0}"), - new KarmaEntry(4999, "The Admirable {0}"), - new KarmaEntry(9999, "The Famed {0}"), - new KarmaEntry(10000, "The Great {0}") + new KarmaEntry(-10000, "The Nefarious "), + new KarmaEntry(-5000, "The Wicked "), + new KarmaEntry(-2500, "The Vile "), + new KarmaEntry(-1250, "The Ignoble "), + new KarmaEntry(-625, "The Notorious "), + new KarmaEntry(624, "The Prominent "), + new KarmaEntry(1249, "The Reputable "), + new KarmaEntry(2499, "The Proper "), + new KarmaEntry(4999, "The Admirable "), + new KarmaEntry(9999, "The Famed "), + new KarmaEntry(10000, "The Great ") } ), new( 9999, new[] { - new KarmaEntry(-10000, "The Dread {0}"), - new KarmaEntry(-5000, "The Evil {0}"), - new KarmaEntry(-2500, "The Villainous {0}"), - new KarmaEntry(-1250, "The Sinister {0}"), - new KarmaEntry(-625, "The Infamous {0}"), - new KarmaEntry(624, "The Renowned {0}"), - new KarmaEntry(1249, "The Distinguished {0}"), - new KarmaEntry(2499, "The Eminent {0}"), - new KarmaEntry(4999, "The Noble {0}"), - new KarmaEntry(9999, "The Illustrious {0}"), - new KarmaEntry(10000, "The Glorious {0}") + new KarmaEntry(-10000, "The Dread "), + new KarmaEntry(-5000, "The Evil "), + new KarmaEntry(-2500, "The Villainous "), + new KarmaEntry(-1250, "The Sinister "), + new KarmaEntry(-625, "The Infamous "), + new KarmaEntry(624, "The Renowned "), + new KarmaEntry(1249, "The Distinguished "), + new KarmaEntry(2499, "The Eminent "), + new KarmaEntry(4999, "The Noble "), + new KarmaEntry(9999, "The Illustrious "), + new KarmaEntry(10000, "The Glorious ") } ), new( 10000, new[] { - new KarmaEntry(-10000, "The Dread {1} {0}"), - new KarmaEntry(-5000, "The Evil {1} {0}"), - new KarmaEntry(-2500, "The Dark {1} {0}"), - new KarmaEntry(-1250, "The Sinister {1} {0}"), - new KarmaEntry(-625, "The Dishonored {1} {0}"), - new KarmaEntry(624, "{1} {0}"), - new KarmaEntry(1249, "The Distinguished {1} {0}"), - new KarmaEntry(2499, "The Eminent {1} {0}"), - new KarmaEntry(4999, "The Noble {1} {0}"), - new KarmaEntry(9999, "The Illustrious {1} {0}"), - new KarmaEntry(10000, "The Glorious {1} {0}") + new KarmaEntry(-10000, "The Dread "), + new KarmaEntry(-5000, "The Evil "), + new KarmaEntry(-2500, "The Dark "), + new KarmaEntry(-1250, "The Sinister "), + new KarmaEntry(-625, "The Dishonored "), + new KarmaEntry(624, ""), + new KarmaEntry(1249, "The Distinguished "), + new KarmaEntry(2499, "The Eminent "), + new KarmaEntry(4999, "The Noble "), + new KarmaEntry(9999, "The Illustrious "), + new KarmaEntry(10000, "The Glorious ") } ) }; @@ -274,7 +269,7 @@ namespace Server.Misc public static string ComputeTitle(Mobile beholder, Mobile beheld) { - var title = new StringBuilder(); + using var title = ValueStringBuilder.Create(); var fame = beheld.Fame; var karma = beheld.Karma; @@ -297,7 +292,21 @@ namespace Server.Misc if (karma <= ke.m_Karma || j == karmaEntries.Length - 1) { - title.AppendFormat(ke.m_Title, beheld.Name, beheld.Female ? "Lady" : "Lord"); + if (karma >= 10000) + { + if (beheld.Female) + { + title.Append($"{ke.m_Title}Lady {beheld.Name}"); + } + else + { + title.Append($"{ke.m_Title}Lord {beheld.Name}"); + } + } + else + { + title.Append($"{ke.m_Title}{beheld.Name}"); + } break; } } @@ -313,40 +322,14 @@ namespace Server.Misc if (beheld is PlayerMobile mobile && mobile.DisplayChampionTitle) { - var info = mobile.ChampionTitles; - - if (info.Harrower > 0) + var titleLabel = mobile.GetChampionTitleLabel(); + if (titleLabel > 0) { - title.Append($": {HarrowerTitles[Math.Min(HarrowerTitles.Length, info.Harrower) - 1]} of Evil"); - } - else - { - int highestValue = 0, highestType = 0; - for (var i = 0; i < ChampionSpawnInfo.Table.Length; i++) - { - var v = info.GetValue(i); + // Should this be translated to the receivers language? Prefix titles aren't? + title.Append($": {Localization.GetText(titleLabel)}"); - if (v > highestValue) - { - highestValue = v; - highestType = i; - } - } - - var offset = highestValue switch - { - > 800 => 3, - > 300 => highestValue / 300, - _ => 0 - }; - - if (offset > 0) - { - var champInfo = ChampionSpawnInfo.GetInfo((ChampionSpawnType)highestType); - title.Append( - $": {champInfo.LevelNames[Math.Min(offset, champInfo.LevelNames.Length) - 1]} of the {champInfo.Name}" - ); - } + // Do not display the skills title + return title.ToString(); } } diff --git a/Projects/UOContent/Mobiles/ChampionTitleInfo.cs b/Projects/UOContent/Mobiles/ChampionTitleInfo.cs deleted file mode 100644 index d7dd2f829..000000000 --- a/Projects/UOContent/Mobiles/ChampionTitleInfo.cs +++ /dev/null @@ -1,339 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using Server.Engines.CannedEvil; - -namespace Server.Mobiles -{ - [PropertyObject] - public class ChampionTitleInfo - { - private const int LossAmount = 90; - private static TimeSpan LossDelay = TimeSpan.FromDays(1.0); - - private TitleInfo[] m_Values; - - public ChampionTitleInfo() - { - } - - public ChampionTitleInfo(IGenericReader reader) - { - var version = reader.ReadEncodedInt(); - - switch (version) - { - case 0: - { - Harrower = reader.ReadEncodedInt(); - - var length = reader.ReadEncodedInt(); - - if (length == 0) - { - break; - } - - m_Values = new TitleInfo[length]; - - for (var i = 0; i < length; i++) - { - m_Values[i] = new TitleInfo(reader); - } - - if (m_Values.Length != ChampionSpawnInfo.Table.Length) - { - var oldValues = m_Values; - m_Values = new TitleInfo[ChampionSpawnInfo.Table.Length]; - - for (var i = 0; i < m_Values.Length && i < oldValues.Length; i++) - { - m_Values[i] = oldValues[i]; - } - } - - break; - } - } - } - - [CommandProperty(AccessLevel.GameMaster)] - public int Pestilence - { - get => GetValue(ChampionSpawnType.Pestilence); - set => SetValue(ChampionSpawnType.Pestilence, value); - } - - [CommandProperty(AccessLevel.GameMaster)] - public int Abyss - { - get => GetValue(ChampionSpawnType.Abyss); - set => SetValue(ChampionSpawnType.Abyss, value); - } - - [CommandProperty(AccessLevel.GameMaster)] - public int Arachnid - { - get => GetValue(ChampionSpawnType.Arachnid); - set => SetValue(ChampionSpawnType.Arachnid, value); - } - - [CommandProperty(AccessLevel.GameMaster)] - public int ColdBlood - { - get => GetValue(ChampionSpawnType.ColdBlood); - set => SetValue(ChampionSpawnType.ColdBlood, value); - } - - [CommandProperty(AccessLevel.GameMaster)] - public int ForestLord - { - get => GetValue(ChampionSpawnType.ForestLord); - set => SetValue(ChampionSpawnType.ForestLord, value); - } - - [CommandProperty(AccessLevel.GameMaster)] - public int SleepingDragon - { - get => GetValue(ChampionSpawnType.SleepingDragon); - set => SetValue(ChampionSpawnType.SleepingDragon, value); - } - - [CommandProperty(AccessLevel.GameMaster)] - public int UnholyTerror - { - get => GetValue(ChampionSpawnType.UnholyTerror); - set => SetValue(ChampionSpawnType.UnholyTerror, value); - } - - [CommandProperty(AccessLevel.GameMaster)] - public int VerminHorde - { - get => GetValue(ChampionSpawnType.VerminHorde); - set => SetValue(ChampionSpawnType.VerminHorde, value); - } - - [CommandProperty(AccessLevel.GameMaster)] - public int Harrower { get; set; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetValue(ChampionSpawnType type) => GetValue((int)type); - - public int GetValue(int index) - { - if (index < 0 || index >= m_Values?.Length) - { - return 0; - } - - return m_Values?[index]?.Value ?? 0; - } - - public DateTime GetLastDecay(int index) - { - if (index < 0 || index >= m_Values?.Length) - { - return DateTime.MinValue; - } - - return m_Values?[index]?.LastDecay ?? DateTime.MinValue; - } - - public void SetValue(ChampionSpawnType type, int value) - { - var index = (int)type; - if (index < 0 || index >= ChampionSpawnInfo.Table.Length) - { - return; - } - - m_Values ??= new TitleInfo[ChampionSpawnInfo.Table.Length]; - var title = m_Values[index]; - if (title == null) - { - if (value > 0) - { - m_Values[index] = new TitleInfo(value, Core.Now); - } - return; - } - - title.Value = Math.Max(value, 0); - title.LastDecay = title.Value == 0 ? DateTime.MinValue : Core.Now; - } - - public void Award(ChampionSpawnType type, int value) - { - var index = (int)type; - if (value <= 0 || index < 0 || index >= ChampionSpawnInfo.Table.Length) - { - return; - } - - m_Values ??= new TitleInfo[ChampionSpawnInfo.Table.Length]; - var title = m_Values[index]; - if (title == null) - { - m_Values[index] = new TitleInfo(value, Core.Now); - return; - } - - title.Value += value; - if (title.LastDecay == DateTime.MinValue) - { - title.LastDecay = Core.Now; - } - } - - public void Atrophy(int index, int value) - { - if (value <= 0 || index < 0 || index >= ChampionSpawnInfo.Table.Length) - { - return; - } - - var title = m_Values?[index]; - if (title == null) - { - return; - } - - var before = title.Value; - - title.Value -= Math.Min(value, title.Value); - - if (before != title.Value) - { - title.LastDecay = Core.Now; - } - } - - public override string ToString() => "..."; - - public static void Serialize(IGenericWriter writer, ChampionTitleInfo titles) - { - writer.WriteEncodedInt(0); // version - - writer.WriteEncodedInt(titles.Harrower); - - var length = titles.m_Values?.Length ?? 0; - writer.WriteEncodedInt(length); - - for (var i = 0; i < length; i++) - { - titles.m_Values![i] ??= new TitleInfo(); - - TitleInfo.Serialize(writer, titles.m_Values[i]); - } - } - - public static bool ShouldAtrophy(PlayerMobile pm) - { - var t = pm.ChampionTitles; - if (t?.m_Values == null) - { - return false; - } - - for (var i = 0; i < t.m_Values.Length; i++) - { - var decay = t.GetLastDecay(i); - if (decay > DateTime.MinValue && decay + LossDelay < Core.Now) - { - return true; - } - } - - return false; - } - - public static void CheckAtrophy(PlayerMobile pm) - { - var t = pm.ChampionTitles; - if (t?.m_Values == null) - { - return; - } - - for (var i = 0; i < t.m_Values.Length; i++) - { - var decay = t.GetLastDecay(i); - if (decay > DateTime.MinValue && decay + LossDelay < Core.Now) - { - t.Atrophy(i, LossAmount); - } - } - } - - // Called when killing a harrower. Will give a minimum of 1 point. - public static void AwardHarrowerTitle(PlayerMobile pm) - { - var t = pm.ChampionTitles; - if (t == null) - { - return; - } - - if (t.m_Values == null) - { - if (t.Harrower == 0) - { - t.Harrower = 1; - } - return; - } - - t.m_Values ??= new TitleInfo[ChampionSpawnInfo.Table.Length]; - - var count = 1; - for (var i = 0; i < t.m_Values.Length; i++) - { - if (t.m_Values[i].Value > 900) - { - count++; - } - } - - t.Harrower = Math.Max(count, t.Harrower); // Harrower titles never decay. - } - - private class TitleInfo - { - public TitleInfo() - { - } - - public TitleInfo(int value, DateTime lastDecay) - { - Value = value; - LastDecay = lastDecay; - } - - public TitleInfo(IGenericReader reader) - { - var version = reader.ReadEncodedInt(); - - switch (version) - { - case 0: - { - Value = reader.ReadEncodedInt(); - LastDecay = reader.ReadDateTime(); - break; - } - } - } - - public int Value { get; set; } - - public DateTime LastDecay { get; set; } - - public static void Serialize(IGenericWriter writer, TitleInfo info) - { - writer.WriteEncodedInt(0); // version - - writer.WriteEncodedInt(info.Value); - writer.Write(info.LastDecay); - } - } - } -} diff --git a/Projects/UOContent/Mobiles/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs index fcb93e317..636e8d34b 100644 --- a/Projects/UOContent/Mobiles/PlayerMobile.cs +++ b/Projects/UOContent/Mobiles/PlayerMobile.cs @@ -4,6 +4,7 @@ using Server.Accounting; using Server.Collections; using Server.ContextMenus; using Server.Engines.BulkOrders; +using Server.Engines.CannedEvil; using Server.Engines.ConPVP; using Server.Engines.Craft; using Server.Engines.Help; @@ -206,8 +207,6 @@ namespace Server.Mobiles m_GameTime = TimeSpan.Zero; m_GuildRank = RankDefinition.Lowest; - - ChampionTitles = new ChampionTitleInfo(); } public PlayerMobile(Serial s) : base(s) @@ -693,8 +692,8 @@ namespace Server.Mobiles set => SetFlag(PlayerFlag.DisplayChampionTitle, value); } - [CommandProperty(AccessLevel.GameMaster)] - public ChampionTitleInfo ChampionTitles { get; private set; } + [CommandProperty(AccessLevel.GameMaster, canModify: true)] + public ChampionTitleContext ChampionTitles => this.GetOrCreateChampionTitleContext(); [CommandProperty(AccessLevel.GameMaster)] public int ShortTermMurders @@ -1859,21 +1858,13 @@ namespace Server.Mobiles { if (AutoRenewInsurance) { - list.Add( - new CallbackEntry( - 6202, - CancelRenewInventoryInsurance - ) - ); // Cancel Renewing Inventory Insurance + // Cancel Renewing Inventory Insurance + list.Add(new CallbackEntry(6202, CancelRenewInventoryInsurance)); } else { - list.Add( - new CallbackEntry( - 6200, - AutoRenewInventoryInsurance - ) - ); // Auto Renew Inventory Insurance + // Auto Renew Inventory Insurance + list.Add(new CallbackEntry(6200, AutoRenewInventoryInsurance)); } } } @@ -2855,6 +2846,7 @@ namespace Server.Mobiles switch (version) { + case 33: // Removes champion title case 32: // Removes virtue properties case 31: // Removed Short/Long Term Elapse case 30: @@ -2928,7 +2920,11 @@ namespace Server.Mobiles } case 23: { - ChampionTitles = new ChampionTitleInfo(reader); + if (version < 33) + { + ChampionTitles.Deserialize(reader); + } + goto case 22; } case 22: @@ -3165,8 +3161,6 @@ namespace Server.Mobiles LastOnline = ((Account)Account).LastLogin; } - ChampionTitles ??= new ChampionTitleInfo(); - if (AccessLevel > AccessLevel.Player) { IgnoreMobiles = true; @@ -3184,8 +3178,6 @@ namespace Server.Mobiles } } - CheckAtrophies(); - if (Hidden) // Hiding is the only buff where it has an effect that's serialized. { AddBuff(new BuffInfo(BuffIcon.HidingAndOrStealth, 1075655)); @@ -3196,7 +3188,7 @@ namespace Server.Mobiles { base.Serialize(writer); - writer.Write(32); // version + writer.Write(33); // version if (Stabled == null) { @@ -3251,8 +3243,6 @@ namespace Server.Mobiles } } - ChampionTitleInfo.Serialize(writer, ChampionTitles); - writer.WriteEncodedInt(ToTItemsTurnedIn); writer.Write(ToTTotalMonsterFame); // This ain't going to be a small #. @@ -3317,22 +3307,6 @@ namespace Server.Mobiles writer.Write(GameTime); } - // Do we need to run an after serialize? - public override bool ShouldExecuteAfterSerialize => ShouldAtrophy(); - - public override void AfterSerialize() - { - base.AfterSerialize(); - CheckAtrophies(); - } - - public bool ShouldAtrophy() => ChampionTitleInfo.ShouldAtrophy(this); - - public void CheckAtrophies() - { - ChampionTitleInfo.CheckAtrophy(this); - } - public override bool CanSee(Mobile m) { if (m is CharacterStatue statue) @@ -3439,6 +3413,17 @@ namespace Server.Mobiles } } + // TODO: Add the Titles Menu for later Eras: + // https://uo.com/wiki/ultima-online-wiki/player/skill-titles-order/ + if (DisplayChampionTitle) + { + var titleLabel = this.GetChampionTitleLabel(); + if (titleLabel > 0) + { + list.Add(titleLabel); + } + } + if (Core.ML && AllFollowers != null) { foreach (var follower in AllFollowers) @@ -4457,6 +4442,7 @@ namespace Server.Mobiles } DisplayChampionTitle = !DisplayChampionTitle; + InvalidateProperties(); } public virtual bool HasRecipe(Recipe r) => r != null && HasRecipe(r.ID); diff --git a/Projects/UOContent/Mobiles/Special/Harrower.cs b/Projects/UOContent/Mobiles/Special/Harrower.cs index bb5bfbf9c..0f3fa3345 100644 --- a/Projects/UOContent/Mobiles/Special/Harrower.cs +++ b/Projects/UOContent/Mobiles/Special/Harrower.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Server.Engines.CannedEvil; using Server.Engines.Virtues; using Server.Items; using Server.Spells; @@ -325,7 +326,7 @@ namespace Server.Mobiles if (ds.m_HasRight && ds.m_Mobile is PlayerMobile mobile) { - ChampionTitleInfo.AwardHarrowerTitle(mobile); + ChampionTitleSystem.AwardHarrowerTitle(mobile); } }