using System; using System.Collections.Generic; using ModernUO.Serialization; using Server.Collections; using Server.ContextMenus; using Server.Engines.PartySystem; using Server.Gumps; using Server.Network; namespace Server.Items; [SerializationGenerator(4, false)] public partial class TreasureMapChest : LockableContainer { [Tidy] [CanBeNull] [SerializableField(0, setter: "private")] private List _guardians; [SerializableField(1)] [SerializedCommandProperty(AccessLevel.GameMaster)] private bool _temporary; [SerializableField(2)] [SerializedCommandProperty(AccessLevel.GameMaster)] private Mobile _owner; [SerializableField(3)] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _level; [SerializableField(4)] [SerializedCommandProperty(AccessLevel.GameMaster)] [DeserializeTimer(nameof(DeserializeExpireTimer))] private Timer _expireTimer; private void DeserializeExpireTimer(TimeSpan delay) { if (!_temporary) { _expireTimer = Timer.DelayCall(delay, Delete); } } private void MigrateFrom(V3Content content) { _guardians = content.Guardians; _temporary = content.Temporary; _owner = content.Owner; _level = content.Level; _lifted = content.Lifted; if (content.ExpireTimerDelay != TimeSpan.MinValue) { DeserializeExpireTimer(content.ExpireTimerDelay); } } [Tidy] [CanBeNull] [SerializableField(5, setter: "private")] [SerializedCommandProperty(AccessLevel.GameMaster)] private HashSet _lifted; // False only while the constructor fills the chest; deserialized chests are always filled. private bool _filled; [Constructible] public TreasureMapChest(int level) : this(null, level) { } public TreasureMapChest(Mobile owner, int level, bool temporary = false) : base(0xE40) { _owner = owner; _level = level; _temporary = temporary; _expireTimer = Timer.DelayCall(TimeSpan.FromHours(3.0), Delete); Fill(this, level); _filled = true; } public override int LabelNumber => 3000541; public static Type[] Artifacts { get; } = [ typeof(CandelabraOfSouls), typeof(GoldBricks), typeof(PhillipsWoodenSteed), typeof(ArcticDeathDealer), typeof(BlazeOfDeath), typeof(BurglarsBandana), typeof(CavortingClub), typeof(DreadPirateHat), typeof(EnchantedTitanLegBone), typeof(GwennosHarp), typeof(IolosLute), typeof(LunaLance), typeof(NightsKiss), typeof(NoxRangersHeavyCrossbow), typeof(PolarBearMask), typeof(VioletCourage), typeof(HeartOfTheLion), typeof(ColdBlood), typeof(AlchemistsBauble) ]; [CommandProperty(AccessLevel.GameMaster)] public DateTime DeleteTime => _expireTimer.Next; public override bool IsDecoContainer => false; private static void GetRandomAOSStats(out int attributeCount, out int min, out int max) { var rnd = Utility.Random(15); if (Core.SE) { if (rnd < 1) { attributeCount = Utility.RandomMinMax(3, 5); min = 50; max = 100; } else if (rnd < 3) { attributeCount = Utility.RandomMinMax(2, 5); min = 40; max = 80; } else if (rnd < 6) { attributeCount = Utility.RandomMinMax(2, 4); min = 30; max = 60; } else if (rnd < 10) { attributeCount = Utility.RandomMinMax(1, 3); min = 20; max = 40; } else { attributeCount = 1; min = 10; max = 20; } return; } if (rnd < 1) { attributeCount = Utility.RandomMinMax(2, 5); min = 20; max = 70; } else if (rnd < 3) { attributeCount = Utility.RandomMinMax(2, 4); min = 20; max = 50; } else if (rnd < 6) { attributeCount = Utility.RandomMinMax(2, 3); min = 20; max = 40; } else if (rnd < 10) { attributeCount = Utility.RandomMinMax(1, 2); min = 10; max = 30; } else { attributeCount = 1; min = 10; max = 20; } } public static void Fill(LockableContainer cont, int level) { cont.Movable = false; cont.Locked = true; if (level == 0) { cont.LockLevel = ILockpickable.CannotPick; cont.DropItem(new Gold(Utility.RandomMinMax(50, 100))); if (Utility.RandomDouble() < 0.75) { cont.DropItem(new TreasureMap(0, Map.Trammel)); } } else { cont.TrapType = TrapType.ExplosionTrap; cont.TrapPower = level * 25; cont.TrapLevel = level; cont.RequiredSkill = level switch { 1 => 36, 2 => 76, 3 => 84, 4 => 92, _ => 100 }; cont.LockLevel = cont.RequiredSkill - 10; cont.MaxLockLevel = cont.RequiredSkill + 40; // Publish 67 gold change // if (Core.SA) // cont.DropItem( new Gold( level * 5000 ) ); // else cont.DropItem(new Gold(level * 1000)); for (var i = 0; i < level * 5; ++i) { cont.DropItem(Loot.RandomScroll(0, 63, SpellbookType.Regular)); } var numberItems = Core.SE ? level switch { 1 => 5, 2 => 10, 3 => 15, 4 => 38, 5 => 50, 6 => 60, _ => 0 } : level * 6; for (var i = 0; i < numberItems; ++i) { var item = Core.AOS ? Loot.RandomArmorOrShieldOrWeaponOrJewelry() : Loot.RandomArmorOrShieldOrWeapon(); if (item is BaseWeapon weapon) { if (Core.AOS) { GetRandomAOSStats(out var attributeCount, out var min, out var max); BaseRunicTool.ApplyAttributesTo(weapon, attributeCount, min, max); } else { weapon.DamageLevel = (WeaponDamageLevel)Utility.Random(6); weapon.AccuracyLevel = (WeaponAccuracyLevel)Utility.Random(6); weapon.DurabilityLevel = (WeaponDurabilityLevel)Utility.Random(6); } cont.DropItem(weapon); } else if (item is BaseArmor armor) { if (Core.AOS) { GetRandomAOSStats(out var attributeCount, out var min, out var max); BaseRunicTool.ApplyAttributesTo(armor, attributeCount, min, max); } else { armor.ProtectionLevel = (ArmorProtectionLevel)Utility.Random(6); armor.Durability = (ArmorDurabilityLevel)Utility.Random(6); } cont.DropItem(armor); } else if (item is BaseHat hat) { if (Core.AOS) { GetRandomAOSStats(out var attributeCount, out var min, out var max); BaseRunicTool.ApplyAttributesTo(hat, attributeCount, min, max); } cont.DropItem(hat); } else if (item is BaseJewel jewel) { GetRandomAOSStats(out var attributeCount, out var min, out var max); BaseRunicTool.ApplyAttributesTo(jewel, attributeCount, min, max); cont.DropItem(jewel); } } } var reagents = level == 0 ? 12 : level * 3; for (var i = 0; i < reagents; i++) { var item = Loot.RandomPossibleReagent(); item.Amount = Utility.RandomMinMax(40, 60); cont.DropItem(item); } var gems = level == 0 ? 2 : level * 3; for (var i = 0; i < gems; i++) { var item = Loot.RandomGem(); cont.DropItem(item); } if (level == 6 && Core.AOS) { cont.DropItem(Artifacts.RandomElement().CreateInstance()); } } public override bool CheckLocked(Mobile from) { if (!Locked) { return false; } if (_level == 0 && from.AccessLevel < AccessLevel.GameMaster) { if (_guardians != null) { for (var i = 0; i < _guardians.Count; i++) { var m = _guardians[i]; if (m.Alive) { // You must first kill the guardians before you may open this chest. from.SendLocalizedMessage(1046448); return true; } } } LockPick(from); return false; } return base.CheckLocked(from); } private bool CheckLoot(Mobile m, bool criminalAction) { if (_temporary) { return false; } if (m.AccessLevel >= AccessLevel.GameMaster || _owner == null || m == _owner) { return true; } if (Party.Get(_owner)?.Contains(m) == true) { return true; } var map = Map; if ((map?.Rules & MapRules.HarmfulRestrictions) == 0) { if (criminalAction) { m.CriminalAction(true); } else { m.SendLocalizedMessage(1010630); // Taking someone else's treasure is a criminal offense! } return true; } m.SendLocalizedMessage(1010631); // You did not discover this chest! return false; } public override bool CheckItemUse(Mobile from, Item item) => CheckLoot(from, item != this) && base.CheckItemUse(from, item); public override bool CheckLift(Mobile from, Item item, ref LRReason reject) => CheckLoot(from, true) && base.CheckLift(from, item, ref reject); public override void OnItemAdded(Item item) { base.OnItemAdded(item); if (_filled) { _lifted ??= []; _lifted.Add(item); } } public override void OnItemLifted(Mobile from, Item item) { from.RevealingAction(); if (_lifted?.Contains(item) != true) { _lifted ??= []; _lifted.Add(item); if (Utility.RandomDouble() < 0.1) // 10% chance to spawn a new monster { TreasureMap.Spawn(_level, GetWorldLocation(), Map, from, false); } } base.OnItemLifted(from, item); } public override bool CheckHold(Mobile m, Item item, bool message, bool checkItems, int plusItems, int plusWeight) { if (m.AccessLevel < AccessLevel.GameMaster) { m.SendLocalizedMessage(1048122, "", 0x8A5); // The chest refuses to be filled with treasure again. return false; } return base.CheckHold(m, item, message, checkItems, plusItems, plusWeight); } private void Deserialize(IGenericReader reader, int version) { _owner = reader.ReadEntity(); _level = reader.ReadInt(); var expireTimerNext = reader.ReadDeltaTime(); DeserializeExpireTimer(expireTimerNext == DateTime.MinValue ? TimeSpan.MinValue : expireTimerNext - Core.Now); _lifted = reader.ReadEntitySet(); } private void MigrateFrom(V2Content content) { _guardians = content.Guardians; if (_guardians.Count == 0) { _guardians = null; } _temporary = content.Temporary; _owner = content.Owner; _level = content.Level; _lifted = content.Lifted; if (_lifted.Count == 0) { _lifted = null; } var expireTimerDelay = content.ExpireTimerDelay; DeserializeExpireTimer(expireTimerDelay); } [AfterDeserialization] private void AfterDeserialization() { _filled = true; if (_expireTimer == null) { Timer.DelayCall(Delete); } } public override void OnAfterDelete() { _expireTimer?.Stop(); _expireTimer = null; base.OnAfterDelete(); } public override void GetContextMenuEntries(Mobile from, ref PooledRefList list) { base.GetContextMenuEntries(from, ref list); if (from.Alive) { list.Add(new RemoveEntry(from == _owner)); } } public void BeginRemove(Mobile from) { RemoveGump.DisplayTo(from, this); } public void EndRemove(Mobile from) { if (Deleted || from != _owner || !from.InRange(GetWorldLocation(), 3)) { return; } from.SendLocalizedMessage(1048124, "", 0x8A5); // The old, rusted chest crumbles when you hit it. Delete(); } private class RemoveGump : StaticGump { private readonly TreasureMapChest _chest; public override bool Singleton => true; private RemoveGump(TreasureMapChest chest) : base(15, 15) => _chest = chest; public static void DisplayTo(Mobile from, TreasureMapChest chest) { if (from?.NetState == null || !from.Alive || chest == null || chest.Deleted) { return; } from.SendGump(new RemoveGump(chest)); } protected override void BuildLayout(ref StaticGumpBuilder builder) { builder.AddPage(); builder.SetNoClose(); builder.SetNoDispose(); builder.AddBackground(30, 0, 240, 240, 2620); // When this treasure chest is removed, any items still inside of it will be lost. builder.AddHtmlLocalized(45, 15, 200, 80, 1048125, 0x7FFF); // Are you certain you're ready to remove this chest? builder.AddHtmlLocalized(45, 95, 200, 60, 1048126, 0x7FFF); builder.AddButton(40, 153, 4005, 4007, 1); builder.AddHtmlLocalized(75, 155, 180, 40, 1048127, 0x7FFF); // Remove the Treasure Chest builder.AddButton(40, 195, 4005, 4007, 2); builder.AddHtmlLocalized(75, 197, 180, 35, 1006045, 0x7FFF); // Cancel } public override void OnResponse(NetState sender, in RelayInfo info) { if (info.ButtonID == 1) { _chest.EndRemove(sender.Mobile); } } } private class RemoveEntry : ContextMenuEntry { public RemoveEntry(bool enabled) : base(6149, 3) => Enabled = enabled; public override void OnClick(Mobile from, IEntity target) { if (!from.CheckAlive() || target is not TreasureMapChest chest || chest.Deleted || from != chest._owner) { return; } chest.BeginRemove(from); } } }