Core/Quests Implement quest campaigns

This commit is contained in:
Shauren
2026-03-08 21:23:39 +01:00
parent 1510fc3d4c
commit 654d967e04
14 changed files with 308 additions and 2 deletions

View File

@@ -0,0 +1,59 @@
--
-- Table structure for table `campaign`
--
DROP TABLE IF EXISTS `campaign`;
CREATE TABLE `campaign` (
`ID` int unsigned NOT NULL DEFAULT '0',
`Title` text,
`Description` text,
`UiTextureKitID` int NOT NULL DEFAULT '0',
`RewardQuestID` int NOT NULL DEFAULT '0',
`Prerequisite` int NOT NULL DEFAULT '0',
`Stalled` int NOT NULL DEFAULT '0',
`Completed` int NOT NULL DEFAULT '0',
`OnlyStallIf` int NOT NULL DEFAULT '0',
`UiQuestDetailsThemeID` int NOT NULL DEFAULT '0',
`Flags` int NOT NULL DEFAULT '0',
`DisplayPriority` int NOT NULL DEFAULT '0',
`SortAsNormalQuest` int NOT NULL DEFAULT '0',
`UseMinimalHeader` 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 `campaign_locale`
--
DROP TABLE IF EXISTS `campaign_locale`;
CREATE TABLE `campaign_locale` (
`ID` int unsigned NOT NULL DEFAULT '0',
`locale` varchar(4) NOT NULL,
`Title_lang` text,
`Description_lang` text,
`VerifiedBuild` int NOT NULL DEFAULT '0',
PRIMARY KEY (`ID`,`locale`,`VerifiedBuild`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
PARTITION BY LIST COLUMNS(locale)
(PARTITION deDE VALUES IN ('deDE') ENGINE = InnoDB,
PARTITION esES VALUES IN ('esES') ENGINE = InnoDB,
PARTITION esMX VALUES IN ('esMX') ENGINE = InnoDB,
PARTITION frFR VALUES IN ('frFR') ENGINE = InnoDB,
PARTITION itIT VALUES IN ('itIT') ENGINE = InnoDB,
PARTITION koKR VALUES IN ('koKR') ENGINE = InnoDB,
PARTITION ptBR VALUES IN ('ptBR') ENGINE = InnoDB,
PARTITION ruRU VALUES IN ('ruRU') ENGINE = InnoDB,
PARTITION zhCN VALUES IN ('zhCN') ENGINE = InnoDB,
PARTITION zhTW VALUES IN ('zhTW') ENGINE = InnoDB);
--
-- Table structure for table `campaign_x_quest_line`
--
DROP TABLE IF EXISTS `campaign_x_quest_line`;
CREATE TABLE `campaign_x_quest_line` (
`ID` int unsigned NOT NULL DEFAULT '0',
`CampaignID` int unsigned NOT NULL DEFAULT '0',
`QuestLineID` int unsigned NOT NULL DEFAULT '0',
`OrderIndex` 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;

View File

@@ -298,6 +298,18 @@ void HotfixDatabaseConnection::DoPrepareStatements()
" WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
PREPARE_MAX_ID_STMT(HOTFIX_SEL_BROADCAST_TEXT_DURATION, "SELECT MAX(ID) + 1 FROM broadcast_text_duration", CONNECTION_SYNCH);
// Campaign.db2
PrepareStatement(HOTFIX_SEL_CAMPAIGN, "SELECT ID, Title, Description, UiTextureKitID, RewardQuestID, Prerequisite, Stalled, Completed, "
"OnlyStallIf, UiQuestDetailsThemeID, Flags, DisplayPriority, SortAsNormalQuest, UseMinimalHeader FROM campaign WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
PREPARE_MAX_ID_STMT(HOTFIX_SEL_CAMPAIGN, "SELECT MAX(ID) + 1 FROM campaign", CONNECTION_SYNCH);
PREPARE_LOCALE_STMT(HOTFIX_SEL_CAMPAIGN, "SELECT ID, Title_lang, Description_lang FROM campaign_locale WHERE (`VerifiedBuild` > 0) = ?"
" AND locale = ?", CONNECTION_SYNCH);
// CampaignXQuestLine.db2
PrepareStatement(HOTFIX_SEL_CAMPAIGN_X_QUEST_LINE, "SELECT ID, CampaignID, QuestLineID, OrderIndex FROM campaign_x_quest_line"
" WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
PREPARE_MAX_ID_STMT(HOTFIX_SEL_CAMPAIGN_X_QUEST_LINE, "SELECT MAX(ID) + 1 FROM campaign_x_quest_line", CONNECTION_SYNCH);
// CfgCategories.db2
PrepareStatement(HOTFIX_SEL_CFG_CATEGORIES, "SELECT ID, Name, LocaleMask, CreateCharsetMask, ExistingCharsetMask, Flags, `Order`"
" FROM cfg_categories WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);

View File

@@ -188,6 +188,13 @@ enum HotfixDatabaseStatements : uint32
HOTFIX_SEL_BROADCAST_TEXT_DURATION,
HOTFIX_SEL_BROADCAST_TEXT_DURATION_MAX_ID,
HOTFIX_SEL_CAMPAIGN,
HOTFIX_SEL_CAMPAIGN_MAX_ID,
HOTFIX_SEL_CAMPAIGN_LOCALE,
HOTFIX_SEL_CAMPAIGN_X_QUEST_LINE,
HOTFIX_SEL_CAMPAIGN_X_QUEST_LINE_MAX_ID,
HOTFIX_SEL_CFG_CATEGORIES,
HOTFIX_SEL_CFG_CATEGORIES_MAX_ID,
HOTFIX_SEL_CFG_CATEGORIES_LOCALE,

View File

@@ -3998,6 +3998,10 @@ bool CriteriaHandler::ModifierSatisfied(ModifierTreeEntry const* modifier, uint6
if (referencePlayer->m_activePlayerData->TimerunningSeasonID != int32(reqValue))
return false;
break;
case ModifierTreeType::PlayerHasCompletedCampaign: // 388
if (!QuestMgr::IsCampaignCompletedByPlayer(reqValue, referencePlayer))
return false;
break;
case ModifierTreeType::TargetCreatureClassificationEqual: // 389
{
Creature const* targetCreature = Object::ToCreature(ref);

View File

@@ -810,6 +810,42 @@ struct BroadcastTextDurationLoadInfo
static constexpr DB2LoadInfo Instance{ Fields, 4, &BroadcastTextDurationMeta::Instance, HOTFIX_SEL_BROADCAST_TEXT_DURATION };
};
struct CampaignLoadInfo
{
static constexpr DB2FieldMeta Fields[14] =
{
{ .IsSigned = false, .Type = FT_INT, .Name = "ID" },
{ .IsSigned = false, .Type = FT_STRING, .Name = "Title" },
{ .IsSigned = false, .Type = FT_STRING, .Name = "Description" },
{ .IsSigned = true, .Type = FT_INT, .Name = "UiTextureKitID" },
{ .IsSigned = true, .Type = FT_INT, .Name = "RewardQuestID" },
{ .IsSigned = true, .Type = FT_INT, .Name = "Prerequisite" },
{ .IsSigned = true, .Type = FT_INT, .Name = "Stalled" },
{ .IsSigned = true, .Type = FT_INT, .Name = "Completed" },
{ .IsSigned = true, .Type = FT_INT, .Name = "OnlyStallIf" },
{ .IsSigned = true, .Type = FT_INT, .Name = "UiQuestDetailsThemeID" },
{ .IsSigned = true, .Type = FT_INT, .Name = "Flags" },
{ .IsSigned = true, .Type = FT_INT, .Name = "DisplayPriority" },
{ .IsSigned = true, .Type = FT_INT, .Name = "SortAsNormalQuest" },
{ .IsSigned = true, .Type = FT_INT, .Name = "UseMinimalHeader" },
};
static constexpr DB2LoadInfo Instance{ Fields, 14, &CampaignMeta::Instance, HOTFIX_SEL_CAMPAIGN };
};
struct CampaignXQuestLineLoadInfo
{
static constexpr DB2FieldMeta Fields[4] =
{
{ .IsSigned = false, .Type = FT_INT, .Name = "ID" },
{ .IsSigned = false, .Type = FT_INT, .Name = "CampaignID" },
{ .IsSigned = false, .Type = FT_INT, .Name = "QuestLineID" },
{ .IsSigned = false, .Type = FT_INT, .Name = "OrderIndex" },
};
static constexpr DB2LoadInfo Instance{ Fields, 4, &CampaignXQuestLineMeta::Instance, HOTFIX_SEL_CAMPAIGN_X_QUEST_LINE };
};
struct CfgCategoriesLoadInfo
{
static constexpr DB2FieldMeta Fields[7] =

View File

@@ -85,6 +85,8 @@ DB2Storage<BattlemasterListEntry> sBattlemasterListStore("Battlema
DB2Storage<BattlemasterListXMapEntry> sBattlemasterListXMapStore("BattlemasterListXMap.db2", &BattlemasterListXMapLoadInfo::Instance);
DB2Storage<BroadcastTextEntry> sBroadcastTextStore("BroadcastText.db2", &BroadcastTextLoadInfo::Instance);
DB2Storage<BroadcastTextDurationEntry> sBroadcastTextDurationStore("BroadcastTextDuration.db2", &BroadcastTextDurationLoadInfo::Instance);
DB2Storage<CampaignEntry> sCampaignStore("Campaign.db2", &CampaignLoadInfo::Instance);
DB2Storage<CampaignXQuestLineEntry> sCampaignXQuestLineStore("CampaignXQuestLine.db2", &CampaignXQuestLineLoadInfo::Instance);
DB2Storage<Cfg_CategoriesEntry> sCfgCategoriesStore("Cfg_Categories.db2", &CfgCategoriesLoadInfo::Instance);
DB2Storage<Cfg_RegionsEntry> sCfgRegionsStore("Cfg_Regions.db2", &CfgRegionsLoadInfo::Instance);
DB2Storage<ChallengeModeItemBonusOverrideEntry> sChallengeModeItemBonusOverrideStore("ChallengeModeItemBonusOverride.db2", &ChallengeModeItemBonusOverrideLoadInfo::Instance);
@@ -714,6 +716,8 @@ uint32 DB2Manager::LoadStores(std::string const& dataPath, LocaleConstant defaul
LOAD_DB2(sBattlemasterListXMapStore);
LOAD_DB2(sBroadcastTextStore);
LOAD_DB2(sBroadcastTextDurationStore);
LOAD_DB2(sCampaignStore);
LOAD_DB2(sCampaignXQuestLineStore);
LOAD_DB2(sCfgCategoriesStore);
LOAD_DB2(sCfgRegionsStore);
LOAD_DB2(sChallengeModeItemBonusOverrideStore);

View File

@@ -67,6 +67,8 @@ TC_GAME_API extern DB2Storage<BattlePetSpeciesStateEntry> sBattlePetSp
TC_GAME_API extern DB2Storage<BattlemasterListEntry> sBattlemasterListStore;
TC_GAME_API extern DB2Storage<BattlemasterListXMapEntry> sBattlemasterListXMapStore;
TC_GAME_API extern DB2Storage<BroadcastTextEntry> sBroadcastTextStore;
TC_GAME_API extern DB2Storage<CampaignEntry> sCampaignStore;
TC_GAME_API extern DB2Storage<CampaignXQuestLineEntry> sCampaignXQuestLineStore;
TC_GAME_API extern DB2Storage<Cfg_CategoriesEntry> sCfgCategoriesStore;
TC_GAME_API extern DB2Storage<Cfg_RegionsEntry> sCfgRegionsStore;
TC_GAME_API extern DB2Storage<ChallengeModeItemBonusOverrideEntry> sChallengeModeItemBonusOverrideStore;

View File

@@ -571,6 +571,34 @@ struct BroadcastTextDurationEntry
uint32 BroadcastTextID;
};
struct CampaignEntry
{
uint32 ID;
LocalizedString Title;
LocalizedString Description;
int32 UiTextureKitID;
int32 RewardQuestID;
int32 Prerequisite;
int32 Stalled;
int32 Completed;
int32 OnlyStallIf;
int32 UiQuestDetailsThemeID;
int32 Flags;
int32 DisplayPriority;
int32 SortAsNormalQuest;
int32 UseMinimalHeader;
bool HasFlag(CampaignFlags flag) const { return EnumFlag(static_cast<CampaignFlags>(Flags)).HasFlag(flag); }
};
struct CampaignXQuestLineEntry
{
uint32 ID;
uint32 CampaignID;
uint32 QuestLineID;
uint32 OrderIndex;
};
struct Cfg_CategoriesEntry
{
uint32 ID;

View File

@@ -284,6 +284,14 @@ enum class BattlemasterListFlags : uint32
DEFINE_ENUM_FLAG(BattlemasterListFlags);
enum class CampaignFlags : int32
{
DontUseJourneyQuestBang = 0x01,
IsContainer = 0x02
};
DEFINE_ENUM_FLAG(CampaignFlags);
enum class CfgCategoriesCharsets : uint8
{
Any = 0x00,
@@ -2035,7 +2043,7 @@ enum class ModifierTreeType : int32
PlayerHasActiveTraitSubTree = 385, // Player has active trait config with {TraitSubTree}
PlayerIsInTimerunningSeason = 386, // Player is timerunning {TimerunningSeason}
PlayerIsInSoloRBG = 387, /*NYI*/ // Player is in solo RBG (BG Blitz)
PlayerHasCompletedCampaign = 388, /*NYI*/ // Player has completed campaign "{Campaign}"
PlayerHasCompletedCampaign = 388, // Player has completed campaign "{Campaign}"
TargetCreatureClassificationEqual = 389, // Creature classification is {CreatureClassification}
PlayerDataElementCharacterBetween = 390, // Player {PlayerDataElementCharacter} is between {#Amount} and {#Amount2}
PlayerDataElementAccountBetween = 391, // Player {PlayerDataElementAccount} is between {#Amount} and {#Amount2}

View File

@@ -108,6 +108,7 @@
#include "QueryHolder.h"
#include "QueryResultStructured.h"
#include "QuestDef.h"
#include "QuestMgr.h"
#include "QuestObjectiveCriteriaMgr.h"
#include "QuestPackets.h"
#include "RealmList.h"
@@ -16073,6 +16074,8 @@ QuestGiverStatus Player::GetQuestDialogStatus(Object const* questgiver) const
result |= quest->HasFlag(QUEST_FLAGS_HIDE_REWARD_POI) ? QuestGiverStatus::CovenantCallingRewardCompleteNoPOI : QuestGiverStatus::CovenantCallingRewardCompletePOI;
else if (quest->HasFlagEx(QUEST_FLAGS_EX_LEGENDARY))
result |= quest->HasFlag(QUEST_FLAGS_HIDE_REWARD_POI) ? QuestGiverStatus::LegendaryRewardCompleteNoPOI : QuestGiverStatus::LegendaryRewardCompletePOI;
else if (QuestMgr::IsCampaignQuestStatusVisibleForPlayer(questId, this))
result |= quest->HasFlag(QUEST_FLAGS_HIDE_REWARD_POI) ? QuestGiverStatus::JourneyRewardCompleteNoPOI : QuestGiverStatus::JourneyRewardCompletePOI;
else if (quest->IsDailyOrWeekly())
result |= quest->HasFlag(QUEST_FLAGS_HIDE_REWARD_POI) ? QuestGiverStatus::RepeatableRewardCompleteNoPOI : QuestGiverStatus::RepeatableRewardCompletePOI;
else
@@ -16087,6 +16090,8 @@ QuestGiverStatus Player::GetQuestDialogStatus(Object const* questgiver) const
result |= QuestGiverStatus::CovenantCallingReward;
else if (quest->HasFlagEx(QUEST_FLAGS_EX_LEGENDARY))
result |= QuestGiverStatus::LegendaryReward;
else if (QuestMgr::IsCampaignQuestStatusVisibleForPlayer(questId, this))
result |= QuestGiverStatus::JourneyReward;
else if (quest->IsDailyOrWeekly())
result |= QuestGiverStatus::RepeatableReward;
else
@@ -16130,6 +16135,8 @@ QuestGiverStatus Player::GetQuestDialogStatus(Object const* questgiver) const
result |= QuestGiverStatus::CovenantCallingQuest;
else if (quest->HasFlagEx(QUEST_FLAGS_EX_LEGENDARY))
result |= isTrivial ? QuestGiverStatus::TrivialLegendaryQuest : QuestGiverStatus::LegendaryQuest;
else if (QuestMgr::IsCampaignQuestStatusVisibleForPlayer(questId, this))
result |= isTrivial ? QuestGiverStatus::TrivialJourneyQuest : QuestGiverStatus::JourneyQuest;
else if (quest->IsDailyOrWeekly())
result |= isTrivial ? QuestGiverStatus::TrivialRepeatableQuest : QuestGiverStatus::RepeatableQuest;
else
@@ -16139,6 +16146,8 @@ QuestGiverStatus Player::GetQuestDialogStatus(Object const* questgiver) const
result |= QuestGiverStatus::FutureImportantQuest;
else if (quest->HasFlagEx(QUEST_FLAGS_EX_LEGENDARY))
result |= QuestGiverStatus::FutureLegendaryQuest;
else if (QuestMgr::IsCampaignQuestStatusVisibleForPlayer(questId, this))
result |= QuestGiverStatus::FutureJourneyQuest;
else
result |= QuestGiverStatus::Future;
}

View File

@@ -17,6 +17,7 @@
#include "QuestMgr.h"
#include "DB2Stores.h"
#include "MapUtils.h"
#include "ObjectMgr.h"
#include "Player.h"
#include <algorithm>
@@ -24,13 +25,65 @@
namespace
{
std::unordered_map<uint32, std::vector<QuestLineXQuestEntry const*>> QuestsByQuestLine;
struct QuestLineData
{
QuestLineXQuestEntry const* QuestLineQuest = nullptr;
std::vector<CampaignEntry const*>* Campaigns = nullptr;
};
std::map<uint32, std::vector<CampaignEntry const*>> CampaignsByQuestLine;
std::unordered_map<uint32, std::vector<QuestLineData>> QuestLineDataByQuest;
struct CampaignQuestLine
{
uint32 CampaignId = 0;
uint32 QuestLineId = 0;
friend std::strong_ordering operator<=>(CampaignQuestLine const& left, CampaignQuestLine const& right) = default;
};
std::vector<CampaignQuestLine> CampaignQuestLines;
struct CampaignQuestLinesSentinel
{
std::vector<CampaignQuestLine>::const_iterator End;
uint32 CampaignId;
friend bool operator==(std::vector<CampaignQuestLine>::const_iterator const& itr, CampaignQuestLinesSentinel const& end)
{
return itr == end.End || itr->CampaignId != end.CampaignId;
}
};
Trinity::IteratorPair<std::vector<CampaignQuestLine>::iterator, CampaignQuestLinesSentinel> GetQuestLinesForCampaign(uint32 campaignId)
{
return Trinity::Containers::MakeIteratorPair(
std::ranges::lower_bound(CampaignQuestLines, campaignId, std::ranges::less(), &CampaignQuestLine::CampaignId),
CampaignQuestLinesSentinel{ .End = CampaignQuestLines.end(), .CampaignId = campaignId });
}
}
void QuestMgr::Load()
{
for (CampaignXQuestLineEntry const* campaignQuestLine : sCampaignXQuestLineStore)
{
if (CampaignEntry const* campaign = sCampaignStore.LookupEntry(campaignQuestLine->CampaignID))
{
CampaignsByQuestLine[campaignQuestLine->QuestLineID].push_back(campaign);
CampaignQuestLines.push_back({ .CampaignId = campaignQuestLine->CampaignID, .QuestLineId = campaignQuestLine->QuestLineID });
}
}
for (QuestLineXQuestEntry const* questLineQuest : sQuestLineXQuestStore)
{
QuestsByQuestLine[questLineQuest->QuestLineID].push_back(questLineQuest);
QuestLineData& questLineData = QuestLineDataByQuest[questLineQuest->QuestID].emplace_back();
questLineData.QuestLineQuest = questLineQuest;
questLineData.Campaigns = Trinity::Containers::MapGetValuePtr(CampaignsByQuestLine, questLineQuest->QuestLineID);
}
std::ranges::sort(CampaignQuestLines);
for (auto& [_, questLineQuests] : QuestsByQuestLine)
std::ranges::sort(questLineQuests, std::ranges::less(), &QuestLineXQuestEntry::OrderIndex);
}
@@ -99,3 +152,67 @@ void QuestMgr::SkipQuestLineForPlayer(uint32 questLineId, Player* player)
std::ranges::transform(questLineQuests, questIds.begin(), &QuestLineXQuestEntry::QuestID);
player->SkipQuests(questIds);
}
bool QuestMgr::IsCampaignCompletedByPlayer(uint32 campaignId, Player const* player)
{
auto questLines = GetQuestLinesForCampaign(campaignId);
if (questLines.begin() == questLines.end())
return false;
for (CampaignQuestLine const& campaignQuestLine : questLines)
if (!IsQuestLineCompletedByPlayer(campaignQuestLine.QuestLineId, player))
return false;
// all questlines completed
return true;
}
bool QuestMgr::IsCampaignQuestStatusVisibleForPlayer(uint32 questId, Player const* player)
{
auto itr = QuestLineDataByQuest.find(questId);
if (itr == QuestLineDataByQuest.end())
return false;
for (QuestLineData const& questLineData : itr->second)
{
if (!questLineData.Campaigns)
continue;
for (CampaignEntry const* campaign : *questLineData.Campaigns)
{
if (campaign->HasFlag(CampaignFlags::DontUseJourneyQuestBang))
continue;
if (!ConditionMgr::IsPlayerMeetingCondition(player, campaign->Prerequisite))
continue;
if (!ConditionMgr::IsPlayerMeetingCondition(player, campaign->Stalled))
continue;
if (campaign->Completed && ConditionMgr::IsPlayerMeetingCondition(player, campaign->Completed))
continue;
if (!ConditionMgr::IsPlayerMeetingCondition(player, campaign->OnlyStallIf))
continue;
return true;
}
}
return false;
}
void QuestMgr::SkipCampaignForPlayer(uint32 campaignId, Player* player)
{
std::vector<uint32> questIds;
for (CampaignQuestLine const& campaignQuestLine : GetQuestLinesForCampaign(campaignId))
{
std::ptrdiff_t oldSize = std::ssize(questIds);
std::span<QuestLineXQuestEntry const* const> questLineQuests = GetQuestsForQuestLine(campaignQuestLine.QuestLineId);
questIds.resize(oldSize + questLineQuests.size());
std::ranges::transform(questLineQuests, questIds.begin() + oldSize, &QuestLineXQuestEntry::QuestID);
}
player->SkipQuests(questIds);
}

View File

@@ -41,6 +41,13 @@ struct QuestLineStats { uint32 Completed = 0; uint32 Total = 0; };
TC_GAME_API QuestLineStats GetQuestLineStatsForPlayer(uint32 questLineId, Player const* player);
TC_GAME_API void SkipQuestLineForPlayer(uint32 questLineId, Player* player);
// Campaign
TC_GAME_API bool IsCampaignCompletedByPlayer(uint32 campaignId, Player const* player);
TC_GAME_API bool IsCampaignQuestStatusVisibleForPlayer(uint32 questId, Player const* player);
TC_GAME_API void SkipCampaignForPlayer(uint32 campaignId, Player* player);
}
#endif // TRINITYCORE_CAMPAIGN_MGR_H

View File

@@ -445,6 +445,7 @@ class TC_GAME_API Spell
void EffectLearnAzeriteEssencePower();
void EffectCreatePrivateConversation();
void EffectApplyMountEquipment();
void EffectSkipCampaign();
void EffectSendChatMessage();
void EffectGrantBattlePetExperience();
void EffectLearnTransmogIllusion();

View File

@@ -372,7 +372,7 @@ NonDefaultConstructible<SpellEffectHandlerFn> SpellEffectHandlers[TOTAL_SPELL_EF
&Spell::EffectUnused, //280 SPELL_EFFECT_280
&Spell::EffectNULL, //281 SPELL_EFFECT_LEARN_SOULBIND_CONDUIT
&Spell::EffectNULL, //282 SPELL_EFFECT_CONVERT_ITEMS_TO_CURRENCY
&Spell::EffectNULL, //283 SPELL_EFFECT_COMPLETE_CAMPAIGN
&Spell::EffectSkipCampaign, //283 SPELL_EFFECT_COMPLETE_CAMPAIGN
&Spell::EffectSendChatMessage, //284 SPELL_EFFECT_SEND_CHAT_MESSAGE
&Spell::EffectNULL, //285 SPELL_EFFECT_MODIFY_KEYSTONE_2
&Spell::EffectGrantBattlePetExperience, //286 SPELL_EFFECT_GRANT_BATTLEPET_EXPERIENCE
@@ -6053,6 +6053,18 @@ void Spell::EffectApplyMountEquipment()
playerTarget->SendDirectMessage(applyMountEquipmentResult.Write());
}
void Spell::EffectSkipCampaign()
{
if (effectHandleMode != SPELL_EFFECT_HANDLE_HIT_TARGET)
return;
Player* target = Object::ToPlayer(unitTarget);
if (!target)
return;
QuestMgr::SkipCampaignForPlayer(effectInfo->MiscValue, target);
}
void Spell::EffectSendChatMessage()
{
if (effectHandleMode != SPELL_EFFECT_HANDLE_HIT_TARGET)