mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
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:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user