diff --git a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp index 65cb8be7c..f2a994f5c 100644 --- a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp +++ b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp @@ -101,6 +101,8 @@ void BossSpiderQueenEnemyServer::OnDie(Entity* self, Entity* killer) { } } + SpiderSkillManager(self, false); + // There is suppose to be a 0.1 second delay here but that may be admitted? auto* controller = Game::entityManager->GetZoneControlEntity(); @@ -299,19 +301,55 @@ void BossSpiderQueenEnemyServer::SpiderWaveManager(Entity* self) { } void BossSpiderQueenEnemyServer::ToggleForSpecial(Entity* self, const bool state) { - self->SetBoolean(u"stoppedFlag", state); - - combat->SetDisabled(state); + // Kept apart from "stoppedFlag", which is the no-players-around attack stop (ToggleAttacking) + self->SetBoolean(u"isSpecialAttacking", state); + UpdateSpecialStun(self); } -void BossSpiderQueenEnemyServer::RunRainOfFire(Entity* self) { - if (self->GetBoolean(u"stoppedFlag")) { - self->AddTimer("ROF", GeneralUtils::GenerateRandomNumber(10, 20)); +void BossSpiderQueenEnemyServer::SetRainOfFireStun(Entity* self, const bool state) { + // Live: a separate "can't attack" stun from the start of a rain of fire until its last impact + self->SetBoolean(u"rainOfFireStun", state); + UpdateSpecialStun(self); +} +void BossSpiderQueenEnemyServer::UpdateSpecialStun(Entity* self) { + combat->SetDisabled(self->GetBoolean(u"isSpecialAttacking") || self->GetBoolean(u"rainOfFireStun")); +} + +void BossSpiderQueenEnemyServer::SpiderSkillManager(Entity* self, const bool active) { + if (!active) { + self->CancelTimer("PollSpiderSkillManager"); return; } + // Within smashSkillLength of the melee smash: run the special once the smash is over + if (self->GetBoolean(u"bSpecialLock")) { + self->SetBoolean(u"bSpecialQueued", true); + return; + } + + if (self->GetBoolean(u"isWithdrawn")) return; + + // Stage 2 only fires the rapid fire shooter, stage 3 only the rain of fire + if (m_CurrentBossStage == 2) { + RunRapidFireShooter(self); + } else if (m_CurrentBossStage == 3) { + RunRainOfFire(self); + } +} + +void BossSpiderQueenEnemyServer::OnSkillCast(Entity* self, const uint32_t skillID) { + if (skillID != bossSmashSkill) return; + + // The melee smash locks the specials for as long as it lasts + self->SetBoolean(u"bSpecialLock", true); + self->CancelTimer("UnlockSpecials"); + self->AddTimer("UnlockSpecials", smashSkillLength); +} + +void BossSpiderQueenEnemyServer::RunRainOfFire(Entity* self) { ToggleForSpecial(self, true); + SetRainOfFireStun(self, true); impactList.clear(); @@ -370,9 +408,9 @@ void BossSpiderQueenEnemyServer::RainOfFireManager(Entity* self) { return; } - ToggleForSpecial(self, false); + SetRainOfFireStun(self, false); - self->AddTimer("ROF", GeneralUtils::GenerateRandomNumber(20, 40)); + self->AddTimer("PollSpiderSkillManager", static_cast(GeneralUtils::GenerateRandomNumber(s2DelayMin, s2DelayMax))); } void BossSpiderQueenEnemyServer::RapidFireShooterManager(Entity* self) { @@ -381,8 +419,6 @@ void BossSpiderQueenEnemyServer::RapidFireShooterManager(Entity* self) { self->AddTimer("RFSTauntComplete", animationTime); - ToggleForSpecial(self, false); - return; } @@ -400,16 +436,10 @@ void BossSpiderQueenEnemyServer::RapidFireShooterManager(Entity* self) { void BossSpiderQueenEnemyServer::RunRapidFireShooter(Entity* self) { const auto targets = self->GetTargetsInPhantom(); - if (self->GetBoolean(u"stoppedFlag")) { - self->AddTimer("RFS", GeneralUtils::GenerateRandomNumber(5, 10)); - - return; - } - if (targets.empty()) { LOG("Failed to find RFS targets"); - self->AddTimer("RFS", GeneralUtils::GenerateRandomNumber(5, 10)); + self->AddTimer("PollSpiderSkillManager", static_cast(GeneralUtils::GenerateRandomNumber(s1DelayMin, s1DelayMax))); return; } @@ -429,8 +459,6 @@ void BossSpiderQueenEnemyServer::RunRapidFireShooter(Entity* self) { RapidFireShooterManager(self); PlayAnimAndReturnTime(self, spiderSingleShot); - - self->AddTimer("RFS", GeneralUtils::GenerateRandomNumber(10, 15)); } void BossSpiderQueenEnemyServer::OnTimerDone(Entity* self, const std::string timerName) { @@ -477,31 +505,21 @@ void BossSpiderQueenEnemyServer::OnTimerDone(Entity* self, const std::string tim } else if (timerName == "StartROF") { //Re-enable Spider Boss - //ToggleForSpecial(self, false); + ToggleForSpecial(self, false); RainOfFireManager(self); } else if (timerName == "PollSpiderSkillManager") { //Call the skill manager again to attempt to run the current Spider Boss //stage's special attack again - //SpiderSkillManager(self, true); - PlayAnimAndReturnTime(self, spiderJeerAnim); + SpiderSkillManager(self, true); - } else if (timerName == "RFS") { - RunRapidFireShooter(self); - } else if (timerName == "ROF") { - RunRainOfFire(self); } else if (timerName == "RFSTauntComplete") { - //Determine an appropriate random time to check our manager again - // local spiderCooldownDelay = math.random(s1DelayMin, s1DelayMax) - - //Set a timer based on our random cooldown determination - //to pulse the SpiderSkillManager again - - //GAMEOBJ:GetTimer():AddTimerWithCancel(spiderCooldownDelay, "PollSpiderSkillManager", self) + //Set a timer based on a random cooldown to pulse the SpiderSkillManager again + self->AddTimer("PollSpiderSkillManager", static_cast(GeneralUtils::GenerateRandomNumber(s1DelayMin, s1DelayMax))); //Re-enable Spider Boss - //ToggleForSpecial(self, false); + ToggleForSpecial(self, false); } else if (timerName == "WithdrawComplete") { //Play the Spider Boss' mountain idle anim @@ -591,19 +609,12 @@ void BossSpiderQueenEnemyServer::OnTimerDone(Entity* self, const std::string tim //Did we queue a spcial attack? if (self->GetBoolean(u"bSpecialQueued")) { self->SetBoolean(u"bSpecialQueued", false); + SpiderSkillManager(self, true); } } } void BossSpiderQueenEnemyServer::OnHitOrHealResult(Entity* self, Entity* attacker, int32_t damage) { - if (m_CurrentBossStage > 0 && !self->HasTimer("RFS")) { - self->AddTimer("RFS", 5.0f); - } - - if (m_CurrentBossStage > 0 && !self->HasTimer("ROF")) { - self->AddTimer("ROF", 10.0f); - } - if (m_CurrentBossStage > ThresholdTable.size()) { return; } @@ -616,8 +627,13 @@ void BossSpiderQueenEnemyServer::OnHitOrHealResult(Entity* self, Entity* attacke if (!isWithdrawn) { self->CancelAllTimers(); + // Stop whatever special was running + attackTargetTable.clear(); + impactList.clear(); self->SetBoolean(u"isSpecialAttacking", false); + self->SetBoolean(u"rainOfFireStun", false); self->SetBoolean(u"bSpecialLock", false); + self->SetBoolean(u"bSpecialQueued", false); WithdrawSpider(self, true); } diff --git a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h index 9f1a4fc8d..813e533c1 100644 --- a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h +++ b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h @@ -34,6 +34,13 @@ public: void ToggleForSpecial(Entity* self, bool state); + void SetRainOfFireStun(Entity* self, bool state); + + // Runs the special attack of the current stage (live SpiderSkillManager); false stops polling + void SpiderSkillManager(Entity* self, bool active); + + void OnSkillCast(Entity* self, uint32_t skillID) override; + void RunRainOfFire(Entity* self); void RainOfFireManager(Entity* self); @@ -56,6 +63,7 @@ public: private: void ToggleAttacking(Entity& self, bool on); + void UpdateSpecialStun(Entity* self); //Regular variables: DestroyableComponent* destroyable = nullptr; ControllablePhysicsComponent* controllable = nullptr; @@ -102,10 +110,11 @@ private: const float smashSkillLength = 3.1f; //Time (in seconds) the boss smash skill lasts - const float s1DelayMin = 10.0f; //Minimum time until calling for another Rapid Fire Shot - const float s1DelayMax = 15.0f; //Maximum time until calling for another Rapid Fire Shot - const float s2DelayMin = 10.0f; //Minimum time until calling for another Rain Of Fire - const float s2DelayMax = 15.0f; //Maximum time until calling for another Rain Of Fire + // Whole seconds, as live's math.random(min, max) + const int32_t s1DelayMin = 10; //Minimum time until calling for another Rapid Fire Shot + const int32_t s1DelayMax = 15; //Maximum time until calling for another Rapid Fire Shot + const int32_t s2DelayMin = 10; //Minimum time until calling for another Rain Of Fire + const int32_t s2DelayMax = 15; //Maximum time until calling for another Rain Of Fire const unsigned int instanceZoneID = 1102; //Zone ID for the Spider Queen fight instance const unsigned int instanceMissionID = 1941; //Achievement to update for beating the instanced Boss diff --git a/tests/dGameTests/SpiderQueenTests.cpp b/tests/dGameTests/SpiderQueenTests.cpp index 990eafbdf..c8f2719d9 100644 --- a/tests/dGameTests/SpiderQueenTests.cpp +++ b/tests/dGameTests/SpiderQueenTests.cpp @@ -93,3 +93,28 @@ TEST_F(SpiderQueenTest, OnlySpiderlingsMakeTheBossScream) { const auto packets = Capture([&] { bossScript.OnNotifyObject(boss, other, "SpiderlingDied", 0, 0); }); EXPECT_TRUE(packets.empty()); } + +TEST_F(SpiderQueenTest, TheMeleeSmashLocksTheSpecialsForItsLength) { + bossScript.OnSkillCast(boss, 303); + EXPECT_FALSE(boss->GetBoolean(u"bSpecialLock")); + + bossScript.OnSkillCast(boss, 322); + EXPECT_TRUE(boss->GetBoolean(u"bSpecialLock")); + boss->Update(0.0f); + EXPECT_TRUE(boss->HasTimer("UnlockSpecials")); + + // A special due during the smash waits for it + bossScript.SpiderSkillManager(boss, true); + EXPECT_TRUE(boss->GetBoolean(u"bSpecialQueued")); + + bossScript.OnTimerDone(boss, "UnlockSpecials"); + EXPECT_FALSE(boss->GetBoolean(u"bSpecialLock")); + EXPECT_FALSE(boss->GetBoolean(u"bSpecialQueued")); +} + +TEST_F(SpiderQueenTest, NoSpecialsBeforeTheFirstWave) { + // Stage 1 has no special attack; the skill manager does nothing + const auto packets = Capture([&] { bossScript.SpiderSkillManager(boss, true); }); + EXPECT_TRUE(packets.empty()); + EXPECT_FALSE(boss->GetBoolean(u"isSpecialAttacking")); +}