fix(scripts): Spider Queen specials follow her stage and wait out the smash (issue 1970)

Every hit started both the rapid fire shooter and the rain of fire timers, from stage 1 on, so the two specials
ran in every stage and on top of each other, each turning the boss's AI off and on again under the other's
animations. They also wrote the "stoppedFlag" that the no-players-around attack stop uses, which could leave her
stopped for good. As the live script: after she comes back down, a skill manager fires the rapid fire shooter in
stage 2 and the rain of fire in stage 3 only, again 10 to 15 s after each ends; for 3.1 s after her melee smash
(skill 322) a due special waits and fires when the smash ends. The rain of fire keeps her from attacking until
its last impact.

Check in game: Spider Queen fight: no specials before the first spiderling wave; stage 2 only rapid fire, stage 3
only rain of fire; she never freezes in the smash animation and keeps attacking after each special.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 10:18:03 -05:00
parent 4900f11cf1
commit 990cc74aa0
3 changed files with 96 additions and 46 deletions

View File

@@ -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? // There is suppose to be a 0.1 second delay here but that may be admitted?
auto* controller = Game::entityManager->GetZoneControlEntity(); auto* controller = Game::entityManager->GetZoneControlEntity();
@@ -299,19 +301,55 @@ void BossSpiderQueenEnemyServer::SpiderWaveManager(Entity* self) {
} }
void BossSpiderQueenEnemyServer::ToggleForSpecial(Entity* self, const bool state) { void BossSpiderQueenEnemyServer::ToggleForSpecial(Entity* self, const bool state) {
self->SetBoolean(u"stoppedFlag", state); // Kept apart from "stoppedFlag", which is the no-players-around attack stop (ToggleAttacking)
self->SetBoolean(u"isSpecialAttacking", state);
combat->SetDisabled(state); UpdateSpecialStun(self);
} }
void BossSpiderQueenEnemyServer::RunRainOfFire(Entity* self) { void BossSpiderQueenEnemyServer::SetRainOfFireStun(Entity* self, const bool state) {
if (self->GetBoolean(u"stoppedFlag")) { // Live: a separate "can't attack" stun from the start of a rain of fire until its last impact
self->AddTimer("ROF", GeneralUtils::GenerateRandomNumber<float>(10, 20)); 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; 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); ToggleForSpecial(self, true);
SetRainOfFireStun(self, true);
impactList.clear(); impactList.clear();
@@ -370,9 +408,9 @@ void BossSpiderQueenEnemyServer::RainOfFireManager(Entity* self) {
return; return;
} }
ToggleForSpecial(self, false); SetRainOfFireStun(self, false);
self->AddTimer("ROF", GeneralUtils::GenerateRandomNumber<float>(20, 40)); self->AddTimer("PollSpiderSkillManager", static_cast<float>(GeneralUtils::GenerateRandomNumber<int32_t>(s2DelayMin, s2DelayMax)));
} }
void BossSpiderQueenEnemyServer::RapidFireShooterManager(Entity* self) { void BossSpiderQueenEnemyServer::RapidFireShooterManager(Entity* self) {
@@ -381,8 +419,6 @@ void BossSpiderQueenEnemyServer::RapidFireShooterManager(Entity* self) {
self->AddTimer("RFSTauntComplete", animationTime); self->AddTimer("RFSTauntComplete", animationTime);
ToggleForSpecial(self, false);
return; return;
} }
@@ -400,16 +436,10 @@ void BossSpiderQueenEnemyServer::RapidFireShooterManager(Entity* self) {
void BossSpiderQueenEnemyServer::RunRapidFireShooter(Entity* self) { void BossSpiderQueenEnemyServer::RunRapidFireShooter(Entity* self) {
const auto targets = self->GetTargetsInPhantom(); const auto targets = self->GetTargetsInPhantom();
if (self->GetBoolean(u"stoppedFlag")) {
self->AddTimer("RFS", GeneralUtils::GenerateRandomNumber<float>(5, 10));
return;
}
if (targets.empty()) { if (targets.empty()) {
LOG("Failed to find RFS targets"); LOG("Failed to find RFS targets");
self->AddTimer("RFS", GeneralUtils::GenerateRandomNumber<float>(5, 10)); self->AddTimer("PollSpiderSkillManager", static_cast<float>(GeneralUtils::GenerateRandomNumber<int32_t>(s1DelayMin, s1DelayMax)));
return; return;
} }
@@ -429,8 +459,6 @@ void BossSpiderQueenEnemyServer::RunRapidFireShooter(Entity* self) {
RapidFireShooterManager(self); RapidFireShooterManager(self);
PlayAnimAndReturnTime(self, spiderSingleShot); PlayAnimAndReturnTime(self, spiderSingleShot);
self->AddTimer("RFS", GeneralUtils::GenerateRandomNumber<float>(10, 15));
} }
void BossSpiderQueenEnemyServer::OnTimerDone(Entity* self, const std::string timerName) { 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") { } else if (timerName == "StartROF") {
//Re-enable Spider Boss //Re-enable Spider Boss
//ToggleForSpecial(self, false); ToggleForSpecial(self, false);
RainOfFireManager(self); RainOfFireManager(self);
} else if (timerName == "PollSpiderSkillManager") { } else if (timerName == "PollSpiderSkillManager") {
//Call the skill manager again to attempt to run the current Spider Boss //Call the skill manager again to attempt to run the current Spider Boss
//stage's special attack again //stage's special attack again
//SpiderSkillManager(self, true); SpiderSkillManager(self, true);
PlayAnimAndReturnTime(self, spiderJeerAnim);
} else if (timerName == "RFS") {
RunRapidFireShooter(self);
} else if (timerName == "ROF") {
RunRainOfFire(self);
} else if (timerName == "RFSTauntComplete") { } else if (timerName == "RFSTauntComplete") {
//Determine an appropriate random time to check our manager again //Set a timer based on a random cooldown to pulse the SpiderSkillManager again
// local spiderCooldownDelay = math.random(s1DelayMin, s1DelayMax) self->AddTimer("PollSpiderSkillManager", static_cast<float>(GeneralUtils::GenerateRandomNumber<int32_t>(s1DelayMin, s1DelayMax)));
//Set a timer based on our random cooldown determination
//to pulse the SpiderSkillManager again
//GAMEOBJ:GetTimer():AddTimerWithCancel(spiderCooldownDelay, "PollSpiderSkillManager", self)
//Re-enable Spider Boss //Re-enable Spider Boss
//ToggleForSpecial(self, false); ToggleForSpecial(self, false);
} else if (timerName == "WithdrawComplete") { } else if (timerName == "WithdrawComplete") {
//Play the Spider Boss' mountain idle anim //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? //Did we queue a spcial attack?
if (self->GetBoolean(u"bSpecialQueued")) { if (self->GetBoolean(u"bSpecialQueued")) {
self->SetBoolean(u"bSpecialQueued", false); self->SetBoolean(u"bSpecialQueued", false);
SpiderSkillManager(self, true);
} }
} }
} }
void BossSpiderQueenEnemyServer::OnHitOrHealResult(Entity* self, Entity* attacker, int32_t damage) { 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()) { if (m_CurrentBossStage > ThresholdTable.size()) {
return; return;
} }
@@ -616,8 +627,13 @@ void BossSpiderQueenEnemyServer::OnHitOrHealResult(Entity* self, Entity* attacke
if (!isWithdrawn) { if (!isWithdrawn) {
self->CancelAllTimers(); self->CancelAllTimers();
// Stop whatever special was running
attackTargetTable.clear();
impactList.clear();
self->SetBoolean(u"isSpecialAttacking", false); self->SetBoolean(u"isSpecialAttacking", false);
self->SetBoolean(u"rainOfFireStun", false);
self->SetBoolean(u"bSpecialLock", false); self->SetBoolean(u"bSpecialLock", false);
self->SetBoolean(u"bSpecialQueued", false);
WithdrawSpider(self, true); WithdrawSpider(self, true);
} }

View File

@@ -34,6 +34,13 @@ public:
void ToggleForSpecial(Entity* self, bool state); 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 RunRainOfFire(Entity* self);
void RainOfFireManager(Entity* self); void RainOfFireManager(Entity* self);
@@ -56,6 +63,7 @@ public:
private: private:
void ToggleAttacking(Entity& self, bool on); void ToggleAttacking(Entity& self, bool on);
void UpdateSpecialStun(Entity* self);
//Regular variables: //Regular variables:
DestroyableComponent* destroyable = nullptr; DestroyableComponent* destroyable = nullptr;
ControllablePhysicsComponent* controllable = nullptr; ControllablePhysicsComponent* controllable = nullptr;
@@ -102,10 +110,11 @@ private:
const float smashSkillLength = 3.1f; //Time (in seconds) the boss smash skill lasts 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 // Whole seconds, as live's math.random(min, max)
const float s1DelayMax = 15.0f; //Maximum time until calling for another Rapid Fire Shot const int32_t s1DelayMin = 10; //Minimum time until calling for another Rapid Fire Shot
const float s2DelayMin = 10.0f; //Minimum time until calling for another Rain Of Fire const int32_t s1DelayMax = 15; //Maximum time until calling for another Rapid Fire Shot
const float s2DelayMax = 15.0f; //Maximum time until calling for another Rain Of Fire 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 instanceZoneID = 1102; //Zone ID for the Spider Queen fight instance
const unsigned int instanceMissionID = 1941; //Achievement to update for beating the instanced Boss const unsigned int instanceMissionID = 1941; //Achievement to update for beating the instanced Boss

View File

@@ -93,3 +93,28 @@ TEST_F(SpiderQueenTest, OnlySpiderlingsMakeTheBossScream) {
const auto packets = Capture([&] { bossScript.OnNotifyObject(boss, other, "SpiderlingDied", 0, 0); }); const auto packets = Capture([&] { bossScript.OnNotifyObject(boss, other, "SpiderlingDied", 0, 0); });
EXPECT_TRUE(packets.empty()); 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"));
}