aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--sql/updates/hotfixes/master/2024_05_01_00_hotfixes.sql24
-rw-r--r--sql/updates/world/master/2024_05_01_00_world.sql6
-rw-r--r--src/server/database/Database/Implementation/HotfixDatabase.cpp9
-rw-r--r--src/server/database/Database/Implementation/HotfixDatabase.h6
-rw-r--r--src/server/game/DataStores/DB2LoadInfo.h25
-rw-r--r--src/server/game/DataStores/DB2Stores.cpp4
-rw-r--r--src/server/game/DataStores/DB2Stores.h2
-rw-r--r--src/server/game/DataStores/DB2Structure.h15
-rw-r--r--src/server/game/Entities/Creature/Creature.cpp1
-rw-r--r--src/server/game/Entities/Player/Player.cpp2
-rw-r--r--src/server/game/Entities/Player/Player.h4
-rw-r--r--src/server/game/Entities/Unit/Unit.cpp2
-rw-r--r--src/server/game/Entities/Unit/Unit.h2
-rw-r--r--src/server/game/Handlers/SpellHandler.cpp33
-rw-r--r--src/server/game/Server/Packets/SpellPackets.cpp70
-rw-r--r--src/server/game/Server/Packets/SpellPackets.h76
-rw-r--r--src/server/game/Server/Protocol/Opcodes.cpp12
-rw-r--r--src/server/game/Server/WorldSession.h8
-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
-rw-r--r--src/server/scripts/Spells/spell_evoker.cpp99
27 files changed, 732 insertions, 93 deletions
diff --git a/sql/updates/hotfixes/master/2024_05_01_00_hotfixes.sql b/sql/updates/hotfixes/master/2024_05_01_00_hotfixes.sql
new file mode 100644
index 00000000000..10b253c4fcb
--- /dev/null
+++ b/sql/updates/hotfixes/master/2024_05_01_00_hotfixes.sql
@@ -0,0 +1,24 @@
+--
+-- Table structure for table `spell_empower`
+--
+DROP TABLE IF EXISTS `spell_empower`;
+CREATE TABLE `spell_empower` (
+ `ID` int unsigned NOT NULL DEFAULT '0',
+ `SpellID` int NOT NULL DEFAULT '0',
+ `Unused1000` int NOT NULL DEFAULT '0',
+ `VerifiedBuild` int NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+--
+-- Table structure for table `spell_empower_stage`
+--
+DROP TABLE IF EXISTS `spell_empower_stage`;
+CREATE TABLE `spell_empower_stage` (
+ `ID` int unsigned NOT NULL DEFAULT '0',
+ `Stage` int NOT NULL DEFAULT '0',
+ `DurationMs` int NOT NULL DEFAULT '0',
+ `SpellEmpowerID` int unsigned NOT NULL DEFAULT '0',
+ `VerifiedBuild` int NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`,`VerifiedBuild`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/sql/updates/world/master/2024_05_01_00_world.sql b/sql/updates/world/master/2024_05_01_00_world.sql
new file mode 100644
index 00000000000..b1e4744f4de
--- /dev/null
+++ b/sql/updates/world/master/2024_05_01_00_world.sql
@@ -0,0 +1,6 @@
+DELETE FROM `spell_script_names` WHERE `ScriptName` IN ('spell_evo_fire_breath','spell_evo_fire_breath_damage','spell_evo_scouring_flame');
+INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES
+(357208,'spell_evo_fire_breath'),
+(382266,'spell_evo_fire_breath'),
+(357209,'spell_evo_fire_breath_damage'),
+(357209,'spell_evo_scouring_flame');
diff --git a/src/server/database/Database/Implementation/HotfixDatabase.cpp b/src/server/database/Database/Implementation/HotfixDatabase.cpp
index 4e5bec49b13..ed55602c4b7 100644
--- a/src/server/database/Database/Implementation/HotfixDatabase.cpp
+++ b/src/server/database/Database/Implementation/HotfixDatabase.cpp
@@ -1507,6 +1507,15 @@ void HotfixDatabaseConnection::DoPrepareStatements()
"ImplicitTarget2, SpellID FROM spell_effect WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
PREPARE_MAX_ID_STMT(HOTFIX_SEL_SPELL_EFFECT, "SELECT MAX(ID) + 1 FROM spell_effect", CONNECTION_SYNCH);
+ // SpellEmpower.db2
+ PrepareStatement(HOTFIX_SEL_SPELL_EMPOWER, "SELECT ID, SpellID, Unused1000 FROM spell_empower WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_SPELL_EMPOWER, "SELECT MAX(ID) + 1 FROM spell_empower", CONNECTION_SYNCH);
+
+ // SpellEmpowerStage.db2
+ PrepareStatement(HOTFIX_SEL_SPELL_EMPOWER_STAGE, "SELECT ID, Stage, DurationMs, SpellEmpowerID FROM spell_empower_stage"
+ " WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
+ PREPARE_MAX_ID_STMT(HOTFIX_SEL_SPELL_EMPOWER_STAGE, "SELECT MAX(ID) + 1 FROM spell_empower_stage", CONNECTION_SYNCH);
+
// SpellEquippedItems.db2
PrepareStatement(HOTFIX_SEL_SPELL_EQUIPPED_ITEMS, "SELECT ID, SpellID, EquippedItemClass, EquippedItemInvTypes, EquippedItemSubclass"
" FROM spell_equipped_items WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
diff --git a/src/server/database/Database/Implementation/HotfixDatabase.h b/src/server/database/Database/Implementation/HotfixDatabase.h
index 7b1b6804571..d1b3f022312 100644
--- a/src/server/database/Database/Implementation/HotfixDatabase.h
+++ b/src/server/database/Database/Implementation/HotfixDatabase.h
@@ -873,6 +873,12 @@ enum HotfixDatabaseStatements : uint32
HOTFIX_SEL_SPELL_EFFECT,
HOTFIX_SEL_SPELL_EFFECT_MAX_ID,
+ HOTFIX_SEL_SPELL_EMPOWER,
+ HOTFIX_SEL_SPELL_EMPOWER_MAX_ID,
+
+ HOTFIX_SEL_SPELL_EMPOWER_STAGE,
+ HOTFIX_SEL_SPELL_EMPOWER_STAGE_MAX_ID,
+
HOTFIX_SEL_SPELL_EQUIPPED_ITEMS,
HOTFIX_SEL_SPELL_EQUIPPED_ITEMS_MAX_ID,
diff --git a/src/server/game/DataStores/DB2LoadInfo.h b/src/server/game/DataStores/DB2LoadInfo.h
index b363192dfdf..614ac4f19f2 100644
--- a/src/server/game/DataStores/DB2LoadInfo.h
+++ b/src/server/game/DataStores/DB2LoadInfo.h
@@ -4970,6 +4970,31 @@ struct SpellEffectLoadInfo
static constexpr DB2LoadInfo Instance{ Fields, 36, &SpellEffectMeta::Instance, HOTFIX_SEL_SPELL_EFFECT };
};
+struct SpellEmpowerLoadInfo
+{
+ static constexpr DB2FieldMeta Fields[3] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "SpellID" },
+ { true, FT_INT, "Unused1000" },
+ };
+
+ static constexpr DB2LoadInfo Instance{ Fields, 3, &SpellEmpowerMeta::Instance, HOTFIX_SEL_SPELL_EMPOWER };
+};
+
+struct SpellEmpowerStageLoadInfo
+{
+ static constexpr DB2FieldMeta Fields[4] =
+ {
+ { false, FT_INT, "ID" },
+ { true, FT_INT, "Stage" },
+ { true, FT_INT, "DurationMs" },
+ { false, FT_INT, "SpellEmpowerID" },
+ };
+
+ static constexpr DB2LoadInfo Instance{ Fields, 4, &SpellEmpowerStageMeta::Instance, HOTFIX_SEL_SPELL_EMPOWER_STAGE };
+};
+
struct SpellEquippedItemsLoadInfo
{
static constexpr DB2FieldMeta Fields[5] =
diff --git a/src/server/game/DataStores/DB2Stores.cpp b/src/server/game/DataStores/DB2Stores.cpp
index 3f3cf755a5e..d0f2bbcb08f 100644
--- a/src/server/game/DataStores/DB2Stores.cpp
+++ b/src/server/game/DataStores/DB2Stores.cpp
@@ -297,6 +297,8 @@ DB2Storage<SpellCooldownsEntry> sSpellCooldownsStore("SpellCoold
DB2Storage<SpellDurationEntry> sSpellDurationStore("SpellDuration.db2", &SpellDurationLoadInfo::Instance);
DB2Storage<SpellEffectEntry> sSpellEffectStore("SpellEffect.db2", &SpellEffectLoadInfo::Instance);
DB2Storage<SpellEquippedItemsEntry> sSpellEquippedItemsStore("SpellEquippedItems.db2", &SpellEquippedItemsLoadInfo::Instance);
+DB2Storage<SpellEmpowerEntry> sSpellEmpowerStore("SpellEmpower.db2", &SpellEmpowerLoadInfo::Instance);
+DB2Storage<SpellEmpowerStageEntry> sSpellEmpowerStageStore("SpellEmpowerStage.db2", &SpellEmpowerStageLoadInfo::Instance);
DB2Storage<SpellFocusObjectEntry> sSpellFocusObjectStore("SpellFocusObject.db2", &SpellFocusObjectLoadInfo::Instance);
DB2Storage<SpellInterruptsEntry> sSpellInterruptsStore("SpellInterrupts.db2", &SpellInterruptsLoadInfo::Instance);
DB2Storage<SpellItemEnchantmentEntry> sSpellItemEnchantmentStore("SpellItemEnchantment.db2", &SpellItemEnchantmentLoadInfo::Instance);
@@ -899,6 +901,8 @@ uint32 DB2Manager::LoadStores(std::string const& dataPath, LocaleConstant defaul
LOAD_DB2(sSpellDurationStore);
LOAD_DB2(sSpellEffectStore);
LOAD_DB2(sSpellEquippedItemsStore);
+ LOAD_DB2(sSpellEmpowerStore);
+ LOAD_DB2(sSpellEmpowerStageStore);
LOAD_DB2(sSpellFocusObjectStore);
LOAD_DB2(sSpellInterruptsStore);
LOAD_DB2(sSpellItemEnchantmentStore);
diff --git a/src/server/game/DataStores/DB2Stores.h b/src/server/game/DataStores/DB2Stores.h
index fda324418ca..4f3c9676c8d 100644
--- a/src/server/game/DataStores/DB2Stores.h
+++ b/src/server/game/DataStores/DB2Stores.h
@@ -230,6 +230,8 @@ TC_GAME_API extern DB2Storage<SpellClassOptionsEntry> sSpellClassO
TC_GAME_API extern DB2Storage<SpellCooldownsEntry> sSpellCooldownsStore;
TC_GAME_API extern DB2Storage<SpellDurationEntry> sSpellDurationStore;
TC_GAME_API extern DB2Storage<SpellEffectEntry> sSpellEffectStore;
+TC_GAME_API extern DB2Storage<SpellEmpowerEntry> sSpellEmpowerStore;
+TC_GAME_API extern DB2Storage<SpellEmpowerStageEntry> sSpellEmpowerStageStore;
TC_GAME_API extern DB2Storage<SpellEquippedItemsEntry> sSpellEquippedItemsStore;
TC_GAME_API extern DB2Storage<SpellFocusObjectEntry> sSpellFocusObjectStore;
TC_GAME_API extern DB2Storage<SpellInterruptsEntry> sSpellInterruptsStore;
diff --git a/src/server/game/DataStores/DB2Structure.h b/src/server/game/DataStores/DB2Structure.h
index 4afcdbb0635..5f6bbd10933 100644
--- a/src/server/game/DataStores/DB2Structure.h
+++ b/src/server/game/DataStores/DB2Structure.h
@@ -3613,6 +3613,21 @@ struct SpellEffectEntry
SpellEffectAttributes GetEffectAttributes() const { return static_cast<SpellEffectAttributes>(EffectAttributes); }
};
+struct SpellEmpowerEntry
+{
+ uint32 ID;
+ int32 SpellID;
+ int32 Unused1000;
+};
+
+struct SpellEmpowerStageEntry
+{
+ uint32 ID;
+ int32 Stage;
+ int32 DurationMs;
+ uint32 SpellEmpowerID;
+};
+
struct SpellEquippedItemsEntry
{
uint32 ID;
diff --git a/src/server/game/Entities/Creature/Creature.cpp b/src/server/game/Entities/Creature/Creature.cpp
index 9c39a8c7d99..dbb69e78ce6 100644
--- a/src/server/game/Entities/Creature/Creature.cpp
+++ b/src/server/game/Entities/Creature/Creature.cpp
@@ -542,6 +542,7 @@ bool Creature::InitEntry(uint32 entry, CreatureData const* data /*= nullptr*/)
SetModRangedHaste(1.0f);
SetModHasteRegen(1.0f);
SetModTimeRate(1.0f);
+ SetSpellEmpowerStage(-1);
SetSpeedRate(MOVE_WALK, creatureInfo->speed_walk);
SetSpeedRate(MOVE_RUN, creatureInfo->speed_run);
diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp
index 85f9cdaf74d..ea0adbb522d 100644
--- a/src/server/game/Entities/Player/Player.cpp
+++ b/src/server/game/Entities/Player/Player.cpp
@@ -281,6 +281,7 @@ Player::Player(WorldSession* session) : Unit(true), m_sceneMgr(this)
m_legacyRaidDifficulty = DIFFICULTY_10_N;
m_lastPotionId = 0;
+ m_empowerMinHoldStagePercent = 1.0f;
m_auraBaseFlatMod.fill(0.0f);
m_auraBasePctMod.fill(1.0f);
@@ -2404,6 +2405,7 @@ void Player::InitStatsForLevel(bool reapplyMods)
SetModRangedHaste(1.0f);
SetModHasteRegen(1.0f);
SetModTimeRate(1.0f);
+ SetSpellEmpowerStage(-1);
// reset size before reapply auras
SetObjectScale(1.0f);
diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h
index 758c3ccd28a..988b2852022 100644
--- a/src/server/game/Entities/Player/Player.h
+++ b/src/server/game/Entities/Player/Player.h
@@ -1952,6 +1952,9 @@ class TC_GAME_API Player final : public Unit, public GridObject<Player>
void SetLastPotionId(uint32 item_id) { m_lastPotionId = item_id; }
void UpdatePotionCooldown(Spell* spell = nullptr);
+ float GetEmpowerMinHoldStagePercent() const { return m_empowerMinHoldStagePercent; }
+ void SetEmpowerMinHoldStagePercent(float empowerMinHoldStagePercent) { m_empowerMinHoldStagePercent = empowerMinHoldStagePercent; }
+
void SetResurrectRequestData(WorldObject const* caster, uint32 health, uint32 mana, uint32 appliedAura);
void ClearResurrectRequestData()
{
@@ -3078,6 +3081,7 @@ class TC_GAME_API Player final : public Unit, public GridObject<Player>
std::unordered_map<uint32 /*overridenSpellId*/, std::unordered_set<uint32> /*newSpellId*/> m_overrideSpells;
uint32 m_lastPotionId; // last used health/mana potion in combat, that block next potion use
std::unordered_map<uint32, StoredAuraTeleportLocation> m_storedAuraTeleportLocations;
+ float m_empowerMinHoldStagePercent;
SpecializationInfo _specializationInfo;
diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp
index a5a0b56df0e..55a697f32db 100644
--- a/src/server/game/Entities/Unit/Unit.cpp
+++ b/src/server/game/Entities/Unit/Unit.cpp
@@ -3071,7 +3071,7 @@ void Unit::FinishSpell(CurrentSpellTypes spellType, SpellCastResult result /*= S
return;
if (spellType == CURRENT_CHANNELED_SPELL)
- spell->SendChannelUpdate(0);
+ spell->SendChannelUpdate(0, result);
spell->finish(result);
}
diff --git a/src/server/game/Entities/Unit/Unit.h b/src/server/game/Entities/Unit/Unit.h
index 3a01deadd62..cae58533142 100644
--- a/src/server/game/Entities/Unit/Unit.h
+++ b/src/server/game/Entities/Unit/Unit.h
@@ -1429,6 +1429,8 @@ class TC_GAME_API Unit : public WorldObject
RemoveDynamicUpdateFieldValue(m_values.ModifyValue(&Unit::m_unitData).ModifyValue(&UF::UnitData::ChannelObjects), index);
}
void ClearChannelObjects() { ClearDynamicUpdateFieldValues(m_values.ModifyValue(&Unit::m_unitData).ModifyValue(&UF::UnitData::ChannelObjects)); }
+ int8 GetSpellEmpowerStage() const { return m_unitData->SpellEmpowerStage; }
+ void SetSpellEmpowerStage(int8 stage) { SetUpdateFieldValue(m_values.ModifyValue(&Unit::m_unitData).ModifyValue(&UF::UnitData::SpellEmpowerStage), stage); }
void SetCurrentCastSpell(Spell* pSpell);
void InterruptSpell(CurrentSpellTypes spellType, bool withDelayed = true, bool withInstant = true);
diff --git a/src/server/game/Handlers/SpellHandler.cpp b/src/server/game/Handlers/SpellHandler.cpp
index aaf74f89acc..2515320fc4b 100644
--- a/src/server/game/Handlers/SpellHandler.cpp
+++ b/src/server/game/Handlers/SpellHandler.cpp
@@ -393,6 +393,39 @@ void WorldSession::HandleCancelChanneling(WorldPackets::Spells::CancelChannellin
mover->InterruptSpell(CURRENT_CHANNELED_SPELL);
}
+void WorldSession::HandleSetEmpowerMinHoldStagePercent(WorldPackets::Spells::SetEmpowerMinHoldStagePercent const& setEmpowerMinHoldStagePercent)
+{
+ _player->SetEmpowerMinHoldStagePercent(setEmpowerMinHoldStagePercent.MinHoldStagePercent);
+}
+
+void WorldSession::HandleSpellEmpowerRelease(WorldPackets::Spells::SpellEmpowerRelease const& spellEmpowerRelease)
+{
+ // ignore for remote control state (for player case)
+ Unit* mover = _player->GetUnitBeingMoved();
+ if (mover != _player && mover->GetTypeId() == TYPEID_PLAYER)
+ return;
+
+ Spell* spell = mover->GetCurrentSpell(CURRENT_CHANNELED_SPELL);
+ if (!spell || spell->GetSpellInfo()->Id != uint32(spellEmpowerRelease.SpellID) || !spell->IsEmpowerSpell())
+ return;
+
+ spell->SetEmpowerReleasedByClient(true);
+}
+
+void WorldSession::HandleSpellEmpowerRestart(WorldPackets::Spells::SpellEmpowerRestart const& spellEmpowerRestart)
+{
+ // ignore for remote control state (for player case)
+ Unit* mover = _player->GetUnitBeingMoved();
+ if (mover != _player && mover->GetTypeId() == TYPEID_PLAYER)
+ return;
+
+ Spell* spell = mover->GetCurrentSpell(CURRENT_CHANNELED_SPELL);
+ if (!spell || spell->GetSpellInfo()->Id != uint32(spellEmpowerRestart.SpellID) || !spell->IsEmpowerSpell())
+ return;
+
+ spell->SetEmpowerReleasedByClient(false);
+}
+
void WorldSession::HandleTotemDestroyed(WorldPackets::Totem::TotemDestroyed& totemDestroyed)
{
// ignore for remote control state
diff --git a/src/server/game/Server/Packets/SpellPackets.cpp b/src/server/game/Server/Packets/SpellPackets.cpp
index 83ff89ef569..8be021086f5 100644
--- a/src/server/game/Server/Packets/SpellPackets.cpp
+++ b/src/server/game/Server/Packets/SpellPackets.cpp
@@ -851,6 +851,76 @@ WorldPacket const* SpellChannelUpdate::Write()
return &_worldPacket;
}
+WorldPacket const* SpellEmpowerStart::Write()
+{
+ _worldPacket << CastID;
+ _worldPacket << CasterGUID;
+ _worldPacket << uint32(Targets.size());
+ _worldPacket << int32(SpellID);
+ _worldPacket << Visual;
+ _worldPacket << EmpowerDuration;
+ _worldPacket << MinHoldTime;
+ _worldPacket << HoldAtMaxTime;
+ _worldPacket << uint32(StageDurations.size());
+
+ for (ObjectGuid const& target : Targets)
+ _worldPacket << target;
+
+ for (Duration<Milliseconds, uint32> stageDuration : StageDurations)
+ _worldPacket << stageDuration;
+
+ _worldPacket.WriteBit(InterruptImmunities.has_value());
+ _worldPacket.WriteBit(HealPrediction.has_value());
+ _worldPacket.FlushBits();
+
+ if (InterruptImmunities)
+ _worldPacket << *InterruptImmunities;
+
+ if (HealPrediction)
+ _worldPacket << *HealPrediction;
+
+ return &_worldPacket;
+}
+
+WorldPacket const* SpellEmpowerUpdate::Write()
+{
+ _worldPacket << CastID;
+ _worldPacket << CasterGUID;
+ _worldPacket << TimeRemaining;
+ _worldPacket << uint32(StageDurations.size());
+ _worldPacket << uint8(Status);
+ _worldPacket.FlushBits();
+
+ for (Duration<Milliseconds, uint32> stageDuration : StageDurations)
+ _worldPacket << stageDuration;
+
+ return &_worldPacket;
+}
+
+void SetEmpowerMinHoldStagePercent::Read()
+{
+ _worldPacket >> MinHoldStagePercent;
+}
+
+void SpellEmpowerRelease::Read()
+{
+ _worldPacket >> SpellID;
+}
+
+void SpellEmpowerRestart::Read()
+{
+ _worldPacket >> SpellID;
+}
+
+WorldPacket const* SpellEmpowerSetStage::Write()
+{
+ _worldPacket << CastID;
+ _worldPacket << CasterGUID;
+ _worldPacket << int32(Stage);
+
+ return &_worldPacket;
+}
+
WorldPacket const* ResurrectRequest::Write()
{
_worldPacket << ResurrectOffererGUID;
diff --git a/src/server/game/Server/Packets/SpellPackets.h b/src/server/game/Server/Packets/SpellPackets.h
index f344eb53899..42c58e35772 100644
--- a/src/server/game/Server/Packets/SpellPackets.h
+++ b/src/server/game/Server/Packets/SpellPackets.h
@@ -830,6 +830,82 @@ namespace WorldPackets
int32 TimeRemaining = 0;
};
+ class SpellEmpowerStart final : public ServerPacket
+ {
+ public:
+ SpellEmpowerStart() : ServerPacket(SMSG_SPELL_EMPOWER_START, 16 + 16 + 4 + 8 + 4 + 4 + 4 + 1) { }
+
+ WorldPacket const* Write() override;
+
+ ObjectGuid CastID;
+ ObjectGuid CasterGUID;
+ int32 SpellID = 0;
+ SpellCastVisual Visual;
+ Duration<Milliseconds, uint32> EmpowerDuration;
+ Duration<Milliseconds, uint32> MinHoldTime;
+ Duration<Milliseconds, uint32> HoldAtMaxTime;
+ std::vector<ObjectGuid> Targets;
+ std::vector<Duration<Milliseconds, uint32>> StageDurations;
+ Optional<SpellChannelStartInterruptImmunities> InterruptImmunities;
+ Optional<SpellTargetedHealPrediction> HealPrediction;
+ };
+
+ class SpellEmpowerUpdate final : public ServerPacket
+ {
+ public:
+ SpellEmpowerUpdate() : ServerPacket(SMSG_SPELL_EMPOWER_UPDATE, 16 + 16 + 4 + 4 + 1) { }
+
+ WorldPacket const* Write() override;
+
+ ObjectGuid CastID;
+ ObjectGuid CasterGUID;
+ Duration<Milliseconds, int32> TimeRemaining;
+ std::vector<Duration<Milliseconds, uint32>> StageDurations;
+ uint8 Status = 0;
+ };
+
+ class SetEmpowerMinHoldStagePercent final : public ClientPacket
+ {
+ public:
+ SetEmpowerMinHoldStagePercent(WorldPacket&& packet) : ClientPacket(CMSG_SET_EMPOWER_MIN_HOLD_STAGE_PERCENT, std::move(packet)) { }
+
+ void Read() override;
+
+ float MinHoldStagePercent = 1.0f;
+ };
+
+ class SpellEmpowerRelease final : public ClientPacket
+ {
+ public:
+ SpellEmpowerRelease(WorldPacket&& packet) : ClientPacket(CMSG_SPELL_EMPOWER_RELEASE, std::move(packet)) { }
+
+ void Read() override;
+
+ int32 SpellID = 0;
+ };
+
+ class SpellEmpowerRestart final : public ClientPacket
+ {
+ public:
+ SpellEmpowerRestart(WorldPacket&& packet) : ClientPacket(CMSG_SPELL_EMPOWER_RESTART, std::move(packet)) { }
+
+ void Read() override;
+
+ int32 SpellID = 0;
+ };
+
+ class SpellEmpowerSetStage final : public ServerPacket
+ {
+ public:
+ SpellEmpowerSetStage() : ServerPacket(SMSG_SPELL_EMPOWER_SET_STAGE, 16 + 16 + 4) { }
+
+ WorldPacket const* Write() override;
+
+ ObjectGuid CastID;
+ ObjectGuid CasterGUID;
+ int32 Stage = 0;
+ };
+
class ResurrectRequest final : public ServerPacket
{
public:
diff --git a/src/server/game/Server/Protocol/Opcodes.cpp b/src/server/game/Server/Protocol/Opcodes.cpp
index e25b2ef6d74..054cf392d94 100644
--- a/src/server/game/Server/Protocol/Opcodes.cpp
+++ b/src/server/game/Server/Protocol/Opcodes.cpp
@@ -876,7 +876,7 @@ void OpcodeTable::Initialize()
DEFINE_HANDLER(CMSG_SET_CURRENCY_FLAGS, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL);
DEFINE_HANDLER(CMSG_SET_DIFFICULTY_ID, STATUS_UNHANDLED, PROCESS_THREADUNSAFE, &WorldSession::Handle_NULL);
DEFINE_HANDLER(CMSG_SET_DUNGEON_DIFFICULTY, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleSetDungeonDifficultyOpcode);
- DEFINE_HANDLER(CMSG_SET_EMPOWER_MIN_HOLD_STAGE_PERCENT, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL);
+ DEFINE_HANDLER(CMSG_SET_EMPOWER_MIN_HOLD_STAGE_PERCENT, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleSetEmpowerMinHoldStagePercent);
DEFINE_HANDLER(CMSG_SET_EVERYONE_IS_ASSISTANT, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleSetEveryoneIsAssistant);
DEFINE_HANDLER(CMSG_SET_EXCLUDED_CHAT_CENSOR_SOURCES, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL);
DEFINE_HANDLER(CMSG_SET_FACTION_AT_WAR, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleSetFactionAtWar);
@@ -918,8 +918,8 @@ void OpcodeTable::Initialize()
DEFINE_HANDLER(CMSG_SPECTATE_CHANGE, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL);
DEFINE_HANDLER(CMSG_SPAWN_TRACKING_UPDATE, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL);
DEFINE_HANDLER(CMSG_SPELL_CLICK, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleSpellClick);
- DEFINE_HANDLER(CMSG_SPELL_EMPOWER_RELEASE, STATUS_UNHANDLED, PROCESS_THREADSAFE, &WorldSession::Handle_NULL);
- DEFINE_HANDLER(CMSG_SPELL_EMPOWER_RESTART, STATUS_UNHANDLED, PROCESS_THREADSAFE, &WorldSession::Handle_NULL);
+ DEFINE_HANDLER(CMSG_SPELL_EMPOWER_RELEASE, STATUS_LOGGEDIN, PROCESS_THREADSAFE, &WorldSession::HandleSpellEmpowerRelease);
+ DEFINE_HANDLER(CMSG_SPELL_EMPOWER_RESTART, STATUS_LOGGEDIN, PROCESS_THREADSAFE, &WorldSession::HandleSpellEmpowerRestart);
DEFINE_HANDLER(CMSG_SPIRIT_HEALER_ACTIVATE, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleSpiritHealerActivate);
DEFINE_HANDLER(CMSG_SPLIT_GUILD_BANK_ITEM, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleSplitGuildBankItem);
DEFINE_HANDLER(CMSG_SPLIT_GUILD_BANK_ITEM_TO_INVENTORY, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleSplitGuildBankItemToInventory);
@@ -2058,9 +2058,9 @@ void OpcodeTable::Initialize()
DEFINE_SERVER_OPCODE_HANDLER(SMSG_SPELL_DAMAGE_SHIELD, STATUS_NEVER, CONNECTION_TYPE_INSTANCE);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_SPELL_DELAYED, STATUS_NEVER, CONNECTION_TYPE_INSTANCE);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_SPELL_DISPELL_LOG, STATUS_NEVER, CONNECTION_TYPE_INSTANCE);
- DEFINE_SERVER_OPCODE_HANDLER(SMSG_SPELL_EMPOWER_SET_STAGE, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE);
- DEFINE_SERVER_OPCODE_HANDLER(SMSG_SPELL_EMPOWER_START, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE);
- DEFINE_SERVER_OPCODE_HANDLER(SMSG_SPELL_EMPOWER_UPDATE, STATUS_UNHANDLED, CONNECTION_TYPE_INSTANCE);
+ DEFINE_SERVER_OPCODE_HANDLER(SMSG_SPELL_EMPOWER_SET_STAGE, STATUS_NEVER, CONNECTION_TYPE_INSTANCE);
+ DEFINE_SERVER_OPCODE_HANDLER(SMSG_SPELL_EMPOWER_START, STATUS_NEVER, CONNECTION_TYPE_INSTANCE);
+ DEFINE_SERVER_OPCODE_HANDLER(SMSG_SPELL_EMPOWER_UPDATE, STATUS_NEVER, CONNECTION_TYPE_INSTANCE);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_SPELL_ENERGIZE_LOG, STATUS_NEVER, CONNECTION_TYPE_INSTANCE);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_SPELL_EXECUTE_LOG, STATUS_NEVER, CONNECTION_TYPE_INSTANCE);
DEFINE_SERVER_OPCODE_HANDLER(SMSG_SPELL_FAILED_OTHER, STATUS_NEVER, CONNECTION_TYPE_INSTANCE);
diff --git a/src/server/game/Server/WorldSession.h b/src/server/game/Server/WorldSession.h
index fe569b4a7b2..214a04d4714 100644
--- a/src/server/game/Server/WorldSession.h
+++ b/src/server/game/Server/WorldSession.h
@@ -737,6 +737,9 @@ namespace WorldPackets
class UpdateMissileTrajectory;
class TradeSkillSetFavorite;
class KeyboundOverride;
+ class SetEmpowerMinHoldStagePercent;
+ class SpellEmpowerRelease;
+ class SpellEmpowerRestart;
}
namespace Talent
@@ -1488,7 +1491,6 @@ class TC_GAME_API WorldSession
void HandleItemTextQuery(WorldPackets::Query::ItemTextQuery& itemTextQuery);
void HandleMailCreateTextItem(WorldPackets::Mail::MailCreateTextItem& createTextItem);
void HandleQueryNextMailTime(WorldPackets::Mail::MailQueryNextMailTime& queryNextMailTime);
- void HandleCancelChanneling(WorldPackets::Spells::CancelChannelling& cancelChanneling);
void HandleSplitItemOpcode(WorldPackets::Item::SplitItem& splitItem);
void HandleSwapInvItemOpcode(WorldPackets::Item::SwapInvItem& swapInvItem);
@@ -1525,6 +1527,10 @@ class TC_GAME_API WorldSession
void HandleCancelModSpeedNoControlAuras(WorldPackets::Spells::CancelModSpeedNoControlAuras& cancelModSpeedNoControlAuras);
void HandleCancelAutoRepeatSpellOpcode(WorldPackets::Spells::CancelAutoRepeatSpell& cancelAutoRepeatSpell);
void HandleCancelQueuedSpellOpcode(WorldPackets::Spells::CancelQueuedSpell& cancelQueuedSpell);
+ void HandleCancelChanneling(WorldPackets::Spells::CancelChannelling& cancelChanneling);
+ void HandleSetEmpowerMinHoldStagePercent(WorldPackets::Spells::SetEmpowerMinHoldStagePercent const& setEmpowerMinHoldStagePercent);
+ void HandleSpellEmpowerRelease(WorldPackets::Spells::SpellEmpowerRelease const& spellEmpowerRelease);
+ void HandleSpellEmpowerRestart(WorldPackets::Spells::SpellEmpowerRestart const& spellEmpowerRestart);
void HandleMissileTrajectoryCollision(WorldPackets::Spells::MissileTrajectoryCollision& packet);
void HandleUpdateMissileTrajectory(WorldPackets::Spells::UpdateMissileTrajectory& packet);
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)
diff --git a/src/server/scripts/Spells/spell_evoker.cpp b/src/server/scripts/Spells/spell_evoker.cpp
index 8004f4115e7..adb1794a7bc 100644
--- a/src/server/scripts/Spells/spell_evoker.cpp
+++ b/src/server/scripts/Spells/spell_evoker.cpp
@@ -32,7 +32,9 @@
enum EvokerSpells
{
+ SPELL_EVOKER_BLAST_FURNACE = 375510,
SPELL_EVOKER_ENERGIZING_FLAME = 400006,
+ SPELL_EVOKER_FIRE_BREATH_DAMAGE = 357209,
SPELL_EVOKER_GLIDE_KNOCKBACK = 358736,
SPELL_EVOKER_HOVER = 358267,
SPELL_EVOKER_LIVING_FLAME = 361469,
@@ -40,6 +42,7 @@ enum EvokerSpells
SPELL_EVOKER_LIVING_FLAME_HEAL = 361509,
SPELL_EVOKER_PERMEATING_CHILL_TALENT = 370897,
SPELL_EVOKER_PYRE_DAMAGE = 357212,
+ SPELL_EVOKER_SCOURING_FLAME = 378438,
SPELL_EVOKER_SOAR_RACIAL = 369536
};
@@ -78,6 +81,72 @@ class spell_evo_charged_blast : public AuraScript
}
};
+// 357208 Fire Breath (Red)
+// 382266 Fire Breath (Red)
+class spell_evo_fire_breath : public SpellScript
+{
+public:
+ struct data
+ {
+ int32 EmpowerLevel;
+ };
+
+ bool Validate(SpellInfo const* /*spellInfo*/) override
+ {
+ return ValidateSpellInfo({ SPELL_EVOKER_FIRE_BREATH_DAMAGE, SPELL_EVOKER_BLAST_FURNACE });
+ }
+
+ void OnComplete(int32 completedStageCount) const
+ {
+ int32 dotTicks = 10 - (completedStageCount - 1) * 3;
+ if (AuraEffect const* blastFurnace = GetCaster()->GetAuraEffect(SPELL_EVOKER_BLAST_FURNACE, EFFECT_0))
+ dotTicks += blastFurnace->GetAmount() / 2;
+
+ GetCaster()->CastSpell(GetCaster(), SPELL_EVOKER_FIRE_BREATH_DAMAGE, CastSpellExtraArgs()
+ .SetTriggeringSpell(GetSpell())
+ .SetTriggerFlags(TRIGGERED_IGNORE_CAST_IN_PROGRESS | TRIGGERED_DONT_REPORT_CAST_ERROR)
+ .AddSpellMod(SPELLVALUE_DURATION_PCT, 100 * dotTicks)
+ .SetCustomArg(data{ .EmpowerLevel = completedStageCount }));
+ }
+
+ void Register() override
+ {
+ OnEmpowerCompleted += SpellOnEmpowerStageCompletedFn(spell_evo_fire_breath::OnComplete);
+ }
+};
+
+// 357209 Fire Breath (Red)
+class spell_evo_fire_breath_damage : public SpellScript
+{
+ bool Validate(SpellInfo const* spellInfo) override
+ {
+ return ValidateSpellEffect({ { spellInfo->Id, EFFECT_2 } })
+ && spellInfo->GetEffect(EFFECT_2).IsAura(SPELL_AURA_MOD_SILENCE); // validate we are removing the correct effect
+ }
+
+ void AddBonusUpfrontDamage(Unit const* victim, int32& /*damage*/, int32& flatMod, float& /*pctMod*/) const
+ {
+ spell_evo_fire_breath::data const* params = std::any_cast<spell_evo_fire_breath::data>(&GetSpell()->m_customArg);
+ if (!params)
+ return;
+
+ // damage is done after aura is applied, grab periodic amount
+ if (AuraEffect const* fireBreath = victim->GetAuraEffect(GetSpellInfo()->Id, EFFECT_1, GetCaster()->GetGUID()))
+ flatMod += fireBreath->GetEstimatedAmount().value_or(fireBreath->GetAmount()) * (params->EmpowerLevel - 1) * 3;
+ }
+
+ void RemoveUnusedEffect(std::list<WorldObject*>& targets) const
+ {
+ targets.clear();
+ }
+
+ void Register() override
+ {
+ CalcDamage += SpellCalcDamageFn(spell_evo_fire_breath_damage::AddBonusUpfrontDamage);
+ OnObjectAreaTargetSelect += SpellObjectAreaTargetSelectFn(spell_evo_fire_breath_damage::RemoveUnusedEffect, EFFECT_2, TARGET_UNIT_CONE_CASTER_TO_DEST_ENEMY);
+ }
+};
+
// 358733 - Glide (Racial)
class spell_evo_glide : public SpellScript
{
@@ -203,12 +272,42 @@ class spell_evo_pyre : public SpellScript
}
};
+// 357209 Fire Breath (Red)
+class spell_evo_scouring_flame : public SpellScript
+{
+ bool Validate(SpellInfo const* /*spellInfo*/) override
+ {
+ return ValidateSpellInfo({ SPELL_EVOKER_SCOURING_FLAME });
+ }
+
+ void HandleScouringFlame(std::list<WorldObject*>& targets) const
+ {
+ if (!GetCaster()->HasAura(SPELL_EVOKER_SCOURING_FLAME))
+ targets.clear();
+ }
+
+ void CalcDispelCount(SpellEffIndex /*effIndex*/)
+ {
+ if (spell_evo_fire_breath::data const* params = std::any_cast<spell_evo_fire_breath::data>(&GetSpell()->m_customArg))
+ SetEffectValue(params->EmpowerLevel);
+ }
+
+ void Register() override
+ {
+ OnObjectAreaTargetSelect += SpellObjectAreaTargetSelectFn(spell_evo_scouring_flame::HandleScouringFlame, EFFECT_3, TARGET_UNIT_CONE_CASTER_TO_DEST_ENEMY);
+ OnEffectHitTarget += SpellEffectFn(spell_evo_scouring_flame::CalcDispelCount, EFFECT_3, SPELL_EFFECT_DISPEL);
+ }
+};
+
void AddSC_evoker_spell_scripts()
{
RegisterSpellScript(spell_evo_azure_strike);
RegisterSpellScript(spell_evo_charged_blast);
+ RegisterSpellScript(spell_evo_fire_breath);
+ RegisterSpellScript(spell_evo_fire_breath_damage);
RegisterSpellScript(spell_evo_glide);
RegisterSpellScript(spell_evo_living_flame);
RegisterSpellScript(spell_evo_permeating_chill);
RegisterSpellScript(spell_evo_pyre);
+ RegisterSpellScript(spell_evo_scouring_flame);
}