/* * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License as published by the * Free Software Foundation; either version 3 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ #include "AreaDefines.h" #include "CellImpl.h" #include "CombatAI.h" #include "CreatureScript.h" #include "Duration.h" #include "GameEventMgr.h" #include "GameObjectAI.h" #include "GameObjectScript.h" #include "GridNotifiersImpl.h" #include "Object.h" #include "ObjectDefines.h" #include "ScriptedCreature.h" #include "ScriptedGossip.h" #include "SpellInfo.h" #include "SpellScript.h" #include "SpellScriptLoader.h" #include "Unit.h" #include "Weather.h" #include "WeatherMgr.h" #include "WorldState.h" #include "scourge_invasion.h" #include class go_necropolis : public GameObjectAI { public: go_necropolis(GameObject* gameobject) : GameObjectAI(gameobject) { me->setActive(true); } }; struct npc_herald_of_the_lich_king : public ScriptedAI { npc_herald_of_the_lich_king(Creature* creature) : ScriptedAI(creature) { me->SetReactState(REACT_PASSIVE); } void InitializeAI() override { //m_go->SetVisibilityModifier(3000.0f); me->setActive(true); ScheduleTimedEvent(5s, [&]() { Talk(HERALD_OF_THE_LICH_KING_SAY_ATTACK_RANDOM); }, 150ms, 1h); } void UpdateWeather(bool startEvent) { if (Weather* weather = WeatherMgr::FindWeather(me->GetZoneId())) { if (startEvent) weather->SetWeather(WEATHER_TYPE_STORM, 0.25f); else weather->SetWeather(WEATHER_TYPE_RAIN, 0.0f); } else if (Weather* weather = WeatherMgr::AddWeather(me->GetZoneId())) { if (startEvent) weather->SetWeather(WEATHER_TYPE_STORM, 0.25f); else weather->SetWeather(WEATHER_TYPE_RAIN, 0.0f); } } void DoAction(int32 action) override { if (action == EVENT_HERALD_OF_THE_LICH_KING_ZONE_START) { Talk(HERALD_OF_THE_LICH_KING_SAY_ATTACK_START); ChangeZoneEventStatus(true); UpdateWeather(true); } else if (action == EVENT_HERALD_OF_THE_LICH_KING_ZONE_STOP) { Talk(HERALD_OF_THE_LICH_KING_SAY_ATTACK_END); ChangeZoneEventStatus(false); UpdateWeather(false); me->DespawnOrUnsummon(); } } void ChangeZoneEventStatus(bool startEvent) { switch (me->GetZoneId()) { case AREA_WINTERSPRING: if (startEvent) { 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 AREA_TANARIS: if (startEvent) { 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 AREA_AZSHARA: if (startEvent) { 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 AREA_BLASTED_LANDS: if (startEvent) { 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 AREA_EASTERN_PLAGUELANDS: if (startEvent) { 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 AREA_BURNING_STEPPES: if (startEvent) { 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; default: break; } sWorldState->Save(SAVE_ID_SCOURGE_INVASION); } void UpdateAI(uint32 diff) override { scheduler.Update(diff); } }; struct npc_necropolis : public ScriptedAI { npc_necropolis(Creature* creature) : ScriptedAI(creature) { me->setActive(true); } void SpellHit(Unit* /* caster */, SpellInfo const* spell) override { if (me->HasAura(SPELL_COMMUNIQUE_TIMER_NECROPOLIS)) return; if (spell->Id == SPELL_COMMUNIQUE_PROXY_TO_NECROPOLIS) DoCastSelf(SPELL_COMMUNIQUE_TIMER_NECROPOLIS, true); } }; struct npc_necropolis_health : public ScriptedAI { explicit npc_necropolis_health(Creature* creature) : ScriptedAI(creature) { me->SetFullHealth(); // RegenHealth is disabled } void SpellHit(Unit* /*caster*/, SpellInfo const* spell) override { if (spell->Id == SPELL_COMMUNIQUE_CAMP_TO_RELAY_DEATH) DoCastSelf(SPELL_ZAP_NECROPOLIS, true); // deals damage to self // Just to make sure it finally dies! if (spell->Id == SPELL_ZAP_NECROPOLIS) if (++_zapCount >= 3) me->KillSelf(); } void JustDied(Unit* /*killer*/) override { if (Creature* necropolis = GetClosestCreatureWithEntry(me, NPC_NECROPOLIS, ATTACK_DISTANCE)) me->CastSpell(necropolis, SPELL_DESPAWNER_OTHER, true); SIRemaining remainingID; switch (me->GetZoneId()) { case AREA_TANARIS: remainingID = SI_REMAINING_TANARIS; break; case AREA_BLASTED_LANDS: remainingID = SI_REMAINING_BLASTED_LANDS; break; case AREA_EASTERN_PLAGUELANDS: remainingID = SI_REMAINING_EASTERN_PLAGUELANDS; break; case AREA_BURNING_STEPPES: remainingID = SI_REMAINING_BURNING_STEPPES; break; case AREA_WINTERSPRING: remainingID = SI_REMAINING_WINTERSPRING; break; case AREA_AZSHARA: remainingID = SI_REMAINING_AZSHARA; break; default: return; } uint32 remaining = sWorldState->GetSIRemaining(remainingID); if (remaining > 0) sWorldState->SetSIRemaining(remainingID, (remaining - 1)); } void SpellHitTarget(Unit* target, SpellInfo const* spellInfo) override { // Make sure necropoli despawn after SPELL_DESPAWNER_OTHER is triggered. if (spellInfo->Id == SPELL_DESPAWNER_OTHER && target->GetEntry() == NPC_NECROPOLIS) { DespawnNecropolis(); dynamic_cast(target)->DespawnOrUnsummon(); me->DespawnOrUnsummon(); } } void DespawnNecropolis() { std::list necropolisList; me->GetGameObjectListWithEntryInGrid( necropolisList, { GO_NECROPOLIS_TINY, GO_NECROPOLIS_SMALL, GO_NECROPOLIS_MEDIUM, GO_NECROPOLIS_BIG, GO_NECROPOLIS_HUGE }, ATTACK_DISTANCE ); for (GameObject* const& necropolis : necropolisList) necropolis->DespawnOrUnsummon(); } private: int _zapCount = 0; // 3 = death. }; struct npc_necropolis_proxy : public ScriptedAI { npc_necropolis_proxy(Creature* creature) : ScriptedAI(creature) { me->setActive(true); } void SpellHit(Unit* /*caster*/, SpellInfo const* spellInfo) override { switch (spellInfo->Id) { case SPELL_COMMUNIQUE_NECROPOLIS_TO_PROXIES: DoCastSelf(SPELL_COMMUNIQUE_PROXY_TO_RELAY, true); break; case SPELL_COMMUNIQUE_RELAY_TO_PROXY: DoCastSelf(SPELL_COMMUNIQUE_PROXY_TO_NECROPOLIS, true); break; case SPELL_COMMUNIQUE_CAMP_TO_RELAY_DEATH: if (Creature* health = GetClosestCreatureWithEntry(me, NPC_NECROPOLIS_HEALTH, 200.0f)) me->CastSpell(health, SPELL_COMMUNIQUE_CAMP_TO_RELAY_DEATH, true); break; default: break; } } void SpellHitTarget(Unit* /*target*/, SpellInfo const* spellInfo) override { // Make sure me 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) me->DespawnOrUnsummon(); } }; struct npc_necropolis_relay : public ScriptedAI { npc_necropolis_relay(Creature* creature) : ScriptedAI(creature) { me->setActive(true); } void SpellHit(Unit* /*caster*/, SpellInfo const* spell) override { switch (spell->Id) { case SPELL_COMMUNIQUE_PROXY_TO_RELAY: DoCastSelf(SPELL_COMMUNIQUE_RELAY_TO_CAMP, true); break; case SPELL_COMMUNIQUE_CAMP_TO_RELAY: DoCastSelf(SPELL_COMMUNIQUE_RELAY_TO_PROXY, true); break; case SPELL_COMMUNIQUE_CAMP_TO_RELAY_DEATH: if (Creature* proxy = GetClosestCreatureWithEntry(me, NPC_NECROPOLIS_PROXY, 200.0f)) me->CastSpell(proxy, SPELL_COMMUNIQUE_CAMP_TO_RELAY_DEATH, true); break; default: break; } } void SpellHitTarget(Unit* /*target*/, SpellInfo const* spell) override { // Make sure `me` despawns 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) me->DespawnOrUnsummon(); } }; struct npc_necrotic_shard : public ScriptedAI { npc_necrotic_shard(Creature* creature) : ScriptedAI(creature) { me->setActive(true); me->SetReactState(REACT_PASSIVE); // No healing possible. me->ApplySpellImmune(0, IMMUNITY_EFFECT, SPELL_EFFECT_HEAL, true); me->ApplySpellImmune(0, IMMUNITY_EFFECT, SPELL_EFFECT_HEAL_PCT, true); me->ApplySpellImmune(0, IMMUNITY_EFFECT, SPELL_EFFECT_HEAL_MAX_HEALTH, true); me->ApplySpellImmune(0, IMMUNITY_STATE, SPELL_AURA_PERIODIC_HEAL, true); } void ScheduleMinionSpawnTask() { scheduler.Schedule(5s, [this](TaskContext context) // Spawn Minions every 5 seconds. { HandleShardMinionSpawnerSmall(); context.Repeat(5s); }); } // This is a placeholder for SPELL_MINION_SPAWNER_BUTTRESS [27888] which also activates unknown, not sniffable gameobjects // and happens every hour if a Damaged Necrotic Shard is active. The Cultists despawning after 1 hour, // so this just resets everything and spawn them again and Refill the Health of the Shard. void ScheduleCultistSpawnTask() { scheduler.Schedule(5s, [this](TaskContext context) // Spawn Cultists every 60 minutes. { DespawnShadowsOfDoom(); // Despawn all remaining Shadows before respawning the Cultists? SummonCultists(); context.Repeat(1h); }); } void ScheduleTasks() { if (me->GetEntry() == NPC_NECROTIC_SHARD) { // Just in case. std::list shardList; me->GetCreatureListWithEntryInGrid( shardList, { NPC_NECROTIC_SHARD, NPC_DAMAGED_NECROTIC_SHARD }, CONTACT_DISTANCE ); for (Creature* shard : shardList) if (shard != me) shard->DespawnOnEvade(); scheduler.Schedule(10s, [this](const TaskContext& /*context*/) // Check if Doodads are spawned 5 seconds after spawn. If not: spawn them { std::list objectList; me->GetGameObjectListWithEntryInGrid( objectList, {GO_UNDEAD_FIRE, GO_UNDEAD_FIRE_AURA, GO_SKULLPILE_01, GO_SKULLPILE_02, GO_SKULLPILE_03, GO_SKULLPILE_04}, 50.0f ); for (GameObject* go : objectList) if (go && !go->isSpawned()) { go->SetRespawnTime(0); go->Respawn(); } }); } else if (me->GetEntry() == NPC_DAMAGED_NECROTIC_SHARD) { UpdateFindersAmount(); ScheduleMinionSpawnTask(); ScheduleCultistSpawnTask(); } ScheduleTimedEvent(25s, [&]() -> void // Check if there are a summoning circle, otherwise despawn { if (!GetClosestGameObjectWithEntry(me, GO_SUMMON_CIRCLE, 2.0f)) { DespawnEventDoodads(); me->DespawnOrUnsummon(); } }, 60s); } void Reset() override { scheduler.CancelAll(); ScheduleTasks(); } bool HasCampTypeAura() { return me->HasAnyAuras(SPELL_CAMP_TYPE_GHOST_SKELETON, SPELL_CAMP_TYPE_GHOST_GHOUL, SPELL_CAMP_TYPE_GHOUL_SKELETON); }; void SpellHit(Unit* caster, SpellInfo const* spell) override { switch (spell->Id) { case SPELL_ZAP_CRYSTAL_CORPSE: { Creature::DealDamage(me, me, (me->GetMaxHealth() / 4), nullptr, DIRECT_DAMAGE, SPELL_SCHOOL_MASK_NORMAL, nullptr, false); _zapCount++; if (_zapCount >= 4) me->KillSelf(); break; } case SPELL_COMMUNIQUE_RELAY_TO_CAMP: { me->CastSpell(static_cast(nullptr), SPELL_CAMP_RECEIVES_COMMUNIQUE, true); break; } case SPELL_CHOOSE_CAMP_TYPE: { _spellCampType = RAND(SPELL_CAMP_TYPE_GHOUL_SKELETON, SPELL_CAMP_TYPE_GHOST_GHOUL, SPELL_CAMP_TYPE_GHOST_SKELETON); DoCastSelf(_spellCampType, true); break; } case SPELL_CAMP_RECEIVES_COMMUNIQUE: { if (!HasCampTypeAura() && me->GetEntry() == NPC_NECROTIC_SHARD) { UpdateFindersAmount(); DoCastSelf(SPELL_CHOOSE_CAMP_TYPE, true); ScheduleMinionSpawnTask(); } break; } case SPELL_FIND_CAMP_TYPE: { // Don't spawn more Minions than finders. if (_nearbyFinderCount < HasMinion(me, 60.0f)) return; static constexpr std::pair auraSpellMap[] = { {SPELL_CAMP_TYPE_GHOST_SKELETON, SPELL_PH_SUMMON_MINION_TRAP_GHOST_SKELETON}, {SPELL_CAMP_TYPE_GHOST_GHOUL, SPELL_PH_SUMMON_MINION_TRAP_GHOST_GHOUL }, {SPELL_CAMP_TYPE_GHOUL_SKELETON, SPELL_PH_SUMMON_MINION_TRAP_GHOUL_SKELETON} }; for (auto const& [aura, spell] : auraSpellMap) if (me->HasAura(aura)) { caster->CastSpell(caster, spell, true); break; } break; } default: break; } } void SpellHitTarget(Unit* /*target*/, SpellInfo const* spellInfo) override { if (me->GetEntry() != NPC_DAMAGED_NECROTIC_SHARD) return; if (spellInfo->Id == SPELL_COMMUNIQUE_CAMP_TO_RELAY_DEATH) me->DespawnOrUnsummon(); } // Only Minions and the shard itself can deal damage. void DamageTaken(Unit* attacker, uint32& damage, DamageEffectType /*damageType*/, SpellSchoolMask /*damageSchoolMask*/) override { if (attacker && attacker->GetFactionTemplateEntry() != me->GetFactionTemplateEntry()) damage = 0; } void JustDied(Unit* /*killer*/) override { switch (me->GetEntry()) { case NPC_NECROTIC_SHARD: if (Creature* shard = me->SummonCreature(NPC_DAMAGED_NECROTIC_SHARD, me->GetPosition(), TEMPSUMMON_MANUAL_DESPAWN)) { uint32 spellId = _spellCampType ? _spellCampType : static_cast(SPELL_CHOOSE_CAMP_TYPE); shard->CastSpell(shard, spellId, true); me->DespawnOrUnsummon(); } break; case NPC_DAMAGED_NECROTIC_SHARD: // Buff Players. DoCastSelf(SPELL_SOUL_REVIVAL, true); // Sending the Death Bolt. if (Creature* relay = GetClosestCreatureWithEntry(me, NPC_NECROPOLIS_RELAY, 200.0f)) me->CastSpell(relay, SPELL_COMMUNIQUE_CAMP_TO_RELAY_DEATH, true); DespawnCultists(); // Despawn remaining Cultists (should never happen). DespawnEventDoodads(); sWorldState->Save(SAVE_ID_SCOURGE_INVASION); break; default: break; } } // 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. void HandleShardMinionSpawnerSmall() { uint32 spawnLimit = urand(1, 3); // Up to 3 spawns. uint32 spawned = 0; std::list finderList; GetCreatureListWithEntryInGrid(finderList, me, 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(Acore::ObjectDistanceOrderPred(me)); for (Creature* const& finder : finderList) { // Stop summoning Minions if we reached the max spawn amount. if (spawned == spawnLimit) break; // Skip dead finders. if (!finder->IsAlive()) continue; // Don't take finders with Minions. if (HasMinion(finder)) 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 (finder->CastSpell(me, SPELL_FIND_CAMP_TYPE, true) == SPELL_CAST_OK) { // Values are from Sniffs (rounded). Shortest and Longest respawn time from a finder on the same spot. finder->DespawnOrUnsummon(0s, randtime(150s, 200s)); // Delayed despawn to prevent stacked spawns spawned++; } } } // Respawn the Cultists. void SummonCultists() { std::list summonerShieldList; me->GetGameObjectListWithEntryInGrid(summonerShieldList, GO_SUMMONER_SHIELD, INSPECT_DISTANCE); for (GameObject* const& summonerShield : summonerShieldList) summonerShield->DespawnOrUnsummon(); // 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 B's positions are sometimes way off? if (GameObject* go = GetClosestGameObjectWithEntry(me, GO_SUMMON_CIRCLE, CONTACT_DISTANCE)) { for (int i = 0; i < 4; ++i) { float angle = (float(i) * (M_PI / 2.f)) + go->GetOrientation(); float x = go->GetPositionX() + 6.95f * std::cos(angle); float y = go->GetPositionY() + 6.75f * std::sin(angle); float z = go->GetPositionZ() + 5.0f; me->UpdateGroundPositionZ(x, y, z); me->SummonCreature(NPC_CULTIST_ENGINEER, x, y, z, angle - M_PI, TEMPSUMMON_TIMED_OR_DEAD_DESPAWN, IN_MILLISECONDS * HOUR); } } } static uint32 HasMinion(Unit* searcher, float searchDistance = ATTACK_DISTANCE) { uint32 minionCounter = 0; std::list minionList; searcher->GetCreatureListWithEntryInGrid( minionList, { NPC_SKELETAL_SHOCKTROOPER, NPC_GHOUL_BERSERKER, NPC_SPECTRAL_SOLDIER, NPC_LUMBERING_HORROR, NPC_BONE_WITCH, NPC_SPIRIT_OF_THE_DAMNED }, searchDistance ); for (Creature const* minion : minionList) if (minion && minion->IsAlive()) minionCounter++; return minionCounter; } // Count all finders to limit Minions spawns. void UpdateFindersAmount() { _nearbyFinderCount = 0; std::list finderList; me->GetCreatureListWithEntryInGrid(finderList, NPC_SCOURGE_INVASION_MINION_FINDER, 60.0f); for (Creature const* finder : finderList) if (finder) _nearbyFinderCount++; } void DespawnCultists() { std::list cultistList; me->GetCreatureListWithEntryInGrid(cultistList, NPC_CULTIST_ENGINEER, INSPECT_DISTANCE); for (Creature* cultist : cultistList) if (cultist) cultist->DespawnOrUnsummon(); } void DespawnShadowsOfDoom() { std::list shadowList; me->GetCreatureListWithEntryInGrid(shadowList, NPC_SHADOW_OF_DOOM, 200.0f); for (Creature* shadow : shadowList) if (shadow && shadow->IsAlive() && !shadow->IsInCombat()) shadow->DespawnOrUnsummon(); } // Remove Objects from the event around the Shard (Yes this is Blizzlike). void DespawnEventDoodads() { std::list doodadList; me->GetGameObjectListWithEntryInGrid( doodadList, { GO_SUMMON_CIRCLE, GO_UNDEAD_FIRE, GO_UNDEAD_FIRE_AURA, GO_SKULLPILE_01, GO_SKULLPILE_02, GO_SKULLPILE_03, GO_SKULLPILE_04, GO_SUMMONER_SHIELD }, 60.0f ); for (GameObject* const& doodad : doodadList) { doodad->SetRespawnDelay(-1); doodad->DespawnOrUnsummon(); } std::list finderList; me->GetCreatureListWithEntryInGrid( finderList, NPC_SCOURGE_INVASION_MINION_FINDER, 60.0f ); for (Creature* const& finder : finderList) finder->DespawnOrUnsummon(); } void UpdateAI(uint32 diff) override { scheduler.Update(diff); } private: uint32 _spellCampType = 0; uint32 _nearbyFinderCount = 0; uint8 _zapCount = 0; // 4 = death. }; /* Minion Spawner */ struct npc_minion_spawner : public ScriptedAI { npc_minion_spawner(Creature* creature) : ScriptedAI(creature) { me->SetReactState(REACT_PASSIVE); } void JustSummoned(Creature* summon) override { me->SetRespawnTime(0); me->SetCorpseDelay(0); summon->SetWanderDistance(1.0f); DoCastAOE(SPELL_MINION_SPAWN_IN); } void Reset() override { scheduler.Schedule(5s, [this](TaskContext const& /*context*/) // Spawn Minions every 5 seconds. { uint32 entry; switch (me->GetEntry()) { case NPC_SCOURGE_INVASION_MINION_SPAWNER_GHOST_GHOUL: entry = CanSpawnRareMinion() ? RAND(NPC_SPIRIT_OF_THE_DAMNED, NPC_LUMBERING_HORROR) : RAND(NPC_SPECTRAL_SOLDIER, NPC_GHOUL_BERSERKER); break; case NPC_SCOURGE_INVASION_MINION_SPAWNER_GHOST_SKELETON: entry = CanSpawnRareMinion() ? RAND(NPC_SPIRIT_OF_THE_DAMNED, NPC_BONE_WITCH) : RAND(NPC_SPECTRAL_SOLDIER, NPC_SKELETAL_SHOCKTROOPER); break; case NPC_SCOURGE_INVASION_MINION_SPAWNER_GHOUL_SKELETON: entry = CanSpawnRareMinion() ? RAND(NPC_LUMBERING_HORROR, NPC_BONE_WITCH) : RAND(NPC_GHOUL_BERSERKER, NPC_SKELETAL_SHOCKTROOPER); break; default: entry = NPC_GHOUL_BERSERKER; // just in case. break; } if (Creature* minion = me->SummonCreature(entry, me->GetPosition(), TEMPSUMMON_TIMED_OR_DEAD_DESPAWN, IN_MILLISECONDS * HOUR)) { minion->SetWanderDistance(1.0f); DoCastAOE(SPELL_MINION_SPAWN_IN); } }); } bool CanSpawnRareMinion() { std::list uncommonMinionList; me->GetCreatureListWithEntryInGrid( uncommonMinionList, { NPC_LUMBERING_HORROR, NPC_BONE_WITCH, NPC_SPIRIT_OF_THE_DAMNED }, 100.0f ); for (Creature const* minion : uncommonMinionList) if (minion) 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; } void UpdateAI(uint32 diff) override { scheduler.Update(diff); } }; struct npc_cultist_engineer : public ScriptedAI { npc_cultist_engineer(Creature* creature) : ScriptedAI(creature) { } void sGossipSelect(Player* player, uint32 /*sender*/, uint32 /*action*/) override { CloseGossipMenuFor(player); player->DestroyItemCount(ITEM_NECROTIC_RUNE, 8, true); player->CastSpell(static_cast(nullptr), SPELL_SUMMON_BOSS, true); // Player summons a Shadow of Doom for 1 hour. DoCastSelf(SPELL_QUIET_SUICIDE, true); } void Reset() override { scheduler.CancelAll(); me->SetReactState(REACT_PASSIVE); me->SetCorpseDelay(10); // Corpse despawns 10 seconds after a Shadow of Doom spawns. scheduler.Schedule(0s, [this](TaskContext const& /*context*/) { DoCastSelf(SPELL_CREATE_SUMMONER_SHIELD, true); DoCastSelf(SPELL_MINION_SPAWN_IN, true); }).Schedule(1s, [this](TaskContext const& /*context*/) { DoCastSelf(SPELL_BUTTRESS_CHANNEL, true); }); } void JustDied(Unit*) override { scheduler.CancelAll(); if (Creature* shard = GetClosestCreatureWithEntry(me, NPC_DAMAGED_NECROTIC_SHARD, 15.0f)) shard->CastSpell(shard, SPELL_DAMAGE_CRYSTAL, true); if (GameObject* gameObject = GetClosestGameObjectWithEntry(me, GO_SUMMONER_SHIELD, CONTACT_DISTANCE)) gameObject->Delete(); } void UpdateAI(uint32 diff) override { scheduler.Update(diff); } }; struct npc_flameshocker : public CombatAI { npc_flameshocker(Creature* creature) : CombatAI(creature) { } void Reset() override { scheduler.CancelAll(); scheduler.Schedule(2s, [this](TaskContext context) { DoCastVictim(RAND(SPELL_FLAMESHOCKERS_TOUCH, SPELL_FLAMESHOCKERS_TOUCH2), true); context.Repeat(30s, 45s); }); } void JustDied(Unit* /*killer*/) override { DoCastSelf(SPELL_FLAMESHOCKERS_REVENGE, true); } void UpdateAI(uint32 const diff) override { scheduler.Update(diff); if (!UpdateVictim()) return; DoMeleeAttackIfReady(); } }; struct npc_pallid_horror : public ScriptedAI { npc_pallid_horror(Creature* creature) : ScriptedAI(creature), _summons(me) { } void InitializeAI() override { _summons.DespawnAll(); me->SetCorpseDelay(10); // Corpse despawns 10 seconds after a crystal spawns. UpdateWeather(true); ScheduleTasks(); me->AddAura(SPELL_AURA_OF_FEAR, me); me->SetWalk(false); } void ScheduleTasks() { scheduler.Schedule(0s, [this](const TaskContext& /*context*/) { SummonFlameshockers(); }).Schedule(1s, [this](TaskContext context) { Talk(PALLID_HORROR_SAY_RANDOM_YELL); context.Repeat(65s, 300s); }).Schedule(11s, 81s, [this](TaskContext context) { DoCastVictim(SPELL_DAMAGE_VS_GUARDS, true); context.Repeat(); }).Schedule(2s, [this](TaskContext context) { if (_summons.size() >= 30) { context.Repeat(10s); return; } std::list targets; FlameshockerCheck check; Acore::CreatureListSearcher searcher(me, targets, check); Cell::VisitObjects(me, searcher, VISIBILITY_DISTANCE_NORMAL); if (!targets.empty()) { Creature* target = Acore::Containers::SelectRandomContainerElement(targets); float x, y, z; target->GetNearPoint(target, x, y, z, 5.0f, 5.0f, 0.0f); if (Creature* creature = me->SummonCreature(NPC_FLAMESHOCKER, x, y, z, target->GetOrientation(), TEMPSUMMON_TIMED_DESPAWN_OUT_OF_COMBAT, 5 * IN_MILLISECONDS)) _summons.Summon(creature); } context.Repeat(2s); }); } void SummonFlameshockers() { uint32 const amount = urand(5, 9); // sniffed are group sizes of 5-9 shockers on spawn. for (uint32 i = 0; i < amount; ++i) { if (Creature* summon = me->SummonCreature( NPC_FLAMESHOCKER, me->GetPositionX(), me->GetPositionY(), me->GetPositionZ(), 0.0f, TEMPSUMMON_TIMED_OR_CORPSE_DESPAWN, HOUR * IN_MILLISECONDS)) { float angle = static_cast(i) * (M_PI / (static_cast(amount) / 2.f)) + me->GetOrientation(); summon->GetMotionMaster()->Clear(true); summon->GetMotionMaster()->MoveFollow(me, 2.5f, angle); _summons.Summon(summon); } } } void JustSummoned(Creature* summon) override { summon->CastSpell(summon, SPELL_MINION_SPAWN_IN, true); summon->SetWalk(false); } void JustDied(Unit* /*unit*/) override { // wrong Varian: missing special event Varian NPC that uses the old model // if (Creature* creature = GetClosestCreatureWithEntry(me, NPC_VARIAN, VISIBILITY_DISTANCE_NORMAL)) // creature->Say(1); if (Creature* creature = GetClosestCreatureWithEntry(me, NPC_LADY_SYLVANAS_WINDRUNNER, VISIBILITY_DISTANCE_NORMAL)) creature->Say(SYLVANAS_SAY_ATTACK_END); // Kill all custom summoned Flameshockers. _summons.DoForAllSummons([](WorldObject* summon) { if (Creature* creature = summon->ToCreature()) creature->KillSelf(); }); // Spawn necrotic crystal gobject DoCastSelf((me->GetZoneId() == AREA_UNDERCITY ? SPELL_SUMMON_FAINT_NECROTIC_CRYSTAL : SPELL_SUMMON_CRACKED_NECROTIC_CRYSTAL), true); TimePoint now = std::chrono::steady_clock::now(); uint32 cityAttackTimer = urand(CITY_ATTACK_TIMER_MIN, CITY_ATTACK_TIMER_MAX); TimePoint nextAttack = now + std::chrono::seconds(cityAttackTimer); uint64 timeToNextAttack = std::chrono::duration_cast(nextAttack - now).count(); SITimers index = me->GetZoneId() == AREA_UNDERCITY ? SI_TIMER_UNDERCITY : SI_TIMER_STORMWIND; sWorldState->SetSITimer(index, nextAttack); sWorldState->SetPallidGuid(index, ObjectGuid()); UpdateWeather(false); LOG_INFO("gameevent", "[Scourge Invasion Event] The Scourge has been defeated in {}, next attack starting in {} minutes", me->GetZoneId() == AREA_UNDERCITY ? "Undercity" : "Stormwind", timeToNextAttack); } void CorpseRemoved(uint32& /*respawnDelay*/) override { // Remove all custom summoned Flameshockers. _summons.DespawnAll(); } void UpdateAI(uint32 const diff) override { scheduler.Update(diff); if (!UpdateVictim()) return; DoMeleeAttackIfReady(); } void UpdateWeather(bool startEvent) { if (Weather* weather = WeatherMgr::FindWeather(me->GetZoneId())) { if (startEvent) weather->SetWeather(WEATHER_TYPE_STORM, 0.25f); else weather->SetWeather(WEATHER_TYPE_RAIN, 0.0f); } else if (Weather* weather = WeatherMgr::AddWeather(me->GetZoneId())) { if (startEvent) weather->SetWeather(WEATHER_TYPE_STORM, 0.25f); else weather->SetWeather(WEATHER_TYPE_RAIN, 0.0f); } } private: struct FlameshockerCheck { bool operator()(Creature* creature) { return !creature->IsCivilian() && creature->GetEntry() != NPC_FLAMESHOCKER; } }; SummonList _summons; }; // 28091 - Despawner, self (server-side) class spell_despawner_self : public SpellScript { PrepareSpellScript(spell_despawner_self); bool Validate(SpellInfo const* /*spell*/) override { return ValidateSpellInfo({SPELL_SPIRIT_SPAWN_OUT}); } void HandleDummy(SpellEffIndex /*effIndex*/) { if (Unit* caster = GetCaster()) if (!caster->IsInCombat()) caster->CastSpell(caster, SPELL_SPIRIT_SPAWN_OUT, true); } void Register() override { OnEffectHitTarget += SpellEffectFn(spell_despawner_self::HandleDummy, EFFECT_0, SPELL_EFFECT_DUMMY); } }; // 28345 - Communique Trigger (server-side) class spell_communique_trigger : public SpellScript { PrepareSpellScript(spell_communique_trigger); bool Validate(SpellInfo const* /*spell*/) override { return ValidateSpellInfo({SPELL_COMMUNIQUE_CAMP_TO_RELAY}); } void HandleDummy(SpellEffIndex /*effIndex*/) { if (Unit* target = GetHitUnit()) target->CastSpell(static_cast(nullptr), SPELL_COMMUNIQUE_CAMP_TO_RELAY, true); } void Register() override { OnEffectHitTarget += SpellEffectFn(spell_communique_trigger::HandleDummy, EFFECT_0, SPELL_EFFECT_DUMMY); } }; // 28265 - Scourge Strike class spell_scourge_invasion_scourge_strike : public SpellScript { PrepareSpellScript(spell_scourge_invasion_scourge_strike); SpellCastResult CheckCast() { Unit* target = GetExplTargetUnit(); if (!target || target->IsPlayer() || target->IsCharmedOwnedByPlayerOrPlayer()) return SPELL_FAILED_BAD_TARGETS; return SPELL_CAST_OK; } void Register() override { OnCheckCast += SpellCheckCastFn(spell_scourge_invasion_scourge_strike::CheckCast); } }; void AddSC_scourge_invasion() { RegisterGameObjectAI(go_necropolis); RegisterCreatureAI(npc_herald_of_the_lich_king); RegisterCreatureAI(npc_necropolis); RegisterCreatureAI(npc_necropolis_health); RegisterCreatureAI(npc_necropolis_proxy); RegisterCreatureAI(npc_necropolis_relay); RegisterCreatureAI(npc_necrotic_shard); RegisterCreatureAI(npc_minion_spawner); RegisterCreatureAI(npc_pallid_horror); RegisterCreatureAI(npc_cultist_engineer); RegisterCreatureAI(npc_flameshocker); RegisterSpellScript(spell_communique_trigger); RegisterSpellScript(spell_despawner_self); RegisterSpellScript(spell_scourge_invasion_scourge_strike); }