diff --git a/data/sql/updates/pending_db_world/rev_1660074715862707400.sql b/data/sql/updates/pending_db_world/rev_1660074715862707400.sql new file mode 100644 index 000000000..ac19529df --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1660074715862707400.sql @@ -0,0 +1,4 @@ +-- +UPDATE `creature_template` SET `unit_flags` = `unit_flags` | 33554432, `ScriptName` = 'npc_dirt_mound' WHERE `entry` = 15712; + +UPDATE `gameobject_template_addon` SET `flags` = `flags` | 16 WHERE `entry` = 180795; diff --git a/src/server/game/AI/CoreAI/UnitAI.cpp b/src/server/game/AI/CoreAI/UnitAI.cpp index 761a01ac0..1304a6cfe 100644 --- a/src/server/game/AI/CoreAI/UnitAI.cpp +++ b/src/server/game/AI/CoreAI/UnitAI.cpp @@ -90,6 +90,24 @@ bool UnitAI::DoSpellAttackIfReady(uint32 spell) return false; } +void UnitAI::DoSpellAttackToRandomTargetIfReady(uint32 spell, uint32 threatTablePosition /*= 0*/, float dist /*= 0.f*/, bool playerOnly /*= true*/) +{ + if (me->HasUnitState(UNIT_STATE_CASTING) || !me->isAttackReady()) + return; + + if (SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spell)) + { + if (Unit* target = SelectTarget(SelectTargetMethod::Random, threatTablePosition, dist, playerOnly)) + { + if (me->IsWithinCombatRange(target, spellInfo->GetMaxRange(false))) + { + me->CastSpell(target, spell, false); + me->resetAttackTimer(); + } + } + } +} + Unit* UnitAI::SelectTarget(SelectTargetMethod targetType, uint32 position, float dist, bool playerOnly, int32 aura) { return SelectTarget(targetType, position, DefaultTargetSelector(me, dist, playerOnly, aura)); diff --git a/src/server/game/AI/CoreAI/UnitAI.h b/src/server/game/AI/CoreAI/UnitAI.h index 4f8b00d41..a423997b9 100644 --- a/src/server/game/AI/CoreAI/UnitAI.h +++ b/src/server/game/AI/CoreAI/UnitAI.h @@ -330,6 +330,7 @@ public: void DoMeleeAttackIfReady(); bool DoSpellAttackIfReady(uint32 spell); + void DoSpellAttackToRandomTargetIfReady(uint32 spell, uint32 threatTablePosition = 0, float dist = 0.f, bool playerOnly = true); static AISpellInfoType* AISpellInfo; static void FillAISpellInfo(); diff --git a/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_ouro.cpp b/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_ouro.cpp index 59f655c92..974620cb8 100644 --- a/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_ouro.cpp +++ b/src/server/scripts/Kalimdor/TempleOfAhnQiraj/boss_ouro.cpp @@ -15,25 +15,47 @@ * with this program. If not, see . */ -/* ScriptData -SDName: Boss_Ouro -SD%Complete: 85 -SDComment: No model for submerging. Currently just invisible. -SDCategory: Temple of Ahn'Qiraj -EndScriptData */ - +#include "Cell.h" +#include "CellImpl.h" +#include "GridNotifiers.h" +#include "GridNotifiersImpl.h" +#include "Player.h" #include "ScriptMgr.h" #include "ScriptedCreature.h" +#include "TaskScheduler.h" #include "temple_of_ahnqiraj.h" enum Spells { + // Ouro SPELL_SWEEP = 26103, - SPELL_SANDBLAST = 26102, + SPELL_SAND_BLAST = 26102, SPELL_GROUND_RUPTURE = 26100, - SPELL_BIRTH = 26262, // The Birth Animation + SPELL_BERSERK = 26615, + SPELL_BOULDER = 26616, + SPELL_OURO_SUBMERGE_VISUAL = 26063, + SPELL_SUMMON_SANDWORM_BASE = 26133, + + // Misc - Mounds, Ouro Spawner + SPELL_BIRTH = 26586, SPELL_DIRTMOUND_PASSIVE = 26092, - SPELL_SUMMON_OURO = 26061 + SPELL_SUMMON_OURO = 26061, + SPELL_SUMMON_OURO_MOUNDS = 26058, + SPELL_QUAKE = 26093, + SPELL_SUMMON_SCARABS = 26060, + SPELL_SUMMON_OURO_AURA = 26642, + SPELL_DREAM_FOG = 24780 +}; + +enum Misc +{ + GROUP_EMERGED = 0, + GROUP_PHASE_TRANSITION = 1, + + NPC_DIRT_MOUND = 15712, + GO_SANDWORM_BASE = 180795, + + DATA_OURO_HEALTH = 0 }; struct npc_ouro_spawner : public ScriptedAI @@ -48,15 +70,15 @@ struct npc_ouro_spawner : public ScriptedAI void Reset() override { hasSummoned = false; - DoCast(me, SPELL_DIRTMOUND_PASSIVE); + DoCastSelf(SPELL_DIRTMOUND_PASSIVE); } void MoveInLineOfSight(Unit* who) override { // Spawn Ouro on LoS check - if (!hasSummoned && who->GetTypeId() == TYPEID_PLAYER && me->IsWithinDistInMap(who, 40.0f)) + if (!hasSummoned && who->GetTypeId() == TYPEID_PLAYER && me->IsWithinDistInMap(who, 40.0f) && !who->ToPlayer()->IsGameMaster()) { - DoCast(me, SPELL_SUMMON_OURO); + DoCastSelf(SPELL_SUMMON_OURO); hasSummoned = true; } @@ -69,43 +91,170 @@ struct npc_ouro_spawner : public ScriptedAI if (creature->GetEntry() == NPC_OURO) { creature->SetInCombatWithZone(); - creature->CastSpell(creature, SPELL_BIRTH, false); me->DespawnOrUnsummon(); } } - }; -struct boss_ouro : public ScriptedAI +struct boss_ouro : public BossAI { - boss_ouro(Creature* creature) : ScriptedAI(creature) { } + boss_ouro(Creature* creature) : BossAI(creature, DATA_OURO) + { + SetCombatMovement(false); + me->SetControlled(true, UNIT_STATE_ROOT); + _scheduler.SetValidator([this] { return !me->HasUnitState(UNIT_STATE_CASTING); }); + } - uint32 Sweep_Timer; - uint32 SandBlast_Timer; - uint32 Submerge_Timer; - uint32 Back_Timer; - uint32 ChangeTarget_Timer; - uint32 Spawn_Timer; + void DamageTaken(Unit* /*attacker*/, uint32& damage, DamageEffectType, SpellSchoolMask) override + { + if (me->HealthBelowPctDamaged(20, damage) && !_enraged) + { + DoCastSelf(SPELL_BERSERK, true); + _enraged = true; + _scheduler.CancelGroup(GROUP_PHASE_TRANSITION); + _scheduler.Schedule(1s, [this](TaskContext context) + { + if (!IsPlayerWithinMeleeRange()) + DoSpellAttackToRandomTargetIfReady(SPELL_BOULDER); - bool Enrage; - bool Submerged; + context.Repeat(); + }) + .Schedule(20s, [this](TaskContext context) + { + DoCastSelf(SPELL_SUMMON_OURO_MOUNDS, true); + context.Repeat(); + }); + } + } + + void Submerge() + { + me->AttackStop(); + me->SetReactState(REACT_PASSIVE); + me->SetUnitFlag(UNIT_FLAG_NOT_SELECTABLE | UNIT_FLAG_NON_ATTACKABLE); + _submergeMelee = 0; + _submerged = true; + DoCastSelf(SPELL_OURO_SUBMERGE_VISUAL); + _scheduler.CancelGroup(GROUP_EMERGED); + _scheduler.CancelGroup(GROUP_PHASE_TRANSITION); + + if (GameObject* base = me->FindNearestGameObject(GO_SANDWORM_BASE, 10.f)) + { + base->Use(me); + base->DespawnOrUnsummon(6s); + } + + DoCastSelf(SPELL_SUMMON_OURO_MOUNDS, true); + // According to sniffs, Ouro uses his mounds to respawn. The health management could be a little scuffed. + std::list ouroMounds; + me->GetCreatureListWithEntryInGrid(ouroMounds, NPC_DIRT_MOUND, 200.f); + if (!ouroMounds.empty()) // This can't be possible, but just to be sure. + { + if (Creature* mound = Acore::Containers::SelectRandomContainerElement(ouroMounds)) + { + mound->AddAura(SPELL_SUMMON_OURO_AURA, mound); + mound->AI()->SetData(DATA_OURO_HEALTH, me->GetHealth()); + } + } + + me->DespawnOrUnsummon(1000); + } + + void CastGroundRupture() + { + std::list targets; + Acore::AllWorldObjectsInRange checker(me, 10.0f); + Acore::WorldObjectListSearcher searcher(me, targets, checker); + Cell::VisitAllObjects(me, searcher, 10.0f); + + for (WorldObject* target : targets) + { + if (Unit* unitTarget = target->ToUnit()) + { + if (unitTarget->IsHostileTo(me)) + DoCast(unitTarget, SPELL_GROUND_RUPTURE, true); + } + } + } + + void SpellHitTarget(Unit* target, SpellInfo const* spellInfo) override + { + if (spellInfo->Id == SPELL_SAND_BLAST && target) + me->GetThreatMgr().modifyThreatPercent(target, 100); + } + + void Emerge() + { + DoCastSelf(SPELL_BIRTH); + DoCastSelf(SPELL_SUMMON_SANDWORM_BASE, true); + me->SetReactState(REACT_AGGRESSIVE); + CastGroundRupture(); + _scheduler + .Schedule(20s, GROUP_EMERGED, [this](TaskContext context) + { + DoCastVictim(SPELL_SAND_BLAST); + context.Repeat(); + }) + .Schedule(22s, GROUP_EMERGED, [this](TaskContext context) + { + DoCastVictim(SPELL_SWEEP); + context.Repeat(); + }) + .Schedule(90s, GROUP_PHASE_TRANSITION, [this](TaskContext /*context*/) + { + Submerge(); + }) + .Schedule(3s, GROUP_PHASE_TRANSITION, [this](TaskContext context) + { + if (!IsPlayerWithinMeleeRange() && !_submerged) + { + if (_submergeMelee < 10) + { + _submergeMelee++; + } + else + { + if (!_enraged) + Submerge(); + _submergeMelee = 0; + } + } + else + { + _submergeMelee = 0; + } + + if (!_submerged) + context.Repeat(1s); + }); + } void Reset() override { - Sweep_Timer = urand(5000, 10000); - SandBlast_Timer = urand(20000, 35000); - Submerge_Timer = urand(90000, 150000); - Back_Timer = urand(30000, 45000); - ChangeTarget_Timer = urand(5000, 8000); - Spawn_Timer = urand(10000, 20000); - - Enrage = false; - Submerged = false; + instance->SetBossState(DATA_OURO, NOT_STARTED); + _scheduler.CancelAll(); + _submergeMelee = 0; + _submerged = false; + _enraged = false; } - void EnterCombat(Unit* /*who*/) override + void EnterEvadeMode(EvadeReason /*why*/) override { - DoCastVictim(SPELL_BIRTH); + DoCastSelf(SPELL_OURO_SUBMERGE_VISUAL); + me->DespawnOrUnsummon(1000); + // Remove after the header file is sorted with the bosses first. + if (Creature* ouroSpawner = instance->GetCreature(DATA_OURO_SPAWNER)) + ouroSpawner->Respawn(); + instance->SetBossState(DATA_OURO, FAIL); + if (GameObject* base = me->FindNearestGameObject(GO_SANDWORM_BASE, 200.f)) + base->DespawnOrUnsummon(); + } + + void EnterCombat(Unit* who) override + { + Emerge(); + + BossAI::EnterCombat(who); } void UpdateAI(uint32 diff) override @@ -114,67 +263,112 @@ struct boss_ouro : public ScriptedAI if (!UpdateVictim()) return; - //Sweep_Timer - if (!Submerged && Sweep_Timer <= diff) - { - DoCastVictim(SPELL_SWEEP); - Sweep_Timer = urand(15000, 30000); - } - else Sweep_Timer -= diff; - - //SandBlast_Timer - if (!Submerged && SandBlast_Timer <= diff) - { - DoCastVictim(SPELL_SANDBLAST); - SandBlast_Timer = urand(20000, 35000); - } - else SandBlast_Timer -= diff; - - //Submerge_Timer - if (!Submerged && Submerge_Timer <= diff) - { - //Cast - me->HandleEmoteCommand(EMOTE_ONESHOT_SUBMERGE); - me->SetUnitFlag(UNIT_FLAG_NOT_SELECTABLE); - me->SetFaction(FACTION_FRIENDLY); - DoCast(me, SPELL_DIRTMOUND_PASSIVE); - - Submerged = true; - Back_Timer = urand(30000, 45000); - } - else Submerge_Timer -= diff; - - //ChangeTarget_Timer - if (Submerged && ChangeTarget_Timer <= diff) - { - Unit* target = SelectTarget(SelectTargetMethod::Random, 0); - - if (target) - me->NearTeleportTo(target->GetPositionX(), target->GetPositionY(), target->GetPositionZ(), me->GetOrientation()); - - ChangeTarget_Timer = urand(10000, 20000); - } - else ChangeTarget_Timer -= diff; - - //Back_Timer - if (Submerged && Back_Timer <= diff) - { - me->RemoveUnitFlag(UNIT_FLAG_NOT_SELECTABLE); - me->SetFaction(FACTION_MONSTER); - - DoCastVictim(SPELL_GROUND_RUPTURE); - - Submerged = false; - Submerge_Timer = urand(60000, 120000); - } - else Back_Timer -= diff; - - DoMeleeAttackIfReady(); + _scheduler.Update(diff, [this] + { + DoMeleeAttackIfReady(); + }); } + +protected: + TaskScheduler _scheduler; + bool _enraged; + uint8 _submergeMelee; + bool _submerged; + + bool IsPlayerWithinMeleeRange() const + { + return me->IsWithinMeleeRange(me->GetVictim()); + } +}; + +struct npc_dirt_mound : ScriptedAI +{ + npc_dirt_mound(Creature* creature) : ScriptedAI(creature) + { + _instance = creature->GetInstanceScript(); + } + + void JustSummoned(Creature* creature) override + { + if (creature->GetEntry() == NPC_OURO) + { + creature->SetInCombatWithZone(); + creature->SetHealth(_ouroHealth); + } + } + + void SetData(uint32 type, uint32 data) override + { + if (type == DATA_OURO_HEALTH) + _ouroHealth = data; + } + + void EnterCombat(Unit* /*who*/) override + { + DoZoneInCombat(); + _scheduler.Schedule(30s, [this](TaskContext /*context*/) + { + DoCastSelf(SPELL_SUMMON_SCARABS, true); + me->DespawnOrUnsummon(1000); + }) + .Schedule(100ms, [this](TaskContext context) + { + ChaseNewTarget(); + context.Repeat(5s, 10s); + }); + } + + void ChaseNewTarget() + { + DoResetThreat(); + if (Unit* target = SelectTarget(SelectTargetMethod::Random, 0, 200.f, true)) + { + me->AddThreat(target, 1000000.f); + AttackStart(target); + } + } + + void UpdateAI(uint32 diff) override + { + if (!UpdateVictim()) + return; + + _scheduler.Update(diff); + } + + void Reset() override + { + DoCastSelf(SPELL_DIRTMOUND_PASSIVE, true); + DoCastSelf(SPELL_DREAM_FOG, true); + DoCastSelf(SPELL_QUAKE, true); + } + + void EnterEvadeMode(EvadeReason /*why*/) override + { + if (_instance) + { + _instance->SetBossState(DATA_OURO, FAIL); + + // Remove after the header file is sorted with the bosses first. + if (Creature* ouroSpawner = _instance->GetCreature(DATA_OURO_SPAWNER)) + ouroSpawner->Respawn(); + } + + if (GameObject* base = me->FindNearestGameObject(GO_SANDWORM_BASE, 200.f)) + base->DespawnOrUnsummon(); + + me->DespawnOrUnsummon(); + } + +protected: + TaskScheduler _scheduler; + uint32 _ouroHealth; + InstanceScript* _instance; }; void AddSC_boss_ouro() { RegisterTempleOfAhnQirajCreatureAI(npc_ouro_spawner); RegisterTempleOfAhnQirajCreatureAI(boss_ouro); + RegisterTempleOfAhnQirajCreatureAI(npc_dirt_mound); } diff --git a/src/server/scripts/Kalimdor/TempleOfAhnQiraj/instance_temple_of_ahnqiraj.cpp b/src/server/scripts/Kalimdor/TempleOfAhnQiraj/instance_temple_of_ahnqiraj.cpp index 3056a305f..215e4c790 100644 --- a/src/server/scripts/Kalimdor/TempleOfAhnQiraj/instance_temple_of_ahnqiraj.cpp +++ b/src/server/scripts/Kalimdor/TempleOfAhnQiraj/instance_temple_of_ahnqiraj.cpp @@ -30,7 +30,8 @@ EndScriptData */ ObjectData const creatureData[] = { { NPC_SARTURA, DATA_SARTURA }, - { NPC_EYE_OF_CTHUN, DATA_EYE_OF_CTHUN } + { NPC_EYE_OF_CTHUN, DATA_EYE_OF_CTHUN }, + { NPC_OURO_SPAWNER, DATA_OURO_SPAWNER } }; class instance_temple_of_ahnqiraj : public InstanceMapScript @@ -102,6 +103,10 @@ public: case NPC_VISCIDUS: ViscidusGUID = creature->GetGUID(); break; + case NPC_OURO_SPAWNER: + if (GetBossState(DATA_OURO) != DONE) + creature->Respawn(); + break; } InstanceScript::OnCreatureCreate(creature); @@ -177,6 +182,27 @@ public: break; } } + + bool SetBossState(uint32 type, EncounterState state) override + { + if (!InstanceScript::SetBossState(type, state)) + return false; + + switch (type) + { + case DATA_OURO: + if (state == FAIL) + { + if (Creature* ouroSpawner = GetCreature(DATA_OURO)) + ouroSpawner->Respawn(); + } + break; + default: + break; + } + + return true; + } }; }; diff --git a/src/server/scripts/Kalimdor/TempleOfAhnQiraj/temple_of_ahnqiraj.h b/src/server/scripts/Kalimdor/TempleOfAhnQiraj/temple_of_ahnqiraj.h index 63727f51b..9301c9503 100644 --- a/src/server/scripts/Kalimdor/TempleOfAhnQiraj/temple_of_ahnqiraj.h +++ b/src/server/scripts/Kalimdor/TempleOfAhnQiraj/temple_of_ahnqiraj.h @@ -36,7 +36,9 @@ enum DataTypes DATA_VEKNILASHISDEAD = 10, DATA_VEKNILASH_DEATH = 11, DATA_FANKRISS = 12, - DATA_BUG_TRIO_DEATH = 14, + DATA_OURO = 13, + DATA_OURO_SPAWNER = 14, + DATA_BUG_TRIO_DEATH = 15, DATA_CTHUN_PHASE = 20, DATA_VISCIDUS = 21, DATA_SARTURA = 22, @@ -67,6 +69,7 @@ enum Creatures NPC_VEKLOR = 15276, NPC_VEKNILASH = 15275, NPC_OURO = 15517, + NPC_OURO_SPAWNER = 15957, NPC_SARTURA = 15516 };