// Copyright (c) CypherCore All rights reserved. // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. using Framework.Constants; using Framework.Dynamic; using Game.DataStorage; using Game.Miscellaneous; using Game.Networking.Packets; using Game.Spells; using System; using System.Collections.Generic; using System.Linq; namespace Game.Entities { public partial class Player { void UpdateSkillsForLevel() { Race race = GetRace(); ushort maxSkill = GetMaxSkillValueForLevel(); SkillInfo skillInfoField = m_activePlayerData.Skill; foreach (var pair in mSkillStatus) { if (pair.Value.State == SkillState.Deleted || skillInfoField.SkillRank[pair.Value.Pos] == 0) continue; uint pskill = pair.Key; SkillRaceClassInfoRecord rcEntry = Global.DB2Mgr.GetSkillRaceClassInfo(pskill, GetRace(), GetClass()); if (rcEntry == null) continue; if (Global.SpellMgr.GetSkillRangeType(rcEntry) == SkillRangeType.Level) { if (rcEntry.HasFlag(SkillRaceClassInfoFlags.AlwaysMaxValue)) SetSkillRank(pair.Value.Pos, maxSkill); SetSkillMaxRank(pair.Value.Pos, maxSkill); if (pair.Value.State != SkillState.New) pair.Value.State = SkillState.Changed; } // Update level dependent skillline spells LearnSkillRewardedSpells(rcEntry.SkillID, skillInfoField.SkillRank[pair.Value.Pos], race); } } public ushort GetSkillValue(SkillType skill) { return GetSkillValue((uint)skill); } public ushort GetSkillValue(uint skill) { if (skill == 0) return 0; SkillInfo skillInfo = m_activePlayerData.Skill; var skillStatusData = mSkillStatus.LookupByKey(skill); if (skillStatusData == null || skillStatusData.State == SkillState.Deleted || skillInfo.SkillRank[skillStatusData.Pos] == 0) return 0; int result = skillInfo.SkillRank[skillStatusData.Pos]; result += skillInfo.SkillTempBonus[skillStatusData.Pos]; result += skillInfo.SkillPermBonus[skillStatusData.Pos]; return (ushort)(result < 0 ? 0 : result); } ushort GetMaxSkillValue(SkillType skill) { return GetMaxSkillValue((uint)skill); } ushort GetMaxSkillValue(uint skill) { if (skill == 0) return 0; SkillInfo skillInfo = m_activePlayerData.Skill; var skillStatusData = mSkillStatus.LookupByKey(skill); if (skillStatusData == null || skillStatusData.State == SkillState.Deleted || skillInfo.SkillRank[skillStatusData.Pos] == 0) return 0; int result = skillInfo.SkillMaxRank[skillStatusData.Pos]; result += skillInfo.SkillTempBonus[skillStatusData.Pos]; result += skillInfo.SkillPermBonus[skillStatusData.Pos]; return (ushort)(result < 0 ? 0 : result); } public ushort GetPureSkillValue(SkillType skill) { return GetPureSkillValue((uint)skill); } public ushort GetPureSkillValue(uint skill) { if (skill == 0) return 0; SkillInfo skillInfo = m_activePlayerData.Skill; var skillStatusData = mSkillStatus.LookupByKey(skill); if (skillStatusData == null || skillStatusData.State == SkillState.Deleted || skillInfo.SkillRank[skillStatusData.Pos] == 0) return 0; return skillInfo.SkillRank[skillStatusData.Pos]; } public ushort GetSkillStep(SkillType skill) { return GetSkillStep((uint)skill); } public ushort GetSkillStep(uint skill) { if (skill == 0) return 0; SkillInfo skillInfo = m_activePlayerData.Skill; var skillStatusData = mSkillStatus.LookupByKey(skill); if (skillStatusData == null || skillStatusData.State == SkillState.Deleted || skillInfo.SkillRank[skillStatusData.Pos] == 0) return 0; return skillInfo.SkillStep[skillStatusData.Pos]; } public ushort GetPureMaxSkillValue(SkillType skill) { if (skill == 0) return 0; SkillInfo skillInfo = m_activePlayerData.Skill; var skillStatusData = mSkillStatus.LookupByKey(skill); if (skillStatusData == null || skillStatusData.State == SkillState.Deleted || skillInfo.SkillRank[skillStatusData.Pos] == 0) return 0; return skillInfo.SkillMaxRank[skillStatusData.Pos]; } public ushort GetBaseSkillValue(SkillType skill) { if (skill == 0) return 0; SkillInfo skillInfo = m_activePlayerData.Skill; var skillStatusData = mSkillStatus.LookupByKey(skill); if (skillStatusData == null || skillStatusData.State == SkillState.Deleted || skillInfo.SkillRank[skillStatusData.Pos] == 0) return 0; int result = skillInfo.SkillRank[skillStatusData.Pos]; result += skillInfo.SkillPermBonus[skillStatusData.Pos]; return (ushort)(result < 0 ? 0 : result); } public ushort GetSkillPermBonusValue(uint skill) { if (skill == 0) return 0; SkillInfo skillInfo = m_activePlayerData.Skill; var skillStatusData = mSkillStatus.LookupByKey(skill); if (skillStatusData == null || skillStatusData.State == SkillState.Deleted || skillInfo.SkillRank[skillStatusData.Pos] == 0) return 0; return skillInfo.SkillPermBonus[skillStatusData.Pos]; } public ushort GetSkillTempBonusValue(uint skill) { if (skill == 0) return 0; SkillInfo skillInfo = m_activePlayerData.Skill; var skillStatusData = mSkillStatus.LookupByKey(skill); if (skillStatusData == null || skillStatusData.State == SkillState.Deleted || skillInfo.SkillRank[skillStatusData.Pos] == 0) return 0; return skillInfo.SkillTempBonus[skillStatusData.Pos]; } void InitializeSelfResurrectionSpells() { ClearSelfResSpell(); uint[] spells = new uint[3]; var dummyAuras = GetAuraEffectsByType(AuraType.Dummy); foreach (var auraEffect in dummyAuras) { // Soulstone Resurrection // prio: 3 (max, non death persistent) if (auraEffect.GetSpellInfo().SpellFamilyName == SpellFamilyNames.Warlock && auraEffect.GetSpellInfo().SpellFamilyFlags[1].HasAnyFlag(0x1000000u)) spells[0] = 3026; // Twisting Nether // prio: 2 (max) else if (auraEffect.GetId() == 23701 && RandomHelper.randChance(10)) spells[1] = 23700; } // Reincarnation (passive spell) // prio: 1 if (HasSpell(20608) && !GetSpellHistory().HasCooldown(21169)) spells[2] = 21169; foreach (uint selfResSpell in spells) if (selfResSpell != 0) AddSelfResSpell(selfResSpell); } public void PetSpellInitialize() { Pet pet = GetPet(); if (pet == null) return; Log.outDebug(LogFilter.Pet, "Pet Spells Groups"); CharmInfo charmInfo = pet.GetCharmInfo(); PetSpells petSpellsPacket = new(); petSpellsPacket.PetGUID = pet.GetGUID(); petSpellsPacket.CreatureFamily = (ushort)pet.GetCreatureTemplate().Family; // creature family (required for pet talents) petSpellsPacket.Specialization = pet.GetSpecialization(); petSpellsPacket.TimeLimit = (uint)pet.GetDuration(); petSpellsPacket.ReactState = pet.GetReactState(); petSpellsPacket.CommandState = charmInfo.GetCommandState(); // action bar loop for (byte i = 0; i < SharedConst.ActionBarIndexMax; ++i) petSpellsPacket.ActionButtons[i] = charmInfo.GetActionBarEntry(i).packedData; if (pet.IsPermanentPetFor(this)) { // spells loop foreach (var pair in pet.m_spells) { if (pair.Value.state == PetSpellState.Removed) continue; petSpellsPacket.Actions.Add(UnitActionBarEntry.MAKE_UNIT_ACTION_BUTTON(pair.Key, (uint)pair.Value.active)); } } // Cooldowns pet.GetSpellHistory().WritePacket(petSpellsPacket); SendPacket(petSpellsPacket); } public bool CanSeeSpellClickOn(Creature creature) { if (!creature.HasNpcFlag(NPCFlags.SpellClick)) return false; var clickBounds = Global.ObjectMgr.GetSpellClickInfoMapBounds(creature.GetEntry()); if (clickBounds.Empty()) return true; foreach (var spellClickInfo in clickBounds) { if (!spellClickInfo.IsFitToRequirements(this, creature)) return false; if (Global.ConditionMgr.IsObjectMeetingSpellClickConditions(creature.GetEntry(), spellClickInfo.spellId, this, creature)) return true; } return false; } public override SpellInfo GetCastSpellInfo(SpellInfo spellInfo, ref TriggerCastFlags triggerFlag, GetCastSpellInfoContext context) { var overrides = m_overrideSpells.LookupByKey(spellInfo.Id); if (!overrides.Empty()) { foreach (uint spellId in overrides) { if (context.AddSpell(spellId)) { SpellInfo newInfo = Global.SpellMgr.GetSpellInfo(spellId, GetMap().GetDifficultyID()); if (newInfo != null) return GetCastSpellInfo(newInfo, ref triggerFlag, context); } } } return base.GetCastSpellInfo(spellInfo, ref triggerFlag, context); } public void SetOverrideSpellsId(uint overrideSpellsId) { SetUpdateFieldValue(m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.OverrideSpellsID), overrideSpellsId); } public void AddOverrideSpell(uint overridenSpellId, uint newSpellId) { m_overrideSpells.Add(overridenSpellId, newSpellId); } public void RemoveOverrideSpell(uint overridenSpellId, uint newSpellId) { m_overrideSpells.Remove(overridenSpellId, newSpellId); } void LearnSpecializationSpells() { var specSpells = Global.DB2Mgr.GetSpecializationSpells((uint)GetPrimarySpecialization()); if (specSpells != null) { for (int j = 0; j < specSpells.Count; ++j) { SpecializationSpellsRecord specSpell = specSpells[j]; SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(specSpell.SpellID, Difficulty.None); if (spellInfo == null || spellInfo.SpellLevel > GetLevel()) continue; LearnSpell(specSpell.SpellID, true); if (specSpell.OverridesSpellID != 0) AddOverrideSpell(specSpell.OverridesSpellID, specSpell.SpellID); } } } void RemoveSpecializationSpells() { for (uint i = 0; i < PlayerConst.MaxSpecializations; ++i) { ChrSpecializationRecord specialization = Global.DB2Mgr.GetChrSpecializationByIndex(GetClass(), i); if (specialization != null) { var specSpells = Global.DB2Mgr.GetSpecializationSpells(specialization.Id); if (specSpells != null) { for (int j = 0; j < specSpells.Count; ++j) { SpecializationSpellsRecord specSpell = specSpells[j]; RemoveSpell(specSpell.SpellID); if (specSpell.OverridesSpellID != 0) RemoveOverrideSpell(specSpell.OverridesSpellID, specSpell.SpellID); } } for (uint j = 0; j < PlayerConst.MaxMasterySpells; ++j) { uint mastery = specialization.MasterySpellID[j]; if (mastery != 0) RemoveAurasDueToSpell(mastery); } } } } public void SendSpellCategoryCooldowns() { SpellCategoryCooldown cooldowns = new(); var categoryCooldownAuras = GetAuraEffectsByType(AuraType.ModSpellCategoryCooldown); foreach (AuraEffect aurEff in categoryCooldownAuras) { uint categoryId = (uint)aurEff.GetMiscValue(); var cooldownInfo = cooldowns.CategoryCooldowns.Find(p => p.Category == categoryId); if (cooldownInfo == null) cooldowns.CategoryCooldowns.Add(new SpellCategoryCooldown.CategoryCooldownInfo(categoryId, -aurEff.GetAmount())); else cooldownInfo.ModCooldown -= aurEff.GetAmount(); } SendPacket(cooldowns); } void InitializeSkillFields() { uint i = 0; foreach (SkillLineRecord skillLine in CliDB.SkillLineStorage.Values) { SkillRaceClassInfoRecord rcEntry = Global.DB2Mgr.GetSkillRaceClassInfo(skillLine.Id, GetRace(), GetClass()); if (rcEntry != null) { SetSkillLineId(i, (ushort)skillLine.Id); SetSkillStartingRank(i, 1); mSkillStatus.Add(skillLine.Id, new SkillStatusData(i, SkillState.Unchanged)); if (++i >= SkillConst.MaxPlayerSkills) break; } } } public bool UpdateSkillPro(SkillType skillId, int chance, uint step) { return UpdateSkillPro((uint)skillId, chance, step); } public bool UpdateSkillPro(uint skillId, int chance, uint step) { // levels sync. with spell requirement for skill levels to learn // bonus abilities in sSkillLineAbilityStore // Used only to avoid scan DBC at each skill grow uint[] bonusSkillLevels = { 75, 150, 225, 300, 375, 450, 525, 600, 700, 850 }; Log.outDebug(LogFilter.Player, "UpdateSkillPro(SkillId {0}, Chance {0:D3}%)", skillId, chance / 10.0f); if (skillId == 0) return false; if (chance <= 0) // speedup in 0 chance case { Log.outDebug(LogFilter.Player, "Player:UpdateSkillPro Chance={0:D3}% missed", chance / 10.0f); return false; } var skillStatusData = mSkillStatus.LookupByKey(skillId); if (skillStatusData == null || skillStatusData.State == SkillState.Deleted) return false; SkillInfo skillInfoField = m_activePlayerData.Skill; ushort value = skillInfoField.SkillRank[skillStatusData.Pos]; ushort max = skillInfoField.SkillMaxRank[skillStatusData.Pos]; if (max == 0 || value == 0 || value >= max) return false; if (RandomHelper.IRand(1, 1000) > chance) { Log.outDebug(LogFilter.Player, "Player:UpdateSkillPro Chance={0:F3}% missed", chance / 10.0f); return false; } ushort new_value = (ushort)(value + step); if (new_value > max) new_value = max; SetSkillRank(skillStatusData.Pos, new_value); if (skillStatusData.State != SkillState.New) skillStatusData.State = SkillState.Changed; foreach (uint bsl in bonusSkillLevels) { if (value < bsl && new_value >= bsl) { LearnSkillRewardedSpells(skillId, new_value, GetRace()); break; } } UpdateSkillEnchantments(skillId, value, new_value); UpdateCriteria(CriteriaType.SkillRaised, skillId); Log.outDebug(LogFilter.Player, "Player:UpdateSkillPro Chance={0:F3}% taken", chance / 10.0f); return true; } void UpdateSkillEnchantments(uint skill_id, ushort curr_value, ushort new_value) { for (byte i = 0; i < InventorySlots.ReagentBagEnd; ++i) { if (m_items[i] != null) { for (EnchantmentSlot slot = 0; slot < EnchantmentSlot.Max; ++slot) { uint ench_id = m_items[i].GetEnchantmentId(slot); if (ench_id == 0) continue; SpellItemEnchantmentRecord Enchant = CliDB.SpellItemEnchantmentStorage.LookupByKey(ench_id); if (Enchant == null) return; if (Enchant.RequiredSkillID == skill_id) { // Checks if the enchantment needs to be applied or removed if (curr_value < Enchant.RequiredSkillRank && new_value >= Enchant.RequiredSkillRank) ApplyEnchantment(m_items[i], slot, true); else if (new_value < Enchant.RequiredSkillRank && curr_value >= Enchant.RequiredSkillRank) ApplyEnchantment(m_items[i], slot, false); } // If we're dealing with a gem inside a prismatic socket we need to check the prismatic socket requirements // rather than the gem requirements itself. If the socket has no color it is a prismatic socket. if ((slot == EnchantmentSlot.Sock1 || slot == EnchantmentSlot.Sock2 || slot == EnchantmentSlot.Sock3) && m_items[i].GetSocketColor((uint)(slot - EnchantmentSlot.Sock1)) == 0) { SpellItemEnchantmentRecord pPrismaticEnchant = CliDB.SpellItemEnchantmentStorage.LookupByKey(m_items[i].GetEnchantmentId(EnchantmentSlot.Prismatic)); if (pPrismaticEnchant != null && pPrismaticEnchant.RequiredSkillID == skill_id) { if (curr_value < pPrismaticEnchant.RequiredSkillRank && new_value >= pPrismaticEnchant.RequiredSkillRank) ApplyEnchantment(m_items[i], slot, true); else if (new_value < pPrismaticEnchant.RequiredSkillRank && curr_value >= pPrismaticEnchant.RequiredSkillRank) ApplyEnchantment(m_items[i], slot, false); } } } } } } void UpdateEnchantTime(uint time) { for (var i = 0; i < m_enchantDuration.Count; ++i) { var enchantDuration = m_enchantDuration[i]; if (enchantDuration.item.GetEnchantmentId(enchantDuration.slot) == 0) { m_enchantDuration.Remove(enchantDuration); } else if (enchantDuration.leftduration <= time) { ApplyEnchantment(enchantDuration.item, enchantDuration.slot, false, false); enchantDuration.item.ClearEnchantment(enchantDuration.slot); m_enchantDuration.Remove(enchantDuration); } else if (enchantDuration.leftduration > time) { enchantDuration.leftduration -= time; } } } void ApplyEnchantment(Item item, bool apply) { for (EnchantmentSlot slot = 0; slot < EnchantmentSlot.Max; ++slot) ApplyEnchantment(item, slot, apply); } public void ApplyEnchantment(Item item, EnchantmentSlot slot, bool apply, bool apply_dur = true, bool ignore_condition = false) { if (item == null || !item.IsEquipped()) return; if (slot >= EnchantmentSlot.Max) return; uint enchant_id = item.GetEnchantmentId(slot); if (enchant_id == 0) return; SpellItemEnchantmentRecord pEnchant = CliDB.SpellItemEnchantmentStorage.LookupByKey(enchant_id); if (pEnchant == null) return; if (!ignore_condition && pEnchant.ConditionID != 0 && !EnchantmentFitsRequirements(pEnchant.ConditionID, -1)) return; if (pEnchant.MinLevel > GetLevel()) return; if (pEnchant.RequiredSkillID > 0 && pEnchant.RequiredSkillRank > GetSkillValue((SkillType)pEnchant.RequiredSkillID)) return; // If we're dealing with a gem inside a prismatic socket we need to check the prismatic socket requirements // rather than the gem requirements itself. If the socket has no color it is a prismatic socket. if ((slot == EnchantmentSlot.Sock1 || slot == EnchantmentSlot.Sock2 || slot == EnchantmentSlot.Sock3)) { if (item.GetSocketColor((uint)(slot - EnchantmentSlot.Sock1)) == 0) { // Check if the requirements for the prismatic socket are met before applying the gem stats SpellItemEnchantmentRecord pPrismaticEnchant = CliDB.SpellItemEnchantmentStorage.LookupByKey(item.GetEnchantmentId(EnchantmentSlot.Prismatic)); if (pPrismaticEnchant == null || (pPrismaticEnchant.RequiredSkillID > 0 && pPrismaticEnchant.RequiredSkillRank > GetSkillValue((SkillType)pPrismaticEnchant.RequiredSkillID))) return; } // Cogwheel gems dont have requirement data set in SpellItemEnchantment.dbc, but they do have it in Item-sparse.db2 SocketedGem gem = item.GetGem((ushort)(slot - EnchantmentSlot.Sock1)); if (gem != null) { ItemTemplate gemTemplate = Global.ObjectMgr.GetItemTemplate(gem.ItemId); if (gemTemplate != null) if (gemTemplate.GetRequiredSkill() != 0 && GetSkillValue((SkillType)gemTemplate.GetRequiredSkill()) < gemTemplate.GetRequiredSkillRank()) return; } } if (!item.IsBroken()) { for (int s = 0; s < ItemConst.MaxItemEnchantmentEffects; ++s) { ItemEnchantmentType enchant_display_type = (ItemEnchantmentType)pEnchant.Effect[s]; uint enchant_amount = pEnchant.EffectPointsMin[s]; uint enchant_spell_id = pEnchant.EffectArg[s]; switch (enchant_display_type) { case ItemEnchantmentType.None: break; case ItemEnchantmentType.CombatSpell: // processed in Player.CastItemCombatSpell break; case ItemEnchantmentType.Damage: { WeaponAttackType attackType = Player.GetAttackBySlot(item.GetSlot(), item.GetTemplate().GetInventoryType()); if (attackType != WeaponAttackType.Max) UpdateDamageDoneMods(attackType, apply ? -1 : (int)slot); } break; case ItemEnchantmentType.EquipSpell: if (enchant_spell_id != 0) { if (apply) CastSpell(this, enchant_spell_id, item); else RemoveAurasDueToItemSpell(enchant_spell_id, item.GetGUID()); } break; case ItemEnchantmentType.Resistance: if (pEnchant.ScalingClass != 0) { int scalingClass = pEnchant.ScalingClass; if ((m_unitData.MinItemLevel != 0 || m_unitData.MaxItemLevel != 0) && pEnchant.ScalingClassRestricted != 0) scalingClass = pEnchant.ScalingClassRestricted; uint minLevel = pEnchant.HasFlag(SpellItemEnchantmentFlags.ScaleAsAGem) ? 1 : 60u; uint scalingLevel = GetLevel(); byte maxLevel = (byte)(pEnchant.MaxLevel != 0 ? pEnchant.MaxLevel : CliDB.SpellScalingGameTable.GetTableRowCount() - 1); if (minLevel > GetLevel()) scalingLevel = minLevel; else if (maxLevel < GetLevel()) scalingLevel = maxLevel; GtSpellScalingRecord spellScaling = CliDB.SpellScalingGameTable.GetRow(scalingLevel); if (spellScaling != null) enchant_amount = (uint)(pEnchant.EffectScalingPoints[s] * CliDB.GetSpellScalingColumnForClass(spellScaling, scalingClass)); } enchant_amount = Math.Max(enchant_amount, 1u); HandleStatFlatModifier((UnitMods)((uint)UnitMods.ResistanceStart + enchant_spell_id), UnitModifierFlatType.Total, enchant_amount, apply); break; case ItemEnchantmentType.Stat: { if (pEnchant.ScalingClass != 0) { int scalingClass = pEnchant.ScalingClass; if ((m_unitData.MinItemLevel != 0 || m_unitData.MaxItemLevel != 0) && pEnchant.ScalingClassRestricted != 0) scalingClass = pEnchant.ScalingClassRestricted; uint minLevel = pEnchant.HasFlag(SpellItemEnchantmentFlags.ScaleAsAGem) ? 1 : 60u; uint scalingLevel = GetLevel(); byte maxLevel = (byte)(pEnchant.MaxLevel != 0 ? pEnchant.MaxLevel : CliDB.SpellScalingGameTable.GetTableRowCount() - 1); if (minLevel > GetLevel()) scalingLevel = minLevel; else if (maxLevel < GetLevel()) scalingLevel = maxLevel; GtSpellScalingRecord spellScaling = CliDB.SpellScalingGameTable.GetRow(scalingLevel); if (spellScaling != null) enchant_amount = (uint)(pEnchant.EffectScalingPoints[s] * CliDB.GetSpellScalingColumnForClass(spellScaling, scalingClass)); } enchant_amount = Math.Max(enchant_amount, 1u); Log.outDebug(LogFilter.Player, "Adding {0} to stat nb {1}", enchant_amount, enchant_spell_id); switch ((ItemModType)enchant_spell_id) { case ItemModType.Mana: Log.outDebug(LogFilter.Player, "+ {0} MANA", enchant_amount); HandleStatFlatModifier(UnitMods.Mana, UnitModifierFlatType.Base, enchant_amount, apply); break; case ItemModType.Health: Log.outDebug(LogFilter.Player, "+ {0} HEALTH", enchant_amount); HandleStatFlatModifier(UnitMods.Health, UnitModifierFlatType.Base, enchant_amount, apply); break; case ItemModType.Agility: Log.outDebug(LogFilter.Player, "+ {0} AGILITY", enchant_amount); HandleStatFlatModifier(UnitMods.StatAgility, UnitModifierFlatType.Total, enchant_amount, apply); UpdateStatBuffMod(Stats.Agility); break; case ItemModType.Strength: Log.outDebug(LogFilter.Player, "+ {0} STRENGTH", enchant_amount); HandleStatFlatModifier(UnitMods.StatStrength, UnitModifierFlatType.Total, enchant_amount, apply); UpdateStatBuffMod(Stats.Strength); break; case ItemModType.Intellect: Log.outDebug(LogFilter.Player, "+ {0} INTELLECT", enchant_amount); HandleStatFlatModifier(UnitMods.StatIntellect, UnitModifierFlatType.Total, enchant_amount, apply); UpdateStatBuffMod(Stats.Intellect); break; //case ItemModType.Spirit: //Log.outDebug(LogFilter.Player, "+ {0} SPIRIT", enchant_amount); //HandleStatModifier(UnitMods.StatSpirit, UnitModifierType.TotalValue, enchant_amount, apply); //ApplyStatBuffMod(Stats.Spirit, enchant_amount, apply); //break; case ItemModType.Stamina: Log.outDebug(LogFilter.Player, "+ {0} STAMINA", enchant_amount); HandleStatFlatModifier(UnitMods.StatStamina, UnitModifierFlatType.Total, enchant_amount, apply); UpdateStatBuffMod(Stats.Stamina); break; case ItemModType.DefenseSkillRating: ApplyRatingMod(CombatRating.DefenseSkill, (int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} DEFENSE", enchant_amount); break; case ItemModType.DodgeRating: ApplyRatingMod(CombatRating.Dodge, (int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} DODGE", enchant_amount); break; case ItemModType.ParryRating: ApplyRatingMod(CombatRating.Parry, (int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} PARRY", enchant_amount); break; case ItemModType.BlockRating: ApplyRatingMod(CombatRating.Block, (int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} SHIELD_BLOCK", enchant_amount); break; case ItemModType.HitMeleeRating: ApplyRatingMod(CombatRating.HitMelee, (int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} MELEE_HIT", enchant_amount); break; case ItemModType.HitRangedRating: ApplyRatingMod(CombatRating.HitRanged, (int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} RANGED_HIT", enchant_amount); break; case ItemModType.HitSpellRating: ApplyRatingMod(CombatRating.HitSpell, (int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} SPELL_HIT", enchant_amount); break; case ItemModType.CritMeleeRating: ApplyRatingMod(CombatRating.CritMelee, (int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} MELEE_CRIT", enchant_amount); break; case ItemModType.CritRangedRating: ApplyRatingMod(CombatRating.CritRanged, (int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} RANGED_CRIT", enchant_amount); break; case ItemModType.CritSpellRating: ApplyRatingMod(CombatRating.CritSpell, (int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} SPELL_CRIT", enchant_amount); break; case ItemModType.HasteSpellRating: ApplyRatingMod(CombatRating.HasteSpell, (int)enchant_amount, apply); break; case ItemModType.HitRating: ApplyRatingMod(CombatRating.HitMelee, (int)enchant_amount, apply); ApplyRatingMod(CombatRating.HitRanged, (int)enchant_amount, apply); ApplyRatingMod(CombatRating.HitSpell, (int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} HIT", enchant_amount); break; case ItemModType.CritRating: ApplyRatingMod(CombatRating.CritMelee, (int)enchant_amount, apply); ApplyRatingMod(CombatRating.CritRanged, (int)enchant_amount, apply); ApplyRatingMod(CombatRating.CritSpell, (int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} CRITICAL", enchant_amount); break; case ItemModType.ResilienceRating: ApplyRatingMod(CombatRating.ResiliencePlayerDamage, (int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} RESILIENCE", enchant_amount); break; case ItemModType.HasteRating: ApplyRatingMod(CombatRating.HasteMelee, (int)enchant_amount, apply); ApplyRatingMod(CombatRating.HasteRanged, (int)enchant_amount, apply); ApplyRatingMod(CombatRating.HasteSpell, (int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} HASTE", enchant_amount); break; case ItemModType.ExpertiseRating: ApplyRatingMod(CombatRating.Expertise, (int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} EXPERTISE", enchant_amount); break; case ItemModType.AttackPower: HandleStatFlatModifier(UnitMods.AttackPower, UnitModifierFlatType.Total, enchant_amount, apply); HandleStatFlatModifier(UnitMods.AttackPowerRanged, UnitModifierFlatType.Total, enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} ATTACK_POWER", enchant_amount); break; case ItemModType.RangedAttackPower: HandleStatFlatModifier(UnitMods.AttackPowerRanged, UnitModifierFlatType.Total, enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} RANGED_ATTACK_POWER", enchant_amount); break; case ItemModType.ManaRegeneration: ApplyManaRegenBonus((int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} MANA_REGENERATION", enchant_amount); break; case ItemModType.ArmorPenetrationRating: ApplyRatingMod(CombatRating.ArmorPenetration, (int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} ARMOR PENETRATION", enchant_amount); break; case ItemModType.SpellPower: ApplySpellPowerBonus((int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} SPELL_POWER", enchant_amount); break; case ItemModType.HealthRegen: ApplyHealthRegenBonus((int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} HEALTH_REGENERATION", enchant_amount); break; case ItemModType.SpellPenetration: ApplySpellPenetrationBonus((int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} SPELL_PENETRATION", enchant_amount); break; case ItemModType.BlockValue: HandleBaseModFlatValue(BaseModGroup.ShieldBlockValue, enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} BLOCK_VALUE", enchant_amount); break; case ItemModType.MasteryRating: ApplyRatingMod(CombatRating.Mastery, (int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} MASTERY", enchant_amount); break; case ItemModType.Versatility: ApplyRatingMod(CombatRating.VersatilityDamageDone, (int)enchant_amount, apply); ApplyRatingMod(CombatRating.VersatilityHealingDone, (int)enchant_amount, apply); ApplyRatingMod(CombatRating.VersatilityDamageTaken, (int)enchant_amount, apply); Log.outDebug(LogFilter.Player, "+ {0} VERSATILITY", enchant_amount); break; default: break; } break; } case ItemEnchantmentType.Totem: // Shaman Rockbiter Weapon { WeaponAttackType attackType = Player.GetAttackBySlot(item.GetSlot(), item.GetTemplate().GetInventoryType()); if (attackType != WeaponAttackType.Max) UpdateDamageDoneMods(attackType, apply ? -1 : (int)slot); break; } case ItemEnchantmentType.UseSpell: // processed in Player.CastItemUseSpell break; case ItemEnchantmentType.PrismaticSocket: case ItemEnchantmentType.ArtifactPowerBonusRankByType: case ItemEnchantmentType.ArtifactPowerBonusRankByID: case ItemEnchantmentType.BonusListID: case ItemEnchantmentType.BonusListCurve: case ItemEnchantmentType.ArtifactPowerBonusRankPicker: // nothing do.. break; default: Log.outError(LogFilter.Player, "Unknown item enchantment (id = {0}) display type: {1}", enchant_id, enchant_display_type); break; } } } // visualize enchantment at player and equipped items if (slot == EnchantmentSlot.Perm && item.GetSlot() < m_playerData.VisibleItems.GetSize()) { VisibleItem visibleItem = m_values.ModifyValue(m_playerData).ModifyValue(m_playerData.VisibleItems, item.GetSlot()); SetUpdateFieldValue(visibleItem.ModifyValue(visibleItem.ItemVisual), item.GetVisibleItemVisual(this)); } if (apply_dur) { if (apply) { // set duration uint duration = item.GetEnchantmentDuration(slot); if (duration > 0) AddEnchantmentDuration(item, slot, duration); } else { // duration == 0 will remove EnchantDuration AddEnchantmentDuration(item, slot, 0); } } } public void ModifySkillBonus(SkillType skillid, int val, bool talent) { ModifySkillBonus((uint)skillid, val, talent); } public void ModifySkillBonus(uint skillid, int val, bool talent) { SkillInfo skillInfoField = m_activePlayerData.Skill; var skillStatusData = mSkillStatus.LookupByKey(skillid); if (skillStatusData == null || skillStatusData.State == SkillState.Deleted || skillInfoField.SkillRank[skillStatusData.Pos] == 0) return; if (talent) SetSkillPermBonus(skillStatusData.Pos, (ushort)(skillInfoField.SkillPermBonus[skillStatusData.Pos] + val)); else SetSkillTempBonus(skillStatusData.Pos, (ushort)(skillInfoField.SkillTempBonus[skillStatusData.Pos] + val)); // Apply/Remove bonus to child skill lines var childSkillLines = Global.DB2Mgr.GetSkillLinesForParentSkill(skillid); if (childSkillLines != null) foreach (var childSkillLine in childSkillLines) ModifySkillBonus(childSkillLine.Id, val, talent); } public void StopCastingBindSight() { Unit target = GetViewpoint()?.ToUnit(); if (target != null) { target.RemoveAurasByType(AuraType.BindSight, GetGUID()); target.RemoveAurasByType(AuraType.ModPossess, GetGUID()); target.RemoveAurasByType(AuraType.ModPossessPet, GetGUID()); } } void AddEnchantmentDurations(Item item) { for (EnchantmentSlot x = 0; x < EnchantmentSlot.Max; ++x) { if (item.GetEnchantmentId(x) == 0) continue; uint duration = item.GetEnchantmentDuration(x); if (duration > 0) AddEnchantmentDuration(item, x, duration); } } void AddEnchantmentDuration(Item item, EnchantmentSlot slot, uint duration) { if (item == null) return; if (slot >= EnchantmentSlot.Max) return; for (var i = 0; i < m_enchantDuration.Count; ++i) { var enchantDuration = m_enchantDuration[i]; if (enchantDuration.item == item && enchantDuration.slot == slot) { enchantDuration.item.SetEnchantmentDuration(enchantDuration.slot, enchantDuration.leftduration, this); m_enchantDuration.Remove(enchantDuration); break; } } if (duration > 0) { GetSession().SendItemEnchantTimeUpdate(GetGUID(), item.GetGUID(), (uint)slot, duration / 1000); m_enchantDuration.Add(new EnchantDuration(item, slot, duration)); } } void RemoveEnchantmentDurations(Item item) { for (var i = 0; i < m_enchantDuration.Count; ++i) { var enchantDuration = m_enchantDuration[i]; if (enchantDuration.item == item) { // save duration in item item.SetEnchantmentDuration(enchantDuration.slot, enchantDuration.leftduration, this); m_enchantDuration.Remove(enchantDuration); } } } void RemoveEnchantmentDurationsReferences(Item item) { for (var i = 0; i < m_enchantDuration.Count; ++i) { var enchantDuration = m_enchantDuration[i]; if (enchantDuration.item == item) m_enchantDuration.Remove(enchantDuration); } } public void RemoveArenaEnchantments(EnchantmentSlot slot) { // remove enchantments from equipped items first to clean up the m_enchantDuration list for (var i = 0; i < m_enchantDuration.Count; ++i) { var enchantDuration = m_enchantDuration[i]; if (enchantDuration.slot == slot) { if (enchantDuration.item != null && enchantDuration.item.GetEnchantmentId(slot) != 0) { // Poisons and DK runes are enchants which are allowed on arenas if (Global.SpellMgr.IsArenaAllowedEnchancment(enchantDuration.item.GetEnchantmentId(slot))) continue; // remove from stats ApplyEnchantment(enchantDuration.item, slot, false, false); // remove visual enchantDuration.item.ClearEnchantment(slot); } // remove from update list m_enchantDuration.Remove(enchantDuration); } } // remove enchants from inventory items // NOTE: no need to remove these from stats, since these aren't equipped // in inventory int inventoryEnd = InventorySlots.ItemStart + GetInventorySlotCount(); for (byte i = InventorySlots.ItemStart; i < inventoryEnd; ++i) { Item pItem = GetItemByPos(InventorySlots.Bag0, i); if (pItem != null && !Global.SpellMgr.IsArenaAllowedEnchancment(pItem.GetEnchantmentId(slot))) pItem.ClearEnchantment(slot); } // in inventory bags for (byte i = InventorySlots.BagStart; i < InventorySlots.BagEnd; ++i) { Bag pBag = GetBagByPos(i); if (pBag != null) { for (byte j = 0; j < pBag.GetBagSize(); j++) { Item pItem = pBag.GetItemByPos(j); if (pItem != null && !Global.SpellMgr.IsArenaAllowedEnchancment(pItem.GetEnchantmentId(slot))) pItem.ClearEnchantment(slot); } } } } public void UpdatePotionCooldown(Spell spell = null) { // no potion used i combat or still in combat if (m_lastPotionId == 0 || IsInCombat()) return; // Call not from spell cast, send cooldown event for item spells if no in combat if (spell == null) { // spell/item pair let set proper cooldown (except not existed charged spell cooldown spellmods for potions) ItemTemplate proto = Global.ObjectMgr.GetItemTemplate(m_lastPotionId); if (proto != null) for (byte idx = 0; idx < proto.Effects.Count; ++idx) { if (proto.Effects[idx].SpellID != 0 && proto.Effects[idx].TriggerType == ItemSpelltriggerType.OnUse) { SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo((uint)proto.Effects[idx].SpellID, Difficulty.None); if (spellInfo != null) GetSpellHistory().SendCooldownEvent(spellInfo, m_lastPotionId); } } } // from spell cases (m_lastPotionId set in Spell.SendSpellCooldown) else { if (spell.IsIgnoringCooldowns()) return; else GetSpellHistory().SendCooldownEvent(spell.m_spellInfo, m_lastPotionId, spell); } m_lastPotionId = 0; } public bool CanUseMastery() { ChrSpecializationRecord chrSpec = GetPrimarySpecializationEntry(); if (chrSpec != null) return HasSpell(chrSpec.MasterySpellID[0]) || HasSpell(chrSpec.MasterySpellID[1]); return false; } public bool HasSkill(SkillType skill) { return HasSkill((uint)skill); } public bool HasSkill(uint skill) { if (skill == 0) return false; SkillInfo skillInfoField = m_activePlayerData.Skill; var skillStatusData = mSkillStatus.LookupByKey(skill); return skillStatusData != null && skillStatusData.State != SkillState.Deleted && skillInfoField.SkillRank[skillStatusData.Pos] != 0; } public void SetSkill(SkillType skill, uint step, uint newVal, uint maxVal) { SetSkill((uint)skill, step, newVal, maxVal); } public void SetSkill(uint id, uint step, uint newVal, uint maxVal) { SkillLineRecord skillEntry = CliDB.SkillLineStorage.LookupByKey(id); if (skillEntry == null) { Log.outError(LogFilter.Misc, $"Player::SetSkill: Skill (SkillID: {id}) not found in SkillLineStore for player '{GetName()}' ({GetGUID()})"); return; } ushort currVal; var skillStatusData = mSkillStatus.LookupByKey(id); SkillInfo skillInfoField = m_activePlayerData.Skill; void refreshSkillBonusAuras() { // Temporary bonuses foreach (AuraEffect effect in GetAuraEffectsByType(AuraType.ModSkill)) if (effect.GetMiscValue() == id) effect.HandleEffect(this, AuraEffectHandleModes.Skill, true); foreach (AuraEffect effect in GetAuraEffectsByType(AuraType.ModSkill2)) if (effect.GetMiscValue() == id) effect.HandleEffect(this, AuraEffectHandleModes.Skill, true); // Permanent bonuses foreach (AuraEffect effect in GetAuraEffectsByType(AuraType.ModSkillTalent)) if (effect.GetMiscValue() == id) effect.HandleEffect(this, AuraEffectHandleModes.Skill, true); } // Handle already stored skills if (skillStatusData != null) { currVal = skillInfoField.SkillRank[skillStatusData.Pos]; // Activate and update skill line if (newVal != 0) { // enable parent skill line if missing if (skillEntry.ParentSkillLineID != 0 && skillEntry.ParentTierIndex > 0 && GetSkillStep(skillEntry.ParentSkillLineID) < skillEntry.ParentTierIndex) { var rcEntry = Global.DB2Mgr.GetSkillRaceClassInfo(skillEntry.ParentSkillLineID, GetRace(), GetClass()); if (rcEntry != null) { var tier = Global.ObjectMgr.GetSkillTier(rcEntry.SkillTierID); if (tier != null) SetSkill(skillEntry.ParentSkillLineID, (uint)skillEntry.ParentTierIndex, Math.Max(GetPureSkillValue(skillEntry.ParentSkillLineID), 1u), tier.GetValueForTierIndex(skillEntry.ParentTierIndex - 1)); } } // if skill value is going down, update enchantments before setting the new value if (newVal < currVal) UpdateSkillEnchantments(id, currVal, (ushort)newVal); // update step SetSkillStep(skillStatusData.Pos, (ushort)step); // update value SetSkillRank(skillStatusData.Pos, (ushort)newVal); SetSkillMaxRank(skillStatusData.Pos, (ushort)maxVal); LearnSkillRewardedSpells(id, newVal, GetRace()); // if skill value is going up, update enchantments after setting the new value if (newVal > currVal) { UpdateSkillEnchantments(id, currVal, (ushort)newVal); if (id == (uint)SkillType.Riding) UpdateMountCapability(); } UpdateCriteria(CriteriaType.SkillRaised, id); UpdateCriteria(CriteriaType.AchieveSkillStep, id); // update skill state if (skillStatusData.State == SkillState.Unchanged || skillStatusData.State == SkillState.Deleted) { if (currVal == 0) // activated skill, mark as new to save into database { skillStatusData.State = skillStatusData.State != SkillState.Deleted ? SkillState.New : SkillState.Changed; // skills marked as SKILL_DELETED already exist in database, mark as changed instead of new // Set profession line int freeProfessionSlot = FindEmptyProfessionSlotFor(id); if (freeProfessionSlot != -1) SetUpdateFieldValue(ref m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.ProfessionSkillLine, freeProfessionSlot), id); refreshSkillBonusAuras(); } else // updated skill, mark as changed to save into database skillStatusData.State = SkillState.Changed; } } else if (currVal != 0 && newVal == 0) // Deactivate skill line { // Try to store profession tools and accessories into the bag // If we can't, we can't unlearn the profession int professionSlot = GetProfessionSlotFor(id); if (professionSlot != -1) { byte professionSlotStart = (byte)(ProfessionSlots.Profession1Tool + professionSlot * ProfessionSlots.MaxCount); // Get all profession items equipped for (byte slotOffset = 0; slotOffset < ProfessionSlots.MaxCount; ++slotOffset) { Item professionItem = GetItemByPos(InventorySlots.Bag0, (byte)(professionSlotStart + slotOffset)); if (professionItem != null) { // Store item in bag List professionItemDest = new(); if (CanStoreItem(ItemConst.NullBag, ItemConst.NullSlot, professionItemDest, professionItem, false) != InventoryResult.Ok) { SendPacket(new DisplayGameError(GameError.InvFull)); return; } RemoveItem(InventorySlots.Bag0, professionItem.GetSlot(), true); StoreItem(professionItemDest, professionItem, true); } } // Clear profession lines SetUpdateFieldValue(ref m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.ProfessionSkillLine, professionSlot), 0u); } //remove enchantments needing this skill UpdateSkillEnchantments(id, currVal, 0); // clear skill fields SetSkillStep(skillStatusData.Pos, 0); SetSkillRank(skillStatusData.Pos, 0); SetSkillStartingRank(skillStatusData.Pos, 1); SetSkillMaxRank(skillStatusData.Pos, 0); SetSkillTempBonus(skillStatusData.Pos, 0); SetSkillPermBonus(skillStatusData.Pos, 0); // mark as deleted so the next save will delete the data from the database skillStatusData.State = skillStatusData.State != SkillState.New ? SkillState.Deleted : SkillState.Unchanged; // skills marked as SKILL_NEW don't exist in database (this distinction is not neccessary for deletion but for re-learning the same skill before save to db happens) // remove all spells that related to this skill List skillLineAbilities = Global.DB2Mgr.GetSkillLineAbilitiesBySkill(id); foreach (SkillLineAbilityRecord skillLineAbility in skillLineAbilities) RemoveSpell(Global.SpellMgr.GetFirstSpellInChain(skillLineAbility.Spell)); List childSkillLines = Global.DB2Mgr.GetSkillLinesForParentSkill(id); if (childSkillLines != null) { foreach (SkillLineRecord childSkillLine in childSkillLines) { if (childSkillLine.ParentSkillLineID == id) SetSkill(childSkillLine.Id, 0, 0, 0); } } } } else { // We are about to learn a skill that has been added outside of normal circumstances (Game Master command, scripts etc.) byte skillSlot = 0; // Find a free skill slot for (int i = 0; i < SkillConst.MaxPlayerSkills; ++i) { if (((SkillInfo)m_activePlayerData.Skill).SkillLineID[i] == 0) { skillSlot = (byte)i; break; } } if (skillSlot == 0) { Log.outError(LogFilter.Misc, $"Tried to add skill {id} but player {GetName()} ({GetGUID()}) cannot have additional skills"); return; } if (skillEntry.ParentSkillLineID != 0) { if (skillEntry.ParentTierIndex > 0) { SkillRaceClassInfoRecord rcEntry = Global.DB2Mgr.GetSkillRaceClassInfo(skillEntry.ParentSkillLineID, GetRace(), GetClass()); if (rcEntry != null) { SkillTiersEntry tier = Global.ObjectMgr.GetSkillTier(rcEntry.SkillTierID); if (tier != null) { ushort skillval = GetPureSkillValue((SkillType)skillEntry.ParentSkillLineID); SetSkill(skillEntry.ParentSkillLineID, (uint)skillEntry.ParentTierIndex, Math.Max(skillval, (ushort)1), tier.GetValueForTierIndex(skillEntry.ParentTierIndex - 1)); } } } } else { // also learn missing child skills at 0 value List childSkillLines = Global.DB2Mgr.GetSkillLinesForParentSkill(id); if (childSkillLines != null) foreach (SkillLineRecord childSkillLine in childSkillLines) if (!HasSkill((SkillType)childSkillLine.Id)) SetSkill(childSkillLine.Id, 0, 0, 0); int freeProfessionSlot = FindEmptyProfessionSlotFor(id); if (freeProfessionSlot != -1) SetUpdateFieldValue(ref m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.ProfessionSkillLine, freeProfessionSlot), id); } if (skillStatusData == null) SetSkillLineId(skillSlot, (ushort)id); SetSkillStep(skillSlot, (ushort)step); SetSkillRank(skillSlot, (ushort)newVal); SetSkillStartingRank(skillSlot, 1); SetSkillMaxRank(skillSlot, (ushort)maxVal); // apply skill bonuses SetSkillTempBonus(skillSlot, 0); SetSkillPermBonus(skillSlot, 0); UpdateSkillEnchantments(id, 0, (ushort)newVal); mSkillStatus.Add(id, new SkillStatusData(skillSlot, SkillState.New)); if (newVal != 0) { refreshSkillBonusAuras(); // Learn all spells for skill LearnSkillRewardedSpells(id, newVal, GetRace()); UpdateCriteria(CriteriaType.SkillRaised, id); UpdateCriteria(CriteriaType.AchieveSkillStep, id); } } } public bool UpdateCraftSkill(SpellInfo spellInfo) { if (spellInfo.HasAttribute(SpellAttr1.NoSkillIncrease)) return false; Log.outDebug(LogFilter.Player, "UpdateCraftSkill spellid {0}", spellInfo.Id); var bounds = Global.SpellMgr.GetSkillLineAbilityMapBounds(spellInfo.Id); foreach (var _spell_idx in bounds) { if (_spell_idx.SkillupSkillLineID != 0) { uint SkillValue = GetPureSkillValue((SkillType)_spell_idx.SkillupSkillLineID); // Alchemy Discoveries here if (spellInfo.Mechanic == Mechanics.Discovery) { uint discoveredSpell = SkillDiscovery.GetSkillDiscoverySpell(_spell_idx.SkillupSkillLineID, spellInfo.Id, this); if (discoveredSpell != 0) LearnSpell(discoveredSpell, false); } uint craft_skill_gain = _spell_idx.NumSkillUps * WorldConfig.GetUIntValue(WorldCfg.SkillGainCrafting); return UpdateSkillPro(_spell_idx.SkillupSkillLineID, SkillGainChance(SkillValue, _spell_idx.TrivialSkillLineRankHigh, (uint)(_spell_idx.TrivialSkillLineRankHigh + _spell_idx.TrivialSkillLineRankLow) / 2, _spell_idx.TrivialSkillLineRankLow), craft_skill_gain); } } return false; } public bool UpdateGatherSkill(uint skillId, uint skillValue, uint redLevel, uint multiplicator = 1, WorldObject obj = null) { return UpdateGatherSkill((SkillType)skillId, skillValue, redLevel, multiplicator, obj); } public bool UpdateGatherSkill(SkillType skillId, uint skillValue, uint redLevel, uint multiplicator = 1, WorldObject obj = null) { Log.outDebug(LogFilter.Player, $"UpdateGatherSkill(SkillId {skillId} SkillLevel {skillValue} RedLevel {redLevel})"); SkillLineRecord skillEntry = CliDB.SkillLineStorage.LookupByKey(skillId); if (skillEntry == null) return false; uint gatheringSkillGain = WorldConfig.GetUIntValue(WorldCfg.SkillGainGathering); uint baseSkillLevelStep = 30; uint yellowLevel = redLevel + baseSkillLevelStep; uint greenLevel = yellowLevel + baseSkillLevelStep; uint grayLevel = greenLevel + baseSkillLevelStep; GameObject go = obj?.ToGameObject(); if (go != null) { if (go.GetGoInfo().GetTrivialSkillLow() != 0) yellowLevel = go.GetGoInfo().GetTrivialSkillLow(); if (go.GetGoInfo().GetTrivialSkillHigh() != 0) grayLevel = go.GetGoInfo().GetTrivialSkillHigh(); greenLevel = (yellowLevel + grayLevel) / 2; } // For skinning and Mining chance decrease with level. 1-74 - no decrease, 75-149 - 2 times, 225-299 - 8 times switch ((SkillType)skillEntry.ParentSkillLineID) { case SkillType.Herbalism: return UpdateSkillPro(skillId, SkillGainChance(skillValue, grayLevel, greenLevel, yellowLevel) * (int)multiplicator, gatheringSkillGain); case SkillType.Skinning: if (WorldConfig.GetIntValue(WorldCfg.SkillChanceSkinningSteps) == 0) return UpdateSkillPro(skillId, SkillGainChance(skillValue, grayLevel, greenLevel, yellowLevel) * (int)multiplicator, gatheringSkillGain); else return UpdateSkillPro(skillId, (int)(SkillGainChance(skillValue, grayLevel, greenLevel, yellowLevel) * multiplicator) >> (int)(skillValue / WorldConfig.GetIntValue(WorldCfg.SkillChanceSkinningSteps)), gatheringSkillGain); case SkillType.Mining: if (WorldConfig.GetIntValue(WorldCfg.SkillChanceMiningSteps) == 0) return UpdateSkillPro(skillId, SkillGainChance(skillValue, grayLevel, greenLevel, yellowLevel) * (int)multiplicator, gatheringSkillGain); else return UpdateSkillPro(skillId, (int)(SkillGainChance(skillValue, grayLevel, greenLevel, yellowLevel) * multiplicator) >> (int)(skillValue / WorldConfig.GetIntValue(WorldCfg.SkillChanceMiningSteps)), gatheringSkillGain); } return false; } byte GetFishingStepsNeededToLevelUp(uint SkillValue) { // These formulas are guessed to be as close as possible to how the skill difficulty curve for fishing was on Retail. if (SkillValue < 75) return 1; if (SkillValue <= 300) return (byte)(SkillValue / 44); return (byte)(SkillValue / 31); } public bool UpdateFishingSkill(int expansion) { Log.outDebug(LogFilter.Player, $"Player::UpdateFishingSkill: Player '{GetName()}' ({GetGUID()}) Expansion: {expansion}"); uint fishingSkill = GetProfessionSkillForExp(SkillType.Fishing, expansion); if (fishingSkill == 0 || !HasSkill(fishingSkill)) return false; uint skillValue = GetPureSkillValue(fishingSkill); if (skillValue >= GetMaxSkillValue(fishingSkill)) return false; byte stepsNeededToLevelUp = GetFishingStepsNeededToLevelUp(skillValue); ++m_fishingSteps; if (m_fishingSteps >= stepsNeededToLevelUp) { m_fishingSteps = 0; uint gatheringSkillGain = WorldConfig.GetUIntValue(WorldCfg.SkillGainGathering); return UpdateSkillPro(fishingSkill, 100 * 10, gatheringSkillGain); } return false; } public uint GetProfessionSkillForExp(SkillType skill, int expansion) { return GetProfessionSkillForExp((uint)skill, expansion); } public uint GetProfessionSkillForExp(uint skill, int expansion) { SkillLineRecord skillEntry = CliDB.SkillLineStorage.LookupByKey(skill); if (skillEntry == null) return 0; if (skillEntry.ParentSkillLineID != 0 || (skillEntry.CategoryID != SkillCategory.Profession && skillEntry.CategoryID != SkillCategory.Secondary)) return 0; // The value -3 from ContentTuning refers to the current expansion if (expansion < 0) expansion = (int)PlayerConst.CurrentExpansion; var childSkillLines = Global.DB2Mgr.GetSkillLinesForParentSkill(skillEntry.Id); if (childSkillLines != null) { foreach (var childSkillLine in childSkillLines) { // Values of ParentTierIndex in SkillLine.db2 start at 4 (Classic) and increase by one for each expansion skillLine // Subtract 4 (BASE_PARENT_TIER_INDEX) from this value to obtain the expansion of the skillLine int skillLineExpansion = childSkillLine.ParentTierIndex - 4; if (expansion == skillLineExpansion) return childSkillLine.Id; } } return 0; } int SkillGainChance(uint SkillValue, uint GrayLevel, uint GreenLevel, uint YellowLevel) { if (SkillValue >= GrayLevel) return WorldConfig.GetIntValue(WorldCfg.SkillChanceGrey) * 10; if (SkillValue >= GreenLevel) return WorldConfig.GetIntValue(WorldCfg.SkillChanceGreen) * 10; if (SkillValue >= YellowLevel) return WorldConfig.GetIntValue(WorldCfg.SkillChanceYellow) * 10; return WorldConfig.GetIntValue(WorldCfg.SkillChanceOrange) * 10; } bool EnchantmentFitsRequirements(uint enchantmentcondition, sbyte slot) { if (enchantmentcondition == 0) return true; SpellItemEnchantmentConditionRecord Condition = CliDB.SpellItemEnchantmentConditionStorage.LookupByKey(enchantmentcondition); if (Condition == null) return true; byte[] curcount = { 0, 0, 0, 0 }; //counting current equipped gem colors for (byte i = EquipmentSlot.Start; i < EquipmentSlot.End; ++i) { if (i == slot) continue; Item pItem2 = GetItemByPos(InventorySlots.Bag0, i); if (pItem2 != null && !pItem2.IsBroken()) { foreach (SocketedGem gemData in pItem2.m_itemData.Gems) { ItemTemplate gemProto = Global.ObjectMgr.GetItemTemplate(gemData.ItemId); if (gemProto == null) continue; GemPropertiesRecord gemProperty = CliDB.GemPropertiesStorage.LookupByKey(gemProto.GetGemProperties()); if (gemProperty == null) continue; uint GemColor = (uint)gemProperty.Type; for (byte b = 0, tmpcolormask = 1; b < 4; b++, tmpcolormask <<= 1) { if (Convert.ToBoolean(tmpcolormask & GemColor)) ++curcount[b]; } } } } bool activate = true; for (byte i = 0; i < 5; i++) { if (Condition.LtOperandType[i] == 0) continue; uint _cur_gem = curcount[Condition.LtOperandType[i] - 1]; // if have use them as count, else use from Condition uint _cmp_gem = Condition.RtOperandType[i] != 0 ? curcount[Condition.RtOperandType[i] - 1] : Condition.RtOperand[i]; switch (Condition.Operator[i]) { case 2: // requires less than ( || ) gems activate &= (_cur_gem < _cmp_gem); break; case 3: // requires more than ( || ) gems activate &= (_cur_gem > _cmp_gem); break; case 5: // requires at least than ( || ) gems activate &= (_cur_gem >= _cmp_gem); break; } } Log.outDebug(LogFilter.Player, "Checking Condition {0}, there are {1} Meta Gems, {2} Red Gems, {3} Yellow Gems and {4} Blue Gems, Activate:{5}", enchantmentcondition, curcount[0], curcount[1], curcount[2], curcount[3], activate ? "yes" : "no"); return activate; } void CorrectMetaGemEnchants(byte exceptslot, bool apply) { //cycle all equipped items for (byte slot = EquipmentSlot.Start; slot < EquipmentSlot.End; ++slot) { //enchants for the slot being socketed are handled by Player.ApplyItemMods if (slot == exceptslot) continue; Item pItem = GetItemByPos(InventorySlots.Bag0, slot); if (pItem == null || pItem.GetSocketColor(0) == 0) continue; for (EnchantmentSlot enchant_slot = EnchantmentSlot.Sock1; enchant_slot < EnchantmentSlot.Sock3; ++enchant_slot) { uint enchant_id = pItem.GetEnchantmentId(enchant_slot); if (enchant_id == 0) continue; SpellItemEnchantmentRecord enchantEntry = CliDB.SpellItemEnchantmentStorage.LookupByKey(enchant_id); if (enchantEntry == null) continue; uint condition = enchantEntry.ConditionID; if (condition != 0) { //was enchant active with/without item? bool wasactive = EnchantmentFitsRequirements(condition, (sbyte)(apply ? exceptslot : -1)); //should it now be? if (wasactive ^ EnchantmentFitsRequirements(condition, (sbyte)(apply ? -1 : exceptslot))) { // ignore item gem conditions //if state changed, (dis)apply enchant ApplyEnchantment(pItem, enchant_slot, !wasactive, true, true); } } } } } public void CastItemUseSpell(Item item, SpellCastTargets targets, ObjectGuid castCount, uint[] misc) { if (!item.GetTemplate().HasFlag(ItemFlags.Legacy)) { // item spells casted at use foreach (ItemEffectRecord effectData in item.GetEffects()) { // wrong triggering type if (effectData.TriggerType != ItemSpelltriggerType.OnUse) continue; SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo((uint)effectData.SpellID, Difficulty.None); if (spellInfo == null) { Log.outError(LogFilter.Player, "Player.CastItemUseSpell: Item (Entry: {0}) in have wrong spell id {1}, ignoring", item.GetEntry(), effectData.SpellID); continue; } Spell spell = new(this, spellInfo, TriggerCastFlags.None); SpellPrepare spellPrepare = new(); spellPrepare.ClientCastID = castCount; spellPrepare.ServerCastID = spell.m_castId; SendPacket(spellPrepare); spell.m_fromClient = true; spell.m_CastItem = item; spell.m_misc.Data0 = misc[0]; spell.m_misc.Data1 = misc[1]; spell.Prepare(targets); return; } } // Item enchantments spells casted at use for (EnchantmentSlot e_slot = 0; e_slot < EnchantmentSlot.Max; ++e_slot) { uint enchant_id = item.GetEnchantmentId(e_slot); var pEnchant = CliDB.SpellItemEnchantmentStorage.LookupByKey(enchant_id); if (pEnchant == null) continue; for (byte s = 0; s < ItemConst.MaxItemEnchantmentEffects; ++s) { if (pEnchant.Effect[s] != ItemEnchantmentType.UseSpell) continue; SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(pEnchant.EffectArg[s], Difficulty.None); if (spellInfo == null) { Log.outError(LogFilter.Player, "Player.CastItemUseSpell Enchant {0}, cast unknown spell {1}", enchant_id, pEnchant.EffectArg[s]); continue; } Spell spell = new(this, spellInfo, TriggerCastFlags.None); SpellPrepare spellPrepare = new(); spellPrepare.ClientCastID = castCount; spellPrepare.ServerCastID = spell.m_castId; SendPacket(spellPrepare); spell.m_fromClient = true; spell.m_CastItem = item; spell.m_misc.Data0 = misc[0]; spell.m_misc.Data1 = misc[1]; spell.Prepare(targets); return; } } } public uint GetLastPotionId() { return m_lastPotionId; } public void SetLastPotionId(uint item_id) { m_lastPotionId = item_id; } public void LearnSkillRewardedSpells(uint skillId, uint skillValue, Race race) { uint classMask = GetClassMask(); List skillLineAbilities = Global.DB2Mgr.GetSkillLineAbilitiesBySkill(skillId); foreach (var ability in skillLineAbilities) { if (ability.SkillLine != skillId) continue; SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(ability.Spell, Difficulty.None); if (spellInfo == null) continue; switch (ability.AcquireMethod) { case SkillLineAbilityAcquireMethod.AutomaticSkillRank: case SkillLineAbilityAcquireMethod.AutomaticCharLevel: break; case SkillLineAbilityAcquireMethod.LearnedOrAutomaticCharLevel: // Treat as AutomaticCharLevel when conditions are met, otherwise treat it as Learned (trainer or quest) if (spellInfo.ShowFutureSpellPlayerConditionID != 0 && !ConditionManager.IsPlayerMeetingCondition(this, spellInfo.ShowFutureSpellPlayerConditionID)) continue; if (!Global.ConditionMgr.IsObjectMeetingNotGroupedConditions(ConditionSourceType.SkillLineAbility, ability.Id, this)) continue; break; default: continue; } // Check race if set var raceMask = new RaceMask(ability.RaceMask); if (!raceMask.IsEmpty() && !raceMask.HasRace(race)) continue; // Check class if set if (ability.ClassMask != 0 && !Convert.ToBoolean(ability.ClassMask & classMask)) continue; // Check level, skip class spells if not high enough uint requiredLevel = Math.Max(spellInfo.SpellLevel, spellInfo.BaseLevel); if (requiredLevel > GetLevel()) continue; // need unlearn spell if (skillValue < ability.MinSkillLineRank && ability.AcquireMethod == SkillLineAbilityAcquireMethod.AutomaticSkillRank) RemoveSpell(ability.Spell); // need learn else if (!IsInWorld) AddSpell(ability.Spell, true, true, true, false, false, ability.SkillLine); else LearnSpell(ability.Spell, true, ability.SkillLine); } } int GetProfessionSlotFor(uint skillId) { for (var i = 0; i < m_activePlayerData.ProfessionSkillLine.GetSize(); ++i) if (m_activePlayerData.ProfessionSkillLine[i] == skillId) return i; return -1; } int FindEmptyProfessionSlotFor(uint skillId) { SkillLineRecord skillEntry = CliDB.SkillLineStorage.LookupByKey(skillId); if (skillEntry == null) return -1; if (skillEntry.ParentSkillLineID != 0 || skillEntry.CategoryID != SkillCategory.Profession) return -1; int index = 0; // if there is no same profession, find any free slot foreach (var b in m_activePlayerData.ProfessionSkillLine) { if (b == 0) return index; index++; } return -1; } void RemoveItemDependentAurasAndCasts(Item pItem) { foreach (var pair in GetOwnedAuras()) { Aura aura = pair.Value; // skip not self applied auras SpellInfo spellInfo = aura.GetSpellInfo(); if (aura.GetCasterGUID() != GetGUID()) continue; // skip if not item dependent or have alternative item if (HasItemFitToSpellRequirements(spellInfo, pItem)) continue; // no alt item, remove aura, restart check RemoveOwnedAura(pair); } // currently casted spells can be dependent from item for (CurrentSpellTypes i = 0; i < CurrentSpellTypes.Max; ++i) { Spell spell = GetCurrentSpell(i); if (spell != null) if (spell.GetState() != SpellState.Delayed && !HasItemFitToSpellRequirements(spell.m_spellInfo, pItem)) InterruptSpell(i); } } public bool HasItemFitToSpellRequirements(SpellInfo spellInfo, Item ignoreItem = null) { if (spellInfo.EquippedItemClass < 0) return true; // scan other equipped items for same requirements (mostly 2 daggers/etc) // for optimize check 2 used cases only switch (spellInfo.EquippedItemClass) { case ItemClass.Weapon: { Item item = GetUseableItemByPos(InventorySlots.Bag0, EquipmentSlot.MainHand); if (item != null) if (item != ignoreItem && item.IsFitToSpellRequirements(spellInfo)) return true; item = GetUseableItemByPos(InventorySlots.Bag0, EquipmentSlot.OffHand); if (item != null) if (item != ignoreItem && item.IsFitToSpellRequirements(spellInfo)) return true; break; } case ItemClass.Armor: { if (!spellInfo.HasAttribute(SpellAttr8.RequiresEquippedInvTypes)) { // most used check: shield only if ((spellInfo.EquippedItemSubClassMask & (1 << (int)ItemSubClassArmor.Shield)) != 0) { Item item = GetUseableItemByPos(InventorySlots.Bag0, EquipmentSlot.OffHand); if (item != null) if (item != ignoreItem && item.IsFitToSpellRequirements(spellInfo)) return true; // special check to filter things like Shield Wall, the aura is not permanent and must stay even without required item if (!spellInfo.IsPassive()) { foreach (var spellEffectInfo in spellInfo.GetEffects()) if (spellEffectInfo.IsAura()) return true; } } // tabard not have dependent spells for (byte i = EquipmentSlot.Start; i < EquipmentSlot.MainHand; ++i) { Item item = GetUseableItemByPos(InventorySlots.Bag0, i); if (item != null) if (item != ignoreItem && item.IsFitToSpellRequirements(spellInfo)) return true; } } else { // requires item equipped in all armor slots foreach (byte i in new[] { EquipmentSlot.Head, EquipmentSlot.Shoulders, EquipmentSlot.Chest, EquipmentSlot.Waist, EquipmentSlot.Legs, EquipmentSlot.Feet, EquipmentSlot.Wrist, EquipmentSlot.Hands }) { Item item = GetUseableItemByPos(InventorySlots.Bag0, i); if (item == null || item == ignoreItem || !item.IsFitToSpellRequirements(spellInfo)) return false; } return true; } break; } default: Log.outError(LogFilter.Player, "HasItemFitToSpellRequirements: Not handled spell requirement for item class {0}", spellInfo.EquippedItemClass); break; } return false; } public Dictionary GetSpellMap() { return m_spells; } public override SpellSchoolMask GetMeleeDamageSchoolMask(WeaponAttackType attackType = WeaponAttackType.BaseAttack) { Item weapon = GetWeaponForAttack(attackType, true); if (weapon != null) return (SpellSchoolMask)(1 << (int)weapon.GetTemplate().GetDamageType()); return SpellSchoolMask.Normal; } void CastAllObtainSpells() { int inventoryEnd = InventorySlots.ItemStart + GetInventorySlotCount(); for (byte slot = InventorySlots.ItemStart; slot < inventoryEnd; ++slot) { Item item = GetItemByPos(InventorySlots.Bag0, slot); if (item != null) ApplyItemObtainSpells(item, true); } for (byte i = InventorySlots.BagStart; i < InventorySlots.ReagentBagEnd; ++i) { Bag bag = GetBagByPos(i); if (bag == null) continue; for (byte slot = 0; slot < bag.GetBagSize(); ++slot) { Item item = bag.GetItemByPos(slot); if (item != null) ApplyItemObtainSpells(item, true); } } } void ApplyItemObtainSpells(Item item, bool apply) { if (item.GetTemplate().HasFlag(ItemFlags.Legacy)) return; foreach (ItemEffectRecord effect in item.GetEffects()) { if (effect.TriggerType != ItemSpelltriggerType.OnPickup) // On obtain trigger continue; int spellId = effect.SpellID; if (spellId <= 0) continue; if (apply) { if (!HasAura((uint)spellId)) CastSpell(this, (uint)spellId, new CastSpellExtraArgs().SetCastItem(item)); } else RemoveAurasDueToSpell((uint)spellId); } } // this one rechecks weapon auras and stores them in BaseModGroup container // needed for things like axe specialization applying only to axe weapons in case of dual-wield void UpdateWeaponDependentCritAuras(WeaponAttackType attackType) { BaseModGroup modGroup; switch (attackType) { case WeaponAttackType.BaseAttack: modGroup = BaseModGroup.CritPercentage; break; case WeaponAttackType.OffAttack: modGroup = BaseModGroup.OffhandCritPercentage; break; case WeaponAttackType.RangedAttack: modGroup = BaseModGroup.RangedCritPercentage; break; default: return; } float amount = 0.0f; amount += GetTotalAuraModifier(AuraType.ModWeaponCritPercent, auraEffect => CheckAttackFitToAuraRequirement(attackType, auraEffect)); // these auras don't have item requirement (only Combat Expertise in 3.3.5a) amount += GetTotalAuraModifier(AuraType.ModCritPct); SetBaseModFlatValue(modGroup, amount); } public void UpdateAllWeaponDependentCritAuras() { for (var attackType = WeaponAttackType.BaseAttack; attackType < WeaponAttackType.Max; ++attackType) UpdateWeaponDependentCritAuras(attackType); } public void UpdateWeaponDependentAuras(WeaponAttackType attackType) { UpdateWeaponDependentCritAuras(attackType); UpdateDamageDoneMods(attackType); UpdateDamagePctDoneMods(attackType); } public void ApplyItemDependentAuras(Item item, bool apply) { if (apply) { var spells = GetSpellMap(); foreach (var pair in spells) { if (pair.Value.State == PlayerSpellState.Removed || pair.Value.Disabled) continue; SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(pair.Key, Difficulty.None); if (spellInfo == null || !spellInfo.IsPassive() || spellInfo.EquippedItemClass < 0) continue; if (!HasAura(pair.Key) && HasItemFitToSpellRequirements(spellInfo)) AddAura(pair.Key, this); // no SMSG_SPELL_GO in sniff found } } else RemoveItemDependentAurasAndCasts(item); } public override bool CheckAttackFitToAuraRequirement(WeaponAttackType attackType, AuraEffect aurEff) { SpellInfo spellInfo = aurEff.GetSpellInfo(); if (spellInfo.EquippedItemClass == ItemClass.None) return true; Item item = GetWeaponForAttack(attackType, true); if (item == null || !item.IsFitToSpellRequirements(spellInfo)) return false; return true; } public void AddTemporarySpell(uint spellId) { var spell = m_spells.LookupByKey(spellId); // spell already added - do not do anything if (spell != null) return; PlayerSpell newspell = new(); newspell.State = PlayerSpellState.Temporary; newspell.Active = true; newspell.Dependent = false; newspell.Disabled = false; m_spells[spellId] = newspell; } public void RemoveTemporarySpell(uint spellId) { var spell = m_spells.LookupByKey(spellId); // spell already not in list - do not do anything if (spell == null) return; // spell has other state than temporary - do not change it if (spell.State != PlayerSpellState.Temporary) return; m_spells.Remove(spellId); } public void UpdateZoneDependentAuras(uint newZone) { // Some spells applied at enter into zone (with subzones), aura removed in UpdateAreaDependentAuras that called always at zone.area update var saBounds = Global.SpellMgr.GetSpellAreaForAreaMapBounds(newZone); foreach (var spell in saBounds) if (spell.flags.HasAnyFlag(SpellAreaFlag.AutoCast) && spell.IsFitToRequirements(this, newZone, 0)) if (!HasAura(spell.spellId)) CastSpell(this, spell.spellId, true); } public void UpdateAreaDependentAuras(uint newArea) { // remove auras from spells with area limitations foreach (var pair in GetOwnedAuras()) { // use m_zoneUpdateId for speed: UpdateArea called from UpdateZone or instead UpdateZone in both cases m_zoneUpdateId up-to-date if (pair.Value.GetSpellInfo().CheckLocation(GetMapId(), m_zoneUpdateId, newArea, this) != SpellCastResult.SpellCastOk) RemoveOwnedAura(pair, AuraRemoveMode.Interrupt); } // some auras applied at subzone enter var saBounds = Global.SpellMgr.GetSpellAreaForAreaMapBounds(newArea); foreach (var spell in saBounds) if (spell.flags.HasAnyFlag(SpellAreaFlag.AutoCast) && spell.IsFitToRequirements(this, m_zoneUpdateId, newArea)) if (!HasAura(spell.spellId)) CastSpell(this, spell.spellId, true); } public void ApplyModToSpell(SpellModifier mod, Spell spell) { if (spell == null) return; // don't do anything with no charges if (mod.ownerAura.IsUsingCharges() && mod.ownerAura.GetCharges() == 0) return; // register inside spell, proc system uses this to drop charges spell.m_appliedMods.Add(mod.ownerAura); } public void LearnCustomSpells() { if (!WorldConfig.GetBoolValue(WorldCfg.StartAllSpells)) return; // learn default race/class spells PlayerInfo info = Global.ObjectMgr.GetPlayerInfo(GetRace(), GetClass()); foreach (var tspell in info.customSpells) { Log.outDebug(LogFilter.Player, "PLAYER (Class: {0} Race: {1}): Adding initial spell, id = {2}", GetClass(), GetRace(), tspell); if (!IsInWorld) // will send in INITIAL_SPELLS in list anyway at map add AddSpell(tspell, true, true, true, false); else // but send in normal spell in game learn case LearnSpell(tspell, true); } } public void LearnDefaultSkills() { // learn default race/class skills PlayerInfo info = Global.ObjectMgr.GetPlayerInfo(GetRace(), GetClass()); foreach (var rcInfo in info.skills) { if (HasSkill((SkillType)rcInfo.SkillID)) continue; if (rcInfo.MinLevel > GetLevel()) continue; LearnDefaultSkill(rcInfo); } } public void LearnDefaultSkill(SkillRaceClassInfoRecord rcInfo) { SkillType skillId = (SkillType)rcInfo.SkillID; switch (Global.SpellMgr.GetSkillRangeType(rcInfo)) { case SkillRangeType.Language: SetSkill(skillId, 0, 300, 300); break; case SkillRangeType.Level: { ushort skillValue = 1; ushort maxValue = GetMaxSkillValueForLevel(); if (rcInfo.HasFlag(SkillRaceClassInfoFlags.AlwaysMaxValue)) skillValue = maxValue; else if (GetClass() == Class.Deathknight) skillValue = (ushort)Math.Min(Math.Max(1, (GetLevel() - 1) * 5), maxValue); SetSkill(skillId, 0, skillValue, maxValue); break; } case SkillRangeType.Mono: SetSkill(skillId, 0, 1, 1); break; case SkillRangeType.Rank: { SkillTiersEntry tier = Global.ObjectMgr.GetSkillTier(rcInfo.SkillTierID); ushort maxValue = (ushort)tier.GetValueForTierIndex(0); ushort skillValue = 1; if (rcInfo.HasFlag(SkillRaceClassInfoFlags.AlwaysMaxValue)) skillValue = maxValue; else if (GetClass() == Class.Deathknight) skillValue = (ushort)Math.Min(Math.Max(1, (GetLevel() - 1) * 5), maxValue); SetSkill(skillId, 1, skillValue, maxValue); break; } default: break; } } void SendKnownSpells() { SendKnownSpells knownSpells = new(); knownSpells.InitialLogin = IsLoading(); foreach (var spell in m_spells.ToList()) { if (spell.Value.State == PlayerSpellState.Removed) continue; if (!spell.Value.Active || spell.Value.Disabled) continue; knownSpells.KnownSpells.Add(spell.Key); if (spell.Value.Favorite) knownSpells.FavoriteSpells.Add(spell.Key); } SendPacket(knownSpells); } void SendUnlearnSpells() { SendPacket(new SendUnlearnSpells()); } public void LearnSpell(uint spellId, bool dependent, uint fromSkill = 0, bool suppressMessaging = false, int? traitDefinitionId = null) { PlayerSpell spell = m_spells.LookupByKey(spellId); bool disabled = (spell != null) && spell.Disabled; bool active = !disabled || spell.Active; bool favorite = spell != null ? spell.Favorite : false; bool learning = AddSpell(spellId, active, true, dependent, false, false, fromSkill, favorite, traitDefinitionId); // prevent duplicated entires in spell book, also not send if not in world (loading) if (learning && IsInWorld) { LearnedSpells learnedSpells = new(); LearnedSpellInfo learnedSpellInfo = new(); learnedSpellInfo.SpellID = spellId; learnedSpellInfo.Favorite = favorite; learnedSpellInfo.TraitDefinitionID = traitDefinitionId; learnedSpells.SuppressMessaging = suppressMessaging; learnedSpells.ClientLearnedSpellData.Add(learnedSpellInfo); SendPacket(learnedSpells); } // learn all disabled higher ranks and required spells (recursive) if (disabled) { uint nextSpell = Global.SpellMgr.GetNextSpellInChain(spellId); if (nextSpell != 0) { var _spell = m_spells.LookupByKey(nextSpell); if (spellId != 0 && _spell.Disabled) LearnSpell(nextSpell, false, fromSkill); } var spellsRequiringSpell = Global.SpellMgr.GetSpellsRequiringSpellBounds(spellId); foreach (var id in spellsRequiringSpell) { var spell1 = m_spells.LookupByKey(id); if (spell1 != null && spell1.Disabled) LearnSpell(id, false, fromSkill); } } else UpdateQuestObjectiveProgress(QuestObjectiveType.LearnSpell, (int)spellId, 1); } public void RemoveSpell(uint spellId, bool disabled = false, bool learnLowRank = true, bool suppressMessaging = false) { var pSpell = m_spells.LookupByKey(spellId); if (pSpell == null) return; if (pSpell.State == PlayerSpellState.Removed || (disabled && pSpell.Disabled) || pSpell.State == PlayerSpellState.Temporary) return; // unlearn non talent higher ranks (recursive) uint nextSpell = Global.SpellMgr.GetNextSpellInChain(spellId); if (nextSpell != 0) { SpellInfo spellInfo1 = Global.SpellMgr.GetSpellInfo(nextSpell, Difficulty.None); if (HasSpell(nextSpell) && !spellInfo1.HasAttribute(SpellCustomAttributes.IsTalent)) RemoveSpell(nextSpell, disabled, false); } //unlearn spells dependent from recently removed spells var spellsRequiringSpell = Global.SpellMgr.GetSpellsRequiringSpellBounds(spellId); foreach (var id in spellsRequiringSpell) RemoveSpell(id, disabled); // re-search, it can be corrupted in prev loop pSpell = m_spells.LookupByKey(spellId); if (pSpell == null) return; // already unleared bool cur_active = pSpell.Active; bool cur_dependent = pSpell.Dependent; int? traitDefinitionId = pSpell.TraitDefinitionId; if (disabled) { pSpell.Disabled = disabled; if (pSpell.State != PlayerSpellState.New) pSpell.State = PlayerSpellState.Changed; } else { if (pSpell.State == PlayerSpellState.New) m_spells.Remove(spellId); else pSpell.State = PlayerSpellState.Removed; } RemoveOwnedAura(spellId, GetGUID()); SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(spellId, Difficulty.None); // remove pet auras for (byte i = 0; i < spellInfo.GetEffects().Count; ++i) { PetAura petSpell = Global.SpellMgr.GetPetAura(spellId, i); if (petSpell != null) RemovePetAura(petSpell); } // update free primary prof.points (if not overflow setting, can be in case GM use before .learn prof. learning) if (spellInfo.IsPrimaryProfessionFirstRank()) { uint freeProfs = GetFreePrimaryProfessionPoints() + 1; if (freeProfs <= WorldConfig.GetIntValue(WorldCfg.MaxPrimaryTradeSkill)) SetFreePrimaryProfessions(freeProfs); } // remove dependent skill var spellLearnSkill = Global.SpellMgr.GetSpellLearnSkill(spellId); if (spellLearnSkill != null) { uint prev_spell = Global.SpellMgr.GetPrevSpellInChain(spellId); if (prev_spell == 0) // first rank, remove skill SetSkill(spellLearnSkill.skill, 0, 0, 0); else { // search prev. skill setting by spell ranks chain var prevSkill = Global.SpellMgr.GetSpellLearnSkill(prev_spell); while (prevSkill == null && prev_spell != 0) { prev_spell = Global.SpellMgr.GetPrevSpellInChain(prev_spell); prevSkill = Global.SpellMgr.GetSpellLearnSkill(Global.SpellMgr.GetFirstSpellInChain(prev_spell)); } if (prevSkill == null) // not found prev skill setting, remove skill SetSkill(spellLearnSkill.skill, 0, 0, 0); else // set to prev. skill setting values { ushort skill_value = GetPureSkillValue(prevSkill.skill); ushort skill_max_value = GetPureMaxSkillValue(prevSkill.skill); ushort new_skill_max_value = prevSkill.maxvalue; if (new_skill_max_value == 0) { var rcInfo = Global.DB2Mgr.GetSkillRaceClassInfo((uint)prevSkill.skill, GetRace(), GetClass()); if (rcInfo != null) { switch (Global.SpellMgr.GetSkillRangeType(rcInfo)) { case SkillRangeType.Language: skill_value = 300; new_skill_max_value = 300; break; case SkillRangeType.Level: new_skill_max_value = GetMaxSkillValueForLevel(); break; case SkillRangeType.Mono: new_skill_max_value = 1; break; case SkillRangeType.Rank: { var tier = Global.ObjectMgr.GetSkillTier(rcInfo.SkillTierID); new_skill_max_value = (ushort)tier.GetValueForTierIndex(prevSkill.step - 1); break; } default: break; } if (rcInfo.HasFlag(SkillRaceClassInfoFlags.AlwaysMaxValue)) skill_value = new_skill_max_value; } } else if (skill_value > prevSkill.value) skill_value = prevSkill.value; if (skill_max_value > new_skill_max_value) skill_max_value = new_skill_max_value; if (skill_value > new_skill_max_value) skill_value = new_skill_max_value; SetSkill(prevSkill.skill, prevSkill.step, skill_value, skill_max_value); } } } // remove dependent spells var spell_bounds = Global.SpellMgr.GetSpellLearnSpellMapBounds(spellId); foreach (var spellNode in spell_bounds) { bool hasOtherSpellTeachingThis = Global.SpellMgr.GetSpellLearnedBySpellMapBounds(spellNode.Spell).Any(learnNode => { if (learnNode.SourceSpell == spellId) return false; if (!learnNode.Active) return false; return HasSpell(learnNode.SourceSpell); }); if (hasOtherSpellTeachingThis) continue; RemoveSpell(spellNode.Spell, disabled); if (spellNode.OverridesSpell != 0) RemoveOverrideSpell(spellNode.OverridesSpell, spellNode.Spell); } // activate lesser rank in spellbook/action bar, and cast it if need bool prev_activate = false; uint prev_id = Global.SpellMgr.GetPrevSpellInChain(spellId); if (prev_id != 0) { // if ranked non-stackable spell: need activate lesser rank and update dendence state // No need to check for spellInfo != NULL here because if cur_active is true, then that means that the spell was already in m_spells, and only valid spells can be pushed there. if (cur_active && spellInfo.IsRanked()) { // need manually update dependence state (learn spell ignore like attempts) var prevSpell = m_spells.LookupByKey(prev_id); if (prevSpell != null) { if (prevSpell.Dependent != cur_dependent) { prevSpell.Dependent = cur_dependent; if (prevSpell.State != PlayerSpellState.New) prevSpell.State = PlayerSpellState.Changed; } // now re-learn if need re-activate if (!prevSpell.Active && learnLowRank) { if (AddSpell(prev_id, true, false, prevSpell.Dependent, prevSpell.Disabled)) { // downgrade spell ranks in spellbook and action bar SendSupercededSpell(spellId, prev_id); prev_activate = true; } } } } } if (traitDefinitionId.HasValue) { var traitDefinition = CliDB.TraitDefinitionStorage.LookupByKey(traitDefinitionId.Value); if (traitDefinition != null) RemoveOverrideSpell(traitDefinition.OverridesSpellID, spellId); } m_overrideSpells.Remove(spellId); if (m_canTitanGrip) { if (spellInfo != null && spellInfo.IsPassive() && spellInfo.HasEffect(SpellEffectName.TitanGrip)) { RemoveAurasDueToSpell(m_titanGripPenaltySpellId); SetCanTitanGrip(false); } } if (CanDualWield()) { if (spellInfo != null && spellInfo.IsPassive() && spellInfo.HasEffect(SpellEffectName.DualWield)) SetCanDualWield(false); } if (WorldConfig.GetBoolValue(WorldCfg.OffhandCheckAtSpellUnlearn)) AutoUnequipOffhandIfNeed(); // remove from spell book if not replaced by lesser rank if (!prev_activate) { UnlearnedSpells unlearnedSpells = new(); unlearnedSpells.SpellID.Add(spellId); unlearnedSpells.SuppressMessaging = suppressMessaging; SendPacket(unlearnedSpells); } } public void SetSpellFavorite(uint spellId, bool favorite) { var spell = m_spells.LookupByKey(spellId); if (spell == null) return; spell.Favorite = favorite; if (spell.State == PlayerSpellState.Unchanged) spell.State = PlayerSpellState.Changed; } bool HandlePassiveSpellLearn(SpellInfo spellInfo) { // note: form passives activated with shapeshift spells be implemented by HandleShapeshiftBoosts instead of spell_learn_spell // talent dependent passives activated at form apply have proper stance data ShapeShiftForm form = GetShapeshiftForm(); bool need_cast = (spellInfo.Stances == 0 || (form != 0 && Convert.ToBoolean(spellInfo.Stances & (1ul << ((int)form - 1)))) || (form == 0 && spellInfo.HasAttribute(SpellAttr2.AllowWhileNotShapeshiftedCasterForm))); // Check EquippedItemClass // passive spells which apply aura and have an item requirement are to be added manually, instead of casted if (spellInfo.EquippedItemClass >= 0) { foreach (var spellEffectInfo in spellInfo.GetEffects()) { if (spellEffectInfo.IsAura()) { if (!HasAura(spellInfo.Id) && HasItemFitToSpellRequirements(spellInfo)) AddAura(spellInfo.Id, this); return false; } } } //Check CasterAuraStates return need_cast && (spellInfo.CasterAuraState == 0 || HasAuraState(spellInfo.CasterAuraState)); } public void AddStoredAuraTeleportLocation(uint spellId) { StoredAuraTeleportLocation storedLocation = new(); storedLocation.Loc = new WorldLocation(this); storedLocation.CurrentState = StoredAuraTeleportLocation.State.Changed; m_storedAuraTeleportLocations[spellId] = storedLocation; } public void RemoveStoredAuraTeleportLocation(uint spellId) { StoredAuraTeleportLocation storedLocation = m_storedAuraTeleportLocations.LookupByKey(spellId); if (storedLocation != null) storedLocation.CurrentState = StoredAuraTeleportLocation.State.Deleted; } public WorldLocation GetStoredAuraTeleportLocation(uint spellId) { StoredAuraTeleportLocation auraLocation = m_storedAuraTeleportLocations.LookupByKey(spellId); if (auraLocation != null) return auraLocation.Loc; return null; } bool AddSpell(uint spellId, bool active, bool learning, bool dependent, bool disabled, bool loading = false, uint fromSkill = 0, bool favorite = false, int? traitDefinitionId = null) { SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(spellId, Difficulty.None); if (spellInfo == null) { // do character spell book cleanup (all characters) if (!IsInWorld && !learning) { Log.outError(LogFilter.Spells, "Player.AddSpell: Spell (ID: {0}) does not exist. deleting for all characters in `character_spell`.", spellId); DeleteSpellFromAllPlayers(spellId); } else Log.outError(LogFilter.Spells, "Player.AddSpell: Spell (ID: {0}) does not exist", spellId); return false; } if (!Global.SpellMgr.IsSpellValid(spellInfo, this, false)) { // do character spell book cleanup (all characters) if (!IsInWorld && !learning) { Log.outError(LogFilter.Spells, "Player.AddSpell: Spell (ID: {0}) is invalid. deleting for all characters in `character_spell`.", spellId); DeleteSpellFromAllPlayers(spellId); } else Log.outError(LogFilter.Spells, "Player.AddSpell: Spell (ID: {0}) is invalid", spellId); return false; } PlayerSpellState state = learning ? PlayerSpellState.New : PlayerSpellState.Unchanged; bool dependent_set = false; bool disabled_case = false; bool superceded_old = false; PlayerSpell spell = m_spells.LookupByKey(spellId); if (spell != null && spell.State == PlayerSpellState.Temporary) RemoveTemporarySpell(spellId); if (spell != null) { uint next_active_spell_id = 0; // fix activate state for non-stackable low rank (and find next spell for !active case) if (spellInfo.IsRanked()) { uint next = Global.SpellMgr.GetNextSpellInChain(spellId); if (next != 0) { if (HasSpell(next)) { // high rank already known so this must !active active = false; next_active_spell_id = next; } } } // not do anything if already known in expected state if (spell.State != PlayerSpellState.Removed && spell.Active == active && spell.Dependent == dependent && spell.Disabled == disabled) { if (!IsInWorld && !learning) spell.State = PlayerSpellState.Unchanged; return false; } // dependent spell known as not dependent, overwrite state if (spell.State != PlayerSpellState.Removed && !spell.Dependent && dependent) { spell.Dependent = dependent; if (spell.State != PlayerSpellState.New) spell.State = PlayerSpellState.Changed; dependent_set = true; } if (spell.TraitDefinitionId != traitDefinitionId) { if (spell.TraitDefinitionId.HasValue) { TraitDefinitionRecord traitDefinition = CliDB.TraitDefinitionStorage.LookupByKey(spell.TraitDefinitionId.Value); if (traitDefinition != null) RemoveOverrideSpell((uint)traitDefinition.OverridesSpellID, spellId); } spell.TraitDefinitionId = traitDefinitionId; } spell.Favorite = favorite; // update active state for known spell if (spell.Active != active && spell.State != PlayerSpellState.Removed && !spell.Disabled) { spell.Active = active; if (!IsInWorld && !learning && !dependent_set) // explicitly load from DB and then exist in it already and set correctly spell.State = PlayerSpellState.Unchanged; else if (spell.State != PlayerSpellState.New) spell.State = PlayerSpellState.Changed; if (active) { if (spellInfo.IsPassive() && HandlePassiveSpellLearn(spellInfo)) CastSpell(this, spellId, true); } else if (IsInWorld) { if (next_active_spell_id != 0) SendSupercededSpell(spellId, next_active_spell_id); else { UnlearnedSpells removedSpells = new(); removedSpells.SpellID.Add(spellId); SendPacket(removedSpells); } } return active; } if (spell.Disabled != disabled && spell.State != PlayerSpellState.Removed) { if (spell.State != PlayerSpellState.New) spell.State = PlayerSpellState.Changed; spell.Disabled = disabled; if (disabled) return false; disabled_case = true; } else { switch (spell.State) { case PlayerSpellState.Unchanged: return false; case PlayerSpellState.Removed: { m_spells.Remove(spellId); state = PlayerSpellState.Changed; break; } default: { // can be in case spell loading but learned at some previous spell loading if (!IsInWorld && !learning && !dependent_set) spell.State = PlayerSpellState.Unchanged; return false; } } } } if (!disabled_case) // skip new spell adding if spell already known (disabled spells case) { // non talent spell: learn low ranks (recursive call) uint prev_spell = Global.SpellMgr.GetPrevSpellInChain(spellId); if (prev_spell != 0) { if (!IsInWorld || disabled) // at spells loading, no output, but allow save AddSpell(prev_spell, active, true, true, disabled, false, fromSkill); else // at normal learning LearnSpell(prev_spell, true, fromSkill); } PlayerSpell newspell = new(); newspell.State = state; newspell.Active = active; newspell.Dependent = dependent; newspell.Disabled = disabled; newspell.Favorite = favorite; if (traitDefinitionId.HasValue) newspell.TraitDefinitionId = traitDefinitionId.Value; // replace spells in action bars and spellbook to bigger rank if only one spell rank must be accessible if (newspell.Active && !newspell.Disabled && spellInfo.IsRanked()) { foreach (var _spell in m_spells) { if (_spell.Value.State == PlayerSpellState.Removed) continue; SpellInfo i_spellInfo = Global.SpellMgr.GetSpellInfo(_spell.Key, Difficulty.None); if (i_spellInfo == null) continue; if (spellInfo.IsDifferentRankOf(i_spellInfo)) { if (_spell.Value.Active) { if (spellInfo.IsHighRankOf(i_spellInfo)) { if (IsInWorld) // not send spell (re-/over-)learn packets at loading SendSupercededSpell(_spell.Key, spellId); // mark old spell as disable (SMSG_SUPERCEDED_SPELL replace it in client by new) _spell.Value.Active = false; if (_spell.Value.State != PlayerSpellState.New) _spell.Value.State = PlayerSpellState.Changed; superceded_old = true; // new spell replace old in action bars and spell book. } else { if (IsInWorld) // not send spell (re-/over-)learn packets at loading SendSupercededSpell(spellId, _spell.Key); // mark new spell as disable (not learned yet for client and will not learned) newspell.Active = false; if (newspell.State != PlayerSpellState.New) newspell.State = PlayerSpellState.Changed; } } } } } m_spells[spellId] = newspell; // return false if spell disabled if (newspell.Disabled) return false; } bool castSpell = false; // cast talents with SPELL_EFFECT_LEARN_SPELL (other dependent spells will learned later as not auto-learned) // note: all spells with SPELL_EFFECT_LEARN_SPELL isn't passive if (!loading && spellInfo.HasAttribute(SpellCustomAttributes.IsTalent) && spellInfo.HasEffect(SpellEffectName.LearnSpell)) { // ignore stance requirement for talent learn spell (stance set for spell only for client spell description show) castSpell = true; } // also cast passive spells (including all talents without SPELL_EFFECT_LEARN_SPELL) with additional checks else if (spellInfo.IsPassive()) castSpell = HandlePassiveSpellLearn(spellInfo); else if (spellInfo.HasEffect(SpellEffectName.SkillStep)) castSpell = true; else if (spellInfo.HasAttribute(SpellAttr1.CastWhenLearned)) castSpell = true; if (castSpell) { CastSpellExtraArgs args = new(TriggerCastFlags.FullMask); if (traitDefinitionId.HasValue) { TraitConfig traitConfig = GetTraitConfig((int)(uint)m_activePlayerData.ActiveCombatTraitConfigID); if (traitConfig != null) { int traitEntryIndex = traitConfig.Entries.FindIndexIf(traitEntry => { return CliDB.TraitNodeEntryStorage.LookupByKey(traitEntry.TraitNodeEntryID)?.TraitDefinitionID == traitDefinitionId; }); int rank = 0; if (traitEntryIndex >= 0) rank = traitConfig.Entries[traitEntryIndex].Rank + traitConfig.Entries[traitEntryIndex].GrantedRanks; if (rank > 0) { var traitDefinitionEffectPoints = TraitMgr.GetTraitDefinitionEffectPointModifiers(traitDefinitionId.Value); if (traitDefinitionEffectPoints != null) { foreach (TraitDefinitionEffectPointsRecord traitDefinitionEffectPoint in traitDefinitionEffectPoints) { if (traitDefinitionEffectPoint.EffectIndex >= spellInfo.GetEffects().Count) continue; float basePoints = Global.DB2Mgr.GetCurveValueAt((uint)traitDefinitionEffectPoint.CurveID, rank); if (traitDefinitionEffectPoint.GetOperationType() == TraitPointsOperationType.Multiply) basePoints *= spellInfo.GetEffect((uint)traitDefinitionEffectPoint.EffectIndex).CalcBaseValue(this, null, 0, -1); args.AddSpellMod(SpellValueMod.BasePoint0 + traitDefinitionEffectPoint.EffectIndex, (int)basePoints); } } } } } CastSpell(this, spellId, args); if (spellInfo.HasEffect(SpellEffectName.SkillStep)) return false; } if (traitDefinitionId.HasValue) { TraitDefinitionRecord traitDefinition = CliDB.TraitDefinitionStorage.LookupByKey(traitDefinitionId.Value); if (traitDefinition != null && traitDefinition.OverridesSpellID != 0) AddOverrideSpell(traitDefinition.OverridesSpellID, spellId); } // update free primary prof.points (if any, can be none in case GM .learn prof. learning) uint freeProfs = GetFreePrimaryProfessionPoints(); if (freeProfs != 0) { if (spellInfo.IsPrimaryProfessionFirstRank()) SetFreePrimaryProfessions(freeProfs - 1); } var skill_bounds = Global.SpellMgr.GetSkillLineAbilityMapBounds(spellId); SpellLearnSkillNode spellLearnSkill = Global.SpellMgr.GetSpellLearnSkill(spellId); if (spellLearnSkill != null) { // add dependent skills if this spell is not learned from adding skill already if ((uint)spellLearnSkill.skill != fromSkill) { ushort skill_value = GetPureSkillValue(spellLearnSkill.skill); ushort skill_max_value = GetPureMaxSkillValue(spellLearnSkill.skill); if (skill_value < spellLearnSkill.value) skill_value = spellLearnSkill.value; ushort new_skill_max_value = spellLearnSkill.maxvalue; if (new_skill_max_value == 0) { var rcInfo = Global.DB2Mgr.GetSkillRaceClassInfo((uint)spellLearnSkill.skill, GetRace(), GetClass()); if (rcInfo != null) { switch (Global.SpellMgr.GetSkillRangeType(rcInfo)) { case SkillRangeType.Language: skill_value = 300; new_skill_max_value = 300; break; case SkillRangeType.Level: new_skill_max_value = GetMaxSkillValueForLevel(); break; case SkillRangeType.Mono: new_skill_max_value = 1; break; case SkillRangeType.Rank: { var tier = Global.ObjectMgr.GetSkillTier(rcInfo.SkillTierID); new_skill_max_value = (ushort)tier.GetValueForTierIndex(spellLearnSkill.step - 1); break; } default: break; } if (rcInfo.HasFlag(SkillRaceClassInfoFlags.AlwaysMaxValue)) skill_value = new_skill_max_value; } } if (skill_max_value < new_skill_max_value) skill_max_value = new_skill_max_value; SetSkill(spellLearnSkill.skill, spellLearnSkill.step, skill_value, skill_max_value); } } else { // not ranked skills foreach (var _spell_idx in skill_bounds) { SkillLineRecord pSkill = CliDB.SkillLineStorage.LookupByKey(_spell_idx.SkillLine); if (pSkill == null) continue; if (_spell_idx.SkillLine == fromSkill) continue; // Runeforging special case if ((_spell_idx.AcquireMethod == SkillLineAbilityAcquireMethod.AutomaticCharLevel && !HasSkill((SkillType)_spell_idx.SkillLine)) || ((_spell_idx.SkillLine == (int)SkillType.Runeforging) && _spell_idx.TrivialSkillLineRankHigh == 0)) { SkillRaceClassInfoRecord rcInfo = Global.DB2Mgr.GetSkillRaceClassInfo(_spell_idx.SkillLine, GetRace(), GetClass()); if (rcInfo != null) LearnDefaultSkill(rcInfo); } } } // learn dependent spells var spell_bounds = Global.SpellMgr.GetSpellLearnSpellMapBounds(spellId); foreach (var spellNode in spell_bounds) { if (!spellNode.AutoLearned) { if (!IsInWorld || !spellNode.Active) // at spells loading, no output, but allow save AddSpell(spellNode.Spell, spellNode.Active, true, true, false); else // at normal learning LearnSpell(spellNode.Spell, true); } if (spellNode.OverridesSpell != 0 && spellNode.Active) AddOverrideSpell(spellNode.OverridesSpell, spellNode.Spell); } if (!GetSession().PlayerLoading()) { // not ranked skills foreach (var _spell_idx in skill_bounds) { UpdateCriteria(CriteriaType.LearnTradeskillSkillLine, _spell_idx.SkillLine); UpdateCriteria(CriteriaType.LearnSpellFromSkillLine, _spell_idx.SkillLine); } UpdateCriteria(CriteriaType.LearnOrKnowSpell, spellId); } // needs to be when spell is already learned, to prevent infinite recursion crashes if (Global.DB2Mgr.GetMount(spellId) != null) GetSession().GetCollectionMgr().AddMount(spellId, MountStatusFlags.None, false, !IsInWorld); // return true (for send learn packet) only if spell active (in case ranked spells) and not replace old spell return active && !disabled && !superceded_old; } public override bool HasSpell(uint spellId) { var spell = m_spells.LookupByKey(spellId); if (spell != null) return spell.State != PlayerSpellState.Removed && !spell.Disabled; return false; } public bool HasActiveSpell(uint spellId) { var spell = m_spells.LookupByKey(spellId); if (spell != null) return spell.State != PlayerSpellState.Removed && spell.Active && !spell.Disabled; return false; } public void AddSpellMod(SpellModifier mod, bool apply) { Log.outDebug(LogFilter.Spells, "Player.AddSpellMod {0}", mod.spellId); // First, manipulate our spellmodifier container if (apply) m_spellMods[(int)mod.op][(int)mod.type].Add(mod); else m_spellMods[(int)mod.op][(int)mod.type].Remove(mod); // Now, send spellmodifier packet switch (mod.type) { case SpellModType.Flat: case SpellModType.Pct: if (!IsLoading()) { ServerOpcodes opcode = (mod.type == SpellModType.Flat ? ServerOpcodes.SetFlatSpellModifier : ServerOpcodes.SetPctSpellModifier); SetSpellModifier packet = new(opcode); // @todo Implement sending of bulk modifiers instead of single SpellModifierInfo spellMod = new(); spellMod.ModIndex = (byte)mod.op; for (int eff = 0; eff < 128; ++eff) { FlagArray128 mask = new(); mask[eff / 32] = 1u << (eff % 32); if ((mod as SpellModifierByClassMask).mask & mask) { SpellModifierData modData = new(); if (mod.type == SpellModType.Flat) { modData.ModifierValue = 0.0f; foreach (SpellModifierByClassMask spell in m_spellMods[(int)mod.op][(int)SpellModType.Flat]) if (spell.mask & mask) modData.ModifierValue += spell.value; } else { modData.ModifierValue = 1.0f; foreach (SpellModifierByClassMask spell in m_spellMods[(int)mod.op][(int)SpellModType.Pct]) if (spell.mask & mask) modData.ModifierValue *= 1.0f + MathFunctions.CalculatePct(1.0f, spell.value); } modData.ClassIndex = (byte)eff; spellMod.ModifierData.Add(modData); } } packet.Modifiers.Add(spellMod); SendPacket(packet); } break; case SpellModType.LabelFlat: if (apply) { AddDynamicUpdateFieldValue(m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.SpellFlatModByLabel), (mod as SpellFlatModifierByLabel).value); } else { int firstIndex = m_activePlayerData.SpellFlatModByLabel.FindIndex((mod as SpellFlatModifierByLabel).value); if (firstIndex >= 0) RemoveDynamicUpdateFieldValue(m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.SpellFlatModByLabel), firstIndex); } break; case SpellModType.LabelPct: if (apply) { AddDynamicUpdateFieldValue(m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.SpellPctModByLabel), (mod as SpellPctModifierByLabel).value); } else { int firstIndex = m_activePlayerData.SpellPctModByLabel.FindIndex((mod as SpellPctModifierByLabel).value); if (firstIndex >= 0) RemoveDynamicUpdateFieldValue(m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.SpellPctModByLabel), firstIndex); } break; } } public void ApplySpellMod(SpellInfo spellInfo, SpellModOp op, ref int basevalue, Spell spell = null) { float totalmul = 1.0f; int totalflat = 0; GetSpellModValues(spellInfo, op, spell, basevalue, ref totalflat, ref totalmul); basevalue = (int)(((float)basevalue + totalflat) * totalmul); } public void ApplySpellMod(SpellInfo spellInfo, SpellModOp op, ref uint basevalue, Spell spell = null) { float totalmul = 1.0f; int totalflat = 0; GetSpellModValues(spellInfo, op, spell, basevalue, ref totalflat, ref totalmul); basevalue = (uint)(((float)basevalue + totalflat) * totalmul); } public void ApplySpellMod(SpellInfo spellInfo, SpellModOp op, ref float basevalue, Spell spell = null) { float totalmul = 1.0f; int totalflat = 0; GetSpellModValues(spellInfo, op, spell, basevalue, ref totalflat, ref totalmul); basevalue = (float)(basevalue + totalflat) * totalmul; } public void ApplySpellMod(SpellInfo spellInfo, SpellModOp op, ref double basevalue, Spell spell = null) { float totalmul = 1.0f; int totalflat = 0; GetSpellModValues(spellInfo, op, spell, basevalue, ref totalflat, ref totalmul); basevalue = (double)(basevalue + totalflat) * totalmul; } public void GetSpellModValues(SpellInfo spellInfo, SpellModOp op, Spell spell, T baseValue, ref int flat, ref float pct) where T : IComparable { flat = 0; pct = 1.0f; if (!spellInfo.IsAffectedBySpellMods()) return; // Drop charges for triggering spells instead of triggered ones if (m_spellModTakingSpell != null) spell = m_spellModTakingSpell; switch (op) { // special case, if a mod makes spell instant, only consume that mod case SpellModOp.ChangeCastTime: { SpellModifier modInstantSpell = null; foreach (SpellModifierByClassMask mod in m_spellMods[(int)op][(int)SpellModType.Pct]) { if (IsAffectedBySpellmod(spellInfo, mod, spell) == 0) continue; if (baseValue.CompareTo(10000) < 0 && mod.value <= -100) { modInstantSpell = mod; break; } } if (modInstantSpell == null) { foreach (SpellPctModifierByLabel mod in m_spellMods[(int)op][(int)SpellModType.LabelPct]) { if (IsAffectedBySpellmod(spellInfo, mod, spell) == 0) continue; if (baseValue.CompareTo(10000) < 0 && mod.value.ModifierValue <= -1.0f) { modInstantSpell = mod; break; } } } if (modInstantSpell != null) { ApplyModToSpell(modInstantSpell, spell); pct = 0.0f; return; } break; } // special case if two mods apply 100% critical chance, only consume one case SpellModOp.CritChance: { SpellModifier modCritical = null; foreach (SpellModifierByClassMask mod in m_spellMods[(int)op][(int)SpellModType.Flat]) { if (IsAffectedBySpellmod(spellInfo, mod, spell) == 0) continue; if (mod.value >= 100) { modCritical = mod; break; } } if (modCritical == null) { foreach (SpellFlatModifierByLabel mod in m_spellMods[(int)op][(int)SpellModType.LabelFlat]) { if (IsAffectedBySpellmod(spellInfo, mod, spell) == 0) continue; if (mod.value.ModifierValue >= 100) { modCritical = mod; break; } } } if (modCritical != null) { ApplyModToSpell(modCritical, spell); flat = 100; return; } break; } default: break; } foreach (SpellModifierByClassMask mod in m_spellMods[(int)op][(int)SpellModType.Flat]) { int applyCount = IsAffectedBySpellmod(spellInfo, mod, spell); if (applyCount == 0) continue; int value = mod.value; if (value == 0) continue; flat += value * applyCount; ApplyModToSpell(mod, spell); } foreach (SpellFlatModifierByLabel mod in m_spellMods[(int)op][(int)SpellModType.LabelFlat]) { int applyCount = IsAffectedBySpellmod(spellInfo, mod, spell); if (applyCount == 0) continue; int value = mod.value.ModifierValue; if (value == 0) continue; flat += value * applyCount; ApplyModToSpell(mod, spell); } foreach (SpellModifierByClassMask mod in m_spellMods[(int)op][(int)SpellModType.Pct]) { int applyCount = IsAffectedBySpellmod(spellInfo, mod, spell); if (applyCount == 0) continue; // skip percent mods for null basevalue (most important for spell mods with charges) if (baseValue + (dynamic)flat == 0) continue; int value = mod.value; if (value == 0) continue; // special case (skip > 10sec spell casts for instant cast setting) if (op == SpellModOp.ChangeCastTime) { if (baseValue.CompareTo(10000) > 0 && value <= -100) continue; } pct *= MathF.Pow(1.0f + MathFunctions.CalculatePct(1.0f, value), applyCount); ApplyModToSpell(mod, spell); } foreach (SpellPctModifierByLabel mod in m_spellMods[(int)op][(int)SpellModType.LabelPct]) { int applyCount = IsAffectedBySpellmod(spellInfo, mod, spell); if (applyCount == 0) continue; // skip percent mods for null basevalue (most important for spell mods with charges) if (baseValue + (dynamic)flat == 0) continue; float value = mod.value.ModifierValue; if (value == 1.0f) continue; // special case (skip > 10sec spell casts for instant cast setting) if (op == SpellModOp.ChangeCastTime) { if (baseValue.CompareTo(10000) > 0 && value <= -1.0f) continue; } pct *= MathF.Pow(value, applyCount); ApplyModToSpell(mod, spell); } } int IsAffectedBySpellmod(SpellInfo spellInfo, SpellModifier mod, Spell spell) { if (mod == null || spellInfo == null) return 0; // First time this aura applies a mod to us and is out of charges if (spell != null && mod.ownerAura.IsUsingCharges() && mod.ownerAura.GetCharges() == 0 && !spell.m_appliedMods.Contains(mod.ownerAura)) return 0; switch (mod.op) { case SpellModOp.Duration: // +duration to infinite duration spells making them limited if (spellInfo.GetDuration() == -1) return 0; break; case SpellModOp.CritChance: // mod crit to spells that can't crit if (!spellInfo.HasAttribute(SpellCustomAttributes.CanCrit)) return 0; break; case SpellModOp.PointsIndex0: // check if spell has any effect at that index case SpellModOp.Points: if (spellInfo.GetEffects().Count <= 0) return 0; break; case SpellModOp.PointsIndex1: // check if spell has any effect at that index if (spellInfo.GetEffects().Count <= 1) return 0; break; case SpellModOp.PointsIndex2: // check if spell has any effect at that index if (spellInfo.GetEffects().Count <= 2) return 0; break; case SpellModOp.PointsIndex3: // check if spell has any effect at that index if (spellInfo.GetEffects().Count <= 3) return 0; break; case SpellModOp.PointsIndex4: // check if spell has any effect at that index if (spellInfo.GetEffects().Count <= 4) return 0; break; default: break; } return spellInfo.IsAffectedBySpellMod(mod); } public void SetSpellModTakingSpell(Spell spell, bool apply) { if (apply && m_spellModTakingSpell != null) return; if (!apply && (m_spellModTakingSpell == null || m_spellModTakingSpell != spell)) return; m_spellModTakingSpell = apply ? spell : null; } void SendSpellModifiers() { SetSpellModifier flatMods = new(ServerOpcodes.SetFlatSpellModifier); SetSpellModifier pctMods = new(ServerOpcodes.SetPctSpellModifier); for (var i = 0; i < (int)SpellModOp.Max; ++i) { SpellModifierInfo flatMod = new(); SpellModifierInfo pctMod = new(); flatMod.ModIndex = pctMod.ModIndex = (byte)i; for (byte j = 0; j < 128; ++j) { FlagArray128 mask = new(); mask[j / 32] = 1u << (j % 32); SpellModifierData flatData; SpellModifierData pctData; flatData.ClassIndex = j; flatData.ModifierValue = 0.0f; pctData.ClassIndex = j; pctData.ModifierValue = 1.0f; foreach (SpellModifierByClassMask mod in m_spellMods[i][(int)SpellModType.Flat]) { if (mod.mask & mask) flatData.ModifierValue += mod.value; } foreach (SpellModifierByClassMask mod in m_spellMods[i][(int)SpellModType.Pct]) { if (mod.mask & mask) pctData.ModifierValue *= 1.0f + MathFunctions.CalculatePct(1.0f, mod.value); } flatMod.ModifierData.Add(flatData); pctMod.ModifierData.Add(pctData); } flatMod.ModifierData.RemoveAll(mod => MathFunctions.fuzzyEq(mod.ModifierValue, 0.0f)); pctMod.ModifierData.RemoveAll(mod => MathFunctions.fuzzyEq(mod.ModifierValue, 1.0f)); flatMods.Modifiers.Add(flatMod); pctMods.Modifiers.Add(pctMod); } if (!flatMods.Modifiers.Empty()) SendPacket(flatMods); if (!pctMods.Modifiers.Empty()) SendPacket(pctMods); } void SendSupercededSpell(uint oldSpell, uint newSpell) { SupercededSpells supercededSpells = new(); LearnedSpellInfo learnedSpellInfo = new(); learnedSpellInfo.SpellID = newSpell; learnedSpellInfo.Superceded = (int)oldSpell; supercededSpells.ClientLearnedSpellData.Add(learnedSpellInfo); SendPacket(supercededSpells); } public void UpdateEquipSpellsAtFormChange() { for (byte i = 0; i < InventorySlots.ReagentBagEnd; ++i) { if (m_items[i] != null && !m_items[i].IsBroken() && CanUseAttackType(GetAttackBySlot(i, m_items[i].GetTemplate().GetInventoryType()))) { ApplyItemEquipSpell(m_items[i], false, true); // remove spells that not fit to form ApplyItemEquipSpell(m_items[i], true, true); // add spells that fit form but not active } } UpdateItemSetAuras(true); } void UpdateItemSetAuras(bool formChange = false) { // item set bonuses not dependent from item broken state for (int setindex = 0; setindex < ItemSetEff.Count; ++setindex) { ItemSetEffect eff = ItemSetEff[setindex]; if (eff == null) continue; foreach (ItemSetSpellRecord itemSetSpell in eff.SetBonuses) { SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(itemSetSpell.SpellID, Difficulty.None); if (itemSetSpell.ChrSpecID != 0 && (ChrSpecialization)itemSetSpell.ChrSpecID != GetPrimarySpecialization()) ApplyEquipSpell(spellInfo, null, false, false); // item set aura is not for current spec else { ApplyEquipSpell(spellInfo, null, false, formChange); // remove spells that not fit to form - removal is skipped if shapeshift condition is satisfied ApplyEquipSpell(spellInfo, null, true, formChange); // add spells that fit form but not active } } } } public int GetSpellPenetrationItemMod() { return m_spellPenetrationItemMod; } public void RemoveArenaSpellCooldowns(bool removeActivePetCooldowns) { // remove cooldowns on spells that have < 10 min CD GetSpellHistory().ResetCooldowns(cooldownEntry => { SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(cooldownEntry.SpellId, Difficulty.None); TimeSpan cooldown = TimeSpan.Zero; TimeSpan categoryCooldown = TimeSpan.Zero; uint categoryId = 0; SpellHistory.GetCooldownDurations(spellInfo, cooldownEntry.ItemId, ref cooldown, ref categoryId, ref categoryCooldown); return cooldown < TimeSpan.FromMinutes(10) && categoryCooldown < TimeSpan.FromMinutes(10) && !spellInfo.HasAttribute(SpellAttr6.DoNotResetCooldownInArena); }, true); // pet cooldowns if (removeActivePetCooldowns) { Pet pet = GetPet(); if (pet != null) pet.GetSpellHistory().ResetAllCooldowns(); } } /**********************************/ /*************Runes****************/ /**********************************/ public void SetRuneCooldown(byte index, uint cooldown) { m_runes.Cooldown[index] = cooldown; m_runes.SetRuneState(index, (cooldown == 0)); int activeRunes = m_runes.Cooldown.Count(p => p == 0); if (activeRunes != GetPower(PowerType.Runes)) SetPower(PowerType.Runes, activeRunes); } public byte GetRunesState() { return (byte)(m_runes.RuneState & ((1 << GetMaxPower(PowerType.Runes)) - 1)); } public uint GetRuneBaseCooldown() { float cooldown = RuneCooldowns.Base; var regenAura = GetAuraEffectsByType(AuraType.ModPowerRegenPercent); foreach (var i in regenAura) if (i.GetMiscValue() == (int)PowerType.Runes) cooldown *= 1.0f - i.GetAmount() / 100.0f; // Runes cooldown are now affected by player's haste from equipment ... float hastePct = GetRatingBonusValue(CombatRating.HasteMelee); // ... and some auras. hastePct += GetTotalAuraModifier(AuraType.ModMeleeHaste); hastePct += GetTotalAuraModifier(AuraType.ModMeleeHaste2); hastePct += GetTotalAuraModifier(AuraType.ModMeleeHaste3); cooldown *= 1.0f - (hastePct / 100.0f); return (uint)cooldown; } public void ResyncRunes() { int maxRunes = GetMaxPower(PowerType.Runes); ResyncRunes data = new(); data.Runes.Start = (byte)((1 << maxRunes) - 1); data.Runes.Count = GetRunesState(); float baseCd = GetRuneBaseCooldown(); for (byte i = 0; i < maxRunes; ++i) data.Runes.Cooldowns.Add((byte)((baseCd - GetRuneCooldown(i)) / baseCd * 255)); SendPacket(data); } public void InitRunes() { if (GetClass() != Class.Deathknight) return; uint runeIndex = GetPowerIndex(PowerType.Runes); if (runeIndex == (int)PowerType.Max) return; m_runes = new Runes(); m_runes.RuneState = 0; for (byte i = 0; i < PlayerConst.MaxRunes; ++i) SetRuneCooldown(i, 0); // reset cooldowns // set a base regen timer equal to 10 sec SetUpdateFieldValue(ref m_values.ModifyValue(m_unitData).ModifyValue(m_unitData.PowerRegenFlatModifier, (int)runeIndex), 0.0f); SetUpdateFieldValue(ref m_values.ModifyValue(m_unitData).ModifyValue(m_unitData.PowerRegenInterruptedFlatModifier, (int)runeIndex), 0.0f); } public void UpdateAllRunesRegen() { if (GetClass() != Class.Deathknight) return; uint runeIndex = GetPowerIndex(PowerType.Runes); if (runeIndex == (int)PowerType.Max) return; PowerTypeRecord runeEntry = Global.DB2Mgr.GetPowerTypeEntry(PowerType.Runes); uint cooldown = GetRuneBaseCooldown(); SetUpdateFieldValue(ref m_values.ModifyValue(m_unitData).ModifyValue(m_unitData.PowerRegenFlatModifier, (int)runeIndex), (float)(1 * Time.InMilliseconds) / cooldown - runeEntry.RegenPeace); SetUpdateFieldValue(ref m_values.ModifyValue(m_unitData).ModifyValue(m_unitData.PowerRegenInterruptedFlatModifier, (int)runeIndex), (float)(1 * Time.InMilliseconds) / cooldown - runeEntry.RegenCombat); } public uint GetRuneCooldown(byte index) { return m_runes.Cooldown[index]; } public bool CanNoReagentCast(SpellInfo spellInfo) { // don't take reagents for spells with SPELL_ATTR5_NO_REAGENT_WHILE_PREP if (spellInfo.HasAttribute(SpellAttr5.NoReagentCostWithAura) && HasUnitFlag(UnitFlags.Preparation)) return true; // Check no reagent use mask FlagArray128 noReagentMask = new(); noReagentMask[0] = m_activePlayerData.NoReagentCostMask[0]; noReagentMask[1] = m_activePlayerData.NoReagentCostMask[1]; noReagentMask[2] = m_activePlayerData.NoReagentCostMask[2]; noReagentMask[3] = m_activePlayerData.NoReagentCostMask[3]; if (spellInfo.SpellFamilyFlags & noReagentMask) return true; return false; } public void SetNoRegentCostMask(FlagArray128 mask) { for (byte i = 0; i < 4; ++i) SetUpdateFieldValue(ref m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.NoReagentCostMask, i), mask[i]); } public void CastItemCombatSpell(DamageInfo damageInfo) { Unit target = damageInfo.GetVictim(); if (target == null || !target.IsAlive() || target == this) return; for (byte i = EquipmentSlot.Start; i < EquipmentSlot.End; ++i) { // If usable, try to cast item spell Item item = GetItemByPos(InventorySlots.Bag0, i); if (item != null) { if (!item.IsBroken() && CanUseAttackType(damageInfo.GetAttackType())) { ItemTemplate proto = item.GetTemplate(); if (proto != null) { // Additional check for weapons if (proto.GetClass() == ItemClass.Weapon) { // offhand item cannot proc from main hand hit etc byte slot; switch (damageInfo.GetAttackType()) { case WeaponAttackType.BaseAttack: case WeaponAttackType.RangedAttack: slot = EquipmentSlot.MainHand; break; case WeaponAttackType.OffAttack: slot = EquipmentSlot.OffHand; break; default: slot = EquipmentSlot.End; break; } if (slot != i) continue; // Check if item is useable (forms or disarm) if (damageInfo.GetAttackType() == WeaponAttackType.BaseAttack) if (!IsUseEquipedWeapon(true) && !IsInFeralForm()) continue; } CastItemCombatSpell(damageInfo, item, proto); } } } } } public void CastItemCombatSpell(DamageInfo damageInfo, Item item, ItemTemplate proto) { // Can do effect if any damage done to target // for done procs allow normal + critical + absorbs by default bool canTrigger = damageInfo.GetHitMask().HasAnyFlag(ProcFlagsHit.Normal | ProcFlagsHit.Critical | ProcFlagsHit.Absorb); if (canTrigger) { if (!item.GetTemplate().HasFlag(ItemFlags.Legacy)) { foreach (ItemEffectRecord effectData in item.GetEffects()) { // wrong triggering type if (effectData.TriggerType != ItemSpelltriggerType.OnProc) continue; SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo((uint)effectData.SpellID, Difficulty.None); if (spellInfo == null) { Log.outError(LogFilter.Player, "WORLD: unknown Item spellid {0}", effectData.SpellID); continue; } float chance = spellInfo.ProcChance; if (proto.SpellPPMRate != 0) { uint WeaponSpeed = GetBaseAttackTime(damageInfo.GetAttackType()); chance = GetPPMProcChance(WeaponSpeed, proto.SpellPPMRate, spellInfo); } else if (chance > 100.0f) chance = GetWeaponProcChance(); if (RandomHelper.randChance(chance) && Global.ScriptMgr.OnCastItemCombatSpell(this, damageInfo.GetVictim(), spellInfo, item)) CastSpell(damageInfo.GetVictim(), spellInfo.Id, item); } } } // item combat enchantments for (byte e_slot = 0; e_slot < (byte)EnchantmentSlot.Max; ++e_slot) { uint enchant_id = item.GetEnchantmentId((EnchantmentSlot)e_slot); SpellItemEnchantmentRecord pEnchant = CliDB.SpellItemEnchantmentStorage.LookupByKey(enchant_id); if (pEnchant == null) continue; for (byte s = 0; s < ItemConst.MaxItemEnchantmentEffects; ++s) { if (pEnchant.Effect[s] != ItemEnchantmentType.CombatSpell) continue; SpellEnchantProcEntry entry = Global.SpellMgr.GetSpellEnchantProcEvent(enchant_id); if (entry != null && entry.HitMask != 0) { // Check hit/crit/dodge/parry requirement if ((entry.HitMask & (uint)damageInfo.GetHitMask()) == 0) continue; } else { // for done procs allow normal + critical + absorbs by default if (!canTrigger) continue; } // check if enchant procs only on white hits if (entry != null && entry.AttributesMask.HasAnyFlag(EnchantProcAttributes.WhiteHit) && damageInfo.GetSpellInfo() != null) continue; SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(pEnchant.EffectArg[s], Difficulty.None); if (spellInfo == null) { Log.outError(LogFilter.Player, "Player.CastItemCombatSpell(GUID: {0}, name: {1}, enchant: {2}): unknown spell {3} is casted, ignoring...", GetGUID().ToString(), GetName(), enchant_id, pEnchant.EffectArg[s]); continue; } float chance = pEnchant.EffectPointsMin[s] != 0 ? pEnchant.EffectPointsMin[s] : GetWeaponProcChance(); if (entry != null) { if (entry.ProcsPerMinute != 0) chance = GetPPMProcChance(proto.GetDelay(), entry.ProcsPerMinute, spellInfo); else if (entry.Chance != 0) chance = entry.Chance; } // Apply spell mods ApplySpellMod(spellInfo, SpellModOp.ProcChance, ref chance); // Shiv has 100% chance to apply the poison if (FindCurrentSpellBySpellId(5938) != null && e_slot == (byte)EnchantmentSlot.Temp) chance = 100.0f; if (RandomHelper.randChance(chance)) { if (spellInfo.IsPositive()) CastSpell(this, spellInfo.Id, item); else CastSpell(damageInfo.GetVictim(), spellInfo.Id, item); } if (RandomHelper.randChance(chance)) { Unit target = spellInfo.IsPositive() ? this : damageInfo.GetVictim(); CastSpellExtraArgs args = new(item); // reduce effect values if enchant is limited if (entry != null && entry.AttributesMask.HasAnyFlag(EnchantProcAttributes.Limit60) && target.GetLevelForTarget(this) > 60) { int lvlDifference = (int)target.GetLevelForTarget(this) - 60; int lvlPenaltyFactor = 4; // 4% lost effectiveness per level int effectPct = Math.Max(0, 100 - (lvlDifference * lvlPenaltyFactor)); foreach (var spellEffectInfo in spellInfo.GetEffects()) { if (spellEffectInfo.IsEffect()) args.AddSpellMod(SpellValueMod.BasePoint0 + (int)spellEffectInfo.EffectIndex, MathFunctions.CalculatePct(spellEffectInfo.CalcValue(this), effectPct)); } } CastSpell(target, spellInfo.Id, args); } } } } float GetWeaponProcChance() { // normalized proc chance for weapon attack speed // (odd formula...) if (IsAttackReady(WeaponAttackType.BaseAttack)) return (GetBaseAttackTime(WeaponAttackType.BaseAttack) * 1.8f / 1000.0f); else if (HaveOffhandWeapon() && IsAttackReady(WeaponAttackType.OffAttack)) return (GetBaseAttackTime(WeaponAttackType.OffAttack) * 1.6f / 1000.0f); return 0; } public void ResetSpells(bool myClassOnly = false) { // not need after this call if (HasAtLoginFlag(AtLoginFlags.ResetSpells)) RemoveAtLoginFlag(AtLoginFlags.ResetSpells, true); // make full copy of map (spells removed and marked as deleted at another spell remove // and we can't use original map for safe iterative with visit each spell at loop end var smap = GetSpellMap(); uint family; if (myClassOnly) { ChrClassesRecord clsEntry = CliDB.ChrClassesStorage.LookupByKey(GetClass()); if (clsEntry == null) return; family = clsEntry.SpellClassSet; foreach (var spellId in smap.Keys) { SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(spellId, Difficulty.None); if (spellInfo == null) continue; // skip server-side/triggered spells if (spellInfo.SpellLevel == 0) continue; // skip wrong class/race skills if (!IsSpellFitByClassAndRace(spellInfo.Id)) continue; // skip other spell families if ((uint)spellInfo.SpellFamilyName != family) continue; // skip broken spells if (!Global.SpellMgr.IsSpellValid(spellInfo, this, false)) continue; } } else foreach (var spellId in smap.Keys) RemoveSpell(spellId, false, false); // only iter.first can be accessed, object by iter.second can be deleted already LearnDefaultSkills(); LearnCustomSpells(); LearnQuestRewardedSpells(); } public void SetPetSpellPower(uint spellPower) { SetUpdateFieldValue(m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.PetSpellPower), spellPower); } public void SetSkillLineId(uint pos, ushort skillLineId) { SkillInfo skillInfo = m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.Skill); SetUpdateFieldValue(ref skillInfo.ModifyValue(skillInfo.SkillLineID, (int)pos), skillLineId); } public void SetSkillStep(uint pos, ushort step) { SkillInfo skillInfo = m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.Skill); SetUpdateFieldValue(ref skillInfo.ModifyValue(skillInfo.SkillStep, (int)pos), step); } public void SetSkillRank(uint pos, ushort rank) { SkillInfo skillInfo = m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.Skill); SetUpdateFieldValue(ref skillInfo.ModifyValue(skillInfo.SkillRank, (int)pos), rank); } public void SetSkillStartingRank(uint pos, ushort starting) { SkillInfo skillInfo = m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.Skill); SetUpdateFieldValue(ref skillInfo.ModifyValue(skillInfo.SkillStartingRank, (int)pos), starting); } public void SetSkillMaxRank(uint pos, ushort max) { SkillInfo skillInfo = m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.Skill); SetUpdateFieldValue(ref skillInfo.ModifyValue(skillInfo.SkillMaxRank, (int)pos), max); } public void SetSkillTempBonus(uint pos, ushort bonus) { SkillInfo skillInfo = m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.Skill); SetUpdateFieldValue(ref skillInfo.ModifyValue(skillInfo.SkillTempBonus, (int)pos), bonus); } public void SetSkillPermBonus(uint pos, ushort bonus) { SkillInfo skillInfo = m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.Skill); SetUpdateFieldValue(ref skillInfo.ModifyValue(skillInfo.SkillPermBonus, (int)pos), bonus); } public void RequestSpellCast(SpellCastRequest castRequest) { // We are overriding an already existing spell cast request so inform the client that the old cast is being replaced if (_pendingSpellCastRequest != null) CancelPendingCastRequest(); _pendingSpellCastRequest = castRequest; // If we can process the cast request right now, do it. if (CanExecutePendingSpellCastRequest()) ExecutePendingSpellCastRequest(); } public void CancelPendingCastRequest() { if (_pendingSpellCastRequest == null) return; // We have to inform the client that the cast has been canceled. Otherwise the cast button will remain highlightened CastFailed castFailed = new(); castFailed.CastID = _pendingSpellCastRequest.CastRequest.CastID; castFailed.SpellID = (int)_pendingSpellCastRequest.CastRequest.SpellID; castFailed.Reason = SpellCastResult.DontReport; SendPacket(castFailed); _pendingSpellCastRequest = null; } // A spell can be queued up within 400 milliseconds before global cooldown expires or the cast finishes static TimeSpan SPELL_QUEUE_TIME_WINDOW = TimeSpan.FromMilliseconds(400); public bool CanRequestSpellCast(SpellInfo spellInfo, Unit castingUnit) { if (castingUnit.GetSpellHistory().GetRemainingGlobalCooldown(spellInfo) > SPELL_QUEUE_TIME_WINDOW) return false; foreach (CurrentSpellTypes spellSlot in new[] { CurrentSpellTypes.Melee, CurrentSpellTypes.Generic }) { Spell spell = GetCurrentSpell(spellSlot); if (spell != null && TimeSpan.FromMilliseconds(spell.GetRemainingCastTime()) > SPELL_QUEUE_TIME_WINDOW) return false; } return true; } void ExecutePendingSpellCastRequest() { if (_pendingSpellCastRequest == null) return; TriggerCastFlags triggerFlag = TriggerCastFlags.None; Unit castingUnit = _pendingSpellCastRequest.CastingUnitGUID == GetGUID() ? this : Global.ObjAccessor.GetUnit(this, _pendingSpellCastRequest.CastingUnitGUID); // client provided targets SpellCastTargets targets = new(castingUnit, _pendingSpellCastRequest.CastRequest); // The spell cast has been requested by using an item. Handle the cast accordingly. if (_pendingSpellCastRequest.ItemData != null) { if (ProcessItemCast(_pendingSpellCastRequest, targets)) _pendingSpellCastRequest = null; else CancelPendingCastRequest(); return; } // check known spell or raid marker spell (which not requires player to know it) SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(_pendingSpellCastRequest.CastRequest.SpellID, GetMap().GetDifficultyID()); Player plrCaster = castingUnit.ToPlayer(); if (plrCaster != null && !plrCaster.HasActiveSpell(spellInfo.Id) && !spellInfo.HasAttribute(SpellAttr8.SkipIsKnownCheck)) { bool allow = false; // allow casting of unknown spells for special lock cases GameObject go = targets.GetGOTarget(); if (go != null && go.GetSpellForLock(plrCaster) == spellInfo) allow = true; // allow casting of spells triggered by clientside periodic trigger auras if (castingUnit.HasAuraTypeWithTriggerSpell(AuraType.PeriodicTriggerSpellFromClient, spellInfo.Id)) { allow = true; triggerFlag = TriggerCastFlags.FullMask; } if (!allow) { CancelPendingCastRequest(); return; } } // Check possible spell cast overrides var (overrideSpellInfo, overrideTriggerFlag) = castingUnit.GetCastSpellInfo(spellInfo); spellInfo = overrideSpellInfo; triggerFlag |= overrideTriggerFlag; if (spellInfo.IsPassive()) { CancelPendingCastRequest(); return; } // can't use our own spells when we're in possession of another unit if (IsPossessing()) { CancelPendingCastRequest(); return; } // Client is resending autoshot cast opcode when other spell is cast during shoot rotation // Skip it to prevent "interrupt" message // Also check targets! target may have changed and we need to interrupt current spell if (spellInfo.IsAutoRepeatRangedSpell()) { Spell currentSpell = castingUnit.GetCurrentSpell(CurrentSpellTypes.AutoRepeat); if (currentSpell != null) { if (currentSpell.m_spellInfo == spellInfo && currentSpell.m_targets.GetUnitTargetGUID() == targets.GetUnitTargetGUID()) { CancelPendingCastRequest(); return; } } } // auto-selection buff level base at target level (in spellInfo) if (targets.GetUnitTarget() != null) { SpellInfo actualSpellInfo = spellInfo.GetAuraRankForLevel(targets.GetUnitTarget().GetLevelForTarget(this)); // if rank not found then function return NULL but in explicit cast case original spell can be cast and later failed with appropriate error message if (actualSpellInfo != null) spellInfo = actualSpellInfo; } Spell spell = new Spell(castingUnit, spellInfo, triggerFlag); SpellPrepare spellPrepare = new(); spellPrepare.ClientCastID = _pendingSpellCastRequest.CastRequest.CastID; spellPrepare.ServerCastID = spell.m_castId; SendPacket(spellPrepare); spell.m_fromClient = true; spell.m_misc.Data0 = _pendingSpellCastRequest.CastRequest.Misc[0]; spell.m_misc.Data1 = _pendingSpellCastRequest.CastRequest.Misc[1]; spell.Prepare(targets); _pendingSpellCastRequest = null; } bool ProcessItemCast(SpellCastRequest castRequest, SpellCastTargets targets) { Item item = GetUseableItemByPos(castRequest.ItemData.PackSlot, castRequest.ItemData.Slot); if (item == null) { SendEquipError(InventoryResult.ItemNotFound); return false; } if (item.GetGUID() != castRequest.ItemData.CastItem) { SendEquipError(InventoryResult.ItemNotFound); return false; } ItemTemplate proto = item.GetTemplate(); if (proto == null) { SendEquipError(InventoryResult.ItemNotFound, item); return false; } // some item classes can be used only in equipped state if (proto.GetInventoryType() != InventoryType.NonEquip && !item.IsEquipped()) { SendEquipError(InventoryResult.ItemNotFound, item); return false; } InventoryResult msg = CanUseItem(item); if (msg != InventoryResult.Ok) { SendEquipError(msg, item, null); return false; } // only allow conjured consumable, bandage, poisons (all should have the 2^21 item flag set in DB) if (proto.GetClass() == ItemClass.Consumable && !proto.HasFlag(ItemFlags.IgnoreDefaultArenaRestrictions) && InArena()) { SendEquipError(InventoryResult.NotDuringArenaMatch, item); return false; } // don't allow items banned in arena if (proto.HasFlag(ItemFlags.NotUseableInArena) && InArena()) { SendEquipError(InventoryResult.NotDuringArenaMatch, item); return false; } if (IsInCombat()) { foreach (var effect in item.GetEffects()) { SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo((uint)effect.SpellID, GetMap().GetDifficultyID()); if (spellInfo != null) { if (!spellInfo.CanBeUsedInCombat(this)) { SendEquipError(InventoryResult.NotInCombat, item); return false; } } } } // check also BIND_ON_ACQUIRE and BIND_QUEST for .additem or .additemset case by GM (not binded at adding to inventory) if (item.GetBonding() == ItemBondingType.OnUse || item.GetBonding() == ItemBondingType.OnAcquire || item.GetBonding() == ItemBondingType.Quest) { if (!item.IsSoulBound()) { item.SetState(ItemUpdateState.Changed, this); item.SetBinding(true); GetSession().GetCollectionMgr().AddItemAppearance(item); } } RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.ItemUse); // Note: If script stop casting it must send appropriate data to client to prevent stuck item in gray state. if (!Global.ScriptMgr.OnItemUse(this, item, targets, castRequest.CastRequest.CastID)) { // no script or script not process request by self CastItemUseSpell(item, targets, castRequest.CastRequest.CastID, castRequest.CastRequest.Misc); } return true; } bool CanExecutePendingSpellCastRequest() { if (_pendingSpellCastRequest == null) return false; Unit castingUnit = _pendingSpellCastRequest.CastingUnitGUID == GetGUID() ? this : Global.ObjAccessor.GetUnit(this, _pendingSpellCastRequest.CastingUnitGUID); if (castingUnit == null || !castingUnit.IsInWorld || (castingUnit != this && GetUnitBeingMoved() != castingUnit)) { // If the casting unit is no longer available, just cancel the entire spell cast request and be done with it CancelPendingCastRequest(); return false; } // Generic and melee spells have to wait, channeled spells can be processed immediately. if (castingUnit.GetCurrentSpell(CurrentSpellTypes.Channeled) == null && castingUnit.HasUnitState(UnitState.Casting)) return false; // Waiting for the global cooldown to expire before attempting to execute the cast request if (castingUnit.GetSpellHistory().GetRemainingGlobalCooldown(Global.SpellMgr.GetSpellInfo(_pendingSpellCastRequest.CastRequest.SpellID, GetMap().GetDifficultyID())) > TimeSpan.Zero) return false; return true; } } public class PlayerSpell { public PlayerSpellState State; public bool Active; public bool Dependent; public bool Disabled; public bool Favorite; public int? TraitDefinitionId; } }