feat(Core/WorldState): init Scourge Invasion pre-wrath event (#22286)

Co-authored-by: Orozxy <crusadermeile@gmail.com>
Co-authored-by: Killerwife <killerwife@gmail.com>
Co-authored-by: Ryan Turner <AugustoMendes4426@gmail.com>
This commit is contained in:
Jelle Meeus
2025-07-12 20:54:18 +02:00
committed by GitHub
parent 68bac6ee0c
commit 8e083fbde3
13 changed files with 8947 additions and 5 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
--
-- Creates the reference loot (40110) for Haunted Memento (40110)
DELETE FROM `reference_loot_template` WHERE `Entry` = 40110 AND `Item` = 40110;
INSERT INTO `reference_loot_template` (`Entry`, `Item`, `Reference`, `Chance`, `QuestRequired`, `LootMode`, `GroupId`, `MinCount`, `MaxCount`, `Comment`) VALUES
(40110, 40110, 0, 100, 0, 1, 1, 1, 1, 'Scourge Invasion Event - Item: Haunted Memento');
-- Adds the reference loot (40110) above for the Haunted Memento (40110) to Ghoul Berserker (16141), Skeletal Shocktrooper (16298) and Spectral Soldier (16299)
DELETE FROM `creature_loot_template` WHERE `Entry` IN (16141, 16298, 16299) AND `Item` = 40110 AND `Reference` = 40110;
INSERT INTO `creature_loot_template` (`Entry`, `Item`, `Reference`, `Chance`, `QuestRequired`, `LootMode`, `GroupId`, `MinCount`, `MaxCount`, `Comment`) VALUES
(16141, 40110, 40110, 2, 0, 1, 1, 1, 1, 'Reference Loot: Scourge Invasion Event - Item: Haunted Memento'),
(16298, 40110, 40110, 2, 0, 1, 1, 1, 1, 'Reference Loot: Scourge Invasion Event - Item: Haunted Memento'),
(16299, 40110, 40110, 2, 0, 1, 1, 1, 1, 'Reference Loot: Scourge Invasion Event - Item: Haunted Memento');

View File

@@ -573,7 +573,7 @@ bool Condition::Meets(ConditionSourceInfo& sourceInfo)
}
case CONDITION_WORLD_SCRIPT:
{
condMeets = sWorldState->IsConditionFulfilled(static_cast<WorldStateCondition>(ConditionValue1), static_cast<WorldStateConditionState>(ConditionValue2));
condMeets = sWorldState->IsConditionFulfilled(ConditionValue1, ConditionValue2);
break;
}
default:

View File

@@ -2511,6 +2511,13 @@ void WorldObject::GetGameObjectListWithEntryInGrid(std::list<GameObject*>& gameo
Cell::VisitGridObjects(this, searcher, maxSearchRange);
}
void WorldObject::GetGameObjectListWithEntryInGrid(std::list<GameObject*>& gameobjectList, std::vector<uint32> const& entries, float maxSearchRange) const
{
Acore::AllGameObjectsMatchingOneEntryInRange check(this, entries, maxSearchRange);
Acore::GameObjectListSearcher searcher(this, gameobjectList, check);
Cell::VisitGridObjects(this, searcher, maxSearchRange);
}
void WorldObject::GetCreatureListWithEntryInGrid(std::list<Creature*>& creatureList, uint32 entry, float maxSearchRange) const
{
Acore::AllCreaturesOfEntryInRange check(this, entry, maxSearchRange);
@@ -2518,6 +2525,13 @@ void WorldObject::GetCreatureListWithEntryInGrid(std::list<Creature*>& creatureL
Cell::VisitGridObjects(this, searcher, maxSearchRange);
}
void WorldObject::GetCreatureListWithEntryInGrid(std::list<Creature*>& creatureList, std::vector<uint32> const& entries, float maxSearchRange) const
{
Acore::AllCreaturesMatchingOneEntryInRange check(this, entries, maxSearchRange);
Acore::CreatureListSearcher searcher(this, creatureList, check);
Cell::VisitGridObjects(this, searcher, maxSearchRange);
}
void WorldObject::GetDeadCreatureListInGrid(std::list<Creature*>& creaturedeadList, float maxSearchRange, bool alive /*= false*/) const
{
Acore::AllDeadCreaturesInRange check(this, maxSearchRange, alive);

View File

@@ -604,7 +604,9 @@ public:
[[nodiscard]] Player* SelectNearestPlayer(float distance = 0) const;
void GetGameObjectListWithEntryInGrid(std::list<GameObject*>& lList, uint32 uiEntry, float fMaxSearchRange) const;
void GetGameObjectListWithEntryInGrid(std::list<GameObject*>& gameobjectList, std::vector<uint32> const& entries, float maxSearchRange) const;
void GetCreatureListWithEntryInGrid(std::list<Creature*>& lList, uint32 uiEntry, float fMaxSearchRange) const;
void GetCreatureListWithEntryInGrid(std::list<Creature*>& creatureList, std::vector<uint32> const& entries, float maxSearchRange) const;
void GetDeadCreatureListInGrid(std::list<Creature*>& lList, float maxSearchRange, bool alive = false) const;
void DestroyForNearbyPlayers();

View File

@@ -1447,6 +1447,24 @@ namespace Acore
float m_fRange;
};
class AllGameObjectsMatchingOneEntryInRange
{
public:
AllGameObjectsMatchingOneEntryInRange(WorldObject const* object, std::vector<uint32> entries, float maxRange) : m_pObject(object), m_uiEntries(entries), m_fRange(maxRange) {}
bool operator()(GameObject *go)
{
if (std::ranges::any_of( m_uiEntries, [go](uint32 entry) { return go->GetEntry() == entry; }) && m_pObject->IsWithinDist(go, m_fRange, false))
return true;
return false;
}
private:
WorldObject const* m_pObject;
std::vector<uint32> m_uiEntries;
float m_fRange;
};
class AllCreaturesOfEntryInRange
{
public:
@@ -1465,6 +1483,24 @@ namespace Acore
float m_fRange;
};
class AllCreaturesMatchingOneEntryInRange
{
public:
AllCreaturesMatchingOneEntryInRange(WorldObject const* object, std::vector<uint32> entries, float maxRange) : m_pObject(object), m_uiEntries(entries), m_fRange(maxRange) {}
bool operator()(Unit* unit)
{
if (std::ranges::any_of(m_uiEntries, [unit](uint32 entry) { return unit->GetEntry() == entry; }) && m_pObject->IsWithinDist(unit, m_fRange, false))
return true;
return false;
}
private:
WorldObject const* m_pObject;
std::vector<uint32> m_uiEntries;
float m_fRange;
};
class MostHPMissingGroupInRange
{
public:

View File

@@ -31,6 +31,7 @@ enum AreaTableIDs : uint32
AREA_ELWYNN_FOREST = 12,
AREA_DUROTAR = 14,
AREA_DUSTWALLOW_MARSH = 15,
AREA_AZSHARA = 16,
AREA_THE_BARRENS = 17,
AREA_WESTERN_PLAGUELANDS = 28,
AREA_STRANGLETHORN_VALE = 33,

View File

@@ -15,13 +15,18 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AreaDefines.h"
#include "CreatureAIImpl.h"
#include "GameEventMgr.h"
#include "Log.h"
#include "MapMgr.h"
#include "Player.h"
#include "SharedDefines.h"
#include "UnitAI.h"
#include "Weather.h"
#include "WorldState.h"
#include "WorldStateDefines.h"
#include <chrono>
WorldState* WorldState::instance()
{
@@ -87,12 +92,43 @@ void WorldState::Load()
}
break;
}
case SAVE_ID_SCOURGE_INVASION:
if (data.size())
{
try
{
uint32 state;
loadStream >> state;
m_siData.m_state = SIState(state);
for (TimePoint& m_timer : m_siData.m_timers)
{
uint64 time;
loadStream >> time;
m_timer = TimePoint(std::chrono::milliseconds(time));
}
loadStream >> m_siData.m_battlesWon >> m_siData.m_lastAttackZone;
for (unsigned int& i : m_siData.m_remaining)
loadStream >> i;
}
catch (std::exception& e)
{
LOG_ERROR("scripts", "WorldState::Load: Exception reading ScourgeInvasion data {}", e.what());
m_siData.Reset();
}
}
break;
}
} while (result->NextRow());
}
StartSunsReachPhase(true);
StartSunwellGatePhase();
HandleSunsReachSubPhaseTransition(m_sunsReachData.m_subphaseMask, true);
if (m_siData.m_state == STATE_1_ENABLED)
{
StartScourgeInvasion(false);
HandleDefendedZones();
}
}
void WorldState::LoadWorldStates()
@@ -156,6 +192,12 @@ void WorldState::Save(WorldStateSaveIds saveId)
SaveHelper(expansionData, SAVE_ID_QUEL_DANAS);
break;
}
case SAVE_ID_SCOURGE_INVASION:
{
std::string siData = m_siData.GetData();
SaveHelper(siData, SAVE_ID_SCOURGE_INVASION);
break;
}
default:
break;
}
@@ -167,7 +209,7 @@ void WorldState::SaveHelper(std::string& stringToSave, WorldStateSaveIds saveId)
CharacterDatabase.Execute("INSERT INTO world_state(Id,Data) VALUES('{}','{}')", saveId, stringToSave.data());
}
bool WorldState::IsConditionFulfilled(WorldStateCondition conditionId, WorldStateConditionState state) const
bool WorldState::IsConditionFulfilled(uint32 conditionId, uint32 state) const
{
switch (conditionId)
{
@@ -179,6 +221,18 @@ bool WorldState::IsConditionFulfilled(WorldStateCondition conditionId, WorldStat
case WORLD_STATE_CONDITION_THE_PURPLE_PRINCESS:
case WORLD_STATE_CONDITION_THE_THUNDERCALLER:
return _transportStates.at(conditionId) == state;
case WORLD_STATE_SCOURGE_INVASION_WINTERSPRING:
return GetSIRemaining(SI_REMAINING_WINTERSPRING) > 0;
case WORLD_STATE_SCOURGE_INVASION_AZSHARA:
return GetSIRemaining(SI_REMAINING_AZSHARA) > 0;
case WORLD_STATE_SCOURGE_INVASION_BLASTED_LANDS:
return GetSIRemaining(SI_REMAINING_BLASTED_LANDS) > 0;
case WORLD_STATE_SCOURGE_INVASION_BURNING_STEPPES:
return GetSIRemaining(SI_REMAINING_BURNING_STEPPES) > 0;
case WORLD_STATE_SCOURGE_INVASION_TANARIS:
return GetSIRemaining(SI_REMAINING_TANARIS) > 0;
case WORLD_STATE_SCOURGE_INVASION_EASTERN_PLAGUELANDS:
return GetSIRemaining(SI_REMAINING_EASTERN_PLAGUELANDS) > 0;
default:
LOG_ERROR("scripts", "WorldState::IsConditionFulfilled: Unhandled WorldStateCondition {}", conditionId);
return false;
@@ -190,6 +244,18 @@ void WorldState::HandleConditionStateChange(WorldStateCondition conditionId, Wor
_transportStates[conditionId] = state;
}
Map* WorldState::GetMap(uint32 mapId, Position const& invZone)
{
Map* map = sMapMgr->FindBaseMap(mapId);
if (!map)
LOG_ERROR("scripts",
"ScourgeInvasionEvent::GetMap found no map with mapId {} , x: {}, y: {}.",
mapId,
invZone.GetPositionX(),
invZone.GetPositionY());
return map;
}
void WorldState::HandleExternalEvent(WorldStateEvent eventId, uint32 param)
{
std::lock_guard<std::mutex> guard(_mutex);
@@ -245,6 +311,32 @@ void WorldState::Update(uint32 diff)
_adalSongOfBattleTimer -= diff;
}
}
if (m_siData.m_state != STATE_0_DISABLED)
{
if (m_siData.m_broadcastTimer <= diff)
{
m_siData.m_broadcastTimer = 10000;
BroadcastSIWorldstates();
if (m_siData.m_state == STATE_1_ENABLED)
{
for (auto& zone : m_siData.m_cityAttacks)
{
if (zone.second.zoneId == AREA_UNDERCITY)
StartNewCityAttackIfTime(SI_TIMER_UNDERCITY, zone.second.zoneId);
else if (zone.second.zoneId == AREA_STORMWIND_CITY)
StartNewCityAttackIfTime(SI_TIMER_STORMWIND, zone.second.zoneId);
}
TimePoint now = std::chrono::steady_clock::now();
for (auto& zone : m_siData.m_activeInvasions)
HandleActiveZone(GetTimerIdForZone(zone.second.zoneId), zone.second.zoneId, zone.second.remainingNecropoli, now);
}
}
else
m_siData.m_broadcastTimer -= diff;
}
}
void WorldState::HandlePlayerEnterZone(Player* player, AreaTableIDs zoneId)
@@ -939,6 +1031,67 @@ std::string WorldState::GetSunsReachPrintout()
return output;
}
std::string WorldState::GetScourgeInvasionPrintout()
{
std::lock_guard<std::mutex> guard(m_siData.m_siMutex);
std::string output = "Scourge Invasion Status:\n";
auto formatState = [this]() -> std::string
{
switch (m_siData.m_state)
{
case STATE_0_DISABLED:
return "Disabled";
case STATE_1_ENABLED:
return "Enabled";
default:
return "Unknown";
}
};
auto formatRemaining = [this](SIRemaining val, char const* name) { return " " + std::string(name) + ": " + std::to_string(m_siData.m_remaining[val]) + "\n"; };
output += "State: " + formatState() + " (" + std::to_string(static_cast<uint32>(m_siData.m_state)) + ")\n";
output += "Battles Won: " + std::to_string(m_siData.m_battlesWon) + "\n";
output += "Last Attack Zone ID: " + std::to_string(m_siData.m_lastAttackZone) + "\n";
output += "Zone Necropolis Count Remaining:\n";
output += formatRemaining(SI_REMAINING_WINTERSPRING, "Winterspring");
output += formatRemaining(SI_REMAINING_AZSHARA, "Azshara");
output += formatRemaining(SI_REMAINING_BLASTED_LANDS, "Blasted Lands");
output += formatRemaining(SI_REMAINING_BURNING_STEPPES, "Burning Steppes");
output += formatRemaining(SI_REMAINING_TANARIS, "Tanaris");
output += formatRemaining(SI_REMAINING_EASTERN_PLAGUELANDS, "Eastern Plaguelands");
output += "Zone Timers (time until next event):\n";
TimePoint now = std::chrono::steady_clock::now();
auto formatTimer = [this](SITimers timerId, char const* name, TimePoint now)
{
TimePoint tp = m_siData.m_timers[timerId];
std::string timerStr;
if (tp.time_since_epoch().count() == 0)
timerStr = "0 (not set)";
else if (tp <= now)
timerStr = "Elapsed";
else
{
auto diff = std::chrono::duration_cast<std::chrono::seconds>(tp - now);
timerStr = std::to_string(diff.count()) + "s remaining";
}
return " " + std::string(name) + ": " + timerStr + "\n";
};
output += formatTimer(SI_TIMER_WINTERSPRING, "Winterspring Invasion", now);
output += formatTimer(SI_TIMER_AZSHARA, "Azshara Invasion", now);
output += formatTimer(SI_TIMER_BLASTED_LANDS, "Blasted Lands Invasion", now);
output += formatTimer(SI_TIMER_BURNING_STEPPES, "Burning Steppes Invasion", now);
output += formatTimer(SI_TIMER_TANARIS, "Tanaris Invasion", now);
output += formatTimer(SI_TIMER_EASTERN_PLAGUELANDS, "Eastern Plaguelands Invasion", now);
output += formatTimer(SI_TIMER_STORMWIND, "Stormwind City Attack", now);
output += formatTimer(SI_TIMER_UNDERCITY, "Undercity Attack", now);
return output;
}
std::string SunsReachReclamationData::GetData()
{
std::string output = std::to_string(m_phase) + " " + std::to_string(m_subphaseMask);
@@ -1087,8 +1240,726 @@ uint32 SunsReachReclamationData::GetSunwellGatePercentage(uint32 gate)
return percentage < 0 ? 0 : uint32(percentage);
}
void WorldState::SetScourgeInvasionState(SIState state)
{
SIState oldState = m_siData.m_state;
if (oldState == state)
return;
m_siData.m_state = state;
if (oldState == STATE_0_DISABLED)
StartScourgeInvasion(true);
else if (state == STATE_0_DISABLED)
StopScourgeInvasion();
Save(SAVE_ID_SCOURGE_INVASION);
}
void WorldState::SendScourgeInvasionMail()
{
QueryResult result = CharacterDatabase.Query("SELECT guid FROM characters WHERE level >= 50");
if (result)
{
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
MailDraft draft(MAIL_TEMPLATE_ARGENT_DAWN_NEEDS_YOUR_HELP);
uint32 count = 0;
do
{
Field* fields = result->Fetch();
ObjectGuid playerGUID = ObjectGuid::Create<HighGuid::Player>(fields[0].Get<uint32>());
// Add item manually. SendMailTo does not add items for offline players
if (Item* item = Item::CreateItem(ITEM_A_LETTER_FROM_THE_KEEPER_OF_THE_ROLLS, 1))
{
item->SaveToDB(trans);
draft.AddItem(item);
}
draft.SendMailTo(trans, MailReceiver(playerGUID.GetCounter()), NPC_ARGENT_EMISSARY, MAIL_CHECK_MASK_HAS_BODY);
++count;
} while (result->NextRow());
CharacterDatabase.CommitTransaction(trans);
LOG_INFO("WorldState", "SendScourgeInvasionMail sent to {} characters", count);
}
}
void WorldState::StartScourgeInvasion(bool sendMail)
{
sGameEventMgr->StartEvent(GAME_EVENT_SCOURGE_INVASION, false);
if (sendMail)
SendScourgeInvasionMail();
BroadcastSIWorldstates();
if (m_siData.m_state == STATE_1_ENABLED)
{
for (auto& zone : m_siData.m_cityAttacks)
{
if (zone.second.zoneId == AREA_UNDERCITY)
StartNewCityAttackIfTime(SI_TIMER_UNDERCITY, zone.second.zoneId);
else if (zone.second.zoneId == AREA_STORMWIND_CITY)
StartNewCityAttackIfTime(SI_TIMER_STORMWIND, zone.second.zoneId);
}
// randomization of init so that not every invasion starts the same way
std::vector<uint32> randomIds;
randomIds.reserve(m_siData.m_activeInvasions.size());
for (auto const& [zoneId, _] : m_siData.m_activeInvasions)
randomIds.push_back(zoneId);
Acore::Containers::RandomShuffle(randomIds);
for (uint32 id : randomIds)
OnEnable(m_siData.m_activeInvasions[id]);
}
}
ScourgeInvasionData::ScourgeInvasionData()
: m_state(STATE_0_DISABLED), m_battlesWon(0), m_lastAttackZone(0), m_remaining{}, m_broadcastTimer(10000)
{
m_activeInvasions.emplace(
AREA_WINTERSPRING,
InvasionZone{
.map = MAP_KALIMDOR,
.zoneId = AREA_WINTERSPRING,
.necropolisCount = 3,
.remainingNecropoli = SI_REMAINING_WINTERSPRING,
.mouth = { Position{7736.56f, -4033.75f, 696.327f, 5.51524f} }
}
);
m_activeInvasions.emplace(
AREA_TANARIS,
InvasionZone{
.map = MAP_KALIMDOR,
.zoneId = AREA_TANARIS,
.necropolisCount = 3,
.remainingNecropoli = SI_REMAINING_TANARIS,
.mouth = { Position{-8352.68f, -3972.68f, 10.0753f, 2.14675f} }
}
);
m_activeInvasions.emplace(
AREA_AZSHARA,
InvasionZone{
.map = MAP_KALIMDOR,
.zoneId = AREA_AZSHARA,
.necropolisCount = 2,
.remainingNecropoli = SI_REMAINING_AZSHARA,
.mouth = { Position{3273.75f, -4276.98f, 125.509f, 5.44543f} }
}
);
m_activeInvasions.emplace(
AREA_BLASTED_LANDS,
InvasionZone{
.map = MAP_EASTERN_KINGDOMS,
.zoneId = AREA_BLASTED_LANDS,
.necropolisCount = 2,
.remainingNecropoli = SI_REMAINING_BLASTED_LANDS,
.mouth = { Position{-11429.3f, -3327.82f, 7.73628f, 1.0821f} }
}
);
m_activeInvasions.emplace(
AREA_EASTERN_PLAGUELANDS,
InvasionZone{
.map = MAP_EASTERN_KINGDOMS,
.zoneId = AREA_EASTERN_PLAGUELANDS,
.necropolisCount = 2,
.remainingNecropoli = SI_REMAINING_EASTERN_PLAGUELANDS,
.mouth = { Position{2014.55f, -4934.52f, 73.9846f, 0.0698132f} }
}
);
m_activeInvasions.emplace(
AREA_BURNING_STEPPES,
InvasionZone{
.map = MAP_EASTERN_KINGDOMS,
.zoneId = AREA_BURNING_STEPPES,
.necropolisCount = 2,
.remainingNecropoli = SI_REMAINING_BURNING_STEPPES,
.mouth = { Position{-8229.53f, -1118.11f, 144.012f, 6.17846f} }
}
);
m_cityAttacks.emplace(
AREA_UNDERCITY,
CityAttack{
.map = MAP_EASTERN_KINGDOMS,
.zoneId = AREA_UNDERCITY,
.pallid = {
Position{1595.87f, 440.539f, -46.3349f, 2.28207f}, // Royal Quarter
Position{1659.2f, 265.988f, -62.1788f, 3.64283f} // Trade Quarter
}
}
);
m_cityAttacks.emplace(
AREA_STORMWIND_CITY,
CityAttack{
.map = MAP_EASTERN_KINGDOMS,
.zoneId = AREA_STORMWIND_CITY,
.pallid = {
Position{-8578.15f, 886.382f, 87.3148f, 0.586275f}, // Stormwind Keep
Position{-8578.15f, 886.382f, 87.3148f, 0.586275f} // Trade District
}
}
);
}
void ScourgeInvasionData::Reset()
{
std::lock_guard<std::mutex> guard(m_siMutex);
for (auto& timepoint : m_timers)
timepoint = TimePoint();
m_battlesWon = 0;
m_lastAttackZone = 0;
m_broadcastTimer = 10000;
memset(m_remaining, 0, sizeof(m_remaining));
}
std::string ScourgeInvasionData::GetData()
{
std::string output = std::to_string(m_state) + " ";
for (auto& timer : m_timers)
output += std::to_string(timer.time_since_epoch().count()) + " ";
output += std::to_string(m_battlesWon) + " " + std::to_string(m_lastAttackZone) + " ";
for (auto& remaining : m_remaining)
output += std::to_string(remaining) + " ";
return output;
}
void WorldState::StopScourgeInvasion()
{
sGameEventMgr->StopEvent(GAME_EVENT_SCOURGE_INVASION);
sGameEventMgr->StopEvent(GAME_EVENT_SCOURGE_INVASION_WINTERSPRING);
sGameEventMgr->StopEvent(GAME_EVENT_SCOURGE_INVASION_TANARIS);
sGameEventMgr->StopEvent(GAME_EVENT_SCOURGE_INVASION_AZSHARA);
sGameEventMgr->StopEvent(GAME_EVENT_SCOURGE_INVASION_BLASTED_LANDS);
sGameEventMgr->StopEvent(GAME_EVENT_SCOURGE_INVASION_EASTERN_PLAGUELANDS);
sGameEventMgr->StopEvent(GAME_EVENT_SCOURGE_INVASION_BURNING_STEPPES);
BroadcastSIWorldstates();
m_siData.Reset();
for (auto& [_, cityData] : m_siData.m_cityAttacks)
OnDisable(cityData);
for (auto& [_, zoneData] : m_siData.m_activeInvasions)
OnDisable(zoneData);
}
uint32 WorldState::GetSIRemaining(SIRemaining remaining) const
{
return m_siData.m_remaining[remaining];
}
uint32 WorldState::GetSIRemainingByZone(uint32 zoneId) const
{
SIRemaining remainingId;
switch (zoneId)
{
case AREA_WINTERSPRING: remainingId = SI_REMAINING_WINTERSPRING; break;
case AREA_AZSHARA: remainingId = SI_REMAINING_AZSHARA; break;
case AREA_EASTERN_PLAGUELANDS: remainingId = SI_REMAINING_EASTERN_PLAGUELANDS; break;
case AREA_BLASTED_LANDS: remainingId = SI_REMAINING_BLASTED_LANDS; break;
case AREA_BURNING_STEPPES: remainingId = SI_REMAINING_BURNING_STEPPES; break;
case AREA_TANARIS: remainingId = SI_REMAINING_TANARIS; break;
default:
LOG_ERROR("WorldState", "GetSIRemainingByZone called with invalid zone ID: {}", zoneId);
return 0;
}
return GetSIRemaining(remainingId);
}
void WorldState::SetSIRemaining(SIRemaining remaining, uint32 value)
{
std::lock_guard<std::mutex> guard(m_siData.m_siMutex);
m_siData.m_remaining[remaining] = value;
Save(SAVE_ID_SCOURGE_INVASION);
}
TimePoint WorldState::GetSITimer(SITimers timer)
{
return m_siData.m_timers[timer];
}
void WorldState::SetSITimer(SITimers timer, TimePoint timePoint)
{
m_siData.m_timers[timer] = timePoint;
}
uint32 WorldState::GetBattlesWon()
{
std::lock_guard<std::mutex> guard(m_siData.m_siMutex);
return m_siData.m_battlesWon;
}
void WorldState::AddBattlesWon(int32 count)
{
std::lock_guard<std::mutex> guard(m_siData.m_siMutex);
m_siData.m_battlesWon += count;
HandleDefendedZones();
Save(SAVE_ID_SCOURGE_INVASION);
}
uint32 WorldState::GetLastAttackZone()
{
std::lock_guard<std::mutex> guard(m_siData.m_siMutex);
return m_siData.m_lastAttackZone;
}
void WorldState::SetLastAttackZone(uint32 zoneId)
{
std::lock_guard<std::mutex> guard(m_siData.m_siMutex);
m_siData.m_lastAttackZone = zoneId;
}
void WorldState::BroadcastSIWorldstates()
{
uint32 victories = GetBattlesWon();
uint32 remainingAzshara = GetSIRemaining(SI_REMAINING_AZSHARA);
uint32 remainingBlastedLands = GetSIRemaining(SI_REMAINING_BLASTED_LANDS);
uint32 remainingBurningSteppes = GetSIRemaining(SI_REMAINING_BURNING_STEPPES);
uint32 remainingEasternPlaguelands = GetSIRemaining(SI_REMAINING_EASTERN_PLAGUELANDS);
uint32 remainingTanaris = GetSIRemaining(SI_REMAINING_TANARIS);
uint32 remainingWinterspring = GetSIRemaining(SI_REMAINING_WINTERSPRING);
sMapMgr->DoForAllMaps([&](Map* map) -> void
{
switch (map->GetId())
{
case MAP_EASTERN_KINGDOMS:
case MAP_KALIMDOR:
map->DoForAllPlayers([&](Player* pl)
{
// do not process players which are not in world
if (!pl->IsInWorld())
return;
pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_INVASION_AZSHARA, remainingAzshara > 0 ? 1 : 0);
pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_INVASION_BLASTED_LANDS, remainingBlastedLands > 0 ? 1 : 0);
pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_INVASION_BURNING_STEPPES, remainingBurningSteppes > 0 ? 1 : 0);
pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_INVASION_EASTERN_PLAGUELANDS, remainingEasternPlaguelands > 0 ? 1 : 0);
pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_INVASION_TANARIS, remainingTanaris > 0 ? 1 : 0);
pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_INVASION_WINTERSPRING, remainingWinterspring > 0 ? 1 : 0);
pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_INVASION_VICTORIES, victories);
pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_INVASION_NECROPOLIS_AZSHARA, remainingAzshara);
pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_INVASION_NECROPOLIS_BLASTED_LANDS, remainingBlastedLands);
pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_INVASION_NECROPOLIS_BURNING_STEPPES, remainingBurningSteppes);
pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_INVASION_NECROPOLIS_EASTERN_PLAGUELANDS, remainingEasternPlaguelands);
pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_INVASION_NECROPOLIS_TANARIS, remainingTanaris);
pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_INVASION_NECROPOLIS_WINTERSPRING, remainingWinterspring);
});
default:
break;
}
});
}
void WorldState::HandleDefendedZones()
{
if (m_siData.m_battlesWon < 50)
{
sGameEventMgr->StopEvent(GAME_EVENT_SCOURGE_INVASION_50_INVASIONS);
sGameEventMgr->StopEvent(GAME_EVENT_SCOURGE_INVASION_100_INVASIONS);
sGameEventMgr->StopEvent(GAME_EVENT_SCOURGE_INVASION_150_INVASIONS);
}
else if (m_siData.m_battlesWon >= 50 && m_siData.m_battlesWon < 100)
sGameEventMgr->StartEvent(GAME_EVENT_SCOURGE_INVASION_50_INVASIONS);
else if (m_siData.m_battlesWon >= 100 && m_siData.m_battlesWon < 150)
{
sGameEventMgr->StopEvent(GAME_EVENT_SCOURGE_INVASION_50_INVASIONS);
sGameEventMgr->StartEvent(GAME_EVENT_SCOURGE_INVASION_100_INVASIONS);
}
else if (m_siData.m_battlesWon >= 150)
{
sGameEventMgr->StopEvent(GAME_EVENT_SCOURGE_INVASION);
sGameEventMgr->StopEvent(GAME_EVENT_SCOURGE_INVASION_50_INVASIONS);
sGameEventMgr->StopEvent(GAME_EVENT_SCOURGE_INVASION_100_INVASIONS);
sGameEventMgr->StartEvent(GAME_EVENT_SCOURGE_INVASION_INVASIONS_DONE);
sGameEventMgr->StartEvent(GAME_EVENT_SCOURGE_INVASION_150_INVASIONS);
}
}
void WorldState::StartZoneEvent(SIZoneIds eventId)
{
switch (eventId)
{
case SI_ZONE_AZSHARA: StartNewInvasion(AREA_AZSHARA); break;
case SI_ZONE_BLASTED_LANDS: StartNewInvasion(AREA_BLASTED_LANDS); break;
case SI_ZONE_BURNING_STEPPES: StartNewInvasion(AREA_BURNING_STEPPES); break;
case SI_ZONE_EASTERN_PLAGUELANDS: StartNewInvasion(AREA_EASTERN_PLAGUELANDS); break;
case SI_ZONE_TANARIS: StartNewInvasion(AREA_TANARIS); break;
case SI_ZONE_WINTERSPRING: StartNewInvasion(AREA_WINTERSPRING); break;
case SI_ZONE_STORMWIND: StartNewCityAttack(AREA_STORMWIND_CITY); break;
case SI_ZONE_UNDERCITY: StartNewCityAttack(AREA_UNDERCITY); break;
default:
break;
}
}
void WorldState::StartNewInvasionIfTime(uint32 attackTimeVar, uint32 zoneId)
{
TimePoint now = std::chrono::steady_clock::now();
// Not yet time
if (now < sWorldState->GetSITimer(SITimers(attackTimeVar)))
return;
StartNewInvasion(zoneId);
}
void WorldState::StartNewCityAttackIfTime(uint32 attackTimeVar, uint32 zoneId)
{
TimePoint now = std::chrono::steady_clock::now();
// Not yet time
if (now < sWorldState->GetSITimer(SITimers(attackTimeVar)))
return;
StartNewCityAttack(zoneId);
uint32 cityAttackTimer = urand(CITY_ATTACK_TIMER_MIN, CITY_ATTACK_TIMER_MAX);
TimePoint next_attack = now + std::chrono::seconds(cityAttackTimer);
sWorldState->SetSITimer(SITimers(attackTimeVar), next_attack);
}
void WorldState::StartNewInvasion(uint32 zoneId)
{
if (IsActiveZone(zoneId))
return;
// Don't attack same zone as before.
if (zoneId == sWorldState->GetLastAttackZone())
return;
// If we have at least one victory and more than 1 active zones stop here.
if (GetActiveZones() > 1 && sWorldState->GetBattlesWon() > 0)
return;
LOG_DEBUG("gameevent", "Scourge Invasion Event: Starting new invasion in {}.", zoneId);
ScourgeInvasionData::InvasionZone& zone = m_siData.m_activeInvasions[zoneId];
Map* map = GetMap(zone.map, zone.mouth[0]);
if (!map)
{
LOG_ERROR("gameevent", "ScourgeInvasionEvent::StartNewInvasion unable to access required map ({}). Retrying next update.", zone.map);
return;
}
switch (zoneId)
{
case AREA_AZSHARA: sGameEventMgr->StartEvent(GAME_EVENT_SCOURGE_INVASION_AZSHARA); break;
case AREA_BLASTED_LANDS: sGameEventMgr->StartEvent(GAME_EVENT_SCOURGE_INVASION_BLASTED_LANDS); break;
case AREA_BURNING_STEPPES: sGameEventMgr->StartEvent(GAME_EVENT_SCOURGE_INVASION_BURNING_STEPPES); break;
case AREA_EASTERN_PLAGUELANDS: sGameEventMgr->StartEvent(GAME_EVENT_SCOURGE_INVASION_EASTERN_PLAGUELANDS); break;
case AREA_TANARIS: sGameEventMgr->StartEvent(GAME_EVENT_SCOURGE_INVASION_TANARIS); break;
case AREA_WINTERSPRING: sGameEventMgr->StartEvent(GAME_EVENT_SCOURGE_INVASION_WINTERSPRING); break;
default:
LOG_ERROR("gameevent", "ScourgeInvasionEvent::StartNewInvasion unknown zoneId {}.", zoneId);
return;
}
if (map)
SummonMouth(map, zone, zone.mouth[0], true);
}
void WorldState::StartNewCityAttack(uint32 zoneId)
{
LOG_DEBUG("gameevent", "Scourge Invasion Event: Starting new City attack in zone {}.", zoneId);
ScourgeInvasionData::CityAttack& zone = m_siData.m_cityAttacks[zoneId];
uint32 SpawnLocationID = urand(0, static_cast<uint32>(zone.pallid.size() - 1));
Map* map = GetMap(zone.map, zone.pallid[SpawnLocationID]);
// If any of the required maps are not available we return. Will cause the invasion to be started
// on next update instead
if (!map)
{
LOG_ERROR("gameevent", "ScourgeInvasionEvent::StartNewCityAttackIfTime unable to access required map (%{}). Retrying next update.", zone.map);
return;
}
if (m_siData.m_pendingPallids.find(zoneId) != m_siData.m_pendingPallids.end())
return;
if (map && SummonPallid(map, zone, zone.pallid[SpawnLocationID], SpawnLocationID))
LOG_DEBUG("gameevent", "ScourgeInvasionEvent::StartNewCityAttackIfTime pallid spawned in {}.", zone.map);
else
LOG_DEBUG("gameevent", "ScourgeInvasionEvent::StartNewCityAttackIfTime unable to spawn pallid in {}.", zone.map);
}
bool WorldState::ResumeInvasion(ScourgeInvasionData::InvasionZone& zone)
{
// Dont have a save variable to know which necropolises had already been destroyed, so we
// just summon the same amount, but not necessarily the same necropolises
LOG_DEBUG("gameevent", "Scourge Invasion Event: Resuming Scourge invasion in zone {}", zone.zoneId);
uint32 num_necropolises_remaining = sWorldState->GetSIRemaining(SIRemaining(zone.remainingNecropoli));
// Just making sure we can access all maps before starting the invasion
for (uint32 i = 0; i < num_necropolises_remaining; i++)
{
if (!GetMap(zone.map, zone.mouth[0]))
{
LOG_ERROR("gameevent", "ScourgeInvasionEvent::ResumeInvasion map {} not accessible. Retry next update.", zone.map);
return false;
}
}
Map* mapPtr = GetMap(zone.map, zone.mouth[0]);
if (!mapPtr)
{
LOG_ERROR("gameevent", "ScourgeInvasionEvent::ResumeInvasion failed getting map, even after making sure they were loaded....");
return false;
}
SummonMouth(mapPtr, zone, zone.mouth[0], false);
return true;
}
bool WorldState::SummonMouth(Map* map, ScourgeInvasionData::InvasionZone& zone, Position position, bool newInvasion)
{
AddPendingInvasion(zone.zoneId);
// Remove old mouth if required.
if (Creature* existingMouth = map->GetCreature(zone.mouthGuid))
existingMouth->AddObjectToRemoveList();
if (Creature* mouth = map->SummonCreature(NPC_HERALD_OF_THE_LICH_KING, position))
{
mouth->GetAI()->DoAction(EVENT_HERALD_OF_THE_LICH_KING_ZONE_START);
sWorldState->SetMouthGuid(zone.zoneId, mouth->GetGUID());
if (newInvasion)
sWorldState->SetSIRemaining(SIRemaining(zone.remainingNecropoli), zone.necropolisCount);
}
sWorldState->RemovePendingInvasion(zone.zoneId);
return true;
}
enum PallidHorrorPaths
{
PATH_STORMWIND_KEEP = 163941,
PATH_STORMWIND_TRADE_DISTRICT = 163942,
PATH_UNDERCITY_TRADE_QUARTER = 163943,
PATH_UNDERCITY_ROYAL_QUARTER = 163944,
};
bool WorldState::SummonPallid(Map* map, ScourgeInvasionData::CityAttack& zone, const Position& position, uint32 spawnLoc)
{
AddPendingPallid(zone.zoneId);
// Remove old pallid if required.
uint32 pathID = 0;
if (Creature* existingPallid = map->GetCreature(zone.pallidGuid))
existingPallid->AddObjectToRemoveList();
// if (Creature* pallid = map->SummonCreature(RAND(NPC_PALLID_HORROR, NPC_PATCHWORK_TERROR), position))
if (Creature* pallid = map->SummonCreature(NPC_PALLID_HORROR, position))
{
pallid->GetMotionMaster()->Clear(false);
if (pallid->GetZoneId() == AREA_UNDERCITY)
pathID = spawnLoc == 0 ? PATH_UNDERCITY_ROYAL_QUARTER : PATH_UNDERCITY_TRADE_QUARTER;
else
pathID = spawnLoc == 0 ? PATH_STORMWIND_KEEP : PATH_STORMWIND_TRADE_DISTRICT;
pallid->GetMotionMaster()->MovePath(pathID, false);
sWorldState->SetPallidGuid(zone.zoneId, pallid->GetGUID());
}
sWorldState->RemovePendingPallid(zone.zoneId);
return true;
}
void WorldState::HandleActiveZone(uint32 attackTimeVar, uint32 zoneId, uint32 remainingVar, TimePoint now)
{
TimePoint timePoint = sWorldState->GetSITimer(SITimers(attackTimeVar));
ScourgeInvasionData::InvasionZone& zone = m_siData.m_activeInvasions[zoneId];
Map* map = sMapMgr->FindMap(zone.map, 0);
if (zone.zoneId != zoneId)
return;
uint32 remaining = GetSIRemaining(SIRemaining(remainingVar));
// Calculate the next possible attack between ZONE_ATTACK_TIMER_MIN and ZONE_ATTACK_TIMER_MAX.
uint32 zoneAttackTimer = urand(ZONE_ATTACK_TIMER_MIN, ZONE_ATTACK_TIMER_MAX);
TimePoint next_attack = now + std::chrono::seconds(zoneAttackTimer);
uint64 timeToNextAttack = std::chrono::duration_cast<std::chrono::minutes>(next_attack-now).count();
if (zone.mouthGuid)
{
// Handles the inactive zone, without a Mouth of Kel'Thuzad summoned (which spawns the whole zone event).
Creature* mouth = map->GetCreature(zone.mouthGuid);
if (!mouth)
sWorldState->SetMouthGuid(zone.zoneId, ObjectGuid()); // delays spawning until next tick
// Handles the active zone that has no necropolis left.
else if (timePoint < now && remaining == 0)
{
sWorldState->SetSITimer(SITimers(attackTimeVar), next_attack);
sWorldState->AddBattlesWon(1);
sWorldState->SetLastAttackZone(zoneId);
LOG_INFO("gameevent", "[Scourge Invasion Event] The Scourge has been defeated in {}, next attack starting in {} minutes.", zoneId, timeToNextAttack);
LOG_DEBUG("gameevent", "[Scourge Invasion Event] {} victories", sWorldState->GetBattlesWon());
if (mouth)
mouth->GetAI()->DoAction(EVENT_HERALD_OF_THE_LICH_KING_ZONE_STOP);
else
LOG_ERROR("gameevent", "ScourgeInvasionEvent::HandleActiveZone ObjectGuid {} not found.", zone.mouthGuid.ToString());
}
}
else
{
// If more than one zones are alreay being attacked, set the timer again to ZONE_ATTACK_TIMER.
if (GetActiveZones() > 1)
sWorldState->SetSITimer(SITimers(attackTimeVar), next_attack);
// Try to start the zone if attackTimeVar is 0.
StartNewInvasionIfTime(attackTimeVar, zoneId);
}
}
void WorldState::SetPallidGuid(uint32 zoneId, ObjectGuid guid)
{
m_siData.m_cityAttacks[zoneId].pallidGuid = guid;
}
void WorldState::SetMouthGuid(uint32 zoneId, ObjectGuid guid)
{
m_siData.m_activeInvasions[zoneId].mouthGuid = guid;
}
void WorldState::AddPendingInvasion(uint32 zoneId)
{
std::lock_guard<std::mutex> guard(m_siData.m_siMutex);
m_siData.m_pendingInvasions.insert(zoneId);
}
void WorldState::RemovePendingInvasion(uint32 zoneId)
{
std::lock_guard<std::mutex> guard(m_siData.m_siMutex);
m_siData.m_pendingInvasions.erase(zoneId);
}
void WorldState::AddPendingPallid(uint32 zoneId)
{
std::lock_guard<std::mutex> guard(m_siData.m_siMutex);
m_siData.m_pendingPallids.insert(zoneId);
}
void WorldState::RemovePendingPallid(uint32 zoneId)
{
std::lock_guard<std::mutex> guard(m_siData.m_siMutex);
m_siData.m_pendingPallids.erase(zoneId);
}
void WorldState::OnEnable(ScourgeInvasionData::InvasionZone& zone)
{
// If there were remaining necropolises in the old zone before shutdown, we
// restore that zone
if (sWorldState->GetSIRemaining(SIRemaining(zone.remainingNecropoli)) > 0)
ResumeInvasion(zone);
// Otherwise we start a new Invasion
else
StartNewInvasionIfTime(GetTimerIdForZone(zone.zoneId), zone.zoneId);
}
void WorldState::OnDisable(ScourgeInvasionData::InvasionZone& zone)
{
if (!zone.mouthGuid)
return;
Map* map = GetMap(zone.map, zone.mouth[0]);
if (Creature* mouth = map->GetCreature(zone.mouthGuid))
mouth->DespawnOrUnsummon();
}
void WorldState::OnDisable(ScourgeInvasionData::CityAttack& zone)
{
if (!zone.pallidGuid)
return;
Map* map = GetMap(zone.map, zone.pallid[0]);
if (Creature* pallid = map->GetCreature(zone.pallidGuid))
pallid->DespawnOrUnsummon();
}
bool WorldState::IsActiveZone(uint32 /*zoneId*/)
{
return false;
}
// returns the amount of pending zones and active zones with a mouth creature on the map
uint32 WorldState::GetActiveZones()
{
size_t i = m_siData.m_pendingInvasions.size();
for (auto const& [zoneId, invasionData] : m_siData.m_activeInvasions)
{
Map* map = GetMap(invasionData.map, invasionData.mouth[0]);
if (!map)
{
LOG_ERROR("gameevent", "ScourgeInvasionEvent::GetActiveZones no map for zone {}.", invasionData.map);
continue;
}
Creature* mouth = map->GetCreature(invasionData.mouthGuid);
if (mouth)
i++;
}
return i;
}
uint32 WorldState::GetTimerIdForZone(uint32 zoneId)
{
uint32 attackTime = 0;
switch (zoneId)
{
case AREA_TANARIS: attackTime = SI_TIMER_TANARIS; break;
case AREA_BLASTED_LANDS: attackTime = SI_TIMER_BLASTED_LANDS; break;
case AREA_EASTERN_PLAGUELANDS: attackTime = SI_TIMER_EASTERN_PLAGUELANDS; break;
case AREA_BURNING_STEPPES: attackTime = SI_TIMER_BURNING_STEPPES; break;
case AREA_WINTERSPRING: attackTime = SI_TIMER_WINTERSPRING; break;
case AREA_AZSHARA: attackTime = SI_TIMER_AZSHARA; break;
default:
LOG_ERROR("gameevent", "ScourgeInvasionEvent::GetTimerIdForZone unknown zoneId {}.", zoneId);
return 0;
}
return attackTime;
}
void WorldState::FillInitialWorldStates(WorldPackets::WorldState::InitWorldStates& packet, uint32 zoneId, uint32 /*areaId*/)
{
if (m_siData.m_state != STATE_0_DISABLED) // scourge invasion active - need to send all worldstates
{
uint32 victories = GetBattlesWon();
uint32 remainingAzshara = GetSIRemaining(SI_REMAINING_AZSHARA);
uint32 remainingBlastedLands = GetSIRemaining(SI_REMAINING_BLASTED_LANDS);
uint32 remainingBurningSteppes = GetSIRemaining(SI_REMAINING_BURNING_STEPPES);
uint32 remainingEasternPlaguelands = GetSIRemaining(SI_REMAINING_EASTERN_PLAGUELANDS);
uint32 remainingTanaris = GetSIRemaining(SI_REMAINING_TANARIS);
uint32 remainingWinterspring = GetSIRemaining(SI_REMAINING_WINTERSPRING);
packet.Worldstates.reserve(13);
packet.Worldstates.emplace_back(WORLD_STATE_SCOURGE_INVASION_AZSHARA, remainingAzshara > 0 ? 1 : 0);
packet.Worldstates.emplace_back(WORLD_STATE_SCOURGE_INVASION_BLASTED_LANDS, remainingBlastedLands > 0 ? 1 : 0);
packet.Worldstates.emplace_back(WORLD_STATE_SCOURGE_INVASION_BURNING_STEPPES, remainingBurningSteppes > 0 ? 1 : 0);
packet.Worldstates.emplace_back(WORLD_STATE_SCOURGE_INVASION_EASTERN_PLAGUELANDS, remainingEasternPlaguelands > 0 ? 1 : 0);
packet.Worldstates.emplace_back(WORLD_STATE_SCOURGE_INVASION_TANARIS, remainingTanaris > 0 ? 1 : 0);
packet.Worldstates.emplace_back(WORLD_STATE_SCOURGE_INVASION_WINTERSPRING, remainingWinterspring > 0 ? 1 : 0);
packet.Worldstates.emplace_back(WORLD_STATE_SCOURGE_INVASION_VICTORIES, victories);
packet.Worldstates.emplace_back(WORLD_STATE_SCOURGE_INVASION_NECROPOLIS_AZSHARA, remainingAzshara);
packet.Worldstates.emplace_back(WORLD_STATE_SCOURGE_INVASION_NECROPOLIS_BLASTED_LANDS, remainingBlastedLands);
packet.Worldstates.emplace_back(WORLD_STATE_SCOURGE_INVASION_NECROPOLIS_BURNING_STEPPES, remainingBurningSteppes);
packet.Worldstates.emplace_back(WORLD_STATE_SCOURGE_INVASION_NECROPOLIS_EASTERN_PLAGUELANDS, remainingEasternPlaguelands);
packet.Worldstates.emplace_back(WORLD_STATE_SCOURGE_INVASION_NECROPOLIS_TANARIS, remainingTanaris);
packet.Worldstates.emplace_back(WORLD_STATE_SCOURGE_INVASION_NECROPOLIS_WINTERSPRING, remainingWinterspring);
}
switch (zoneId)
{
case AREA_ISLE_OF_QUEL_DANAS:
@@ -1136,5 +2007,7 @@ void WorldState::FillInitialWorldStates(WorldPackets::WorldState::InitWorldState
}
break;
}
default:
break;
}
}

View File

@@ -20,6 +20,7 @@
#include "AreaDefines.h"
#include "Player.h"
#include "WorldStateDefines.h"
#include <atomic>
enum WorldStateCondition
@@ -56,11 +57,33 @@ enum WorldStateSpells
enum WorldStateSaveIds
{
SAVE_ID_SCOURGE_INVASION = 3,
SAVE_ID_QUEL_DANAS = 20,
};
enum WorldStateGameEvents
{
// Scourge Invasion
GAME_EVENT_SCOURGE_INVASION = 17,
GAME_EVENT_SCOURGE_INVASION_WINTERSPRING = 121,
GAME_EVENT_SCOURGE_INVASION_TANARIS = 122,
GAME_EVENT_SCOURGE_INVASION_AZSHARA = 123,
GAME_EVENT_SCOURGE_INVASION_BLASTED_LANDS = 124,
GAME_EVENT_SCOURGE_INVASION_EASTERN_PLAGUELANDS = 125,
GAME_EVENT_SCOURGE_INVASION_BURNING_STEPPES = 126,
GAME_EVENT_SCOURGE_INVASION_50_INVASIONS = 127,
GAME_EVENT_SCOURGE_INVASION_100_INVASIONS = 128,
GAME_EVENT_SCOURGE_INVASION_150_INVASIONS = 129,
GAME_EVENT_SCOURGE_INVASION_INVASIONS_DONE = 130,
// Zombie infestation
GAME_EVENT_ZOMBIE_INFESTATION_PHASE_1 = 110,
GAME_EVENT_ZOMBIE_INFESTATION_PHASE_2 = 111,
GAME_EVENT_ZOMBIE_INFESTATION_PHASE_3 = 112,
GAME_EVENT_ZOMBIE_INFESTATION_PHASE_4 = 113,
GAME_EVENT_ZOMBIE_INFESTATION_PHASE_5 = 114,
GAME_EVENT_ZOMBIE_INFESTATION_PHASE_6 = 115,
// Isle phases
GAME_EVENT_QUEL_DANAS_PHASE_1 = 101,
GAME_EVENT_QUEL_DANAS_PHASE_2_ONLY = 102,
@@ -84,6 +107,110 @@ enum WorldStateGameEvents
GAME_EVENT_SWP_GATES_PHASE_3 = 119, // All Gates Open
};
enum SIMisc
{
EVENT_HERALD_OF_THE_LICH_KING_ZONE_START = 7,
EVENT_HERALD_OF_THE_LICH_KING_ZONE_STOP = 8,
NPC_PALLID_HORROR = 16394,
NPC_PATCHWORK_TERROR = 16382,
NPC_HERALD_OF_THE_LICH_KING = 16995,
ITEM_A_LETTER_FROM_THE_KEEPER_OF_THE_ROLLS = 22723,
NPC_ARGENT_EMISSARY = 16285,
MAIL_TEMPLATE_ARGENT_DAWN_NEEDS_YOUR_HELP = 171,
};
enum SIState : uint32
{
STATE_0_DISABLED,
STATE_1_ENABLED,
SI_STATE_MAX,
};
enum SIZoneIds
{
SI_ZONE_AZSHARA,
SI_ZONE_BLASTED_LANDS,
SI_ZONE_BURNING_STEPPES,
SI_ZONE_EASTERN_PLAGUELANDS,
SI_ZONE_TANARIS,
SI_ZONE_WINTERSPRING,
SI_ZONE_STORMWIND,
SI_ZONE_UNDERCITY
};
enum SITimers
{
SI_TIMER_AZSHARA,
SI_TIMER_BLASTED_LANDS,
SI_TIMER_BURNING_STEPPES,
SI_TIMER_EASTERN_PLAGUELANDS,
SI_TIMER_TANARIS,
SI_TIMER_WINTERSPRING,
SI_TIMER_STORMWIND,
SI_TIMER_UNDERCITY,
SI_TIMER_MAX,
};
enum SIRemaining
{
SI_REMAINING_AZSHARA,
SI_REMAINING_BLASTED_LANDS,
SI_REMAINING_BURNING_STEPPES,
SI_REMAINING_EASTERN_PLAGUELANDS,
SI_REMAINING_TANARIS,
SI_REMAINING_WINTERSPRING,
SI_REMAINING_MAX,
};
enum SICityTimers
{
// These timers may fail if you set it under 1 minute.
ZONE_ATTACK_TIMER_MIN = 60 * 45, // 45 min.
ZONE_ATTACK_TIMER_MAX = 60 * 60, // 60 min.
CITY_ATTACK_TIMER_MIN = 60 * 45, // 45 min.
CITY_ATTACK_TIMER_MAX = 60 * 60, // 60 min.
};
struct ScourgeInvasionData
{
struct InvasionZone
{
uint32 map;
uint32 zoneId;
uint32 necropolisCount;
uint32 remainingNecropoli;
std::vector<Position> mouth;
ObjectGuid mouthGuid {};
};
struct CityAttack
{
uint32 map;
uint32 zoneId;
std::vector<Position> pallid;
ObjectGuid pallidGuid {};
};
SIState m_state;
TimePoint m_timers[SI_TIMER_MAX];
uint32 m_battlesWon;
uint32 m_lastAttackZone;
uint32 m_remaining[SI_REMAINING_MAX];
uint64 m_broadcastTimer;
std::mutex m_siMutex;
std::set<uint32> m_pendingInvasions;
std::set<uint32> m_pendingPallids;
std::map<uint32, InvasionZone> m_activeInvasions;
std::map<uint32, CityAttack> m_cityAttacks;
ScourgeInvasionData();
void Reset();
std::string GetData();
};
enum SunsReachPhases
{
SUNS_REACH_PHASE_1_STAGING_AREA,
@@ -166,7 +293,7 @@ class WorldState
void SaveHelper(std::string& stringToSave, WorldStateSaveIds saveId);
void HandlePlayerEnterZone(Player* player, AreaTableIDs zoneId);
void HandlePlayerLeaveZone(Player* player, AreaTableIDs zoneId);
bool IsConditionFulfilled(WorldStateCondition conditionId, WorldStateConditionState state = WORLD_STATE_CONDITION_STATE_NONE) const;
bool IsConditionFulfilled(uint32 conditionId, uint32 state = WORLD_STATE_CONDITION_STATE_NONE) const;
void HandleConditionStateChange(WorldStateCondition conditionId, WorldStateConditionState state);
void HandleExternalEvent(WorldStateEvent eventId, uint32 param);
void Update(uint32 diff);
@@ -195,8 +322,55 @@ class WorldState
bool _isMagtheridonHeadSpawnedHorde;
bool _isMagtheridonHeadSpawnedAlliance;
SunsReachReclamationData m_sunsReachData;
std::map<WorldStateCondition, std::atomic<WorldStateConditionState>> _transportStates; // atomic to avoid having to lock
std::map<uint32, std::atomic<WorldStateConditionState>> _transportStates; // atomic to avoid having to lock
std::mutex _mutex; // all World State operations are threat unsafe
// Scourge Invasion
public:
void SetScourgeInvasionState(SIState state);
void StartScourgeInvasion(bool sendMail);
void StopScourgeInvasion();
[[nodiscard]] uint32 GetSIRemaining(SIRemaining remaining) const;
[[nodiscard]] uint32 GetSIRemainingByZone(uint32 zoneId) const;
void SetSIRemaining(SIRemaining remaining, uint32 value);
TimePoint GetSITimer(SITimers timer);
void SetSITimer(SITimers timer, TimePoint timePoint);
uint32 GetBattlesWon();
void AddBattlesWon(int32 count);
uint32 GetLastAttackZone();
void SetLastAttackZone(uint32 zoneId);
void BroadcastSIWorldstates();
void HandleDefendedZones();
std::string GetScourgeInvasionPrintout();
void StartZoneEvent(SIZoneIds eventId);
void StartNewInvasionIfTime(uint32 attackTimeVar, uint32 zoneId);
void StartNewCityAttackIfTime(uint32 attackTimeVar, uint32 zoneId);
void StartNewInvasion(uint32 zoneId);
void StartNewCityAttack(uint32 zoneId);
bool ResumeInvasion(ScourgeInvasionData::InvasionZone& zone);
bool SummonMouth(Map* map, ScourgeInvasionData::InvasionZone& zone, Position position, bool newInvasion);
bool SummonPallid(Map* map, ScourgeInvasionData::CityAttack& zone, const Position& position, uint32 spawnLoc);
void HandleActiveZone(uint32 attackTimeVar, uint32 zoneId, uint32 remainingVar, TimePoint now);
Map* GetMap(uint32 mapId, Position const& invZone);
bool IsActiveZone(uint32 zoneId);
uint32 GetActiveZones();
uint32 GetTimerIdForZone(uint32 zoneId);
void SetPallidGuid(uint32 zoneId, ObjectGuid guid);
void SetMouthGuid(uint32 zoneId, ObjectGuid guid);
void AddPendingInvasion(uint32 zoneId);
void RemovePendingInvasion(uint32 zoneId);
void AddPendingPallid(uint32 zoneId);
void RemovePendingPallid(uint32 zoneId);
void OnEnable(ScourgeInvasionData::InvasionZone& zone);
void OnDisable(ScourgeInvasionData::InvasionZone& zone);
void OnDisable(ScourgeInvasionData::CityAttack& zone);
private:
void SendScourgeInvasionMail();
ScourgeInvasionData m_siData;
};
#define sWorldState WorldState::instance()

View File

@@ -39,9 +39,18 @@ public:
{ "gatecounter", HandleSunwellGateCounterCommand, SEC_ADMINISTRATOR, Console::Yes },
};
static ChatCommandTable scourgeInvasionCommandTable =
{
{ "show", HandleScourgeInvasionCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "state", HandleScourgeInvasionStateCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "battleswon", HandleScourgeInvasionBattlesWonCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "startzone", HandleScourgeInvasionStartZone, SEC_ADMINISTRATOR, Console::Yes },
};
static ChatCommandTable worldStateCommandTable =
{
{ "sunsreach", sunsreachCommandTable }
{ "sunsreach", sunsreachCommandTable },
{ "scourgeinvasion", scourgeInvasionCommandTable }
};
static ChatCommandTable commandTable =
@@ -118,6 +127,46 @@ public:
handler->PSendSysMessage(sWorldState->GetSunsReachPrintout());
return true;
}
static bool HandleScourgeInvasionCommand(ChatHandler* handler)
{
handler->PSendSysMessage(sWorldState->GetScourgeInvasionPrintout());
return true;
}
static bool HandleScourgeInvasionStateCommand(ChatHandler* handler, uint32 value)
{
if (value >= SI_STATE_MAX)
{
handler->PSendSysMessage("Syntax: .worldstate scourgeinvasion state <value>.");
handler->PSendSysMessage("Valid values are: 0 (Disabled), 1 (Enabled).");
return true;
}
sWorldState->SetScourgeInvasionState(SIState(value));
handler->PSendSysMessage("Scourge Invasion state set to {}.", value);
handler->PSendSysMessage(sWorldState->GetScourgeInvasionPrintout());
return true;
}
static bool HandleScourgeInvasionBattlesWonCommand(ChatHandler* /* handler */, int32 value)
{
sWorldState->AddBattlesWon(value);
return true;
}
static bool HandleScourgeInvasionStartZone(ChatHandler* handler, uint32 value)
{
if (value >= SI_TIMER_MAX)
{
handler->PSendSysMessage("Syntax: .worldstate scourgeinvasion startzone <value>.\nvalid values: 0-7");
return true;
}
sWorldState->StartZoneEvent(SIZoneIds(value));
handler->PSendSysMessage("Scourge Invasion event started for zone {}.", value);
handler->PSendSysMessage(sWorldState->GetScourgeInvasionPrintout());
return true;
}
};
void AddSC_worldstate_commandscript()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,427 @@
/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by the
* Free Software Foundation; either version 3 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SCOURGE_INVASION_H
#define SCOURGE_INVASION_H
enum ScourgeInvasionSpells
{
SPELL_SPIRIT_PARTICLES_PURPLE = 28126, // Purple Minions Aura.
// GameObject Necropolis
SPELL_SUMMON_NECROPOLIS_CRITTERS = 27866, // Spawns NPCs Necropolis Health and Necropolis.
// Necropolis Health -> Necropolis
SPELL_DESPAWNER_OTHER = 28349, // Casted by the NPC "Necropolis health" after getting hit by,
// on the NPC "Necropolis" which destroys itself and the Necropolis Object.
// Necropolis Health
SPELL_ZAP_NECROPOLIS = 28386, // There are always 3 Necrotic Shards spawns per Necropolis. This Spell is castet on the NPC "Necropolis Health" if a Shard dies and does 40 Physical damage.
// NPC "Necropolis Health" has 42 health. 42 health / 3 Shards = 14 damage.
// We have set the armor value from the NPC "Necropolis Health" to 950 to reduce the damage from 40 to 14.
// Necropolis -> Proxy
SPELL_COMMUNIQUE_TIMER_NECROPOLIS = 28395, // Periodically triggers 28373 Communique, Necropolis-to-Proxies every 15 seconds.
SPELL_COMMUNIQUE_NECROPOLIS_TO_PROXIES = 28373, // purple bolt Visual (BIG).
// Proxy -> Necropolis
SPELL_COMMUNIQUE_PROXY_TO_NECROPOLIS = 28367, // Purple bolt Visual (SMALL).
// Proxy -> Relay
SPELL_COMMUNIQUE_PROXY_TO_RELAY = 28366, // purple bolt Visual (BIG).
// Relay -> Proxy
SPELL_COMMUNIQUE_RELAY_TO_PROXY = 28365, // Purple bolt Visual (SMALL).
// Relay -> Shard
SPELL_COMMUNIQUE_RELAY_TO_CAMP = 28326, // Purple bolt Visual (BIG).
// Shard
SPELL_CREATE_CRYSTAL = 28344, // Spawn a Necrotic Shard.
SPELL_CREATE_CRYSTAL_CORPSE = 27895, // Summon (Damaged Necrotic Shard).
SPELL_CAMP_RECEIVES_COMMUNIQUE = 28449, // Impact Visual.
SPELL_COMMUNIQUE_TIMER_CAMP = 28346, // Cast on npc_necrotic_shard on spawn? Periodically triggers 28345 Communique Trigger every 35 seconds.
SPELL_COMMUNIQUE_TRIGGER = 28345, // Triggers 28281 SPELL_COMMUNIQUE_CAMP_TO_RELAY via void Spell::EffectDummy.
SPELL_DAMAGE_CRYSTAL = 28041, // 100 Damage (Physical). Casted on itself, if 16143 (Shadow of Doom) spawns.
SPELL_SOUL_REVIVAL = 28681, // Increases all damage caused by 10%.
SPELL_CAMP_TYPE_GHOST_SKELETON = 28197, // Camp Type, tells the NPC "Scourge Invasion Minion, finder" which Camp type the Shard has.
SPELL_CAMP_TYPE_GHOST_GHOUL = 28198, // ""
SPELL_CAMP_TYPE_GHOUL_SKELETON = 28199, // ""
SPELL_MINION_SPAWNER_SMALL = 27887, // Triggers 27885 (Disturb Minion Trap, small) every 5 seconds. Activates up to 3 unknown Objects wich spawns the Minions.
SPELL_MINION_SPAWNER_BUTTRESS = 27888, // Triggers 27886 (Disturb Minion Trap, Buttress) every 1 hour. Activates unknown Objects, They may also spawn the Cultists.
SPELL_CHOOSE_CAMP_TYPE = 28201, // casted by Necrotic Shard.
// Shard -> Relay
SPELL_COMMUNIQUE_CAMP_TO_RELAY = 28281, // Purple bolt Visual (SMALL)
SPELL_COMMUNIQUE_CAMP_TO_RELAY_DEATH = 28351, // Visual when Damaged Necrotic Shard dies.
// Camp - Minion spawning system
SPELL_FIND_CAMP_TYPE = 28203, // casted by Scourge Invasion Minion, finder.
// Scourge Invasion Minion, spawner, Ghost/Ghoul
SPELL_PH_SUMMON_MINION_TRAP_GHOST_GHOUL = 27883,
// Scourge Invasion Minion, spawner, Ghost/Skeleton
SPELL_PH_SUMMON_MINION_TRAP_GHOST_SKELETON = 28186,
// Scourge Invasion Minion, spawner, Ghoul/Skeleton
SPELL_PH_SUMMON_MINION_TRAP_GHOUL_SKELETON = 28187,
// Minions Spells
SPELL_ZAP_CRYSTAL = 28032, // 15 damage to a Necrotic Shard on death.
SPELL_MINION_SPAWN_IN = 28234, // Pink Lightning.
SPELL_SPIRIT_SPAWN_OUT = 17680, // Makes invisible.
SPELL_MINION_DESPAWN_TIMER = 28090, // Triggers 28091 (Despawner, self) every 150 seconds. Triggers 17680 SPELL_SPIRIT_SPAWN_OUT via void Spell::EffectDummy.
SPELL_CONTROLLER_TIMER = 28095, // Triggers 28091 (Despawner, self) every 60 seconds for 1 hour. (Unknown who is casting this).
SPELL_DESPAWNER_SELF = 28091, // Trigger from Spell above.
SPELL_SUMMON_SCOURGE_CONTROLLER = 28092,
// Minion Abilities
SPELL_SCOURGE_STRIKE = 28265, // Pink Lightning (Instakill).
SPELL_ENRAGE = 8599, // Used by 16141 (Ghoul Berserker).
SPELL_BONE_SHARDS = 17014, // [shortest sniff CD: 16,583 seconds] Used by 16299 (Skeletal Shocktrooper).
SPELL_INFECTED_BITE = 7367, // [shortest sniff CD: 13,307 seconds] Used by 16141 (Ghoul Berserker).
SPELL_DEMORALIZING_SHOUT = 16244, // [shortest sniff CD: 19,438 seconds] Used by 16298 (Spectral Soldier).
SPELL_SUNDER_ARMOR = 21081, // [shortest sniff CD: 6,489 seconds] Used by 16298 (Spectral Soldier).
SPELL_SHADOW_WORD_PAIN = 589, // Used by 16438 (Skeletal Trooper).
SPELL_DUAL_WIELD = 674, // Used by Skeletal Soldier and Skeletal Shocktrooper.
// Marks of the Dawn
SPELL_CREATE_LESSER_MARK_OF_THE_DAWN = 28319, // Create Lesser Mark of the Dawn.
SPELL_CREATE_MARK_OF_THE_DAWN = 28320, // Create Mark of the Dawn.
SPELL_CREATE_GREATER_MARK_OF_THE_DAWN = 28321, // Create Greater Mark of the Dawn.
// Rare Minions
SPELL_KNOCKDOWN = 16790, // Used by 14697 (Lumbering Horror).
SPELL_TRAMPLE = 5568, // Used by 14697 (Lumbering Horror).
SPELL_AURA_OF_FEAR = 28313, // Used by 14697 (Lumbering Horror).
SPELL_RIBBON_OF_SOULS = 16243, // [shortest sniff CD: 1,638 seconds] Used by 16379 (Spirit of the Damned).
SPELL_PSYCHIC_SCREAM = 22884, // or 26042, used by 16379 (Spirit of the Damned).
SPELL_MINION_DESPAWN_TIMER_UNCOMMON = 28292, // Triggers 28091 (Despawner, self) every 10 minutes. Triggers 17680 SPELL_SPIRIT_SPAWN_OUT via void Spell::EffectDummy.
SPELL_ARCANE_BOLT = 13748, /* 20720 Used by 16380 (Bone Witch).
https://classicdb.ch/?npc=16380#abilities says 13748 but 20720 is the only "Arcane Bolt" whichs requires no mana.
Danage is very high, so i guess it has a very long cd.
Spell description in the Bestiary is: Hurls a magical bolt at an enemy, inflicting Arcane damage.
*/
// Cultist Engineer
SPELL_CREATE_SUMMONER_SHIELD = 28132, // Summon Object - Temporary (181142),
// Casted exactly the same time with 28234 (Minion Spawn-in) on spawn.
SPELL_BUTTRESS_CHANNEL = 28078, // Channeled by Cultist Engineer on Damaged Necrotic Shard shortly after spawning.
SPELL_BUTTRESS_TRAP = 28054, // Unknown.
SPELL_KILL_SUMMONER_SUMMON_BOSS = 28250, // Reagents, 1 Necrotic Rune
// Probably used to spawn Shadow of Doom. Casting sequence (All these [x] spells are being casted the following order within 1-2 seconds):
SPELL_PH_KILL_SUMMONER_BUFF = 27852, // [1] Casted by Cultist on Player.
SPELL_KILL_SUMMONER_WHO_WILL_SUMMON_BOSS = 27894, // [2] Casted by Player on Cultist.
SPELL_QUIET_SUICIDE = 3617, // [3] Instakill, casted exactly same time as 31316 (Summon Boss Buff).
SPELL_SUMMON_BOSS_BUFF = 31316, // [4] Summon Boss Buff, casted on Player
SPELL_SUMMON_BOSS = 31315, /* [5] Reagents, 8 Necrotic Rune, Summon (Shadow of Doom) for 1 hour.
The question is: What happens after this hour if the Shadow of Doom despawns?
Do the cultists respawn and channeling again on the damaged shard or
Does the Necrotic crystal respawn without Cultists or Shadows of Doom?
*/
// Shadow of Doom
SPELL_SPAWN_SMOKE = 10389, // Spawning Visual.
SPELL_ZAP_CRYSTAL_CORPSE = 28056, // Casted on Shard if Shadow of Doom dies.
SPELL_MINDFLAY = 16568,
SPELL_FEAR = 12542,
// Pallid Horror - Patchwerk Terror (also uses: 28315)
SPELL_SUMMON_CRACKED_NECROTIC_CRYSTAL = 28424, // Alliance.
SPELL_SUMMON_FAINT_NECROTIC_CRYSTAL = 28699, // Horde.
SPELL_DAMAGE_VS_GUARDS = 28364, // [shortest sniff CD: 11 seconds, longest 81 sec] hits 13839 (Royal Dreadguard).
// Flameshocker (also uses: 28234, 17680)
SPELL_FLAMESHOCKERS_TOUCH = 28314, // [shortest sniff CD: 30 seconds]
SPELL_FLAMESHOCKERS_REVENGE = 28323, // On death.
SPELL_FLAMESHOCKERS_TOUCH2 = 28329, // [shortest sniff CD: 30 seconds]
SPELL_FLAMESHOCKER_IMMOLATE_VISUAL = 28330
/*
These spells are not used by any NPCs or GameObjects.
The [PH] in the name means it's a placeholder. B often adds that to the names of things they add to the game but haven't finalized.
The fact that the [PH] is still there means the quest was never finished. (Google)
SPELL_PH_SUMMON_MINION_PARENT_GHOST_GHOUL = 28183,
SPELL_PH_SUMMON_MINION_PARENT_GHOST_SKELETON = 28184,
SPELL_PH_SUMMON_MINION_PARENT_GHOUL_SKELETON = 28185,
SPELL_PH_GET_TOKEN = 27922, // Create Item "Necrotic Rune".
SPELL_PH_BUTTRESS_ACTIVATOR = 28086,
SPELL_PH_CRYSTAL_CORPSE_DESPAWN = 28020,
SPELL_PH_CRYSTAL_CORPSE_TIMER = 28018, // Triggers 28020 ([PH] Crystal Corpse Despawn) after 2 hours.
SPELL_PH_CYSTAL_BAZOOKA = 27849,
SPELL_PH_SUMMON_BUTTRESS = 28024, // Summon (Cultist Engineer) for 1 hour.
SPELL_DND_SUMMON_CRYSTAL_MINION_FINDER = 28227,
*/
};
enum ScourgeInvasionNPC
{
// Visible NPCs
NPC_NECROTIC_SHARD = 16136,
NPC_DAMAGED_NECROTIC_SHARD = 16172,
NPC_CULTIST_ENGINEER = 16230,
NPC_SHADOW_OF_DOOM = 16143,
// Camp Helpers (invisible)
NPC_SCOURGE_INVASION_MINION_FINDER = 16356, // Casting 28203 (Find Camp Type).
NPC_SCOURGE_INVASION_MINION_SPAWNER_GHOST_GHOUL = 16306,
NPC_SCOURGE_INVASION_MINION_SPAWNER_GHOST_SKELETON = 16336,
NPC_SCOURGE_INVASION_MINION_SPAWNER_GHOUL_SKELETON = 16338,
// Necropolis Helpers (invisible)
NPC_NECROPOLIS = 16401,
NPC_NECROPOLIS_HEALTH = 16421,
NPC_NECROPOLIS_PROXY = 16398,
NPC_NECROPOLIS_RELAY = 16386,
// Minions
NPC_SKELETAL_SHOCKTROOPER = 16299,
NPC_GHOUL_BERSERKER = 16141,
NPC_SPECTRAL_SOLDIER = 16298,
// Rare Minions
NPC_LUMBERING_HORROR = 14697,
NPC_BONE_WITCH = 16380,
NPC_SPIRIT_OF_THE_DAMNED = 16379,
// 50 Zones cleared
NPC_ARGENT_DAWN_INITIATE = 16384,
NPC_ARGENT_DAWN_CLERIC = 16435,
// 100 Zones cleared
NPC_ARGENT_DAWN_PRIEST = 16436,
NPC_ARGENT_DAWN_PALADIN = 16395,
// 150 Zones cleared
NPC_ARGENT_DAWN_CRUSADER = 16433,
NPC_ARGENT_DAWN_CHAMPION = 16434,
// Low level Minions
NPC_SKELETAL_TROOPER = 16438,
NPC_SPECTRAL_SPIRIT = 16437,
NPC_SKELETAL_SOLDIER = 16422,
NPC_SPECTRAL_APPARITATION = 16423,
// Stormwind - Undercity Attacks https://www.youtube.com/watch?v=c0QjLqHVPRU&t=17s
// NPC_PALLID_HORROR = 16394,
// NPC_PATCHWORK_TERROR = 16382,
NPC_CRACKED_NECROTIC_CRYSTAL = 16431,
NPC_FAINT_NECROTIC_CRYSTAL = 16531,
NPC_FLAMESHOCKER = 16383,
NPC_HIGHLORD_BOLVAR_FORDRAGON = 1748,
NPC_VARIAN = 29611,
NPC_LADY_SYLVANAS_WINDRUNNER = 10181,
NPC_VARIMATHRAS = 2425,
NPC_ROYAL_DREADGUARD = 13839,
NPC_STORMWIND_ROYAL_GUARD = 1756,
NPC_UNDERCITY_ELITE_GUARDIAN = 16432,
NPC_UNDERCITY_GUARDIAN = 5624,
NPC_DEATHGUARD_ELITE = 7980,
NPC_STORMWIND_CITY_GUARD = 68,
NPC_STORMWIND_ELITE_GUARD = 16396,
// Citizens
NPC_RENATO_GALLINA = 1432,
NPC_MICHAEL_GARRETT = 4551,
NPC_HANNAH_AKELEY = 4575,
NPC_INNKEEPER_NORMAN = 6741,
NPC_OFFICER_MALOOF = 15766,
NPC_STEPHANIE_TURNER = 6174,
NPC_THOMAS_MILLER = 3518,
NPC_WILLIAM_MONTAGUE = 4549
};
enum ScourgeInvasionObjects
{
// Invisible Objects
GO_BUTTRESS_TRAP = 181112, // [Guessed] Those objects can't be sniffed and are not available in any database.
GO_SUMMON_MINION_TRAP_GHOST_GHOUL = 181111, // Object is not in sniffed files or any database such as WoWHead, but spell 28196 (Create Minion Trap: Ghost/Skeleton) should probably summon them.
GO_SUMMON_MINION_TRAP_GHOST_SKELETON = 181155, // ""
GO_SUMMON_MINION_TRAP_GHOUL_SKELETON = 181156, // ""
// Visible Objects
GO_SUMMON_CIRCLE = 181136,
GO_SUMMONER_SHIELD = 181142,
GO_UNDEAD_FIRE = 181173,
GO_UNDEAD_FIRE_AURA = 181174,
GO_SKULLPILE_01 = 181191,
GO_SKULLPILE_02 = 181192,
GO_SKULLPILE_03 = 181193,
GO_SKULLPILE_04 = 181194,
GO_NECROPOLIS_TINY = 181154, // Necropolis (scale 1.0).
GO_NECROPOLIS_SMALL = 181373, // Necropolis (scale 1.5).
GO_NECROPOLIS_MEDIUM = 181374, // Necropolis (scale 2.0).
GO_NECROPOLIS_BIG = 181215, // Necropolis (scale 2.5).
GO_NECROPOLIS_HUGE = 181223, // Necropolis (scale 3.5).
GO_NECROPOLIS_CITY = 181172, // Necropolis at the Citys (scale 2.5).
};
enum ScourgeInvasionMisc
{
ITEM_NECROTIC_RUNE = 22484,
ACTION_FLAMESHOCKER_SCHEDULE_DESPAWN = 1, // Used by Flameshocker to schedule despawn.
};
enum ScourgeInvasionNPCEvents
{
EVENT_SHARD_MINION_SPAWNER_SMALL = 1,
EVENT_SHARD_MINION_SPAWNER_BUTTRESS = 2,
EVENT_SPAWNER_SUMMON_MINION = 3,
EVENT_SHARD_FIND_DAMAGED_SHARD = 4,
EVENT_CULTIST_CHANNELING = 5,
EVENT_HERALD_OF_THE_LICH_KING_YELL = 6,
// EVENT_HERALD_OF_THE_LICH_KING_ZONE_START = 7,
// EVENT_HERALD_OF_THE_LICH_KING_ZONE_STOP = 8,
EVENT_HERALD_OF_THE_LICH_KING_UPDATE = 9,
// Shadow of Doom Events
EVENT_DOOM_MINDFLAY = 20,
EVENT_DOOM_FEAR = 21,
EVENT_DOOM_START_ATTACK = 22,
// Rare Events
EVENT_RARE_KNOCKDOWN = 31,
EVENT_RARE_TRAMPLE = 32,
EVENT_RARE_RIBBON_OF_SOULS = 33,
// Minion Events
EVENT_MINION_ENRAGE = 40,
EVENT_MINION_BONE_SHARDS = 41,
EVENT_MINION_INFECTED_BITE = 42,
EVENT_MINION_DAZED = 43,
EVENT_MINION_DEMORALIZING_SHOUT = 44,
EVENT_MINION_SUNDER_ARMOR = 45,
EVENT_MINION_ARCANE_BOLT = 46,
EVENT_MINION_PSYCHIC_SCREAM = 47,
EVENT_MINION_SCOURGE_STRIKE = 48,
EVENT_MINION_SHADOW_WORD_PAIN = 49,
EVENT_MINION_FLAMESHOCKERS_TOUCH = 50,
EVENT_MINION_FLAMESHOCKERS_DESPAWN = 51,
// Pallid Horror Events
EVENT_PALLID_RANDOM_YELL = 52,
EVENT_PALLID_SPELL_DAMAGE_VS_GUARDS = 53,
EVENT_SYLVANAS_ANSWER_YELL = 54,
EVENT_PALLID_RANDOM_SAY = 55,
EVENT_PALLID_SUMMON_FLAMESHOCKER = 56
};
enum ScourgeInvasionQuests
{
QUEST_UNDER_THE_SHADOW = 9153,
QUEST_CRACKED_NECROTIC_CRYSTAL = 9292,
QUEST_FAINT_NECROTIC_CRYSTAL = 9310
};
enum ScourgeInvasionTalk
{
HERALD_OF_THE_LICH_KING_SAY_ATTACK_START = 0,
HERALD_OF_THE_LICH_KING_SAY_ATTACK_END = 1,
HERALD_OF_THE_LICH_KING_SAY_ATTACK_RANDOM = 2,
PALLID_HORROR_SAY_RANDOM_YELL = 0,
SHADOW_OF_DOOM_SAY_AGGRO = 0,
SYLVANAS_SAY_ATTACK_END = 3,
VARIAN_SAY_ATTACK_END = 3
};
enum ScourgeInvasionLang
{
// Pallid Horror random yelling every 65-300 seconds
BCT_PALLID_HORROR_YELL1 = 12329, // What? This not Naxxramas! We not like this place... destroy!
BCT_PALLID_HORROR_YELL2 = 12327, // Raaarrrrggghhh! We come for you!
BCT_PALLID_HORROR_YELL3 = 12326, // Kel'Thuzad say to tell you... DIE!
BCT_PALLID_HORROR_YELL4 = 12342, // Why you run away? We make your corpse into Scourge.
BCT_PALLID_HORROR_YELL5 = 12343, // No worry, we find you.
BCT_PALLID_HORROR_YELL6 = 12330, // You spare parts! We make more Scourge in necropolis.
BCT_PALLID_HORROR_YELL7 = 12328, // Hahaha, your guards no match for Scourge!
BCT_PALLID_HORROR_YELL8 = 12325, // We come destroy puny ones!
// Undercity Guardian
BCT_UNDERCITY_GUARDIAN_ROGUES_QUARTER = 12336, // Rogues' Quarter attacked by Scourge! Help!
BCT_UNDERCITY_GUARDIAN_MAGIC_QUARTER = 12335, // Scourge attack Magic Quarter!
BCT_UNDERCITY_GUARDIAN_TRADE_QUARTER = 12353, // There Scourge outside Trade Quarter!
BCT_UNDERCITY_GUARDIAN_SEWERS = 12334, // Scourge in sewers! We need help!
// Undercity Elite Guardian
BCT_UNDERCITY_ELITE_GUARDIAN_1 = 12354, // Scourge inside Trade Quarter! Destroy!
// Royal Dreadguard
BCT_UNDERCITY_ROYAL_DREADGUARD_1 = 12337, // The Scourge are at the entrance to the Royal Quarter! Kill them!!
// Varimathras
BCT_UNDERCITY_VARIMATHRAS_1 = 12333, // Dreadguard, hold your line. Halt the advance of those Scourge!
// Lady Sylvanas Windrunner
BCT_UNDERCITY_SYLVANAS_1 = 12331, // The Scourge attack against my court has been eliminated. You may go about your business.
BCT_UNDERCITY_SYLVANAS_2 = 12332, // My Royal Dreadguard, you will deal with this matter as befits your station. That, or you will wish that you had.
// Citizens
BCT_UNDERCITY_RANDOM_1 = 12355, // Scourge spotted nearby!
BCT_STORMWIND_RANDOM_1 = 12366, // Scourge spotted nearby! Renato Gallina
BCT_UNDERCITY_RANDOM_2 = 12356, // I just saw a Scourge! Kill it!
BCT_STORMWIND_RANDOM_2 = 12367, // I just saw a Scourge! Kill it! Thomas Miller
BCT_UNDERCITY_RANDOM_3 = 12357, // Did you see that? There's a Scourge over there! Michael Garrett, Hannah Akeley
BCT_STORMWIND_RANDOM_3 = 12368, // Did you see that? There's a Scourge over there! Thomas Miller
BCT_UNDERCITY_RANDOM_4 = 12359, // There's one of the Scourge, right over there! Innkeeper Norman, Michael Garrett
BCT_STORMWIND_RANDOM_4 = 12370, // There's one of the Scourge, right over there!
BCT_UNDERCITY_RANDOM_5 = 12357, // Did you see that? There's a Scourge over there! Michael Garrett, Hannah Akeley
BCT_STORMWIND_RANDOM_5 = 12368, // Did you see that? There's a Scourge over there! Thomas Miller
BCT_UNDERCITY_RANDOM_6 = 12361, // Will these unrelenting Scourge attacks never end? Innkeeper Norman, William Montague
BCT_STORMWIND_RANDOM_6 = 12372, // Will these unrelenting Scourge attacks never end?
BCT_UNDERCITY_RANDOM_7 = 12360, // This has gone too far. How dare the Scourge attack Undercity! Destroy it before more come! Innkeeper Norman
BCT_STORMWIND_RANDOM_7 = 12371, // This has gone too far. How dare the Scourge attack Stormwind! Destroy it before more come! Stephanie Turner
BCT_UNDERCITY_RANDOM_8 = 12362, // Destroy the Scourge invader now, before it's too late! Michael Garrett
BCT_STORMWIND_RANDOM_8 = 12373, // Destroy the Scourge invader now, before it's too late! Officer Maloof
BCT_UNDERCITY_RANDOM_9 = 12358, // How can I get anything done with the Scourge running amok in here?! Innkeeper Norman
BCT_STORMWIND_RANDOM_9 = 12369, // How can I get anything done with the Scourge running amok around here?! Stephanie Turner
// Stormwind City Guard
BCT_STORMWIND_CITY_GUARD_1 = 12310, // To arms! Scourge spotted in the Cathedral of Light!
BCT_STORMWIND_CITY_GUARD_2 = 12311, // Scourge in the Trade District! Have at them!
BCT_STORMWIND_CITY_GUARD_3 = 12315, // Light help us... the Scourge are in the Park!
// Stormwind Royal Guard
BCT_STORMWIND_CITY_GUARD_4 = 12316, // The Scourge are at the castle entrance! For Stormwind! For King Anduin!
// Highlord Bolvar Fordragon?
BCT_STORMWIND_BOLVAR_1 = 12317, // Hold the line! Protect the King at all costs!
BCT_STORMWIND_BOLVAR_2 = 12318, // Good work, one and all! The Scourge at the castle have been defeated.
// Misc
BCT_CULTIST_ENGINEER_OPTION = 12112, // Use 8 necrotic runes and disrupt his ritual.
BCT_GIVE_MAGIC_ITEM_OPTION = 12302, // Give me one of your magic items.
BCT_SHADOW_OF_DOOM_TEXT_0 = 12420, // Our dark master has noticed your trifling, and sends me to bring a message... of doom!
BCT_SHADOW_OF_DOOM_TEXT_1 = 12421, // These heroics mean nothing, $c. Your future is sealed and your soul is doomed to servitude!
BCT_SHADOW_OF_DOOM_TEXT_2 = 12422, // Your battle here is but the smallest mote of a world wide invasion, whelp! It is time you learned of the powers you face!
BCT_SHADOW_OF_DOOM_TEXT_3 = 12243, // You will not stop our deepening shadow, $c. Now... join us! Join the ranks of the Chosen!
BCT_HERALD_OF_THE_LICH_KING_ZONE_ATTACK_START_1 = 13121, // Spawn.
BCT_HERALD_OF_THE_LICH_KING_ZONE_ATTACK_START_2 = 13125, // Spawn. 53 min between 2-3 in sniffs.
BCT_HERALD_OF_THE_LICH_KING_ZONE_ATTACK_ENDS_1 = 13165, // Despawn.
BCT_HERALD_OF_THE_LICH_KING_ZONE_ATTACK_ENDS_2 = 13164, // Despawn.
BCT_HERALD_OF_THE_LICH_KING_ZONE_ATTACK_ENDS_3 = 13163, // Despawn.
BCT_HERALD_OF_THE_LICH_KING_RANDOM_1 = 13126, // Random.
BCT_HERALD_OF_THE_LICH_KING_RANDOM_2 = 13124, // Random.
BCT_HERALD_OF_THE_LICH_KING_RANDOM_3 = 13122, // 180 seconds between 5-6 in sniffs.
BCT_HERALD_OF_THE_LICH_KING_RANDOM_4 = 13123, // Random. 30 min between 8-2 in sniffs.
BCT_CULTIST_ENGINEER_GOSSIP = 8436, // 12111 - This cultist is in a deep trance...
};
#endif

View File

@@ -34,6 +34,7 @@ void AddSC_npc_stave_of_ancients();
void AddSC_server_mail();
void AddSC_transport_zeppelins();
void AddSC_suns_reach_reclamation();
void AddSC_scourge_invasion();
// The name of this function should match:
// void Add${NameOfDirectory}Scripts()
@@ -57,4 +58,5 @@ void AddWorldScripts()
AddSC_server_mail();
AddSC_transport_zeppelins();
AddSC_suns_reach_reclamation();
AddSC_scourge_invasion();
}