diff options
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, |
