aboutsummaryrefslogtreecommitdiff
path: root/src/server/game/Spells
diff options
context:
space:
mode:
authorShauren <shauren.trinity@gmail.com>2024-05-01 22:26:53 +0200
committerShauren <shauren.trinity@gmail.com>2024-05-01 22:26:53 +0200
commita39d0db9ec64f6bf38716abaade5b7835f2db338 (patch)
tree1e18f96b3600ab02d9cb8fc6288ac0cded23b6ce /src/server/game/Spells
parentcdc6719b8368907292f090978f6bdd6b8c73834d (diff)
Core/Spells: Implemented evoker empower spell mechanic
Diffstat (limited to 'src/server/game/Spells')
-rw-r--r--src/server/game/Spells/Auras/SpellAuras.cpp10
-rw-r--r--src/server/game/Spells/Spell.cpp222
-rw-r--r--src/server/game/Spells/Spell.h21
-rw-r--r--src/server/game/Spells/SpellInfo.cpp8
-rw-r--r--src/server/game/Spells/SpellInfo.h2
-rw-r--r--src/server/game/Spells/SpellMgr.cpp112
-rw-r--r--src/server/game/Spells/SpellMgr.h2
-rw-r--r--src/server/game/Spells/SpellScript.h48
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)