using System; using System.Collections.Generic; using log4net; using ACE.Common; using ACE.DatLoader; using ACE.DatLoader.Entity; using ACE.DatLoader.FileTypes; using ACE.Database; using ACE.Entity.Enum; using ACE.Entity.Enum.Properties; using ACE.Server.WorldObjects; namespace ACE.Server.Entity { /// /// The Spell class for game code /// A wrapper around SpellBase and Database.Spell /// public partial class Spell: IEquatable { private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); /// /// The spell information from the client DAT /// public SpellBase _spellBase; /// /// The spell information from the server DB /// public Database.Models.World.Spell _spell; /// /// Returns TRUE if spell is missing from either the client DAT or the server spell db /// public bool NotFound { get => _spellBase == null || _spell == null; } /// /// The components required to cast the spell /// public SpellFormula Formula; /// /// Constructs a Spell from a spell ID /// /// If FALSE, only loads the DAT info (faster) public Spell(uint spellID, bool loadDB = true) { Init(spellID, loadDB); } /// /// Constructs a Spell from a spell ID /// public Spell(int spellID, bool loadDB = true) { Init((uint)spellID, loadDB); } /// /// Constructs a Spell from a Spell enum /// public Spell(SpellId spell, bool loadDB = true) { Init((uint)spell, loadDB); } /// /// Default initializer /// public void Init(uint spellID, bool loadDB = true) { DatManager.PortalDat.SpellTable.Spells.TryGetValue(spellID, out _spellBase); if (loadDB) _spell = DatabaseManager.World.GetCachedSpell(spellID); if (_spellBase != null) Formula = new SpellFormula(this, _formula); if (loadDB && (_spell == null || _spellBase == null)) log.DebugFormat("Spell.Init(spellID = {0}, loadDB = {1}) failed! {2} {3}", spellID, loadDB, (_spell == null ? "_spell was null" : ""), (_spellBase == null ? "_spellBase was null" : "")); } /// /// Uses the server spell level formula, /// which checks the power level of the spell, /// and compares to the minimum power for each spell level. /// public uint Level { // this uses the server / power-based formula // for the client / scarab-based formula, use Formula.Level get { for (uint spellLevel = SpellFormula.MaxSpellLevel; spellLevel > 0; spellLevel--) { var minPower = SpellFormula.MinPower[spellLevel]; if (Power >= minPower) return spellLevel; } return 0; } } /// /// Returns TRUE if the spell levels are the same between the client and server formulas /// public bool LevelMatch { get => Formula.Level == Level; } /// /// Returns TRUE if this is a beneficial spell /// public bool IsBeneficial => Flags.HasFlag(SpellFlags.Beneficial); /// /// Returns TRUE if this is a harmful spell /// public bool IsHarmful { get => !IsBeneficial; } /// /// Returns TRUE if this spell is resistable /// public bool IsResistable => Flags.HasFlag(SpellFlags.Resistable); public bool IsProjectile => NumProjectiles > 0; public bool IsSelfTargeted => Flags.HasFlag(SpellFlags.SelfTargeted); public bool IsTracking => !Flags.HasFlag(SpellFlags.NonTrackingProjectile); public bool IsFellowshipSpell { get { // some spells are missing SpellFlags.FellowshipSpell: // 3043 - Kiss of the Grave // 3320 - Lesser Corrosive Ward // 3375 - Fungal Bloom // 3470 - Lesser Endless Well // 3474 - Lesser Soothing Wind // 3478 - Lesser Golden Wind // some spells have SpellFlags.FellowshipSpell, but aren't an actual Fellow* MetaSpellType: // 3337 - Inferno Ward (Enchantment) // 3381 - Debilitating Spore (Boost) // 3382 - Diseased Air (Boost) // 3406 - Kivik Lir's Boon (Enchantment) return Flags.HasFlag(SpellFlags.FellowshipSpell) || MetaSpellType >= SpellType.FellowBoost && MetaSpellType <= SpellType.FellowDispel; } } public List TryBurnComponents(Player player) { var consumed = new List(); // the base rate for each component is defined per-spell var baseRate = ComponentLoss; // get magic skill mod var magicSkill = GetMagicSkill(); var playerSkill = player.GetCreatureSkill(magicSkill); var skillMod = Math.Min(1.0f, (float)Power / playerSkill.Current); //Console.WriteLine($"TryBurnComponents.SkillMod: {skillMod}"); //DebugComponents(); foreach (var component in Formula.CurrentFormula) { if (!SpellFormula.SpellComponentsTable.SpellComponents.TryGetValue(component, out var spellComponent)) { Console.WriteLine($"Spell.TryBurnComponents(): Couldn't find SpellComponent {component}"); continue; } // component burn rate = spell base rate * component destruction modifier * skillMod? var burnRate = baseRate * spellComponent.CDM * skillMod; // TODO: curve? var rng = ThreadSafeRandom.Next(0.0f, 1.0f); if (rng < burnRate) consumed.Add(component); } return consumed; } public void DebugComponents() { var baseComponents = Formula.Components; var currComponents = Formula.CurrentFormula; Console.WriteLine($"{Name}:"); Console.WriteLine($"Base formula: {string.Join(", ", GetComponentNames(baseComponents))}"); Console.WriteLine($"Current formula: {string.Join(", ", GetComponentNames(currComponents))}"); } public static string GetConsumeString(List components) { var compNames = GetComponentNames(components); return $"The spell consumed the following components: {string.Join(", ", compNames)}"; } public static List GetComponentNames(List components) { var compNames = new List(); foreach (var component in components) { if (!SpellFormula.SpellComponentsTable.SpellComponents.TryGetValue(component, out var spellComponent)) { Console.WriteLine($"Spell.GetComponentNames(): Couldn't find SpellComponent {component}"); continue; } compNames.Add(spellComponent.Name); } return compNames; } public static uint SpellComponentDIDs = 0x27000002; public static uint GetComponentWCID(uint compID) { var dualDIDs = DatManager.PortalDat.ReadFromDat(SpellComponentDIDs); if (!dualDIDs.ClientEnumToID.TryGetValue(compID, out var wcid)) { Console.WriteLine($"GetComponentWCID({compID}): couldn't find component ID"); return 0; } return wcid; } public Skill GetMagicSkill() { switch (School) { case MagicSchool.CreatureEnchantment: return Skill.CreatureEnchantment; case MagicSchool.ItemEnchantment: return Skill.ItemEnchantment; case MagicSchool.LifeMagic: return Skill.LifeMagic; case MagicSchool.WarMagic: return Skill.WarMagic; case MagicSchool.VoidMagic: return Skill.VoidMagic; } return Skill.None; } /// /// Returns TRUE if spell category matches impen / bane / brittlemail / lure /// public bool IsImpenBaneType { get { switch (Category) { case SpellCategory n when n >= SpellCategory.ArmorValueRaising && n <= SpellCategory.AcidicResistanceLowering: case SpellCategory.ArmorValueRaisingRare: case SpellCategory.AcidResistanceRaisingRare: case SpellCategory.BludgeonResistanceRaisingRare: case SpellCategory.ColdResistanceRaisingRare: case SpellCategory.ElectricResistanceRaisingRare: case SpellCategory.FireResistanceRaisingRare: case SpellCategory.PierceResistanceRaisingRare: case SpellCategory.SlashResistanceRaisingRare: return true; default: return false; } } } /// /// Returns TRUE if spell category matches spells that should redirect to items player is holding /// public bool IsItemRedirectableType { get { switch (Category) { case SpellCategory.DamageRaisingRare: case SpellCategory.AttackModRaisingRare: case SpellCategory.DefenseModRaisingRare: case SpellCategory.WeaponTimeRaisingRare: case SpellCategory.AppraisalResistanceLoweringRare: case SpellCategory.MaxDamageRaising: return true; default: return false; } } } public bool IsNegativeRedirectable => IsHarmful && (IsImpenBaneType || IsOtherNegativeRedirectable); public bool IsOtherNegativeRedirectable { get { switch (Category) { case SpellCategory.DamageLowering: // encompasses both blood and spirit loather, inconsistent with spirit drinker in dat case SpellCategory.DefenseModLowering: case SpellCategory.AttackModLowering: case SpellCategory.WeaponTimeLowering: // verified case SpellCategory.ManaConversionModLowering: // hermetic void, replaced hide value, unchanged category in dat return true; } return false; } } public bool IsPortalSpell { get { switch (MetaSpellType) { case SpellType.PortalLink: case SpellType.PortalRecall: case SpellType.PortalSending: case SpellType.PortalSummon: case SpellType.FellowPortalSending: return true; } return false; } } /// /// Handles forward compatibility for old item spells which should be auras /// public bool HasItemCategory { get { switch (Category) { case SpellCategory.AttackModRaising: case SpellCategory.DamageRaising: case SpellCategory.DefenseModRaising: case SpellCategory.WeaponTimeRaising: // verified case SpellCategory.ManaConversionModRaising: case SpellCategory.SpellDamageRaising: return true; } return false; } } /// /// Returns TRUE for any spells which could potentially affect the run rate, /// such as spells which alter run / quickness / strength /// public bool UpdatesRunRate { get { if (_spell == null) return false; // this is commented out as below in UpdatesMaxVitals // i forget the exact reasoning, are all the proper hooks in places for each vitae %, // and not just add/remove? /*if (_spell.Id == 666) // vitae return true;*/ if (StatModType.HasFlag(EnchantmentTypeFlags.Attribute) && (StatModKey == (uint)PropertyAttribute.Strength || StatModKey == (uint)PropertyAttribute.Quickness)) return true; if (StatModType.HasFlag(EnchantmentTypeFlags.Skill) && StatModKey == (uint)Skill.Run) return true; return false; } } /// /// Returns a list of MaxVitals affected by this spell /// public List UpdatesMaxVitals { get { var maxVitals = new List(); if (_spell == null) return maxVitals; /*if (_spell.Id == 666) // Vitae { maxVitals.Add(PropertyAttribute2nd.MaxHealth); maxVitals.Add(PropertyAttribute2nd.MaxStamina); maxVitals.Add(PropertyAttribute2nd.MaxMana); return maxVitals; }*/ if (StatModType.HasFlag(EnchantmentTypeFlags.SecondAtt) && StatModKey != 0) maxVitals.Add((PropertyAttribute2nd)StatModKey); else if (StatModType.HasFlag(EnchantmentTypeFlags.Attribute)) { switch ((PropertyAttribute)StatModKey) { case PropertyAttribute.Endurance: maxVitals.Add(PropertyAttribute2nd.MaxHealth); maxVitals.Add(PropertyAttribute2nd.MaxStamina); break; case PropertyAttribute.Self: maxVitals.Add(PropertyAttribute2nd.MaxMana); break; } } return maxVitals; } } /// /// Returns TRUE if this spell is a DamageOverTime or HealingOverTime spell /// public bool IsDamageOverTime { get { if (Flags.HasFlag(SpellFlags.DamageOverTime)) return true; switch (Category) { case SpellCategory.HealOverTimeRaising: case SpellCategory.DamageOverTimeRaising: case SpellCategory.AetheriaProcHealthOverTimeRaising: case SpellCategory.AetheriaProcDamageOverTimeRaising: case SpellCategory.NetherDamageOverTimeRaising: case SpellCategory.NetherDamageOverTimeRaising2: case SpellCategory.NetherDamageOverTimeRaising3: return true; } switch ((PropertyInt)StatModKey) { case PropertyInt.HealOverTime: case PropertyInt.DamageOverTime: return true; } return false; } } public bool HasExtraTick => IsDamageOverTime; public bool Equals(Spell spell) { return spell != null && Id == spell.Id; } public override int GetHashCode() { return Id.GetHashCode(); } public override string ToString() { return Name; } } }