diff options
| author | treeston <treeston.mmoc@gmail.com> | 2016-08-24 18:17:50 +0200 |
|---|---|---|
| committer | joschiwald <joschiwald.trinity@gmail.com> | 2017-02-18 15:57:19 +0100 |
| commit | df0d32be819ab4af7af400dabcc8d009b30a336e (patch) | |
| tree | 96a71e426112121e009f255f29da5a5feea0ca77 /src | |
| parent | 41ed592d05521f04ed459de86afb72572ad3207e (diff) | |
Merge remote-tracking branch 'Treeston/3.3.5-kelthuzad' into 3.3.5 (PR #16634)
(cherry picked from commit 47a9cb1e562b1132df2b7b14e01d9e4f01e85717)
...Huh. Wonder how this one didn't make travis complain on the PR.
(cherry picked from commit 93a68d58814013a1c656bf17afcc94938457adfd)
Was Travis completely napping on this one?
(cherry picked from commit 2d7dec0d39765dbaad3d3f4c9e203070bcd772d5)
Diffstat (limited to 'src')
| -rw-r--r-- | src/server/game/AI/PlayerAI/PlayerAI.cpp | 8 | ||||
| -rw-r--r-- | src/server/game/AI/PlayerAI/PlayerAI.h | 6 | ||||
| -rw-r--r-- | src/server/scripts/Northrend/Naxxramas/boss_kelthuzad.cpp | 1385 | ||||
| -rw-r--r-- | src/server/scripts/Northrend/Naxxramas/instance_naxxramas.cpp | 19 | ||||
| -rw-r--r-- | src/server/scripts/Northrend/Naxxramas/naxxramas.h | 11 |
5 files changed, 813 insertions, 616 deletions
diff --git a/src/server/game/AI/PlayerAI/PlayerAI.cpp b/src/server/game/AI/PlayerAI/PlayerAI.cpp index ec7df34613f..2a660fc31d7 100644 --- a/src/server/game/AI/PlayerAI/PlayerAI.cpp +++ b/src/server/game/AI/PlayerAI/PlayerAI.cpp @@ -696,7 +696,7 @@ PlayerAI::TargetedSpell SimpleCharmedPlayerAI::SelectAppropriateCastForSpec() VerifyAndPushSpellCast(spells, SPELL_HAMMER_OF_JUSTICE, TARGET_VICTIM, 6); VerifyAndPushSpellCast(spells, SPELL_HAND_OF_FREEDOM, TARGET_SELF, 3); VerifyAndPushSpellCast(spells, SPELL_HAND_OF_PROTECTION, TARGET_SELF, 1); - if (Creature* creatureCharmer = ObjectAccessor::GetCreature(*me, me->GetCharmerGUID())) + if (Creature* creatureCharmer = GetCharmer()) { if (creatureCharmer->IsDungeonBoss() || creatureCharmer->isWorldBoss()) VerifyAndPushSpellCast(spells, SPELL_HAND_OF_SACRIFICE, creatureCharmer, 10); @@ -881,7 +881,7 @@ PlayerAI::TargetedSpell SimpleCharmedPlayerAI::SelectAppropriateCastForSpec() case TALENT_SPEC_DEATHKNIGHT_BLOOD: VerifyAndPushSpellCast(spells, SPELL_RUNE_TAP, TARGET_NONE, 2); VerifyAndPushSpellCast(spells, SPELL_HYSTERIA, TARGET_SELF, 5); - if (Creature* creatureCharmer = ObjectAccessor::GetCreature(*me, me->GetCharmerGUID())) + if (Creature* creatureCharmer = GetCharmer()) if (!creatureCharmer->IsDungeonBoss() && !creatureCharmer->isWorldBoss()) VerifyAndPushSpellCast(spells, SPELL_HYSTERIA, creatureCharmer, 15); VerifyAndPushSpellCast(spells, SPELL_HEART_STRIKE, TARGET_VICTIM, 2); @@ -1068,7 +1068,7 @@ PlayerAI::TargetedSpell SimpleCharmedPlayerAI::SelectAppropriateCastForSpec() } VerifyAndPushSpellCast(spells, SPELL_TRANQUILITY, TARGET_NONE, 10); VerifyAndPushSpellCast(spells, SPELL_NATURE_SWIFTNESS, TARGET_NONE, 7); - if (Creature* creatureCharmer = ObjectAccessor::GetCreature(*me, me->GetCharmerGUID())) + if (Creature* creatureCharmer = GetCharmer()) { VerifyAndPushSpellCast(spells, SPELL_NOURISH, creatureCharmer, 5); VerifyAndPushSpellCast(spells, SPELL_WILD_GROWTH, creatureCharmer, 5); @@ -1157,7 +1157,7 @@ PlayerAI::TargetedSpell SimpleCharmedPlayerAI::SelectAppropriateCastForSpec() static const float CASTER_CHASE_DISTANCE = 28.0f; void SimpleCharmedPlayerAI::UpdateAI(const uint32 diff) { - Creature* charmer = me->GetCharmer() ? me->GetCharmer()->ToCreature() : nullptr; + Creature* charmer = GetCharmer(); if (!charmer) return; diff --git a/src/server/game/AI/PlayerAI/PlayerAI.h b/src/server/game/AI/PlayerAI/PlayerAI.h index b06f81f76c9..78c8c3c27b5 100644 --- a/src/server/game/AI/PlayerAI/PlayerAI.h +++ b/src/server/game/AI/PlayerAI/PlayerAI.h @@ -30,6 +30,12 @@ class TC_GAME_API PlayerAI : public UnitAI void OnCharmed(bool /*apply*/) override { } // charm AI application for players is handled by Unit::SetCharmedBy / Unit::RemoveCharmedBy + Creature* GetCharmer() const + { + if (me->GetCharmerGUID().IsCreature()) + return ObjectAccessor::GetCreature(*me, me->GetCharmerGUID()); + return nullptr; + } // helper functions to determine player info uint16 GetSpec(Player const* who = nullptr) const { return (!who || who == me) ? _selfSpec : who->GetUInt32Value(PLAYER_FIELD_CURRENT_SPEC_ID); } static bool IsPlayerHealer(Player const* who); diff --git a/src/server/scripts/Northrend/Naxxramas/boss_kelthuzad.cpp b/src/server/scripts/Northrend/Naxxramas/boss_kelthuzad.cpp index 9dbb3dc2f4c..4182772740c 100644 --- a/src/server/scripts/Northrend/Naxxramas/boss_kelthuzad.cpp +++ b/src/server/scripts/Northrend/Naxxramas/boss_kelthuzad.cpp @@ -16,27 +16,19 @@ * with this program. If not, see <http://www.gnu.org/licenses/>. */ -/* ScriptData -SDName: Boss_KelThuzad -SD%Complete: 80% -SDComment: VERIFY SCRIPT -SDCategory: Naxxramas -EndScriptData */ - #include "ScriptMgr.h" #include "ScriptedCreature.h" #include "SpellScript.h" #include "SpellAuraEffects.h" #include "naxxramas.h" -#include "Player.h" +#include "PlayerAI.h" enum Texts { SAY_AGGRO = 7, SAY_SLAY = 8, SAY_DEATH = 9, - SAY_CHAIN = 10, - SAY_FROST_BLAST = 11, + SAY_CHAINS = 10, SAY_REQUEST_AID = 12, //start of phase 3 EMOTE_PHASE_TWO = 13, SAY_SUMMON_MINIONS = 14, //start of phase 1 @@ -45,219 +37,165 @@ enum Texts // The Lich King SAY_ANSWER_REQUEST = 3, - // Old World Trigger - SAY_GUARDIAN_SPAWNED = 0 + // Guardian of Icecrown + EMOTE_GUARDIAN_FLEE = 0, + EMOTE_GUARDIAN_APPEAR = 1 }; enum Events { - EVENT_NONE, - EVENT_BOLT, - EVENT_NOVA, - EVENT_CHAIN, - EVENT_CHAINED_SPELL, - EVENT_DETONATE, - EVENT_FISSURE, - EVENT_BLAST, - - EVENT_WASTE, - EVENT_ABOMIN, - EVENT_WEAVER, - EVENT_ICECROWN, - EVENT_TRIGGER, - - EVENT_PHASE, - EVENT_MORTAL_WOUND, - - EVENT_ANSWER_REQUEST, - EVENT_SUMMON_GUARDIANS + // phase one + EVENT_SKELETON = 1, + EVENT_BANSHEE, + EVENT_ABOMINATION, + EVENT_DESPAWN_MINIONS, + EVENT_PHASE_TWO, + + // phase two + EVENT_FROSTBOLT_VOLLEY, + EVENT_SHADOW_FISSURE, + EVENT_DETONATE_MANA, + EVENT_FROST_BLAST, + EVENT_CHAINS, + + // phase three transition + EVENT_TRANSITION_REPLY, + EVENT_TRANSITION_SUMMON, }; -enum Spells +enum Actions { - SPELL_FROST_BOLT = 28478, - SPELL_FROST_BOLT_AOE = 28479, - SPELL_SHADOW_FISURE = 27810, - SPELL_VOID_BLAST = 27812, - SPELL_MANA_DETONATION = 27819, - SPELL_MANA_DETONATION_DAMAGE = 27820, - SPELL_FROST_BLAST = 27808, - SPELL_CHAINS_OF_KELTHUZAD = 28410, //28408 script effect - SPELL_KELTHUZAD_CHANNEL = 29423, - SPELL_BERSERK = 28498, - - //spells for chained - //warlock - SPELL_CURSE_OF_AGONY = 47864, - SPELL_SHADOW_BOLT = 47809, - //shaman - SPELL_EARTH_SHOCK = 49231, - SPELL_HEALING_WAVE = 49273, - //mage - SPELL_FROST_FIREBOLT = 47610, - SPELL_ARCANE_MISSILES = 42846, - //rogue - SPELL_HEMORRHAGE = 48660, - SPELL_MUTILATE = 48666, - //paladin - SPELL_HOLY_SHOCK = 48825, - SPELL_HAMMER_OF_JUSTICE = 10308, - //priest - SPELL_VAMPIRIC_TOUCH = 48160, - SPELL_RENEW = 48068, - //hunter - SPELL_MULTI_SHOT = 49048, - SPELL_VOLLEY = 58434, - //warrior - SPELL_BLADESTORM = 46924, - SPELL_CLEAVE = 47520, - //druid - SPELL_MOONFIRE = 48463, - SPELL_LIFEBLOOM = 48451, - //death knight - SPELL_PLAGUE_STRIKE = 49921, - SPELL_HOWLING_BLAST = 51411, - - // Abomination spells - SPELL_FRENZY = 28468, - SPELL_MORTAL_WOUND = 28467 + ACTION_BEGIN_ENCOUNTER, + ACTION_JUST_SUMMONED, + ACTION_ABOMINATION_DIED, + ACTION_KELTHUZAD_DIED }; -enum Phases +enum KTData { - PHASE_ONE = 1, // Players move in the circle and Kel'Thuzad spawns his minions. - PHASE_TWO = 2, // Starts on a timer. - PHASE_THREE = 3 // At 45% health. + DATA_MINION_POCKET_ID, + DATA_ABOMINATION_DEATH_COUNT }; -enum Creatures +enum Spells { - NPC_WASTE = 16427, // Soldiers of the Frozen Wastes - NPC_ABOMINATION = 16428, // Unstoppable Abominations - NPC_WEAVER = 16429, // Soul Weavers - NPC_ICECROWN = 16441 // Guardians of Icecrown + // Kel'thuzad - Phase one + SPELL_VISUAL_CHANNEL = 29423, // channeled throughout phase one + + // Kel'thuzad - Phase two + SPELL_FROSTBOLT_SINGLE = 28478, + SPELL_FROSTBOLT_VOLLEY = 28479, + SPELL_SHADOW_FISSURE = 27810, + SPELL_DETONATE_MANA = 27819, + SPELL_MANA_DETONATION_DAMAGE = 27820, + SPELL_FROST_BLAST = 27808, + SPELL_CHAINS = 28410, + SPELL_CHAINS_DUMMY = 28408, // this holds the category cooldown - the main chains spell can't have one as it is cast multiple times + + SPELL_BERSERK = 28498, + + // Unstoppable Abomination + SPELL_MORTAL_WOUND = 28467, + + // Guardian of Icecrown + SPELL_BLOOD_TAP = 28470 }; -Position const Pos[12] = +static const uint8 nGuardianSpawns = 4; +static const uint8 nMinionGroups = 7; +enum SummonGroups { - {3783.272705f, -5062.697266f, 143.711203f, 3.617599f}, //LEFT_FAR - {3730.291260f, -5027.239258f, 143.956909f, 4.461900f}, //LEFT_MIDDLE - {3757.6f, -5172.0f, 143.7f, 1.97f}, //WINDOW_PORTAL05 - {3759.355225f, -5174.128418f, 143.802383f, 2.170104f}, //RIGHT_FAR - {3700.724365f, -5185.123047f, 143.928024f, 1.309310f}, //RIGHT_MIDDLE - {3700.86f, -5181.29f, 143.928024f, 1.42f}, //WINDOW_PORTAL04 - {3754.431396f, -5080.727734f, 142.036316f, 3.736189f}, //LEFT_FAR - {3724.396484f, -5061.330566f, 142.032700f, 4.564785f}, //LEFT_MIDDLE - {3732.02f, -5028.53f, 143.92f, 4.49f}, //WINDOW_PORTAL02 - {3687.571777f, -5126.831055f, 142.017807f, 0.604023f}, //RIGHT_FAR - {3707.990733f, -5151.450195f, 142.032562f, 1.376855f}, //RIGHT_MIDDLE - {3782.76f, -5062.97f, 143.79f, 3.82f} //WINDOW_PORTAL03 + SUMMON_GROUP_GUARDIAN_FIRST = 01 /*..04 */, + SUMMON_GROUP_MINION_FIRST = 05 /*..11 */ }; +static const std::initializer_list<NAXData64> portalList = { DATA_KELTHUZAD_PORTAL01, DATA_KELTHUZAD_PORTAL02, DATA_KELTHUZAD_PORTAL03, DATA_KELTHUZAD_PORTAL04 }; -//creatures in corners -//Unstoppable Abominations -#define MAX_ABOMINATIONS 21 -Position const PosAbominations[MAX_ABOMINATIONS] = +enum Phases { - {3755.52f, -5155.22f, 143.480f, 2.0f}, - {3744.35f, -5164.03f, 143.590f, 2.00f}, - {3749.28f, -5159.04f, 143.190f, 2.0f}, - {3774.47f, -5076.28f, 143.528f, 2.15912f}, - {3765.94f, -5074.15f, 143.186f, 3.77233f}, - {3763.15f, -5063.36f, 143.694f, 3.77233f}, - {3737.81f, -5045.69f, 143.709f, 4.9033f}, - {3728.13f, -5045.01f, 143.355f, 1.45069f}, - {3721.56f, -5048.35f, 143.542f, 1.45069f}, - {3689.55f, -5049.66f, 143.637f, 5.2104f}, - {3681.71f, -5053.03f, 143.242f, 2.47957f}, - {3677.64f, -5061.44f, 143.358f, 2.47957f}, - {3654.2f, -5090.87f, 143.469f, 1.0313f}, - {3650.39f, -5097.45f, 143.496f, 2.5047f}, - {3658.7f, -5103.59f, 143.607f, 3.3278f}, - {3659.02f, -5133.97f, 143.624f, 3.84538f}, - {3666.33f, -5139.34f, 143.183f, 3.84538f}, - {3669.74f, -5149.63f, 143.678f, 0.528643f}, - {3695.53f, -5169.53f, 143.671f, 2.11908f}, - {3701.98f, -5166.51f, 143.395f, 1.24257f}, - {3709.62f, -5169.15f, 143.576f, 5.97695f} + PHASE_ONE = 1, + PHASE_TWO = 2 // "phase three" is not actually a phase in events, as timers from phase two carry over }; -//Soldiers of the Frozen Wastes -#define MAX_WASTES 49 -Position const PosWastes[MAX_WASTES] = +enum Movements { - {3754.41f, -5147.24f, 143.204f, 2.0f}, - {3754.68f, -5156.17f, 143.418f, 2.0f}, - {3757.91f, -5160.12f, 143.503f, 2.0f}, - {3752.67f, -5164.6f, 143.395f, 2.0f}, - {3745.42f, -5164.47f, 143.565f, 2.74587f}, - {3741.2f, -5155.92f, 143.17f, 5.29134f}, - {3746.57f, -5148.82f, 143.176f, 5.07772f}, - {3778.14f, -5070.1f, 143.568f, 3.16208f}, - {3775.09f, -5078.97f, 143.65f, 2.81022f}, - {3773.54f, -5083.47f, 143.758f, 3.21549f}, - {3765, -5078.29f, 143.176f, 4.36688f}, - {3766.94f, -5072.63f, 143.184f, 5.27951f}, - {3762.68f, -5064.94f, 143.635f, 3.95297f}, - {3769.9f, -5059.94f, 143.74f, 3.36549f}, - {3736.33f, -5042.18f, 143.643f, 5.9471f}, - {3727.51f, -5040.58f, 143.502f, 0.871859f}, - {3719.89f, -5049.64f, 143.58f, 4.75172f}, - {3720.69f, -5044.43f, 143.662f, 1.87245f}, - {3725.69f, -5048.99f, 143.363f, 2.48271f}, - {3732.33f, -5054.01f, 143.44f, 3.59405f}, - {3738.09f, -5051.06f, 143.718f, 4.70931f}, - {3682.76f, -5063.5f, 143.175f, 0.636238f}, - {3686.7f, -5060.58f, 143.18f, 0.636238f}, - {3682.45f, -5057.21f, 143.184f, 5.61252f}, - {3677.57f, -5053.34f, 143.369f, 1.52531f}, - {3677.3f, -5062.26f, 143.369f, 2.73482f}, - {3691.21f, -5053.02f, 143.421f, 5.93218f}, - {3685.22f, -5053.34f, 143.314f, 4.70303f}, - {3652.11f, -5088.47f, 143.555f, 0.793317f}, - {3648.23f, -5093.21f, 143.311f, 1.18837f}, - {3648.14f, -5100.11f, 143.632f, 2.12221f}, - {3653.88f, -5099.7f, 143.558f, 3.04348f}, - {3661.23f, -5100.33f, 143.42f, 4.08335f}, - {3663.49f, -5092.77f, 143.346f, 4.47134f}, - {3661.85f, -5087.99f, 143.571f, 1.0148f}, - {3664.56f, -5149.01f, 143.532f, 0.0762528f}, - {3665.01f, -5142.04f, 143.201f, 1.72009f}, - {3671.15f, -5142.92f, 143.174f, 4.81535f}, - {3670.18f, -5134.38f, 143.177f, 5.37534f}, - {3664.33f, -5131.69f, 143.262f, 5.39576f}, - {3658.21f, -5133.63f, 143.662f, 1.3863f}, - {3659.7f, -5144.49f, 143.363f, 2.32328f}, - {3705.71f, -5179.63f, 143.746f, 1.06743f}, - {3696.77f, -5177.45f, 143.759f, 5.36748f}, - {3700.97f, -5173.13f, 143.52f, 3.26575f}, - {3708.53f, -5172.19f, 143.573f, 3.26575f}, - {3712.49f, -5167.62f, 143.657f, 5.63295f}, - {3704.89f, -5161.84f, 143.239f, 5.63295f}, - {3695.66f, -5164.63f, 143.674f, 1.54416f} + MOVEMENT_MINION_RANDOM = 1, }; -//Soul Weavers -#define MAX_WEAVERS 7 -Position const PosWeavers[MAX_WEAVERS] = +enum Creatures { - {3752.45f, -5168.35f, 143.562f, 1.6094f}, - {3772.2f, -5070.04f, 143.329f, 1.93686f}, - {3732.28f, -5032.88f, 143.771f, 3.08355f}, - {3689.05f, -5055.7f, 143.172f, 6.09554f}, - {3649.45f, -5093.17f, 143.299f, 2.51805f}, - {3659.7f, -5144.49f, 143.363f, 4.08806f}, - {3704.71f, -5175.96f, 143.597f, 3.36549f} + NPC_SKELETON1 = 16427, // Soldiers of the Frozen Wastes + NPC_SKELETON2 = 23561, + NPC_ABOMINATION1 = 16428, // Unstoppable Abominations + NPC_ABOMINATION2 = 23562, + NPC_BANSHEE1 = 16429, // Soul Weavers + NPC_BANSHEE2 = 23563, + NPC_GUARDIAN = 16441 // Guardians of Icecrown +}; + +static const uint8 nMinionSpawnPoints = 7; +static const Position minionSpawnPoints[nMinionSpawnPoints] = { + { 3768.40f, -5072.00f, 143.65f }, // summon group 5 + { 3729.30f, -5044.10f, 143.65f }, // summon group 6 + { 3683.00f, -5054.05f, 143.65f }, // summon group 7 + { 3654.15f, -5093.48f, 143.65f }, // summon group 8 + { 3664.55f, -5140.50f, 143.65f }, // summon group 9 + { 3704.00f, -5170.00f, 143.65f }, // summon group 10 + { 3751.95f, -5158.90f, 143.65f } // summon group 11 }; +static inline Position const& GetRandomMinionSpawnPoint() +{ + return minionSpawnPoints[urand(0, nMinionSpawnPoints - 1)]; +} + +// uniformly distribute on the circle +static Position GetRandomPositionOnCircle(Position const& center, float radius) +{ + double angle = rand_norm() * M_2_PI; + double relDistance = rand_norm() + rand_norm(); + if (relDistance > 1) + relDistance = 1 - relDistance; + return Position(center.GetPositionX() + std::sin(angle)*relDistance*radius, center.GetPositionY() + std::cos(angle)*relDistance*radius, center.GetPositionZ()); +} -// predicate function to select not charmed target -struct NotCharmedTargetSelector : public std::unary_function<Unit*, bool> +class KelThuzadCharmedPlayerAI : public SimpleCharmedPlayerAI { - NotCharmedTargetSelector() { } + public: + KelThuzadCharmedPlayerAI(Player* player) : SimpleCharmedPlayerAI(player) { } + + struct CharmedPlayerTargetSelectPred : public std::unary_function<Unit*, bool> + { + bool operator()(Unit const* target) const + { + Player const* pTarget = target->ToPlayer(); + if (!pTarget) + return false; + if (pTarget->HasAura(SPELL_CHAINS)) + return false; + if (pTarget->HasBreakableByDamageCrowdControlAura()) + return false; + // We _really_ dislike healers. So we hit them in the face. Repeatedly. Exclusively. + return PlayerAI::IsPlayerHealer(pTarget); + } + }; + + Unit* SelectAttackTarget() const override + { + if (Creature* charmer = GetCharmer()) + { + if (Unit* target = charmer->AI()->SelectTarget(SELECT_TARGET_RANDOM, 0, CharmedPlayerTargetSelectPred())) + return target; + if (Unit* target = charmer->AI()->SelectTarget(SELECT_TARGET_RANDOM, 0, 0.0f, true, -SPELL_CHAINS)) + return target; + } + return nullptr; + } +}; +struct ManaUserTargetSelector : public std::unary_function<Unit*, bool> +{ bool operator()(Unit const* target) const { - return !target->IsCharmed(); + return target->GetTypeId() == TYPEID_PLAYER && target->getPowerType() == POWER_MANA; } }; @@ -268,358 +206,362 @@ public: struct boss_kelthuzadAI : public BossAI { - boss_kelthuzadAI(Creature* creature) : BossAI(creature, BOSS_KELTHUZAD), spawns(creature) - { - Initialize(); - uiFaction = me->getFaction(); - } - - void Initialize() - { - nGuardiansOfIcecrownCount = 0; - - nAbomination = 0; - nWeaver = 0; - } - - uint32 uiFaction; - - uint8 nGuardiansOfIcecrownCount; - uint8 nAbomination; - uint8 nWeaver; - - std::map<ObjectGuid, float> chained; - - SummonList spawns; // adds spawn by the trigger. kept in separated list (i.e. not in summons) - - void ResetPlayerScale() - { - std::map<ObjectGuid, float>::const_iterator itr; - for (itr = chained.begin(); itr != chained.end(); ++itr) + public: + boss_kelthuzadAI(Creature* creature) : BossAI(creature, BOSS_KELTHUZAD), _skeletonCount(0), _bansheeCount(0), _abominationCount(0), _abominationDeathCount(0), _frostboltCooldown(0), _phaseThree(false), _guardianCount(0) { - if (Player* charmed = ObjectAccessor::GetPlayer(*me, itr->first)) - charmed->SetObjectScale(itr->second); + for (uint8 i = 0; i < nGuardianSpawns; ++i) + _guardianGroups[i] = SUMMON_GROUP_GUARDIAN_FIRST + i; } - chained.clear(); - } - - void Reset() override - { - _Reset(); - - me->setFaction(35); - me->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE); - me->SetControlled(true, UNIT_STATE_ROOT); - - ResetPlayerScale(); - spawns.DespawnAll(); - - instance->SetData(DATA_ABOMINATION_KILLED, 0); - - if (GameObject* trigger = ObjectAccessor::GetGameObject(*me, instance->GetGuidData(DATA_KELTHUZAD_TRIGGER))) + void Reset() override { - trigger->ResetDoorOrButton(); - trigger->SetLootState(GO_READY); + if (!me->IsAlive()) + return; + _Reset(); + me->SetReactState(REACT_PASSIVE); + me->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IMMUNE_TO_PC | UNIT_FLAG_NOT_SELECTABLE); + _skeletonCount = 0; + _bansheeCount = 0; + _abominationCount = 0; + _abominationDeathCount = 0; + _phaseThree = false; } - - for (uint8 i = 0; i <= 3; ++i) + void EnterEvadeMode(EvadeReason /*why*/) override { - if (GameObject* portal = ObjectAccessor::GetGameObject(*me, instance->GetGuidData(DATA_KELTHUZAD_PORTAL01 + i))) - if (!((portal->getLootState() == GO_READY) || (portal->getLootState() == GO_NOT_READY))) - portal->ResetDoorOrButton(); - } + for (NAXData64 portalData : portalList) + if (GameObject* portal = ObjectAccessor::GetGameObject(*me, instance->GetGuidData(portalData))) + portal->SetGoState(GO_STATE_READY); - Initialize(); - } + Reset(); + _DespawnAtEvade(); + } - void KilledUnit(Unit* /*victim*/) override - { - Talk(SAY_SLAY); - } + void JustSummoned (Creature* summon) override + { // prevent DoZoneInCombat + summons.Summon(summon); + } - void JustDied(Unit* /*killer*/) override - { - _JustDied(); - Talk(SAY_DEATH); + void KilledUnit(Unit* victim) override + { + if (victim->GetTypeId() == TYPEID_PLAYER) + Talk(SAY_SLAY); + } - ResetPlayerScale(); - } + void JustDied(Unit* /*killer*/) override + { + SummonList::iterator it = summons.begin(); + while (it != summons.end()) + if (Creature* cSummon = ObjectAccessor::GetCreature(*me, *it)) + { + if (cSummon->IsAlive() && cSummon->GetEntry() == NPC_GUARDIAN) + { + cSummon->AI()->DoAction(ACTION_KELTHUZAD_DIED); + it = summons.erase(it); // prevent them from being despawned by _JustDied + } + else + ++it; + } - void EnterCombat(Unit* /*who*/) override - { - me->setFaction(uiFaction); - _EnterCombat(); - for (uint8 i = 0; i <= 3; ++i) + _JustDied(); + Talk(SAY_DEATH); + } + + void DamageTaken(Unit* /*attacker*/, uint32& damage) override { - if (GameObject* portal = ObjectAccessor::GetGameObject(*me, instance->GetGuidData(DATA_KELTHUZAD_PORTAL01 + i))) - portal->ResetDoorOrButton(); + if (events.IsInPhase(PHASE_ONE)) + damage = 0; } - DoCast(me, SPELL_KELTHUZAD_CHANNEL, false); - Talk(SAY_SUMMON_MINIONS); - me->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE); - me->SetControlled(true, UNIT_STATE_ROOT); - me->SetFloatValue(UNIT_FIELD_COMBATREACH, 4); - me->SetFloatValue(UNIT_FIELD_BOUNDINGRADIUS, 4); - events.SetPhase(PHASE_ONE); - events.ScheduleEvent(EVENT_TRIGGER, 5000); - events.ScheduleEvent(EVENT_WASTE, 15000); - events.ScheduleEvent(EVENT_ABOMIN, 30000); - events.ScheduleEvent(EVENT_WEAVER, 50000); - events.ScheduleEvent(EVENT_PHASE, 228000); - } - void DamageTaken(Unit* /*attacker*/, uint32& damage) override - { - if (events.IsInPhase(PHASE_TWO) && me->HealthBelowPctDamaged(45, damage)) + void SpellHit(Unit* /*caster*/, SpellInfo const* spell) override { - Talk(SAY_REQUEST_AID); - events.SetPhase(PHASE_THREE); - events.ScheduleEvent(EVENT_ANSWER_REQUEST, 4000); - - for (uint8 i = 0; i <= 3; ++i) + if (spell->Id == SPELL_CHAINS_DUMMY) { - if (GameObject* portal = ObjectAccessor::GetGameObject(*me, instance->GetGuidData(DATA_KELTHUZAD_PORTAL01 + i))) - if (portal->getLootState() == GO_READY) - portal->UseDoorOrButton(); + Talk(SAY_CHAINS); + std::list<Unit*> targets; + SelectTargetList(targets, 3, SELECT_TARGET_RANDOM, 0.0f, true); + for (Unit* target : targets) + if (me->GetVictim() != target) // skip MT + DoCast(target, SPELL_CHAINS); } } - } - void UpdateAI(uint32 diff) override - { - if (!UpdateVictim()) - return; + void UpdateAI(uint32 diff) override + { + if (!UpdateVictim()) + return; - events.Update(diff); + events.Update(diff); - if (events.IsInPhase(PHASE_ONE)) - { - while (uint32 eventId = events.ExecuteEvent()) + if (_frostboltCooldown < diff) + _frostboltCooldown = 0; + else + _frostboltCooldown -= diff; + + if (!events.IsInPhase(PHASE_ONE) && me->HasUnitState(UNIT_STATE_CASTING)) + return; + + if (!_phaseThree && HealthBelowPct(45)) { - switch (eventId) + _phaseThree = true; + _guardianCount = 0; + Talk(SAY_REQUEST_AID); + events.ScheduleEvent(EVENT_TRANSITION_REPLY, Seconds(4), 0, PHASE_TWO); + events.ScheduleEvent(EVENT_TRANSITION_SUMMON, randtime(Seconds(7), Seconds(9)), 0, PHASE_TWO); + events.ScheduleEvent(EVENT_TRANSITION_SUMMON, randtime(Seconds(13), Seconds(15)), 0, PHASE_TWO); + if (Is25ManRaid()) { - case EVENT_WASTE: - DoSummon(NPC_WASTE, Pos[RAND(0, 3, 6, 9)]); - events.Repeat(2000, 5000); - break; - case EVENT_ABOMIN: - if (nAbomination < 8) - { - DoSummon(NPC_ABOMINATION, Pos[RAND(1, 4, 7, 10)]); - nAbomination++; - events.Repeat(20000); - } - break; - case EVENT_WEAVER: - if (nWeaver < 8) - { - DoSummon(NPC_WEAVER, Pos[RAND(0, 3, 6, 9)]); - nWeaver++; - events.Repeat(25000); - } - break; - case EVENT_TRIGGER: - if (GameObject* trigger = ObjectAccessor::GetGameObject(*me, instance->GetGuidData(DATA_KELTHUZAD_TRIGGER))) - trigger->SetLootState(GO_JUST_DEACTIVATED); - break; - case EVENT_PHASE: - events.Reset(); - Talk(SAY_AGGRO); - Talk(EMOTE_PHASE_TWO); - spawns.DespawnAll(); - me->RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NOT_SELECTABLE); - me->SetControlled(false, UNIT_STATE_ROOT); - me->CastStop(); - - DoStartMovement(me->GetVictim()); - events.ScheduleEvent(EVENT_BOLT, urand(5000, 10000)); - events.ScheduleEvent(EVENT_NOVA, 15000); - events.ScheduleEvent(EVENT_DETONATE, urand(30000, 40000)); - events.ScheduleEvent(EVENT_FISSURE, urand(10000, 30000)); - events.ScheduleEvent(EVENT_BLAST, urand(60000, 120000)); - if (GetDifficulty() == DIFFICULTY_25_N) - events.ScheduleEvent(EVENT_CHAIN, urand(30000, 60000)); - events.SetPhase(PHASE_TWO); - break; - default: - break; + events.ScheduleEvent(EVENT_TRANSITION_SUMMON, randtime(Seconds(19), Seconds(21)), 0, PHASE_TWO); + events.ScheduleEvent(EVENT_TRANSITION_SUMMON, randtime(Seconds(25), Seconds(27)), 0, PHASE_TWO); } } - } - else - { - if (me->HasUnitState(UNIT_STATE_CASTING)) - return; - if (uint32 eventId = events.ExecuteEvent()) + while (uint32 eventId = events.ExecuteEvent()) { + if (_frostboltCooldown <= 4 * IN_MILLISECONDS) // stop casting bolts for 4 seconds after doing another action + _frostboltCooldown = 4 * IN_MILLISECONDS; switch (eventId) { - case EVENT_BOLT: - DoCastVictim(SPELL_FROST_BOLT); - events.Repeat(5000, 10000); - break; - case EVENT_NOVA: - DoCastAOE(SPELL_FROST_BOLT_AOE); - events.Repeat(15000, 30000); - break; - case EVENT_CHAIN: + case EVENT_SKELETON: { - uint32 count = urand(1, 3); - for (uint8 i = 1; i <= count; i++) + ++_skeletonCount; + if (_skeletonCount == 1) // the first skeleton is actually one of the pre-existing ones - I'm not sure why, but that's what the sniffs say { - Unit* target = SelectTarget(SELECT_TARGET_RANDOM, 1, 200, true); - if (target && !target->IsCharmed() && (chained.find(target->GetGUID()) == chained.end())) - { - DoCast(target, SPELL_CHAINS_OF_KELTHUZAD); - float scale = target->GetObjectScale(); - chained.insert(std::make_pair(target->GetGUID(), scale)); - target->SetObjectScale(scale * 2); - events.ScheduleEvent(EVENT_CHAINED_SPELL, 2000); //core has 2000ms to set unit flag charm + std::list<Creature*> skeletons; + me->GetCreatureListWithEntryInGrid(skeletons, NPC_SKELETON2, 200.0f); + if (skeletons.empty()) + { // prevent UB + EnterEvadeMode(EVADE_REASON_OTHER); + return; } + std::list<Creature*>::iterator it = skeletons.begin(); + std::advance(it, urand(0, skeletons.size() - 1)); + (*it)->SetReactState(REACT_AGGRESSIVE); + (*it)->AI()->DoZoneInCombat(); // will select a player on our threat list as we are the summoner } - if (!chained.empty()) - Talk(SAY_CHAIN); - events.Repeat(100000, 180000); - break; - } - case EVENT_CHAINED_SPELL: - { - std::map<ObjectGuid, float>::iterator itr; - for (itr = chained.begin(); itr != chained.end();) + else { - if (Unit* player = ObjectAccessor::GetPlayer(*me, itr->first)) - { - if (!player->IsCharmed()) - { - player->SetObjectScale(itr->second); - std::map<ObjectGuid, float>::iterator next = itr; - ++next; - chained.erase(itr); - itr = next; - continue; - } - - if (Unit* target = SelectTarget(SELECT_TARGET_TOPAGGRO, 0, NotCharmedTargetSelector())) - { - switch (player->getClass()) - { - case CLASS_DRUID: - if (urand(0, 1)) - player->CastSpell(target, SPELL_MOONFIRE, false); - else - player->CastSpell(me, SPELL_LIFEBLOOM, false); - break; - case CLASS_HUNTER: - player->CastSpell(target, RAND(SPELL_MULTI_SHOT, SPELL_VOLLEY), false); - break; - case CLASS_MAGE: - player->CastSpell(target, RAND(SPELL_FROST_FIREBOLT, SPELL_ARCANE_MISSILES), false); - break; - case CLASS_WARLOCK: - player->CastSpell(target, RAND(SPELL_CURSE_OF_AGONY, SPELL_SHADOW_BOLT), true); - break; - case CLASS_WARRIOR: - player->CastSpell(target, RAND(SPELL_BLADESTORM, SPELL_CLEAVE), false); - break; - case CLASS_PALADIN: - if (urand(0, 1)) - player->CastSpell(target, SPELL_HAMMER_OF_JUSTICE, false); - else - player->CastSpell(me, SPELL_HOLY_SHOCK, false); - break; - case CLASS_PRIEST: - if (urand(0, 1)) - player->CastSpell(target, SPELL_VAMPIRIC_TOUCH, false); - else - player->CastSpell(me, SPELL_RENEW, false); - break; - case CLASS_SHAMAN: - if (urand(0, 1)) - player->CastSpell(target, SPELL_EARTH_SHOCK, false); - else - player->CastSpell(me, SPELL_HEALING_WAVE, false); - break; - case CLASS_ROGUE: - player->CastSpell(target, RAND(SPELL_HEMORRHAGE, SPELL_MUTILATE), false); - break; - case CLASS_DEATH_KNIGHT: - if (urand(0, 1)) - player->CastSpell(target, SPELL_PLAGUE_STRIKE, true); - else - player->CastSpell(target, SPELL_HOWLING_BLAST, true); - break; - } - } - } - ++itr; + // retail uses server-side spell 28421 for this + Creature* summon = me->SummonCreature(NPC_SKELETON1, GetRandomMinionSpawnPoint(), TEMPSUMMON_CORPSE_TIMED_DESPAWN, 2 * IN_MILLISECONDS); + summon->AI()->DoZoneInCombat(); } - if (!chained.empty()) - events.Repeat(5000); + uint8 nextTime = 0; + if (_skeletonCount < 10) + nextTime = 5; + else if (_skeletonCount < 19) + nextTime = 4; + else if (_skeletonCount < 31) + nextTime = 3; + else if (_skeletonCount == 31) + nextTime = 4; + else if (_skeletonCount < 72) + nextTime = 2; + + if (nextTime) + events.ScheduleEvent(EVENT_SKELETON, Seconds(nextTime), 0, PHASE_ONE); + break; + } + case EVENT_BANSHEE: + { + ++_bansheeCount; + // retail uses server-side spell 28423 for this + Creature* summon = me->SummonCreature(NPC_BANSHEE1, GetRandomMinionSpawnPoint(), TEMPSUMMON_CORPSE_TIMED_DESPAWN, 2 * IN_MILLISECONDS); + summon->AI()->DoZoneInCombat(); + + uint8 nextTime = 0; + if (_bansheeCount < 3) + nextTime = 30; + else if (_bansheeCount < 7) + nextTime = 20; + else if (_bansheeCount < 9) + nextTime = 15; + + if (nextTime) + events.ScheduleEvent(EVENT_BANSHEE, Seconds(nextTime), 0, PHASE_ONE); break; } - case EVENT_DETONATE: + + case EVENT_ABOMINATION: { - std::vector<Unit*> unitList; - ThreatContainer::StorageType const &threatList = me->getThreatManager().getThreatList(); - for (ThreatContainer::StorageType::const_iterator itr = threatList.begin(); itr != threatList.end(); ++itr) - { - Unit* const target = (*itr)->getTarget(); + ++_abominationCount; + // retail uses server-side spell 28422 for this + Creature* summon = me->SummonCreature(NPC_ABOMINATION1, GetRandomMinionSpawnPoint(), TEMPSUMMON_CORPSE_TIMED_DESPAWN, 2 * IN_MILLISECONDS); + summon->AI()->DoZoneInCombat(); + + uint8 nextTime = 0; + if (_abominationCount < 3) + nextTime = 30; + else if (_abominationCount < 7) + nextTime = 20; + else if (_abominationCount < 9) + nextTime = 15; + + if (nextTime) + events.ScheduleEvent(EVENT_ABOMINATION, Seconds(nextTime), 0, PHASE_ONE); + break; + } - if (target->GetTypeId() == TYPEID_PLAYER - && target->getPowerType() == POWER_MANA - && target->GetPower(POWER_MANA)) - { - unitList.push_back(target); - } - } + case EVENT_DESPAWN_MINIONS: + { + // we need a temp vector, as we can't modify summons while iterating (this would cause UB) + std::vector<Creature*> toDespawn; + toDespawn.reserve(summons.size()); + for (ObjectGuid sGuid : summons) + if (Creature* summon = ObjectAccessor::GetCreature(*me, sGuid)) + if (!summon->IsInCombat()) + toDespawn.push_back(summon); + for (Creature* summon : toDespawn) + summon->DespawnOrUnsummon(); + Talk(SAY_AGGRO); + break; + } - if (!unitList.empty()) - { - std::vector<Unit*>::const_iterator itr = unitList.begin(); - advance(itr, rand32() % unitList.size()); - DoCast(*itr, SPELL_MANA_DETONATION); - Talk(SAY_SPECIAL); - } + case EVENT_PHASE_TWO: + me->CastStop(); + events.SetPhase(PHASE_TWO); + me->RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NOT_SELECTABLE | UNIT_FLAG_IMMUNE_TO_PC); + me->getThreatManager().resetAllAggro(); + me->SetReactState(REACT_AGGRESSIVE); + Talk(EMOTE_PHASE_TWO); - events.Repeat(20000, 50000); + _frostboltCooldown = 2 * IN_MILLISECONDS; + events.ScheduleEvent(EVENT_FROSTBOLT_VOLLEY, randtime(Seconds(24), Seconds(28)), 0, PHASE_TWO); + events.ScheduleEvent(EVENT_SHADOW_FISSURE, randtime(Seconds(6), Seconds(10)), 0, PHASE_TWO); + events.ScheduleEvent(EVENT_DETONATE_MANA, randtime(Seconds(27), Seconds(33)), 0, PHASE_TWO); + events.ScheduleEvent(EVENT_FROST_BLAST, randtime(Seconds(25), Seconds(45)), 0, PHASE_TWO); + if (Is25ManRaid()) + events.ScheduleEvent(EVENT_CHAINS, randtime(Seconds(60), Seconds(80)), 0, PHASE_TWO); break; - } - case EVENT_FISSURE: - if (Unit* target = SelectTarget(SELECT_TARGET_RANDOM, 0)) - DoCast(target, SPELL_SHADOW_FISURE); - events.Repeat(10000, 45000); + + case EVENT_FROSTBOLT_VOLLEY: + DoCastAOE(SPELL_FROSTBOLT_VOLLEY); + events.Repeat(randtime(Seconds(16), Seconds(18))); break; - case EVENT_BLAST: - if (Unit* target = SelectTarget(SELECT_TARGET_RANDOM, RAID_MODE(1, 0), 0, true)) + + case EVENT_SHADOW_FISSURE: + if (Unit* target = SelectTarget(SELECT_TARGET_RANDOM, 0, 0.0f, true)) + DoCast(target, SPELL_SHADOW_FISSURE); + events.Repeat(randtime(Seconds(14), Seconds(17))); + break; + + case EVENT_DETONATE_MANA: + { + ManaUserTargetSelector pred; + if (Unit* target = SelectTarget(SELECT_TARGET_RANDOM, 0, pred)) + DoCast(target, SPELL_DETONATE_MANA); + events.Repeat(randtime(Seconds(30), Seconds(40))); + break; + } + + case EVENT_FROST_BLAST: + if (Unit* target = SelectTarget(SELECT_TARGET_RANDOM, 1, 0.0f, true)) DoCast(target, SPELL_FROST_BLAST); - if (rand32() % 2) - Talk(SAY_FROST_BLAST); - events.Repeat(30000, 90000); + events.Repeat(randtime(Seconds(25), Seconds(45))); + break; + + case EVENT_CHAINS: + { + DoCastAOE(SPELL_CHAINS_DUMMY); + events.Repeat(Minutes(1) + randtime(Seconds(0), Seconds(20))); break; - case EVENT_ANSWER_REQUEST: + } + + case EVENT_TRANSITION_REPLY: if (Creature* lichKing = ObjectAccessor::GetCreature(*me, instance->GetGuidData(DATA_LICH_KING))) lichKing->AI()->Talk(SAY_ANSWER_REQUEST); - events.ScheduleEvent(EVENT_SUMMON_GUARDIANS, 5000); - break; - case EVENT_SUMMON_GUARDIANS: - if (Creature* guardian = DoSummon(NPC_ICECROWN, Pos[RAND(2, 5, 8, 11)])) - guardian->SetFloatValue(UNIT_FIELD_COMBATREACH, 2); - ++nGuardiansOfIcecrownCount; - if (nGuardiansOfIcecrownCount < RAID_MODE(2, 4)) - events.ScheduleEvent(EVENT_SUMMON_GUARDIANS, 5000); + for (NAXData64 portalData : portalList) + if (GameObject* portal = ObjectAccessor::GetGameObject(*me, instance->GetGuidData(portalData))) + portal->SetGoState(GO_STATE_ACTIVE); break; - default: + + case EVENT_TRANSITION_SUMMON: + { + uint8 selected = urand(_guardianCount, nGuardianSpawns - 1); + if (selected != _guardianCount) + std::swap(_guardianGroups[selected], _guardianGroups[_guardianCount]); + + std::list<TempSummon*> summoned; + // server-side spell 28454 is used on retail - no point replicating this in spell_dbc + me->SummonCreatureGroup(_guardianGroups[_guardianCount++], &summoned); + for (TempSummon* guardian : summoned) + guardian->AI()->DoAction(ACTION_JUST_SUMMONED); break; + } } } - DoMeleeAttackIfReady(); + if (!_frostboltCooldown) + { + DoCastVictim(SPELL_FROSTBOLT_SINGLE); + _frostboltCooldown = 3 * IN_MILLISECONDS; + } + else + DoMeleeAttackIfReady(); } - } + + uint32 GetData(uint32 data) const override + { + if (data == DATA_ABOMINATION_DEATH_COUNT) + return _abominationDeathCount; + return 0; + } + + void DoAction(int32 action) override + { + switch (action) + { + case ACTION_BEGIN_ENCOUNTER: + if (instance->GetBossState(BOSS_KELTHUZAD) != NOT_STARTED) + return; + me->RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IMMUNE_TO_PC); + instance->SetBossState(BOSS_KELTHUZAD, IN_PROGRESS); + events.SetPhase(PHASE_ONE); + DoZoneInCombat(); + DoCastAOE(SPELL_VISUAL_CHANNEL); + Talk(SAY_SUMMON_MINIONS); + + for (uint8 group = SUMMON_GROUP_MINION_FIRST; group < SUMMON_GROUP_MINION_FIRST + nMinionGroups; ++group) + { + std::list<TempSummon*> summoned; + me->SummonCreatureGroup(group, &summoned); + for (TempSummon* summon : summoned) + { + summon->SetReactState(REACT_PASSIVE); + summon->AI()->SetData(DATA_MINION_POCKET_ID, group); + } + } + + events.ScheduleEvent(EVENT_SKELETON, Seconds(5), 0, PHASE_ONE); + events.ScheduleEvent(EVENT_BANSHEE, Seconds(30), 0, PHASE_ONE); + events.ScheduleEvent(EVENT_ABOMINATION, Seconds(30), 0, PHASE_ONE); + events.ScheduleEvent(EVENT_DESPAWN_MINIONS, Minutes(3) + Seconds(33), 0, PHASE_ONE); + events.ScheduleEvent(EVENT_PHASE_TWO, Minutes(3) + Seconds(48), 0, PHASE_ONE); + break; + + case ACTION_ABOMINATION_DIED: + ++_abominationDeathCount; + break; + + default: + break; + } + } + + PlayerAI* GetAIForCharmedPlayer(Player* player) override + { + return new KelThuzadCharmedPlayerAI(player); + } + + private: + uint8 _skeletonCount; + uint8 _bansheeCount; + uint8 _abominationCount; + uint8 _abominationDeathCount; + uint32 _frostboltCooldown; + bool _phaseThree; + uint32 _guardianCount; + std::array<uint32, nGuardianSpawns> _guardianGroups; }; CreatureAI* GetAI(Creature* creature) const override @@ -628,188 +570,427 @@ public: } }; -class at_kelthuzad_center : public AreaTriggerScript +static const float MINION_AGGRO_DISTANCE = 20.0f; +// @hack the entire _movementTimer logic only exists because RandomMovementGenerator gets really confused due to the unique map geography of KT's room (it's placed on top of a copy of Winterspring). +// As of the time of writing, RMG sometimes selects positions on the "floor" below the room, causing Abominations to path wildly through the room. +// This custom movement code prevents this by simply ignoring z coord calculation (the floor of the minion coves is flat anyway). +// Dev from the future that is reading this, if RMG has been fixed on the current core revision, please get rid of this hack. Thank you! +struct npc_kelthuzad_minionAI : public ScriptedAI { -public: - at_kelthuzad_center() : AreaTriggerScript("at_kelthuzad_center") { } - - bool OnTrigger(Player* player, const AreaTriggerEntry* /*areaTrigger*/, bool /*entered*/) override - { - if (player->IsGameMaster()) - return false; - - InstanceScript* instance = player->GetInstanceScript(); - if (!instance || instance->IsEncounterInProgress() || instance->GetBossState(BOSS_KELTHUZAD) == DONE) - return false; - - Creature* pKelthuzad = ObjectAccessor::GetCreature(*player, instance->GetGuidData(DATA_KELTHUZAD)); - if (!pKelthuzad) - return false; + public: + npc_kelthuzad_minionAI(Creature* creature) : ScriptedAI(creature), instance(creature->GetInstanceScript()), _movementTimer(urandms(4,12)), _home(me->GetPosition()) { } - boss_kelthuzad::boss_kelthuzadAI* pKelthuzadAI = CAST_AI(boss_kelthuzad::boss_kelthuzadAI, pKelthuzad->AI()); - if (!pKelthuzadAI) - return false; + void Reset() override + { + } - pKelthuzadAI->AttackStart(player); - if (GameObject* trigger = ObjectAccessor::GetGameObject(*player, instance->GetGuidData(DATA_KELTHUZAD_TRIGGER))) + void EnterEvadeMode(EvadeReason why) override { - if (trigger->getLootState() == GO_READY) - trigger->UseDoorOrButton(); + ScriptedAI::EnterEvadeMode(why); + if (Creature* kelThuzad = ObjectAccessor::GetCreature(*me, instance->GetGuidData(DATA_KELTHUZAD))) + kelThuzad->AI()->EnterEvadeMode(EVADE_REASON_OTHER); + } - // Note: summon must be done by trigger and not by KT. - // Otherwise, they attack immediately as KT is in combat. - for (uint8 i = 0; i < MAX_ABOMINATIONS; ++i) + void EnterCombat(Unit* who) override + { + _movementTimer = 0; // once it's zero, it'll never get checked again + if (!me->HasReactState(REACT_PASSIVE)) { - if (Creature* sum = trigger->SummonCreature(NPC_ABOMINATION, PosAbominations[i])) - { - pKelthuzadAI->spawns.Summon(sum); - sum->GetMotionMaster()->MoveRandom(9.0f); - sum->SetReactState(REACT_DEFENSIVE); - } + DoZoneInCombat(); + return; } - for (uint8 i = 0; i < MAX_WASTES; ++i) - { - if (Creature* sum = trigger->SummonCreature(NPC_WASTE, PosWastes[i])) + + std::list<Creature*> others; + me->GetCreatureListWithEntryInGrid(others, me->GetEntry(), 80.0f); + for (Creature* other : others) + if (other->AI()->GetData(DATA_MINION_POCKET_ID) == pocketId) { - pKelthuzadAI->spawns.Summon(sum); - sum->GetMotionMaster()->MoveRandom(5.0f); - sum->SetReactState(REACT_DEFENSIVE); + other->SetReactState(REACT_AGGRESSIVE); + other->AI()->AttackStart(who); } + me->SetReactState(REACT_AGGRESSIVE); + AttackStart(who); + ScriptedAI::EnterCombat(who); + } + + void AttackStart(Unit* who) override + { + ScriptedAI::AttackStart(who); + } + + void MoveInLineOfSight(Unit* who) override + { + if (!me->HasReactState(REACT_PASSIVE)) + { + ScriptedAI::MoveInLineOfSight(who); + return; } - for (uint8 i = 0; i < MAX_WEAVERS; ++i) + + if (me->CanStartAttack(who, false) && me->GetDistance2d(who) <= MINION_AGGRO_DISTANCE) + EnterCombat(who); + } + + void SetData(uint32 data, uint32 value) override + { + if (data == DATA_MINION_POCKET_ID) + pocketId = value; + } + + uint32 GetData(uint32 data) const override + { + if (data == DATA_MINION_POCKET_ID) + return pocketId; + return 0; + } + + void MovementInform(uint32 /*type*/, uint32 id) override + { + if (id == MOVEMENT_MINION_RANDOM) + _movementTimer = urandms(2, 10) + urandms(2, 10); + } + + void UpdateRandomMovement(uint32 diff) + { + if (!_movementTimer) + return; + + if (_movementTimer <= diff) { - if (Creature* sum = trigger->SummonCreature(NPC_WEAVER, PosWeavers[i])) - { - pKelthuzadAI->spawns.Summon(sum); - sum->GetMotionMaster()->MoveRandom(9.0f); - sum->SetReactState(REACT_DEFENSIVE); - } + _movementTimer = 0; + me->GetMotionMaster()->MovePoint(MOVEMENT_MINION_RANDOM, GetRandomPositionOnCircle(_home, 3.0f)); } + else + _movementTimer -= diff; } - return true; + protected: + InstanceScript* const instance; + uint32 pocketId; + + private: + uint32 _movementTimer; + Position const _home; +}; + +class npc_kelthuzad_skeleton : public CreatureScript +{ +public: + npc_kelthuzad_skeleton() : CreatureScript("npc_kelthuzad_skeleton") { } + + struct npc_kelthuzad_skeletonAI : public npc_kelthuzad_minionAI + { + npc_kelthuzad_skeletonAI(Creature* creature) : npc_kelthuzad_minionAI(creature) { } + + void UpdateAI(uint32 diff) override + { + UpdateRandomMovement(diff); + + if (!UpdateVictim()) + return; + + DoMeleeAttackIfReady(); + } + }; + + CreatureAI* GetAI(Creature* creature) const override + { + return GetInstanceAI<npc_kelthuzad_skeletonAI>(creature); + } +}; + +class npc_kelthuzad_banshee : public CreatureScript +{ +public: + npc_kelthuzad_banshee() : CreatureScript("npc_kelthuzad_banshee") { } + + struct npc_kelthuzad_bansheeAI : public npc_kelthuzad_minionAI + { + npc_kelthuzad_bansheeAI(Creature* creature) : npc_kelthuzad_minionAI(creature) { } + + void UpdateAI(uint32 diff) override + { + UpdateRandomMovement(diff); + + if (!UpdateVictim()) + return; + + DoMeleeAttackIfReady(); + } + }; + + CreatureAI* GetAI(Creature* creature) const override + { + return GetInstanceAI<npc_kelthuzad_bansheeAI>(creature); } }; class npc_kelthuzad_abomination : public CreatureScript { - public: - npc_kelthuzad_abomination() : CreatureScript("npc_kelthuzad_abomination") { } +public: + npc_kelthuzad_abomination() : CreatureScript("npc_kelthuzad_abomination") { } + + struct npc_kelthuzad_abominationAI : public npc_kelthuzad_minionAI + { + npc_kelthuzad_abominationAI(Creature* creature) : npc_kelthuzad_minionAI(creature), _woundTimer(urandms(10, 20)) { } - struct npc_kelthuzad_abominationAI : public ScriptedAI + void UpdateAI(uint32 diff) override { - npc_kelthuzad_abominationAI(Creature* creature) : ScriptedAI(creature) + UpdateRandomMovement(diff); + + if (!UpdateVictim()) + return; + + if (_woundTimer <= diff) + { + _woundTimer = urandms(14, 18); + DoCastVictim(SPELL_MORTAL_WOUND); + } + else + _woundTimer -= diff; + + DoMeleeAttackIfReady(); + } + + void JustDied(Unit* killer) override + { + if (Creature* kelThuzad = ObjectAccessor::GetCreature(*me, instance->GetGuidData(DATA_KELTHUZAD))) + kelThuzad->AI()->DoAction(ACTION_ABOMINATION_DIED); + npc_kelthuzad_minionAI::JustDied(killer); + } + + uint32 _woundTimer; + }; + + CreatureAI* GetAI(Creature* creature) const override + { + return GetInstanceAI<npc_kelthuzad_abominationAI>(creature); + } +}; + +class npc_kelthuzad_guardian : public CreatureScript +{ +public: + npc_kelthuzad_guardian() : CreatureScript("npc_kelthuzad_guardian") { } + + struct npc_kelthuzad_guardianAI : public ScriptedAI + { + public: + npc_kelthuzad_guardianAI(Creature* creature) : ScriptedAI(creature), instance(creature->GetInstanceScript()), _visibilityTimer(0), _bloodTapTimer(0) { } + + void DoAction(int32 action) override + { + switch (action) + { + case ACTION_JUST_SUMMONED: + me->SetVisible(false); + me->SetHomePosition(me->GetPosition()); + DoZoneInCombat(); + me->SetCombatPulseDelay(5); + _visibilityTimer = 2 * IN_MILLISECONDS; + _bloodTapTimer = 25 * IN_MILLISECONDS; + break; + case ACTION_KELTHUZAD_DIED: + Talk(EMOTE_GUARDIAN_FLEE); + me->SetReactState(REACT_PASSIVE); + me->RemoveAllAuras(); + me->CombatStop(); + me->StopMoving(); + me->SetFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_IMMUNE_TO_PC); + me->DespawnOrUnsummon(30 * IN_MILLISECONDS); // just in case anything interrupts the movement + me->GetMotionMaster()->MoveTargetedHome(); + default: + break; + } + } + + void EnterEvadeMode(EvadeReason why) override { - _instance = creature->GetInstanceScript(); + if (Creature* kelthuzad = ObjectAccessor::GetCreature(*me, instance->GetGuidData(DATA_KELTHUZAD))) + kelthuzad->AI()->EnterEvadeMode(); + ScriptedAI::EnterEvadeMode(why); + } + + void JustReachedHome() override + { + me->DespawnOrUnsummon(); } void Reset() override { - _events.Reset(); - _events.ScheduleEvent(EVENT_MORTAL_WOUND, urand(2000, 5000)); - DoCast(me, SPELL_FRENZY, true); + me->SetCombatPulseDelay(0); + ScriptedAI::Reset(); } void UpdateAI(uint32 diff) override { + if (_visibilityTimer) + { + if (diff > _visibilityTimer) + _visibilityTimer -= diff; + else + { + me->SetVisible(true); + Talk(EMOTE_GUARDIAN_APPEAR); + _visibilityTimer = 0; + } + } + if (!UpdateVictim()) return; - _events.Update(diff); - - while (uint32 eventId = _events.ExecuteEvent()) + if (_bloodTapTimer <= diff) { - switch (eventId) - { - case EVENT_MORTAL_WOUND: - DoCastVictim(SPELL_MORTAL_WOUND, true); - _events.ScheduleEvent(EVENT_MORTAL_WOUND, urand(10000, 15000)); - break; - default: - break; - } + DoCastVictim(SPELL_BLOOD_TAP); + _bloodTapTimer = urandms(18, 26); } - } + else + _bloodTapTimer -= diff; - void JustDied(Unit* /*killer*/) override - { - _instance->SetData(DATA_ABOMINATION_KILLED, _instance->GetData(DATA_ABOMINATION_KILLED) + 1); + DoMeleeAttackIfReady(); } private: - InstanceScript* _instance; - EventMap _events; - }; + InstanceScript* const instance; + uint32 _visibilityTimer; + uint32 _bloodTapTimer; + }; + + CreatureAI* GetAI(Creature* creature) const override + { + return GetInstanceAI<npc_kelthuzad_guardianAI>(creature); + } +}; + +class spell_kelthuzad_chains : public SpellScriptLoader +{ +public: + spell_kelthuzad_chains() : SpellScriptLoader("spell_kelthuzad_chains") { } + + class spell_kelthuzad_chains_AuraScript : public AuraScript + { + PrepareAuraScript(spell_kelthuzad_chains_AuraScript); + + void HandleApply(AuraEffect const* /*eff*/, AuraEffectHandleModes /*mode*/) + { + Unit* target = GetTarget(); + float scale = target->GetObjectScale(); + ApplyPercentModFloatVar(scale, 200.0f, true); + target->SetObjectScale(scale); + } + + void HandleRemove(AuraEffect const* /*eff*/, AuraEffectHandleModes /*mode*/) + { + Unit* target = GetTarget(); + float scale = target->GetObjectScale(); + ApplyPercentModFloatVar(scale, 200.0f, false); + target->SetObjectScale(scale); + } - CreatureAI* GetAI(Creature* creature) const override + void Register() override { - return GetInstanceAI<npc_kelthuzad_abominationAI>(creature); + AfterEffectApply += AuraEffectApplyFn(spell_kelthuzad_chains_AuraScript::HandleApply, EFFECT_0, SPELL_AURA_AOE_CHARM, AURA_EFFECT_HANDLE_REAL); + AfterEffectRemove += AuraEffectApplyFn(spell_kelthuzad_chains_AuraScript::HandleRemove, EFFECT_0, SPELL_AURA_AOE_CHARM, AURA_EFFECT_HANDLE_REAL); } + }; + + AuraScript* GetAuraScript() const override + { + return new spell_kelthuzad_chains_AuraScript(); + } }; class spell_kelthuzad_detonate_mana : public SpellScriptLoader { - public: - spell_kelthuzad_detonate_mana() : SpellScriptLoader("spell_kelthuzad_detonate_mana") { } - - class spell_kelthuzad_detonate_mana_AuraScript : public AuraScript - { - PrepareAuraScript(spell_kelthuzad_detonate_mana_AuraScript); +public: + spell_kelthuzad_detonate_mana() : SpellScriptLoader("spell_kelthuzad_detonate_mana") { } - bool Validate(SpellInfo const* /*spell*/) override - { - if (!sSpellMgr->GetSpellInfo(SPELL_MANA_DETONATION_DAMAGE)) - return false; - return true; - } + class spell_kelthuzad_detonate_mana_AuraScript : public AuraScript + { + PrepareAuraScript(spell_kelthuzad_detonate_mana_AuraScript); - void HandleScript(AuraEffect const* aurEff) - { - PreventDefaultAction(); + bool Validate(SpellInfo const* /*spell*/) override + { + if (!sSpellMgr->GetSpellInfo(SPELL_MANA_DETONATION_DAMAGE)) + return false; + return true; + } - Unit* target = GetTarget(); - if (int32 mana = int32(target->GetMaxPower(POWER_MANA) / 10)) - { - mana = target->ModifyPower(POWER_MANA, -mana); - target->CastCustomSpell(SPELL_MANA_DETONATION_DAMAGE, SPELLVALUE_BASE_POINT0, -mana * 10, target, true, NULL, aurEff); - } - } + void HandleScript(AuraEffect const* aurEff) + { + PreventDefaultAction(); - void Register() override + Unit* target = GetTarget(); + if (int32 mana = int32(target->GetMaxPower(POWER_MANA) / 10)) { - OnEffectPeriodic += AuraEffectPeriodicFn(spell_kelthuzad_detonate_mana_AuraScript::HandleScript, EFFECT_0, SPELL_AURA_PERIODIC_TRIGGER_SPELL); + mana = target->ModifyPower(POWER_MANA, -mana); + target->CastCustomSpell(SPELL_MANA_DETONATION_DAMAGE, SPELLVALUE_BASE_POINT0, -mana * 10, target, true, NULL, aurEff); } - }; + } - AuraScript* GetAuraScript() const override + void Register() override { - return new spell_kelthuzad_detonate_mana_AuraScript(); + OnEffectPeriodic += AuraEffectPeriodicFn(spell_kelthuzad_detonate_mana_AuraScript::HandleScript, EFFECT_0, SPELL_AURA_PERIODIC_TRIGGER_SPELL); } + }; + + AuraScript* GetAuraScript() const override + { + return new spell_kelthuzad_detonate_mana_AuraScript(); + } +}; + +class at_kelthuzad_center : public AreaTriggerScript +{ +public: + at_kelthuzad_center() : AreaTriggerScript("at_kelthuzad_center") { } + + bool OnTrigger(Player* player, const AreaTriggerEntry* /*at*/, bool entered) override + { + InstanceScript* instance = player->GetInstanceScript(); + if (!instance || instance->GetBossState(BOSS_KELTHUZAD) != NOT_STARTED || !entered) + return true; + + if (player->IsGameMaster()) + return true; + + Creature* kelThuzad = ObjectAccessor::GetCreature(*player, instance->GetGuidData(DATA_KELTHUZAD)); + if (!kelThuzad) + return true; + + kelThuzad->AI()->DoAction(ACTION_BEGIN_ENCOUNTER); + + return true; + } }; class achievement_just_cant_get_enough : public AchievementCriteriaScript { public: - achievement_just_cant_get_enough() : AchievementCriteriaScript("achievement_just_cant_get_enough") { } + achievement_just_cant_get_enough() : AchievementCriteriaScript("achievement_just_cant_get_enough") { } - bool OnCheck(Player* /*player*/, Unit* target) override - { - if (!target) - return false; + bool OnCheck(Player* /*player*/, Unit* target) override + { + if (!target) + return false; - if (InstanceScript* instance = target->GetInstanceScript()) - if (instance->GetData(DATA_ABOMINATION_KILLED) >= 18) - return true; + if (InstanceScript* instance = target->GetInstanceScript()) + if (Creature* kelThuzad = ObjectAccessor::GetCreature(*target, instance->GetGuidData(DATA_KELTHUZAD))) + if (kelThuzad->AI()->GetData(DATA_ABOMINATION_DEATH_COUNT) >= 18) + return true; - return false; - } + return false; + } }; void AddSC_boss_kelthuzad() { new boss_kelthuzad(); - new at_kelthuzad_center(); + new npc_kelthuzad_skeleton(); + new npc_kelthuzad_banshee(); new npc_kelthuzad_abomination(); + new npc_kelthuzad_guardian(); + new spell_kelthuzad_chains(); new spell_kelthuzad_detonate_mana(); + new at_kelthuzad_center(); new achievement_just_cant_get_enough(); } diff --git a/src/server/scripts/Northrend/Naxxramas/instance_naxxramas.cpp b/src/server/scripts/Northrend/Naxxramas/instance_naxxramas.cpp index ff8708a1a05..8c3eb42979f 100644 --- a/src/server/scripts/Northrend/Naxxramas/instance_naxxramas.cpp +++ b/src/server/scripts/Northrend/Naxxramas/instance_naxxramas.cpp @@ -97,6 +97,7 @@ ObjectData const objectData[] = { GO_NAXX_PORTAL_CONSTRUCT, DATA_NAXX_PORTAL_CONSTRUCT }, { GO_NAXX_PORTAL_PLAGUE, DATA_NAXX_PORTAL_PLAGUE }, { GO_NAXX_PORTAL_MILITARY, DATA_NAXX_PORTAL_MILITARY }, + { GO_KELTHUZAD_THRONE, DATA_KELTHUZAD_THRONE }, { 0, 0, } }; @@ -115,7 +116,6 @@ class instance_naxxramas : public InstanceMapScript LoadDoorData(doorData); LoadObjectData(nullptr, objectData); - AbominationCount = 0; hadAnubRekhanGreet = false; hadFaerlinaGreet = false; hadThaddiusGreet = false; @@ -230,6 +230,10 @@ class instance_naxxramas : public InstanceMapScript if (GetBossState(BOSS_HORSEMEN) == DONE) go->RemoveFlag(GAMEOBJECT_FLAGS, GO_FLAG_NOT_SELECTABLE); break; + case GO_KELTHUZAD_THRONE: + if (GetBossState(BOSS_KELTHUZAD) == DONE) + go->RemoveFlag(GAMEOBJECT_FLAGS, GO_FLAG_NOT_SELECTABLE); + break; case GO_BIRTH: if (hadSapphironBirth || GetBossState(BOSS_SAPPHIRON) == DONE) { @@ -270,9 +274,6 @@ class instance_naxxramas : public InstanceMapScript if (GameObject* gate = instance->GetGameObject(GothikGateGUID)) gate->SetGoState(GOState(value)); break; - case DATA_ABOMINATION_KILLED: - AbominationCount = value; - break; case DATA_HAD_ANUBREKHAN_GREET: hadAnubRekhanGreet = (value == 1u); break; @@ -294,8 +295,6 @@ class instance_naxxramas : public InstanceMapScript { switch (id) { - case DATA_ABOMINATION_KILLED: - return AbominationCount; case DATA_HAD_ANUBREKHAN_GREET: return hadAnubRekhanGreet ? 1u : 0u; case DATA_HAD_FAERLINA_GREET: @@ -418,6 +417,12 @@ class instance_naxxramas : public InstanceMapScript case BOSS_SAPPHIRON: if (state == DONE) events.ScheduleEvent(EVENT_DIALOGUE_SAPPHIRON_KELTHUZAD, Seconds(6)); + HandleGameObject(KelthuzadDoorGUID, false); + break; + case BOSS_KELTHUZAD: + if (state == DONE) + if (GameObject* throne = GetGameObject(DATA_KELTHUZAD_THRONE)) + throne->RemoveFlag(GAMEOBJECT_FLAGS, GO_FLAG_NOT_SELECTABLE); break; default: break; @@ -493,7 +498,6 @@ class instance_naxxramas : public InstanceMapScript case EVENT_DIALOGUE_SAPPHIRON_KELTHUZAD: if (Creature* kelthuzad = instance->GetCreature(KelthuzadGUID)) kelthuzad->AI()->Talk(SAY_DIALOGUE_SAPPHIRON_KELTHUZAD); - HandleGameObject(KelthuzadDoorGUID, false); events.ScheduleEvent(EVENT_DIALOGUE_SAPPHIRON_LICHKING, Seconds(6)); break; case EVENT_DIALOGUE_SAPPHIRON_LICHKING: @@ -614,7 +618,6 @@ class instance_naxxramas : public InstanceMapScript ObjectGuid PortalsGUID[4]; ObjectGuid KelthuzadDoorGUID; ObjectGuid LichKingGUID; - uint8 AbominationCount; bool hadAnubRekhanGreet; bool hadFaerlinaGreet; bool hadThaddiusGreet; diff --git a/src/server/scripts/Northrend/Naxxramas/naxxramas.h b/src/server/scripts/Northrend/Naxxramas/naxxramas.h index c5521175d54..e847179ddeb 100644 --- a/src/server/scripts/Northrend/Naxxramas/naxxramas.h +++ b/src/server/scripts/Northrend/Naxxramas/naxxramas.h @@ -50,12 +50,12 @@ enum NAXData DATA_HAD_SAPPHIRON_BIRTH, DATA_HORSEMEN_CHECK_ACHIEVEMENT_CREDIT, - DATA_ABOMINATION_KILLED, DATA_NAXX_PORTAL_ARACHNID, DATA_NAXX_PORTAL_CONSTRUCT, DATA_NAXX_PORTAL_PLAGUE, - DATA_NAXX_PORTAL_MILITARY + DATA_NAXX_PORTAL_MILITARY, + DATA_KELTHUZAD_THRONE }; enum NAXData64 @@ -121,6 +121,7 @@ enum NAXGameObjectsIds GO_KELTHUZAD_PORTAL03 = 181404, GO_KELTHUZAD_PORTAL04 = 181405, GO_KELTHUZAD_TRIGGER = 181444, + GO_KELTHUZAD_THRONE = 181640, GO_ROOM_ANUBREKHAN = 181126, GO_PASSAGE_ANUBREKHAN = 181195, GO_PASSAGE_FAERLINA = 194022, @@ -138,14 +139,20 @@ enum NAXGameObjectsIds GO_ROOM_HORSEMEN = 181119, GO_PASSAGE_SAPPHIRON = 181225, GO_ROOM_KELTHUZAD = 181228, + + // End of wing portals GO_ARAC_PORTAL = 181575, GO_PLAG_PORTAL = 181577, GO_MILI_PORTAL = 181578, GO_CONS_PORTAL = 181576, + + // "Glow" effect on center-side portal GO_ARAC_EYE_RAMP = 181212, GO_PLAG_EYE_RAMP = 181211, GO_MILI_EYE_RAMP = 181210, GO_CONS_EYE_RAMP = 181213, + + // "Glow" effect on boss-side portal GO_ARAC_EYE_RAMP_BOSS = 181233, GO_PLAG_EYE_RAMP_BOSS = 181231, GO_MILI_EYE_RAMP_BOSS = 181230, |
