diff options
| author | Shauren <shauren.trinity@gmail.com> | 2024-05-01 22:26:53 +0200 |
|---|---|---|
| committer | Shauren <shauren.trinity@gmail.com> | 2024-05-01 22:26:53 +0200 |
| commit | a39d0db9ec64f6bf38716abaade5b7835f2db338 (patch) | |
| tree | 1e18f96b3600ab02d9cb8fc6288ac0cded23b6ce /src/server/game/Spells | |
| parent | cdc6719b8368907292f090978f6bdd6b8c73834d (diff) | |
Core/Spells: Implemented evoker empower spell mechanic
Diffstat (limited to 'src/server/game/Spells')
| -rw-r--r-- | src/server/game/Spells/Auras/SpellAuras.cpp | 10 | ||||
| -rw-r--r-- | src/server/game/Spells/Spell.cpp | 222 | ||||
| -rw-r--r-- | src/server/game/Spells/Spell.h | 21 | ||||
| -rw-r--r-- | src/server/game/Spells/SpellInfo.cpp | 8 | ||||
| -rw-r--r-- | src/server/game/Spells/SpellInfo.h | 2 | ||||
| -rw-r--r-- | src/server/game/Spells/SpellMgr.cpp | 112 | ||||
| -rw-r--r-- | src/server/game/Spells/SpellMgr.h | 2 | ||||
| -rw-r--r-- | src/server/game/Spells/SpellScript.h | 48 |
8 files changed, 340 insertions, 85 deletions
diff --git a/src/server/game/Spells/Auras/SpellAuras.cpp b/src/server/game/Spells/Auras/SpellAuras.cpp index 4d3f41764c7..c28730263bb 100644 --- a/src/server/game/Spells/Auras/SpellAuras.cpp +++ b/src/server/game/Spells/Auras/SpellAuras.cpp @@ -883,8 +883,14 @@ int32 Aura::CalcMaxDuration(Unit* caster) const maxDuration = -1; // IsPermanent() checks max duration (which we are supposed to calculate here) - if (maxDuration != -1 && modOwner) - modOwner->ApplySpellMod(spellInfo, SpellModOp::Duration, maxDuration); + if (maxDuration != -1) + { + if (modOwner) + modOwner->ApplySpellMod(spellInfo, SpellModOp::Duration, maxDuration); + + if (spellInfo->IsEmpowerSpell()) + maxDuration += SPELL_EMPOWER_HOLD_TIME_AT_MAX; + } return maxDuration; } diff --git a/src/server/game/Spells/Spell.cpp b/src/server/game/Spells/Spell.cpp index 22095eae8c7..fd354cdcfb1 100644 --- a/src/server/game/Spells/Spell.cpp +++ b/src/server/game/Spells/Spell.cpp @@ -511,6 +511,7 @@ m_spellValue(new SpellValue(m_spellInfo, caster)), _spellEvent(nullptr) m_referencedFromCurrentSpell = false; m_executedCurrently = false; m_delayStart = 0; + m_delayMoment = 0; m_delayAtDamageCount = 0; m_applyMultiplierMask = 0; @@ -601,6 +602,9 @@ m_spellValue(new SpellValue(m_spellInfo, caster)), _spellEvent(nullptr) m_channelTargetEffectMask = 0; + if (m_spellInfo->IsEmpowerSpell()) + m_empower = std::make_unique<EmpowerData>(); + // Determine if spell can be reflected back to the caster // Patch 1.2 notes: Spell Reflection no longer reflects abilities m_canReflect = caster->IsUnit() @@ -608,8 +612,6 @@ m_spellValue(new SpellValue(m_spellInfo, caster)), _spellEvent(nullptr) && !m_spellInfo->HasAttribute(SPELL_ATTR1_NO_REFLECTION) && !m_spellInfo->HasAttribute(SPELL_ATTR0_NO_IMMUNITIES) && !m_spellInfo->IsPassive(); - CleanupTargetList(); - for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) m_destTargets[i] = SpellDestination(*m_caster); } @@ -1531,7 +1533,7 @@ void Spell::SelectImplicitCasterDestTargets(SpellEffectInfo const& spellEffectIn if (liquidLevel <= ground) // When there is no liquid Map::GetWaterOrGroundLevel returns ground level { SendCastResult(SPELL_FAILED_NOT_HERE); - SendChannelUpdate(0); + SendChannelUpdate(0, SPELL_FAILED_NOT_HERE); finish(SPELL_FAILED_NOT_HERE); return; } @@ -1539,7 +1541,7 @@ void Spell::SelectImplicitCasterDestTargets(SpellEffectInfo const& spellEffectIn if (ground + 0.75 > liquidLevel) { SendCastResult(SPELL_FAILED_TOO_SHALLOW); - SendChannelUpdate(0); + SendChannelUpdate(0, SPELL_FAILED_TOO_SHALLOW); finish(SPELL_FAILED_TOO_SHALLOW); return; } @@ -3504,7 +3506,7 @@ SpellCastResult Spell::prepare(SpellCastTargets const& targets, AuraEffect const // a possible alternative sollution for those would be validating aura target on unit state change if (triggeredByAura && triggeredByAura->IsPeriodic() && !triggeredByAura->GetBase()->IsPassive()) { - SendChannelUpdate(0); + SendChannelUpdate(0, result); triggeredByAura->GetBase()->SetDuration(0); } @@ -3628,7 +3630,7 @@ void Spell::cancel() if (Unit* unit = m_caster->GetGUID() == targetInfo.TargetGUID ? m_caster->ToUnit() : ObjectAccessor::GetUnit(*m_caster, targetInfo.TargetGUID)) unit->RemoveOwnedAura(m_spellInfo->Id, m_originalCasterGUID, 0, AURA_REMOVE_BY_CANCEL); - SendChannelUpdate(0); + SendChannelUpdate(0, SPELL_FAILED_INTERRUPTED); SendInterrupted(0); SendCastResult(SPELL_FAILED_INTERRUPTED); @@ -3997,10 +3999,12 @@ void Spell::handle_immediate() if (m_spellInfo->IsChanneled()) { int32 duration = m_spellInfo->GetDuration(); - if (duration > 0 || m_spellValue->Duration) + if (duration > 0 || m_spellValue->Duration > 0) { if (!m_spellValue->Duration) { + int32 originalDuration = duration; + // First mod_duration then haste - see Missile Barrage // Apply duration mod if (Player* modOwner = m_caster->GetSpellModOwner()) @@ -4010,6 +4014,27 @@ void Spell::handle_immediate() // Apply haste mods m_caster->ModSpellDurationTime(m_spellInfo, duration, this); + + if (IsEmpowerSpell()) + { + float ratio = float(duration) / float(originalDuration); + m_empower->StageDurations.resize(m_spellInfo->EmpowerStageThresholds.size()); + Milliseconds totalExceptLastStage = 0ms; + for (std::size_t i = 0; i < m_empower->StageDurations.size() - 1; ++i) + { + m_empower->StageDurations[i] = Milliseconds(int64(m_spellInfo->EmpowerStageThresholds[i].count() * ratio)); + totalExceptLastStage += m_empower->StageDurations[i]; + } + + m_empower->StageDurations.back() = Milliseconds(duration) - totalExceptLastStage; + + if (Player const* playerCaster = m_caster->ToPlayer()) + m_empower->MinHoldTime = Milliseconds(int64(m_empower->StageDurations[0].count() * playerCaster->GetEmpowerMinHoldStagePercent())); + else + m_empower->MinHoldTime = m_empower->StageDurations[0]; + + duration += SPELL_EMPOWER_HOLD_TIME_AT_MAX; + } } else duration = *m_spellValue->Duration; @@ -4306,9 +4331,47 @@ void Spell::update(uint32 difftime) } } + if (IsEmpowerSpell()) + { + auto getCompletedEmpowerStages = [&]() -> int32 + { + Milliseconds passed(m_channeledDuration - m_timer); + for (std::size_t i = 0; i < m_empower->StageDurations.size(); ++i) + { + passed -= m_empower->StageDurations[i]; + if (passed < 0ms) + return i; + } + + return m_empower->StageDurations.size(); + }; + + int32 completedStages = getCompletedEmpowerStages(); + if (completedStages != m_empower->CompletedStages) + { + WorldPackets::Spells::SpellEmpowerSetStage empowerSetStage; + empowerSetStage.CastID = m_castId; + empowerSetStage.CasterGUID = m_caster->GetGUID(); + empowerSetStage.Stage = m_empower->CompletedStages; + m_caster->SendMessageToSet(empowerSetStage.Write(), true); + + m_empower->CompletedStages = completedStages; + m_caster->ToUnit()->SetSpellEmpowerStage(completedStages); + + CallScriptEmpowerStageCompletedHandlers(completedStages); + } + + if (CanReleaseEmpowerSpell()) + { + m_empower->IsReleased = true; + m_timer = 0; + CallScriptEmpowerCompletedHandlers(m_empower->CompletedStages); + } + } + if (m_timer == 0) { - SendChannelUpdate(0); + SendChannelUpdate(0, SPELL_CAST_OK); finish(); // We call the hook here instead of in Spell::finish because we only want to call it for completed channeling. Everything else is handled by interrupts @@ -4371,6 +4434,9 @@ void Spell::finish(SpellCastResult result) if (WorldPackets::Traits::TraitConfig const* traitConfig = std::any_cast<WorldPackets::Traits::TraitConfig>(&m_customArg)) m_caster->ToPlayer()->SendDirectMessage(WorldPackets::Traits::TraitConfigCommitFailed(traitConfig->ID).Write()); + if (IsEmpowerSpell()) + unitCaster->GetSpellHistory()->ResetCooldown(m_spellInfo->Id, true); + return; } @@ -4400,6 +4466,13 @@ void Spell::finish(SpellCastResult result) // Stop Attack for some spells if (m_spellInfo->HasAttribute(SPELL_ATTR0_CANCELS_AUTO_ATTACK_COMBAT)) unitCaster->AttackStop(); + + if (IsEmpowerSpell()) + { + // Empower spells trigger gcd at the end of cast instead of at start + if (SpellInfo const* gcd = sSpellMgr->GetSpellInfo(SPELL_EMPOWER_HARDCODED_GCD, DIFFICULTY_NONE)) + unitCaster->GetSpellHistory()->AddGlobalCooldown(gcd, Milliseconds(gcd->StartRecoveryTime)); + } } template<class T> @@ -5210,7 +5283,7 @@ void Spell::SendInterrupted(uint8 result) m_caster->SendMessageToSet(failedPacket.Write(), true); } -void Spell::SendChannelUpdate(uint32 time) +void Spell::SendChannelUpdate(uint32 time, Optional<SpellCastResult> result) { // GameObjects don't channel Unit* unitCaster = m_caster->ToUnit(); @@ -5222,12 +5295,31 @@ void Spell::SendChannelUpdate(uint32 time) unitCaster->ClearChannelObjects(); unitCaster->SetChannelSpellId(0); unitCaster->SetChannelVisual({}); + unitCaster->SetSpellEmpowerStage(-1); } - WorldPackets::Spells::SpellChannelUpdate spellChannelUpdate; - spellChannelUpdate.CasterGUID = unitCaster->GetGUID(); - spellChannelUpdate.TimeRemaining = time; - unitCaster->SendMessageToSet(spellChannelUpdate.Write(), true); + if (IsEmpowerSpell()) + { + WorldPackets::Spells::SpellEmpowerUpdate spellEmpowerUpdate; + spellEmpowerUpdate.CastID = m_castId; + spellEmpowerUpdate.CasterGUID = unitCaster->GetGUID(); + spellEmpowerUpdate.TimeRemaining = Milliseconds(time); + if (time > 0) + spellEmpowerUpdate.StageDurations.assign(m_empower->StageDurations.begin(), m_empower->StageDurations.end()); + else if (result && result != SPELL_CAST_OK) + spellEmpowerUpdate.Status = 1; + else + spellEmpowerUpdate.Status = 4; + + unitCaster->SendMessageToSet(spellEmpowerUpdate.Write(), true); + } + else + { + WorldPackets::Spells::SpellChannelUpdate spellChannelUpdate; + spellChannelUpdate.CasterGUID = unitCaster->GetGUID(); + spellChannelUpdate.TimeRemaining = time; + unitCaster->SendMessageToSet(spellChannelUpdate.Write(), true); + } } void Spell::SendChannelStart(uint32 duration) @@ -5285,32 +5377,57 @@ void Spell::SendChannelStart(uint32 duration) unitCaster->SetChannelSpellId(m_spellInfo->Id); unitCaster->SetChannelVisual(m_SpellVisual); - WorldPackets::Spells::SpellChannelStart spellChannelStart; - spellChannelStart.CasterGUID = unitCaster->GetGUID(); - spellChannelStart.SpellID = m_spellInfo->Id; - spellChannelStart.Visual = m_SpellVisual; - spellChannelStart.ChannelDuration = duration; + auto setImmunitiesAndHealPrediction = [&](Optional<WorldPackets::Spells::SpellChannelStartInterruptImmunities>& interruptImmunities, Optional<WorldPackets::Spells::SpellTargetedHealPrediction>& healPrediction) + { + uint32 schoolImmunityMask = unitCaster->GetSchoolImmunityMask(); + uint32 mechanicImmunityMask = unitCaster->GetMechanicImmunityMask(); - uint32 schoolImmunityMask = unitCaster->GetSchoolImmunityMask(); - uint32 mechanicImmunityMask = unitCaster->GetMechanicImmunityMask(); + if (schoolImmunityMask || mechanicImmunityMask) + { + interruptImmunities.emplace(); + interruptImmunities->SchoolImmunities = schoolImmunityMask; + interruptImmunities->Immunities = mechanicImmunityMask; + } - if (schoolImmunityMask || mechanicImmunityMask) - { - spellChannelStart.InterruptImmunities.emplace(); - spellChannelStart.InterruptImmunities->SchoolImmunities = schoolImmunityMask; - spellChannelStart.InterruptImmunities->Immunities = mechanicImmunityMask; - } + if (m_spellInfo->HasAttribute(SPELL_ATTR8_HEAL_PREDICTION) && m_caster->IsUnit()) + { + healPrediction.emplace(); + if (unitCaster->m_unitData->ChannelObjects.size() == 1 && unitCaster->m_unitData->ChannelObjects[0].IsUnit()) + healPrediction->TargetGUID = unitCaster->m_unitData->ChannelObjects[0]; - if (m_spellInfo->HasAttribute(SPELL_ATTR8_HEAL_PREDICTION) && m_caster->IsUnit()) + UpdateSpellHealPrediction(healPrediction->Predict, true); + } + }; + + if (IsEmpowerSpell()) { - WorldPackets::Spells::SpellTargetedHealPrediction& healPrediction = spellChannelStart.HealPrediction.emplace(); - if (unitCaster->m_unitData->ChannelObjects.size() == 1 && unitCaster->m_unitData->ChannelObjects[0].IsUnit()) - healPrediction.TargetGUID = unitCaster->m_unitData->ChannelObjects[0]; + unitCaster->SetSpellEmpowerStage(0); - UpdateSpellHealPrediction(healPrediction.Predict, true); + WorldPackets::Spells::SpellEmpowerStart spellEmpowerStart; + spellEmpowerStart.CastID = m_castId; + spellEmpowerStart.CasterGUID = unitCaster->GetGUID(); + spellEmpowerStart.SpellID = m_spellInfo->Id; + spellEmpowerStart.Visual = m_SpellVisual; + spellEmpowerStart.EmpowerDuration = std::reduce(m_empower->StageDurations.begin(), m_empower->StageDurations.end()); + spellEmpowerStart.MinHoldTime = m_empower->MinHoldTime; + spellEmpowerStart.HoldAtMaxTime = Milliseconds(SPELL_EMPOWER_HOLD_TIME_AT_MAX); + spellEmpowerStart.Targets.assign(unitCaster->m_unitData->ChannelObjects.begin(), unitCaster->m_unitData->ChannelObjects.end()); + spellEmpowerStart.StageDurations.assign(m_empower->StageDurations.begin(), m_empower->StageDurations.end()); + setImmunitiesAndHealPrediction(spellEmpowerStart.InterruptImmunities, spellEmpowerStart.HealPrediction); + + unitCaster->SendMessageToSet(spellEmpowerStart.Write(), true); } + else + { + WorldPackets::Spells::SpellChannelStart spellChannelStart; + spellChannelStart.CasterGUID = unitCaster->GetGUID(); + spellChannelStart.SpellID = m_spellInfo->Id; + spellChannelStart.Visual = m_SpellVisual; + spellChannelStart.ChannelDuration = duration; + setImmunitiesAndHealPrediction(spellChannelStart.InterruptImmunities, spellChannelStart.HealPrediction); - unitCaster->SendMessageToSet(spellChannelStart.Write(), true); + unitCaster->SendMessageToSet(spellChannelStart.Write(), true); + } } void Spell::SendResurrectRequest(Player* target) @@ -8209,6 +8326,23 @@ bool Spell::IsPositive() const return m_spellInfo->IsPositive() && (!m_triggeredByAuraSpell || m_triggeredByAuraSpell->IsPositive()); } +void Spell::SetEmpowerReleasedByClient(bool release) +{ + m_empower->IsReleasedByClient = release; +} + +bool Spell::CanReleaseEmpowerSpell() const +{ + if (m_empower->IsReleased) + return false; + + if (!m_empower->IsReleasedByClient && m_timer) + return false; + + Milliseconds passedTime(m_channeledDuration - m_timer); + return passedTime >= m_empower->MinHoldTime; +} + bool Spell::IsNeedSendToClient() const { return m_SpellVisual.SpellXSpellVisualID || m_SpellVisual.ScriptVisualID || m_spellInfo->IsChanneled() || @@ -8897,6 +9031,30 @@ void Spell::CallScriptOnResistAbsorbCalculateHandlers(DamageInfo const& damageIn } } +void Spell::CallScriptEmpowerStageCompletedHandlers(int32 completedStagesCount) +{ + for (SpellScript* script : m_loadedScripts) + { + script->_PrepareScriptCall(SPELL_SCRIPT_HOOK_EMPOWER_STAGE_COMPLETED); + for (SpellScript::EmpowerStageCompletedHandler const& empowerStageCompleted : script->OnEmpowerStageCompleted) + empowerStageCompleted.Call(script, completedStagesCount); + + script->_FinishScriptCall(); + } +} + +void Spell::CallScriptEmpowerCompletedHandlers(int32 completedStagesCount) +{ + for (SpellScript* script : m_loadedScripts) + { + script->_PrepareScriptCall(SPELL_SCRIPT_HOOK_EMPOWER_COMPLETED); + for (SpellScript::EmpowerStageCompletedHandler const& empowerStageCompleted : script->OnEmpowerCompleted) + empowerStageCompleted.Call(script, completedStagesCount); + + script->_FinishScriptCall(); + } +} + bool Spell::CheckScriptEffectImplicitTargets(uint32 effIndex, uint32 effIndexToCheck) { // Skip if there are not any script diff --git a/src/server/game/Spells/Spell.h b/src/server/game/Spells/Spell.h index 2da7e1d7e8a..8f25f5844dd 100644 --- a/src/server/game/Spells/Spell.h +++ b/src/server/game/Spells/Spell.h @@ -20,6 +20,7 @@ #include "ConditionMgr.h" #include "DBCEnums.h" +#include "Duration.h" #include "ModelIgnoreFlags.h" #include "ObjectGuid.h" #include "Optional.h" @@ -77,6 +78,8 @@ enum class WorldObjectSpellAreaTargetSearchReason; #define MAX_SPELL_RANGE_TOLERANCE 3.0f #define TRAJECTORY_MISSILE_SIZE 3.0f #define AOE_DAMAGE_TARGET_CAP SI64LIT(20) +#define SPELL_EMPOWER_HOLD_TIME_AT_MAX (1 * IN_MILLISECONDS) +#define SPELL_EMPOWER_HARDCODED_GCD 359115u enum SpellCastFlags : uint32 { @@ -552,7 +555,7 @@ class TC_GAME_API Spell void ExecuteLogEffectResurrect(SpellEffectName effect, Unit* target); void SendSpellInterruptLog(Unit* victim, uint32 spellId); void SendInterrupted(uint8 result); - void SendChannelUpdate(uint32 time); + void SendChannelUpdate(uint32 time, Optional<SpellCastResult> result = {}); void SendChannelStart(uint32 duration); void SendResurrectRequest(Player* target); @@ -625,6 +628,10 @@ class TC_GAME_API Spell bool IsAutoActionResetSpell() const; bool IsPositive() const; + bool IsEmpowerSpell() const { return m_empower != nullptr; } + void SetEmpowerReleasedByClient(bool release); + bool CanReleaseEmpowerSpell() const; + bool IsTriggeredByAura(SpellInfo const* auraSpellInfo) const { return (auraSpellInfo == m_triggeredByAuraSpell); } int32 GetProcChainLength() const { return m_procChainLength; } @@ -709,6 +716,16 @@ class TC_GAME_API Spell bool m_autoRepeat; uint8 m_runesState; + struct EmpowerData + { + Milliseconds MinHoldTime = 0ms; + std::vector<Milliseconds> StageDurations; + int32 CompletedStages = 0; + bool IsReleasedByClient = false; + bool IsReleased = false; + }; + std::unique_ptr<EmpowerData> m_empower; + uint8 m_delayAtDamageCount; bool IsDelayableNoMore() { @@ -885,6 +902,8 @@ class TC_GAME_API Spell void CallScriptObjectAreaTargetSelectHandlers(std::list<WorldObject*>& targets, SpellEffIndex effIndex, SpellImplicitTargetInfo const& targetType); void CallScriptObjectTargetSelectHandlers(WorldObject*& target, SpellEffIndex effIndex, SpellImplicitTargetInfo const& targetType); void CallScriptDestinationTargetSelectHandlers(SpellDestination& target, SpellEffIndex effIndex, SpellImplicitTargetInfo const& targetType); + void CallScriptEmpowerStageCompletedHandlers(int32 completedStagesCount); + void CallScriptEmpowerCompletedHandlers(int32 completedStagesCount); bool CheckScriptEffectImplicitTargets(uint32 effIndex, uint32 effIndexToCheck); std::vector<SpellScript*> m_loadedScripts; diff --git a/src/server/game/Spells/SpellInfo.cpp b/src/server/game/Spells/SpellInfo.cpp index 472ecf7fb95..acd095dd005 100644 --- a/src/server/game/Spells/SpellInfo.cpp +++ b/src/server/game/Spells/SpellInfo.cpp @@ -1290,6 +1290,9 @@ SpellInfo::SpellInfo(SpellNameEntry const* spellName, ::Difficulty difficulty, S CooldownAuraSpellId = _cooldowns->AuraSpellID; } + // SpellEmpowerStageEntry + std::ranges::transform(data.EmpowerStages, std::back_inserter(EmpowerStageThresholds), [](SpellEmpowerStageEntry const* stage) { return Milliseconds(stage->DurationMs); }); + // SpellEquippedItemsEntry if (SpellEquippedItemsEntry const* _equipped = data.EquippedItems) { @@ -1743,6 +1746,11 @@ bool SpellInfo::IsAutoRepeatRangedSpell() const return HasAttribute(SPELL_ATTR2_AUTO_REPEAT); } +bool SpellInfo::IsEmpowerSpell() const +{ + return !EmpowerStageThresholds.empty(); +} + bool SpellInfo::HasInitialAggro() const { return !(HasAttribute(SPELL_ATTR1_NO_THREAT) || HasAttribute(SPELL_ATTR2_NO_INITIAL_THREAT) || HasAttribute(SPELL_ATTR4_NO_HARMFUL_THREAT)); diff --git a/src/server/game/Spells/SpellInfo.h b/src/server/game/Spells/SpellInfo.h index 88f33903c01..c66da5ccad4 100644 --- a/src/server/game/Spells/SpellInfo.h +++ b/src/server/game/Spells/SpellInfo.h @@ -411,6 +411,7 @@ class TC_GAME_API SpellInfo uint32 SchoolMask = 0; uint32 ChargeCategoryId = 0; std::unordered_set<uint32> Labels; + std::vector<Milliseconds> EmpowerStageThresholds; // SpellScalingEntry struct ScalingInfo @@ -501,6 +502,7 @@ class TC_GAME_API SpellInfo bool IsNextMeleeSwingSpell() const; bool IsRangedWeaponSpell() const; bool IsAutoRepeatRangedSpell() const; + bool IsEmpowerSpell() const; bool HasInitialAggro() const; bool HasHitDelay() const; diff --git a/src/server/game/Spells/SpellMgr.cpp b/src/server/game/Spells/SpellMgr.cpp index 3c5641f5fb9..d689481661d 100644 --- a/src/server/game/Spells/SpellMgr.cpp +++ b/src/server/game/Spells/SpellMgr.cpp @@ -2548,6 +2548,18 @@ void SpellMgr::LoadSpellInfoStore() for (SpellCooldownsEntry const* cooldowns : sSpellCooldownsStore) loadData[{ cooldowns->SpellID, Difficulty(cooldowns->DifficultyID) }].Cooldowns = cooldowns; + for (SpellEmpowerStageEntry const* empowerStage : sSpellEmpowerStageStore) + { + if (SpellEmpowerEntry const* empower = sSpellEmpowerStore.LookupEntry(empowerStage->SpellEmpowerID)) + { + std::vector<SpellEmpowerStageEntry const*>& empowerStages = loadData[{empower->SpellID, DIFFICULTY_NONE}].EmpowerStages; + + auto where = std::ranges::lower_bound(empowerStages, empowerStage->Stage, std::ranges::less(), &SpellEmpowerStageEntry::Stage); + + empowerStages.insert(where, empowerStage); + } + } + for (SpellEquippedItemsEntry const* equippedItems : sSpellEquippedItemsStore) loadData[{ equippedItems->SpellID, DIFFICULTY_NONE }].EquippedItems = equippedItems; @@ -2598,98 +2610,98 @@ void SpellMgr::LoadSpellInfoStore() { SpellVisualVector& visuals = loadData[{ visual->SpellID, Difficulty(visual->DifficultyID) }].Visuals; - auto where = std::lower_bound(visuals.begin(), visuals.end(), visual, [](SpellXSpellVisualEntry const* first, SpellXSpellVisualEntry const* second) - { - return first->CasterPlayerConditionID > second->CasterPlayerConditionID; - }); + auto where = std::ranges::lower_bound(visuals, visual->CasterPlayerConditionID, std::ranges::greater(), &SpellXSpellVisualEntry::CasterPlayerConditionID); // sorted with unconditional visuals being last visuals.insert(where, visual); } - for (std::pair<std::pair<uint32, Difficulty> const, SpellInfoLoadHelper>& data : loadData) + for (auto& [key, data] : loadData) { - SpellNameEntry const* spellNameEntry = sSpellNameStore.LookupEntry(data.first.first); + SpellNameEntry const* spellNameEntry = sSpellNameStore.LookupEntry(key.first); if (!spellNameEntry) continue; // fill blanks - if (DifficultyEntry const* difficultyEntry = sDifficultyStore.LookupEntry(data.first.second)) + if (DifficultyEntry const* difficultyEntry = sDifficultyStore.LookupEntry(key.second)) { do { - if (SpellInfoLoadHelper const* fallbackData = Trinity::Containers::MapGetValuePtr(loadData, { data.first.first, Difficulty(difficultyEntry->FallbackDifficultyID) })) + if (SpellInfoLoadHelper const* fallbackData = Trinity::Containers::MapGetValuePtr(loadData, { key.first, Difficulty(difficultyEntry->FallbackDifficultyID) })) { - if (!data.second.AuraOptions) - data.second.AuraOptions = fallbackData->AuraOptions; + if (!data.AuraOptions) + data.AuraOptions = fallbackData->AuraOptions; + + if (!data.AuraRestrictions) + data.AuraRestrictions = fallbackData->AuraRestrictions; - if (!data.second.AuraRestrictions) - data.second.AuraRestrictions = fallbackData->AuraRestrictions; + if (!data.CastingRequirements) + data.CastingRequirements = fallbackData->CastingRequirements; - if (!data.second.CastingRequirements) - data.second.CastingRequirements = fallbackData->CastingRequirements; + if (!data.Categories) + data.Categories = fallbackData->Categories; - if (!data.second.Categories) - data.second.Categories = fallbackData->Categories; + if (!data.ClassOptions) + data.ClassOptions = fallbackData->ClassOptions; - if (!data.second.ClassOptions) - data.second.ClassOptions = fallbackData->ClassOptions; + if (!data.Cooldowns) + data.Cooldowns = fallbackData->Cooldowns; - if (!data.second.Cooldowns) - data.second.Cooldowns = fallbackData->Cooldowns; + for (std::size_t i = 0; i < data.Effects.size(); ++i) + if (!data.Effects[i]) + data.Effects[i] = fallbackData->Effects[i]; - for (std::size_t i = 0; i < data.second.Effects.size(); ++i) - if (!data.second.Effects[i]) - data.second.Effects[i] = fallbackData->Effects[i]; + if (data.EmpowerStages.empty()) + data.EmpowerStages = fallbackData->EmpowerStages; - if (!data.second.EquippedItems) - data.second.EquippedItems = fallbackData->EquippedItems; + if (!data.EquippedItems) + data.EquippedItems = fallbackData->EquippedItems; - if (!data.second.Interrupts) - data.second.Interrupts = fallbackData->Interrupts; + if (!data.Interrupts) + data.Interrupts = fallbackData->Interrupts; - if (data.second.Labels.empty()) - data.second.Labels = fallbackData->Labels; + if (data.Labels.empty()) + data.Labels = fallbackData->Labels; - if (!data.second.Levels) - data.second.Levels = fallbackData->Levels; + if (!data.Levels) + data.Levels = fallbackData->Levels; - if (!data.second.Misc) - data.second.Misc = fallbackData->Misc; + if (!data.Misc) + data.Misc = fallbackData->Misc; for (std::size_t i = 0; i < fallbackData->Powers.size(); ++i) - if (!data.second.Powers[i]) - data.second.Powers[i] = fallbackData->Powers[i]; + if (!data.Powers[i]) + data.Powers[i] = fallbackData->Powers[i]; - if (!data.second.Reagents) - data.second.Reagents = fallbackData->Reagents; + if (!data.Reagents) + data.Reagents = fallbackData->Reagents; - if (data.second.ReagentsCurrency.empty()) - data.second.ReagentsCurrency = fallbackData->ReagentsCurrency; + if (data.ReagentsCurrency.empty()) + data.ReagentsCurrency = fallbackData->ReagentsCurrency; - if (!data.second.Scaling) - data.second.Scaling = fallbackData->Scaling; + if (!data.Scaling) + data.Scaling = fallbackData->Scaling; - if (!data.second.Shapeshift) - data.second.Shapeshift = fallbackData->Shapeshift; + if (!data.Shapeshift) + data.Shapeshift = fallbackData->Shapeshift; - if (!data.second.TargetRestrictions) - data.second.TargetRestrictions = fallbackData->TargetRestrictions; + if (!data.TargetRestrictions) + data.TargetRestrictions = fallbackData->TargetRestrictions; - if (!data.second.Totems) - data.second.Totems = fallbackData->Totems; + if (!data.Totems) + data.Totems = fallbackData->Totems; // visuals fall back only to first difficulty that defines any visual // they do not stack all difficulties in fallback chain - if (data.second.Visuals.empty()) - data.second.Visuals = fallbackData->Visuals; + if (data.Visuals.empty()) + data.Visuals = fallbackData->Visuals; } difficultyEntry = sDifficultyStore.LookupEntry(difficultyEntry->FallbackDifficultyID); } while (difficultyEntry); } - mSpellInfoMap.emplace(spellNameEntry, data.first.second, data.second); + mSpellInfoMap.emplace(spellNameEntry, key.second, data); } TC_LOG_INFO("server.loading", ">> Loaded SpellInfo store in {} ms", GetMSTimeDiffToNow(oldMSTime)); diff --git a/src/server/game/Spells/SpellMgr.h b/src/server/game/Spells/SpellMgr.h index 1771e04676b..e8a6c2a8b1b 100644 --- a/src/server/game/Spells/SpellMgr.h +++ b/src/server/game/Spells/SpellMgr.h @@ -51,6 +51,7 @@ struct SpellCategoriesEntry; struct SpellClassOptionsEntry; struct SpellCooldownsEntry; struct SpellEffectEntry; +struct SpellEmpowerStageEntry; struct SpellEquippedItemsEntry; struct SpellInterruptsEntry; struct SpellLabelEntry; @@ -667,6 +668,7 @@ struct SpellInfoLoadHelper SpellClassOptionsEntry const* ClassOptions = nullptr; SpellCooldownsEntry const* Cooldowns = nullptr; std::array<SpellEffectEntry const*, MAX_SPELL_EFFECTS> Effects = { }; + std::vector<SpellEmpowerStageEntry const*> EmpowerStages; SpellEquippedItemsEntry const* EquippedItems = nullptr; SpellInterruptsEntry const* Interrupts = nullptr; std::vector<SpellLabelEntry const*> Labels; diff --git a/src/server/game/Spells/SpellScript.h b/src/server/game/Spells/SpellScript.h index 9c28dd10d7d..91933d7dc0d 100644 --- a/src/server/game/Spells/SpellScript.h +++ b/src/server/game/Spells/SpellScript.h @@ -238,6 +238,8 @@ enum SpellScriptHookType SPELL_SCRIPT_HOOK_CALC_HEALING, SPELL_SCRIPT_HOOK_ON_PRECAST, SPELL_SCRIPT_HOOK_CALC_CAST_TIME, + SPELL_SCRIPT_HOOK_EMPOWER_STAGE_COMPLETED, + SPELL_SCRIPT_HOOK_EMPOWER_COMPLETED, }; #define HOOK_SPELL_HIT_START SPELL_SCRIPT_HOOK_EFFECT_HIT @@ -785,6 +787,40 @@ public: SafeWrapperType _safeWrapper; }; + class EmpowerStageCompletedHandler final + { + public: + using EmpowerStageFnType = void(SpellScript::*)(int32); + + using SafeWrapperType = void(*)(SpellScript* spellScript, EmpowerStageFnType callImpl, int32 completedStagesCount); + + template<typename ScriptFunc> + explicit EmpowerStageCompletedHandler(ScriptFunc handler) + { + using ScriptClass = GetScriptClass_t<ScriptFunc>; + + static_assert(sizeof(EmpowerStageFnType) >= sizeof(ScriptFunc)); + static_assert(alignof(EmpowerStageFnType) >= alignof(ScriptFunc)); + + static_assert(std::is_invocable_r_v<void, ScriptFunc, ScriptClass, int32>, + "EmpowerStageCompleted/EmpowerCompleted signature must be \"void HandleEmpowerStageCompleted(int32 completedStagesCount)\""); + + _callImpl = reinterpret_cast<EmpowerStageFnType>(handler); + _safeWrapper = [](SpellScript* spellScript, EmpowerStageFnType callImpl, int32 completedStagesCount) -> void + { + return (static_cast<ScriptClass*>(spellScript)->*reinterpret_cast<ScriptFunc>(callImpl))(completedStagesCount); + }; + } + + void Call(SpellScript* spellScript, int32 completedStagesCount) const + { + return _safeWrapper(spellScript, _callImpl, completedStagesCount); + } + private: + EmpowerStageFnType _callImpl; + SafeWrapperType _safeWrapper; + }; + // left for custom compatibility only, DO NOT USE #define PrepareSpellScript(CLASSNAME) @@ -888,6 +924,16 @@ public: HookList<OnCalculateResistAbsorbHandler> OnCalculateResistAbsorb; #define SpellOnResistAbsorbCalculateFn(F) OnCalculateResistAbsorbHandler(&F) + // example: OnEmpowerStageCompleted += SpellOnEmpowerStageCompletedFn(class::function); + // where function is void function(int32 completedStages) + HookList<EmpowerStageCompletedHandler> OnEmpowerStageCompleted; + #define SpellOnEmpowerStageCompletedFn(F) EmpowerStageCompletedHandler(&F) + + // example: OnEmpowerCompleted += SpellOnEmpowerCompletedFn(class::function); + // where function is void function(int32 completedStages) + HookList<EmpowerStageCompletedHandler> OnEmpowerCompleted; + #define SpellOnEmpowerCompletedFn(F) EmpowerStageCompletedHandler(&F) + // hooks are executed in following order, at specified event of spell: // 1. OnPrecast - executed during spell preparation (before cast bar starts) // 2. BeforeCast - executed when spell preparation is finished (when cast bar becomes full) before cast is handled @@ -908,6 +954,8 @@ public: // 14. OnEffectHitTarget - executed just before specified effect handler call - called for each target from spell target map // 15. OnHit - executed just before spell deals damage and procs auras - when spell hits target - called for each target from spell target map // 16. AfterHit - executed just after spell finishes all it's jobs for target - called for each target from spell target map + // 17. OnEmpowerStageCompleted - executed when empowered spell completes each stage + // 18. OnEmpowerCompleted - executed when empowered spell is released // this hook is only executed after a successful dispel of any aura // OnEffectSuccessfulDispel - executed just after effect successfully dispelled aura(s) |
