From b85aea85d2a921c9d43b5ac9a0b20d9b4dae01bb Mon Sep 17 00:00:00 2001 From: Killerwife Date: Wed, 1 Sep 2021 23:52:02 -0400 Subject: [PATCH] Port scourge invasions from vmangos Closes https://github.com/cmangos/issues/issues/2313 Credit: Vmangos for original implementation. Killerwife rewrites for our worldstate system. Mantislord event data. Anon EAI port. Killerwife rest of db data. --- doc/script_commands.txt | 1 + sql/base/dbc/cmangos_fixes/Spell.sql | 7 + sql/scriptdev2/scriptdev2.sql | 14 + sql/scriptdev2/spell.sql | 5 +- src/game/AI/BaseAI/CreatureAI.h | 2 + src/game/AI/EventAI/CreatureEventAI.h | 6 +- .../ScriptDevAI/include/sc_grid_searchers.cpp | 16 + .../ScriptDevAI/include/sc_grid_searchers.h | 2 + .../scripts/world/scourge_invasion.cpp | 1162 +++++++++++++++++ .../scripts/world/scourge_invasion.h | 421 ++++++ .../scripts/world/world_map_scripts.cpp | 2 + .../AI/ScriptDevAI/system/ScriptLoader.cpp | 2 + src/game/Chat/Chat.cpp | 10 + src/game/Chat/Chat.h | 4 + src/game/Chat/Level3.cpp | 44 + src/game/DBScripts/ScriptMgr.cpp | 6 +- src/game/DBScripts/ScriptMgr.h | 1 + src/game/Entities/Object.cpp | 4 +- src/game/Globals/ObjectMgr.cpp | 4 +- src/game/Grids/GridNotifiers.h | 43 + src/game/World/WorldState.cpp | 769 ++++++++++- src/game/World/WorldState.h | 150 ++- src/game/World/WorldStateDefines.h | 10 +- src/shared/Util.h | 7 + 24 files changed, 2679 insertions(+), 13 deletions(-) create mode 100644 src/game/AI/ScriptDevAI/scripts/world/scourge_invasion.cpp create mode 100644 src/game/AI/ScriptDevAI/scripts/world/scourge_invasion.h diff --git a/doc/script_commands.txt b/doc/script_commands.txt index d126b29dec..38efec869f 100644 --- a/doc/script_commands.txt +++ b/doc/script_commands.txt @@ -329,6 +329,7 @@ Defining a buddy could be done in several way: 35 SCRIPT_COMMAND_SEND_AI_EVENT * resultingSource = Creature, resultingTarget = Unit * datalong = AIEventType - limited only to EventAI supported events * datalong2 = radius. If radius isn't provided and the target is a creature, then send AIEvent to target + * datalong3 = miscvalue - uint32 - for use in ReceiveAIEvent handler 36 SCRIPT_COMMAND_SET_FACING Turn resultingSource towards resultingTarget * resultingSource = Creature, resultingTarget WorldObject diff --git a/sql/base/dbc/cmangos_fixes/Spell.sql b/sql/base/dbc/cmangos_fixes/Spell.sql index 182b493e18..c89891c6b4 100644 --- a/sql/base/dbc/cmangos_fixes/Spell.sql +++ b/sql/base/dbc/cmangos_fixes/Spell.sql @@ -2108,6 +2108,9 @@ INSERT INTO `spell_template` (`Id`, `SchoolMask`, `Category`, `CastUI`, `Dispel` -- Toxic Gas - used by Garden Gas in Naxx, remove SPELL_ATTR_EX_CHANNELED_1 and CHANNEL_FLAG_MOVEMENT to prevent aura being removed UPDATE spell_template SET AttributesEx=0, ChannelInterruptFlags=0 WHERE Id=30074; +INSERT INTO spell_template (Id, Category, CastUI, Dispel, Mechanic, Attributes, AttributesEx, AttributesEx2, AttributesEx3, AttributesEx4, Stances, StancesNot, Targets, TargetCreatureType, RequiresSpellFocus, CasterAuraState, TargetAuraState, CastingTimeIndex, RecoveryTime, CategoryRecoveryTime, InterruptFlags, AuraInterruptFlags, ChannelInterruptFlags, ProcFlags, ProcChance, ProcCharges, MaxLevel, BaseLevel, SpellLevel, DurationIndex, PowerType, ManaCost, ManaCostPerlevel, ManaPerSecond, ManaPerSecondPerLevel, RangeIndex, Speed, ModalNextSpell, StackAmount, Totem1, Totem2, Reagent1, Reagent2, Reagent3, Reagent4, Reagent5, Reagent6, Reagent7, Reagent8, ReagentCount1, ReagentCount2, ReagentCount3, ReagentCount4, ReagentCount5, ReagentCount6, ReagentCount7, ReagentCount8, EquippedItemClass, EquippedItemSubClassMask, EquippedItemInventoryTypeMask, Effect1, Effect2, Effect3, EffectDieSides1, EffectDieSides2, EffectDieSides3, EffectBaseDice1, EffectBaseDice2, EffectBaseDice3, EffectDicePerLevel1, EffectDicePerLevel2, EffectDicePerLevel3, EffectRealPointsPerLevel1, EffectRealPointsPerLevel2, EffectRealPointsPerLevel3, EffectBasePoints1, EffectBasePoints2, EffectBasePoints3, EffectMechanic1, EffectMechanic2, EffectMechanic3, EffectImplicitTargetA1, EffectImplicitTargetA2, EffectImplicitTargetA3, EffectImplicitTargetB1, EffectImplicitTargetB2, EffectImplicitTargetB3, EffectRadiusIndex1, EffectRadiusIndex2, EffectRadiusIndex3, EffectApplyAuraName1, EffectApplyAuraName2, EffectApplyAuraName3, EffectAmplitude1, EffectAmplitude2, EffectAmplitude3, EffectMultipleValue1, EffectMultipleValue2, EffectMultipleValue3, EffectChainTarget1, EffectChainTarget2, EffectChainTarget3, EffectItemType1, EffectItemType2, EffectItemType3, EffectMiscValue1, EffectMiscValue2, EffectMiscValue3, EffectTriggerSpell1, EffectTriggerSpell2, EffectTriggerSpell3, EffectPointsPerComboPoint1, EffectPointsPerComboPoint2, EffectPointsPerComboPoint3, SpellVisual, SpellIconID, ActiveIconID, SpellPriority, SpellName, SpellName2, SpellName3, SpellName4, SpellName5, SpellName6, SpellName7, SpellName8, Rank1, Rank2, Rank3, Rank4, Rank5, Rank6, Rank7, Rank8, ManaCostPercentage, StartRecoveryCategory, StartRecoveryTime, MaxTargetLevel, SpellFamilyName, SpellFamilyFlags, MaxAffectedTargets, DmgClass, PreventionType, StanceBarOrder, DmgMultiplier1, DmgMultiplier2, DmgMultiplier3, MinFactionId, MinReputation, RequiredAuraVision, SchoolMask, IsServerSide, AttributesServerside) VALUES +(28203, 0, 0, 0, 0, 384, 1024, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 'Find Camp Type', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0); + -- ================== -- MaxAffectedTargets UPDATE `spell_template` SET `MaxAffectedTargets` = 1 WHERE `Id` IN ( @@ -2177,6 +2180,10 @@ UPDATE `spell_template` SET `AttributesServerSide` = `AttributesServerSide`|4 WH 28330 -- Flameshocker - Immolate Visual ); +-- Scourge invasion serverside +INSERT INTO spell_template(Id, SchoolMask, Category, Dispel, Mechanic, Attributes, AttributesEx, AttributesEx2, AttributesEx3, AttributesEx4, AttributesEx5, Stances, StancesNot, Targets, TargetCreatureType, RequiresSpellFocus, CasterAuraState, TargetAuraState, CasterAuraStateNot, TargetAuraStateNot, CastingTimeIndex, RecoveryTime, CategoryRecoveryTime, InterruptFlags, AuraInterruptFlags, ChannelInterruptFlags, procFlags, procChance, procCharges, maxLevel, baseLevel, spellLevel, DurationIndex, powerType, manaCost, manaCostPerLevel, manaPerSecond, manaPerSecondPerLevel, rangeIndex, speed, StackAmount, Totem1, Totem2, Reagent1, Reagent2, Reagent3, Reagent4, Reagent5, Reagent6, Reagent7, Reagent8, ReagentCount1, ReagentCount2, ReagentCount3, ReagentCount4, ReagentCount5, ReagentCount6, ReagentCount7, ReagentCount8, EquippedItemClass, EquippedItemSubClassMask, EquippedItemInventoryTypeMask, Effect1, Effect2, Effect3, EffectDieSides1, EffectDieSides2, EffectDieSides3, EffectBaseDice1, EffectBaseDice2, EffectBaseDice3, EffectDicePerLevel1, EffectDicePerLevel2, EffectDicePerLevel3, EffectRealPointsPerLevel1, EffectRealPointsPerLevel2, EffectRealPointsPerLevel3, EffectBasePoints1, EffectBasePoints2, EffectBasePoints3, EffectMechanic1, EffectMechanic2, EffectMechanic3, EffectImplicitTargetA1, EffectImplicitTargetA2, EffectImplicitTargetA3, EffectImplicitTargetB1, EffectImplicitTargetB2, EffectImplicitTargetB3, EffectRadiusIndex1, EffectRadiusIndex2, EffectRadiusIndex3, EffectApplyAuraName1, EffectApplyAuraName2, EffectApplyAuraName3, EffectAmplitude1, EffectAmplitude2, EffectAmplitude3, EffectMultipleValue1, EffectMultipleValue2, EffectMultipleValue3, EffectChainTarget1, EffectChainTarget2, EffectChainTarget3, EffectItemType1, EffectItemType2, EffectItemType3, EffectMiscValue1, EffectMiscValue2, EffectMiscValue3, EffectMiscValueB1, EffectMiscValueB2, EffectMiscValueB3, EffectTriggerSpell1, EffectTriggerSpell2, EffectTriggerSpell3, EffectPointsPerComboPoint1, EffectPointsPerComboPoint2, EffectPointsPerComboPoint3, SpellVisual, SpellIconID, activeIconID, spellPriority, SpellName, SpellName2, SpellName3, SpellName4, SpellName5, SpellName6, SpellName7, SpellName8, ManaCostPercentage, StartRecoveryCategory, StartRecoveryTime, MaxTargetLevel, SpellFamilyName, SpellFamilyFlags, MaxAffectedTargets, DmgClass, PreventionType, DmgMultiplier1, DmgMultiplier2, DmgMultiplier3, TotemCategory1, TotemCategory2, AreaId) VALUES +('28345', '1', '0', '0', '0', '256', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '0', '0', '0', '0', '0', '0', '101', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '-1', '0', '0', '3', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '0', '0', 'Communique Trigger', '', '', '', '', '', '', '', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '0', '0', '0', '0', '0'), +('28349', '1', '0', '0', '0', '8388608', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '0', '0', '0', '0', '0', '0', '101', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '135', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '-1', '0', '0', '3', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '25', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '0', '0', 'Despawner, other', '', '', '', '', '', '', '', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '0', '0', '0', '0', '0'); -- ============================================================ -- TBC section diff --git a/sql/scriptdev2/scriptdev2.sql b/sql/scriptdev2/scriptdev2.sql index eb689e4dee..b1d6339e84 100644 --- a/sql/scriptdev2/scriptdev2.sql +++ b/sql/scriptdev2/scriptdev2.sql @@ -255,6 +255,20 @@ INSERT INTO scripted_areatrigger VALUES (4786,'at_brewfest_receive_keg'), (4787,'at_brewfest_send_keg'); +/* Scourge Invasion */ +UPDATE creature_template SET ScriptName='scourge_invasion_necrotic_shard' WHERE entry IN (16136,16172); +UPDATE creature_template SET ScriptName='scourge_invasion_necropolis' WHERE entry=16401; +UPDATE creature_template SET ScriptName='scourge_invasion_mouth' WHERE entry=16995; +UPDATE creature_template SET ScriptName='scourge_invasion_necropolis_health' WHERE entry=16421; +UPDATE creature_template SET ScriptName='scourge_invasion_necropolis_relay' WHERE entry=16386; +UPDATE creature_template SET ScriptName='scourge_invasion_necropolis_proxy' WHERE entry=16398; +UPDATE creature_template SET ScriptName='scourge_invasion_minion_spawner' WHERE entry IN (16306,16336,16338); +UPDATE creature_template SET ScriptName='scourge_invasion_cultist_engineer' WHERE entry=16230; +UPDATE creature_template SET ScriptName='scourge_invasion_minion' WHERE entry IN (16143,16383); +UPDATE creature_template SET ScriptName='npc_pallid_horror' WHERE entry IN (16394,16382); +UPDATE gameobject_template SET ScriptName='scourge_invasion_go_circle' WHERE entry=181136; +UPDATE gameobject_template SET ScriptName='scourge_invasion_go_necropolis' WHERE entry IN (181154,181215,181223,181374,181373); + /* */ /* ZONE */ /* */ diff --git a/sql/scriptdev2/spell.sql b/sql/scriptdev2/spell.sql index 55fe2c5046..58963604f8 100644 --- a/sql/scriptdev2/spell.sql +++ b/sql/scriptdev2/spell.sql @@ -118,7 +118,10 @@ INSERT INTO spell_scripts(Id, ScriptName) VALUES (29875,'spell_check_gothik_side'), (29897,'spell_icecrown_guardian_periodic'), (30114,'spell_plague_wave_controller'), -(30132,'spell_sapphiron_iceblock'); +(30132,'spell_sapphiron_iceblock'), +(28091,'spell_despawner_self'), +(28345,'spell_communique_trigger'), +(31315,'spell_summon_boss'); -- TBC INSERT INTO spell_scripts(Id, ScriptName) VALUES diff --git a/src/game/AI/BaseAI/CreatureAI.h b/src/game/AI/BaseAI/CreatureAI.h index e1ddc649cb..7783869991 100644 --- a/src/game/AI/BaseAI/CreatureAI.h +++ b/src/game/AI/BaseAI/CreatureAI.h @@ -31,6 +31,8 @@ class CreatureAI : public UnitAI virtual void Reset(); + virtual void OnRemoveFromWorld() {} + virtual void EnterCombat(Unit* enemy) override; virtual void AttackStart(Unit* who) override; virtual void DamageTaken(Unit* dealer, uint32& damage, DamageEffectType damageType, SpellEntry const* spellInfo) override; diff --git a/src/game/AI/EventAI/CreatureEventAI.h b/src/game/AI/EventAI/CreatureEventAI.h index 2191ca9eef..ac33214afb 100644 --- a/src/game/AI/EventAI/CreatureEventAI.h +++ b/src/game/AI/EventAI/CreatureEventAI.h @@ -771,8 +771,9 @@ struct CreatureEventAI_Event // EVENT_T_TARGET_NOT_REACHABLE = 36 struct { - uint32 unused; - } unreachable; + uint32 eventId; + uint32 data; + } map_event; // RAW struct { @@ -894,6 +895,7 @@ class CreatureEventAI : public CreatureAI uint32 m_EventUpdateTime; // Time between event updates uint32 m_EventDiff; // Time between the last event call + bool m_bEmptyList; // Variables used by Events themselves typedef std::vector CreatureEventAIList; diff --git a/src/game/AI/ScriptDevAI/include/sc_grid_searchers.cpp b/src/game/AI/ScriptDevAI/include/sc_grid_searchers.cpp index 85d8a0e4e5..bd67543f70 100644 --- a/src/game/AI/ScriptDevAI/include/sc_grid_searchers.cpp +++ b/src/game/AI/ScriptDevAI/include/sc_grid_searchers.cpp @@ -43,6 +43,14 @@ void GetGameObjectListWithEntryInGrid(GameObjectList& goList, WorldObject* sourc Cell::VisitGridObjects(source, searcher, maxSearchRange); } +void GetGameObjectListWithEntryInGrid(GameObjectList& goList, WorldObject* source, std::vector const& entries, float maxSearchRange) +{ + MaNGOS::AllGameObjectsMatchingOneEntryInRange check(source, entries, maxSearchRange); + MaNGOS::GameObjectListSearcher searcher(goList, check); + + Cell::VisitGridObjects(source, searcher, maxSearchRange); +} + void GetCreatureListWithEntryInGrid(CreatureList& creatureList, WorldObject* source, uint32 entry, float maxSearchRange) { MaNGOS::AllCreaturesOfEntryInRangeCheck check(source, entry, maxSearchRange); @@ -51,6 +59,14 @@ void GetCreatureListWithEntryInGrid(CreatureList& creatureList, WorldObject* sou Cell::VisitGridObjects(source, searcher, maxSearchRange); } +void GetCreatureListWithEntryInGrid(CreatureList& creatureList, WorldObject* source, std::vector const& entries, float maxSearchRange) +{ + MaNGOS::AllCreaturesMatchingOneEntryInRange check(source, entries, maxSearchRange); + MaNGOS::CreatureListSearcher searcher(creatureList, check); + + Cell::VisitGridObjects(source, searcher, maxSearchRange); +} + void GetPlayerListWithEntryInWorld(PlayerList& playerList, WorldObject* source, float maxSearchRange) { MaNGOS::AnyPlayerInObjectRangeCheck check(source, maxSearchRange); diff --git a/src/game/AI/ScriptDevAI/include/sc_grid_searchers.h b/src/game/AI/ScriptDevAI/include/sc_grid_searchers.h index 6bf7cfcb9b..4f2d239e81 100644 --- a/src/game/AI/ScriptDevAI/include/sc_grid_searchers.h +++ b/src/game/AI/ScriptDevAI/include/sc_grid_searchers.h @@ -37,7 +37,9 @@ GameObject* GetClosestGameObjectWithEntry(WorldObject* source, uint32 entry, flo Creature* GetClosestCreatureWithEntry(WorldObject* source, uint32 entry, float maxSearchRange, bool onlyAlive = true, bool onlyDead = false, bool excludeSelf = false); void GetGameObjectListWithEntryInGrid(GameObjectList& goList, WorldObject* source, uint32 entry, float maxSearchRange); +void GetGameObjectListWithEntryInGrid(GameObjectList& goList, WorldObject* source, std::vector const& entries, float maxSearchRange); void GetCreatureListWithEntryInGrid(CreatureList& creatureList, WorldObject* source, uint32 entry, float maxSearchRange); +void GetCreatureListWithEntryInGrid(CreatureList& creatureList, WorldObject* source, std::vector const& entries, float maxSearchRange); void GetPlayerListWithEntryInWorld(PlayerList& playerList, WorldObject* source, float maxSearchRange); // Used in: hyjalAI.cpp diff --git a/src/game/AI/ScriptDevAI/scripts/world/scourge_invasion.cpp b/src/game/AI/ScriptDevAI/scripts/world/scourge_invasion.cpp new file mode 100644 index 0000000000..df62da14ad --- /dev/null +++ b/src/game/AI/ScriptDevAI/scripts/world/scourge_invasion.cpp @@ -0,0 +1,1162 @@ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +//#include "scriptPCH.h" +#include "scourge_invasion.h" +//#include "CreatureGroups.h" +#include "GameEvents/GameEventMgr.h" +#include "Grids/GridNotifiers.h" +#include "AI/ScriptDevAI/include/sc_common.h" +#include "AI/ScriptDevAI/base/CombatAI.h" +#include "Globals/ObjectMgr.h" +#include "World/WorldStateDefines.h" + +inline uint32 GetCampType(Creature* unit) { return unit->HasAura(SPELL_CAMP_TYPE_GHOST_SKELETON) || unit->HasAura(SPELL_CAMP_TYPE_GHOST_GHOUL) || unit->HasAura(SPELL_CAMP_TYPE_GHOUL_SKELETON); }; + +inline bool IsGuardOrBoss(Unit* unit) { + return unit->GetEntry() == NPC_ROYAL_DREADGUARD || unit->GetEntry() == NPC_STORMWIND_ROYAL_GUARD || unit->GetEntry() == NPC_UNDERCITY_ELITE_GUARDIAN || unit->GetEntry() == NPC_UNDERCITY_GUARDIAN || unit->GetEntry() == NPC_DEATHGUARD_ELITE || + unit->GetEntry() == NPC_STORMWIND_CITY_GUARD || unit->GetEntry() == NPC_HIGHLORD_BOLVAR_FORDRAGON || unit->GetEntry() == NPC_LADY_SYLVANAS_WINDRUNNER || unit->GetEntry() == NPC_VARIMATHRAS; +} + +Unit* SelectRandomFlameshockerSpawnTarget(Creature* unit, Unit* except, float radius) +{ + std::list targets; + + MaNGOS::AnyUnfriendlyUnitInObjectRangeCheck u_check(unit, radius); + MaNGOS::UnitListSearcher searcher(targets, u_check); + Cell::VisitAllObjects(unit, searcher, radius); + + // remove current target + if (except) + targets.remove(except); + + for (std::list::iterator tIter = targets.begin(); tIter != targets.end();) + { + // || !(*tIter)->ToCreature()->CanSummonGuards() - CREATURE_FLAG_EXTRA_SUMMON_GUARD + if (!(*tIter)->IsCreature() || !static_cast(*tIter)->IsCivilian() || (*tIter)->GetZoneId() != unit->GetZoneId() || !unit->CanAttack((*tIter)) || GetClosestCreatureWithEntry((*tIter), NPC_FLAMESHOCKER, VISIBILITY_DISTANCE_TINY)) + { + std::list::iterator tIter2 = tIter; + ++tIter; + targets.erase(tIter2); + } + else + ++tIter; + } + + // no appropriate targets + if (targets.empty()) + return nullptr; + + // select random + uint32 rIdx = urand(0, targets.size() - 1); + std::list::const_iterator tcIter = targets.begin(); + for (uint32 i = 0; i < rIdx; ++i) + ++tcIter; + + return *tcIter; +} + +void ChangeZoneEventStatus(Creature* mouth, bool on) +{ + if (!mouth) + return; + + switch (mouth->GetZoneId()) + { + case ZONEID_WINTERSPRING: + if (on) + { + if (!sGameEventMgr.IsActiveEvent(GAME_EVENT_SCOURGE_INVASION_WINTERSPRING)) + sGameEventMgr.StartEvent(GAME_EVENT_SCOURGE_INVASION_WINTERSPRING, true); + } + else + sGameEventMgr.StopEvent(GAME_EVENT_SCOURGE_INVASION_WINTERSPRING, true); + break; + case ZONEID_TANARIS: + if (on) + { + if (!sGameEventMgr.IsActiveEvent(GAME_EVENT_SCOURGE_INVASION_TANARIS)) + sGameEventMgr.StartEvent(GAME_EVENT_SCOURGE_INVASION_TANARIS, true); + } + else + sGameEventMgr.StopEvent(GAME_EVENT_SCOURGE_INVASION_TANARIS, true); + break; + case ZONEID_AZSHARA: + if (on) + { + if (!sGameEventMgr.IsActiveEvent(GAME_EVENT_SCOURGE_INVASION_AZSHARA)) + sGameEventMgr.StartEvent(GAME_EVENT_SCOURGE_INVASION_AZSHARA, true); + } + else + sGameEventMgr.StopEvent(GAME_EVENT_SCOURGE_INVASION_AZSHARA, true); + break; + case ZONEID_BLASTED_LANDS: + if (on) + { + if (!sGameEventMgr.IsActiveEvent(GAME_EVENT_SCOURGE_INVASION_BLASTED_LANDS)) + sGameEventMgr.StartEvent(GAME_EVENT_SCOURGE_INVASION_BLASTED_LANDS, true); + } + else + sGameEventMgr.StopEvent(GAME_EVENT_SCOURGE_INVASION_BLASTED_LANDS, true); + break; + case ZONEID_EASTERN_PLAGUELANDS: + if (on) + { + if (!sGameEventMgr.IsActiveEvent(GAME_EVENT_SCOURGE_INVASION_EASTERN_PLAGUELANDS)) + sGameEventMgr.StartEvent(GAME_EVENT_SCOURGE_INVASION_EASTERN_PLAGUELANDS, true); + } + else + sGameEventMgr.StopEvent(GAME_EVENT_SCOURGE_INVASION_EASTERN_PLAGUELANDS, true); + break; + case ZONEID_BURNING_STEPPES: + if (on) + { + if (!sGameEventMgr.IsActiveEvent(GAME_EVENT_SCOURGE_INVASION_BURNING_STEPPES)) + sGameEventMgr.StartEvent(GAME_EVENT_SCOURGE_INVASION_BURNING_STEPPES, true); + } + else + sGameEventMgr.StopEvent(GAME_EVENT_SCOURGE_INVASION_BURNING_STEPPES, true); + break; + } +} + +void DespawnEventDoodads(Creature* shard) +{ + if (!shard) + return; + + std::list doodadList; + GetGameObjectListWithEntryInGrid(doodadList, shard, { GOBJ_SUMMON_CIRCLE, GOBJ_UNDEAD_FIRE, GOBJ_UNDEAD_FIRE_AURA, GOBJ_SKULLPILE_01, GOBJ_SKULLPILE_02, GOBJ_SKULLPILE_03, GOBJ_SKULLPILE_04, GOBJ_SUMMONER_SHIELD }, 60.0f); + for (const auto pDoodad : doodadList) + pDoodad->ForcedDespawn(); + + std::list finderList; + GetCreatureListWithEntryInGrid(finderList, shard, { NPC_SCOURGE_INVASION_MINION_FINDER }, 60.0f); + for (const auto pFinder : finderList) + pFinder->ForcedDespawn(); +} + +void DespawnNecropolis(Unit* despawner) +{ + if (!despawner) + return; + + std::list necropolisList; + GetGameObjectListWithEntryInGrid(necropolisList, despawner, { GOBJ_NECROPOLIS_TINY, GOBJ_NECROPOLIS_SMALL, GOBJ_NECROPOLIS_MEDIUM, GOBJ_NECROPOLIS_BIG, GOBJ_NECROPOLIS_HUGE }, ATTACK_DISTANCE); + for (const auto pNecropolis : necropolisList) + pNecropolis->ForcedDespawn(); +} + +void SummonCultists(Unit* shard) +{ + if (!shard) + return; + + std::list summonerShieldList; + GetGameObjectListWithEntryInGrid(summonerShieldList, shard, { GOBJ_SUMMONER_SHIELD }, INSPECT_DISTANCE); + for (const auto pSummonerShield : summonerShieldList) + pSummonerShield->ForcedDespawn(); + + // We don't have all positions sniffed from the Cultists, so why not using this code which placing them almost perfectly into the circle while blizzards positions are some times way off? + if (GameObject* gameObject = GetClosestGameObjectWithEntry(shard, GOBJ_SUMMON_CIRCLE, CONTACT_DISTANCE)) + { + for (int i = 0; i < 4; ++i) + { + float angle = (float(i) * (M_PI / 2)) + gameObject->GetOrientation(); + float x = gameObject->GetPositionX() + 6.95f * cos(angle); + float y = gameObject->GetPositionY() + 6.75f * sin(angle); + float z = gameObject->GetPositionZ() + 5.0f; + shard->UpdateGroundPositionZ(x, y, z); + if (Creature* cultist = shard->SummonCreature(NPC_CULTIST_ENGINEER, x, y, z, angle - M_PI, TEMPSPAWN_TIMED_OR_DEAD_DESPAWN, IN_MILLISECONDS * HOUR, true)) + cultist->AI()->SendAIEvent(AI_EVENT_CUSTOM_A, shard, cultist, NPC_CULTIST_ENGINEER); + } + } +} + +void DespawnCultists(Unit* despawner) +{ + if (!despawner) + return; + + std::list cultistList; + GetCreatureListWithEntryInGrid(cultistList, despawner, { NPC_CULTIST_ENGINEER }, INSPECT_DISTANCE); + for (const auto pCultist : cultistList) + if (pCultist) + pCultist->ForcedDespawn(); +} + +void DespawnShadowsOfDoom(Unit* despawner) +{ + if (!despawner) + return; + + std::list shadowList; + GetCreatureListWithEntryInGrid(shadowList, despawner, { NPC_SHADOW_OF_DOOM }, 200.0f); + for (const auto pShadow : shadowList) + if (pShadow && pShadow->IsAlive() && !pShadow->IsInCombat()) + pShadow->ForcedDespawn(); +} + +uint32 HasMinion(Creature* summoner, float range) +{ + if (!summoner) + return false; + + uint32 minionCounter = 0; + std::list minionList; + GetCreatureListWithEntryInGrid(minionList, summoner, { NPC_SKELETAL_SHOCKTROOPER, NPC_GHOUL_BERSERKER, NPC_SPECTRAL_SOLDIER, NPC_LUMBERING_HORROR, NPC_BONE_WITCH, NPC_SPIRIT_OF_THE_DAMNED }, ATTACK_DISTANCE); + for (const auto pMinion : minionList) + if (pMinion && pMinion->IsAlive()) + minionCounter++; + + return minionCounter; +} + +bool UncommonMinionspawner(Creature* pSummoner) // Rare Minion Spawner. +{ + if (!pSummoner) + return false; + + std::list uncommonMinionList; + GetCreatureListWithEntryInGrid(uncommonMinionList, pSummoner, { NPC_LUMBERING_HORROR, NPC_BONE_WITCH, NPC_SPIRIT_OF_THE_DAMNED }, 100.0f); + for (const auto pMinion : uncommonMinionList) + if (pMinion) + return false; // Already a rare found (dead or alive). + + /* + The chance or timer for a Rare minion spawn is unknown and i don't see an exact pattern for a spawn sequence. + Sniffed are: 19669 Minions and 90 Rares (Ratio: 217 to 1). + */ + uint32 chance = urand(1, 217); + if (chance > 1) + return false; // Above 1 = Minion, else Rare. + + return true; +} + +uint32 GetFindersAmount(Creature* shard) +{ + uint32 finderCounter = 0; + std::list finderList; + GetCreatureListWithEntryInGrid(finderList, shard, { NPC_SCOURGE_INVASION_MINION_FINDER }, 60.0f); + for (const auto pFinder : finderList) + if (pFinder) + finderCounter++; + + return finderCounter; +} + +/* +Circle +*/ +class GoCircle : public GameObjectAI +{ + public: + GoCircle(GameObject* gameobject) : GameObjectAI(gameobject) + { + //m_go->CastSpell(m_go, SPELL_CREATE_CRYSTAL, true); + m_go->CastSpell(nullptr, nullptr, SPELL_CREATE_CRYSTAL, TRIGGERED_OLD_TRIGGERED); + } +}; + +/* +Necropolis +*/ +class GoNecropolis : public GameObjectAI +{ + public: + GoNecropolis(GameObject* gameobject) : GameObjectAI(gameobject) + { + m_go->SetActiveObjectState(true); + //m_go->SetVisibilityModifier(3000.0f); + } +}; + +/* +Mouth of Kel'Thuzad +*/ +struct MouthAI : public ScriptedAI +{ + MouthAI(Creature* creature) : ScriptedAI(creature) + { + AddCustomAction(EVENT_MOUTH_OF_KELTHUZAD_YELL, urand((IN_MILLISECONDS * 150), (IN_MILLISECONDS * HOUR)), [&]() + { + DoBroadcastText(PickRandomValue(BCT_MOUTH_OF_KELTHUZAD_RANDOM_1, BCT_MOUTH_OF_KELTHUZAD_RANDOM_2, BCT_MOUTH_OF_KELTHUZAD_RANDOM_3, BCT_MOUTH_OF_KELTHUZAD_RANDOM_4), m_creature, nullptr, CHAT_TYPE_ZONE_YELL); + ResetTimer(EVENT_MOUTH_OF_KELTHUZAD_YELL, urand((IN_MILLISECONDS * 150), (IN_MILLISECONDS * HOUR))); + }); + SetReactState(REACT_PASSIVE); + } + + void Reset() override {} + + void ReceiveAIEvent(AIEventType eventType, Unit* /*sender*/, Unit* /*invoker*/, uint32 miscValue) override + { + if (eventType == AI_EVENT_CUSTOM_A) + { + switch (miscValue) + { + case EVENT_MOUTH_OF_KELTHUZAD_ZONE_START: + { + ChangeZoneEventStatus(m_creature, true); + m_creature->GetMap()->SetWeather(m_creature->GetZoneId(), WEATHER_TYPE_STORM, 0.25f, true); + DoBroadcastText(PickRandomValue(BCT_MOUTH_OF_KELTHUZAD_ZONE_ATTACK_START_1, BCT_MOUTH_OF_KELTHUZAD_ZONE_ATTACK_START_2), m_creature, nullptr, CHAT_TYPE_ZONE_YELL); + break; + } + case EVENT_MOUTH_OF_KELTHUZAD_ZONE_STOP: + { + DoBroadcastText(PickRandomValue(BCT_MOUTH_OF_KELTHUZAD_ZONE_ATTACK_ENDS_1, BCT_MOUTH_OF_KELTHUZAD_ZONE_ATTACK_ENDS_2, BCT_MOUTH_OF_KELTHUZAD_ZONE_ATTACK_ENDS_3), m_creature, nullptr, CHAT_TYPE_ZONE_YELL); + ChangeZoneEventStatus(m_creature, false); + m_creature->GetMap()->SetWeather(m_creature->GetZoneId(), WEATHER_TYPE_RAIN, 0.0f, false); + m_creature->ForcedDespawn(); + break; + } + } + } + } +}; + +/* +Necropolis +*/ +struct NecropolisAI : public ScriptedAI +{ + NecropolisAI(Creature* creature) : ScriptedAI(creature) + { + m_creature->SetActiveObjectState(true); + //m_creature->SetVisibilityModifier(3000.0f); + } + + void Reset() override {} + + void SpellHit(Unit* caster, SpellEntry const* spell) override + { + if (m_creature->HasAura(SPELL_COMMUNIQUE_TIMER_NECROPOLIS)) + return; + + if (spell->Id == SPELL_COMMUNIQUE_PROXY_TO_NECROPOLIS) + m_creature->CastSpell(m_creature, SPELL_COMMUNIQUE_TIMER_NECROPOLIS, TRIGGERED_OLD_TRIGGERED); // m_creature->AddAura(SPELL_COMMUNIQUE_TIMER_NECROPOLIS); + } + + void UpdateAI(uint32 const diff) override {} +}; + +/* +Necropolis Health +*/ +struct NecropolisHealthAI : public ScriptedAI +{ + NecropolisHealthAI(Creature* creature) : ScriptedAI(creature) + { + //m_creature->SetVisibilityModifier(3000.0f); + } + + int m_zapped = 0; // 3 = death. + + void Reset() override {} + + void SpellHit(Unit* caster, SpellEntry const* spell) override + { + if (spell->Id == SPELL_COMMUNIQUE_CAMP_TO_RELAY_DEATH) + m_creature->CastSpell(m_creature, SPELL_ZAP_NECROPOLIS, TRIGGERED_OLD_TRIGGERED); + + // Just to make sure it finally dies! + if (spell->Id == SPELL_ZAP_NECROPOLIS) + { + if (++m_zapped >= 3) + m_creature->Suicide(); //m_creature->DoKillUnit(m_creature); + } + } + + void JustDied(Unit* killer) override + { + if (Creature* necropolis = GetClosestCreatureWithEntry(m_creature, NPC_NECROPOLIS, ATTACK_DISTANCE)) + m_creature->CastSpell(necropolis, SPELL_DESPAWNER_OTHER, TRIGGERED_OLD_TRIGGERED); + + SIRemaining remainingID; + + switch (m_creature->GetZoneId()) + { + default: + case ZONEID_TANARIS: + remainingID = SI_REMAINING_TANARIS; + break; + case ZONEID_BLASTED_LANDS: + remainingID = SI_REMAINING_BLASTED_LANDS; + break; + case ZONEID_EASTERN_PLAGUELANDS: + remainingID = SI_REMAINING_EASTERN_PLAGUELANDS; + break; + case ZONEID_BURNING_STEPPES: + remainingID = SI_REMAINING_BURNING_STEPPES; + break; + case ZONEID_WINTERSPRING: + remainingID = SI_REMAINING_WINTERSPRING; + break; + case ZONEID_AZSHARA: + remainingID = SI_REMAINING_AZSHARA; + break; + } + + uint32 remaining = sWorldState.GetSIRemaining(remainingID); + if (remaining > 0) + sWorldState.SetSIRemaining(remainingID, (remaining - 1)); + } + + void SpellHitTarget(Unit* target, SpellEntry const* spellInfo) override + { + // Make sure m_creature despawn after SPELL_DESPAWNER_OTHER triggered. + if (spellInfo->Id == SPELL_DESPAWNER_OTHER && target->GetEntry() == NPC_NECROPOLIS) + { + DespawnNecropolis(target); + static_cast(target)->ForcedDespawn(); + m_creature->ForcedDespawn(); + } + } + + void UpdateAI(uint32 const diff) override {} +}; + +/* +Necropolis Proxy +*/ +struct NecropolisProxyAI : public ScriptedAI +{ + NecropolisProxyAI(Creature* creature) : ScriptedAI(creature) + { + m_creature->SetActiveObjectState(true); + //m_creature->SetVisibilityModifier(3000.0f); + Reset(); + } + + void Reset() override {} + + void SpellHit(Unit* caster, SpellEntry const* spellInfo) override + { + switch (spellInfo->Id) + { + case SPELL_COMMUNIQUE_NECROPOLIS_TO_PROXIES: + m_creature->CastSpell(m_creature, SPELL_COMMUNIQUE_PROXY_TO_RELAY, TRIGGERED_OLD_TRIGGERED); + break; + case SPELL_COMMUNIQUE_RELAY_TO_PROXY: + m_creature->CastSpell(m_creature, SPELL_COMMUNIQUE_PROXY_TO_NECROPOLIS, TRIGGERED_OLD_TRIGGERED); + break; + case SPELL_COMMUNIQUE_CAMP_TO_RELAY_DEATH: + if (Creature* health = GetClosestCreatureWithEntry(m_creature, NPC_NECROPOLIS_HEALTH, 200.0f)) + m_creature->CastSpell(health, SPELL_COMMUNIQUE_CAMP_TO_RELAY_DEATH, TRIGGERED_OLD_TRIGGERED); + break; + } + } + + void SpellHitTarget(Unit* target, SpellEntry const* spellInfo) override + { + // Make sure m_creature despawn after SPELL_COMMUNIQUE_CAMP_TO_RELAY_DEATH hits the target to avoid getting hit by Purple bolt again. + if (spellInfo->Id == SPELL_COMMUNIQUE_CAMP_TO_RELAY_DEATH) + m_creature->ForcedDespawn(); + } + + void UpdateAI(uint32 const diff) override {} +}; + +/* +Necropolis Relay +*/ +struct NecropolisRelayAI : public ScriptedAI +{ + NecropolisRelayAI(Creature* creature) : ScriptedAI(creature) + { + m_creature->SetActiveObjectState(true); + //m_creature->SetVisibilityModifier(3000.0f); + Reset(); + } + + void Reset() override {} + + void SpellHit(Unit* caster, SpellEntry const* spell) override + { + switch (spell->Id) + { + case SPELL_COMMUNIQUE_PROXY_TO_RELAY: + m_creature->CastSpell(m_creature, SPELL_COMMUNIQUE_RELAY_TO_CAMP, TRIGGERED_OLD_TRIGGERED); + break; + case SPELL_COMMUNIQUE_CAMP_TO_RELAY: + m_creature->CastSpell(m_creature, SPELL_COMMUNIQUE_RELAY_TO_PROXY, TRIGGERED_OLD_TRIGGERED); + break; + case SPELL_COMMUNIQUE_CAMP_TO_RELAY_DEATH: + if (Creature* pProxy = GetClosestCreatureWithEntry(m_creature, NPC_NECROPOLIS_PROXY, 200.0f)) + m_creature->CastSpell(pProxy, SPELL_COMMUNIQUE_CAMP_TO_RELAY_DEATH, TRIGGERED_OLD_TRIGGERED); + break; + } + } + + void SpellHitTarget(Unit* target, SpellEntry const* spell) override + { + // Make sure m_creature despawn after SPELL_COMMUNIQUE_CAMP_TO_RELAY_DEATH hits the target to avoid getting hit by Purple bolt again. + if (spell->Id == SPELL_COMMUNIQUE_CAMP_TO_RELAY_DEATH) + m_creature->ForcedDespawn(); + } + + void UpdateAI(uint32 const diff) override {} +}; + +/* +Necrotic Shard +*/ +struct NecroticShard : public ScriptedAI +{ + NecroticShard(Creature* creature) : ScriptedAI(creature) + { + m_creature->SetActiveObjectState(true); + AddCustomAction(EVENT_SHARD_MINION_SPAWNER_SMALL, true, [&]() // Spawn Minions every 5 seconds. + { + HandleShardMinionSpawnerSmall(); + }); + //m_creature->SetVisibilityModifier(3000.0f); + if (m_creature->GetEntry() == NPC_DAMAGED_NECROTIC_SHARD) + { + m_finders = GetFindersAmount(m_creature); // Count all finders to limit Minions spawns. + ResetTimer(EVENT_SHARD_MINION_SPAWNER_SMALL, 5000); // Spawn Minions every 5 seconds. + AddCustomAction(EVENT_SHARD_MINION_SPAWNER_BUTTRESS, 5000u, [&]() // Spawn Cultists every 60 minutes. + { + /* + This is a placeholder for SPELL_MINION_SPAWNER_BUTTRESS [27888] which also activates unknown, not sniffable gamebjects + and happens every hour if a Damaged Necrotic Shard is activ. The Cultists despawning after 1 hour, + so this just resets everything and spawn them again and Refill the Health of the Shard. + */ + m_creature->SetHealthPercent(100.f); // m_creature->SetFullHealth(); + // Despawn all remaining Shadows before respawning the Cultists? + DespawnShadowsOfDoom(m_creature); + // Respawn the Cultists. + SummonCultists(m_creature); + + ResetTimer(EVENT_SHARD_MINION_SPAWNER_BUTTRESS, IN_MILLISECONDS * HOUR); + }); + } + else + { + // Just in case. + std::list shardList; + GetCreatureListWithEntryInGrid(shardList, m_creature, { NPC_NECROTIC_SHARD, NPC_DAMAGED_NECROTIC_SHARD }, CONTACT_DISTANCE); + for (const auto shard : shardList) + if (shard != m_creature) + shard->ForcedDespawn(); + } + SetReactState(REACT_PASSIVE); + } + + uint32 m_camptype = 0; + uint32 m_finders = 0; + + void Reset() override + { + DoCastSpellIfCan(nullptr, SPELL_COMMUNIQUE_TIMER_CAMP, CAST_TRIGGERED | CAST_AURA_NOT_PRESENT); + } + + void SpellHit(Unit* caster, SpellEntry const* spell) override + { + switch (spell->Id) + { + case SPELL_ZAP_CRYSTAL_CORPSE: + { + m_creature->DealDamage(m_creature, m_creature, (m_creature->GetMaxHealth() / 4), nullptr, DIRECT_DAMAGE, SPELL_SCHOOL_MASK_NORMAL, nullptr, false); + break; + } + case SPELL_COMMUNIQUE_RELAY_TO_CAMP: + { + m_creature->CastSpell(nullptr, SPELL_CAMP_RECEIVES_COMMUNIQUE, TRIGGERED_OLD_TRIGGERED); + break; + } + case SPELL_CHOOSE_CAMP_TYPE: + { + m_camptype = PickRandomValue(SPELL_CAMP_TYPE_GHOUL_SKELETON, SPELL_CAMP_TYPE_GHOST_GHOUL, SPELL_CAMP_TYPE_GHOST_SKELETON); + m_creature->CastSpell(m_creature, m_camptype, TRIGGERED_OLD_TRIGGERED); + break; + } + case SPELL_CAMP_RECEIVES_COMMUNIQUE: + { + if (!GetCampType(m_creature) && m_creature->GetEntry() == NPC_NECROTIC_SHARD) + { + m_finders = GetFindersAmount(m_creature); + m_creature->CastSpell(m_creature, SPELL_CHOOSE_CAMP_TYPE, TRIGGERED_OLD_TRIGGERED); + ResetTimer(EVENT_SHARD_MINION_SPAWNER_SMALL, 0); // Spawn Minions every 5 seconds. + } + break; + } + case SPELL_FIND_CAMP_TYPE: + { + // Don't spawn more Minions than finders. + if (m_finders < HasMinion(m_creature, 60.0f)) + return; + + // Lets the finder spawn the associated spawner. + if (m_creature->HasAura(SPELL_CAMP_TYPE_GHOST_SKELETON)) + caster->CastSpell(caster, SPELL_PH_SUMMON_MINION_TRAP_GHOST_SKELETON, TRIGGERED_OLD_TRIGGERED); + else if (m_creature->HasAura(SPELL_CAMP_TYPE_GHOST_GHOUL)) + caster->CastSpell(caster, SPELL_PH_SUMMON_MINION_TRAP_GHOST_GHOUL, TRIGGERED_OLD_TRIGGERED); + else if (m_creature->HasAura(SPELL_CAMP_TYPE_GHOUL_SKELETON)) + caster->CastSpell(caster, SPELL_PH_SUMMON_MINION_TRAP_GHOUL_SKELETON, TRIGGERED_OLD_TRIGGERED); + break; + } + } + } + + void SpellHitTarget(Unit* target, SpellEntry const* spellInfo) override + { + if (m_creature->GetEntry() != NPC_DAMAGED_NECROTIC_SHARD) + return; + + if (spellInfo->Id == SPELL_COMMUNIQUE_CAMP_TO_RELAY_DEATH) + m_creature->ForcedDespawn(); + } + + void DamageTaken(Unit* dealer, uint32& damage, DamageEffectType /*damagetype*/, SpellEntry const* /*spellInfo*/) override + { + // Only Minions and the shard itself can deal damage. + if (dealer->GetFactionTemplateEntry() != m_creature->GetFactionTemplateEntry()) + damage = 0; + } + + // No healing possible. + void HealedBy(Unit* healer, uint32& uiHealedAmount) override + { + uiHealedAmount = 0; + } + + void JustDied(Unit* killer) override + { + switch (m_creature->GetEntry()) + { + case NPC_NECROTIC_SHARD: + if (Creature* pShard = m_creature->SummonCreature(NPC_DAMAGED_NECROTIC_SHARD, m_creature->GetPositionX(), m_creature->GetPositionY(), m_creature->GetPositionZ(), m_creature->GetOrientation(), TEMPSPAWN_MANUAL_DESPAWN, 0)) + { + // Get the camp type from the Necrotic Shard. + if (m_camptype) + pShard->CastSpell(pShard, m_camptype, TRIGGERED_OLD_TRIGGERED); + else + pShard->CastSpell(pShard, SPELL_CHOOSE_CAMP_TYPE, TRIGGERED_OLD_TRIGGERED); + + m_creature->ForcedDespawn(); + } + break; + case NPC_DAMAGED_NECROTIC_SHARD: + // Buff Players. + m_creature->CastSpell(m_creature, SPELL_SOUL_REVIVAL, TRIGGERED_OLD_TRIGGERED); + // Sending the Death Bolt. + if (Creature* pRelay = GetClosestCreatureWithEntry(m_creature, NPC_NECROPOLIS_RELAY, 200.0f)) + m_creature->CastSpell(pRelay, SPELL_COMMUNIQUE_CAMP_TO_RELAY_DEATH, TRIGGERED_OLD_TRIGGERED); + // Despawn remaining Cultists (should never happen). + DespawnCultists(m_creature); + // Remove Objects from the event around the Shard (Yes this is Blizzlike). + DespawnEventDoodads(m_creature); + break; + } + } + + void HandleShardMinionSpawnerSmall() + { + /* + This is a placeholder for SPELL_MINION_SPAWNER_SMALL [27887] which also activates unknown, not sniffable objects, which possibly checks whether a minion is in his range + and happens every 15 seconds for both, Necrotic Shard and Damaged Necrotic Shard. + */ + + int finderCounter = 0; + int finderAmount = urand(1, 3); // Up to 3 spawns. + + std::list finderList; + GetCreatureListWithEntryInGrid(finderList, m_creature, { NPC_SCOURGE_INVASION_MINION_FINDER }, 60.0f); + if (finderList.empty()) + return; + + // On a fresh camp, first the minions are spawned close to the shard and then further and further out. + finderList.sort(ObjectDistanceOrder(m_creature)); + + for (const auto& pFinder : finderList) + { + // Stop summoning Minions if we reached the max spawn amount. + if (finderAmount == finderCounter) + break; + + // Skip dead finders. + if (!pFinder->IsAlive()) + continue; + + // Don't take finders with Minions. + if (HasMinion(pFinder, ATTACK_DISTANCE)) + continue; + + /* + A finder disappears after summoning the spawner NPC (which summons the minion). + after 160-185 seconds a finder respawns on the same position as before. + */ + if (pFinder->AI()->DoCastSpellIfCan(m_creature, SPELL_FIND_CAMP_TYPE, CAST_TRIGGERED) == CAST_OK) + { + pFinder->SetRespawnDelay(urand(150, 200)); // Values are from Sniffs (rounded). Shortest and Longest respawn time from a finder on the same spot. + pFinder->ForcedDespawn(); + finderCounter++; + } + } + ResetTimer(EVENT_SHARD_MINION_SPAWNER_SMALL, 5000); + } +}; + +/* +Minion Spawner +*/ +struct MinionspawnerAI : public ScriptedAI +{ + MinionspawnerAI(Creature* creature) : ScriptedAI(creature) + { + AddCustomAction(EVENT_SPAWNER_SUMMON_MINION, 2000, 5000, [&]() // Spawn Minions every 5 seconds. + { + uint32 Entry = NPC_GHOUL_BERSERKER; // just in case. + + switch (m_creature->GetEntry()) + { + case NPC_SCOURGE_INVASION_MINION_SPAWNER_GHOST_GHOUL: + Entry = UncommonMinionspawner(m_creature) ? PickRandomValue(NPC_SPIRIT_OF_THE_DAMNED, NPC_LUMBERING_HORROR) : PickRandomValue(NPC_SPECTRAL_SOLDIER, NPC_GHOUL_BERSERKER); + break; + case NPC_SCOURGE_INVASION_MINION_SPAWNER_GHOST_SKELETON: + Entry = UncommonMinionspawner(m_creature) ? PickRandomValue(NPC_SPIRIT_OF_THE_DAMNED, NPC_BONE_WITCH) : PickRandomValue(NPC_SPECTRAL_SOLDIER, NPC_SKELETAL_SHOCKTROOPER); + break; + case NPC_SCOURGE_INVASION_MINION_SPAWNER_GHOUL_SKELETON: + Entry = UncommonMinionspawner(m_creature) ? PickRandomValue(NPC_LUMBERING_HORROR, NPC_BONE_WITCH) : PickRandomValue(NPC_GHOUL_BERSERKER, NPC_SKELETAL_SHOCKTROOPER); + break; + } + if (Creature* pMinion = m_creature->SummonCreature(Entry, m_creature->GetPositionX(), m_creature->GetPositionY(), m_creature->GetPositionZ(), m_creature->GetOrientation(), TEMPSPAWN_TIMED_OR_DEAD_DESPAWN, IN_MILLISECONDS * HOUR, true)) + { + pMinion->GetMotionMaster()->MoveRandomAroundPoint(pMinion->GetPositionX(), pMinion->GetPositionY(), pMinion->GetPositionZ(), 1.0f); // pMinion->SetWanderDistance(1.0f); // Seems to be very low. + m_creature->CastSpell(nullptr, SPELL_MINION_SPAWN_IN, TRIGGERED_NONE); + } + }); + SetReactState(REACT_PASSIVE); + } + + void Reset() {} +}; + +/* +npc_cultist_engineer +*/ +struct npc_cultist_engineer : public ScriptedAI +{ + npc_cultist_engineer(Creature* creature) : ScriptedAI(creature) + { + AddCustomAction(EVENT_CULTIST_CHANNELING, false, [&]() + { + m_creature->CastSpell(nullptr, SPELL_BUTTRESS_CHANNEL, TRIGGERED_OLD_TRIGGERED); + }); + SetReactState(REACT_PASSIVE); + } + + void Reset() override {} + + void JustDied(Unit*) override + { + if (Creature* shard = GetClosestCreatureWithEntry(m_creature, NPC_DAMAGED_NECROTIC_SHARD, 15.0f)) + { + shard->CastSpell(shard, SPELL_DAMAGE_CRYSTAL, TRIGGERED_OLD_TRIGGERED); + } + if (GameObject* gameObject = GetClosestGameObjectWithEntry(m_creature, GOBJ_SUMMONER_SHIELD, CONTACT_DISTANCE)) + gameObject->Delete(); + } + + void ReceiveAIEvent(AIEventType eventType, Unit* /*sender*/, Unit* invoker, uint32 miscValue) override + { + if (eventType == 7166 && miscValue == 0) + { + if (Player* player = dynamic_cast(invoker)) + { + // Player summons a Shadow of Doom for 1 hour. + player->CastSpell(nullptr, SPELL_SUMMON_BOSS, TRIGGERED_OLD_TRIGGERED); + m_creature->CastSpell(nullptr, SPELL_QUIET_SUICIDE, TRIGGERED_OLD_TRIGGERED); + } + } + if (eventType == AI_EVENT_CUSTOM_A && miscValue == NPC_CULTIST_ENGINEER) + { + m_creature->SetCorpseDelay(10); // Corpse despawns 10 seconds after a Shadow of Doom spawns. + m_creature->CastSpell(m_creature, SPELL_CREATE_SUMMONER_SHIELD, TRIGGERED_OLD_TRIGGERED); + m_creature->CastSpell(m_creature, SPELL_MINION_SPAWN_IN, TRIGGERED_OLD_TRIGGERED); + ResetTimer(EVENT_CULTIST_CHANNELING, 1000); + } + } +}; + +struct SummonBoss : public SpellScript +{ + virtual void OnSummon(Spell* spell, Creature* summon) const override + { + Unit* caster = spell->GetCaster(); + summon->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IMMUNE_TO_PLAYER); + summon->SetFacingToObject(caster); + summon->AI()->SendAIEvent(AI_EVENT_CUSTOM_A, caster, summon, NPC_SHADOW_OF_DOOM); + if (caster->IsPlayer()) + static_cast(caster)->DestroyItemCount(ITEM_NECROTIC_RUNE, 8, true); + } +}; + +/* +npc_minion +Notes: Shard Minions, Rares and Shadow of Doom. +*/ +struct ScourgeMinion : public CombatAI +{ + ScourgeMinion(Creature* creature) : CombatAI(creature, 999) + { + switch (m_creature->GetEntry()) + { + case NPC_SHADOW_OF_DOOM: + AddCombatAction(EVENT_DOOM_MINDFLAY, 2000u); + AddCombatAction(EVENT_DOOM_FEAR, 2000u); + break; + case NPC_FLAMESHOCKER: + AddCombatAction(EVENT_MINION_FLAMESHOCKERS_TOUCH, 2000u); + AddCustomAction(EVENT_MINION_FLAMESHOCKERS_DESPAWN, true, [&]() + { + if (!m_creature->IsInCombat()) + m_creature->CastSpell(m_creature, SPELL_DESPAWNER_SELF, TRIGGERED_OLD_TRIGGERED); + else + ResetTimer(EVENT_MINION_FLAMESHOCKERS_DESPAWN, 60000); + }); + break; + } + } + + ObjectGuid m_summonerGuid; + + + void ReceiveAIEvent(AIEventType eventType, Unit* /*sender*/, Unit* invoker, uint32 miscValue) override + { + if (!invoker) + return; + + if (eventType == AI_EVENT_CUSTOM_A) + { + if (miscValue == NPC_SHADOW_OF_DOOM) + { + ResetTimer(EVENT_DOOM_START_ATTACK, 5000); // Remove Flag (immune to Players) after 5 seconds. + // Pickup random emote like here: https://youtu.be/evOs9aJa2Jw?t=229 + DoBroadcastText(PickRandomValue(BCT_SHADOW_OF_DOOM_TEXT_0, BCT_SHADOW_OF_DOOM_TEXT_1, BCT_SHADOW_OF_DOOM_TEXT_2, BCT_SHADOW_OF_DOOM_TEXT_3), m_creature, invoker); + m_creature->CastSpell(m_creature, SPELL_SPAWN_SMOKE, TRIGGERED_OLD_TRIGGERED); + } + if (miscValue == NPC_FLAMESHOCKER) + ResetTimer(EVENT_MINION_FLAMESHOCKERS_DESPAWN, 60000); + } + } + + void JustDied(Unit* pKiller) override + { + switch (m_creature->GetEntry()) + { + case NPC_SHADOW_OF_DOOM: + m_creature->CastSpell(m_creature, SPELL_ZAP_CRYSTAL_CORPSE, TRIGGERED_OLD_TRIGGERED); + break; + case NPC_FLAMESHOCKER: + m_creature->CastSpell(m_creature, SPELL_FLAMESHOCKERS_REVENGE, TRIGGERED_OLD_TRIGGERED); + break; + } + } + + void SpellHit(Unit* unit, SpellEntry const* spell) override + { + switch (spell->Id) + { + case SPELL_SPIRIT_SPAWN_OUT: + m_creature->ForcedDespawn(3000); + break; + } + } + + void MoveInLineOfSight(Unit* pWho) override + { + if (m_creature->GetEntry() == NPC_FLAMESHOCKER) + if (pWho->IsCreature() && m_creature->IsWithinDistInMap(pWho, VISIBILITY_DISTANCE_TINY) && m_creature->IsWithinLOSInMap(pWho) && !pWho->GetVictim()) + if (IsGuardOrBoss(pWho) && pWho->AI()) + pWho->AI()->AttackStart(m_creature); + + ScriptedAI::MoveInLineOfSight(pWho); + } + + void ExecuteAction(uint32 action) override + { + switch (action) + { + case EVENT_DOOM_START_ATTACK: + { + m_creature->RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IMMUNE_TO_PLAYER); + // Shadow of Doom seems to attack the Summoner here. + if (Player* player = m_creature->GetMap()->GetPlayer(m_summonerGuid)) + { + if (player->IsWithinLOSInMap(m_creature)) + { + m_creature->SetInCombatWith(player); + m_creature->SetDetectionRange(2.0f); + } + } + break; + } + case EVENT_DOOM_MINDFLAY: + { + DoCastSpellIfCan(m_creature->GetVictim(), SPELL_MINDFLAY); + ResetTimer(EVENT_DOOM_MINDFLAY, urand(6500, 13000)); + break; + } + case EVENT_DOOM_FEAR: + { + DoCastSpellIfCan(m_creature->GetVictim(), SPELL_FEAR); + ResetTimer(EVENT_DOOM_FEAR, 14500); + break; + } + case EVENT_MINION_FLAMESHOCKERS_TOUCH: + { + DoCastSpellIfCan(m_creature->GetVictim(), PickRandomValue(SPELL_FLAMESHOCKERS_TOUCH, SPELL_FLAMESHOCKERS_TOUCH2), CAST_TRIGGERED); + ResetTimer(EVENT_MINION_FLAMESHOCKERS_TOUCH, urand(30000, 45000)); + break; + } + } + } + + void UpdateAI(uint32 const diff) override + { + UpdateTimers(diff, m_creature->SelectHostileTarget()); + + // Instakill every mob nearby, except Players, Pets or NPCs with the same faction. + // m_creature->IsValidAttackTarget(m_creature->GetVictim(), true) + if (m_creature->GetEntry() != NPC_FLAMESHOCKER && m_creature->IsWithinDistInMap(m_creature->GetVictim(), 30.0f) && !m_creature->GetVictim()->IsControlledByPlayer() && m_creature->CanAttack(m_creature->GetVictim())) + DoCastSpellIfCan(m_creature->GetVictim(), SPELL_SCOURGE_STRIKE, CAST_TRIGGERED); + + DoMeleeAttackIfReady(); + } +}; + +struct PallidHorrorAI : public CombatAI +{ + std::set m_flameshockers; + + PallidHorrorAI(Creature* creature) : CombatAI(creature, 999) + { + uint32 amount = urand(5, 9); // sniffed are group sizes of 5-9 shockers on spawn. + + if (m_creature->GetHealthPercent() == 100.0f) + { + for (uint32 i = 0; i < amount; ++i) + { + if (Creature* pFlameshocker = m_creature->SummonCreature(NPC_FLAMESHOCKER, m_creature->GetPositionX(), m_creature->GetPositionY(), m_creature->GetPositionZ(), 0.0f, TEMPSPAWN_TIMED_OR_DEAD_DESPAWN, HOUR * IN_MILLISECONDS, true)) + { + float angle = (float(i) * (M_PI / (amount / static_cast(2)))) + m_creature->GetOrientation(); + //pFlameshocker->JoinCreatureGroup(m_creature, 5.0f, angle - M_PI, OPTION_FORMATION_MOVE); // Perfect Circle around the Pallid. + pFlameshocker->CastSpell(pFlameshocker, SPELL_MINION_SPAWN_IN, TRIGGERED_OLD_TRIGGERED); + m_flameshockers.insert(pFlameshocker->GetObjectGuid()); + } + } + } + m_creature->SetCorpseDelay(10); // Corpse despawns 10 seconds after a crystal spawns. + AddCombatAction(EVENT_PALLID_RANDOM_YELL, 5000u); + AddCombatAction(EVENT_PALLID_SPELL_DAMAGE_VS_GUARDS, 5000u); + AddCombatAction(EVENT_PALLID_SUMMON_FLAMESHOCKER, 5000u); + } + + void Reset() override + { + CombatAI::Reset(); + //m_creature->AddAura(SPELL_AURA_OF_FEAR); + m_creature->CastSpell(m_creature, SPELL_AURA_OF_FEAR, TRIGGERED_OLD_TRIGGERED); + } + + void MoveInLineOfSight(Unit* pWho) override + { + if (pWho->IsCreature() && m_creature->IsWithinDistInMap(pWho, VISIBILITY_DISTANCE_TINY) && m_creature->IsWithinLOSInMap(pWho) && !pWho->GetVictim()) + if (IsGuardOrBoss(pWho) && pWho->AI()) + pWho->AI()->AttackStart(m_creature); + + ScriptedAI::MoveInLineOfSight(pWho); + } + + void JustDied(Unit* unit) override + { + if (Creature* creature = GetClosestCreatureWithEntry(m_creature, NPC_HIGHLORD_BOLVAR_FORDRAGON, VISIBILITY_DISTANCE_NORMAL)) + DoBroadcastText(BCT_STORMWIND_BOLVAR_2, creature, m_creature, CHAT_TYPE_ZONE_YELL); + + if (Creature* creature = GetClosestCreatureWithEntry(m_creature, NPC_LADY_SYLVANAS_WINDRUNNER, VISIBILITY_DISTANCE_NORMAL)) + DoBroadcastText(BCT_UNDERCITY_SYLVANAS_1, creature, m_creature, CHAT_TYPE_ZONE_YELL); + + // Remove all custom summoned Flameshockers. + auto flameshockers = m_flameshockers; + for (const auto& guid : flameshockers) + if (Creature* pFlameshocker = m_creature->GetMap()->GetCreature(guid)) + pFlameshocker->Suicide(); //pFlameshocker->DoKillUnit(pFlameshocker); + + m_creature->CastSpell(m_creature, (m_creature->GetZoneId() == ZONEID_UNDERCITY_A ? SPELL_SUMMON_FAINT_NECROTIC_CRYSTAL : SPELL_SUMMON_CRACKED_NECROTIC_CRYSTAL), TRIGGERED_OLD_TRIGGERED); + m_creature->RemoveAurasDueToSpell(SPELL_AURA_OF_FEAR); + + TimePoint now = m_creature->GetMap()->GetCurrentClockTime(); + uint32 cityAttackTimer = urand(CITY_ATTACK_TIMER_MIN, CITY_ATTACK_TIMER_MAX); + TimePoint nextAttack = now + std::chrono::milliseconds(cityAttackTimer); + uint64 timeToNextAttack = (nextAttack - now).count(); + SITimers index = m_creature->GetZoneId() == ZONEID_UNDERCITY_A ? SI_TIMER_UNDERCITY : SI_TIMER_STORMWIND; + sWorldState.SetSITimer(index, nextAttack); + sWorldState.SetPallidGuid(index, ObjectGuid()); + sLog.outBasic("[Scourge Invasion Event] The Scourge has been defeated in %s, next attack starting in %d minutes", m_creature->GetZoneId() == ZONEID_UNDERCITY_A ? "Undercity" : "Stormwind", uint32(timeToNextAttack / 60)); + } + + void SummonedCreatureJustDied(Creature* unit) override + { + // Remove dead Flameshockers here to respawn them if needed. + if (m_flameshockers.find(unit->GetObjectGuid()) != m_flameshockers.end()) + m_flameshockers.erase(unit->GetObjectGuid()); + } + + void SummonedCreatureDespawn(Creature* unit) override + { + // Remove despawned Flameshockers here to respawn them if needed. + if (m_flameshockers.find(unit->GetObjectGuid()) != m_flameshockers.end()) + m_flameshockers.erase(unit->GetObjectGuid()); + } + + void OnRemoveFromWorld() override + { + // Remove all custom summoned Flameshockers. + auto flameshockers = m_flameshockers; + for (const auto& guid : flameshockers) + if (Creature* pFlameshocker = m_creature->GetMap()->GetCreature(guid)) + pFlameshocker->AddObjectToRemoveList(); + } + + void ExecuteAction(uint32 action) override + { + switch (action) + { + case EVENT_PALLID_RANDOM_YELL: + { + DoBroadcastText(PickRandomValue(BCT_PALLID_HORROR_YELL1, BCT_PALLID_HORROR_YELL2, BCT_PALLID_HORROR_YELL3, BCT_PALLID_HORROR_YELL4, + BCT_PALLID_HORROR_YELL5, BCT_PALLID_HORROR_YELL6, BCT_PALLID_HORROR_YELL7, BCT_PALLID_HORROR_YELL8), m_creature, nullptr, CHAT_TYPE_ZONE_YELL); + ResetTimer(EVENT_PALLID_RANDOM_YELL, urand(IN_MILLISECONDS * 65, IN_MILLISECONDS * 300)); + break; + } + case EVENT_PALLID_SPELL_DAMAGE_VS_GUARDS: + { + DoCastSpellIfCan(m_creature->GetVictim(), SPELL_DAMAGE_VS_GUARDS, CAST_TRIGGERED); + ResetTimer(EVENT_PALLID_SPELL_DAMAGE_VS_GUARDS, urand(11000, 81000)); + break; + } + case EVENT_PALLID_SUMMON_FLAMESHOCKER: + { + if (m_flameshockers.size() < 30) + { + if (Unit* target = SelectRandomFlameshockerSpawnTarget(m_creature, (Unit*) nullptr, DEFAULT_VISIBILITY_BGARENAS)) + { + float x, y, z; + target->GetNearPoint(target, x, y, z, 5.0f, 5.0f, 0.0f); + if (Creature* pFlameshocker = m_creature->SummonCreature(NPC_FLAMESHOCKER, x, y, z, target->GetOrientation(), TEMPSPAWN_TIMED_OR_DEAD_DESPAWN, IN_MILLISECONDS * HOUR, true)) + { + m_flameshockers.insert(pFlameshocker->GetObjectGuid()); + pFlameshocker->CastSpell(pFlameshocker, SPELL_MINION_SPAWN_IN, TRIGGERED_OLD_TRIGGERED); + pFlameshocker->AI()->SendAIEvent(AI_EVENT_CUSTOM_A, pFlameshocker, pFlameshocker, NPC_FLAMESHOCKER); + } + } + } + ResetTimer(EVENT_PALLID_SUMMON_FLAMESHOCKER, 2000); + break; + } + } + } +}; + +struct DespawnerSelf : public SpellScript +{ + void OnEffectExecute(Spell* spell, SpellEffectIndex /*effIdx*/) const override + { + Unit* caster = spell->GetCaster(); + if (!caster->IsInCombat()) + caster->CastSpell(nullptr, SPELL_SPIRIT_SPAWN_OUT, TRIGGERED_OLD_TRIGGERED); + } +}; + +struct CommuniqueTrigger : public SpellScript +{ + void OnEffectExecute(Spell* spell, SpellEffectIndex /*effIdx*/) const override + { + if (Unit* target = spell->GetUnitTarget()) + target->CastSpell(nullptr, SPELL_COMMUNIQUE_CAMP_TO_RELAY, TRIGGERED_OLD_TRIGGERED); + } +}; + +void AddSC_scourge_invasion() +{ + Script* newscript; + + newscript = new Script; + newscript->Name = "scourge_invasion_necropolis"; + newscript->GetAI = &GetNewAIInstance; + newscript->RegisterSelf(); + + newscript = new Script; + newscript->Name = "scourge_invasion_mouth"; + newscript->GetAI = &GetNewAIInstance; + newscript->RegisterSelf(); + + newscript = new Script; + newscript->Name = "scourge_invasion_necropolis_health"; + newscript->GetAI = &GetNewAIInstance; + newscript->RegisterSelf(); + + newscript = new Script; + newscript->Name = "scourge_invasion_necropolis_relay"; + newscript->GetAI = &GetNewAIInstance; + newscript->RegisterSelf(); + + newscript = new Script; + newscript->Name = "scourge_invasion_necropolis_proxy"; + newscript->GetAI = &GetNewAIInstance; + newscript->RegisterSelf(); + + newscript = new Script; + newscript->Name = "scourge_invasion_necrotic_shard"; + newscript->GetAI = &GetNewAIInstance; + newscript->RegisterSelf(); + + newscript = new Script; + newscript->Name = "scourge_invasion_minion_spawner"; + newscript->GetAI = &GetNewAIInstance; + newscript->RegisterSelf(); + + newscript = new Script; + newscript->Name = "scourge_invasion_cultist_engineer"; + newscript->GetAI = &GetNewAIInstance; + newscript->RegisterSelf(); + + newscript = new Script; + newscript->Name = "scourge_invasion_minion"; + newscript->GetAI = &GetNewAIInstance; + newscript->RegisterSelf(); + + newscript = new Script; + newscript->Name = "scourge_invasion_go_circle"; + newscript->GetGameObjectAI = &GetNewAIInstance; + newscript->RegisterSelf(); + + newscript = new Script; + newscript->Name = "scourge_invasion_go_necropolis"; + newscript->GetGameObjectAI = &GetNewAIInstance; + newscript->RegisterSelf(); + + newscript = new Script; + newscript->Name = "npc_pallid_horror"; + newscript->GetAI = &GetNewAIInstance; + newscript->RegisterSelf(); + + RegisterSpellScript("spell_summon_boss"); + RegisterSpellScript("spell_despawner_self"); + RegisterSpellScript("spell_communique_trigger"); +} diff --git a/src/game/AI/ScriptDevAI/scripts/world/scourge_invasion.h b/src/game/AI/ScriptDevAI/scripts/world/scourge_invasion.h new file mode 100644 index 0000000000..15bc9fd16d --- /dev/null +++ b/src/game/AI/ScriptDevAI/scripts/world/scourge_invasion.h @@ -0,0 +1,421 @@ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef SCOURGE_INVASION_H +#define SCOURGE_INVASION_H + +#include "World/WorldState.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. Blizzard 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 +{ + // Mouth of Kel'Thuzad + NPC_MOUTH_OF_KELTHUZAD = 16995, + + // 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_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 ScourgeInvasionMisc +{ + ITEM_NECROTIC_RUNE = 22484, + + // Invisible Objects + GOBJ_BUTTRESS_TRAP = 181112, // [Guessed] Those objects can't be sniffed and are not available in any database. + + GOBJ_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. + GOBJ_SUMMON_MINION_TRAP_GHOST_SKELETON = 181155, // "" + GOBJ_SUMMON_MINION_TRAP_GHOUL_SKELETON = 181156, // "" + + // Visible Objects + GOBJ_SUMMON_CIRCLE = 181136, + GOBJ_SUMMONER_SHIELD = 181142, + + GOBJ_UNDEAD_FIRE = 181173, + GOBJ_UNDEAD_FIRE_AURA = 181174, + GOBJ_SKULLPILE_01 = 181191, + GOBJ_SKULLPILE_02 = 181192, + GOBJ_SKULLPILE_03 = 181193, + GOBJ_SKULLPILE_04 = 181194, + + GOBJ_NECROPOLIS_TINY = 181154, // Necropolis (scale 1.0). + GOBJ_NECROPOLIS_SMALL = 181373, // Necropolis (scale 1.5). + GOBJ_NECROPOLIS_MEDIUM = 181374, // Necropolis (scale 2.0). + GOBJ_NECROPOLIS_BIG = 181215, // Necropolis (scale 2.5). + GOBJ_NECROPOLIS_HUGE = 181223, // Necropolis (scale 3.5). + GOBJ_NECROPOLIS_CITY = 181172, // Necropolis at the Citys (scale 2.5). + + // 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. +}; + +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_MOUTH_OF_KELTHUZAD_YELL = 6, + EVENT_MOUTH_OF_KELTHUZAD_ZONE_START = 7, + EVENT_MOUTH_OF_KELTHUZAD_ZONE_STOP = 8, + EVENT_MOUTH_OF_KELTHUZAD_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 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_MOUTH_OF_KELTHUZAD_ZONE_ATTACK_START_1 = 13121, // Spawn. + BCT_MOUTH_OF_KELTHUZAD_ZONE_ATTACK_START_2 = 13125, // Spawn. 53 min between 2-3 in sniffs. + BCT_MOUTH_OF_KELTHUZAD_ZONE_ATTACK_ENDS_1 = 13165, // Despawn. + BCT_MOUTH_OF_KELTHUZAD_ZONE_ATTACK_ENDS_2 = 13164, // Despawn. + BCT_MOUTH_OF_KELTHUZAD_ZONE_ATTACK_ENDS_3 = 13163, // Despawn. + BCT_MOUTH_OF_KELTHUZAD_RANDOM_1 = 13126, // Random. + BCT_MOUTH_OF_KELTHUZAD_RANDOM_2 = 13124, // Random. + BCT_MOUTH_OF_KELTHUZAD_RANDOM_3 = 13122, // 180 seconds between 5-6 in sniffs. + BCT_MOUTH_OF_KELTHUZAD_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 \ No newline at end of file diff --git a/src/game/AI/ScriptDevAI/scripts/world/world_map_scripts.cpp b/src/game/AI/ScriptDevAI/scripts/world/world_map_scripts.cpp index 914937d7ea..991dd1fb87 100644 --- a/src/game/AI/ScriptDevAI/scripts/world/world_map_scripts.cpp +++ b/src/game/AI/ScriptDevAI/scripts/world/world_map_scripts.cpp @@ -30,6 +30,8 @@ EndScriptData */ #include #include #include +#include "AI/ScriptDevAI/scripts/world/scourge_invasion.h" +#include "World/World.h" #include "brewfest.h" enum diff --git a/src/game/AI/ScriptDevAI/system/ScriptLoader.cpp b/src/game/AI/ScriptDevAI/system/ScriptLoader.cpp index ebc1b1ee25..de13e708dc 100644 --- a/src/game/AI/ScriptDevAI/system/ScriptLoader.cpp +++ b/src/game/AI/ScriptDevAI/system/ScriptLoader.cpp @@ -29,6 +29,7 @@ extern void AddSC_spell_scripts(); extern void AddSC_world_map_scripts(); extern void AddSC_boss_highlord_kruul(); extern void AddSC_war_effort(); +extern void AddSC_scourge_invasion(); extern void AddSC_suns_reach_reclamation(); extern void AddSC_shade_of_the_horseman(); extern void AddSC_childrens_week_tbc(); @@ -392,6 +393,7 @@ void AddScripts() AddSC_world_map_scripts(); AddSC_boss_highlord_kruul(); AddSC_war_effort(); + AddSC_scourge_invasion(); AddSC_suns_reach_reclamation(); AddSC_shade_of_the_horseman(); AddSC_childrens_week_tbc(); diff --git a/src/game/Chat/Chat.cpp b/src/game/Chat/Chat.cpp index 46c48ee2ea..959e508e01 100644 --- a/src/game/Chat/Chat.cpp +++ b/src/game/Chat/Chat.cpp @@ -828,6 +828,15 @@ ChatCommand* ChatHandler::getCommandTable() { nullptr, 0, false, nullptr, "", nullptr } }; + static ChatCommand scourgeInvasionTable[] = + { + { "show", SEC_ADMINISTRATOR, false, &ChatHandler::HandleScourgeInvasionCommand, "", nullptr }, + { "state", SEC_ADMINISTRATOR, false, &ChatHandler::HandleScourgeInvasionStateCommand, "", nullptr }, + { "battleswon", SEC_ADMINISTRATOR, false, &ChatHandler::HandleScourgeInvasionBattlesWonCommand, "", nullptr }, + { "startzone", SEC_ADMINISTRATOR, false, &ChatHandler::HandleScourgeInvasionStartZone, "", nullptr }, + { nullptr, 0, false, nullptr, "", nullptr } + }; + static ChatCommand sunsReachReclamationTable[] = { { "phase", SEC_ADMINISTRATOR, false, &ChatHandler::HandleSunsReachReclamationPhaseCommand, "", nullptr }, @@ -841,6 +850,7 @@ ChatCommand* ChatHandler::getCommandTable() static ChatCommand worldStateTable[] = { { "wareffort", SEC_ADMINISTRATOR, false, nullptr, "", warEffortTable }, + { "scourgeinvasion",SEC_ADMINISTRATOR, false, nullptr, "", scourgeInvasionTable }, { "sunsreach", SEC_ADMINISTRATOR, false, nullptr, "", sunsReachReclamationTable }, { "variables", SEC_ADMINISTRATOR, false, &ChatHandler::HandleVariablePrint, "", nullptr }, { "expansion", SEC_ADMINISTRATOR, false, &ChatHandler::HandleExpansionRelease, "", nullptr }, diff --git a/src/game/Chat/Chat.h b/src/game/Chat/Chat.h index 753b9f80e8..84eacced8b 100644 --- a/src/game/Chat/Chat.h +++ b/src/game/Chat/Chat.h @@ -772,6 +772,10 @@ class ChatHandler bool HandleWarEffortCommand(char* args); bool HandleWarEffortPhaseCommand(char* args); bool HandleWarEffortCounterCommand(char* args); + bool HandleScourgeInvasionCommand(char* args); + bool HandleScourgeInvasionStateCommand(char* args); + bool HandleScourgeInvasionBattlesWonCommand(char* args); + bool HandleScourgeInvasionStartZone(char* args); bool HandleSunsReachReclamationPhaseCommand(char* args); bool HandleSunsReachReclamationSubPhaseCommand(char* args); bool HandleSunsReachReclamationCounterCommand(char* args); diff --git a/src/game/Chat/Level3.cpp b/src/game/Chat/Level3.cpp index ba817ebc4a..6ca2366006 100644 --- a/src/game/Chat/Level3.cpp +++ b/src/game/Chat/Level3.cpp @@ -6862,6 +6862,50 @@ bool ChatHandler::HandleWarEffortCounterCommand(char* args) return true; } +bool ChatHandler::HandleScourgeInvasionCommand(char* args) +{ + return true; +} + +bool ChatHandler::HandleScourgeInvasionStateCommand(char* args) +{ + uint32 value; + if (!ExtractUInt32(&args, value) || value >= SI_STATE_MAX) + { + PSendSysMessage("Enter valid value for state."); + return true; + } + + sWorldState.SetScourgeInvasionState(SIState(value)); + return true; +} + +bool ChatHandler::HandleScourgeInvasionBattlesWonCommand(char* args) +{ + int32 value; + if (!ExtractInt32(&args, value)) + { + PSendSysMessage("Enter valid count for battles won."); + return true; + } + + sWorldState.AddBattlesWon(value); + return true; +} + +bool ChatHandler::HandleScourgeInvasionStartZone(char* args) +{ + int32 value; + if (!ExtractInt32(&args, value)) + { + PSendSysMessage("Enter valid SIZoneId (0-7)."); + return true; + } + + sWorldState.StartZoneEvent(SIZoneIds(value)); + return true; +} + bool ChatHandler::HandleSunsReachReclamationPhaseCommand(char* args) { uint32 param; diff --git a/src/game/DBScripts/ScriptMgr.cpp b/src/game/DBScripts/ScriptMgr.cpp index aca7d5fee0..ee91060bf8 100644 --- a/src/game/DBScripts/ScriptMgr.cpp +++ b/src/game/DBScripts/ScriptMgr.cpp @@ -2358,10 +2358,12 @@ bool ScriptAction::ExecuteDbscriptCommand(WorldObject* pSource, WorldObject* pTa // if radius is provided send AI event around if (m_script->sendAIEvent.radius) - ((Creature*)pSource)->AI()->SendAIEventAround(AIEventType(m_script->sendAIEvent.eventType), (Unit*)pTarget, 0, float(m_script->sendAIEvent.radius)); + ((Creature*)pSource)->AI()->SendAIEventAround(AIEventType(m_script->sendAIEvent.eventType), (Unit*)pTarget, 0, float(m_script->sendAIEvent.radius), m_script->sendAIEvent.value); // else if no radius and target is creature send AI event to target else if (pTarget->GetTypeId() == TYPEID_UNIT) - ((Creature*)pSource)->AI()->SendAIEvent(AIEventType(m_script->sendAIEvent.eventType), nullptr, (Unit*)pTarget); + static_cast(pSource)->AI()->SendAIEvent(AIEventType(m_script->sendAIEvent.eventType), nullptr, (Unit*)pTarget, m_script->sendAIEvent.value); + else if (pSource->IsPlayer() && pTarget->IsCreature()) + static_cast(pTarget)->AI()->ReceiveAIEvent(AIEventType(m_script->sendAIEvent.eventType), (Unit*)pSource, (Unit*)pSource, m_script->sendAIEvent.value); break; } case SCRIPT_COMMAND_SET_FACING: // 36 diff --git a/src/game/DBScripts/ScriptMgr.h b/src/game/DBScripts/ScriptMgr.h index 562eddb00a..f4e60755ed 100644 --- a/src/game/DBScripts/ScriptMgr.h +++ b/src/game/DBScripts/ScriptMgr.h @@ -367,6 +367,7 @@ struct ScriptInfo { uint32 eventType; // datalong uint32 radius; // datalong2 + uint32 value; // datalong3 } sendAIEvent; struct // SCRIPT_COMMAND_SET_FACING (36) diff --git a/src/game/Entities/Object.cpp b/src/game/Entities/Object.cpp index 75d8deeae5..0387613f11 100644 --- a/src/game/Entities/Object.cpp +++ b/src/game/Entities/Object.cpp @@ -1773,10 +1773,10 @@ bool WorldObject::IsPositionValid() const return MaNGOS::IsValidMapCoord(m_position.x, m_position.y, m_position.z, m_position.o); } -void WorldObject::MonsterSay(const char* text, uint32 /*language*/, Unit const* target) const +void WorldObject::MonsterSay(char const* text, uint32 language, Unit const* target) const { WorldPacket data; - ChatHandler::BuildChatPacket(data, CHAT_MSG_MONSTER_SAY, text, LANG_UNIVERSAL, CHAT_TAG_NONE, GetObjectGuid(), GetName(), + ChatHandler::BuildChatPacket(data, CHAT_MSG_MONSTER_SAY, text, Language(language), CHAT_TAG_NONE, GetObjectGuid(), GetName(), target ? target->GetObjectGuid() : ObjectGuid(), target ? target->GetName() : ""); SendMessageToSetInRange(data, sWorld.getConfig(CONFIG_FLOAT_LISTEN_RANGE_SAY), true); } diff --git a/src/game/Globals/ObjectMgr.cpp b/src/game/Globals/ObjectMgr.cpp index 84f2faab98..41c864c52a 100644 --- a/src/game/Globals/ObjectMgr.cpp +++ b/src/game/Globals/ObjectMgr.cpp @@ -7830,7 +7830,7 @@ void ObjectMgr::LoadBroadcastTextLocales() sLog.outString(">> Loaded %u texts from %s", count, "broadcast_text_locale"); sLog.outString(); } - + void ObjectMgr::DeleteCreatureData(uint32 guid) { // remove mapid*cellid -> guid_set map @@ -10075,4 +10075,4 @@ bool DoDisplayText(WorldObject* source, int32 entry, Unit const* target, uint32 source->MonsterText(content, type, lang, target); return true; -} +} \ No newline at end of file diff --git a/src/game/Grids/GridNotifiers.h b/src/game/Grids/GridNotifiers.h index 81c7cbf7e8..2d40e7bf77 100644 --- a/src/game/Grids/GridNotifiers.h +++ b/src/game/Grids/GridNotifiers.h @@ -687,6 +687,28 @@ namespace MaNGOS // prevent clone this object AllGameObjectEntriesListInObjectRangeCheck(AllGameObjectEntriesListInObjectRangeCheck const&); }; + + // combine with above somehow? fuck + class AllGameObjectsMatchingOneEntryInRange + { + public: + AllGameObjectsMatchingOneEntryInRange(WorldObject const* pObject, std::vector const& entries, float fMaxRange) + : m_pObject(pObject), entries(entries), m_fRange(fMaxRange) {} + bool operator() (GameObject* pGo) + { + for (const auto entry : entries) { + if (pGo->GetEntry() == entry && m_pObject->IsWithinDist(pGo, m_fRange, false)) { + return true; + } + } + return false; + } + + private: + WorldObject const* m_pObject; + std::vector entries; + float m_fRange; + }; // x y z version of above class AllGameObjectEntriesListInPosRangeCheck @@ -1280,6 +1302,27 @@ namespace MaNGOS float m_fRange; }; + class AllCreaturesMatchingOneEntryInRange + { + public: + AllCreaturesMatchingOneEntryInRange(WorldObject const* pObject, std::vector const& entries, float fMaxRange) + : m_pObject(pObject), entries(entries), m_fRange(fMaxRange) {} + bool operator() (Unit* pUnit) + { + for (const auto entry : entries) { + if (pUnit->GetEntry() == entry && m_pObject->IsWithinDist(pUnit, m_fRange, false)) { + return true; + } + } + return false; + } + + private: + WorldObject const* m_pObject; + std::vector entries; + float m_fRange; + }; + // Player checks and do class AnyPlayerInObjectRangeCheck diff --git a/src/game/World/WorldState.cpp b/src/game/World/WorldState.cpp index 1269d60a02..f22860297f 100644 --- a/src/game/World/WorldState.cpp +++ b/src/game/World/WorldState.cpp @@ -26,6 +26,7 @@ #include #include #include +#include "AI/ScriptDevAI/scripts/world/scourge_invasion.h" enum { @@ -266,12 +267,42 @@ void WorldState::Load() else memset(m_loveIsInTheAirData.counters, 0, sizeof(LoveIsInTheAir)); break; + case SAVE_ID_SCOURGE_INVASION: + if (data.size()) + { + try + { + uint32 state; + loadStream >> state; + m_siData.m_state = SIState(state); + for (uint32 i = 0; i < SI_TIMER_MAX; ++i) + { + uint64 time; + loadStream >> time; + m_siData.m_timers[i] = std::chrono::time_point_cast(Clock::from_time_t(time)); + } + loadStream >> m_siData.m_battlesWon >> m_siData.m_lastAttackZone; + for (uint32 i = 0; i < SI_REMAINING_MAX; ++i) + loadStream >> m_siData.m_remaining[i]; + } + catch (std::exception& e) + { + sLog.outError("%s", e.what()); + m_siData.Reset(); + } + } + break; } } while (result->NextRow()); } StartWarEffortEvent(); SpawnWarEffortGos(); + if (m_siData.m_state == STATE_1_ENABLED) + { + StartScourgeInvasion(); + HandleDefendedZones(); + } RespawnEmeraldDragons(); StartSunsReachPhase(true); StartSunwellGatePhase(); @@ -330,6 +361,12 @@ void WorldState::Save(SaveIds saveId) SaveHelper(loveData, SAVE_ID_LOVE_IS_IN_THE_AIR); break; } + case SAVE_ID_SCOURGE_INVASION: + { + std::string siData = m_siData.GetData(); + SaveHelper(siData, SAVE_ID_SCOURGE_INVASION); + break; + } default: break; } } @@ -660,6 +697,23 @@ bool WorldState::IsConditionFulfilled(uint32 conditionId, uint32 state) const return m_aqData.m_WarEffortCounters[id] == aqWorldStateTotalsMap[id].second; } + // Scourge Invasion + switch (conditionId) + { + case WORLD_STATE_SCOURGE_WINTERSPRING: + return GetSIRemaining(SI_REMAINING_WINTERSPRING) > 0; + case WORLD_STATE_SCOURGE_AZSHARA: + return GetSIRemaining(SI_REMAINING_AZSHARA) > 0; + case WORLD_STATE_SCOURGE_BLASTED_LANDS: + return GetSIRemaining(SI_REMAINING_BLASTED_LANDS) > 0; + case WORLD_STATE_SCOURGE_BURNING_STEPPES: + return GetSIRemaining(SI_REMAINING_BURNING_STEPPES) > 0; + case WORLD_STATE_SCOURGE_TANARIS: + return GetSIRemaining(SI_REMAINING_TANARIS) > 0; + case WORLD_STATE_SCOURGE_EASTERN_PLAGUELANDS: + return GetSIRemaining(SI_REMAINING_EASTERN_PLAGUELANDS) > 0; + } + return m_transportStates.at(conditionId) == state; } @@ -668,6 +722,14 @@ void WorldState::HandleConditionStateChange(uint32 conditionId, uint32 state) m_transportStates[conditionId] = state; } +Map* WorldState::GetMap(uint32 mapId, Position const& invZone) +{ + Map* map = sMapMgr.FindMap(mapId); + if (!map) + sLog.outError("ScourgeInvasionEvent::GetMap found no map with mapId %d, x: %d, y: %d.", mapId, invZone.x, invZone.y); + return map; +} + void WorldState::BuffMagtheridonTeam(Team team) { for (ObjectGuid& guid : m_magtheridonHeadPlayers) @@ -806,6 +868,31 @@ void WorldState::Update(const uint32 diff) ChangeWarEffortPhase2Tier(remainingDays); } } + + 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_attackPoints) + { + if (zone.second.zoneId == ZONEID_UNDERCITY) + StartNewCityAttackIfTime(SI_TIMER_UNDERCITY, zone.second.zoneId); + else if (zone.second.zoneId == ZONEID_STORMWIND_CITY) + StartNewCityAttackIfTime(SI_TIMER_STORMWIND, zone.second.zoneId); + } + + auto now = sWorld.GetCurrentClockTime(); + for (auto& zone : m_siData.m_invasionPoints) + HandleActiveZone(GetTimerIdForZone(zone.second.zoneId), zone.second.zoneId, zone.second.remainingVar, now); + } + } + else m_siData.m_broadcastTimer -= diff; + } } void WorldState::SendWorldstateUpdate(std::mutex& mutex, GuidVector const& guids, uint32 value, uint32 worldStateId) @@ -923,6 +1010,8 @@ void WorldState::RespawnEmeraldDragons() }); } +// War effort section + void WorldState::AddWarEffortProgress(AQResources resource, uint32 count) { std::lock_guard guard(m_aqData.m_warEffortMutex); @@ -1262,7 +1351,9 @@ std::string WorldState::GetAQPrintout() std::string AhnQirajData::GetData() { - std::string output = std::to_string(m_phase) + " " + std::to_string(m_timer); + auto curTime = World::GetCurrentClockTime(); + auto respawnTime = std::chrono::milliseconds(m_timer) + curTime; + std::string output = std::to_string(m_phase) + " " + std::to_string(uint64(Clock::to_time_t(respawnTime))); for (uint32 value : m_WarEffortCounters) output += " " + std::to_string(value); output += " " + std::to_string(m_phase2Tier); @@ -1274,6 +1365,653 @@ uint32 AhnQirajData::GetDaysRemaining() const return uint32(m_timer / (DAY * IN_MILLISECONDS)); } +// Scourge invasion section + +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(); + else if (state == STATE_0_DISABLED) + StopScourgeInvasion(); +} + +void WorldState::StartScourgeInvasion() +{ + sGameEventMgr.StartEvent(GAME_EVENT_SCOURGE_INVASION); + BroadcastSIWorldstates(); + if (m_siData.m_state == STATE_1_ENABLED) + { + for (auto& zone : m_siData.m_attackPoints) + { + if (zone.second.zoneId == ZONEID_UNDERCITY) + StartNewCityAttackIfTime(SI_TIMER_UNDERCITY, zone.second.zoneId); + else if (zone.second.zoneId == ZONEID_STORMWIND_CITY) + StartNewCityAttackIfTime(SI_TIMER_STORMWIND, zone.second.zoneId); + } + + // randomization of init so that not every invasion starts the same way + std::vector randomIds(m_siData.m_invasionPoints.size()); + uint32 i = 0; + for (auto& zone : m_siData.m_invasionPoints) + { + randomIds[i] = zone.first; + ++i; + } + std::shuffle(randomIds.begin(), randomIds.end(), *GetRandomGenerator()); + for (auto id : randomIds) + OnEnable(m_siData.m_invasionPoints[id]); + } +} + +ScourgeInvasionData::ScourgeInvasionData() : m_state(STATE_0_DISABLED), m_battlesWon(0), m_broadcastTimer(10000) +{ + memset(m_remaining, 0, sizeof(m_remaining)); + + InvasionZone winterspring; + { + winterspring.map = 1; + winterspring.zoneId = ZONEID_WINTERSPRING; + winterspring.remainingVar = SI_REMAINING_WINTERSPRING; + winterspring.necroAmount = 3; + winterspring.mouth.push_back(Position(7736.56f, -4033.75f, 696.327f, 5.51524f)); + } + + InvasionZone tanaris; + { + tanaris.map = 1; + tanaris.zoneId = ZONEID_TANARIS; + tanaris.remainingVar = SI_REMAINING_TANARIS; + tanaris.necroAmount = 3; + tanaris.mouth.push_back(Position(-8352.68f, -3972.68f, 10.0753f, 2.14675f)); + } + + InvasionZone azshara; + { + azshara.map = 1; + azshara.zoneId = ZONEID_AZSHARA; + azshara.remainingVar = SI_REMAINING_AZSHARA; + azshara.necroAmount = 2; + azshara.mouth.push_back(Position(3273.75f, -4276.98f, 125.509f, 5.44543f)); + } + + InvasionZone blasted_lands; + { + blasted_lands.map = 0; + blasted_lands.zoneId = ZONEID_BLASTED_LANDS; + blasted_lands.remainingVar = SI_REMAINING_BLASTED_LANDS; + blasted_lands.necroAmount = 2; + blasted_lands.mouth.push_back(Position(-11429.3f, -3327.82f, 7.73628f, 1.0821f)); + } + + InvasionZone eastern_plaguelands; + { + eastern_plaguelands.map = 0; + eastern_plaguelands.zoneId = ZONEID_EASTERN_PLAGUELANDS; + eastern_plaguelands.remainingVar = SI_REMAINING_EASTERN_PLAGUELANDS; + eastern_plaguelands.necroAmount = 2; + eastern_plaguelands.mouth.push_back(Position(2014.55f, -4934.52f, 73.9846f, 0.0698132f)); + } + + InvasionZone burning_steppes; + { + burning_steppes.map = 0; + burning_steppes.zoneId = ZONEID_BURNING_STEPPES; + burning_steppes.remainingVar = SI_REMAINING_BURNING_STEPPES; + burning_steppes.necroAmount = 2; + burning_steppes.mouth.push_back(Position(-8229.53f, -1118.11f, 144.012f, 6.17846f)); + } + + m_invasionPoints.emplace(ZONEID_WINTERSPRING, winterspring); + m_invasionPoints.emplace(ZONEID_TANARIS, tanaris); + m_invasionPoints.emplace(ZONEID_AZSHARA, azshara); + m_invasionPoints.emplace(ZONEID_BLASTED_LANDS, blasted_lands); + m_invasionPoints.emplace(ZONEID_EASTERN_PLAGUELANDS, eastern_plaguelands); + m_invasionPoints.emplace(ZONEID_BURNING_STEPPES, burning_steppes); + + CityAttack undercity; + { + undercity.map = 0; + undercity.zoneId = ZONEID_UNDERCITY; + undercity.pallid.push_back(Position(1595.87f, 440.539f, -46.3349f, 2.28207f)); // Royal Quarter + undercity.pallid.push_back(Position(1659.2f, 265.988f, -62.1788f, 3.64283f)); // Trade Quarter + } + + CityAttack stormwind; + { + stormwind.map = 0; + stormwind.zoneId = ZONEID_STORMWIND_CITY; + stormwind.pallid.push_back(Position(-8578.15f, 886.382f, 87.3148f, 0.586275f)); // Stormwind Keep + stormwind.pallid.push_back(Position(-8578.15f, 886.382f, 87.3148f, 0.586275f)); // Trade District + } + + m_attackPoints.emplace(ZONEID_UNDERCITY, undercity); + m_attackPoints.emplace(ZONEID_STORMWIND_CITY, stormwind); +} + +void ScourgeInvasionData::Reset() +{ + std::lock_guard 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& zone : m_siData.m_attackPoints) + OnDisable(zone.second); + + for (auto& zone : m_siData.m_invasionPoints) + OnDisable(zone.second); +} + +uint32 WorldState::GetSIRemaining(SIRemaining remaining) const +{ + return m_siData.m_remaining[remaining]; +} + +uint32 WorldState::GetSIRemainingByZone(uint32 zoneId) const +{ + SIRemaining remainingId; + switch (zoneId) + { + default: + case ZONEID_WINTERSPRING: remainingId = SI_REMAINING_WINTERSPRING; break; + case ZONEID_AZSHARA: remainingId = SI_REMAINING_AZSHARA; break; + case ZONEID_EASTERN_PLAGUELANDS: remainingId = SI_REMAINING_EASTERN_PLAGUELANDS; break; + case ZONEID_BLASTED_LANDS: remainingId = SI_REMAINING_BLASTED_LANDS; break; + case ZONEID_BURNING_STEPPES: remainingId = SI_REMAINING_BURNING_STEPPES; break; + case ZONEID_TANARIS: remainingId = SI_REMAINING_TANARIS; break; + } + return GetSIRemaining(remainingId); +} + +void WorldState::SetSIRemaining(SIRemaining remaining, uint32 value) +{ + std::lock_guard guard(m_siData.m_siMutex); + m_siData.m_remaining[remaining] = value; +} + +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 guard(m_siData.m_siMutex); + return m_siData.m_battlesWon; +} + +void WorldState::AddBattlesWon(int32 count) +{ + std::lock_guard guard(m_siData.m_siMutex); + m_siData.m_battlesWon += count; + HandleDefendedZones(); +} + +uint32 WorldState::GetLastAttackZone() +{ + std::lock_guard guard(m_siData.m_siMutex); + return m_siData.m_lastAttackZone; +} + +void WorldState::SetLastAttackZone(uint32 zoneId) +{ + std::lock_guard 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); + + HashMapHolder::MapType& m = sObjectAccessor.GetPlayers(); + for (const auto& itr : m) + { + Player* pl = itr.second; + // do not process players which are not in world + if (!pl->IsInWorld()) + continue; + + pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_AZSHARA, remainingAzshara > 0 ? 1 : 0); + pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_BLASTED_LANDS, remainingBlastedLands > 0 ? 1 : 0); + pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_BURNING_STEPPES, remainingBurningSteppes > 0 ? 1 : 0); + pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_EASTERN_PLAGUELANDS, remainingEasternPlaguelands > 0 ? 1 : 0); + pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_TANARIS, remainingTanaris > 0 ? 1 : 0); + pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_WINTERSPRING, remainingWinterspring > 0 ? 1 : 0); + + pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_BATTLES_WON, victories); + pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_NECROPOLIS_AZSHARA, remainingAzshara); + pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_NECROPOLIS_BLASTED_LANDS, remainingBlastedLands); + pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_NECROPOLIS_BURNING_STEPPES, remainingBurningSteppes); + pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_NECROPOLIS_EASTERN_PLAGUELANDS, remainingEasternPlaguelands); + pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_NECROPOLIS_TANARIS, remainingTanaris); + pl->SendUpdateWorldState(WORLD_STATE_SCOURGE_NECROPOLIS_WINTERSPRING, remainingWinterspring); + } +} + +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) + { + default: + case SI_ZONE_AZSHARA: StartNewInvasion(ZONEID_AZSHARA); break; + case SI_ZONE_BLASTED_LANDS: StartNewInvasion(ZONEID_BLASTED_LANDS); break; + case SI_ZONE_BURNING_STEPPES: StartNewInvasion(ZONEID_BURNING_STEPPES); break; + case SI_ZONE_EASTERN_PLAGUELANDS: StartNewInvasion(ZONEID_EASTERN_PLAGUELANDS); break; + case SI_ZONE_TANARIS: StartNewInvasion(ZONEID_TANARIS); break; + case SI_ZONE_WINTERSPRING: StartNewInvasion(ZONEID_WINTERSPRING); break; + case SI_ZONE_STORMWIND: StartNewCityAttack(ZONEID_STORMWIND_CITY); break; + case SI_ZONE_UNDERCITY: StartNewCityAttack(ZONEID_UNDERCITY); break; + } +} + +void WorldState::StartNewInvasionIfTime(uint32 attackTimeVar, uint32 zoneId) +{ + TimePoint now = sWorld.GetCurrentClockTime(); + + // Not yet time + if (now < sWorldState.GetSITimer(SITimers(attackTimeVar))) + return; + + StartNewInvasion(zoneId); +} + +void WorldState::StartNewCityAttackIfTime(uint32 attackTimeVar, uint32 zoneId) +{ + TimePoint now = sWorld.GetCurrentClockTime(); + + // Not yet time + if (now < sWorldState.GetSITimer(SITimers(attackTimeVar))) + return; + + StartNewCityAttack(zoneId); +} + +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; + + sLog.outBasic("[Scourge Invasion Event] Starting new invasion in %d.", zoneId); + + ScourgeInvasionData::InvasionZone& zone = m_siData.m_invasionPoints[zoneId]; + + Map* mapPtr = GetMap(zone.map, zone.mouth[0]); + + if (!mapPtr) + { + sLog.outError("ScourgeInvasionEvent::StartNewInvasionIfTime unable to access required map (%d). Retrying next update.", zone.map); + return; + } + + switch (zoneId) + { + case ZONEID_AZSHARA: sGameEventMgr.StartEvent(GAME_EVENT_SCOURGE_INVASION_AZSHARA); break; + case ZONEID_BLASTED_LANDS: sGameEventMgr.StartEvent(GAME_EVENT_SCOURGE_INVASION_BLASTED_LANDS); break; + case ZONEID_BURNING_STEPPES: sGameEventMgr.StartEvent(GAME_EVENT_SCOURGE_INVASION_BURNING_STEPPES); break; + case ZONEID_EASTERN_PLAGUELANDS: sGameEventMgr.StartEvent(GAME_EVENT_SCOURGE_INVASION_EASTERN_PLAGUELANDS); break; + case ZONEID_TANARIS: sGameEventMgr.StartEvent(GAME_EVENT_SCOURGE_INVASION_TANARIS); break; + case ZONEID_WINTERSPRING: sGameEventMgr.StartEvent(GAME_EVENT_SCOURGE_INVASION_WINTERSPRING); break; + } + + if (mapPtr) + SummonMouth(mapPtr, zone, zone.mouth[0]); +} + +void WorldState::StartNewCityAttack(uint32 zoneId) +{ + sLog.outBasic("[Scourge Invasion Event] Starting new City attack in zone %d.", zoneId); + + ScourgeInvasionData::CityAttack& zone = m_siData.m_attackPoints[zoneId]; + + uint32 SpawnLocationID = (urand(0, zone.pallid.size() - 1)); + + Map* mapPtr = 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 (!mapPtr) + { + sLog.outError("ScourgeInvasionEvent::StartNewCityAttackIfTime unable to access required map (%d). Retrying next update.", zone.map); + return; + } + + if (m_siData.m_pendingPallids.find(zoneId) != m_siData.m_pendingPallids.end()) + return; + + if (!m_siData.m_attackPoints[zoneId].pallidGuid.IsEmpty()) + return; + + if (mapPtr && SummonPallid(mapPtr, zone, zone.pallid[SpawnLocationID], SpawnLocationID)) + sLog.outBasic("[Scourge Invasion Event] Pallid Horror summoned in zone %d.", zoneId); + else + sLog.outError("ScourgeInvasionEvent::StartNewCityAttackIfTime unable to spawn pallid in %d.", 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 + sLog.outBasic("[Scourge Invasion Event] Resuming Scourge invasion in zone %d", zone.zoneId); + + uint32 num_necropolises_remaining = sWorldState.GetSIRemaining(SIRemaining(zone.remainingVar)); + + // 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])) + { + sLog.outError("ScourgeInvasionEvent::ResumeInvasion map %d not accessible. Retry next update.", zone.map); + return false; + } + } + + Map* mapPtr = GetMap(zone.map, zone.mouth[0]); + if (!mapPtr) + { + sLog.outError("ScourgeInvasionEvent::ResumeInvasion failed getting map, even after making sure they were loaded...."); + return false; + } + + SummonMouth(mapPtr, zone, zone.mouth[0]); + + return true; +} + +bool WorldState::SummonMouth(Map* map, ScourgeInvasionData::InvasionZone& zone, Position position) +{ + AddPendingInvasion(zone.zoneId); + map->GetMessager().AddMessage([=](Map* map) + { + // Remove old mouth if required. + Creature* existingMouth = map->GetCreature(zone.mouthGuid); + + if (existingMouth) + existingMouth->RemoveFromWorld(); + + if (Creature* mouth = WorldObject::SummonCreature(TempSpawnSettings(nullptr, NPC_MOUTH_OF_KELTHUZAD, position.x, position.y, position.z, position.o, TEMPSPAWN_DEAD_DESPAWN, 0, true), map)) + { + mouth->AI()->SendAIEvent(AI_EVENT_CUSTOM_A, mouth, mouth, EVENT_MOUTH_OF_KELTHUZAD_ZONE_START); + sWorldState.SetMouthGuid(zone.zoneId, mouth->GetObjectGuid()); + sWorldState.SetSIRemaining(SIRemaining(zone.remainingVar), zone.necroAmount); + } + sWorldState.RemovePendingInvasion(zone.zoneId); + }); + + return true; +} + +bool WorldState::SummonPallid(Map* map, ScourgeInvasionData::CityAttack& zone, Position position, uint32 spawnLoc) +{ + AddPendingPallid(zone.zoneId); + map->GetMessager().AddMessage([=](Map* map) + { + // Remove old pallid if required. + Creature* existingPallid = map->GetCreature(zone.pallidGuid); + uint32 pathID = 0; + + if (existingPallid) + existingPallid->RemoveFromWorld(); + + if (Creature* pallid = WorldObject::SummonCreature(TempSpawnSettings(nullptr, PickRandomValue(NPC_PALLID_HORROR, NPC_PATCHWORK_TERROR), position.x, position.y, position.z, position.o, TEMPSPAWN_DEAD_DESPAWN, 0, true), map)) + { + pallid->GetMotionMaster()->Clear(false, true); + if (pallid->GetZoneId() == ZONEID_UNDERCITY) + pathID = spawnLoc == 0 ? 1 : 0; + else + pathID = spawnLoc == 0 ? 2 : 3; + + pallid->GetMotionMaster()->MoveWaypoint(pathID, PATH_FROM_ENTRY); + + sWorldState.SetPallidGuid(zone.zoneId, pallid->GetObjectGuid()); + } + 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_invasionPoints[zoneId]; + + Map* map = sMapMgr.FindMap(zone.map); + + 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::milliseconds(zoneAttackTimer); + uint64 timeToNextAttack = (next_attack - now).count(); + + if (zone.mouthGuid) + { + map->GetMessager().AddMessage([=](Map* map) + { + // 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); + + sLog.outBasic("[Scourge Invasion Event] The Scourge has been defeated in %d, next attack starting in %d minutes.", zoneId, uint32(timeToNextAttack / 60)); + sLog.outBasic("[Scourge Invasion Event] %d victories", sWorldState.GetBattlesWon()); + + if (mouth) + mouth->AI()->SendAIEvent(AI_EVENT_CUSTOM_A, mouth, mouth, EVENT_MOUTH_OF_KELTHUZAD_ZONE_STOP); + else + sLog.outError("ScourgeInvasionEvent::HandleActiveZone ObjectGuid %d not found.", zone.mouthGuid); + } + }); + } + 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_attackPoints[zoneId].pallidGuid = guid; +} + +void WorldState::SetMouthGuid(uint32 zoneId, ObjectGuid guid) +{ + m_siData.m_invasionPoints[zoneId].mouthGuid = guid; +} + +void WorldState::AddPendingInvasion(uint32 zoneId) +{ + std::lock_guard guard(m_siData.m_siMutex); + m_siData.m_pendingInvasions.insert(zoneId); +} + +void WorldState::RemovePendingInvasion(uint32 zoneId) +{ + std::lock_guard guard(m_siData.m_siMutex); + m_siData.m_pendingInvasions.erase(zoneId); +} + +void WorldState::AddPendingPallid(uint32 zoneId) +{ + std::lock_guard guard(m_siData.m_siMutex); + m_siData.m_pendingPallids.insert(zoneId); +} + +void WorldState::RemovePendingPallid(uint32 zoneId) +{ + std::lock_guard 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.remainingVar)) > 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]); + + map->GetMessager().AddMessage([guid = zone.mouthGuid](Map* map) + { + if (Creature* mouth = map->GetCreature(guid)) + mouth->ForcedDespawn(); + }); +} + +void WorldState::OnDisable(ScourgeInvasionData::CityAttack& zone) +{ + if (!zone.pallidGuid) + return; + + Map* map = GetMap(zone.map, zone.pallid[0]); + + map->GetMessager().AddMessage([guid = zone.pallidGuid](Map* map) + { + if (Creature* pallid = map->GetCreature(guid)) + pallid->ForcedDespawn(); + }); +} + +bool WorldState::IsActiveZone(uint32 zoneId) +{ + return false; +} + +uint32 WorldState::GetActiveZones() +{ + int i = 0; + i += m_siData.m_pendingInvasions.size(); + for (const auto& invasionPoint : m_siData.m_invasionPoints) + { + Map* mapPtr = GetMap(invasionPoint.second.map, invasionPoint.second.mouth[0]); + if (!mapPtr) + { + sLog.outError("ScourgeInvasionEvent::GetActiveZones no map for zone %d.", invasionPoint.second.map); + continue; + } + + Creature* pMouth = mapPtr->GetCreature(invasionPoint.second.mouthGuid); + if (pMouth) + i++; + } + return i; +} + +uint32 WorldState::GetTimerIdForZone(uint32 zoneId) +{ + uint32 attackTime = 0; + switch (zoneId) + { + case ZONEID_TANARIS: attackTime = SI_TIMER_TANARIS; break; + case ZONEID_BLASTED_LANDS: attackTime = SI_TIMER_BLASTED_LANDS; break; + case ZONEID_EASTERN_PLAGUELANDS: attackTime = SI_TIMER_EASTERN_PLAGUELANDS; break; + case ZONEID_BURNING_STEPPES: attackTime = SI_TIMER_BURNING_STEPPES; break; + case ZONEID_WINTERSPRING: attackTime = SI_TIMER_WINTERSPRING; break; + case ZONEID_AZSHARA: attackTime = SI_TIMER_AZSHARA; break; + } + return attackTime; +} + // Highlord Kruul enum HighlordKruul : uint32 { @@ -1888,6 +2626,33 @@ void WorldState::StartExpansionEvent() void WorldState::FillInitialWorldStates(ByteBuffer& data, uint32& count, uint32 zoneId, uint32 /*areaId*/) { + if (m_siData.m_state != PHASE_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); + + FillInitialWorldStateData(data, count, WORLD_STATE_SCOURGE_AZSHARA, remainingAzshara > 0 ? 1 : 0); + FillInitialWorldStateData(data, count, WORLD_STATE_SCOURGE_BLASTED_LANDS, remainingBlastedLands > 0 ? 1 : 0); + FillInitialWorldStateData(data, count, WORLD_STATE_SCOURGE_BURNING_STEPPES, remainingBurningSteppes > 0 ? 1 : 0); + FillInitialWorldStateData(data, count, WORLD_STATE_SCOURGE_EASTERN_PLAGUELANDS, remainingEasternPlaguelands > 0 ? 1 : 0); + FillInitialWorldStateData(data, count, WORLD_STATE_SCOURGE_TANARIS, remainingTanaris > 0 ? 1 : 0); + FillInitialWorldStateData(data, count, WORLD_STATE_SCOURGE_WINTERSPRING, remainingWinterspring > 0 ? 1 : 0); + + FillInitialWorldStateData(data, count, WORLD_STATE_SCOURGE_BATTLES_WON, victories); + FillInitialWorldStateData(data, count, WORLD_STATE_SCOURGE_NECROPOLIS_AZSHARA, remainingAzshara); + FillInitialWorldStateData(data, count, WORLD_STATE_SCOURGE_NECROPOLIS_BLASTED_LANDS, remainingBlastedLands); + FillInitialWorldStateData(data, count, WORLD_STATE_SCOURGE_NECROPOLIS_BURNING_STEPPES, remainingBurningSteppes); + FillInitialWorldStateData(data, count, WORLD_STATE_SCOURGE_NECROPOLIS_EASTERN_PLAGUELANDS, remainingEasternPlaguelands); + FillInitialWorldStateData(data, count, WORLD_STATE_SCOURGE_NECROPOLIS_TANARIS, remainingTanaris); + FillInitialWorldStateData(data, count, WORLD_STATE_SCOURGE_NECROPOLIS_WINTERSPRING, remainingWinterspring); + } + switch (zoneId) { case ZONEID_STORMWIND_CITY: @@ -1968,4 +2733,4 @@ void WorldState::FillInitialWorldStates(ByteBuffer& data, uint32& count, uint32 break; } } -} \ No newline at end of file +} diff --git a/src/game/World/WorldState.h b/src/game/World/WorldState.h index 135169c9b7..6d4c124a9a 100644 --- a/src/game/World/WorldState.h +++ b/src/game/World/WorldState.h @@ -37,6 +37,14 @@ enum ZoneIds ZONEID_TIRISFAL_GLADES = 85, ZONEID_EVERSONG_WOODS = 3430, + ZONEID_WINTERSPRING = 618, + ZONEID_AZSHARA = 16, + ZONEID_EASTERN_PLAGUELANDS = 139, + ZONEID_BLASTED_LANDS = 4, + ZONEID_BURNING_STEPPES = 46, + ZONEID_TANARIS = 440, + ZONEID_UNDERCITY_A = 1497, + ZONEID_STORMWIND_CITY = 1519, ZONEID_DARNASSUS = 1657, ZONEID_IRONFORGE = 1537, @@ -128,6 +136,7 @@ enum SaveIds SAVE_ID_EMERALD_DRAGONS = 0, SAVE_ID_AHN_QIRAJ = 1, SAVE_ID_LOVE_IS_IN_THE_AIR = 2, + SAVE_ID_SCOURGE_INVASION = 3, SAVE_ID_QUEL_DANAS = 20, SAVE_ID_EXPANSION_RELEASE = 21, @@ -158,6 +167,19 @@ enum GameEvents // base perpetual state GAME_EVENT_AHN_QIRAJ_EFFORT_PHASE_5 = 124, + // Scourge Invasion + GAME_EVENT_SCOURGE_INVASION = 17, + GAME_EVENT_SCOURGE_INVASION_WINTERSPRING = 90, + GAME_EVENT_SCOURGE_INVASION_TANARIS = 91, + GAME_EVENT_SCOURGE_INVASION_AZSHARA = 92, + GAME_EVENT_SCOURGE_INVASION_BLASTED_LANDS = 93, + GAME_EVENT_SCOURGE_INVASION_EASTERN_PLAGUELANDS = 94, + GAME_EVENT_SCOURGE_INVASION_BURNING_STEPPES = 95, + GAME_EVENT_SCOURGE_INVASION_50_INVASIONS = 96, + GAME_EVENT_SCOURGE_INVASION_100_INVASIONS = 97, + GAME_EVENT_SCOURGE_INVASION_150_INVASIONS = 98, + GAME_EVENT_SCOURGE_INVASION_INVASIONS_DONE = 99, + // Isle phases GAME_EVENT_QUEL_DANAS_PHASE_1 = 301, GAME_EVENT_QUEL_DANAS_PHASE_2_ONLY = 302, @@ -239,7 +261,6 @@ enum AQPhase PHASE_5_DONE, }; -// To be used struct AhnQirajData { uint32 m_phase; @@ -257,6 +278,90 @@ struct AhnQirajData uint32 GetDaysRemaining() const; }; +enum SIState : uint32 +{ + STATE_0_DISABLED, + STATE_1_ENABLED, + STATE_2_DEBUG, + 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, +}; + +struct ScourgeInvasionData +{ + struct InvasionZone + { + uint32 map; + uint32 zoneId; + uint32 remainingVar; + uint32 necroAmount; + ObjectGuid mouthGuid; + std::vector mouth; + }; + + struct CityAttack + { + uint32 map; + uint32 zoneId; + ObjectGuid pallidGuid; + std::vector pallid; + }; + + 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 m_pendingInvasions; + std::set m_pendingPallids; + std::map m_invasionPoints; + std::map m_attackPoints; + + ScourgeInvasionData(); + + void Reset(); + std::string GetData(); +}; + enum SunsReachPhases { SUNS_REACH_PHASE_1_STAGING_AREA, @@ -392,6 +497,47 @@ class WorldState std::pair GetResourceCounterAndMax(AQResourceGroup group, Team team); std::string GetAQPrintout(); + void SetScourgeInvasionState(SIState state); + void StartScourgeInvasion(); + void StopScourgeInvasion(); + uint32 GetSIRemaining(SIRemaining remaining) const; + 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(); + + 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 SummonPallid(Map* map, ScourgeInvasionData::CityAttack& zone, 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); + // tbc section void BuffMagtheridonTeam(Team team); void DispelMagtheridonTeam(Team team); @@ -462,6 +608,8 @@ class WorldState std::mutex m_loveIsInTheAirMutex; // capital cities optimization + ScourgeInvasionData m_siData; + // tbc section bool m_isMagtheridonHeadSpawnedHorde; bool m_isMagtheridonHeadSpawnedAlliance; diff --git a/src/game/World/WorldStateDefines.h b/src/game/World/WorldStateDefines.h index 97e094e11b..bdc2ab15e7 100644 --- a/src/game/World/WorldStateDefines.h +++ b/src/game/World/WorldStateDefines.h @@ -134,8 +134,16 @@ enum WorldStateID : int32 WORLD_STATE_AQ_SINGED_CORESTONE_TOTAL = 2110, WORLD_STATE_AQ_SINGED_CORESTONE_HORDE_NOW = 2109, - // Scourge Invasion - TODO + // Scourge Invasion WORLD_STATE_SCOURGE_BATTLES_WON = 2219, + // Zone icons + WORLD_STATE_SCOURGE_WINTERSPRING = 2259, + WORLD_STATE_SCOURGE_AZSHARA = 2260, + WORLD_STATE_SCOURGE_BLASTED_LANDS = 2261, + WORLD_STATE_SCOURGE_BURNING_STEPPES = 2262, + WORLD_STATE_SCOURGE_TANARIS = 2263, + WORLD_STATE_SCOURGE_EASTERN_PLAGUELANDS = 2264, + // Remaining WORLD_STATE_SCOURGE_NECROPOLIS_AZSHARA = 2279, WORLD_STATE_SCOURGE_NECROPOLIS_BLASTED_LANDS = 2280, WORLD_STATE_SCOURGE_NECROPOLIS_BURNING_STEPPES = 2281, diff --git a/src/shared/Util.h b/src/shared/Util.h index 657fb73b93..fe7ecc5297 100644 --- a/src/shared/Util.h +++ b/src/shared/Util.h @@ -128,6 +128,13 @@ struct Die uint32 chance[Sides]; }; +template +T PickRandomValue(T first, Args ...rest) +{ + T array[sizeof...(rest) + 1] = { first, rest... }; + return array[urand(0, (sizeof...(rest)))]; +} + inline void ApplyModUInt32Var(uint32& var, int32 val, bool apply) { int32 cur = var;