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?
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<float>(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<float>(20, 40));
self->AddTimer("PollSpiderSkillManager", static_cast<float>(GeneralUtils::GenerateRandomNumber<int32_t>(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<float>(5, 10));
return;
}
if (targets.empty()) {
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;
}
@@ -429,8 +459,6 @@ void BossSpiderQueenEnemyServer::RunRapidFireShooter(Entity* self) {
RapidFireShooterManager(self);
PlayAnimAndReturnTime(self, spiderSingleShot);
self->AddTimer("RFS", GeneralUtils::GenerateRandomNumber<float>(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<float>(GeneralUtils::GenerateRandomNumber<int32_t>(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);
}

View File

@@ -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