aboutsummaryrefslogtreecommitdiff
path: root/src/server/game/Entities
diff options
context:
space:
mode:
authorShauren <shauren.trinity@gmail.com>2022-12-16 22:44:55 +0100
committerShauren <shauren.trinity@gmail.com>2022-12-16 22:44:55 +0100
commit0cc5ab8372f19dad7412038d52dcd39db5e0e171 (patch)
tree99e621862e90fc9f073897445d4a8bdd4fbfa557 /src/server/game/Entities
parent9be60f240960f6538329b5e017f435c6237a89ea (diff)
Core/Players: Implemented new talent system
Diffstat (limited to 'src/server/game/Entities')
-rw-r--r--src/server/game/Entities/Player/Player.cpp791
-rw-r--r--src/server/game/Entities/Player/Player.h37
2 files changed, 789 insertions, 39 deletions
diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp
index ad5e1d2a632..178a053c5b3 100644
--- a/src/server/game/Entities/Player/Player.cpp
+++ b/src/server/game/Entities/Player/Player.cpp
@@ -118,6 +118,8 @@
#include "TerrainMgr.h"
#include "ToyPackets.h"
#include "TradeData.h"
+#include "TraitMgr.h"
+#include "TraitPacketsCommon.h"
#include "Transport.h"
#include "UpdateData.h"
#include "Util.h"
@@ -2802,7 +2804,7 @@ WorldLocation const* Player::GetStoredAuraTeleportLocation(uint32 spellId) const
return nullptr;
}
-bool Player::AddSpell(uint32 spellId, bool active, bool learning, bool dependent, bool disabled, bool loading /*= false*/, int32 fromSkill /*= 0*/)
+bool Player::AddSpell(uint32 spellId, bool active, bool learning, bool dependent, bool disabled, bool loading /*= false*/, int32 fromSkill /*= 0*/, Optional<int32> traitDefinitionId /*= {}*/)
{
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId, DIFFICULTY_NONE);
if (!spellInfo)
@@ -2882,6 +2884,15 @@ bool Player::AddSpell(uint32 spellId, bool active, bool learning, bool dependent
dependent_set = true;
}
+ if (itr->second.TraitDefinitionId != traitDefinitionId)
+ {
+ if (itr->second.TraitDefinitionId)
+ if (TraitDefinitionEntry const* traitDefinition = sTraitDefinitionStore.LookupEntry(*itr->second.TraitDefinitionId))
+ RemoveOverrideSpell(traitDefinition->OverridesSpellID, spellId);
+
+ itr->second.TraitDefinitionId = traitDefinitionId;
+ }
+
// update active state for known spell
if (itr->second.active != active && itr->second.state != PLAYERSPELL_REMOVED && !itr->second.disabled)
{
@@ -2963,6 +2974,8 @@ bool Player::AddSpell(uint32 spellId, bool active, bool learning, bool dependent
newspell.active = active;
newspell.dependent = dependent;
newspell.disabled = disabled;
+ if (traitDefinitionId)
+ newspell.TraitDefinitionId = *traitDefinitionId;
// 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())
@@ -3011,26 +3024,66 @@ bool Player::AddSpell(uint32 spellId, bool active, bool learning, bool dependent
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(SPELL_ATTR0_CU_IS_TALENT) && spellInfo->HasEffect(SPELL_EFFECT_LEARN_SPELL))
- {
// ignore stance requirement for talent learn spell (stance set for spell only for client spell description show)
- CastSpell(this, spellId, true);
- }
+ castSpell = true;
// also cast passive spells (including all talents without SPELL_EFFECT_LEARN_SPELL) with additional checks
else if (spellInfo->IsPassive())
- {
- if (HandlePassiveSpellLearn(spellInfo))
- CastSpell(this, spellId, true);
- }
+ castSpell = HandlePassiveSpellLearn(spellInfo);
else if (spellInfo->HasEffect(SPELL_EFFECT_SKILL_STEP))
+ castSpell = true;
+ else if (spellInfo->HasAttribute(SPELL_ATTR1_CAST_WHEN_LEARNED))
+ castSpell = true;
+
+ if (castSpell)
{
- CastSpell(this, spellId, true);
- return false;
+ CastSpellExtraArgs args;
+ args.SetTriggerFlags(TRIGGERED_FULL_MASK);
+
+ if (traitDefinitionId)
+ {
+ if (UF::TraitConfig const* traitConfig = GetTraitConfig(m_activePlayerData->ActiveCombatTraitConfigID))
+ {
+ int32 traitEntryIndex = traitConfig->Entries.FindIndexIf([traitDefinitionId](UF::TraitEntry const& traitEntry)
+ {
+ return sTraitNodeEntryStore.AssertEntry(traitEntry.TraitNodeEntryID)->TraitDefinitionID == traitDefinitionId;
+ });
+ int32 rank = 0;
+ if (traitEntryIndex >= 0)
+ rank = traitConfig->Entries[traitEntryIndex].Rank + traitConfig->Entries[traitEntryIndex].GrantedRanks;
+
+ if (rank > 0)
+ {
+ if (std::vector<TraitDefinitionEffectPointsEntry const*> const* traitDefinitionEffectPoints = TraitMgr::GetTraitDefinitionEffectPointModifiers(*traitDefinitionId))
+ {
+ for (TraitDefinitionEffectPointsEntry const* traitDefinitionEffectPoint : *traitDefinitionEffectPoints)
+ {
+ if (traitDefinitionEffectPoint->EffectIndex >= int32(spellInfo->GetEffects().size()))
+ continue;
+
+ float basePoints = sDB2Manager.GetCurveValueAt(traitDefinitionEffectPoint->CurveID, rank);
+ if (traitDefinitionEffectPoint->GetOperationType() == TraitPointsOperationType::Multiply)
+ basePoints *= spellInfo->GetEffect(SpellEffIndex(traitDefinitionEffectPoint->EffectIndex)).CalcBaseValue(this, nullptr, 0, -1);
+
+ args.AddSpellMod(SpellValueMod(SPELLVALUE_BASE_POINT0 + traitDefinitionEffectPoint->EffectIndex), basePoints);
+ }
+ }
+ }
+ }
+ }
+
+ CastSpell(this, spellId, args);
+ if (spellInfo->HasEffect(SPELL_EFFECT_SKILL_STEP))
+ return false;
}
- else if (spellInfo->HasAttribute(SPELL_ATTR1_CAST_WHEN_LEARNED))
- CastSpell(this, spellId, true);
+
+ if (traitDefinitionId)
+ if (TraitDefinitionEntry const* traitDefinition = sTraitDefinitionStore.LookupEntry(*traitDefinitionId))
+ AddOverrideSpell(traitDefinition->OverridesSpellID, spellId);
// update free primary prof.points (if any, can be none in case GM .learn prof. learning)
if (uint32 freeProfs = GetFreePrimaryProfessionPoints())
@@ -3168,14 +3221,14 @@ bool Player::HandlePassiveSpellLearn(SpellInfo const* spellInfo)
return need_cast && (!spellInfo->CasterAuraState || HasAuraState(AuraStateType(spellInfo->CasterAuraState)));
}
-void Player::LearnSpell(uint32 spell_id, bool dependent, int32 fromSkill /*= 0*/, bool suppressMessaging /*= false*/)
+void Player::LearnSpell(uint32 spell_id, bool dependent, int32 fromSkill /*= 0*/, bool suppressMessaging /*= false*/, Optional<int32> traitDefinitionId /*= {}*/)
{
PlayerSpellMap::iterator itr = m_spells.find(spell_id);
bool disabled = (itr != m_spells.end()) ? itr->second.disabled : false;
bool active = disabled ? itr->second.active : true;
- bool learning = AddSpell(spell_id, active, true, dependent, false, false, fromSkill);
+ bool learning = AddSpell(spell_id, active, true, dependent, false, false, fromSkill, traitDefinitionId);
// prevent duplicated entires in spell book, also not send if not in world (loading)
if (learning && IsInWorld())
@@ -3183,6 +3236,7 @@ void Player::LearnSpell(uint32 spell_id, bool dependent, int32 fromSkill /*= 0*/
WorldPackets::Spells::LearnedSpells learnedSpells;
WorldPackets::Spells::LearnedSpellInfo& learnedSpellInfo = learnedSpells.ClientLearnedSpellData.emplace_back();
learnedSpellInfo.SpellID = spell_id;
+ learnedSpellInfo.TraitDefinitionID = traitDefinitionId;
learnedSpells.SuppressMessaging = suppressMessaging;
SendDirectMessage(learnedSpells.Write());
}
@@ -4169,6 +4223,14 @@ void Player::DeleteFromDB(ObjectGuid playerguid, uint32 accountId, bool updateRe
Garrison::DeleteFromDB(guid, trans);
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_TRAIT_ENTRIES_BY_CHAR);
+ stmt->setUInt64(0, guid);
+ trans->Append(stmt);
+
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_TRAIT_CONFIGS_BY_CHAR);
+ stmt->setUInt64(0, guid);
+ trans->Append(stmt);
+
sCharacterCache->DeleteCharacterCacheEntry(playerguid, name);
break;
}
@@ -17589,6 +17651,9 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol
LearnDefaultSkills();
LearnCustomSpells();
+ _LoadTraits(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_TRAIT_CONFIGS),
+ holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_TRAIT_ENTRIES)); // must be after loading spells
+
// must be before inventory (some items required reputation check)
m_reputationMgr->LoadFromDB(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_REPUTATION));
@@ -17606,7 +17671,7 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol
// update items with duration and realtime
UpdateItemDuration(time_diff, true);
- _LoadActions(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_ACTIONS));
+ StartLoadingActionButtons();
// unread mails and next delivery time, actual mails not loaded
_LoadMail(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_MAILS),
@@ -19559,6 +19624,7 @@ void Player::SaveToDB(LoginDatabaseTransaction loginTransaction, CharacterDataba
_SaveMonthlyQuestStatus(trans);
_SaveGlyphs(trans);
_SaveTalents(trans);
+ _SaveTraits(trans);
_SaveSpells(trans);
GetSpellHistory()->SaveToDB<Player>(trans);
_SaveActions(trans);
@@ -19661,6 +19727,26 @@ void Player::_SaveCustomizations(CharacterDatabaseTransaction trans)
void Player::_SaveActions(CharacterDatabaseTransaction trans)
{
+ int32 traitConfigId = [&]() -> int32
+ {
+ UF::TraitConfig const* traitConfig = GetTraitConfig(m_activePlayerData->ActiveCombatTraitConfigID);
+ if (!traitConfig)
+ return 0;
+
+ int32 usedSavedTraitConfigIndex = m_activePlayerData->TraitConfigs.FindIndexIf([localIdent = *traitConfig->LocalIdentifier](UF::TraitConfig const& savedConfig)
+ {
+ return static_cast<TraitConfigType>(*savedConfig.Type) == TraitConfigType::Combat
+ && (static_cast<TraitCombatConfigFlags>(*savedConfig.CombatConfigFlags) & TraitCombatConfigFlags::ActiveForSpec) == TraitCombatConfigFlags::None
+ && (static_cast<TraitCombatConfigFlags>(*savedConfig.CombatConfigFlags) & TraitCombatConfigFlags::SharedActionBars) == TraitCombatConfigFlags::None
+ && savedConfig.LocalIdentifier == localIdent;
+ });
+
+ if (usedSavedTraitConfigIndex >= 0)
+ return m_activePlayerData->TraitConfigs[usedSavedTraitConfigIndex].ID;
+
+ return 0;
+ }();
+
CharacterDatabasePreparedStatement* stmt;
for (ActionButtonList::iterator itr = m_actionButtons.begin(); itr != m_actionButtons.end();)
@@ -19671,9 +19757,10 @@ void Player::_SaveActions(CharacterDatabaseTransaction trans)
stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_ACTION);
stmt->setUInt64(0, GetGUID().GetCounter());
stmt->setUInt8(1, GetActiveTalentGroup());
- stmt->setUInt8(2, itr->first);
- stmt->setUInt64(3, itr->second.GetAction());
- stmt->setUInt8(4, uint8(itr->second.GetType()));
+ stmt->setInt32(2, traitConfigId);
+ stmt->setUInt8(3, itr->first);
+ stmt->setUInt64(4, itr->second.GetAction());
+ stmt->setUInt8(5, uint8(itr->second.GetType()));
trans->Append(stmt);
itr->second.uState = ACTIONBUTTON_UNCHANGED;
@@ -19686,6 +19773,7 @@ void Player::_SaveActions(CharacterDatabaseTransaction trans)
stmt->setUInt64(2, GetGUID().GetCounter());
stmt->setUInt8(3, itr->first);
stmt->setUInt8(4, GetActiveTalentGroup());
+ stmt->setInt32(5, traitConfigId);
trans->Append(stmt);
itr->second.uState = ACTIONBUTTON_UNCHANGED;
@@ -19696,6 +19784,7 @@ void Player::_SaveActions(CharacterDatabaseTransaction trans)
stmt->setUInt64(0, GetGUID().GetCounter());
stmt->setUInt8(1, itr->first);
stmt->setUInt8(2, GetActiveTalentGroup());
+ stmt->setInt32(3, traitConfigId);
trans->Append(stmt);
m_actionButtons.erase(itr++);
@@ -26493,6 +26582,146 @@ void Player::_LoadPvpTalents(PreparedQueryResult result)
}
}
+void Player::_LoadTraits(PreparedQueryResult configsResult, PreparedQueryResult entriesResult)
+{
+ std::unordered_multimap<int32, WorldPackets::Traits::TraitEntry> traitEntriesByConfig;
+ if (entriesResult)
+ {
+ // 0 1, 2 3 4
+ // SELECT traitConfigId, traitNodeId, traitNodeEntryId, rank, grantedRanks FROM character_trait_entry WHERE guid = ?
+ do
+ {
+ Field* fields = entriesResult->Fetch();
+ WorldPackets::Traits::TraitEntry traitEntry;
+ traitEntry.TraitNodeID = fields[1].GetInt32();
+ traitEntry.TraitNodeEntryID = fields[2].GetInt32();
+ traitEntry.Rank = fields[3].GetInt32();
+ traitEntry.GrantedRanks = fields[4].GetInt32();
+
+ if (!TraitMgr::IsValidEntry(traitEntry))
+ continue;
+
+ traitEntriesByConfig.emplace(fields[0].GetInt32(), traitEntry);
+
+ } while (entriesResult->NextRow());
+ }
+
+ if (configsResult)
+ {
+ // 0 1 2 3 4 5 6 7
+ // SELECT traitConfigId, type, chrSpecializationId, combatConfigFlags, localIdentifier, skillLineId, traitSystemId, `name` FROM character_trait_config WHERE guid = ?
+ do
+ {
+ Field* fields = configsResult->Fetch();
+ WorldPackets::Traits::TraitConfig traitConfig;
+ traitConfig.ID = fields[0].GetInt32();
+ traitConfig.Type = static_cast<TraitConfigType>(fields[1].GetInt32());
+ switch (traitConfig.Type)
+ {
+ case TraitConfigType::Combat:
+ traitConfig.ChrSpecializationID = fields[2].GetInt32();
+ traitConfig.CombatConfigFlags = static_cast<TraitCombatConfigFlags>(fields[3].GetInt32());
+ traitConfig.LocalIdentifier = fields[4].GetInt32();
+ break;
+ case TraitConfigType::Profession:
+ traitConfig.SkillLineID = fields[5].GetInt32();
+ break;
+ case TraitConfigType::Generic:
+ traitConfig.TraitSystemID = fields[6].GetInt32();
+ break;
+ default:
+ break;
+ }
+
+ traitConfig.Name = fields[7].GetString();
+
+ for (auto&& [_, traitEntry] : Trinity::Containers::MapEqualRange(traitEntriesByConfig, traitConfig.ID))
+ traitConfig.Entries.emplace_back() = traitEntry;
+
+ if (TraitMgr::ValidateConfig(traitConfig, this) != TALENT_LEARN_OK)
+ {
+ traitConfig.Entries.clear();
+ for (UF::TraitEntry const& grantedEntry : TraitMgr::GetGrantedTraitEntriesForConfig(traitConfig, this))
+ traitConfig.Entries.emplace_back(grantedEntry);
+ }
+
+ AddTraitConfig(traitConfig);
+
+ } while (configsResult->NextRow());
+ }
+
+ auto hasConfigForSpec = [&](int32 specId)
+ {
+ return m_activePlayerData->TraitConfigs.FindIndexIf([=](UF::TraitConfig const& traitConfig)
+ {
+ return traitConfig.Type == AsUnderlyingType(TraitConfigType::Combat)
+ && traitConfig.ChrSpecializationID == specId
+ && traitConfig.CombatConfigFlags & AsUnderlyingType(TraitCombatConfigFlags::ActiveForSpec);
+ }) >= 0;
+ };
+
+ auto findFreeLocalIdentifier = [&](int32 specId)
+ {
+ int32 index = 1;
+ while (m_activePlayerData->TraitConfigs.FindIndexIf([specId, index](UF::TraitConfig const& traitConfig)
+ {
+ return traitConfig.Type == AsUnderlyingType(TraitConfigType::Combat)
+ && traitConfig.ChrSpecializationID == specId
+ && traitConfig.LocalIdentifier == index;
+ }) >= 0)
+ ++index;
+
+ return index;
+ };
+
+ for (uint32 i = 0; i < MAX_SPECIALIZATIONS - 1 /*initial spec doesnt get a config*/; ++i)
+ {
+ if (ChrSpecializationEntry const* spec = sDB2Manager.GetChrSpecializationByIndex(GetClass(), i))
+ {
+ if (hasConfigForSpec(spec->ID))
+ continue;
+
+ WorldPackets::Traits::TraitConfig traitConfig;
+ traitConfig.Type = TraitConfigType::Combat;
+ traitConfig.ChrSpecializationID = spec->ID;
+ traitConfig.CombatConfigFlags = TraitCombatConfigFlags::ActiveForSpec;
+ traitConfig.LocalIdentifier = findFreeLocalIdentifier(spec->ID);
+ traitConfig.Name = spec->Name[GetSession()->GetSessionDbcLocale()];
+
+ CreateTraitConfig(traitConfig);
+ }
+ }
+
+ int32 activeConfig = m_activePlayerData->TraitConfigs.FindIndexIf([&](UF::TraitConfig const& traitConfig)
+ {
+ return traitConfig.Type == AsUnderlyingType(TraitConfigType::Combat)
+ && traitConfig.ChrSpecializationID == int32(GetPrimarySpecialization())
+ && traitConfig.CombatConfigFlags & AsUnderlyingType(TraitCombatConfigFlags::ActiveForSpec);
+ });
+
+ if (activeConfig >= 0)
+ SetActiveCombatTraitConfigID(m_activePlayerData->TraitConfigs[activeConfig].ID);
+
+ for (UF::TraitConfig const& traitConfig : m_activePlayerData->TraitConfigs)
+ {
+ switch (static_cast<TraitConfigType>(*traitConfig.Type))
+ {
+ case TraitConfigType::Combat:
+ if (traitConfig.ID != int32(*m_activePlayerData->ActiveCombatTraitConfigID))
+ continue;
+ break;
+ case TraitConfigType::Profession:
+ if (!HasSkill(traitConfig.SkillLineID))
+ continue;
+ break;
+ default:
+ break;
+ }
+
+ ApplyTraitConfig(traitConfig.ID, true);
+ }
+}
+
void Player::_SaveTalents(CharacterDatabaseTransaction trans)
{
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_TALENT);
@@ -26537,6 +26766,97 @@ void Player::_SaveTalents(CharacterDatabaseTransaction trans)
}
}
+void Player::_SaveTraits(CharacterDatabaseTransaction trans)
+{
+ CharacterDatabasePreparedStatement* stmt = nullptr;
+ for (auto& [traitConfigId, state] : m_traitConfigStates)
+ {
+ switch (state)
+ {
+ case PLAYERSPELL_CHANGED:
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_TRAIT_ENTRIES);
+ stmt->setUInt64(0, GetGUID().GetCounter());
+ stmt->setInt32(1, traitConfigId);
+ trans->Append(stmt);
+
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_TRAIT_CONFIGS);
+ stmt->setUInt64(0, GetGUID().GetCounter());
+ stmt->setInt32(1, traitConfigId);
+ trans->Append(stmt);
+
+ if (UF::TraitConfig const* traitConfig = GetTraitConfig(traitConfigId))
+ {
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_TRAIT_CONFIGS);
+ stmt->setUInt64(0, GetGUID().GetCounter());
+ stmt->setInt32(1, traitConfig->ID);
+ stmt->setInt32(2, traitConfig->Type);
+ switch (static_cast<TraitConfigType>(*traitConfig->Type))
+ {
+ case TraitConfigType::Combat:
+ stmt->setInt32(3, traitConfig->ChrSpecializationID);
+ stmt->setInt32(4, traitConfig->CombatConfigFlags);
+ stmt->setInt32(5, traitConfig->LocalIdentifier);
+ stmt->setNull(6);
+ stmt->setNull(7);
+ break;
+ case TraitConfigType::Profession:
+ stmt->setNull(3);
+ stmt->setNull(4);
+ stmt->setNull(5);
+ stmt->setInt32(6, traitConfig->SkillLineID);
+ stmt->setNull(7);
+ break;
+ case TraitConfigType::Generic:
+ stmt->setNull(3);
+ stmt->setNull(4);
+ stmt->setNull(5);
+ stmt->setNull(6);
+ stmt->setInt32(7, traitConfig->TraitSystemID);
+ break;
+ default:
+ break;
+ }
+
+ stmt->setString(8, traitConfig->Name);
+ trans->Append(stmt);
+
+ for (UF::TraitEntry const& traitEntry : traitConfig->Entries)
+ {
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_TRAIT_ENTRIES);
+ stmt->setUInt64(0, GetGUID().GetCounter());
+ stmt->setInt32(1, traitConfig->ID);
+ stmt->setInt32(2, traitEntry.TraitNodeID);
+ stmt->setInt32(3, traitEntry.TraitNodeEntryID);
+ stmt->setInt32(4, traitEntry.Rank);
+ stmt->setInt32(5, traitEntry.GrantedRanks);
+ trans->Append(stmt);
+ }
+ }
+ break;
+ case PLAYERSPELL_REMOVED:
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_TRAIT_ENTRIES);
+ stmt->setUInt64(0, GetGUID().GetCounter());
+ stmt->setInt32(1, traitConfigId);
+ trans->Append(stmt);
+
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_TRAIT_CONFIGS);
+ stmt->setUInt64(0, GetGUID().GetCounter());
+ stmt->setInt32(1, traitConfigId);
+ trans->Append(stmt);
+
+ stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_ACTION_BY_TRAIT_CONFIG);
+ stmt->setUInt64(0, GetGUID().GetCounter());
+ stmt->setInt32(1, traitConfigId);
+ trans->Append(stmt);
+ break;
+ default:
+ break;
+ }
+ }
+
+ m_traitConfigStates.clear();
+}
+
void Player::ActivateTalentGroup(ChrSpecializationEntry const* spec)
{
if (GetActiveTalentGroup() == spec->OrderIndex)
@@ -26634,6 +26954,8 @@ void Player::ActivateTalentGroup(ChrSpecializationEntry const* spec)
RemoveOverrideSpell(talentInfo->OverridesSpellID, talentInfo->SpellID);
}
+ ApplyTraitConfig(m_activePlayerData->ActiveCombatTraitConfigID, false);
+
// Remove spec specific spells
RemoveSpecializationSpells();
@@ -26642,6 +26964,16 @@ void Player::ActivateTalentGroup(ChrSpecializationEntry const* spec)
SetActiveTalentGroup(spec->OrderIndex);
SetPrimarySpecialization(spec->ID);
+ int32 specTraitConfigIndex = m_activePlayerData->TraitConfigs.FindIndexIf([spec](UF::TraitConfig const& traitConfig)
+ {
+ return static_cast<TraitConfigType>(*traitConfig.Type) == TraitConfigType::Combat
+ && traitConfig.ChrSpecializationID == int32(spec->ID)
+ && (static_cast<TraitCombatConfigFlags>(*traitConfig.CombatConfigFlags) & TraitCombatConfigFlags::ActiveForSpec) != TraitCombatConfigFlags::None;
+ });
+ if (specTraitConfigIndex >= 0)
+ SetActiveCombatTraitConfigID(m_activePlayerData->TraitConfigs[specTraitConfigIndex].ID);
+ else
+ SetActiveCombatTraitConfigID(0);
for (uint32 talentId = 0; talentId < sTalentStore.GetNumRows(); ++talentId)
{
@@ -26684,24 +27016,11 @@ void Player::ActivateTalentGroup(ChrSpecializationEntry const* spec)
if (uint32 mastery = spec->MasterySpellID[i])
LearnSpell(mastery, true);
+ ApplyTraitConfig(m_activePlayerData->ActiveCombatTraitConfigID, true);
+
InitTalentForLevel();
- // load them asynchronously
- {
- CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_ACTIONS_SPEC);
- stmt->setUInt64(0, GetGUID().GetCounter());
- stmt->setUInt8(1, GetActiveTalentGroup());
-
- WorldSession* mySess = GetSession();
- mySess->GetQueryProcessor().AddCallback(CharacterDatabase.AsyncQuery(stmt)
- .WithPreparedCallback([mySess](PreparedQueryResult result)
- {
- // safe callback, we can't pass this pointer directly
- // in case player logs out before db response (player would be deleted in that case)
- if (Player* thisPlayer = mySess->GetPlayer())
- thisPlayer->LoadActions(result);
- }));
- }
+ StartLoadingActionButtons();
UpdateDisplayPower();
Powers pw = GetPowerType();
@@ -26759,6 +27078,48 @@ void Player::ActivateTalentGroup(ChrSpecializationEntry const* spec)
}
}
+void Player::StartLoadingActionButtons(std::function<void()>&& callback /*= nullptr*/)
+{
+ int32 traitConfigId = [&]() -> int32
+ {
+ UF::TraitConfig const* traitConfig = GetTraitConfig(m_activePlayerData->ActiveCombatTraitConfigID);
+ if (!traitConfig)
+ return 0;
+
+ int32 usedSavedTraitConfigIndex = m_activePlayerData->TraitConfigs.FindIndexIf([localIdent = *traitConfig->LocalIdentifier](UF::TraitConfig const& savedConfig)
+ {
+ return static_cast<TraitConfigType>(*savedConfig.Type) == TraitConfigType::Combat
+ && (static_cast<TraitCombatConfigFlags>(*savedConfig.CombatConfigFlags) & TraitCombatConfigFlags::ActiveForSpec) == TraitCombatConfigFlags::None
+ && (static_cast<TraitCombatConfigFlags>(*savedConfig.CombatConfigFlags) & TraitCombatConfigFlags::SharedActionBars) == TraitCombatConfigFlags::None
+ && savedConfig.LocalIdentifier == localIdent;
+ });
+
+ if (usedSavedTraitConfigIndex >= 0)
+ return m_activePlayerData->TraitConfigs[usedSavedTraitConfigIndex].ID;
+
+ return 0;
+ }();
+
+ // load them asynchronously
+ CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARACTER_ACTIONS_SPEC);
+ stmt->setUInt64(0, GetGUID().GetCounter());
+ stmt->setUInt8(1, GetActiveTalentGroup());
+ stmt->setInt32(2, traitConfigId);
+
+ WorldSession* mySess = GetSession();
+ mySess->GetQueryProcessor().AddCallback(CharacterDatabase.AsyncQuery(stmt)
+ .WithPreparedCallback([mySess, myGuid = GetGUID(), callback = std::move(callback)](PreparedQueryResult result)
+ {
+ // safe callback, we can't pass this pointer directly
+ // in case player logs out before db response (player would be deleted in that case)
+ if (Player* thisPlayer = mySess->GetPlayer(); thisPlayer && thisPlayer->GetGUID() == myGuid)
+ thisPlayer->LoadActions(result);
+
+ if (callback)
+ callback();
+ }));
+}
+
void Player::LoadActions(PreparedQueryResult result)
{
_LoadActions(result);
@@ -26766,6 +27127,368 @@ void Player::LoadActions(PreparedQueryResult result)
SendActionButtons(1);
}
+void Player::CreateTraitConfig(WorldPackets::Traits::TraitConfig& traitConfig)
+{
+ uint32 configId = TraitMgr::GenerateNewTraitConfigId();
+ auto hasConfigId = [&](int32 id)
+ {
+ return m_activePlayerData->TraitConfigs.FindIndexIf([id](UF::TraitConfig const& config) { return config.ID == id; }) >= 0;
+ };
+
+ while (hasConfigId(configId))
+ configId = TraitMgr::GenerateNewTraitConfigId();
+
+ traitConfig.ID = configId;
+
+ int32 traitConfigIndex = m_activePlayerData->TraitConfigs.size();
+ AddTraitConfig(traitConfig);
+
+ for (UF::TraitEntry const& grantedEntry : TraitMgr::GetGrantedTraitEntriesForConfig(traitConfig, this))
+ {
+ auto entryItr = std::find_if(traitConfig.Entries.begin(), traitConfig.Entries.end(),
+ [&](WorldPackets::Traits::TraitEntry const& entry) { return entry.TraitNodeID == grantedEntry.TraitNodeID && entry.TraitNodeEntryID == grantedEntry.TraitNodeEntryID; });
+
+ if (entryItr == traitConfig.Entries.end())
+ AddDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, traitConfigIndex)
+ .ModifyValue(&UF::TraitConfig::Entries)) = grantedEntry;
+ }
+
+ m_traitConfigStates[configId] = PLAYERSPELL_CHANGED;
+}
+
+void Player::AddTraitConfig(WorldPackets::Traits::TraitConfig const& traitConfig)
+{
+ auto setter = AddDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::TraitConfigs));
+ setter.ModifyValue(&UF::TraitConfig::ID).SetValue(traitConfig.ID);
+ setter.ModifyValue(&UF::TraitConfig::Name).SetValue(traitConfig.Name);
+ setter.ModifyValue(&UF::TraitConfig::Type).SetValue(AsUnderlyingType(traitConfig.Type));
+ setter.ModifyValue(&UF::TraitConfig::SkillLineID).SetValue(traitConfig.SkillLineID);;
+ setter.ModifyValue(&UF::TraitConfig::ChrSpecializationID).SetValue(traitConfig.ChrSpecializationID);
+ setter.ModifyValue(&UF::TraitConfig::CombatConfigFlags).SetValue(AsUnderlyingType(traitConfig.CombatConfigFlags));
+ setter.ModifyValue(&UF::TraitConfig::LocalIdentifier).SetValue(traitConfig.LocalIdentifier);
+ setter.ModifyValue(&UF::TraitConfig::TraitSystemID).SetValue(traitConfig.TraitSystemID);
+
+ for (WorldPackets::Traits::TraitEntry const& traitEntry : traitConfig.Entries)
+ {
+ UF::TraitEntry& newEntry = AddDynamicUpdateFieldValue(setter.ModifyValue(&UF::TraitConfig::Entries));
+ newEntry.TraitNodeID = traitEntry.TraitNodeID;
+ newEntry.TraitNodeEntryID = traitEntry.TraitNodeEntryID;
+ newEntry.Rank = traitEntry.Rank;
+ newEntry.GrantedRanks = traitEntry.GrantedRanks;
+ }
+}
+
+UF::TraitConfig const* Player::GetTraitConfig(int32 configId) const
+{
+ int32 index = m_activePlayerData->TraitConfigs.FindIndexIf([configId](UF::TraitConfig const& config) { return config.ID == configId; });
+ if (index < 0)
+ return nullptr;
+
+ return &m_activePlayerData->TraitConfigs[index];
+}
+
+void Player::UpdateTraitConfig(WorldPackets::Traits::TraitConfig&& newConfig, int32 savedConfigId, bool withCastTime)
+{
+ int32 index = m_activePlayerData->TraitConfigs.FindIndexIf([&](UF::TraitConfig const& config) { return config.ID == newConfig.ID; });
+ if (index < 0)
+ return;
+
+ if (withCastTime)
+ {
+ CastSpell(this, TraitMgr::COMMIT_COMBAT_TRAIT_CONFIG_CHANGES_SPELL_ID, CastSpellExtraArgs(SPELLVALUE_BASE_POINT0, savedConfigId).SetCustomArg(std::move(newConfig)));
+ return;
+ }
+
+ bool isActiveConfig = true;
+ bool loadActionButtons = false;
+ switch (TraitConfigType(*m_activePlayerData->TraitConfigs[index].Type))
+ {
+ case TraitConfigType::Combat:
+ isActiveConfig = newConfig.ID == int32(*m_activePlayerData->ActiveCombatTraitConfigID);
+ loadActionButtons = m_activePlayerData->TraitConfigs[index].LocalIdentifier != newConfig.LocalIdentifier;
+ break;
+ case TraitConfigType::Profession:
+ isActiveConfig = HasSkill(m_activePlayerData->TraitConfigs[index].SkillLineID);
+ break;
+ default:
+ break;
+ }
+
+ std::function<void()> finalizeTraitConfigUpdate = [=, newConfig = std::move(newConfig)]()
+ {
+ SetUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, index)
+ .ModifyValue(&UF::TraitConfig::LocalIdentifier), newConfig.LocalIdentifier);
+
+ ApplyTraitEntryChanges(newConfig.ID, newConfig, isActiveConfig, true);
+
+ if (savedConfigId)
+ ApplyTraitEntryChanges(savedConfigId, newConfig, false, false);
+
+ if (EnumFlag(newConfig.CombatConfigFlags).HasFlag(TraitCombatConfigFlags::StarterBuild))
+ SetTraitConfigUseStarterBuild(newConfig.ID, true);
+ };
+
+ if (loadActionButtons)
+ {
+ CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
+ _SaveActions(trans);
+ CharacterDatabase.CommitTransaction(trans);
+
+ StartLoadingActionButtons(std::move(finalizeTraitConfigUpdate));
+ }
+ else
+ finalizeTraitConfigUpdate();
+}
+
+void Player::ApplyTraitEntryChanges(int32 editedConfigId, WorldPackets::Traits::TraitConfig const& newConfig, bool applyTraits, bool consumeCurrencies)
+{
+ int32 editedIndex = m_activePlayerData->TraitConfigs.FindIndexIf([editedConfigId](UF::TraitConfig const& config) { return config.ID == editedConfigId; });
+ if (editedIndex < 0)
+ return;
+
+ auto makeTraitEntryFinder = [](int32 traitNodeId, int32 traitNodeEntryId)
+ {
+ return [=](auto const& ufEntry) { return ufEntry.TraitNodeID == traitNodeId && ufEntry.TraitNodeEntryID == traitNodeEntryId; };
+ };
+
+ UF::TraitConfig const& editedConfig = m_activePlayerData->TraitConfigs[editedIndex];
+
+ // remove traits not found in new config
+ std::set<int32, std::greater<>> entryIndicesToRemove;
+ for (int32 i = 0; i < int32(editedConfig.Entries.size()); ++i)
+ {
+ UF::TraitEntry const& oldEntry = editedConfig.Entries[i];
+ auto entryItr = std::find_if(newConfig.Entries.begin(), newConfig.Entries.end(), makeTraitEntryFinder(oldEntry.TraitNodeID, oldEntry.TraitNodeEntryID));
+ if (entryItr != newConfig.Entries.end())
+ continue;
+
+ if (applyTraits)
+ ApplyTraitEntry(oldEntry.TraitNodeEntryID, 0, 0, false);
+
+ entryIndicesToRemove.insert(i);
+ }
+
+ for (int32 indexToRemove : entryIndicesToRemove)
+ {
+ RemoveDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, editedIndex)
+ .ModifyValue(&UF::TraitConfig::Entries), indexToRemove);
+ }
+
+ std::vector<WorldPackets::Traits::TraitEntry> costEntries;
+
+ // apply new traits
+ for (std::size_t i = 0; i < newConfig.Entries.size(); ++i)
+ {
+ WorldPackets::Traits::TraitEntry const& newEntry = newConfig.Entries[i];
+ int32 oldEntryIndex = editedConfig.Entries.FindIndexIf(makeTraitEntryFinder(newEntry.TraitNodeID, newEntry.TraitNodeEntryID));
+ if (oldEntryIndex < 0)
+ {
+ if (consumeCurrencies)
+ costEntries.push_back(newEntry);
+
+ UF::TraitEntry& newUfEntry = AddDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, editedIndex)
+ .ModifyValue(&UF::TraitConfig::Entries));
+ newUfEntry.TraitNodeID = newEntry.TraitNodeID;
+ newUfEntry.TraitNodeEntryID = newEntry.TraitNodeEntryID;
+ newUfEntry.Rank = newEntry.Rank;
+ newUfEntry.GrantedRanks = newEntry.GrantedRanks;
+
+ if (applyTraits)
+ ApplyTraitEntry(newUfEntry.TraitNodeEntryID, newUfEntry.Rank, 0, true);
+ }
+ else if (newEntry.Rank != editedConfig.Entries[oldEntryIndex].Rank || newEntry.GrantedRanks != editedConfig.Entries[oldEntryIndex].GrantedRanks)
+ {
+ if (consumeCurrencies && newEntry.Rank > editedConfig.Entries[oldEntryIndex].Rank)
+ {
+ WorldPackets::Traits::TraitEntry& costEntry = costEntries.emplace_back(newEntry);
+ costEntry.Rank -= editedConfig.Entries[oldEntryIndex].Rank;
+ }
+
+ SetUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, editedIndex)
+ .ModifyValue(&UF::TraitConfig::Entries, oldEntryIndex)
+ .ModifyValue(&UF::TraitEntry::Rank), newEntry.Rank);
+
+ SetUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, editedIndex)
+ .ModifyValue(&UF::TraitConfig::Entries, oldEntryIndex)
+ .ModifyValue(&UF::TraitEntry::GrantedRanks), newEntry.GrantedRanks);
+
+ if (applyTraits)
+ ApplyTraitEntry(newEntry.TraitNodeEntryID, newEntry.Rank, newEntry.GrantedRanks, true);
+ }
+ }
+
+ if (consumeCurrencies)
+ {
+ std::map<int32, int32> currencies;
+ for (WorldPackets::Traits::TraitEntry const& costEntry : costEntries)
+ TraitMgr::FillSpentCurrenciesMap(costEntry, currencies);
+
+ for (auto [traitCurrencyId, amount] : currencies)
+ {
+ TraitCurrencyEntry const* traitCurrency = sTraitCurrencyStore.LookupEntry(traitCurrencyId);
+ if (!traitCurrency)
+ continue;
+
+ switch (traitCurrency->GetType())
+ {
+ case TraitCurrencyType::Gold:
+ ModifyMoney(-amount);
+ break;
+ case TraitCurrencyType::CurrencyTypesBased:
+ ModifyCurrency(traitCurrency->CurrencyTypesID, -amount);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ m_traitConfigStates[editedConfigId] = PLAYERSPELL_CHANGED;
+}
+
+void Player::RenameTraitConfig(int32 editedConfigId, std::string&& newName)
+{
+ int32 editedIndex = m_activePlayerData->TraitConfigs.FindIndexIf([editedConfigId](UF::TraitConfig const& traitConfig)
+ {
+ return traitConfig.ID == editedConfigId
+ && static_cast<TraitConfigType>(*traitConfig.Type) == TraitConfigType::Combat
+ && (static_cast<TraitCombatConfigFlags>(*traitConfig.CombatConfigFlags) & TraitCombatConfigFlags::ActiveForSpec) == TraitCombatConfigFlags::None;
+ });
+ if (editedIndex < 0)
+ return;
+
+ SetUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, editedIndex)
+ .ModifyValue(&UF::TraitConfig::Name), std::move(newName));
+
+ m_traitConfigStates[editedConfigId] = PLAYERSPELL_CHANGED;
+}
+
+void Player::DeleteTraitConfig(int32 deletedConfigId)
+{
+ int32 deletedIndex = m_activePlayerData->TraitConfigs.FindIndexIf([deletedConfigId](UF::TraitConfig const& traitConfig)
+ {
+ return traitConfig.ID == deletedConfigId
+ && static_cast<TraitConfigType>(*traitConfig.Type) == TraitConfigType::Combat
+ && (static_cast<TraitCombatConfigFlags>(*traitConfig.CombatConfigFlags) & TraitCombatConfigFlags::ActiveForSpec) == TraitCombatConfigFlags::None;
+ });
+ if (deletedIndex < 0)
+ return;
+
+ RemoveDynamicUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs), deletedIndex);
+
+ m_traitConfigStates[deletedConfigId] = PLAYERSPELL_REMOVED;
+}
+
+void Player::ApplyTraitConfig(int32 configId, bool apply)
+{
+ UF::TraitConfig const* traitConfig = GetTraitConfig(configId);
+ if (!traitConfig)
+ return;
+
+ for (UF::TraitEntry const& traitEntry : traitConfig->Entries)
+ ApplyTraitEntry(traitEntry.TraitNodeEntryID, traitEntry.Rank, traitEntry.GrantedRanks, apply);
+}
+
+void Player::ApplyTraitEntry(int32 traitNodeEntryId, int32 /*rank*/, int32 /*grantedRanks*/, bool apply)
+{
+ TraitNodeEntryEntry const* traitNodeEntry = sTraitNodeEntryStore.LookupEntry(traitNodeEntryId);
+ if (!traitNodeEntry)
+ return;
+
+ TraitDefinitionEntry const* traitDefinition = sTraitDefinitionStore.LookupEntry(traitNodeEntry->TraitDefinitionID);
+ if (!traitDefinition)
+ return;
+
+ if (traitDefinition->SpellID)
+ {
+ if (apply)
+ LearnSpell(traitDefinition->SpellID, true, 0, false, traitNodeEntry->TraitDefinitionID);
+ else
+ RemoveSpell(traitDefinition->SpellID);
+ }
+}
+
+void Player::SetTraitConfigUseStarterBuild(int32 traitConfigId, bool useStarterBuild)
+{
+ int32 configIndex = m_activePlayerData->TraitConfigs.FindIndexIf([traitConfigId](UF::TraitConfig const& traitConfig)
+ {
+ return traitConfig.ID == traitConfigId
+ && static_cast<TraitConfigType>(*traitConfig.Type) == TraitConfigType::Combat
+ && (static_cast<TraitCombatConfigFlags>(*traitConfig.CombatConfigFlags) & TraitCombatConfigFlags::ActiveForSpec) != TraitCombatConfigFlags::None;
+ });
+ if (configIndex < 0)
+ return;
+
+ bool currentlyUsesStarterBuild = EnumFlag(static_cast<TraitCombatConfigFlags>(*m_activePlayerData->TraitConfigs[configIndex].CombatConfigFlags)).HasFlag(TraitCombatConfigFlags::StarterBuild);
+ if (currentlyUsesStarterBuild == useStarterBuild)
+ return;
+
+ if (useStarterBuild)
+ SetUpdateFieldFlagValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, configIndex)
+ .ModifyValue(&UF::TraitConfig::CombatConfigFlags), AsUnderlyingType(TraitCombatConfigFlags::StarterBuild));
+ else
+ RemoveUpdateFieldFlagValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, configIndex)
+ .ModifyValue(&UF::TraitConfig::CombatConfigFlags), AsUnderlyingType(TraitCombatConfigFlags::StarterBuild));
+
+ m_traitConfigStates[traitConfigId] = PLAYERSPELL_CHANGED;
+}
+
+void Player::SetTraitConfigUseSharedActionBars(int32 traitConfigId, bool usesSharedActionBars, bool isLastSelectedSavedConfig)
+{
+ int32 configIndex = m_activePlayerData->TraitConfigs.FindIndexIf([traitConfigId](UF::TraitConfig const& traitConfig)
+ {
+ return traitConfig.ID == traitConfigId
+ && static_cast<TraitConfigType>(*traitConfig.Type) == TraitConfigType::Combat
+ && (static_cast<TraitCombatConfigFlags>(*traitConfig.CombatConfigFlags) & TraitCombatConfigFlags::ActiveForSpec) == TraitCombatConfigFlags::None;
+ });
+ if (configIndex < 0)
+ return;
+
+ bool currentlyUsesSharedActionBars = EnumFlag(static_cast<TraitCombatConfigFlags>(*m_activePlayerData->TraitConfigs[configIndex].CombatConfigFlags)).HasFlag(TraitCombatConfigFlags::SharedActionBars);
+ if (currentlyUsesSharedActionBars == usesSharedActionBars)
+ return;
+
+ if (usesSharedActionBars)
+ {
+ SetUpdateFieldFlagValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, configIndex)
+ .ModifyValue(&UF::TraitConfig::CombatConfigFlags), AsUnderlyingType(TraitCombatConfigFlags::SharedActionBars));
+
+ {
+ CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_ACTION_BY_TRAIT_CONFIG);
+ stmt->setUInt64(0, GetGUID().GetCounter());
+ stmt->setInt32(1, traitConfigId);
+ CharacterDatabase.Execute(stmt);
+ }
+
+ if (isLastSelectedSavedConfig)
+ StartLoadingActionButtons(); // load action buttons that were saved in shared mode
+ }
+ else
+ {
+ RemoveUpdateFieldFlagValue(m_values.ModifyValue(&Player::m_activePlayerData)
+ .ModifyValue(&UF::ActivePlayerData::TraitConfigs, configIndex)
+ .ModifyValue(&UF::TraitConfig::CombatConfigFlags), AsUnderlyingType(TraitCombatConfigFlags::SharedActionBars));
+
+ // trigger a save with traitConfigId
+ for (auto&& [_, button] : m_actionButtons)
+ if (button.uState != ACTIONBUTTON_DELETED)
+ button.uState = ACTIONBUTTON_NEW;
+ }
+
+ m_traitConfigStates[traitConfigId] = PLAYERSPELL_CHANGED;
+}
+
void Player::SetReputation(uint32 factionentry, int32 value)
{
GetReputationMgr().SetReputation(sFactionStore.LookupEntry(factionentry), value);
diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h
index ff4b3b1afa9..1c108fc4cdb 100644
--- a/src/server/game/Entities/Player/Player.h
+++ b/src/server/game/Entities/Player/Player.h
@@ -120,6 +120,12 @@ namespace WorldPackets
{
enum class UpdateCollisionHeightReason : uint8;
}
+
+ namespace Traits
+ {
+ struct TraitConfig;
+ struct TraitEntry;
+ }
}
TC_GAME_API uint32 GetBagSize(Bag const* bag);
@@ -178,10 +184,11 @@ enum PlayerSpellState : uint8
struct PlayerSpell
{
- PlayerSpellState state : 8;
+ PlayerSpellState state;
bool active : 1; // show in spellbook
bool dependent : 1; // learned as result another spell learn, skill grow, quest reward, etc
bool disabled : 1; // first rank has been learned in result talent learn but currently talent unlearned, save max learned ranks
+ Optional<int32> TraitDefinitionId;
};
struct StoredAuraTeleportLocation
@@ -859,7 +866,6 @@ enum PlayerLoginQueryIndex
PLAYER_LOGIN_QUERY_LOAD_AZERITE_MILESTONE_POWERS,
PLAYER_LOGIN_QUERY_LOAD_AZERITE_UNLOCKED_ESSENCES,
PLAYER_LOGIN_QUERY_LOAD_AZERITE_EMPOWERED,
- PLAYER_LOGIN_QUERY_LOAD_ACTIONS,
PLAYER_LOGIN_QUERY_LOAD_MAILS,
PLAYER_LOGIN_QUERY_LOAD_MAIL_ITEMS,
PLAYER_LOGIN_QUERY_LOAD_MAIL_ITEMS_ARTIFACT,
@@ -901,6 +907,8 @@ enum PlayerLoginQueryIndex
PLAYER_LOGIN_QUERY_LOAD_GARRISON_BUILDINGS,
PLAYER_LOGIN_QUERY_LOAD_GARRISON_FOLLOWERS,
PLAYER_LOGIN_QUERY_LOAD_GARRISON_FOLLOWER_ABILITIES,
+ PLAYER_LOGIN_QUERY_LOAD_TRAIT_ENTRIES,
+ PLAYER_LOGIN_QUERY_LOAD_TRAIT_CONFIGS,
MAX_PLAYER_LOGIN_QUERY
};
@@ -1059,7 +1067,7 @@ struct GroupUpdateCounter
int32 UpdateSequenceNumber;
};
-enum TalentLearnResult
+enum TalentLearnResult : int32
{
TALENT_LEARN_OK = 0,
TALENT_FAILED_UNKNOWN = 1,
@@ -1793,8 +1801,8 @@ class TC_GAME_API Player : public Unit, public GridObject<Player>
void SendProficiency(ItemClass itemClass, uint32 itemSubclassMask) const;
void SendKnownSpells();
void SendUnlearnSpells();
- bool AddSpell(uint32 spellId, bool active, bool learning, bool dependent, bool disabled, bool loading = false, int32 fromSkill = 0);
- void LearnSpell(uint32 spell_id, bool dependent, int32 fromSkill = 0, bool suppressMessaging = false);
+ bool AddSpell(uint32 spellId, bool active, bool learning, bool dependent, bool disabled, bool loading = false, int32 fromSkill = 0, Optional<int32> traitDefinitionId = {});
+ void LearnSpell(uint32 spell_id, bool dependent, int32 fromSkill = 0, bool suppressMessaging = false, Optional<int32> traitDefinitionId = {});
void RemoveSpell(uint32 spell_id, bool disabled = false, bool learn_low_rank = true, bool suppressMessaging = false);
void ResetSpells(bool myClassOnly = false);
void LearnCustomSpells();
@@ -1869,8 +1877,23 @@ class TC_GAME_API Player : public Unit, public GridObject<Player>
std::vector<uint32> const& GetGlyphs(uint8 spec) const { return _specializationInfo.Glyphs[spec]; }
std::vector<uint32>& GetGlyphs(uint8 spec) { return _specializationInfo.Glyphs[spec]; }
ActionButtonList const& GetActionButtons() const { return m_actionButtons; }
+ void StartLoadingActionButtons(std::function<void()>&& callback = nullptr);
void LoadActions(PreparedQueryResult result);
+ // Traits
+ void CreateTraitConfig(WorldPackets::Traits::TraitConfig& traitConfig);
+ void AddTraitConfig(WorldPackets::Traits::TraitConfig const& traitConfig);
+ UF::TraitConfig const* GetTraitConfig(int32 configId) const;
+ void UpdateTraitConfig(WorldPackets::Traits::TraitConfig&& newConfig, int32 savedConfigId, bool withCastTime);
+ void ApplyTraitEntryChanges(int32 editedConfigId, WorldPackets::Traits::TraitConfig const& newConfig, bool applyTraits, bool consumeCurrencies);
+ void RenameTraitConfig(int32 editedConfigId, std::string&& newName);
+ void DeleteTraitConfig(int32 deletedConfigId);
+ void ApplyTraitConfig(int32 configId, bool apply);
+ void ApplyTraitEntry(int32 traitNodeEntryId, int32 rank, int32 grantedRanks, bool apply);
+ void SetActiveCombatTraitConfigID(int32 traitConfigId) { SetUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::ActiveCombatTraitConfigID), traitConfigId); }
+ void SetTraitConfigUseStarterBuild(int32 traitConfigId, bool useStarterBuild);
+ void SetTraitConfigUseSharedActionBars(int32 traitConfigId, bool usesSharedActionBars, bool isLastSelectedSavedConfig);
+
uint32 GetFreePrimaryProfessionPoints() const { return m_activePlayerData->CharacterPoints; }
void SetFreePrimaryProfessions(uint16 profs) { SetUpdateFieldValue(m_values.ModifyValue(&Player::m_activePlayerData).ModifyValue(&UF::ActivePlayerData::CharacterPoints), profs); }
void InitPrimaryProfessions();
@@ -2908,6 +2931,7 @@ class TC_GAME_API Player : public Unit, public GridObject<Player>
void _LoadGlyphs(PreparedQueryResult result);
void _LoadTalents(PreparedQueryResult result);
void _LoadPvpTalents(PreparedQueryResult result);
+ void _LoadTraits(PreparedQueryResult configsResult, PreparedQueryResult entriesResult);
void _LoadInstanceTimeRestrictions(PreparedQueryResult result);
void _LoadPetStable(uint32 summonedPetNumber, PreparedQueryResult result);
void _LoadCurrency(PreparedQueryResult result);
@@ -2935,6 +2959,7 @@ class TC_GAME_API Player : public Unit, public GridObject<Player>
void _SaveBGData(CharacterDatabaseTransaction trans);
void _SaveGlyphs(CharacterDatabaseTransaction trans) const;
void _SaveTalents(CharacterDatabaseTransaction trans);
+ void _SaveTraits(CharacterDatabaseTransaction trans);
void _SaveStats(CharacterDatabaseTransaction trans) const;
void _SaveInstanceTimeRestrictions(CharacterDatabaseTransaction trans);
void _SaveCurrency(CharacterDatabaseTransaction trans);
@@ -3014,6 +3039,8 @@ class TC_GAME_API Player : public Unit, public GridObject<Player>
SpecializationInfo _specializationInfo;
+ std::unordered_map<int32, PlayerSpellState> m_traitConfigStates;
+
ActionButtonList m_actionButtons;
float m_auraBaseFlatMod[BASEMOD_END];