using System; using System.Collections.Generic; using ModernUO.Serialization; using Server.Commands; using Server.Engines.Craft; using Server.Ethics; using Server.Mobiles; using Server.Multis; using Server.Network; using Server.Spells; using Server.Systems.FeatureFlags; using Server.Targeting; namespace Server.Items; public enum SpellbookType { Invalid = -1, Regular, Necromancer, Paladin, Ninja, Samurai, Arcanist, Mystic } public enum BookQuality { Regular, Exceptional } [SerializationGenerator(6, false)] public partial class Spellbook : Item, ICraftable, ISlayer, IAosItem { private static readonly Dictionary> _table = new(); private static readonly int[] _legendPropertyCounts = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 properties : 21/52 : 40% 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 property : 15/52 : 29% 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 2 properties : 10/52 : 19% 3, 3, 3, 3, 3, 3 // 3 properties : 6/52 : 12% }; private static readonly int[] _elderPropertyCounts = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 properties : 15/34 : 44% 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 property : 10/34 : 29% 2, 2, 2, 2, 2, 2, // 2 properties : 6/34 : 18% 3, 3, 3 // 3 properties : 3/34 : 9% }; private static readonly int[] _grandPropertyCounts = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 properties : 10/20 : 50% 1, 1, 1, 1, 1, 1, // 1 property : 6/20 : 30% 2, 2, 2, // 2 properties : 3/20 : 15% 3 // 3 properties : 1/20 : 5% }; private static readonly int[] _masterPropertyCounts = { 0, 0, 0, 0, 0, 0, // 0 properties : 6/10 : 60% 1, 1, 1, // 1 property : 3/10 : 30% 2 // 2 properties : 1/10 : 10% }; private static readonly int[] _adeptPropertyCounts = { 0, 0, 0, // 0 properties : 3/4 : 75% 1 // 1 property : 1/4 : 25% }; [InvalidateProperties] [SerializableField(0)] [SerializedCommandProperty(AccessLevel.GameMaster)] private BookQuality _quality; [InvalidateProperties] [SerializableField(1)] [SerializedCommandProperty(AccessLevel.GameMaster)] private string _engravedText; [InvalidateProperties] [SerializableField(2)] [SerializedCommandProperty(AccessLevel.GameMaster)] private string _crafter; [InvalidateProperties] [SerializableField(3)] [SerializedCommandProperty(AccessLevel.GameMaster)] private SlayerName _slayer; [InvalidateProperties] [SerializableField(4)] [SerializedCommandProperty(AccessLevel.GameMaster)] private SlayerName _slayer2; [SerializedIgnoreDupe] [SerializableField(5, setter: "private")] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] private AosAttributes _attributes; [SerializedIgnoreDupe] [SerializableField(6, setter: "private")] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] private AosSkillBonuses _skillBonuses; [SerializableField(8)] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _spellCount; [Constructible] public Spellbook(ulong content = 0, int itemID = 0xEFA) : base(itemID) { _attributes = new AosAttributes(this); _skillBonuses = new AosSkillBonuses(this); Layer = Layer.OneHanded; LootType = LootType.Blessed; // The setter is calculating the spell count Content = content; } public override double DefaultWeight => 3.0; public override bool DisplayWeight => false; public virtual SpellbookType SpellbookType => SpellbookType.Regular; public virtual int BookOffset => 0; public virtual int BookCount => 64; [SerializableField(7, fieldChanged: nameof(OnContentChanged))] [SerializedCommandProperty(AccessLevel.GameMaster)] [InvalidateProperties] private ulong _content; private void OnContentChanged(ulong oldValue, ulong newValue) { // This assignment will mark it as dirty SpellCount = 0; while (newValue > 0) { _spellCount += (int)(newValue & 0x1); newValue >>= 1; } } public override bool DisplayLootType => Core.AOS; public virtual int OnCraft( int quality, bool makersMark, Mobile from, CraftSystem craftSystem, Type typeRes, BaseTool tool, CraftItem craftItem, int resHue ) { var magery = from.Skills.Magery.BaseFixedPoint; if (magery >= 800) { int[] propertyCounts; int minIntensity; int maxIntensity; if (magery >= 1000) { if (magery >= 1200) { propertyCounts = _legendPropertyCounts; } else if (magery >= 1100) { propertyCounts = _elderPropertyCounts; } else { propertyCounts = _grandPropertyCounts; } minIntensity = 55; maxIntensity = 75; } else if (magery >= 900) { propertyCounts = _masterPropertyCounts; minIntensity = 25; maxIntensity = 45; } else { propertyCounts = _adeptPropertyCounts; minIntensity = 0; maxIntensity = 15; } var propertyCount = propertyCounts.RandomElement(); BaseRunicTool.ApplyAttributesTo(this, true, 0, propertyCount, minIntensity, maxIntensity); } if (makersMark) { Crafter = from.RawName; } Quality = (BookQuality)(quality - 1); return quality; } public static void Configure() { CommandSystem.Register("AllSpells", AccessLevel.GameMaster, AllSpells_OnCommand); } [Usage("AllSpells"), Description("Completely fills a targeted spellbook with scrolls.")] private static void AllSpells_OnCommand(CommandEventArgs e) { e.Mobile.BeginTarget(-1, false, TargetFlags.None, AllSpells_OnTarget); e.Mobile.SendMessage("Target the spellbook to fill."); } private static void AllSpells_OnTarget(Mobile from, object obj) { if (obj is Spellbook book) { book.Content = book.BookCount == 64 ? ulong.MaxValue : (1ul << book.BookCount) - 1; from.SendMessage("The spellbook has been filled."); CommandLogging.WriteLine( from, $"{from.AccessLevel} {CommandLogging.Format(from)} filling spellbook {CommandLogging.Format(book)}" ); } else { from.BeginTarget(-1, false, TargetFlags.None, AllSpells_OnTarget); from.SendMessage("That is not a spellbook. Try again."); } } public static void OpenSpellbookRequest(Mobile from, int typeID) { if (!DesignContext.Check(from)) { return; // They are customizing } var type = typeID switch { 1 => SpellbookType.Regular, 2 => SpellbookType.Necromancer, 3 => SpellbookType.Paladin, 4 => SpellbookType.Ninja, 5 => SpellbookType.Samurai, 6 => SpellbookType.Arcanist, 7 => SpellbookType.Mystic, _ => SpellbookType.Regular }; var book = Find(from, -1, type); book?.DisplayTo(from); } public static void TargetedSpell(Mobile from, IEntity target, int spellId) { if (!DesignContext.Check(from)) { return; // They are customizing } var book = Find(from, spellId); if (book?.HasSpell(spellId) != true) { from.SendLocalizedMessage(500015); // You do not have that spell! return; } var move = SpellRegistry.GetSpecialMove(spellId); if (move != null) { SpecialMove.SetCurrentMove(from, move); } else { SpellRegistry.NewSpell(spellId, from, null)?.Cast(); } } public static void CastSpellRequest(Mobile from, int spellID, Item item) { if (!DesignContext.Check(from)) { return; // They are customizing } // Early rejection by spell ID before instantiation if (from is PlayerMobile { AccessLevel: < AccessLevel.Administrator } && FeatureFlagManager.IsSpellBlocked(spellID, out var reason)) { from.SendMessage(0x22, reason); return; } var book = item as Spellbook; if (book?.HasSpell(spellID) != true) { book = Find(from, spellID); } if (book?.HasSpell(spellID) == true) { var move = SpellRegistry.GetSpecialMove(spellID); if (move != null) { SpecialMove.SetCurrentMove(from, move); } else { var spell = SpellRegistry.NewSpell(spellID, from, null); if (spell != null) { spell.Cast(); } else { from.SendLocalizedMessage(502345); // This spell has been temporarily disabled. } } } else { from.SendLocalizedMessage(500015); // You do not have that spell! } } public static SpellbookType GetTypeForSpell(int spellID) { if (spellID >= 0 && spellID < 64) { return SpellbookType.Regular; } if (spellID >= 100 && spellID < 117) { return SpellbookType.Necromancer; } if (spellID >= 200 && spellID < 210) { return SpellbookType.Paladin; } if (spellID >= 400 && spellID < 406) { return SpellbookType.Samurai; } if (spellID >= 500 && spellID < 508) { return SpellbookType.Ninja; } if (spellID >= 600 && spellID < 617) { return SpellbookType.Arcanist; } if (spellID >= 677 && spellID < 693) { return SpellbookType.Mystic; } return SpellbookType.Invalid; } public static Spellbook FindRegular(Mobile from) => Find(from, -1, SpellbookType.Regular); public static Spellbook FindNecromancer(Mobile from) => Find(from, -1, SpellbookType.Necromancer); public static Spellbook FindPaladin(Mobile from) => Find(from, -1, SpellbookType.Paladin); public static Spellbook FindSamurai(Mobile from) => Find(from, -1, SpellbookType.Samurai); public static Spellbook FindNinja(Mobile from) => Find(from, -1, SpellbookType.Ninja); public static Spellbook FindArcanist(Mobile from) => Find(from, -1, SpellbookType.Arcanist); public static Spellbook FindMystic(Mobile from) => Find(from, -1, SpellbookType.Mystic); public static Spellbook Find(Mobile from, int spellID) => Find(from, spellID, GetTypeForSpell(spellID)); public static Spellbook Find(Mobile from, int spellID, SpellbookType type) { if (from == null) { return null; } if (from.Deleted) { _table.Remove(from); return null; } var searchAgain = false; if (!_table.TryGetValue(from, out var list)) { _table[from] = list = FindAllSpellbooks(from); } else { searchAgain = true; } var book = FindSpellbookInList(list, from, spellID, type); if (book == null && searchAgain) { _table[from] = list = FindAllSpellbooks(from); book = FindSpellbookInList(list, from, spellID, type); } return book; } public static Spellbook FindSpellbookInList(List list, Mobile from, int spellID, SpellbookType type) { var pack = from.Backpack; for (var i = list.Count - 1; i >= 0; --i) { if (i >= list.Count) { continue; } var book = list[i]; if (!book.Deleted && (book.Parent == from || pack != null && book.Parent == pack) && ValidateSpellbook(book, spellID, type)) { return book; } list.RemoveAt(i); } return null; } public static List FindAllSpellbooks(Mobile from) { var list = new List(); var spellbook = FindEquippedSpellbook(from); if (spellbook != null) { list.Add(spellbook); } var pack = from.Backpack; for (var i = 0; i < pack?.Items.Count; ++i) { if (pack.Items[i] is Spellbook sp) { list.Add(sp); } } return list; } public static Spellbook FindEquippedSpellbook(Mobile from) => from.FindItemOnLayer(Layer.OneHanded); public static bool ValidateSpellbook(Spellbook book, int spellID, SpellbookType type) => book.SpellbookType == type && (spellID == -1 || book.HasSpell(spellID)); public override bool AllowSecureTrade(Mobile from, Mobile to, Mobile newOwner, bool accepted) => Ethic.CheckTrade(from, to, newOwner, this) && base.AllowSecureTrade(from, to, newOwner, accepted); public override bool CanEquip(Mobile from) => Ethic.CheckEquip(from, this) && from.CanBeginAction() && base.CanEquip(from); public override bool AllowEquippedCast(Mobile from) => true; public override bool OnDragDrop(Mobile from, Item dropped) { if (dropped is not SpellScroll scroll) { return false; } var type = GetTypeForSpell(scroll.SpellID); if (type != SpellbookType) { return false; } if (HasSpell(scroll.SpellID)) { from.SendLocalizedMessage(500179); // That spell is already present in that spellbook. return false; } var val = scroll.SpellID - BookOffset; if (val >= 0 && val < BookCount) { _content |= (ulong)1 << val; ++SpellCount; InvalidateProperties(); scroll.Consume(); from.SendSound(0x249, GetWorldLocation()); return scroll.Deleted; } return false; } public override void OnAfterDuped(Item newItem) { if (newItem is not Spellbook book) { return; } book.Attributes = new AosAttributes(newItem, _attributes); book.SkillBonuses = new AosSkillBonuses(newItem, _skillBonuses); } public override void OnAdded(IEntity parent) { if (Core.AOS && parent is Mobile from) { SkillBonuses.AddTo(from); var strBonus = Attributes.BonusStr; var dexBonus = Attributes.BonusDex; var intBonus = Attributes.BonusInt; if (strBonus != 0 || dexBonus != 0 || intBonus != 0) { var serial = Serial; if (strBonus != 0) { from.AddStatMod(new StatMod(StatType.Str, $"{serial}Str", strBonus, TimeSpan.Zero)); } if (dexBonus != 0) { from.AddStatMod(new StatMod(StatType.Dex, $"{serial}Dex", dexBonus, TimeSpan.Zero)); } if (intBonus != 0) { from.AddStatMod(new StatMod(StatType.Int, $"{serial}Int", intBonus, TimeSpan.Zero)); } } from.CheckStatTimers(); } } public override void OnRemoved(IEntity parent) { if (Core.AOS && parent is Mobile from) { SkillBonuses.Remove(); var serial = Serial; from.RemoveStatMod($"{serial}Str"); from.RemoveStatMod($"{serial}Dex"); from.RemoveStatMod($"{serial}Int"); from.CheckStatTimers(); } } public bool HasSpell(int spellID) { spellID -= BookOffset; return spellID >= 0 && spellID < BookCount && (_content & ((ulong)1 << spellID)) != 0; } public void DisplayTo(Mobile to) { // The client must know about the spellbook or it will crash! var ns = to.NetState; if (ns.CannotSendPackets()) { return; } if (Parent == null) { SendWorldPacketTo(to.NetState); } else if (Parent is Item) { to.NetState.SendContainerContentUpdate(this); } else if (Parent is Mobile) { to.NetState.SendEquipUpdate(this); } to.NetState.SendDisplaySpellbook(Serial); to.NetState.SendSpellbookContent(Serial, ItemID, BookOffset + 1, _content); } public override void GetProperties(IPropertyList list) { base.GetProperties(list); if (_quality == BookQuality.Exceptional) { list.Add(1063341); // exceptional } if (_engravedText != null) { list.Add(1072305, _engravedText); // Engraved: ~1_INSCRIPTION~ } if (_crafter != null) { list.Add(1050043, _crafter); // crafted by ~1_NAME~ } _skillBonuses.GetProperties(list); if (_slayer != SlayerName.None) { var entry = SlayerGroup.GetEntryByName(_slayer); if (entry != null) { list.Add(entry.Title); } } if (_slayer2 != SlayerName.None) { var entry = SlayerGroup.GetEntryByName(_slayer2); if (entry != null) { list.Add(entry.Title); } } _attributes.GetProperties(list); list.Add(1042886, _spellCount); // ~1_NUMBERS_OF_SPELLS~ Spells } public override void OnSingleClick(Mobile from) { base.OnSingleClick(from); if (_crafter != null) { LabelTo(from, 1050043, _crafter); // crafted by ~1_NAME~ } LabelTo(from, 1042886, _spellCount.ToString()); } public override void OnDoubleClick(Mobile from) { var pack = from.Backpack; if (Parent == from || pack != null && Parent == pack) { DisplayTo(from); } else { from.SendLocalizedMessage( 500207 ); // The spellbook must be in your backpack (and not in a container within) to open. } } private void Deserialize(IGenericReader reader, int version) { _quality = (BookQuality)reader.ReadByte(); _engravedText = reader.ReadString(); Timer.DelayCall((item, crafter) => item._crafter = crafter?.RawName, this, reader.ReadEntity()); _slayer = (SlayerName)reader.ReadInt(); _slayer2 = (SlayerName)reader.ReadInt(); _attributes = new AosAttributes(this); _attributes.Deserialize(reader); _skillBonuses = new AosSkillBonuses(this); _skillBonuses.Deserialize(reader); _content = reader.ReadULong(); _spellCount = reader.ReadInt(); if (Core.AOS && Parent is Mobile mobile) { _skillBonuses.AddTo(mobile); } var strBonus = _attributes.BonusStr; var dexBonus = _attributes.BonusDex; var intBonus = _attributes.BonusInt; if (Parent is Mobile m) { if (strBonus != 0 || dexBonus != 0 || intBonus != 0) { var serial = Serial; if (strBonus != 0) { m.AddStatMod(new StatMod(StatType.Str, $"{serial}Str", strBonus, TimeSpan.Zero)); } if (dexBonus != 0) { m.AddStatMod(new StatMod(StatType.Dex, $"{serial}Dex", dexBonus, TimeSpan.Zero)); } if (intBonus != 0) { m.AddStatMod(new StatMod(StatType.Int, $"{serial}Int", intBonus, TimeSpan.Zero)); } } m.CheckStatTimers(); } } }