From 5b04ee546927ff28bc2f3ee765de623a64eafdc3 Mon Sep 17 00:00:00 2001 From: EricksOliveira Date: Wed, 16 Sep 2026 03:55:01 -0300 Subject: [PATCH] =?UTF-8?q?feat(Scripts/ICC):=20script=20the=20Deathbringe?= =?UTF-8?q?r=20Saurfang=20outro=20and=20victory=E2=80=A6=20(#27112)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Skjalf <47818697+Nyeriah@users.noreply.github.com> --- .../rev_1786448115604582700.sql | 13 + .../boss_deathbringer_saurfang.cpp | 613 +++++++++++++++--- .../IcecrownCitadel/icecrown_citadel.h | 26 + .../instance_icecrown_citadel.cpp | 457 ++++++++++++- 4 files changed, 1010 insertions(+), 99 deletions(-) create mode 100644 data/sql/updates/pending_db_world/rev_1786448115604582700.sql diff --git a/data/sql/updates/pending_db_world/rev_1786448115604582700.sql b/data/sql/updates/pending_db_world/rev_1786448115604582700.sql new file mode 100644 index 0000000000..f3d73c2ae7 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1786448115604582700.sql @@ -0,0 +1,13 @@ +-- Deathbringer Saurfang outro: the camp props only spawned on 10N/25N. +UPDATE `gameobject` SET `spawnMask` = 15 WHERE `id` IN (1685, 191345, 187852) AND `guid` IN (12363, 55882, 81649); + +-- Grip of Agony hangs Muradin/High Overlord and their guards in the air, and the script owns the +-- disable-gravity flag for that. Without 512 (CREATURE_FLAG_EXTRA_NO_MOVE_FLAGS_UPDATE) the core +-- clears the flag again on the next terrain update and they drop back to the floor mid-intro. +UPDATE `creature_template` SET `flags_extra` = `flags_extra`|512 WHERE `entry` IN (37187, 37200, 37830, 37920); + +-- Two emote beats of the Alliance outro that have no creature_text row. +DELETE FROM `creature_text` WHERE (`CreatureID` = 37187 AND `GroupID` = 19) OR (`CreatureID` = 37188 AND `GroupID` = 2); +INSERT INTO `creature_text` (`CreatureID`, `GroupID`, `ID`, `Text`, `Type`, `Language`, `Probability`, `Emote`, `Duration`, `Sound`, `BroadcastTextId`, `TextRange`, `comment`) VALUES +(37187, 19, 0, '%s nods.', 16, 0, 0, 273, 0, 0, 37531, 0, 'High Overlord Saurfang - SAY_OUTRO_ALLIANCE_SAURFANG_NOD'), +(37188, 2, 0, '%s wipes her tears away and smiles at King Varian Wrynn.', 16, 0, 0, 0, 0, 0, 37544, 0, 'Lady Jaina Proudmoore - SAY_OUTRO_ALLIANCE_JAINA_SMILE'); diff --git a/src/server/scripts/Northrend/IcecrownCitadel/boss_deathbringer_saurfang.cpp b/src/server/scripts/Northrend/IcecrownCitadel/boss_deathbringer_saurfang.cpp index ad895c5648..406ce838ff 100644 --- a/src/server/scripts/Northrend/IcecrownCitadel/boss_deathbringer_saurfang.cpp +++ b/src/server/scripts/Northrend/IcecrownCitadel/boss_deathbringer_saurfang.cpp @@ -61,12 +61,13 @@ enum ScriptTexts SAY_OUTRO_HORDE_2 = 12, SAY_OUTRO_HORDE_3 = 13, SAY_OUTRO_HORDE_4 = 14, + SAY_OUTRO_ALLIANCE_SAURFANG_NOD = 19, // emote beat, outside the numbered 21 // Muradin Bronzebeard SAY_INTRO_ALLIANCE_1 = 0, SAY_INTRO_ALLIANCE_4 = 1, SAY_INTRO_ALLIANCE_5 = 2, - SAY_OUTRO_ALLIANCE_1 = 3, /// @todo ALLIANCE OUTRO + SAY_OUTRO_ALLIANCE_1 = 3, SAY_OUTRO_ALLIANCE_2 = 4, SAY_OUTRO_ALLIANCE_3 = 5, SAY_OUTRO_ALLIANCE_4 = 6, @@ -80,6 +81,7 @@ enum ScriptTexts // Lady Jaina Proudmoore SAY_OUTRO_ALLIANCE_17 = 0, SAY_OUTRO_ALLIANCE_19 = 1, + SAY_OUTRO_ALLIANCE_JAINA_SMILE = 2, // emote beat, outside the numbered 21 // King Varian Wrynn SAY_OUTRO_ALLIANCE_11 = 0, @@ -167,13 +169,22 @@ enum EventTypes EVENT_OUTRO_ALLIANCE_18 = 40, EVENT_OUTRO_ALLIANCE_19 = 41, EVENT_OUTRO_ALLIANCE_20 = 42, - EVENT_OUTRO_ALLIANCE_21 = 43, EVENT_OUTRO_HORDE_1 = 44, - EVENT_OUTRO_HORDE_2 = 45, EVENT_OUTRO_HORDE_3 = 46, EVENT_OUTRO_HORDE_4 = 47, - EVENT_OUTRO_HORDE_5 = 48, + EVENT_OUTRO_HORDE_GUARDS_KNEEL = 49, + EVENT_OUTRO_HORDE_PICKUP = 50, + EVENT_OUTRO_HORDE_LEAVE = 51, + EVENT_OUTRO_A_GUARDS_KNEEL = 52, + EVENT_OUTRO_A_SAURFANG_NOD = 53, + EVENT_OUTRO_A_JAINA_SMILE = 54, + EVENT_OUTRO_A_SAURFANG_LEAVE = 55, + EVENT_OUTRO_A_DISMISS = 56, + EVENT_OUTRO_A_ROYALS = 57, + EVENT_OUTRO_A_PORTAL_CLOSE = 58, + EVENT_OUTRO_A_SAURFANG_ADVANCE = 59, + EVENT_OUTRO_A_ZEPPELIN_RELEASE = 60, }; enum Phases @@ -191,11 +202,16 @@ enum Actions ACTION_CONTINUE_INTRO = -3781301, ACTION_CHARGE = -3781302, ACTION_START_OUTRO = -3781303, - ACTION_DESPAWN = -3781304, ACTION_INTRO_DONE = -3781305, ACTION_EVADE = -3781306, ACTION_GAIN_SCENT_OF_BLOOD = -3781307, ACTION_MARK_OF_THE_FALLEN_CHAMPION = -72293, + ACTION_READY_WEAPONS = -3781308, + ACTION_OUTRO_DESCEND = -3781309, + ACTION_OUTRO_KNEEL = -3781310, + ACTION_OUTRO_RETREAT = -3781311, + ACTION_OUTRO_FALL_IN = -3781312, + ACTION_OUTRO_STAND_DOWN = -3781313, }; #define DATA_MADE_A_MESS 45374613 // 4537, 4613 are achievement IDs @@ -209,6 +225,15 @@ enum MovePoints POINT_CHOKE = 3781303, POINT_CORPSE = 3781304, POINT_FINAL = 3781305, + POINT_TRANSPORTER = 3781306, + POINT_RETREAT = 3781307, + POINT_A_MURADIN_STAND = 3781308, + POINT_A_SAURFANG_MEET = 3781309, + POINT_A_CORPSE = 3781310, + POINT_A_VARIAN = 3781311, + POINT_A_EXIT = 3781312, + POINT_A_STAND_DOWN = 3781313, + POINT_A_MURADIN_HOME = 3781314, }; Position const deathbringerPos = {-496.3542f, 2211.33f, 541.1138f, 0.0f}; @@ -224,17 +249,35 @@ Position const chargePos[6] = {-509.0040f, 2211.743f, 539.2870f, 0.0f} // back right }; +// Grip of Agony hangs the whole group at one height, 12 yards over the floor at 539.287. Position const chokePos[6] = { - {-514.4834f, 2211.334f, 549.2887f, 0.0f}, // High Overlord Saurfang/Muradin Bronzebeard - {-510.1081f, 2211.592f, 546.3773f, 0.0f}, // front left + {-514.4834f, 2211.334f, 551.2882f, 0.0f}, // High Overlord Saurfang/Muradin Bronzebeard + {-510.1081f, 2211.592f, 551.2882f, 0.0f}, // front left {-513.3210f, 2211.396f, 551.2882f, 0.0f}, // front right - {-507.3684f, 2210.353f, 545.7497f, 0.0f}, // back middle - {-507.0486f, 2212.999f, 545.5512f, 0.0f}, // back left - {-510.7041f, 2211.069f, 546.5298f, 0.0f} // back right + {-507.3684f, 2210.353f, 551.2882f, 0.0f}, // back middle + {-507.0486f, 2212.999f, 551.2882f, 0.0f}, // back left + {-510.7041f, 2211.069f, 551.2882f, 0.0f} // back right }; -//Position const finalPos = {-563.7552f, 2211.328f, 538.7848f, 0.0f}; +Position const finalPos = {-563.7552f, 2211.328f, 538.7848f, 0.0f}; +Position const transporterPos = {-549.0735f, 2211.289f, 539.2917f, 3.1415927f}; + +Position const allianceSaurfangPos = {-521.8657f, 2250.3455f, 539.29240f, 5.2911840f}; +Position const allianceMuradinPos = {-518.2308f, 2232.5356f, 539.29095f, 1.8000813f}; +Position const allianceVarianPos = {-521.0001f, 2238.3192f, 539.29095f, 5.6681833f}; +Position const allianceJainaPos = {-522.2058f, 2236.3171f, 539.29095f, 5.7388660f}; +Position const allianceSaurfangMeetPos = {-519.0500f, 2236.4000f, 539.29095f, 1.8000813f + 3.1415927f}; +Position const allianceVarianMeetPos = {-518.5498f, 2236.5883f, 539.29095f, 2.5265906f}; +float const allianceGuardOffset[6][2] = +{ + { 0.0f, 0.0f}, + { 2.6f, -1.4f}, + { 3.4f, 1.2f}, + { 0.8f, -3.2f}, + {-1.6f, -2.4f}, + {-2.2f, 1.0f} +}; class boss_deathbringer_saurfang : public CreatureScript { @@ -255,6 +298,7 @@ public: me->SetReactState(REACT_DEFENSIVE); events.Reset(); _introDone = false; + _outroStarted = false; _frenzied = false; _fallenChampionCastCount = 0; _transportCheckTimer = 1000; @@ -308,6 +352,7 @@ public: void JustDied(Unit* /*killer*/) override { _JustDied(); + _outroStarted = true; DoCast(me, SPELL_ACHIEVEMENT, true); Talk(SAY_DEATH); @@ -497,6 +542,10 @@ public: void EnterEvadeMode(EvadeReason why) override { + // The outro revives the corpse to carry it, so evading here would roll DONE back to FAIL. + if (_outroStarted) + return; + BossAI::EnterEvadeMode(why); if (Creature* creature = ObjectAccessor::GetCreature(*me, instance->GetGuidData(DATA_SAURFANG_EVENT_NPC))) creature->AI()->DoAction(ACTION_EVADE); @@ -505,6 +554,7 @@ public: private: uint32 _fallenChampionCastCount; bool _introDone; + bool _outroStarted; bool _frenzied; // faster than iterating all auras to find Frenzy uint16 _transportCheckTimer; }; @@ -549,14 +599,17 @@ public: if (!deathbringer || deathbringer->IsInEvadeMode()) return; - if (_guardList.empty()) - { - GetCreatureListWithEntryInGrid(_guardList, me, NPC_SE_KOR_KRON_REAVER, 20.0f); - _guardList.sort(Acore::ObjectDistanceOrderPred(me)); - } + // Rebuilt each run: the guards despawn during the outro. + _guardList.clear(); + std::list guards; + GetCreatureListWithEntryInGrid(guards, me, NPC_SE_KOR_KRON_REAVER, 20.0f); + guards.sort(Acore::ObjectDistanceOrderPred(me)); uint32 x = 1; - for (std::list::iterator itr = _guardList.begin(); itr != _guardList.end(); ++itr) - (*itr)->AI()->SetData(0, x++); + for (Creature* guard : guards) + { + _guardList.push_back(guard->GetGUID()); + guard->AI()->SetData(0, x++); + } me->RemoveNpcFlag(UNIT_NPC_FLAG_GOSSIP); Talk(SAY_INTRO_HORDE_1); @@ -581,13 +634,15 @@ public: me->RemoveAurasDueToSpell(SPELL_GRIP_OF_AGONY); me->SetDisableGravity(false); me->GetMotionMaster()->MovePoint(0, me->GetPositionX(), me->GetPositionY(), 539.2917f, FORCED_MOVEMENT_NONE, 10.0f); - for (std::list::iterator itr = _guardList.begin(); itr != _guardList.end(); ++itr) - (*itr)->AI()->DoAction(ACTION_DESPAWN); + for (ObjectGuid const& guid : _guardList) + if (Creature* guard = ObjectAccessor::GetCreature(*me, guid)) + guard->AI()->DoAction(ACTION_OUTRO_DESCEND); - /*Talk(SAY_OUTRO_HORDE_1); + _instance->SetData(DATA_SAURFANG_CAMP, IN_PROGRESS); + Talk(SAY_OUTRO_HORDE_1); + _events.ScheduleEvent(EVENT_OUTRO_HORDE_GUARDS_KNEEL, 2s + 500ms); _events.ScheduleEvent(EVENT_OUTRO_HORDE_1, 10s); - _events.ScheduleEvent(EVENT_OUTRO_HORDE_2, 18s); - _events.ScheduleEvent(EVENT_OUTRO_HORDE_3, 24s);*/ + _events.ScheduleEvent(EVENT_OUTRO_HORDE_3, 18s); } break; case ACTION_EVADE: @@ -599,14 +654,18 @@ public: me->StopMovingOnCurrentPos(); me->SetDisableGravity(false); EnterEvadeMode(); - for (std::list::iterator itr = _guardList.begin(); itr != _guardList.end(); ++itr) + for (ObjectGuid const& guid : _guardList) { - (*itr)->GetMotionMaster()->Clear(); - (*itr)->GetHomePosition(x, y, z, o); - (*itr)->SetPosition(x, y, z, o); - (*itr)->StopMovingOnCurrentPos(); - (*itr)->SetDisableGravity(false); - (*itr)->AI()->EnterEvadeMode(); + Creature* guard = ObjectAccessor::GetCreature(*me, guid); + if (!guard) + continue; + + guard->GetMotionMaster()->Clear(); + guard->GetHomePosition(x, y, z, o); + guard->SetPosition(x, y, z, o); + guard->StopMovingOnCurrentPos(); + guard->SetDisableGravity(false); + guard->AI()->EnterEvadeMode(); } } break; @@ -633,29 +692,38 @@ public: case POINT_FIRST_STEP: me->SetWalk(false); Talk(SAY_INTRO_HORDE_3); - _events.ScheduleEvent(EVENT_INTRO_HORDE_4, 6500ms, 0, PHASE_INTRO_H); - _events.ScheduleEvent(EVENT_INTRO_HORDE_5, 15s + 500ms, 0, PHASE_INTRO_H); - _events.ScheduleEvent(EVENT_INTRO_HORDE_6, 29s + 500ms, 0, PHASE_INTRO_H); - _events.ScheduleEvent(EVENT_INTRO_HORDE_7, 43s + 800ms, 0, PHASE_INTRO_H); - _events.ScheduleEvent(EVENT_INTRO_HORDE_8, 47s, 0, PHASE_INTRO_H); - _events.ScheduleEvent(EVENT_INTRO_HORDE_9, 48s + 200ms, 0, PHASE_INTRO_H); - _events.ScheduleEvent(EVENT_INTRO_FINISH, 56s + 700ms, 0, PHASE_INTRO_H); + _events.ScheduleEvent(EVENT_INTRO_HORDE_4, 10s, 0, PHASE_INTRO_H); + _events.ScheduleEvent(EVENT_INTRO_HORDE_5, 22s, 0, PHASE_INTRO_H); + _events.ScheduleEvent(EVENT_INTRO_HORDE_6, 36s, 0, PHASE_INTRO_H); + _events.ScheduleEvent(EVENT_INTRO_HORDE_7, 53s, 0, PHASE_INTRO_H); + _events.ScheduleEvent(EVENT_INTRO_HORDE_8, 57s + 800ms, 0, PHASE_INTRO_H); + _events.ScheduleEvent(EVENT_INTRO_HORDE_9, 59s, 0, PHASE_INTRO_H); + _events.ScheduleEvent(EVENT_INTRO_FINISH, 68s, 0, PHASE_INTRO_H); break; - /*case POINT_CORPSE: + case POINT_CORPSE: if (Creature* deathbringer = ObjectAccessor::GetCreature(*me, _instance->GetGuidData(DATA_DEATHBRINGER_SAURFANG))) - { - deathbringer->CastSpell(me, SPELL_RIDE_VEHICLE, true); - deathbringer->SetUnitFlag(UNIT_FLAG_NOT_SELECTABLE); - deathbringer->setDeathState(DeathState::Alive); - } - _events.ScheduleEvent(EVENT_OUTRO_HORDE_4, 1s); - _events.ScheduleEvent(EVENT_OUTRO_HORDE_5, 4s); + me->SetFacingToObject(deathbringer); + me->SetStandState(UNIT_STAND_STATE_KNEEL); + Talk(SAY_OUTRO_HORDE_3); + _events.ScheduleEvent(EVENT_OUTRO_HORDE_PICKUP, 6s); + break; + case POINT_TRANSPORTER: + me->SetFacingTo(0.0f); + Talk(SAY_OUTRO_HORDE_4); + _events.ScheduleEvent(EVENT_OUTRO_HORDE_LEAVE, 8s); break; case POINT_FINAL: - if (Creature* deathbringer = ObjectAccessor::GetCreature(*me, _instance->GetGuidData(DATA_DEATHBRINGER_SAURFANG))) - deathbringer->DespawnOrUnsummon(); - me->DespawnOrUnsummon(); - break;*/ + { + if (Creature* deathbringer = ObjectAccessor::GetCreature(*me, _instance->GetGuidData(DATA_DEATHBRINGER_SAURFANG))) + deathbringer->DespawnOrUnsummon(); + + float x, y, z, o; + me->GetHomePosition(x, y, z, o); + me->SetVisible(false); + me->NearTeleportTo(x, y, z, o); + _instance->SetData(DATA_SAURFANG_CAMP, DONE); + } + break; default: break; } @@ -674,6 +742,9 @@ public: deathbringer->AI()->Talk(SAY_INTRO_HORDE_2); break; case EVENT_INTRO_HORDE_3: + for (ObjectGuid const& guid : _guardList) + if (Creature* guard = ObjectAccessor::GetCreature(*me, guid)) + guard->AI()->DoAction(ACTION_READY_WEAPONS); me->SetWalk(true); me->GetMotionMaster()->MovePoint(POINT_FIRST_STEP, firstStepPos.GetPositionX(), firstStepPos.GetPositionY(), firstStepPos.GetPositionZ()); break; @@ -692,8 +763,9 @@ public: break; case EVENT_INTRO_HORDE_8: Talk(SAY_INTRO_HORDE_8); - for (std::list::iterator itr = _guardList.begin(); itr != _guardList.end(); ++itr) - (*itr)->AI()->DoAction(ACTION_CHARGE); + for (ObjectGuid const& guid : _guardList) + if (Creature* guard = ObjectAccessor::GetCreature(*me, guid)) + guard->AI()->DoAction(ACTION_CHARGE); me->GetMotionMaster()->MoveCharge(chargePos[0].GetPositionX(), chargePos[0].GetPositionY(), chargePos[0].GetPositionZ(), 8.5f, POINT_CHARGE); break; case EVENT_INTRO_HORDE_9: @@ -712,37 +784,55 @@ public: deathbringer->AI()->AttackStart(target); } break; - - /*case EVENT_OUTRO_HORDE_1: - if (Creature* deathbringer = ObjectAccessor::GetCreature(*me, _instance->GetGuidData(DATA_DEATHBRINGER_SAURFANG))) - me->SetFacingToObject(deathbringer); - Talk(SAY_OUTRO_HORDE_2); - break; - case EVENT_OUTRO_HORDE_2: - Talk(SAY_OUTRO_HORDE_3); - break; - case EVENT_OUTRO_HORDE_3: - if (Creature* deathbringer = ObjectAccessor::GetCreature(*me, _instance->GetGuidData(DATA_DEATHBRINGER_SAURFANG))) - { - float x, y, z; - deathbringer->GetClosePoint(x, y, z, deathbringer->GetObjectSize()); - me->SetWalk(true); - me->GetMotionMaster()->MovePoint(POINT_CORPSE, x, y, z); - } - break; - case EVENT_OUTRO_HORDE_4: - me->GetMotionMaster()->MovePoint(POINT_FINAL, finalPos); - break; - case EVENT_OUTRO_HORDE_5: - Talk(SAY_OUTRO_HORDE_4); - break;*/ + case EVENT_OUTRO_HORDE_1: + if (Creature* deathbringer = ObjectAccessor::GetCreature(*me, _instance->GetGuidData(DATA_DEATHBRINGER_SAURFANG))) + me->SetFacingToObject(deathbringer); + Talk(SAY_OUTRO_HORDE_2); + break; + case EVENT_OUTRO_HORDE_GUARDS_KNEEL: + for (ObjectGuid const& guid : _guardList) + if (Creature* guard = ObjectAccessor::GetCreature(*me, guid)) + guard->AI()->DoAction(ACTION_OUTRO_KNEEL); + break; + case EVENT_OUTRO_HORDE_3: + if (Creature* deathbringer = ObjectAccessor::GetCreature(*me, _instance->GetGuidData(DATA_DEATHBRINGER_SAURFANG))) + { + float x, y, z; + deathbringer->GetClosePoint(x, y, z, deathbringer->GetObjectSize()); + me->SetWalk(true); + me->GetMotionMaster()->MovePoint(POINT_CORPSE, x, y, z); + } + break; + case EVENT_OUTRO_HORDE_PICKUP: + if (Creature* deathbringer = ObjectAccessor::GetCreature(*me, _instance->GetGuidData(DATA_DEATHBRINGER_SAURFANG))) + { + // Revive before the cast - Unit::_EnterVehicle refuses a dead passenger. + deathbringer->setDeathState(DeathState::Alive); + deathbringer->SetUnitFlag(UNIT_FLAG_NOT_SELECTABLE); + deathbringer->CastSpell(me, SPELL_RIDE_VEHICLE, true); + } + for (ObjectGuid const& guid : _guardList) + if (Creature* guard = ObjectAccessor::GetCreature(*me, guid)) + guard->AI()->DoAction(ACTION_OUTRO_RETREAT); + _guardList.clear(); + _events.ScheduleEvent(EVENT_OUTRO_HORDE_4, 3s); + break; + case EVENT_OUTRO_HORDE_4: + me->SetStandState(UNIT_STAND_STATE_STAND); + me->SetWalk(true); + me->GetMotionMaster()->MovePoint(POINT_TRANSPORTER, transporterPos); + break; + case EVENT_OUTRO_HORDE_LEAVE: + me->SetWalk(true); + me->GetMotionMaster()->MovePoint(POINT_FINAL, finalPos); + break; } } private: EventMap _events; InstanceScript* _instance; - std::list _guardList; + GuidList _guardList; }; bool OnGossipHello(Player* player, Creature* creature) override @@ -787,11 +877,13 @@ public: npc_muradin_bronzebeard_iccAI(Creature* creature) : ScriptedAI(creature) { _instance = me->GetInstanceScript(); + _outroZeppelinWait = 0; } void Reset() override { _events.Reset(); + _outroZeppelinWait = 0; me->SetNpcFlag(UNIT_NPC_FLAG_GOSSIP); me->SetReactState(REACT_PASSIVE); } @@ -810,17 +902,21 @@ public: if (!deathbringer || deathbringer->IsInEvadeMode()) return; - if (_guardList.empty()) - { - GetCreatureListWithEntryInGrid(_guardList, me, NPC_SE_SKYBREAKER_MARINE, 20.0f); - _guardList.sort(Acore::ObjectDistanceOrderPred(me)); - } + // Rebuilt each run: the guards despawn during the outro. + _guardList.clear(); + std::list guards; + GetCreatureListWithEntryInGrid(guards, me, NPC_SE_SKYBREAKER_MARINE, 20.0f); + guards.sort(Acore::ObjectDistanceOrderPred(me)); uint32 x = 1; - for (std::list::iterator itr = _guardList.begin(); itr != _guardList.end(); ++itr) - (*itr)->AI()->SetData(0, x++); + for (Creature* guard : guards) + { + _guardList.push_back(guard->GetGUID()); + guard->AI()->SetData(0, x++); + } me->RemoveNpcFlag(UNIT_NPC_FLAG_GOSSIP); Talk(SAY_INTRO_ALLIANCE_1); + _outroZeppelinWait = 0; _events.SetPhase(PHASE_INTRO_A); _events.ScheduleEvent(EVENT_INTRO_ALLIANCE_2, 2500ms, 0, PHASE_INTRO_A); _events.ScheduleEvent(EVENT_INTRO_ALLIANCE_3, 20s, 0, PHASE_INTRO_A); @@ -843,10 +939,18 @@ public: me->RemoveAurasDueToSpell(SPELL_GRIP_OF_AGONY); me->SetDisableGravity(false); me->GetMotionMaster()->MovePoint(0, me->GetPositionX(), me->GetPositionY(), 539.2917f, FORCED_MOVEMENT_NONE, 10.0f); - for (std::list::iterator itr = _guardList.begin(); itr != _guardList.end(); ++itr) - (*itr)->AI()->DoAction(ACTION_DESPAWN); + for (ObjectGuid const& guid : _guardList) + if (Creature* guard = ObjectAccessor::GetCreature(*me, guid)) + guard->AI()->DoAction(ACTION_OUTRO_DESCEND); - //Talk(SAY_OUTRO_ALLIANCE_1); + _instance->SetData(DATA_SAURFANG_CAMP, IN_PROGRESS); + Talk(SAY_OUTRO_ALLIANCE_1); + _events.ScheduleEvent(EVENT_OUTRO_A_GUARDS_KNEEL, 2s + 500ms); + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_1, 3s); + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_2, 18s); + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_3, 24s); + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_4, 30s); + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_5, 49s); break; } case ACTION_EVADE: @@ -858,14 +962,18 @@ public: me->StopMovingOnCurrentPos(); me->SetDisableGravity(false); EnterEvadeMode(); - for (std::list::iterator itr = _guardList.begin(); itr != _guardList.end(); ++itr) + for (ObjectGuid const& guid : _guardList) { - (*itr)->GetMotionMaster()->Clear(); - (*itr)->GetHomePosition(x, y, z, o); - (*itr)->SetPosition(x, y, z, o); - (*itr)->StopMovingOnCurrentPos(); - (*itr)->SetDisableGravity(false); - (*itr)->AI()->EnterEvadeMode(); + Creature* guard = ObjectAccessor::GetCreature(*me, guid); + if (!guard) + continue; + + guard->GetMotionMaster()->Clear(); + guard->GetHomePosition(x, y, z, o); + guard->SetPosition(x, y, z, o); + guard->StopMovingOnCurrentPos(); + guard->SetDisableGravity(false); + guard->AI()->EnterEvadeMode(); } } break; @@ -897,6 +1005,15 @@ public: _events.ScheduleEvent(EVENT_INTRO_ALLIANCE_7, 9s, 0, PHASE_INTRO_A); _events.ScheduleEvent(EVENT_INTRO_FINISH, 14s, 0, PHASE_INTRO_A); break; + case POINT_A_MURADIN_HOME: + { + float x, y, z, o; + me->GetHomePosition(x, y, z, o); + me->SetFacingTo(o); + me->SetSheath(SHEATH_STATE_UNARMED); + _instance->SetData(DATA_SAURFANG_CAMP, DONE); + } + break; default: break; } @@ -919,13 +1036,17 @@ public: deathbringer->AI()->Talk(SAY_INTRO_ALLIANCE_3); break; case EVENT_INTRO_ALLIANCE_4: + for (ObjectGuid const& guid : _guardList) + if (Creature* guard = ObjectAccessor::GetCreature(*me, guid)) + guard->AI()->DoAction(ACTION_READY_WEAPONS); me->SetWalk(true); me->GetMotionMaster()->MovePoint(POINT_FIRST_STEP, firstStepPos.GetPositionX(), firstStepPos.GetPositionY(), firstStepPos.GetPositionZ()); break; case EVENT_INTRO_ALLIANCE_5: Talk(SAY_INTRO_ALLIANCE_5); - for (std::list::iterator itr = _guardList.begin(); itr != _guardList.end(); ++itr) - (*itr)->AI()->DoAction(ACTION_CHARGE); + for (ObjectGuid const& guid : _guardList) + if (Creature* guard = ObjectAccessor::GetCreature(*me, guid)) + guard->AI()->DoAction(ACTION_CHARGE); me->GetMotionMaster()->MoveCharge(chargePos[0].GetPositionX(), chargePos[0].GetPositionY(), chargePos[0].GetPositionZ(), 8.5f, POINT_CHARGE); break; case EVENT_INTRO_ALLIANCE_6: @@ -950,13 +1071,246 @@ public: deathbringer->AI()->AttackStart(target); } break; + + case EVENT_OUTRO_A_GUARDS_KNEEL: + for (ObjectGuid const& guid : _guardList) + if (Creature* guard = ObjectAccessor::GetCreature(*me, guid)) + guard->AI()->DoAction(ACTION_OUTRO_KNEEL); + break; + case EVENT_OUTRO_ALLIANCE_1: + Talk(SAY_OUTRO_ALLIANCE_2); + break; + case EVENT_OUTRO_ALLIANCE_2: + Talk(SAY_OUTRO_ALLIANCE_3); + break; + case EVENT_OUTRO_ALLIANCE_3: + Talk(SAY_OUTRO_ALLIANCE_4); + _instance->SetData(DATA_SAURFANG_OUTRO_ZEPPELIN, IN_PROGRESS); + break; + case EVENT_OUTRO_ALLIANCE_4: + Talk(SAY_OUTRO_ALLIANCE_5); + me->SetWalk(false); + me->GetMotionMaster()->MovePoint(POINT_A_MURADIN_STAND, allianceMuradinPos); + for (ObjectGuid const& guid : _guardList) + if (Creature* guard = ObjectAccessor::GetCreature(*me, guid)) + guard->AI()->DoAction(ACTION_OUTRO_FALL_IN); + break; + case EVENT_OUTRO_ALLIANCE_5: + if (_instance->GetData(DATA_SAURFANG_OUTRO_ZEPPELIN) != DONE) + { + // Bounded wait for the ship to dock, so a taxi path that never arrives cannot hang the scene. + if (++_outroZeppelinWait <= 60) + { + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_5, 1s); + break; + } + } + + Talk(SAY_OUTRO_ALLIANCE_6); + if (Creature* saurfang = me->SummonCreature(NPC_SE_HIGH_OVERLORD_SAURFANG, allianceSaurfangPos)) + { + _outroSaurfangGUID = saurfang->GetGUID(); + saurfang->SetReactState(REACT_PASSIVE); + saurfang->SetSheath(SHEATH_STATE_MELEE); + saurfang->SetEmoteState(EMOTE_STATE_READY1H); + // He shares npc_high_overlord_saurfangAI, whose Reset() flags him for gossip. + saurfang->RemoveNpcFlag(UNIT_NPC_FLAG_GOSSIP); + } + _events.ScheduleEvent(EVENT_OUTRO_A_SAURFANG_ADVANCE, 3s); + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_6, 31s); + break; + case EVENT_OUTRO_A_SAURFANG_ADVANCE: + if (Creature* saurfang = ObjectAccessor::GetCreature(*me, _outroSaurfangGUID)) + { + saurfang->SetWalk(true); + saurfang->GetMotionMaster()->MovePoint(POINT_A_SAURFANG_MEET, allianceSaurfangMeetPos); + } + break; + case EVENT_OUTRO_ALLIANCE_6: + Talk(SAY_OUTRO_ALLIANCE_7); + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_7, 5s); + break; + case EVENT_OUTRO_ALLIANCE_7: + if (Creature* saurfang = ObjectAccessor::GetCreature(*me, _outroSaurfangGUID)) + saurfang->AI()->Talk(SAY_OUTRO_ALLIANCE_8); + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_8, 7s); + break; + case EVENT_OUTRO_ALLIANCE_8: + Talk(SAY_OUTRO_ALLIANCE_9); + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_9, 15s); + break; + case EVENT_OUTRO_ALLIANCE_9: + Talk(SAY_OUTRO_ALLIANCE_10); + _instance->SetData(DATA_SAURFANG_OUTRO_PORTAL, IN_PROGRESS); + _events.ScheduleEvent(EVENT_OUTRO_A_ROYALS, 3s); + _events.ScheduleEvent(EVENT_OUTRO_A_PORTAL_CLOSE, 9s); + break; + case EVENT_OUTRO_A_ROYALS: + if (Creature* varian = me->SummonCreature(NPC_SE_KING_VARIAN_WRYNN, allianceVarianPos)) + { + _outroVarianGUID = varian->GetGUID(); + varian->SetReactState(REACT_PASSIVE); + } + if (Creature* jaina = me->SummonCreature(NPC_SE_JAINA_PROUDMOORE, allianceJainaPos)) + { + _outroJainaGUID = jaina->GetGUID(); + jaina->SetReactState(REACT_PASSIVE); + } + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_10, 5s); + break; + case EVENT_OUTRO_A_PORTAL_CLOSE: + _instance->SetData(DATA_SAURFANG_OUTRO_PORTAL, DONE); + break; + case EVENT_OUTRO_ALLIANCE_10: + if (Creature* varian = ObjectAccessor::GetCreature(*me, _outroVarianGUID)) + varian->AI()->Talk(SAY_OUTRO_ALLIANCE_11); + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_11, 6s); + break; + case EVENT_OUTRO_ALLIANCE_11: + { + // He carries the rest of the scene; without him, skip to the cleanup. + Creature* saurfang = ObjectAccessor::GetCreature(*me, _outroSaurfangGUID); + if (!saurfang) + { + _events.ScheduleEvent(EVENT_OUTRO_A_DISMISS, 1s); + break; + } + + saurfang->AI()->Talk(SAY_OUTRO_ALLIANCE_12); + saurfang->SetEmoteState(EMOTE_ONESHOT_NONE); + saurfang->SetSheath(SHEATH_STATE_UNARMED); + if (Creature* deathbringer = ObjectAccessor::GetCreature(*me, _instance->GetGuidData(DATA_DEATHBRINGER_SAURFANG))) + { + float x, y, z; + deathbringer->GetClosePoint(x, y, z, deathbringer->GetObjectSize()); + saurfang->SetWalk(true); + saurfang->GetMotionMaster()->MovePoint(POINT_A_CORPSE, x, y, z); + } + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_12, 14s); + break; + } + case EVENT_OUTRO_ALLIANCE_12: + if (Creature* saurfang = ObjectAccessor::GetCreature(*me, _outroSaurfangGUID)) + { + if (Creature* deathbringer = ObjectAccessor::GetCreature(*me, _instance->GetGuidData(DATA_DEATHBRINGER_SAURFANG))) + saurfang->SetFacingToObject(deathbringer); + saurfang->SetStandState(UNIT_STAND_STATE_KNEEL); + saurfang->AI()->Talk(SAY_OUTRO_ALLIANCE_13); + } + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_13, 8s); + break; + case EVENT_OUTRO_ALLIANCE_13: + if (Creature* saurfang = ObjectAccessor::GetCreature(*me, _outroSaurfangGUID)) + { + saurfang->AI()->Talk(SAY_OUTRO_ALLIANCE_14); + if (Creature* deathbringer = ObjectAccessor::GetCreature(*me, _instance->GetGuidData(DATA_DEATHBRINGER_SAURFANG))) + { + // Revive before the cast - Unit::_EnterVehicle refuses a dead passenger. + deathbringer->setDeathState(DeathState::Alive); + deathbringer->SetUnitFlag(UNIT_FLAG_NOT_SELECTABLE); + deathbringer->CastSpell(saurfang, SPELL_RIDE_VEHICLE, true); + } + saurfang->SetStandState(UNIT_STAND_STATE_STAND); + saurfang->SetWalk(true); + saurfang->GetMotionMaster()->MovePoint(POINT_A_VARIAN, allianceVarianMeetPos); + } + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_14, 18s); + break; + case EVENT_OUTRO_ALLIANCE_14: + if (Creature* saurfang = ObjectAccessor::GetCreature(*me, _outroSaurfangGUID)) + saurfang->AI()->Talk(SAY_OUTRO_ALLIANCE_15); + _events.ScheduleEvent(EVENT_OUTRO_A_ZEPPELIN_RELEASE, 6s); + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_15, 9s); + break; + case EVENT_OUTRO_A_ZEPPELIN_RELEASE: + // The transport only pulls away ~24s after release, timed to lift off as he boards. + _instance->SetData(DATA_SAURFANG_OUTRO_ZEPPELIN, DONE); + break; + case EVENT_OUTRO_ALLIANCE_15: + if (Creature* varian = ObjectAccessor::GetCreature(*me, _outroVarianGUID)) + varian->AI()->Talk(SAY_OUTRO_ALLIANCE_16); + _events.ScheduleEvent(EVENT_OUTRO_A_SAURFANG_NOD, 6s); + _events.ScheduleEvent(EVENT_OUTRO_A_SAURFANG_LEAVE, 18s); + break; + case EVENT_OUTRO_A_SAURFANG_NOD: + if (Creature* saurfang = ObjectAccessor::GetCreature(*me, _outroSaurfangGUID)) + saurfang->AI()->Talk(SAY_OUTRO_ALLIANCE_SAURFANG_NOD); + break; + case EVENT_OUTRO_ALLIANCE_16: + if (Creature* jaina = ObjectAccessor::GetCreature(*me, _outroJainaGUID)) + jaina->AI()->Talk(SAY_OUTRO_ALLIANCE_17); + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_17, 4s); + break; + case EVENT_OUTRO_ALLIANCE_17: + if (Creature* varian = ObjectAccessor::GetCreature(*me, _outroVarianGUID)) + varian->AI()->Talk(SAY_OUTRO_ALLIANCE_18); + _events.ScheduleEvent(EVENT_OUTRO_A_JAINA_SMILE, 4s); + break; + case EVENT_OUTRO_A_JAINA_SMILE: + if (Creature* jaina = ObjectAccessor::GetCreature(*me, _outroJainaGUID)) + jaina->AI()->Talk(SAY_OUTRO_ALLIANCE_JAINA_SMILE); + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_18, 4s); + break; + case EVENT_OUTRO_ALLIANCE_18: + if (Creature* jaina = ObjectAccessor::GetCreature(*me, _outroJainaGUID)) + jaina->AI()->Talk(SAY_OUTRO_ALLIANCE_19); + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_19, 6s); + break; + case EVENT_OUTRO_A_SAURFANG_LEAVE: + if (Creature* saurfang = ObjectAccessor::GetCreature(*me, _outroSaurfangGUID)) + { + saurfang->SetWalk(false); + saurfang->GetMotionMaster()->MovePoint(POINT_A_EXIT, allianceSaurfangPos); + saurfang->DespawnOrUnsummon(5s); + // The body goes with him: despawned later it would drop a revived Deathbringer on the rise. + if (Creature* deathbringer = ObjectAccessor::GetCreature(*me, _instance->GetGuidData(DATA_DEATHBRINGER_SAURFANG))) + deathbringer->DespawnOrUnsummon(5s); + } + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_16, 6s); + break; + case EVENT_OUTRO_ALLIANCE_19: + if (Creature* varian = ObjectAccessor::GetCreature(*me, _outroVarianGUID)) + varian->AI()->Talk(SAY_OUTRO_ALLIANCE_20); + _events.ScheduleEvent(EVENT_OUTRO_ALLIANCE_20, 6s); + break; + case EVENT_OUTRO_ALLIANCE_20: + Talk(SAY_OUTRO_ALLIANCE_21); + for (ObjectGuid const& guid : _guardList) + if (Creature* guard = ObjectAccessor::GetCreature(*me, guid)) + guard->AI()->DoAction(ACTION_OUTRO_STAND_DOWN); + _events.ScheduleEvent(EVENT_OUTRO_A_DISMISS, 8s); + break; + case EVENT_OUTRO_A_DISMISS: + { + if (Creature* deathbringer = ObjectAccessor::GetCreature(*me, _instance->GetGuidData(DATA_DEATHBRINGER_SAURFANG))) + deathbringer->DespawnOrUnsummon(); + if (Creature* varian = ObjectAccessor::GetCreature(*me, _outroVarianGUID)) + varian->DespawnOrUnsummon(); + if (Creature* jaina = ObjectAccessor::GetCreature(*me, _outroJainaGUID)) + jaina->DespawnOrUnsummon(); + _instance->SetData(DATA_SAURFANG_OUTRO_PORTAL, DONE); + _instance->SetData(DATA_SAURFANG_OUTRO_ZEPPELIN, DONE); + _outroSaurfangGUID.Clear(); + _outroVarianGUID.Clear(); + _outroJainaGUID.Clear(); + + float x, y, z, o; + me->GetHomePosition(x, y, z, o); + me->SetWalk(false); + me->GetMotionMaster()->MovePoint(POINT_A_MURADIN_HOME, x, y, z); + } + break; } } private: EventMap _events; InstanceScript* _instance; - std::list _guardList; + GuidList _guardList; + ObjectGuid _outroSaurfangGUID; + ObjectGuid _outroVarianGUID; + ObjectGuid _outroJainaGUID; + uint8 _outroZeppelinWait; }; bool OnGossipHello(Player* player, Creature* creature) override @@ -1004,6 +1358,14 @@ public: me->SetReactState(REACT_PASSIVE); } + // The intro draws their weapons, so every reset path has to put them away again. + // _index is left alone: it is their identity, not encounter state. + void Reset() override + { + me->SetSheath(SHEATH_STATE_UNARMED); + me->SetEmoteState(EMOTE_ONESHOT_NONE); + } + void SetData(uint32 type, uint32 data) override { ASSERT(!type && data && data < 6); @@ -1019,15 +1381,70 @@ public: } } + void MovementInform(uint32 type, uint32 id) override + { + if (type != POINT_MOTION_TYPE) + return; + + if (id == POINT_RETREAT) + { + me->SetSheath(SHEATH_STATE_UNARMED); + me->SetVisible(false); + } + else if (id == POINT_A_STAND_DOWN) + { + float x, y, z, o; + me->GetHomePosition(x, y, z, o); + me->SetFacingTo(o); + me->SetSheath(SHEATH_STATE_UNARMED); + } + } + void DoAction(int32 action) override { if (action == ACTION_CHARGE && _index) { + // Drop the guard stance, otherwise the looping emote overrides the run animation. + me->SetEmoteState(EMOTE_ONESHOT_NONE); me->SetWalk(false); me->GetMotionMaster()->MoveCharge(chargePos[_index].GetPositionX(), chargePos[_index].GetPositionY(), chargePos[_index].GetPositionZ(), 13.0f, POINT_CHARGE); } - else if (action == ACTION_DESPAWN) - me->DespawnOrUnsummon(1ms); + else if (action == ACTION_READY_WEAPONS) + { + me->SetSheath(SHEATH_STATE_MELEE); + me->SetEmoteState(EMOTE_STATE_READY1H); + } + else if (action == ACTION_OUTRO_DESCEND) + { + me->RemoveAurasDueToSpell(SPELL_GRIP_OF_AGONY); + me->SetDisableGravity(false); + me->GetMotionMaster()->MovePoint(0, me->GetPositionX(), me->GetPositionY(), 539.2917f, FORCED_MOVEMENT_NONE, 10.0f); + me->SetEmoteState(EMOTE_STATE_READY1H); + } + else if (action == ACTION_OUTRO_FALL_IN) + { + me->SetStandState(UNIT_STAND_STATE_STAND); + me->SetSheath(SHEATH_STATE_MELEE); + me->SetEmoteState(EMOTE_STATE_READY1H); + me->SetWalk(false); + me->GetMotionMaster()->MovePoint(POINT_A_MURADIN_STAND, allianceMuradinPos.GetPositionX() + allianceGuardOffset[_index][0], + allianceMuradinPos.GetPositionY() + allianceGuardOffset[_index][1], allianceMuradinPos.GetPositionZ()); + } + else if (action == ACTION_OUTRO_KNEEL) + { + // Kneeling is a stand state; the combat-ready emote would override it. + me->SetEmoteState(EMOTE_ONESHOT_NONE); + me->SetStandState(UNIT_STAND_STATE_KNEEL); + } + else if (action == ACTION_OUTRO_RETREAT || action == ACTION_OUTRO_STAND_DOWN) + { + me->SetStandState(UNIT_STAND_STATE_STAND); + me->SetEmoteState(EMOTE_ONESHOT_NONE); + me->SetWalk(false); + float x, y, z, o; + me->GetHomePosition(x, y, z, o); + me->GetMotionMaster()->MovePoint(action == ACTION_OUTRO_RETREAT ? POINT_RETREAT : POINT_A_STAND_DOWN, x, y, z); + } } private: diff --git a/src/server/scripts/Northrend/IcecrownCitadel/icecrown_citadel.h b/src/server/scripts/Northrend/IcecrownCitadel/icecrown_citadel.h index 77b95abb46..2ed868cd19 100644 --- a/src/server/scripts/Northrend/IcecrownCitadel/icecrown_citadel.h +++ b/src/server/scripts/Northrend/IcecrownCitadel/icecrown_citadel.h @@ -134,6 +134,9 @@ enum DataTypes DATA_ENEMY_GUNSHIP = 40, DATA_THE_SKYBREAKER = 41, DATA_ORGRIMS_HAMMER = 42, + DATA_SAURFANG_CAMP = 43, + DATA_SAURFANG_OUTRO_ZEPPELIN = 44, + DATA_SAURFANG_OUTRO_PORTAL = 45, // pussywizard: DATA_BUFF_AVAILABLE = 251, @@ -250,6 +253,13 @@ enum CreaturesIds NPC_SE_HIGH_OVERLORD_SAURFANG = 37187, NPC_SE_KOR_KRON_REAVER = 37920, NPC_SE_SKYBREAKER_MARINE = 37830, + // Victory camp raised on Deathbringer's Rise once Saurfang is defeated + NPC_CAMP_ALLIANCE_MASON = 37902, + NPC_CAMP_SHELY_STEELBOWELS = 37903, // Alliance blacksmith + NPC_CAMP_BRAZIE_GETZ = 37904, // Alliance general goods + NPC_CAMP_WARSONG_PEON = 37930, + NPC_CAMP_APOTHECARY_CANDITH_TOMAS = 37935, // Horde general goods + NPC_CAMP_MORGAN_DAYBLAZE = 37936, // Horde blacksmith NPC_FROST_FREEZE_TRAP = 37744, // Festergut @@ -425,6 +435,22 @@ enum GameObjectsIds GO_DEATHBRINGER_S_CACHE_10H = 202238, GO_DEATHBRINGER_S_CACHE_25H = 202241, GO_SCOURGE_TRANSPORTER_SAURFANG = 202244, + // Victory camp. Forge, bonfire and anvil are the Horde camp's and have spawn rows - the script + // only raises them; everything else, the Alliance camp included, it summons. + GO_SAURFANG_CAMP_FORGE = 1685, + GO_SAURFANG_CAMP_BONFIRE = 187852, + GO_SAURFANG_CAMP_ANVIL = 191345, + GO_SAURFANG_CAMP_TENT_A = 201868, + GO_SAURFANG_CAMP_TENT_H1 = 201886, + GO_SAURFANG_CAMP_TENT_H2 = 201887, + GO_SAURFANG_CAMP_TELEPORTER_A = 201858, + GO_SAURFANG_CAMP_TELEPORTER_H = 201880, + GO_SAURFANG_CAMP_BANNER_A = 201869, + GO_SAURFANG_CAMP_ANVIL_A = 23303, // the Alliance camp uses its own anvil + // Zeppelin is a MO_TRANSPORT (taxi path 1834) created through TransportMgr, like the gunships. + // The portal is a SPELLCASTER, spawned non-selectable so players cannot click its spell. + GO_SAURFANG_OUTRO_ZEPPELIN = 201834, + GO_SAURFANG_OUTRO_PORTAL = 193207, // Professor Putricide GO_ORANGE_PLAGUE_MONSTER_ENTRANCE = 201371, diff --git a/src/server/scripts/Northrend/IcecrownCitadel/instance_icecrown_citadel.cpp b/src/server/scripts/Northrend/IcecrownCitadel/instance_icecrown_citadel.cpp index a1fd7e24a8..a520451db0 100644 --- a/src/server/scripts/Northrend/IcecrownCitadel/instance_icecrown_citadel.cpp +++ b/src/server/scripts/Northrend/IcecrownCitadel/instance_icecrown_citadel.cpp @@ -52,7 +52,20 @@ enum TimedEvents EVENT_QUAKE_SHATTER = 2, EVENT_REBUILD_PLATFORM = 3, EVENT_RESPAWN_GUNSHIP = 4, - EVENT_RESPAWN_SINDRAGOSA = 5 + EVENT_RESPAWN_SINDRAGOSA = 5, + EVENT_SPAWN_SAURFANG_EVENT = 6, + EVENT_SAURFANG_ZEPPELIN_DOCK = 7, + EVENT_SAURFANG_OUTRO_TIMEOUT = 8, + EVENT_SAURFANG_ZEPPELIN_REMOVE = 9, + // The camp is built on screen, stage by stage. + EVENT_SAURFANG_CAMP_TELEPORTERS = 10, + EVENT_SAURFANG_CAMP_WORKERS = 11, + EVENT_SAURFANG_CAMP_TENTS = 12, + EVENT_SAURFANG_CAMP_WORKERS_OUT = 13, + EVENT_SAURFANG_CAMP_VENDORS = 14, + EVENT_SAURFANG_CAMP_SMITH_ARRIVE = 15, + EVENT_SAURFANG_CAMP_WORKERS_BACK = 16, + EVENT_SAURFANG_CAMP_VENDOR_ARRIVE = 17 }; enum Spells @@ -161,6 +174,46 @@ Position const UtherSpawnPos = { -26.58507f, 2211.524f, 30.19898f, 3.124139f Position const SylvanasSpawnPos = { -41.45833f, 2222.891f, 27.98586f, 3.647738f }; Position const SindragosaSpawnPos = { 4818.6997f, 2483.7102f, 287.06497f, 3.286661f }; +// The camps differ: Horde tents from the retail sniff, Alliance ones measured in game and much +// closer together, with their own forge, anvil and banner by the second tent, and no bonfire. +Position const SaurfangCampTentPosH[2] = +{ + { -532.86456f, 2229.0088f, 539.2921f, 2.530723f }, + { -524.55730f, 2238.0920f, 539.2920f, 0.13962449f } +}; + +Position const SaurfangCampTentPosA[2] = +{ + { -531.84283f, 2230.6853f, 539.2918f, 5.625422f }, + { -528.76000f, 2234.7815f, 539.2918f, 5.609714f } +}; + +Position const SaurfangCampBannerPosA = { -533.05540f, 2234.6326f, 539.2918f, 5.621495f }; +Position const SaurfangCampBlacksmithPosA = { -526.41502f, 2232.9104f, 539.2918f, 5.609714f }; +Position const SaurfangCampGeneralGoodsPosA = { -529.46875f, 2228.8513f, 539.2918f, 5.625422f }; +Position const SaurfangCampAnvilPosA = { -525.71640f, 2236.1909f, 539.2918f, 5.609714f }; +Position const SaurfangCampForgePosA = { -528.06140f, 2238.0620f, 539.2918f, 5.609714f }; +// Detour: both Alliance vendors round the south side, on separate corners. +Position const SaurfangCampSmithDetourPosA = { -525.50000f, 2227.5000f, 539.2918f, 0.0f }; +Position const SaurfangCampGoodsDetourPosA = { -528.50000f, 2225.0000f, 539.2918f, 0.0f }; + +Position const SaurfangCampTeleporterPos[2] = +{ + { -560.41840f, 2202.7500f, 539.28534f, 0.0f }, + { -560.29517f, 2220.2153f, 539.28540f, 0.0f } +}; + +Position const SaurfangCampBlacksmithPos = { -520.94100f, 2233.1077f, 539.3463f, 5.3756142f }; +Position const SaurfangCampGeneralGoodsPos = { -530.17017f, 2226.2310f, 539.3463f, 5.4628806f }; +// Travel time for the ~41y between a teleporter pad and its tent site. +Seconds const SaurfangCampWorkerTravel = 7s; +// The Horde smith would otherwise walk straight through the bonfire. +Position const SaurfangCampSmithDetourPos = { -529.00000f, 2233.0000f, 539.2920f, 0.0f }; +Position const SaurfangOutroPortalPos = { -523.55963f, 2238.8900f, 539.29070f, 6.1102815f }; +// Where the zeppelin comes to rest; it is frozen on arrival. +Position const SaurfangOutroZeppelinPos = { -527.66110f, 2254.6910f, 538.53300f, 0.6848107f }; +float const SaurfangOutroZeppelinDockRange = 12.0f; + // Set position traps Spirit Alarm std::vector GoSpiritAlarm_1 = { { -160.96f, 2210.46f, 35.24f, 0.0f }, { -176.27f, 2201.93f, 35.24f, 0.0f}, { -207.83f, 2207.38f, 35.24f, 0.0f } }; std::vector GoSpiritAlarm_2 = { { -178.41f, 2225.11f, 35.24f, 0.0f }, { -195.23f, 2221.55f, 35.24f, 0.0f}, { -209.94f, 2250.34f, 37.99f, 0.0f } }; @@ -216,6 +269,10 @@ public: LichKingHeroicAvailable = true; LichKingRandomWhisperTimer = 120 * IN_MILLISECONDS; DarkwhisperElevatorTimer = DARKWHISPER_ELEVATOR_DWELL_TIME; + _saurfangCampSpawned = false; + _saurfangOutroRunning = false; + _saurfangZeppelinDocked = false; + _saurfangZeppelinLeaving = false; SetHeaders(DataHeader); SetBossNumber(MAX_ENCOUNTERS); @@ -271,6 +328,14 @@ public: if (GetBossState(DATA_LADY_DEATHWHISPER) == DONE && GetBossState(DATA_ICECROWN_GUNSHIP_BATTLE) != DONE) SpawnGunship(); + // Also covers an instance whose Gunship Battle state was set directly, which would + // otherwise keep them hidden for good. + if (GetBossState(DATA_ICECROWN_GUNSHIP_BATTLE) == DONE && !_saurfangOutroRunning) + RestoreHiddenSaurfangEventNpcs(); + + if (GetBossState(DATA_DEATHBRINGER_SAURFANG) == DONE && !_saurfangOutroRunning) + SpawnSaurfangCamp(false); + if (GetBossState(DATA_SINDRAGOSA) != DONE && IsSindragosaIntroDone && !GetCreature(DATA_SINDRAGOSA) && !Events.HasTimeUntilEvent(EVENT_RESPAWN_SINDRAGOSA)) { Events.ScheduleEvent(EVENT_RESPAWN_SINDRAGOSA, 30s); @@ -391,6 +456,11 @@ public: DeathbringerSaurfangGUID = creature->GetGUID(); break; case NPC_SE_HIGH_OVERLORD_SAURFANG: + // Only the static spawn is the event NPC - the Alliance outro summons another + // High Overlord Saurfang, which would otherwise clobber the guid. + if (creature->IsSummon()) + break; + if (GetTeamIdInInstance() == TEAM_ALLIANCE) { creature->UpdateEntry(NPC_SE_MURADIN_BRONZEBEARD, true); @@ -398,9 +468,14 @@ public: } DeathbringerSaurfangEventGUID = creature->GetGUID(); creature->LastUsedScriptID = creature->GetScriptId(); + HideSaurfangEventNpc(creature); break; case NPC_SE_MURADIN_BRONZEBEARD: + if (creature->IsSummon()) + break; + DeathbringerSaurfangEventGUID = creature->GetGUID(); + HideSaurfangEventNpc(creature); break; case NPC_HIGH_OVERLORD_SAURFANG_DUMMY: if (GetTeamIdInInstance() == TEAM_ALLIANCE) @@ -410,8 +485,13 @@ public: } break; case NPC_SE_KOR_KRON_REAVER: + if (creature->IsSummon()) + break; + if (GetTeamIdInInstance() == TEAM_ALLIANCE) creature->UpdateEntry(NPC_SE_SKYBREAKER_MARINE); + SaurfangEventGuardGUIDs.push_back(creature->GetGUID()); + HideSaurfangEventNpc(creature); break; case NPC_FESTERGUT: FestergutGUID = creature->GetGUID(); @@ -747,6 +827,18 @@ public: case GO_SCOURGE_TRANSPORTER_SAURFANG: SaurfangTeleportGUID = go->GetGUID(); break; + case GO_SAURFANG_CAMP_FORGE: + case GO_SAURFANG_CAMP_BONFIRE: + case GO_SAURFANG_CAMP_ANVIL: + // Camp props belong to the aftermath; their spawn rows are unconditional, hence the despawn. + SaurfangCampGUIDs.push_back(go->GetGUID()); + if (GetBossState(DATA_DEATHBRINGER_SAURFANG) != DONE) + go->DespawnOrUnsummon(0ms, Seconds(WEEK)); + // The despawn is persisted, so an instance restarted after the outro comes back + // with them hidden. Horde only - the Alliance camp summons its own props. + else if (GetTeamIdInInstance() == TEAM_HORDE) + go->Respawn(); + break; case GO_PLAGUE_SIGIL: PlagueSigilGUID = go->GetGUID(); if (GetBossState(DATA_PROFESSOR_PUTRICIDE) == DONE) @@ -904,6 +996,9 @@ public: { switch (type) { + case DATA_SAURFANG_OUTRO_ZEPPELIN: + // DONE once it has come to rest, so the outro can wait for the ship instead of guessing. + return _saurfangZeppelinDocked ? DONE : IN_PROGRESS; case DATA_BUFF_AVAILABLE: return (IsBuffAvailable ? 1 : 0); case DATA_WEEKLY_QUEST_ID: @@ -1096,6 +1191,9 @@ public: loot->SetLootRecipient(deathbringer); loot->RemoveGameObjectFlag(GO_FLAG_LOCKED | GO_FLAG_NOT_SELECTABLE | GO_FLAG_NODESPAWN); } + + // Not raised here: DONE fires the moment the boss dies. The outro asks + // via SetData(DATA_SAURFANG_CAMP); OnPlayerEnter covers saved instances. [[fallthrough]]; case NOT_STARTED: if (GameObject* teleporter = instance->GetGameObject(SaurfangTeleportGUID)) @@ -1220,10 +1318,286 @@ public: } } + // Hidden rather than despawned: a despawned creature leaves the map and its guid would no + // longer resolve. + void HideSaurfangEventNpc(Creature* creature) + { + if (GetBossState(DATA_ICECROWN_GUNSHIP_BATTLE) == DONE) + return; + + creature->SetVisible(false); + } + + void RestoreSaurfangEventNpc(Creature* creature) + { + float x, y, z, o; + creature->GetHomePosition(x, y, z, o); + creature->NearTeleportTo(x, y, z, o); + creature->SetStandState(UNIT_STAND_STATE_STAND); + creature->SetEmoteState(EMOTE_ONESHOT_NONE); + creature->SetSheath(SHEATH_STATE_UNARMED); + creature->SetVisible(true); + } + + void SpawnSaurfangEventNpcs() + { + if (Creature* captain = instance->GetCreature(DeathbringerSaurfangEventGUID)) + RestoreSaurfangEventNpc(captain); + + for (ObjectGuid const& guid : SaurfangEventGuardGUIDs) + if (Creature* guard = instance->GetCreature(guid)) + RestoreSaurfangEventNpc(guard); + } + + // Only the ones left hidden: a scene in progress has them visible and placed, and + // teleporting those to their spawn points would break it mid-run. + void RestoreHiddenSaurfangEventNpcs() + { + if (Creature* captain = instance->GetCreature(DeathbringerSaurfangEventGUID)) + if (!captain->IsVisible()) + RestoreSaurfangEventNpc(captain); + + for (ObjectGuid const& guid : SaurfangEventGuardGUIDs) + if (Creature* guard = instance->GetCreature(guid)) + if (!guard->IsVisible()) + RestoreSaurfangEventNpc(guard); + } + + // staged runs the on-screen build: teleporters, then workers raising the tents, then the + // vendors. Unstaged drops the finished camp at once, for an instance already DONE. + void SpawnSaurfangCamp(bool staged) + { + if (_saurfangCampSpawned || !instance->HavePlayers()) + return; + + _saurfangCampSpawned = true; + if (staged) + { + Events.ScheduleEvent(EVENT_SAURFANG_CAMP_TELEPORTERS, 3s); + return; + } + + SummonSaurfangCampTeleporters(); + SpawnSaurfangEventNpcs(); + SpawnSaurfangCampTents(); + SummonSaurfangCampVendor(true, false); + SummonSaurfangCampVendor(false, false); + } + + void SummonSaurfangCampTeleporters() + { + uint32 const teleporter = GetTeamIdInInstance() == TEAM_HORDE ? GO_SAURFANG_CAMP_TELEPORTER_H : GO_SAURFANG_CAMP_TELEPORTER_A; + for (uint8 i = 0; i < 2; ++i) + { + // Summoned ready is the inert model; activating it is what turns the beam on. + if (GameObject* go = instance->SummonGameObject(teleporter, SaurfangCampTeleporterPos[i], 0.0f, 0.0f, 0.0f, 0.0f, WEEK)) + go->SetGoState(GO_STATE_ACTIVE); + } + } + + void SpawnSaurfangCampTeleporters() + { + SummonSaurfangCampTeleporters(); + SpawnSaurfangEventNpcs(); + Events.ScheduleEvent(EVENT_SAURFANG_CAMP_WORKERS, 5s); + } + + void SpawnSaurfangCampWorkers() + { + bool const horde = GetTeamIdInInstance() == TEAM_HORDE; + uint32 const worker = horde ? NPC_CAMP_WARSONG_PEON : NPC_CAMP_ALLIANCE_MASON; + for (uint8 i = 0; i < 2; ++i) + { + if (Creature* builder = instance->SummonCreature(worker, SaurfangCampTeleporterPos[i])) + { + SaurfangCampWorkerGUIDs.push_back(builder->GetGUID()); + builder->SetReactState(REACT_PASSIVE); + builder->SetWalk(false); + builder->GetMotionMaster()->MovePoint(0, (horde ? SaurfangCampTentPosH[i] : SaurfangCampTentPosA[i])); + builder->SetEmoteState(EMOTE_STATE_WORK_MINING); + } + } + + // Timed from the summon, so the run out has to be paid for before the hammering starts. + Events.ScheduleEvent(EVENT_SAURFANG_CAMP_WORKERS_BACK, SaurfangCampWorkerTravel + (horde ? 3s : 15s)); + } + + void SpawnSaurfangCampTents() + { + bool const horde = GetTeamIdInInstance() == TEAM_HORDE; + uint32 const tents[2] = + { + static_cast(horde ? GO_SAURFANG_CAMP_TENT_H1 : GO_SAURFANG_CAMP_TENT_A), + static_cast(horde ? GO_SAURFANG_CAMP_TENT_H2 : GO_SAURFANG_CAMP_TENT_A) + }; + + Position const* tentPos = horde ? SaurfangCampTentPosH : SaurfangCampTentPosA; + for (uint8 i = 0; i < 2; ++i) + instance->SummonGameObject(tents[i], tentPos[i], 0.0f, 0.0f, 0.0f, 0.0f, WEEK); + + if (horde) + { + for (ObjectGuid const& guid : SaurfangCampGUIDs) + if (GameObject* camp = instance->GetGameObject(guid)) + camp->Respawn(); + } + else + { + instance->SummonGameObject(GO_SAURFANG_CAMP_FORGE, SaurfangCampForgePosA, 0.0f, 0.0f, 0.0f, 0.0f, WEEK); + instance->SummonGameObject(GO_SAURFANG_CAMP_ANVIL_A, SaurfangCampAnvilPosA, 0.0f, 0.0f, 0.0f, 0.0f, WEEK); + instance->SummonGameObject(GO_SAURFANG_CAMP_BANNER_A, SaurfangCampBannerPosA, 0.0f, 0.0f, 0.0f, 0.0f, WEEK); + } + } + + // They clear the site before the tent drops, or it lands on top of them. + void SendSaurfangCampWorkersBack() + { + uint8 i = 0; + for (ObjectGuid const& guid : SaurfangCampWorkerGUIDs) + { + if (Creature* builder = instance->GetCreature(guid)) + { + builder->SetEmoteState(EMOTE_ONESHOT_NONE); + builder->SetWalk(false); + builder->GetMotionMaster()->MovePoint(0, SaurfangCampTeleporterPos[i % 2]); + } + ++i; + } + + Events.ScheduleEvent(EVENT_SAURFANG_CAMP_TENTS, 2s); + // They only vanish once they are back standing on the pad. + Events.ScheduleEvent(EVENT_SAURFANG_CAMP_WORKERS_OUT, + SaurfangCampWorkerTravel + (GetTeamIdInInstance() == TEAM_HORDE ? 2s : 5s)); + } + + void DespawnSaurfangCampWorkers() + { + for (ObjectGuid const& guid : SaurfangCampWorkerGUIDs) + if (Creature* builder = instance->GetCreature(guid)) + builder->DespawnOrUnsummon(); + + SaurfangCampWorkerGUIDs.clear(); + Events.ScheduleEvent(EVENT_SAURFANG_CAMP_VENDORS, 1s); + } + + // walkIn false drops the vendor straight on his pitch, for a camp not built on screen. + // The detours exist because the direct line clips the Horde bonfire / the first Alliance tent. + void SummonSaurfangCampVendor(bool smith, bool walkIn) + { + bool const horde = GetTeamIdInInstance() == TEAM_HORDE; + uint32 const entry = smith ? (horde ? NPC_CAMP_MORGAN_DAYBLAZE : NPC_CAMP_SHELY_STEELBOWELS) + : (horde ? NPC_CAMP_APOTHECARY_CANDITH_TOMAS : NPC_CAMP_BRAZIE_GETZ); + Position const& pitch = smith ? (horde ? SaurfangCampBlacksmithPos : SaurfangCampBlacksmithPosA) + : (horde ? SaurfangCampGeneralGoodsPos : SaurfangCampGeneralGoodsPosA); + + if (!walkIn) + { + instance->SummonCreature(entry, pitch); + return; + } + + Creature* vendor = instance->SummonCreature(entry, SaurfangCampTeleporterPos[smith ? 0 : 1]); + if (!vendor) + return; + + vendor->SetWalk(true); + if (horde && !smith) + { + vendor->GetMotionMaster()->MovePoint(0, pitch); + return; + } + + Position const& detour = horde ? SaurfangCampSmithDetourPos + : (smith ? SaurfangCampSmithDetourPosA : SaurfangCampGoodsDetourPosA); + vendor->GetMotionMaster()->MovePoint(0, detour); + + if (smith) + { + SaurfangCampSmithGUID = vendor->GetGUID(); + Events.ScheduleEvent(EVENT_SAURFANG_CAMP_SMITH_ARRIVE, horde ? 19s : 17s); + } + else + { + SaurfangCampGoodsGUID = vendor->GetGUID(); + Events.ScheduleEvent(EVENT_SAURFANG_CAMP_VENDOR_ARRIVE, 15s); + } + } + + void SpawnSaurfangCampVendors() + { + SummonSaurfangCampVendor(true, true); + SummonSaurfangCampVendor(false, true); + } + void SetData(uint32 type, uint32 data) override { switch (type) { + case DATA_SAURFANG_CAMP: + // The boss is DONE for the whole outro, so the encounter state alone cannot tell + // "outro playing" from "outro over". + if (data == IN_PROGRESS) + { + _saurfangOutroRunning = true; + // Safety net: an instance emptying mid-outro would leave the flag set and + // the camp could never be raised. + Events.ScheduleEvent(EVENT_SAURFANG_OUTRO_TIMEOUT, 8min); + } + else if (data == DONE) + { + Events.CancelEvent(EVENT_SAURFANG_OUTRO_TIMEOUT); + _saurfangOutroRunning = false; + SpawnSaurfangCamp(true); + } + break; + case DATA_SAURFANG_OUTRO_ZEPPELIN: + if (data == IN_PROGRESS) + { + if (!SaurfangZeppelinGUID) + { + _saurfangZeppelinDocked = false; + _saurfangZeppelinLeaving = false; + if (MotionTransport* zeppelin = sTransportMgr->CreateTransport(GO_SAURFANG_OUTRO_ZEPPELIN, 0, instance)) + { + SaurfangZeppelinGUID = zeppelin->GetGUID(); + zeppelin->setActive(true); + Events.ScheduleEvent(EVENT_SAURFANG_ZEPPELIN_DOCK, 1s); + } + } + } + // Asked for twice (departure, then cleanup); without the guard the second call + // would restart the removal timer. + else if (SaurfangZeppelinGUID && !_saurfangZeppelinLeaving) + { + Events.CancelEvent(EVENT_SAURFANG_ZEPPELIN_DOCK); + _saurfangZeppelinDocked = false; + _saurfangZeppelinLeaving = true; + if (GameObject* go = instance->GetGameObject(SaurfangZeppelinGUID)) + if (MotionTransport* zeppelin = go->ToMotionTransport()) + zeppelin->EnableMovement(true); + + // Releasing _pendingStop does not launch it: it leaves when the stop frame + // DepartureTime comes round, ~24s. Removal is well past that. + Events.ScheduleEvent(EVENT_SAURFANG_ZEPPELIN_REMOVE, 60s); + } + break; + case DATA_SAURFANG_OUTRO_PORTAL: + if (data == IN_PROGRESS) + { + if (GameObject* portal = instance->SummonGameObject(GO_SAURFANG_OUTRO_PORTAL, SaurfangOutroPortalPos, 0.0f, 0.0f, 0.0f, 0.0f, HOUR)) + { + // SPELLCASTER object carrying spell 59065; scenery here, so it must not be clickable. + portal->SetGameObjectFlag(GO_FLAG_NOT_SELECTABLE); + SaurfangPortalGUID = portal->GetGUID(); + } + } + else + { + if (GameObject* portal = instance->GetGameObject(SaurfangPortalGUID)) + portal->DespawnOrUnsummon(); + SaurfangPortalGUID.Clear(); + } + break; case DATA_BUFF_AVAILABLE: IsBuffAvailable = !!data; if (!IsBuffAvailable) @@ -1761,6 +2135,74 @@ public: // Could happen more than once if more than one player enters before she respawns. Events.CancelEvent(EVENT_RESPAWN_SINDRAGOSA); break; + case EVENT_SPAWN_SAURFANG_EVENT: + SpawnSaurfangEventNpcs(); + break; + case EVENT_SAURFANG_OUTRO_TIMEOUT: + _saurfangOutroRunning = false; + SetData(DATA_SAURFANG_OUTRO_PORTAL, DONE); + SetData(DATA_SAURFANG_OUTRO_ZEPPELIN, DONE); + if (GetBossState(DATA_DEATHBRINGER_SAURFANG) == DONE) + { + SpawnSaurfangCamp(false); + SpawnSaurfangEventNpcs(); + } + break; + case EVENT_SAURFANG_CAMP_TELEPORTERS: + SpawnSaurfangCampTeleporters(); + break; + case EVENT_SAURFANG_CAMP_WORKERS: + SpawnSaurfangCampWorkers(); + break; + case EVENT_SAURFANG_CAMP_TENTS: + SpawnSaurfangCampTents(); + break; + case EVENT_SAURFANG_CAMP_WORKERS_OUT: + DespawnSaurfangCampWorkers(); + break; + case EVENT_SAURFANG_CAMP_WORKERS_BACK: + SendSaurfangCampWorkersBack(); + break; + case EVENT_SAURFANG_CAMP_SMITH_ARRIVE: + if (Creature* smith = instance->GetCreature(SaurfangCampSmithGUID)) + smith->GetMotionMaster()->MovePoint(0, GetTeamIdInInstance() == TEAM_HORDE ? SaurfangCampBlacksmithPos : SaurfangCampBlacksmithPosA); + break; + case EVENT_SAURFANG_CAMP_VENDOR_ARRIVE: + if (Creature* goods = instance->GetCreature(SaurfangCampGoodsGUID)) + goods->GetMotionMaster()->MovePoint(0, SaurfangCampGeneralGoodsPosA); + break; + case EVENT_SAURFANG_CAMP_VENDORS: + SpawnSaurfangCampVendors(); + break; + case EVENT_SAURFANG_ZEPPELIN_REMOVE: + if (GameObject* go = instance->GetGameObject(SaurfangZeppelinGUID)) + { + // A MO_TRANSPORT is not despawned like an ordinary gameobject. + if (MotionTransport* zeppelin = go->ToMotionTransport()) + { + zeppelin->EnableMovement(false); + zeppelin->UnloadStaticPassengers(); + } + go->AddObjectToRemoveList(); + } + _saurfangZeppelinLeaving = false; + SaurfangZeppelinGUID.Clear(); + break; + case EVENT_SAURFANG_ZEPPELIN_DOCK: + { + // Reschedule unconditionally: giving up on a failed lookup would leave + // it looping its path forever. + GameObject* go = instance->GetGameObject(SaurfangZeppelinGUID); + MotionTransport* zeppelin = go ? go->ToMotionTransport() : nullptr; + if (zeppelin && zeppelin->GetExactDist2d(&SaurfangOutroZeppelinPos) <= SaurfangOutroZeppelinDockRange) + { + zeppelin->EnableMovement(false); + _saurfangZeppelinDocked = true; + } + else if (SaurfangZeppelinGUID) + Events.ScheduleEvent(EVENT_SAURFANG_ZEPPELIN_DOCK, 500ms); + } + break; default: break; } @@ -1803,6 +2245,8 @@ public: } if (Creature* captain = source->FindNearestCreature(GetTeamIdInInstance() == TEAM_HORDE ? NPC_IGB_HIGH_OVERLORD_SAURFANG : NPC_IGB_MURADIN_BRONZEBEARD, 200.0f)) captain->AI()->DoAction(ACTION_EXIT_SHIP); + // The captain despawns 18s into his walk off the ship; the event party appears with him. + Events.ScheduleEvent(EVENT_SPAWN_SAURFANG_EVENT, 18s); break; case EVENT_QUAKE: @@ -1930,6 +2374,17 @@ public: ObjectGuid DeathbringerSaurfangEventGUID; // Muradin Bronzebeard or High Overlord Saurfang ObjectGuid DeathbringersCacheGUID; ObjectGuid SaurfangTeleportGUID; + GuidList SaurfangCampGUIDs; + GuidList SaurfangEventGuardGUIDs; + GuidList SaurfangCampWorkerGUIDs; + ObjectGuid SaurfangCampSmithGUID; + ObjectGuid SaurfangCampGoodsGUID; + ObjectGuid SaurfangZeppelinGUID; + ObjectGuid SaurfangPortalGUID; + bool _saurfangCampSpawned; + bool _saurfangOutroRunning; + bool _saurfangZeppelinDocked; + bool _saurfangZeppelinLeaving; ObjectGuid PlagueSigilGUID; ObjectGuid BloodwingSigilGUID; ObjectGuid FrostwingSigilGUID;