Core/Skills: ported skill refactors from master branch
* implement automatic skill learning from dbc data which
This commit is contained in:
1 parent
d4b18aad8f
commit
a9579cb33b
17 files changed
+284
-210
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS `playercreateinfo_skills`;
|
||||||
@@ -70,6 +70,13 @@ namespace Trinity
|
|||||||
|
|
||||||
namespace Containers
|
namespace Containers
|
||||||
{
|
{
|
||||||
|
template <typename M>
|
||||||
|
concept Map = requires (M)
|
||||||
|
{
|
||||||
|
typename M::key_type;
|
||||||
|
typename M::mapped_type;
|
||||||
|
};
|
||||||
|
|
||||||
// replace with std::size in C++17
|
// replace with std::size in C++17
|
||||||
template<class C>
|
template<class C>
|
||||||
constexpr inline std::size_t Size(C const& container)
|
constexpr inline std::size_t Size(C const& container)
|
||||||
@@ -253,15 +260,28 @@ namespace Trinity
|
|||||||
/**
|
/**
|
||||||
* Returns a pointer to mapped value (or the value itself if map stores pointers)
|
* Returns a pointer to mapped value (or the value itself if map stores pointers)
|
||||||
*/
|
*/
|
||||||
template<class M>
|
template <Map M>
|
||||||
inline auto MapGetValuePtr(M& map, typename M::key_type const& key) -> decltype(AddressOrSelf(map.find(key)->second))
|
inline auto MapGetValuePtr(M& map, typename M::key_type const& key)
|
||||||
{
|
{
|
||||||
|
using mapped_type = typename M::mapped_type;
|
||||||
|
|
||||||
auto itr = map.find(key);
|
auto itr = map.find(key);
|
||||||
return itr != map.end() ? AddressOrSelf(itr->second) : nullptr;
|
if constexpr (std::is_pointer_v<mapped_type>)
|
||||||
|
return itr != map.end() ? itr->second : nullptr; // raw pointer
|
||||||
|
else if constexpr (requires(mapped_type const& p) { p.operator->(); })
|
||||||
|
{
|
||||||
|
// smart pointers
|
||||||
|
if constexpr (std::is_copy_constructible_v<mapped_type>)
|
||||||
|
return itr != map.end() ? itr->second : nullptr; // copyable (like shared_ptr)
|
||||||
|
else
|
||||||
|
return itr != map.end() ? std::to_address(itr->second) : nullptr; // non-copyable unique_ptr like, unwrap it to raw pointer
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return itr != map.end() ? std::addressof(itr->second) : nullptr; // value
|
||||||
}
|
}
|
||||||
|
|
||||||
template<class K, class V, template<class, class, class...> class M, class... Rest>
|
template <Map M>
|
||||||
inline void MultimapErasePair(M<K, V, Rest...>& multimap, K const& key, V const& value)
|
inline void MultimapErasePair(M& multimap, typename M::key_type const& key, typename M::mapped_type const& value)
|
||||||
{
|
{
|
||||||
auto range = multimap.equal_range(key);
|
auto range = multimap.equal_range(key);
|
||||||
for (auto itr = range.first; itr != range.second; )
|
for (auto itr = range.first; itr != range.second; )
|
||||||
|
|||||||
@@ -532,10 +532,13 @@ enum class MapFlags : uint32
|
|||||||
|
|
||||||
DEFINE_ENUM_FLAG(MapFlags);
|
DEFINE_ENUM_FLAG(MapFlags);
|
||||||
|
|
||||||
enum AbilytyLearnType
|
enum class SkillLineAbilityAcquireMethod : int32
|
||||||
{
|
{
|
||||||
SKILL_LINE_ABILITY_LEARNED_ON_SKILL_VALUE = 1, // Spell state will update depending on skill value
|
Learned = 0,
|
||||||
SKILL_LINE_ABILITY_LEARNED_ON_SKILL_LEARN = 2 // Spell will be learned/removed together with entire skill
|
AutomaticSkillRank = 1, // Spell state will update depending on skill value
|
||||||
|
AutomaticCharLevel = 2, // Spell will be learned/removed together with entire skill
|
||||||
|
NeverLearned = 3,
|
||||||
|
LearnedOrAutomaticCharLevel = 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ItemEnchantmentType
|
enum ItemEnchantmentType
|
||||||
|
|||||||
@@ -648,7 +648,7 @@ void DBCManager::LoadStores(const std::string& dataPath, uint32 defaultLocale)
|
|||||||
if (skillLine->SkillLine != cFamily->SkillLine[0] && skillLine->SkillLine != cFamily->SkillLine[1])
|
if (skillLine->SkillLine != cFamily->SkillLine[0] && skillLine->SkillLine != cFamily->SkillLine[1])
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (skillLine->AcquireMethod != SKILL_LINE_ABILITY_LEARNED_ON_SKILL_LEARN)
|
if (skillLine->GetAcquireMethod() != SkillLineAbilityAcquireMethod::AutomaticCharLevel)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
sPetFamilySpellsStore[cFamily->ID].insert(spellInfo->ID);
|
sPetFamilySpellsStore[cFamily->ID].insert(spellInfo->ID);
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ TC_GAME_API extern DBCStorage <ResearchProjectEntry> sResearchProjectSto
|
|||||||
TC_GAME_API extern DBCStorage <ResearchSiteEntry> sResearchSiteStore;
|
TC_GAME_API extern DBCStorage <ResearchSiteEntry> sResearchSiteStore;
|
||||||
TC_GAME_API extern DBCStorage <ScalingStatDistributionEntry> sScalingStatDistributionStore;
|
TC_GAME_API extern DBCStorage <ScalingStatDistributionEntry> sScalingStatDistributionStore;
|
||||||
TC_GAME_API extern DBCStorage <ScalingStatValuesEntry> sScalingStatValuesStore;
|
TC_GAME_API extern DBCStorage <ScalingStatValuesEntry> sScalingStatValuesStore;
|
||||||
|
TC_GAME_API extern DBCStorage <SkillRaceClassInfoEntry> sSkillRaceClassInfoStore;
|
||||||
TC_GAME_API extern DBCStorage <SkillLineEntry> sSkillLineStore;
|
TC_GAME_API extern DBCStorage <SkillLineEntry> sSkillLineStore;
|
||||||
TC_GAME_API extern DBCStorage <SkillLineAbilityEntry> sSkillLineAbilityStore;
|
TC_GAME_API extern DBCStorage <SkillLineAbilityEntry> sSkillLineAbilityStore;
|
||||||
TC_GAME_API extern DBCStorage <SkillTiersEntry> sSkillTiersStore;
|
TC_GAME_API extern DBCStorage <SkillTiersEntry> sSkillTiersStore;
|
||||||
|
|||||||
@@ -1521,6 +1521,8 @@ struct SkillLineAbilityEntry
|
|||||||
uint32 TrivialSkillLineRankLow; // 11
|
uint32 TrivialSkillLineRankLow; // 11
|
||||||
uint32 NumSkillUps; // 12
|
uint32 NumSkillUps; // 12
|
||||||
uint32 UniqueBit; // 13
|
uint32 UniqueBit; // 13
|
||||||
|
|
||||||
|
SkillLineAbilityAcquireMethod GetAcquireMethod() const { return static_cast<SkillLineAbilityAcquireMethod>(AcquireMethod); }
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SkillRaceClassInfoEntry
|
struct SkillRaceClassInfoEntry
|
||||||
@@ -1531,7 +1533,7 @@ struct SkillRaceClassInfoEntry
|
|||||||
uint32 ClassMask; // 3
|
uint32 ClassMask; // 3
|
||||||
uint32 Flags; // 4
|
uint32 Flags; // 4
|
||||||
uint32 Availability; // 5
|
uint32 Availability; // 5
|
||||||
//uint32 MinLevel; // 6
|
uint32 MinLevel; // 6
|
||||||
uint32 SkillTierID; // 7
|
uint32 SkillTierID; // 7
|
||||||
//uint32 SkillCostIndex; // 8
|
//uint32 SkillCostIndex; // 8
|
||||||
};
|
};
|
||||||
@@ -1543,6 +1545,17 @@ struct SkillTiersEntry
|
|||||||
uint32 ID; // 0
|
uint32 ID; // 0
|
||||||
//uint32 Cost[MAX_SKILL_STEP]; // 1-16
|
//uint32 Cost[MAX_SKILL_STEP]; // 1-16
|
||||||
uint32 Value[MAX_SKILL_STEP]; // 17-32
|
uint32 Value[MAX_SKILL_STEP]; // 17-32
|
||||||
|
|
||||||
|
uint32 GetValueForTierIndex(uint32 tierIndex) const
|
||||||
|
{
|
||||||
|
if (tierIndex >= MAX_SKILL_STEP)
|
||||||
|
tierIndex = MAX_SKILL_STEP - 1;
|
||||||
|
|
||||||
|
while (Value[tierIndex] == 0 && tierIndex > 0)
|
||||||
|
--tierIndex;
|
||||||
|
|
||||||
|
return Value[tierIndex];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SoundEntriesEntry
|
struct SoundEntriesEntry
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ char const ScalingStatDistributionfmt[] = "niiiiiiiiiiiiiiiiiiiiii";
|
|||||||
char const ScalingStatValuesfmt[] = "iniiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii";
|
char const ScalingStatValuesfmt[] = "iniiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii";
|
||||||
char const SkillLinefmt[] = "nisxixi";
|
char const SkillLinefmt[] = "nisxixi";
|
||||||
char const SkillLineAbilityfmt[] = "niiiixxiiiiiii";
|
char const SkillLineAbilityfmt[] = "niiiixxiiiiiii";
|
||||||
char const SkillRaceClassInfofmt[] = "diiiiixix";
|
char const SkillRaceClassInfofmt[] = "diiiiiiix";
|
||||||
char const SkillTiersfmt[] = "nxxxxxxxxxxxxxxxxiiiiiiiiiiiiiiii";
|
char const SkillTiersfmt[] = "nxxxxxxxxxxxxxxxxiiiiiiiiiiiiiiii";
|
||||||
char const SoundEntriesfmt[] = "nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
|
char const SoundEntriesfmt[] = "nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
|
||||||
char const SpellCastTimefmt[] = "nixx";
|
char const SpellCastTimefmt[] = "nixx";
|
||||||
|
|||||||
@@ -2952,13 +2952,44 @@ bool Player::AddSpell(uint32 spellId, bool active, bool learning, bool dependent
|
|||||||
// add dependent skills if this spell is not learned from adding skill already
|
// add dependent skills if this spell is not learned from adding skill already
|
||||||
if (spellLearnSkill->skill != fromSkill)
|
if (spellLearnSkill->skill != fromSkill)
|
||||||
{
|
{
|
||||||
uint32 skill_value = GetPureSkillValue(spellLearnSkill->skill);
|
uint16 skill_value = GetPureSkillValue(spellLearnSkill->skill);
|
||||||
uint32 skill_max_value = GetPureMaxSkillValue(spellLearnSkill->skill);
|
uint16 skill_max_value = GetPureMaxSkillValue(spellLearnSkill->skill);
|
||||||
|
|
||||||
if (skill_value < spellLearnSkill->value)
|
if (skill_value < spellLearnSkill->value)
|
||||||
skill_value = spellLearnSkill->value;
|
skill_value = spellLearnSkill->value;
|
||||||
|
|
||||||
uint32 new_skill_max_value = spellLearnSkill->maxvalue == 0 ? GetMaxSkillValueForLevel() : spellLearnSkill->maxvalue;
|
uint16 new_skill_max_value = spellLearnSkill->maxvalue;
|
||||||
|
|
||||||
|
if (new_skill_max_value == 0)
|
||||||
|
{
|
||||||
|
if (SkillRaceClassInfoEntry const* rcInfo = sDBCManager.GetSkillRaceClassInfo(spellLearnSkill->skill, getRace(), getClass()))
|
||||||
|
{
|
||||||
|
switch (GetSkillRangeType(rcInfo))
|
||||||
|
{
|
||||||
|
case SKILL_RANGE_LANGUAGE:
|
||||||
|
skill_value = 300;
|
||||||
|
new_skill_max_value = 300;
|
||||||
|
break;
|
||||||
|
case SKILL_RANGE_LEVEL:
|
||||||
|
new_skill_max_value = GetMaxSkillValueForLevel();
|
||||||
|
break;
|
||||||
|
case SKILL_RANGE_MONO:
|
||||||
|
new_skill_max_value = 1;
|
||||||
|
break;
|
||||||
|
case SKILL_RANGE_RANK:
|
||||||
|
{
|
||||||
|
SkillTiersEntry const* tier = sSkillTiersStore.AssertEntry(rcInfo->SkillTierID);
|
||||||
|
new_skill_max_value = tier->GetValueForTierIndex(spellLearnSkill->step - 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rcInfo->Flags & SKILL_FLAG_ALWAYS_MAX_VALUE)
|
||||||
|
skill_value = new_skill_max_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (skill_max_value < new_skill_max_value)
|
if (skill_max_value < new_skill_max_value)
|
||||||
skill_max_value = new_skill_max_value;
|
skill_max_value = new_skill_max_value;
|
||||||
@@ -2978,10 +3009,10 @@ bool Player::AddSpell(uint32 spellId, bool active, bool learning, bool dependent
|
|||||||
if (pSkill->ID == fromSkill)
|
if (pSkill->ID == fromSkill)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
///@todo: confirm if rogues start with lockpicking skill at level 1 but only receive the spell to use it at level 16
|
// Runeforging special case
|
||||||
// Also added for runeforging. It's already confirmed this happens upon learning for Death Knights, not from character creation.
|
if ((_spell_idx->second->GetAcquireMethod() == SkillLineAbilityAcquireMethod::AutomaticCharLevel && !HasSkill(_spell_idx->second->SkillLine)) || ((_spell_idx->second->SkillLine == SKILL_RUNEFORGING) && _spell_idx->second->TrivialSkillLineRankHigh == 0))
|
||||||
if ((_spell_idx->second->AcquireMethod == SKILL_LINE_ABILITY_LEARNED_ON_SKILL_LEARN && !HasSkill(pSkill->ID)) || ((pSkill->ID == SKILL_LOCKPICKING || pSkill->ID == SKILL_RUNEFORGING) && _spell_idx->second->TrivialSkillLineRankHigh == 0))
|
if (SkillRaceClassInfoEntry const* rcInfo = sDBCManager.GetSkillRaceClassInfo(_spell_idx->second->SkillLine, getRace(), getClass()))
|
||||||
LearnDefaultSkill(pSkill->ID, 0);
|
LearnDefaultSkill(rcInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3213,17 +3244,50 @@ void Player::RemoveSpell(uint32 spell_id, bool disabled, bool learn_low_rank)
|
|||||||
SetSkill(spellLearnSkill->skill, 0, 0, 0);
|
SetSkill(spellLearnSkill->skill, 0, 0, 0);
|
||||||
else // set to prev. skill setting values
|
else // set to prev. skill setting values
|
||||||
{
|
{
|
||||||
uint32 skill_value = GetPureSkillValue(prevSkill->skill);
|
uint16 skill_value = GetPureSkillValue(prevSkill->skill);
|
||||||
uint32 skill_max_value = GetPureMaxSkillValue(prevSkill->skill);
|
uint16 skill_max_value = GetPureMaxSkillValue(prevSkill->skill);
|
||||||
|
|
||||||
if (skill_value > prevSkill->value)
|
uint16 new_skill_max_value = prevSkill->maxvalue;
|
||||||
|
|
||||||
|
if (new_skill_max_value == 0)
|
||||||
|
{
|
||||||
|
if (SkillRaceClassInfoEntry const* rcInfo = sDBCManager.GetSkillRaceClassInfo(prevSkill->skill, getRace(), getClass()))
|
||||||
|
{
|
||||||
|
switch (GetSkillRangeType(rcInfo))
|
||||||
|
{
|
||||||
|
case SKILL_RANGE_LANGUAGE:
|
||||||
|
skill_value = 300;
|
||||||
|
new_skill_max_value = 300;
|
||||||
|
break;
|
||||||
|
case SKILL_RANGE_LEVEL:
|
||||||
|
new_skill_max_value = GetMaxSkillValueForLevel();
|
||||||
|
break;
|
||||||
|
case SKILL_RANGE_MONO:
|
||||||
|
new_skill_max_value = 1;
|
||||||
|
break;
|
||||||
|
case SKILL_RANGE_RANK:
|
||||||
|
{
|
||||||
|
SkillTiersEntry const* tier = sSkillTiersStore.AssertEntry(rcInfo->SkillTierID);
|
||||||
|
new_skill_max_value = tier->GetValueForTierIndex(prevSkill->step - 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rcInfo->Flags & SKILL_FLAG_ALWAYS_MAX_VALUE)
|
||||||
|
skill_value = new_skill_max_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (skill_value > prevSkill->value)
|
||||||
skill_value = prevSkill->value;
|
skill_value = prevSkill->value;
|
||||||
|
|
||||||
uint32 new_skill_max_value = prevSkill->maxvalue == 0 ? GetMaxSkillValueForLevel() : prevSkill->maxvalue;
|
|
||||||
|
|
||||||
if (skill_max_value > new_skill_max_value)
|
if (skill_max_value > new_skill_max_value)
|
||||||
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);
|
SetSkill(prevSkill->skill, prevSkill->step, skill_value, skill_max_value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5401,12 +5465,30 @@ void Player::InitializeSkillFields()
|
|||||||
// To "remove" a skill line, set it's values to zero
|
// To "remove" a skill line, set it's values to zero
|
||||||
void Player::SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal)
|
void Player::SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal)
|
||||||
{
|
{
|
||||||
if (!id)
|
SkillLineEntry const* skillEntry = sSkillLineStore.LookupEntry(id);
|
||||||
|
if (!skillEntry)
|
||||||
|
{
|
||||||
|
TC_LOG_ERROR("misc", "Player::SetSkill: Skill (SkillID: %u) not found in SkillLineStore for player '%s' (%s)",
|
||||||
|
id, GetName().c_str(), GetGUID().ToString().c_str());
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
uint16 currVal;
|
uint16 currVal;
|
||||||
SkillStatusMap::iterator itr = mSkillStatus.find(id);
|
SkillStatusMap::iterator itr = mSkillStatus.find(id);
|
||||||
|
|
||||||
|
auto refreshSkillBonusAuras = [&]
|
||||||
|
{
|
||||||
|
// Temporary bonuses
|
||||||
|
for (AuraEffect* effect : GetAuraEffectsByType(SPELL_AURA_MOD_SKILL))
|
||||||
|
if (effect->GetMiscValue() == int32(id))
|
||||||
|
effect->HandleEffect(this, AURA_EFFECT_HANDLE_SKILL, true);
|
||||||
|
|
||||||
|
// Permanent bonuses
|
||||||
|
for (AuraEffect* effect : GetAuraEffectsByType(SPELL_AURA_MOD_SKILL_TALENT))
|
||||||
|
if (effect->GetMiscValue() == int32(id))
|
||||||
|
effect->HandleEffect(this, AURA_EFFECT_HANDLE_SKILL, true);
|
||||||
|
};
|
||||||
|
|
||||||
// Handle already stored skills
|
// Handle already stored skills
|
||||||
if (itr != mSkillStatus.end())
|
if (itr != mSkillStatus.end())
|
||||||
{
|
{
|
||||||
@@ -5441,7 +5523,21 @@ void Player::SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal)
|
|||||||
if (itr->second.uState == SKILL_UNCHANGED)
|
if (itr->second.uState == SKILL_UNCHANGED)
|
||||||
{
|
{
|
||||||
if (currVal == 0) // activated skill, mark as new to save into database
|
if (currVal == 0) // activated skill, mark as new to save into database
|
||||||
itr->second.uState = SKILL_NEW;
|
{
|
||||||
|
itr->second.uState = itr->second.uState != SKILL_DELETED
|
||||||
|
? SKILL_NEW
|
||||||
|
: SKILL_CHANGED; // skills marked as SKILL_DELETED already exist in database, mark as changed instead of new
|
||||||
|
|
||||||
|
if (skillEntry->CategoryID == SKILL_CATEGORY_PROFESSION)
|
||||||
|
{
|
||||||
|
if (!GetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1))
|
||||||
|
SetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1, id);
|
||||||
|
else if (!GetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1 + 1))
|
||||||
|
SetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1 + 1, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshSkillBonusAuras();
|
||||||
|
}
|
||||||
else // updated skill, mark as changed to save into database
|
else // updated skill, mark as changed to save into database
|
||||||
itr->second.uState = SKILL_CHANGED;
|
itr->second.uState = SKILL_CHANGED;
|
||||||
}
|
}
|
||||||
@@ -5467,10 +5563,9 @@ void Player::SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal)
|
|||||||
SetSkillPermBonus(itr->second.pos, 0);
|
SetSkillPermBonus(itr->second.pos, 0);
|
||||||
|
|
||||||
// mark as deleted so the next save will delete the data from the database
|
// mark as deleted so the next save will delete the data from the database
|
||||||
if (itr->second.uState != SKILL_NEW)
|
itr->second.uState = itr->second.uState != SKILL_NEW
|
||||||
itr->second.uState = SKILL_DELETED;
|
? SKILL_DELETED
|
||||||
else
|
: SKILL_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)
|
||||||
itr->second.uState = SKILL_UNCHANGED;
|
|
||||||
|
|
||||||
// remove all spells that related to this skill
|
// remove all spells that related to this skill
|
||||||
if (std::vector<SkillLineAbilityEntry const*> const* skillLineAbilities = sDBCManager.GetSkillLineAbilitiesBySkill(id))
|
if (std::vector<SkillLineAbilityEntry const*> const* skillLineAbilities = sDBCManager.GetSkillLineAbilitiesBySkill(id))
|
||||||
@@ -5509,13 +5604,6 @@ void Player::SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SkillLineEntry const* skillEntry = sSkillLineStore.LookupEntry(id);
|
|
||||||
if (!skillEntry)
|
|
||||||
{
|
|
||||||
TC_LOG_ERROR("misc", "Skill not found in SkillLineStore: skill #%u", id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (skillEntry->CategoryID == SKILL_CATEGORY_PROFESSION)
|
if (skillEntry->CategoryID == SKILL_CATEGORY_PROFESSION)
|
||||||
{
|
{
|
||||||
if (!GetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1))
|
if (!GetUInt32Value(PLAYER_PROFESSION_SKILL_LINE_1))
|
||||||
@@ -5544,15 +5632,7 @@ void Player::SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal)
|
|||||||
UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_REACH_SKILL_LEVEL, id);
|
UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_REACH_SKILL_LEVEL, id);
|
||||||
UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILL_LEVEL, id);
|
UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILL_LEVEL, id);
|
||||||
|
|
||||||
// temporary bonuses
|
refreshSkillBonusAuras();
|
||||||
for (AuraEffect* effect : GetAuraEffectsByType(SPELL_AURA_MOD_SKILL))
|
|
||||||
if (effect->GetMiscValue() == int32(id))
|
|
||||||
effect->HandleEffect(this, AURA_EFFECT_HANDLE_SKILL, true);
|
|
||||||
|
|
||||||
// permanent bonuses
|
|
||||||
for (AuraEffect* effect : GetAuraEffectsByType(SPELL_AURA_MOD_SKILL_TALENT))
|
|
||||||
if (effect->GetMiscValue() == int32(id))
|
|
||||||
effect->HandleEffect(this, AURA_EFFECT_HANDLE_SKILL, true);
|
|
||||||
|
|
||||||
// archaeology skill learned
|
// archaeology skill learned
|
||||||
if (id == SKILL_ARCHAEOLOGY)
|
if (id == SKILL_ARCHAEOLOGY)
|
||||||
@@ -23128,21 +23208,20 @@ void Player::LearnDefaultSkills()
|
|||||||
ASSERT(info);
|
ASSERT(info);
|
||||||
for (PlayerCreateInfoSkills::const_iterator itr = info->skills.begin(); itr != info->skills.end(); ++itr)
|
for (PlayerCreateInfoSkills::const_iterator itr = info->skills.begin(); itr != info->skills.end(); ++itr)
|
||||||
{
|
{
|
||||||
uint32 skillId = itr->SkillId;
|
SkillRaceClassInfoEntry const* rcInfo = *itr;
|
||||||
if (HasSkill(skillId))
|
if (HasSkill(rcInfo->SkillID))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
LearnDefaultSkill(skillId, itr->Rank);
|
if (rcInfo->MinLevel > getLevel())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
LearnDefaultSkill(rcInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::LearnDefaultSkill(uint32 skillId, uint16 rank)
|
void Player::LearnDefaultSkill(SkillRaceClassInfoEntry const* rcInfo)
|
||||||
{
|
{
|
||||||
SkillRaceClassInfoEntry const* rcInfo = sDBCManager.GetSkillRaceClassInfo(skillId, getRace(), getClass());
|
uint16 skillId = rcInfo->SkillID;
|
||||||
if (!rcInfo)
|
|
||||||
return;
|
|
||||||
|
|
||||||
TC_LOG_DEBUG("entities.player.loading", "PLAYER (Class: %u Race: %u): Adding initial skill, id = %u", uint32(getClass()), uint32(getRace()), skillId);
|
|
||||||
switch (GetSkillRangeType(rcInfo))
|
switch (GetSkillRangeType(rcInfo))
|
||||||
{
|
{
|
||||||
case SKILL_RANGE_LANGUAGE:
|
case SKILL_RANGE_LANGUAGE:
|
||||||
@@ -23169,16 +23248,13 @@ void Player::LearnDefaultSkill(uint32 skillId, uint16 rank)
|
|||||||
break;
|
break;
|
||||||
case SKILL_RANGE_RANK:
|
case SKILL_RANGE_RANK:
|
||||||
{
|
{
|
||||||
if (!rank)
|
|
||||||
break;
|
|
||||||
|
|
||||||
SkillTiersEntry const* tier = sSkillTiersStore.LookupEntry(rcInfo->SkillTierID);
|
SkillTiersEntry const* tier = sSkillTiersStore.LookupEntry(rcInfo->SkillTierID);
|
||||||
uint16 maxValue = tier->Value[std::max<int32>(rank - 1, 0)];
|
uint16 maxValue = tier->GetValueForTierIndex(0);
|
||||||
uint16 skillValue = 1;
|
uint16 skillValue = 1;
|
||||||
if (rcInfo->Flags & SKILL_FLAG_ALWAYS_MAX_VALUE)
|
if (rcInfo->Flags & SKILL_FLAG_ALWAYS_MAX_VALUE)
|
||||||
skillValue = maxValue;
|
skillValue = maxValue;
|
||||||
else if (getClass() == CLASS_DEATH_KNIGHT)
|
else if (getClass() == CLASS_DEATH_KNIGHT)
|
||||||
skillValue = std::min(std::max<uint16>({ uint16(1), uint16((getLevel() - 1) * 5) }), maxValue);
|
skillValue = std::min(std::max(uint16(1), uint16((getLevel() - 1) * 5)), maxValue);
|
||||||
|
|
||||||
SetSkill(skillId, 1, skillValue, maxValue);
|
SetSkill(skillId, 1, skillValue, maxValue);
|
||||||
break;
|
break;
|
||||||
@@ -23226,13 +23302,19 @@ void Player::LearnQuestRewardedSpells(Quest const* quest)
|
|||||||
uint32 learned_0 = spellInfo->Effects[0].TriggerSpell;
|
uint32 learned_0 = spellInfo->Effects[0].TriggerSpell;
|
||||||
if (!HasSpell(learned_0))
|
if (!HasSpell(learned_0))
|
||||||
{
|
{
|
||||||
SpellInfo const* learnedInfo = sSpellMgr->GetSpellInfo(learned_0);
|
found = false;
|
||||||
if (!learnedInfo)
|
SkillLineAbilityMapBounds skills = sSpellMgr->GetSkillLineAbilityMapBounds(learned_0);
|
||||||
return;
|
for (auto skillItr = skills.first; skillItr != skills.second; ++skillItr)
|
||||||
|
{
|
||||||
|
if (skillItr->second->GetAcquireMethod() == SkillLineAbilityAcquireMethod::LearnedOrAutomaticCharLevel)
|
||||||
|
{
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// profession specialization can be re-learned from npc
|
if (!found)
|
||||||
if (learnedInfo->Effects[0].Effect == SPELL_EFFECT_TRADE_SKILL && learnedInfo->Effects[1].Effect == 0 && !learnedInfo->SpellLevel)
|
return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CastSpell(this, spell_id, true);
|
CastSpell(this, spell_id, true);
|
||||||
@@ -23265,19 +23347,15 @@ void Player::LearnSkillRewardedSpells(uint32 skillId, uint32 skillValue)
|
|||||||
if (!spellInfo)
|
if (!spellInfo)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
switch (ability->AcquireMethod)
|
switch (ability->GetAcquireMethod())
|
||||||
{
|
{
|
||||||
case SKILL_LINE_ABILITY_LEARNED_ON_SKILL_VALUE:
|
case SkillLineAbilityAcquireMethod::AutomaticSkillRank:
|
||||||
case SKILL_LINE_ABILITY_LEARNED_ON_SKILL_LEARN:
|
case SkillLineAbilityAcquireMethod::AutomaticCharLevel:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// AcquireMethod == 2 && NumSkillUps == 1 --> automatically learn riding skill spell, else we skip it (client shows riding in spellbook as trainable).
|
|
||||||
if (skillId == SKILL_RIDING && (ability->AcquireMethod != SKILL_LINE_ABILITY_LEARNED_ON_SKILL_LEARN || ability->NumSkillUps != 1))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Check race if set
|
// Check race if set
|
||||||
if (ability->RaceMask && !(ability->RaceMask & raceMask))
|
if (ability->RaceMask && !(ability->RaceMask & raceMask))
|
||||||
continue;
|
continue;
|
||||||
@@ -23286,12 +23364,13 @@ void Player::LearnSkillRewardedSpells(uint32 skillId, uint32 skillValue)
|
|||||||
if (ability->ClassMask && !(ability->ClassMask & classMask))
|
if (ability->ClassMask && !(ability->ClassMask & classMask))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// check level, skip class spells if not high enough
|
// Check level, skip class spells if not high enough
|
||||||
if (getLevel() < spellInfo->SpellLevel)
|
uint32 requiredLevel = std::max(spellInfo->SpellLevel, spellInfo->BaseLevel);
|
||||||
|
if (requiredLevel > getLevel())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// need unlearn spell
|
// need unlearn spell
|
||||||
if (skillValue < ability->MinSkillLineRank && ability->AcquireMethod == SKILL_LINE_ABILITY_LEARNED_ON_SKILL_VALUE)
|
if (int32(skillValue) < ability->MinSkillLineRank && ability->GetAcquireMethod() == SkillLineAbilityAcquireMethod::AutomaticSkillRank)
|
||||||
RemoveSpell(ability->Spell);
|
RemoveSpell(ability->Spell);
|
||||||
|
|
||||||
// need learn
|
// need learn
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ struct PlayerTalent;
|
|||||||
struct PlayerTalentInfo;
|
struct PlayerTalentInfo;
|
||||||
struct ScalingStatDistributionEntry;
|
struct ScalingStatDistributionEntry;
|
||||||
struct ScalingStatValuesEntry;
|
struct ScalingStatValuesEntry;
|
||||||
|
struct SkillRaceClassInfoEntry;
|
||||||
struct TrainerSpell;
|
struct TrainerSpell;
|
||||||
struct VendorItem;
|
struct VendorItem;
|
||||||
struct GuildMemberProfessionData;
|
struct GuildMemberProfessionData;
|
||||||
@@ -1549,7 +1550,7 @@ class TC_GAME_API Player : public Unit, public GridObject<Player>
|
|||||||
void ResetSpells(bool myClassOnly = false);
|
void ResetSpells(bool myClassOnly = false);
|
||||||
void LearnCustomSpells();
|
void LearnCustomSpells();
|
||||||
void LearnDefaultSkills();
|
void LearnDefaultSkills();
|
||||||
void LearnDefaultSkill(uint32 skillId, uint16 rank);
|
void LearnDefaultSkill(SkillRaceClassInfoEntry const* rcInfo);
|
||||||
void LearnQuestRewardedSpells();
|
void LearnQuestRewardedSpells();
|
||||||
void LearnQuestRewardedSpells(Quest const* quest);
|
void LearnQuestRewardedSpells(Quest const* quest);
|
||||||
void LearnSpellHighestRank(uint32 spellid);
|
void LearnSpellHighestRank(uint32 spellid);
|
||||||
|
|||||||
@@ -247,9 +247,6 @@ ObjectMgr::ObjectMgr():
|
|||||||
_gameObjectSpawnId(1),
|
_gameObjectSpawnId(1),
|
||||||
DBCLocaleIndex(LOCALE_enUS)
|
DBCLocaleIndex(LOCALE_enUS)
|
||||||
{
|
{
|
||||||
for (uint8 i = 0; i < MAX_CLASSES; ++i)
|
|
||||||
for (uint8 j = 0; j < MAX_RACES; ++j)
|
|
||||||
_playerInfo[j][i] = nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ObjectMgr* ObjectMgr::instance()
|
ObjectMgr* ObjectMgr::instance()
|
||||||
@@ -263,16 +260,6 @@ ObjectMgr::~ObjectMgr()
|
|||||||
for (PetLevelInfoContainer::iterator i = _petInfoStore.begin(); i != _petInfoStore.end(); ++i)
|
for (PetLevelInfoContainer::iterator i = _petInfoStore.begin(); i != _petInfoStore.end(); ++i)
|
||||||
delete[] i->second;
|
delete[] i->second;
|
||||||
|
|
||||||
for (int race = 0; race < MAX_RACES; ++race)
|
|
||||||
{
|
|
||||||
for (int class_ = 0; class_ < MAX_CLASSES; ++class_)
|
|
||||||
{
|
|
||||||
if (_playerInfo[race][class_])
|
|
||||||
delete[] _playerInfo[race][class_]->levelInfo;
|
|
||||||
delete _playerInfo[race][class_];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (CacheVendorItemContainer::iterator itr = _cacheVendorItemStore.begin(); itr != _cacheVendorItemStore.end(); ++itr)
|
for (CacheVendorItemContainer::iterator itr = _cacheVendorItemStore.begin(); itr != _cacheVendorItemStore.end(); ++itr)
|
||||||
itr->second.Clear();
|
itr->second.Clear();
|
||||||
|
|
||||||
@@ -3321,11 +3308,12 @@ PetLevelInfo const* ObjectMgr::GetPetLevelInfo(uint32 creature_id, uint8 level)
|
|||||||
|
|
||||||
void ObjectMgr::PlayerCreateInfoAddItemHelper(uint32 race_, uint32 class_, uint32 itemId, int32 count)
|
void ObjectMgr::PlayerCreateInfoAddItemHelper(uint32 race_, uint32 class_, uint32 itemId, int32 count)
|
||||||
{
|
{
|
||||||
if (!_playerInfo[race_][class_])
|
PlayerInfo* playerInfo = Trinity::Containers::MapGetValuePtr(_playerInfo, { Races(race_), Classes(class_) });
|
||||||
|
if (!playerInfo)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (count > 0)
|
if (count > 0)
|
||||||
_playerInfo[race_][class_]->item.push_back(PlayerCreateInfoItem(itemId, count));
|
playerInfo->item.emplace_back(itemId, count);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (count < -1)
|
if (count < -1)
|
||||||
@@ -3421,7 +3409,7 @@ void ObjectMgr::LoadPlayerInfo()
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayerInfo* info = new PlayerInfo();
|
std::unique_ptr<PlayerInfo> info = std::make_unique<PlayerInfo>();
|
||||||
info->mapId = mapId;
|
info->mapId = mapId;
|
||||||
info->areaId = areaId;
|
info->areaId = areaId;
|
||||||
info->positionX = positionX;
|
info->positionX = positionX;
|
||||||
@@ -3430,7 +3418,7 @@ void ObjectMgr::LoadPlayerInfo()
|
|||||||
info->orientation = orientation;
|
info->orientation = orientation;
|
||||||
info->displayId_m = rEntry->MaleDisplayID;
|
info->displayId_m = rEntry->MaleDisplayID;
|
||||||
info->displayId_f = rEntry->FemaleDisplayID;
|
info->displayId_f = rEntry->FemaleDisplayID;
|
||||||
_playerInfo[current_race][current_class] = info;
|
_playerInfo[{ Races(current_race), Classes(current_class) }] = std::move(info);
|
||||||
|
|
||||||
++count;
|
++count;
|
||||||
}
|
}
|
||||||
@@ -3516,77 +3504,19 @@ void ObjectMgr::LoadPlayerInfo()
|
|||||||
{
|
{
|
||||||
uint32 oldMSTime = getMSTime();
|
uint32 oldMSTime = getMSTime();
|
||||||
|
|
||||||
QueryResult result = WorldDatabase.PQuery("SELECT raceMask, classMask, skill, `rank` FROM playercreateinfo_skills");
|
for (SkillRaceClassInfoEntry const* rcInfo : sSkillRaceClassInfoStore)
|
||||||
|
if (rcInfo->Availability == 1)
|
||||||
if (!result)
|
|
||||||
{
|
|
||||||
TC_LOG_ERROR("server.loading", ">> Loaded 0 player create skills. DB table `playercreateinfo_skills` is empty.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
uint32 count = 0;
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
Field* fields = result->Fetch();
|
|
||||||
uint32 raceMask = fields[0].GetUInt32();
|
|
||||||
uint32 classMask = fields[1].GetUInt32();
|
|
||||||
PlayerCreateInfoSkill skill;
|
|
||||||
skill.SkillId = fields[2].GetUInt16();
|
|
||||||
skill.Rank = fields[3].GetUInt16();
|
|
||||||
|
|
||||||
if (skill.Rank >= MAX_SKILL_STEP)
|
|
||||||
{
|
|
||||||
TC_LOG_ERROR("sql.sql", "Skill rank value %hu set for skill %hu raceMask %u classMask %u is too high, max allowed value is %d", skill.Rank, skill.SkillId, raceMask, classMask, MAX_SKILL_STEP);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (raceMask != 0 && !(raceMask & RACEMASK_ALL_PLAYABLE))
|
|
||||||
{
|
|
||||||
TC_LOG_ERROR("sql.sql", "Wrong race mask %u in `playercreateinfo_skills` table, ignoring.", raceMask);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (classMask != 0 && !(classMask & CLASSMASK_ALL_PLAYABLE))
|
|
||||||
{
|
|
||||||
TC_LOG_ERROR("sql.sql", "Wrong class mask %u in `playercreateinfo_skills` table, ignoring.", classMask);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sSkillLineStore.LookupEntry(skill.SkillId))
|
|
||||||
{
|
|
||||||
TC_LOG_ERROR("sql.sql", "Wrong skill id %u in `playercreateinfo_skills` table, ignoring.", skill.SkillId);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (uint32 raceIndex = RACE_HUMAN; raceIndex < MAX_RACES; ++raceIndex)
|
for (uint32 raceIndex = RACE_HUMAN; raceIndex < MAX_RACES; ++raceIndex)
|
||||||
{
|
if (rcInfo->RaceMask == 0 || (rcInfo->RaceMask & (1 << (raceIndex - 1)) != 0))
|
||||||
if (raceMask == 0 || ((1 << (raceIndex - 1)) & raceMask))
|
|
||||||
{
|
|
||||||
for (uint32 classIndex = CLASS_WARRIOR; classIndex < MAX_CLASSES; ++classIndex)
|
for (uint32 classIndex = CLASS_WARRIOR; classIndex < MAX_CLASSES; ++classIndex)
|
||||||
{
|
if (rcInfo->ClassMask == -1 || rcInfo->ClassMask == 0 || ((1 << (classIndex - 1)) & rcInfo->ClassMask))
|
||||||
if (classMask == 0 || ((1 << (classIndex - 1)) & classMask))
|
if (auto const& playerInfo = Trinity::Containers::MapGetValuePtr(_playerInfo, { Races(raceIndex), Classes(classIndex) }))
|
||||||
{
|
playerInfo->skills.push_back(rcInfo);
|
||||||
if (!sDBCManager.GetSkillRaceClassInfo(skill.SkillId, raceIndex, classIndex))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (PlayerInfo* info = _playerInfo[raceIndex][classIndex])
|
TC_LOG_INFO("server.loading", ">> Loaded player create skills in %u ms", GetMSTimeDiffToNow(oldMSTime));
|
||||||
{
|
|
||||||
info->skills.push_back(skill);
|
|
||||||
++count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (result->NextRow());
|
|
||||||
|
|
||||||
TC_LOG_INFO("server.loading", ">> Loaded %u player create skills in %u ms", count, GetMSTimeDiffToNow(oldMSTime));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load playercreate spells
|
// Load playercreate spells
|
||||||
TC_LOG_INFO("server.loading", "Loading Player Create Spell Data...");
|
TC_LOG_INFO("server.loading", "Loading Player Create Custom Spell Data...");
|
||||||
{
|
{
|
||||||
uint32 oldMSTime = getMSTime();
|
uint32 oldMSTime = getMSTime();
|
||||||
|
|
||||||
@@ -3627,9 +3557,9 @@ void ObjectMgr::LoadPlayerInfo()
|
|||||||
{
|
{
|
||||||
if (classMask == 0 || ((1 << (classIndex - 1)) & classMask))
|
if (classMask == 0 || ((1 << (classIndex - 1)) & classMask))
|
||||||
{
|
{
|
||||||
if (PlayerInfo* info = _playerInfo[raceIndex][classIndex])
|
if (auto const& playerInfo = Trinity::Containers::MapGetValuePtr(_playerInfo, { Races(raceIndex), Classes(classIndex) }))
|
||||||
{
|
{
|
||||||
info->customSpells.push_back(spellId);
|
playerInfo->customSpells.push_back(spellId);
|
||||||
++count;
|
++count;
|
||||||
}
|
}
|
||||||
// We need something better here, the check is not accounting for spells used by multiple races/classes but not all of them.
|
// We need something better here, the check is not accounting for spells used by multiple races/classes but not all of them.
|
||||||
@@ -3687,9 +3617,9 @@ void ObjectMgr::LoadPlayerInfo()
|
|||||||
{
|
{
|
||||||
if (classMask == 0 || ((1 << (classIndex - 1)) & classMask))
|
if (classMask == 0 || ((1 << (classIndex - 1)) & classMask))
|
||||||
{
|
{
|
||||||
if (PlayerInfo* info = _playerInfo[raceIndex][classIndex])
|
if (auto const& playerInfo = Trinity::Containers::MapGetValuePtr(_playerInfo, { Races(raceIndex), Classes(classIndex) }))
|
||||||
{
|
{
|
||||||
info->castSpells.push_back(spellId);
|
playerInfo->castSpells.push_back(spellId);
|
||||||
++count;
|
++count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3736,8 +3666,8 @@ void ObjectMgr::LoadPlayerInfo()
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PlayerInfo* info = _playerInfo[current_race][current_class])
|
if (auto const& playerInfo = Trinity::Containers::MapGetValuePtr(_playerInfo, { Races(current_race), Classes(current_class) }))
|
||||||
info->action.push_back(PlayerCreateInfoAction(fields[2].GetUInt16(), fields[3].GetUInt32(), fields[4].GetUInt16()));
|
playerInfo->action.push_back(PlayerCreateInfoAction(fields[2].GetUInt16(), fields[3].GetUInt32(), fields[4].GetUInt16()));
|
||||||
|
|
||||||
++count;
|
++count;
|
||||||
}
|
}
|
||||||
@@ -3822,12 +3752,12 @@ void ObjectMgr::LoadPlayerInfo()
|
|||||||
|
|
||||||
for (std::size_t race = 0; race < raceStatModifiers.size(); ++race)
|
for (std::size_t race = 0; race < raceStatModifiers.size(); ++race)
|
||||||
{
|
{
|
||||||
if (PlayerInfo* info = _playerInfo[race][current_class])
|
if (auto const& playerInfo = Trinity::Containers::MapGetValuePtr(_playerInfo, { Races(race), Classes(current_class) }))
|
||||||
{
|
{
|
||||||
if (!info->levelInfo)
|
if (!playerInfo->levelInfo)
|
||||||
info->levelInfo = new PlayerLevelInfo[sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)];
|
playerInfo->levelInfo = std::make_unique<PlayerLevelInfo[]>(sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL));
|
||||||
|
|
||||||
PlayerLevelInfo& levelInfo = info->levelInfo[current_level - 1];
|
PlayerLevelInfo& levelInfo = playerInfo->levelInfo[current_level - 1];
|
||||||
for (uint8 i = 0; i < MAX_STATS; i++)
|
for (uint8 i = 0; i < MAX_STATS; i++)
|
||||||
levelInfo.stats[i] = fields[i + 2].GetUInt16() + raceStatModifiers[race].StatModifier[i];
|
levelInfo.stats[i] = fields[i + 2].GetUInt16() + raceStatModifiers[race].StatModifier[i];
|
||||||
}
|
}
|
||||||
@@ -3850,8 +3780,8 @@ void ObjectMgr::LoadPlayerInfo()
|
|||||||
if (!sChrClassesStore.LookupEntry(class_))
|
if (!sChrClassesStore.LookupEntry(class_))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
PlayerInfo* info = _playerInfo[race][class_];
|
auto const& playerInfo = Trinity::Containers::MapGetValuePtr(_playerInfo, { Races(race), Classes(class_) });
|
||||||
if (!info)
|
if (!playerInfo)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// skip expansion races if not playing with expansion
|
// skip expansion races if not playing with expansion
|
||||||
@@ -3867,7 +3797,7 @@ void ObjectMgr::LoadPlayerInfo()
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
// fatal error if no level 1 data
|
// fatal error if no level 1 data
|
||||||
if (!info->levelInfo || info->levelInfo[0].stats[0] == 0)
|
if (!playerInfo->levelInfo || playerInfo->levelInfo[0].stats[0] == 0)
|
||||||
{
|
{
|
||||||
TC_LOG_ERROR("sql.sql", "Race %i Class %i Level 1 does not have stats data!", race, class_);
|
TC_LOG_ERROR("sql.sql", "Race %i Class %i Level 1 does not have stats data!", race, class_);
|
||||||
exit(1);
|
exit(1);
|
||||||
@@ -3876,10 +3806,10 @@ void ObjectMgr::LoadPlayerInfo()
|
|||||||
// fill level gaps
|
// fill level gaps
|
||||||
for (uint8 level = 1; level < sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); ++level)
|
for (uint8 level = 1; level < sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); ++level)
|
||||||
{
|
{
|
||||||
if (info->levelInfo[level].stats[0] == 0)
|
if (playerInfo->levelInfo[level].stats[0] == 0)
|
||||||
{
|
{
|
||||||
TC_LOG_ERROR("sql.sql", "Race %i Class %i Level %i does not have stats data. Using stats data of level %i.", race, class_, level + 1, level);
|
TC_LOG_ERROR("sql.sql", "Race %i Class %i Level %i does not have stats data. Using stats data of level %i.", race, class_, level + 1, level);
|
||||||
info->levelInfo[level] = info->levelInfo[level - 1];
|
playerInfo->levelInfo[level] = playerInfo->levelInfo[level - 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3972,7 +3902,7 @@ void ObjectMgr::GetPlayerLevelInfo(uint32 race, uint32 class_, uint8 level, Play
|
|||||||
if (level < 1 || race >= MAX_RACES || class_ >= MAX_CLASSES)
|
if (level < 1 || race >= MAX_RACES || class_ >= MAX_CLASSES)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
PlayerInfo const* pInfo = _playerInfo[race][class_];
|
PlayerInfo const* pInfo = Trinity::Containers::MapGetValuePtr(_playerInfo, {Races(race), Classes(class_)});
|
||||||
if (!pInfo)
|
if (!pInfo)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -3985,7 +3915,7 @@ void ObjectMgr::GetPlayerLevelInfo(uint32 race, uint32 class_, uint8 level, Play
|
|||||||
void ObjectMgr::BuildPlayerLevelInfo(uint8 race, uint8 _class, uint8 level, PlayerLevelInfo* info) const
|
void ObjectMgr::BuildPlayerLevelInfo(uint8 race, uint8 _class, uint8 level, PlayerLevelInfo* info) const
|
||||||
{
|
{
|
||||||
// base data (last known level)
|
// base data (last known level)
|
||||||
*info = _playerInfo[race][_class]->levelInfo[sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)-1];
|
*info = ASSERT_NOTNULL(Trinity::Containers::MapGetValuePtr(_playerInfo, { Races(race), Classes(_class) }))->levelInfo[sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL) - 1];
|
||||||
|
|
||||||
// if conversion from uint32 to uint8 causes unexpected behaviour, change lvl to uint32
|
// if conversion from uint32 to uint8 causes unexpected behaviour, change lvl to uint32
|
||||||
for (uint8 lvl = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)-1; lvl < level; ++lvl)
|
for (uint8 lvl = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)-1; lvl < level; ++lvl)
|
||||||
@@ -10111,10 +10041,7 @@ PlayerInfo const* ObjectMgr::GetPlayerInfo(uint32 race, uint32 class_) const
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
if (class_ >= MAX_CLASSES)
|
if (class_ >= MAX_CLASSES)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
PlayerInfo const* info = _playerInfo[race][class_];
|
return Trinity::Containers::MapGetValuePtr(_playerInfo, { Races(race), Classes(class_) });
|
||||||
if (!info)
|
|
||||||
return nullptr;
|
|
||||||
return info;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CreatureStaticFlagsOverride const* ObjectMgr::GetCreatureStaticFlagsOverride(ObjectGuid::LowType spawnId, Difficulty difficultyId) const
|
CreatureStaticFlagsOverride const* ObjectMgr::GetCreatureStaticFlagsOverride(ObjectGuid::LowType spawnId, Difficulty difficultyId) const
|
||||||
|
|||||||
@@ -688,15 +688,7 @@ struct PlayerCreateInfoAction
|
|||||||
|
|
||||||
typedef std::vector<PlayerCreateInfoAction> PlayerCreateInfoActions;
|
typedef std::vector<PlayerCreateInfoAction> PlayerCreateInfoActions;
|
||||||
|
|
||||||
struct PlayerCreateInfoSkill
|
typedef std::vector<SkillRaceClassInfoEntry const*> PlayerCreateInfoSkills;
|
||||||
{
|
|
||||||
PlayerCreateInfoSkill() : SkillId(0), Rank(0) { }
|
|
||||||
|
|
||||||
uint16 SkillId;
|
|
||||||
uint16 Rank;
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef std::vector<PlayerCreateInfoSkill> PlayerCreateInfoSkills;
|
|
||||||
|
|
||||||
// existence checked by displayId != 0
|
// existence checked by displayId != 0
|
||||||
struct PlayerInfo
|
struct PlayerInfo
|
||||||
@@ -716,7 +708,7 @@ struct PlayerInfo
|
|||||||
PlayerCreateInfoSkills skills;
|
PlayerCreateInfoSkills skills;
|
||||||
|
|
||||||
//[level-1] 0..MaxPlayerLevel-1
|
//[level-1] 0..MaxPlayerLevel-1
|
||||||
PlayerLevelInfo* levelInfo = nullptr;
|
std::unique_ptr<PlayerLevelInfo[]> levelInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -1734,7 +1726,7 @@ class TC_GAME_API ObjectMgr
|
|||||||
|
|
||||||
void BuildPlayerLevelInfo(uint8 race, uint8 class_, uint8 level, PlayerLevelInfo* plinfo) const;
|
void BuildPlayerLevelInfo(uint8 race, uint8 class_, uint8 level, PlayerLevelInfo* plinfo) const;
|
||||||
|
|
||||||
PlayerInfo* _playerInfo[MAX_RACES][MAX_CLASSES];
|
std::unordered_map<std::pair<Races, Classes>, std::unique_ptr<PlayerInfo>> _playerInfo;
|
||||||
|
|
||||||
typedef std::vector<uint32> PlayerXPperLevel; // [level]
|
typedef std::vector<uint32> PlayerXPperLevel; // [level]
|
||||||
PlayerXPperLevel _playerXPperLevel;
|
PlayerXPperLevel _playerXPperLevel;
|
||||||
|
|||||||
@@ -592,7 +592,7 @@ void GuildMgr::LoadGuildProfessionData()
|
|||||||
GuildProfession prof;
|
GuildProfession prof;
|
||||||
prof.SpellId = entry->Spell;
|
prof.SpellId = entry->Spell;
|
||||||
prof.UniqueBits = entry->UniqueBit;
|
prof.UniqueBits = entry->UniqueBit;
|
||||||
prof.AutoLearn = entry->AcquireMethod == SKILL_LINE_ABILITY_LEARNED_ON_SKILL_VALUE;
|
prof.AutoLearn = entry->GetAcquireMethod() == SkillLineAbilityAcquireMethod::AutomaticSkillRank;
|
||||||
GuildProfessionStore[skill->ID].push_back(prof);
|
GuildProfessionStore[skill->ID].push_back(prof);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -693,7 +693,7 @@ enum SpellAttr9
|
|||||||
SPELL_ATTR9_UNK3 = 0x00000008, // 3
|
SPELL_ATTR9_UNK3 = 0x00000008, // 3
|
||||||
SPELL_ATTR9_SPECIAL_DELAY_CALCULATION = 0x00000010, // 4
|
SPELL_ATTR9_SPECIAL_DELAY_CALCULATION = 0x00000010, // 4
|
||||||
SPELL_ATTR9_SUMMON_PLAYER_TOTEM = 0x00000020, // 5
|
SPELL_ATTR9_SUMMON_PLAYER_TOTEM = 0x00000020, // 5
|
||||||
SPELL_ATTR9_UNK6 = 0x00000040, // 6
|
SPELL_ATTR9_ITEM_CAST_GRANTS_SKILL_GAIN = 0x00000040, // 6 Item Cast Grants Skill Gain
|
||||||
SPELL_ATTR9_UNK7 = 0x00000080, // 7
|
SPELL_ATTR9_UNK7 = 0x00000080, // 7
|
||||||
SPELL_ATTR9_AIMED_SHOT = 0x00000100, // 8
|
SPELL_ATTR9_AIMED_SHOT = 0x00000100, // 8
|
||||||
SPELL_ATTR9_NOT_USABLE_IN_ARENA = 0x00000200, // 9 Cannot be used in arenas
|
SPELL_ATTR9_NOT_USABLE_IN_ARENA = 0x00000200, // 9 Cannot be used in arenas
|
||||||
|
|||||||
@@ -3732,6 +3732,11 @@ void Spell::_cast(bool skipCheck)
|
|||||||
player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_CAST_SPELL, m_spellInfo->Id);
|
player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_CAST_SPELL, m_spellInfo->Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spells that don't create items can have this attribute - handle here
|
||||||
|
if (m_CastItem && m_spellInfo->HasAttribute(SPELL_ATTR9_ITEM_CAST_GRANTS_SKILL_GAIN))
|
||||||
|
if (Player* playerCaster = m_caster->ToPlayer())
|
||||||
|
playerCaster->UpdateCraftSkill(m_spellInfo);
|
||||||
|
|
||||||
if (!(_triggeredCastFlags & TRIGGERED_IGNORE_POWER_AND_REAGENT_COST))
|
if (!(_triggeredCastFlags & TRIGGERED_IGNORE_POWER_AND_REAGENT_COST))
|
||||||
{
|
{
|
||||||
// Powers have to be taken before SendSpellGo
|
// Powers have to be taken before SendSpellGo
|
||||||
|
|||||||
@@ -1401,7 +1401,8 @@ void Spell::DoCreateItem(uint32 /*i*/, uint32 itemtype)
|
|||||||
|
|
||||||
// we succeeded in creating at least one item, so a levelup is possible
|
// we succeeded in creating at least one item, so a levelup is possible
|
||||||
if (bgType == 0)
|
if (bgType == 0)
|
||||||
player->UpdateCraftSkill(m_spellInfo);
|
if (!m_CastItem)
|
||||||
|
player->UpdateCraftSkill(m_spellInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -1437,7 +1438,8 @@ void Spell::EffectCreateItem2(SpellEffIndex effIndex)
|
|||||||
if (m_spellInfo->IsLootCrafting())
|
if (m_spellInfo->IsLootCrafting())
|
||||||
{
|
{
|
||||||
player->AutoStoreLoot(m_spellInfo->Id, LootTemplates_Spell, false);
|
player->AutoStoreLoot(m_spellInfo->Id, LootTemplates_Spell, false);
|
||||||
player->UpdateCraftSkill(m_spellInfo);
|
if (!m_CastItem)
|
||||||
|
player->UpdateCraftSkill(m_spellInfo);
|
||||||
}
|
}
|
||||||
else // If there's no random loot entries for this spell, pick the item associated with this spell
|
else // If there's no random loot entries for this spell, pick the item associated with this spell
|
||||||
{
|
{
|
||||||
@@ -2274,14 +2276,15 @@ void Spell::EffectLearnSkill(SpellEffIndex effIndex)
|
|||||||
if (effectHandleMode != SPELL_EFFECT_HANDLE_HIT_TARGET)
|
if (effectHandleMode != SPELL_EFFECT_HANDLE_HIT_TARGET)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (unitTarget->GetTypeId() != TYPEID_PLAYER)
|
Player* playerTarget = Object::ToPlayer(unitTarget);
|
||||||
|
if (!playerTarget)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (damage < 0)
|
if (damage < 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
uint32 skillid = m_spellInfo->Effects[effIndex].MiscValue;
|
uint32 skillid = m_spellInfo->Effects[effIndex].MiscValue;
|
||||||
SkillRaceClassInfoEntry const* rcEntry = sDBCManager.GetSkillRaceClassInfo(skillid, unitTarget->getRace(), unitTarget->getClass());
|
SkillRaceClassInfoEntry const* rcEntry = sDBCManager.GetSkillRaceClassInfo(skillid, playerTarget->getRace(), playerTarget->getClass());
|
||||||
if (!rcEntry)
|
if (!rcEntry)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -2289,8 +2292,13 @@ void Spell::EffectLearnSkill(SpellEffIndex effIndex)
|
|||||||
if (!tier)
|
if (!tier)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
uint16 skillval = unitTarget->ToPlayer()->GetPureSkillValue(skillid);
|
uint16 skillval = std::max<uint16>(1, playerTarget->GetPureSkillValue(skillid));
|
||||||
unitTarget->ToPlayer()->SetSkill(skillid, m_spellInfo->Effects[effIndex].CalcValue(), std::max<uint16>(skillval, 1), tier->Value[damage - 1]);
|
uint16 maxSkillVal = tier->Value[damage - 1];
|
||||||
|
|
||||||
|
if (rcEntry->Flags & SKILL_FLAG_ALWAYS_MAX_VALUE)
|
||||||
|
skillval = maxSkillVal;
|
||||||
|
|
||||||
|
playerTarget->SetSkill(skillid, damage, skillval, maxSkillVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Spell::EffectPlayMovie(SpellEffIndex effIndex)
|
void Spell::EffectPlayMovie(SpellEffIndex effIndex)
|
||||||
@@ -2347,7 +2355,7 @@ void Spell::EffectEnchantItemPerm(SpellEffIndex effIndex)
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// do not increase skill if vellum used
|
// do not increase skill if vellum used
|
||||||
if (!(m_CastItem && m_CastItem->GetTemplate()->GetFlags() & ITEM_FLAG_NO_REAGENT_COST))
|
if (!m_CastItem)
|
||||||
player->UpdateCraftSkill(m_spellInfo);
|
player->UpdateCraftSkill(m_spellInfo);
|
||||||
|
|
||||||
uint32 enchant_id = m_spellInfo->Effects[effIndex].MiscValue;
|
uint32 enchant_id = m_spellInfo->Effects[effIndex].MiscValue;
|
||||||
@@ -3932,7 +3940,9 @@ void Spell::EffectDisEnchant(SpellEffIndex /*effIndex*/)
|
|||||||
|
|
||||||
if (Player* caster = m_caster->ToPlayer())
|
if (Player* caster = m_caster->ToPlayer())
|
||||||
{
|
{
|
||||||
caster->UpdateCraftSkill(m_spellInfo);
|
if (!m_CastItem)
|
||||||
|
caster->UpdateCraftSkill(m_spellInfo);
|
||||||
|
|
||||||
caster->SendLoot(itemTarget->GetGUID(), LOOT_DISENCHANTING);
|
caster->SendLoot(itemTarget->GetGUID(), LOOT_DISENCHANTING);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4988,12 +4998,37 @@ void Spell::EffectMilling(SpellEffIndex /*effIndex*/)
|
|||||||
player->SendLoot(itemTarget->GetGUID(), LOOT_MILLING);
|
player->SendLoot(itemTarget->GetGUID(), LOOT_MILLING);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Spell::EffectSkill(SpellEffIndex /*effIndex*/)
|
void Spell::EffectSkill(SpellEffIndex effIndex)
|
||||||
{
|
{
|
||||||
if (effectHandleMode != SPELL_EFFECT_HANDLE_HIT)
|
if (effectHandleMode != SPELL_EFFECT_HANDLE_HIT)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
TC_LOG_DEBUG("spells", "WORLD: SkillEFFECT");
|
Player* playerTarget = Object::ToPlayer(GetCaster());
|
||||||
|
if (!playerTarget)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (damage < 1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
uint32 skillid = m_spellInfo->Effects[effIndex].MiscValue;
|
||||||
|
if (playerTarget->GetSkillStep(skillid) >= damage)
|
||||||
|
return;
|
||||||
|
|
||||||
|
SkillRaceClassInfoEntry const* rcEntry = sDBCManager.GetSkillRaceClassInfo(skillid, playerTarget->getRace(), playerTarget->getClass());
|
||||||
|
if (!rcEntry)
|
||||||
|
return;
|
||||||
|
|
||||||
|
SkillTiersEntry const* tier =sSkillTiersStore.AssertEntry(rcEntry->SkillTierID);
|
||||||
|
if (!tier)
|
||||||
|
return;
|
||||||
|
|
||||||
|
uint16 skillval = std::max<uint16>(1, playerTarget->GetPureSkillValue(skillid));
|
||||||
|
uint16 maxSkillVal = tier->Value[damage - 1];
|
||||||
|
|
||||||
|
if (rcEntry->Flags & SKILL_FLAG_ALWAYS_MAX_VALUE)
|
||||||
|
skillval = maxSkillVal;
|
||||||
|
|
||||||
|
playerTarget->SetSkill(skillid, damage, skillval, maxSkillVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* There is currently no need for this effect. We handle it in Battleground.cpp
|
/* There is currently no need for this effect. We handle it in Battleground.cpp
|
||||||
|
|||||||
@@ -1264,7 +1264,7 @@ bool SpellInfo::IsAbilityLearnedWithProfession() const
|
|||||||
for (SkillLineAbilityMap::const_iterator _spell_idx = bounds.first; _spell_idx != bounds.second; ++_spell_idx)
|
for (SkillLineAbilityMap::const_iterator _spell_idx = bounds.first; _spell_idx != bounds.second; ++_spell_idx)
|
||||||
{
|
{
|
||||||
SkillLineAbilityEntry const* pAbility = _spell_idx->second;
|
SkillLineAbilityEntry const* pAbility = _spell_idx->second;
|
||||||
if (!pAbility || pAbility->AcquireMethod != SKILL_LINE_ABILITY_LEARNED_ON_SKILL_VALUE)
|
if (!pAbility || pAbility->GetAcquireMethod() != SkillLineAbilityAcquireMethod::AutomaticSkillRank)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (pAbility->MinSkillLineRank > 0)
|
if (pAbility->MinSkillLineRank > 0)
|
||||||
|
|||||||
@@ -1024,11 +1024,8 @@ void SpellMgr::LoadSpellLearnSkills()
|
|||||||
case SPELL_EFFECT_SKILL:
|
case SPELL_EFFECT_SKILL:
|
||||||
dbc_node.skill = entry->Effects[i].MiscValue;
|
dbc_node.skill = entry->Effects[i].MiscValue;
|
||||||
dbc_node.step = entry->Effects[i].CalcValue();
|
dbc_node.step = entry->Effects[i].CalcValue();
|
||||||
if (dbc_node.skill != SKILL_RIDING)
|
dbc_node.value = 0;
|
||||||
dbc_node.value = 1;
|
dbc_node.maxvalue = 0;
|
||||||
else
|
|
||||||
dbc_node.value = dbc_node.step * 75;
|
|
||||||
dbc_node.maxvalue = dbc_node.step * 75;
|
|
||||||
break;
|
break;
|
||||||
case SPELL_EFFECT_DUAL_WIELD:
|
case SPELL_EFFECT_DUAL_WIELD:
|
||||||
dbc_node.skill = SKILL_DUAL_WIELD;
|
dbc_node.skill = SKILL_DUAL_WIELD;
|
||||||
@@ -2164,7 +2161,7 @@ void SpellMgr::LoadPetLevelupSpellMap()
|
|||||||
if (skillLine->SkillLine != creatureFamily->SkillLine[j])
|
if (skillLine->SkillLine != creatureFamily->SkillLine[j])
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (skillLine->AcquireMethod != SKILL_LINE_ABILITY_LEARNED_ON_SKILL_LEARN)
|
if (skillLine->GetAcquireMethod() != SkillLineAbilityAcquireMethod::AutomaticCharLevel)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
SpellInfo const* spell = GetSpellInfo(skillLine->Spell);
|
SpellInfo const* spell = GetSpellInfo(skillLine->Spell);
|
||||||
|
|||||||
Reference in new issue
Block a user