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 | |
parent | cdc6719b8368907292f090978f6bdd6b8c73834d (diff) |
Core/Spells: Implemented evoker empower spell mechanic
Diffstat (limited to 'src')
25 files changed, 702 insertions, 93 deletions
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); } |