mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
fix(scripts): Spider Queen rapid fire sweeps the three zone target groups around a player (issue 2016)
The live boss script tracks which arena zone volume (Zone1Vol..Zone8Vol, TeleVol for the default Zone3Vol) each player last entered, and its rapid fire picks a random player, takes the three RFS target groups around that zone, sorts each by the targets' CWOrder (CWOrder2 when the sweep crosses between zones 8 and 1), clockwise or counter-clockwise at random, drops the first and last target of the middle group (shared with its neighbours), turns with skill 1480 at the fourth target and fires 1394 at every target in turn, playing attack-shoot-right or attack-shoot-left. The server shot a single random target with the single-shot animation, and the AG property zone never subscribed the boss to the zone volumes. The zone now registers the volumes (retrying until they are spawned) and the boss builds the sweep like the live script. Check in game: AG Spider Queen stage 2; the rapid fire is an arc of many shots sweeping across the arena near the player, left or right, and follows the player to other parts of the arena; after teleporting it starts from the default side. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@
|
|||||||
#include "PlayerManager.h"
|
#include "PlayerManager.h"
|
||||||
#include "eStateChangeType.h"
|
#include "eStateChangeType.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
//----------------------------------------------------------------
|
//----------------------------------------------------------------
|
||||||
@@ -434,31 +435,83 @@ void BossSpiderQueenEnemyServer::RapidFireShooterManager(Entity* self) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void BossSpiderQueenEnemyServer::RunRapidFireShooter(Entity* self) {
|
void BossSpiderQueenEnemyServer::RunRapidFireShooter(Entity* self) {
|
||||||
const auto targets = self->GetTargetsInPhantom();
|
// The sweep starts around a random player, from the zone volume that player last entered
|
||||||
|
const auto& players = PlayerManager::GetAllPlayers();
|
||||||
if (targets.empty()) {
|
if (players.empty()) {
|
||||||
LOG("Failed to find RFS targets");
|
|
||||||
|
|
||||||
self->AddTimer("PollSpiderSkillManager", static_cast<float>(GeneralUtils::GenerateRandomNumber<int32_t>(s1DelayMin, s1DelayMax)));
|
self->AddTimer("PollSpiderSkillManager", static_cast<float>(GeneralUtils::GenerateRandomNumber<int32_t>(s1DelayMin, s1DelayMax)));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ToggleForSpecial(self, true);
|
ToggleForSpecial(self, true);
|
||||||
|
|
||||||
const auto randomTarget = GeneralUtils::GenerateRandomNumber<int32_t>(0, targets.size() - 1);
|
const auto* const attackFocus = players[GeneralUtils::GenerateRandomNumber<size_t>(0, players.size() - 1)];
|
||||||
|
const auto inZone = inZoneTable.find(attackFocus->GetObjectID());
|
||||||
|
const auto& primaryZone = inZone != inZoneTable.end() ? inZone->second : defaultFacingZone;
|
||||||
|
|
||||||
auto attackFocus = targets[randomTarget];
|
// Clockwise or counter-clockwise at random
|
||||||
|
const bool clockwise = GeneralUtils::GenerateRandomNumber<int32_t>(1, 2) == 1;
|
||||||
|
|
||||||
attackTargetTable.push_back(attackFocus);
|
attackTargetTable = BuildRapidFireTargets(primaryZone, clockwise, [](const std::string& group) {
|
||||||
|
std::vector<RapidFireTarget> targets;
|
||||||
|
for (auto* const target : Game::entityManager->GetEntitiesInGroup(group)) {
|
||||||
|
targets.push_back({ target->GetObjectID(), target->GetVar<int32_t>(u"CWOrder"), target->GetVar<int32_t>(u"CWOrder2") });
|
||||||
|
}
|
||||||
|
return targets;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Turn towards the fourth target, then shoot every target in turn
|
||||||
auto* skillComponent = self->GetComponent<SkillComponent>();
|
auto* skillComponent = self->GetComponent<SkillComponent>();
|
||||||
|
skillComponent->CalculateBehavior(1480, 36652, attackTargetTable.size() >= 4 ? attackTargetTable[3] : LWOOBJID_EMPTY, true);
|
||||||
skillComponent->CalculateBehavior(1480, 36652, attackFocus, true);
|
|
||||||
|
|
||||||
RapidFireShooterManager(self);
|
RapidFireShooterManager(self);
|
||||||
|
|
||||||
PlayAnimAndReturnTime(self, spiderSingleShot);
|
PlayAnimAndReturnTime(self, clockwise ? spiderShootRght : spiderShootLeft);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<LWOOBJID> BossSpiderQueenEnemyServer::BuildRapidFireTargets(const std::string& primaryZone, const bool clockwise,
|
||||||
|
const std::function<std::vector<RapidFireTarget>(const std::string& group)>& groupTargets) {
|
||||||
|
std::vector<LWOOBJID> attackTargets;
|
||||||
|
|
||||||
|
const auto zoneGroups = rapidFireTargetTable.find(primaryZone);
|
||||||
|
if (zoneGroups == rapidFireTargetTable.end()) return attackTargets;
|
||||||
|
|
||||||
|
const auto& groups = zoneGroups->second;
|
||||||
|
const auto& primaryGroup = clockwise ? groups[0] : groups[2];
|
||||||
|
const auto& secondaryGroup = groups[1];
|
||||||
|
const auto& tertiaryGroup = clockwise ? groups[2] : groups[0];
|
||||||
|
|
||||||
|
// Zones 8 and 1 meet where CWOrder starts again; sweeps over that edge sort by CWOrder2
|
||||||
|
const bool crossesStart = std::ranges::any_of(groups, [](const std::string& group) {
|
||||||
|
return group == "Zone1Targets" || group == "Zone8Targets";
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const auto* const group : { &primaryGroup, &secondaryGroup, &tertiaryGroup }) {
|
||||||
|
auto targets = groupTargets(*group);
|
||||||
|
std::ranges::stable_sort(targets, [crossesStart, clockwise](const RapidFireTarget& a, const RapidFireTarget& b) {
|
||||||
|
const auto orderA = crossesStart ? a.cwOrder2 : a.cwOrder;
|
||||||
|
const auto orderB = crossesStart ? b.cwOrder2 : b.cwOrder;
|
||||||
|
return clockwise ? orderA < orderB : orderA > orderB;
|
||||||
|
});
|
||||||
|
|
||||||
|
// The middle group leaves out its first and last target
|
||||||
|
const bool isSecondary = group == &secondaryGroup;
|
||||||
|
for (size_t i = 0; i < targets.size(); i++) {
|
||||||
|
if (isSecondary && (i == 0 || i == targets.size() - 1)) continue;
|
||||||
|
attackTargets.push_back(targets[i].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attackTargets;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BossSpiderQueenEnemyServer::OnZoneVolumeEntered(Entity* self, Entity* player, const std::string& volumeGroup) {
|
||||||
|
if (!player || !player->IsPlayer()) return;
|
||||||
|
|
||||||
|
// The aggro volume is not a quadrant (the boss's aggro uses her proximity radius)
|
||||||
|
if (volumeGroup == "AggroVol") return;
|
||||||
|
|
||||||
|
// A teleported player faces the default zone
|
||||||
|
inZoneTable[player->GetObjectID()] = volumeGroup == "TeleVol" ? defaultFacingZone : volumeGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BossSpiderQueenEnemyServer::OnTimerDone(Entity* self, const std::string timerName) {
|
void BossSpiderQueenEnemyServer::OnTimerDone(Entity* self, const std::string timerName) {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "CppScripts.h"
|
#include "CppScripts.h"
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
/*
|
/*
|
||||||
--------------------------------------------------------------
|
--------------------------------------------------------------
|
||||||
|
|
||||||
@@ -49,6 +52,22 @@ public:
|
|||||||
|
|
||||||
void RunRapidFireShooter(Entity* self);
|
void RunRapidFireShooter(Entity* self);
|
||||||
|
|
||||||
|
// A target of the rapid fire shooter (groups Zone1Targets..Zone8Targets) and its clockwise order in the arena
|
||||||
|
struct RapidFireTarget {
|
||||||
|
LWOOBJID id = LWOOBJID_EMPTY;
|
||||||
|
int32_t cwOrder = 0;
|
||||||
|
int32_t cwOrder2 = 0; // the order where the sweep crosses between zones 8 and 1
|
||||||
|
};
|
||||||
|
|
||||||
|
// The rapid fire shooter's targets for a player in primaryZone (Zone1Vol..Zone8Vol), as the live script builds
|
||||||
|
// them: the three target groups around that zone, each sorted in the sweep direction, without the first and last
|
||||||
|
// target of the middle group
|
||||||
|
static std::vector<LWOOBJID> BuildRapidFireTargets(const std::string& primaryZone, bool clockwise,
|
||||||
|
const std::function<std::vector<RapidFireTarget>(const std::string& group)>& groupTargets);
|
||||||
|
|
||||||
|
// A player touched one of the arena's zone volumes (group Zone1Vol..Zone8Vol, AggroVol or TeleVol)
|
||||||
|
void OnZoneVolumeEntered(Entity* self, Entity* player, const std::string& volumeGroup);
|
||||||
|
|
||||||
float PlayAnimAndReturnTime(Entity* self, const std::u16string& animId);
|
float PlayAnimAndReturnTime(Entity* self, const std::u16string& animId);
|
||||||
|
|
||||||
void OnTimerDone(Entity* self, std::string timerName) override;
|
void OnTimerDone(Entity* self, std::string timerName) override;
|
||||||
@@ -89,8 +108,7 @@ private:
|
|||||||
std::vector<LWOOBJID> hatchList = {}; //Global list maintaining a record of all the eggs we have prepped to hatch for a wave
|
std::vector<LWOOBJID> hatchList = {}; //Global list maintaining a record of all the eggs we have prepped to hatch for a wave
|
||||||
|
|
||||||
const std::string defaultFacingZone = "Zone3Vol"; //Maintains a default facing to ensure appropriate Spider Boss positioning for teleported players
|
const std::string defaultFacingZone = "Zone3Vol"; //Maintains a default facing to ensure appropriate Spider Boss positioning for teleported players
|
||||||
std::vector<int> inZoneTable = {}; //Keeps track through player ID index what quadrant of the zone the player is in now
|
std::map<LWOOBJID, std::string> inZoneTable; //Keeps track through player ID index what quadrant of the zone the player is in now
|
||||||
std::vector<int> fromZoneTable = {}; //Keeps track through player ID index what quadrant of the zone the player was last in
|
|
||||||
|
|
||||||
const float defaultAnimPause = 2.5f; //Default period of time to pause between missing animation actions
|
const float defaultAnimPause = 2.5f; //Default period of time to pause between missing animation actions
|
||||||
const std::u16string spiderJeerAnim = u"taunt"; //Animation Spider Boss plays to antagonize the player
|
const std::u16string spiderJeerAnim = u"taunt"; //Animation Spider Boss plays to antagonize the player
|
||||||
@@ -120,7 +138,7 @@ private:
|
|||||||
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
|
||||||
|
|
||||||
//Establishes a link for the Spider to identify rapid fire targets based on zone reference
|
//Establishes a link for the Spider to identify rapid fire targets based on zone reference
|
||||||
const std::map<std::string, std::vector<std::string>> rapidFireTargetTable = {
|
static inline const std::map<std::string, std::vector<std::string>> rapidFireTargetTable = {
|
||||||
{"Zone1Vol", {"Zone8Targets", "Zone1Targets", "Zone2Targets"}},
|
{"Zone1Vol", {"Zone8Targets", "Zone1Targets", "Zone2Targets"}},
|
||||||
{"Zone2Vol", {"Zone1Targets", "Zone2Targets", "Zone3Targets"}},
|
{"Zone2Vol", {"Zone1Targets", "Zone2Targets", "Zone3Targets"}},
|
||||||
{"Zone3Vol", {"Zone2Targets", "Zone3Targets", "Zone4Targets"}},
|
{"Zone3Vol", {"Zone2Targets", "Zone3Targets", "Zone4Targets"}},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "ZoneAgProperty.h"
|
#include "ZoneAgProperty.h"
|
||||||
#include "GeneralUtils.h"
|
#include "GeneralUtils.h"
|
||||||
|
#include "BossSpiderQueenEnemyServer.h"
|
||||||
#include "EntityManager.h"
|
#include "EntityManager.h"
|
||||||
#include "Character.h"
|
#include "Character.h"
|
||||||
#include "Entity.h"
|
#include "Entity.h"
|
||||||
@@ -135,6 +136,32 @@ void ZoneAgProperty::ProcessGroupObjects(Entity* self, const std::string& group)
|
|||||||
spiderBoss->SetVar<LWOOBJID>(varName, objects.front()->GetObjectID());
|
spiderBoss->SetVar<LWOOBJID>(varName, objects.front()->GetObjectID());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ZoneAgProperty::ProcessZoneVolumes(Entity* self) {
|
||||||
|
// Tells the Spider Queen which of the arena's zone volumes each player walks into, so her rapid fire sweeps
|
||||||
|
// start where the player is
|
||||||
|
for (const auto& group : self->GetVar<std::vector<std::string>>(ZoneVolumesGroup)) {
|
||||||
|
const auto volumes = Game::entityManager->GetEntitiesInGroup(group);
|
||||||
|
if (volumes.empty()) {
|
||||||
|
// Not spawned yet: look again shortly
|
||||||
|
self->AddTimer("ProcessGroupObj_ZoneVolumes", 0.3f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* const volume = volumes.front();
|
||||||
|
if (volume->GetVar<bool>(u"spiderBossSensor")) continue; // already reporting
|
||||||
|
volume->SetVar<bool>(u"spiderBossSensor", true);
|
||||||
|
|
||||||
|
volume->AddCollisionPhantomCallback([zoneID = self->GetObjectID(), group](Entity* target) {
|
||||||
|
auto* const zone = Game::entityManager->GetEntity(zoneID);
|
||||||
|
if (!zone) return;
|
||||||
|
auto* const spiderBoss = Game::entityManager->GetEntity(zone->GetVar<LWOOBJID>(u"SpiderBossID"));
|
||||||
|
if (!spiderBoss) return;
|
||||||
|
auto* const bossScript = dynamic_cast<BossSpiderQueenEnemyServer*>(spiderBoss->GetScript());
|
||||||
|
if (bossScript) bossScript->OnZoneVolumeEntered(spiderBoss, target, group);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ZoneAgProperty::SpawnSpots(Entity* self) {
|
void ZoneAgProperty::SpawnSpots(Entity* self) {
|
||||||
for (const auto& spot : self->GetVar<std::vector<std::string>>(ROFTargetsSpawner)) {
|
for (const auto& spot : self->GetVar<std::vector<std::string>>(ROFTargetsSpawner)) {
|
||||||
ActivateSpawner(spot);
|
ActivateSpawner(spot);
|
||||||
@@ -313,6 +340,8 @@ void ZoneAgProperty::BaseTimerDone(Entity* self, const std::string& timerName) {
|
|||||||
ProcessGroupObjects(self, self->GetVar<std::string>(LandTargetGroup));
|
ProcessGroupObjects(self, self->GetVar<std::string>(LandTargetGroup));
|
||||||
} else if (timerName == "ProcessGroupObj_ScreamEmitter") {
|
} else if (timerName == "ProcessGroupObj_ScreamEmitter") {
|
||||||
ProcessGroupObjects(self, self->GetVar<std::string>(SpiderScreamGroup));
|
ProcessGroupObjects(self, self->GetVar<std::string>(SpiderScreamGroup));
|
||||||
|
} else if (timerName == "ProcessGroupObj_ZoneVolumes") {
|
||||||
|
ProcessZoneVolumes(self);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,7 +460,7 @@ void ZoneAgProperty::BaseOnFireEventServerSide(Entity* self, Entity* sender, std
|
|||||||
|
|
||||||
ProcessGroupObjects(self, self->GetVar<std::string>(LandTargetGroup));
|
ProcessGroupObjects(self, self->GetVar<std::string>(LandTargetGroup));
|
||||||
ProcessGroupObjects(self, self->GetVar<std::string>(SpiderScreamGroup));
|
ProcessGroupObjects(self, self->GetVar<std::string>(SpiderScreamGroup));
|
||||||
// ProcessGroupObjects(self, groups.ZoneVolumes);
|
ProcessZoneVolumes(self);
|
||||||
} else if (args == "CheckForPropertyOwner") {
|
} else if (args == "CheckForPropertyOwner") {
|
||||||
sender->SetNetworkVar<std::string>(u"PropertyOwnerID", std::to_string(self->GetVar<LWOOBJID>(u"PropertyOwner")));
|
sender->SetNetworkVar<std::string>(u"PropertyOwnerID", std::to_string(self->GetVar<LWOOBJID>(u"PropertyOwner")));
|
||||||
} else if (args == "ClearProperty") {
|
} else if (args == "ClearProperty") {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public:
|
|||||||
void LoadProperty(Entity* self);
|
void LoadProperty(Entity* self);
|
||||||
|
|
||||||
void ProcessGroupObjects(Entity* self, const std::string& group);
|
void ProcessGroupObjects(Entity* self, const std::string& group);
|
||||||
|
void ProcessZoneVolumes(Entity* self);
|
||||||
|
|
||||||
void SpawnSpots(Entity* self) override;
|
void SpawnSpots(Entity* self) override;
|
||||||
void KillSpots(Entity* self) override;
|
void KillSpots(Entity* self) override;
|
||||||
|
|||||||
@@ -24,4 +24,5 @@ target_precompile_headers(dScriptsServerMapProperty REUSE_FROM dScriptsBase)
|
|||||||
target_include_directories(dScriptsServerMapProperty PUBLIC "."
|
target_include_directories(dScriptsServerMapProperty PUBLIC "."
|
||||||
"AG_Med"
|
"AG_Med"
|
||||||
"AG_Small"
|
"AG_Small"
|
||||||
"NS_Med")
|
"NS_Med"
|
||||||
|
"../../Enemy/AG") # the AG property zone reports zone volumes to the Spider Queen
|
||||||
|
|||||||
@@ -112,6 +112,63 @@ TEST_F(SpiderQueenTest, TheMeleeSmashLocksTheSpecialsForItsLength) {
|
|||||||
EXPECT_FALSE(boss->GetBoolean(u"bSpecialQueued"));
|
EXPECT_FALSE(boss->GetBoolean(u"bSpecialQueued"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// The arena's 16 rapid fire targets as the AG property map places them: target n has CWOrder n and CWOrder2
|
||||||
|
// (n + 8) mod 16 (1 to 16); ZoneNTargets holds the targets 2N-1, 2N and 2N+1 (target 1 again after 16), so
|
||||||
|
// neighbouring groups share their edge target. The object ID is 100 + CWOrder.
|
||||||
|
std::vector<BossSpiderQueenEnemyServer::RapidFireTarget> ArenaGroup(const std::string& group) {
|
||||||
|
const int32_t zone = group[4] - '0';
|
||||||
|
std::vector<BossSpiderQueenEnemyServer::RapidFireTarget> targets;
|
||||||
|
for (int32_t n = 2 * zone - 1; n <= 2 * zone + 1; n++) {
|
||||||
|
const int32_t cwOrder = (n - 1) % 16 + 1;
|
||||||
|
targets.push_back({ 100 + cwOrder, cwOrder, (cwOrder + 7) % 16 + 1 });
|
||||||
|
}
|
||||||
|
// The sweep sorts them; hand them over out of order
|
||||||
|
std::ranges::reverse(targets);
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<LWOOBJID> Ids(const std::vector<int32_t>& cwOrders) {
|
||||||
|
std::vector<LWOOBJID> ids;
|
||||||
|
for (const auto order : cwOrders) ids.push_back(100 + order);
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SpiderQueenTest, RapidFireSweepsTheThreeZonesAroundThePlayerClockwise) {
|
||||||
|
EXPECT_EQ(BossSpiderQueenEnemyServer::BuildRapidFireTargets("Zone5Vol", true, ArenaGroup), Ids({ 7, 8, 9, 10, 11, 12, 13 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SpiderQueenTest, RapidFireSweepsCounterClockwise) {
|
||||||
|
EXPECT_EQ(BossSpiderQueenEnemyServer::BuildRapidFireTargets("Zone5Vol", false, ArenaGroup), Ids({ 13, 12, 11, 10, 9, 8, 7 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SpiderQueenTest, RapidFireSweepsOverTheEdgeBetweenZonesEightAndOne) {
|
||||||
|
EXPECT_EQ(BossSpiderQueenEnemyServer::BuildRapidFireTargets("Zone1Vol", true, ArenaGroup), Ids({ 15, 16, 1, 2, 3, 4, 5 }));
|
||||||
|
EXPECT_EQ(BossSpiderQueenEnemyServer::BuildRapidFireTargets("Zone8Vol", false, ArenaGroup), Ids({ 3, 2, 1, 16, 15, 14, 13 }));
|
||||||
|
EXPECT_EQ(BossSpiderQueenEnemyServer::BuildRapidFireTargets("Zone2Vol", true, ArenaGroup), Ids({ 1, 2, 3, 4, 5, 6, 7 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SpiderQueenTest, ZoneVolumesTellTheBossWhereThePlayerIs) {
|
||||||
|
for (const auto& group : { "Zone1Vol", "Zone2Vol", "Zone3Vol", "Zone4Vol", "Zone5Vol", "Zone6Vol", "Zone7Vol", "Zone8Vol", "AggroVol", "TeleVol" }) {
|
||||||
|
Add(0x2000 + static_cast<LWOOBJID>(entities.size()), 14400, { group });
|
||||||
|
}
|
||||||
|
zone->SetVar<LWOOBJID>(u"SpiderBossID", BOSS);
|
||||||
|
zoneScript.ProcessZoneVolumes(zone);
|
||||||
|
zoneScript.ProcessZoneVolumes(zone); // asked again: each volume reports once
|
||||||
|
for (const auto& entity : entities) {
|
||||||
|
if (entity->GetGroups().size() == 1 && entity->GetGroups()[0].ends_with("Vol")) {
|
||||||
|
EXPECT_TRUE(entity->GetVar<bool>(u"spiderBossSensor"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SpiderQueenTest, ZoneVolumesWaitForTheVolumesToSpawn) {
|
||||||
|
zoneScript.ProcessZoneVolumes(zone);
|
||||||
|
zone->Update(0.0f);
|
||||||
|
EXPECT_TRUE(zone->HasTimer("ProcessGroupObj_ZoneVolumes"));
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(SpiderQueenTest, NoSpecialsBeforeTheFirstWave) {
|
TEST_F(SpiderQueenTest, NoSpecialsBeforeTheFirstWave) {
|
||||||
// Stage 1 has no special attack; the skill manager does nothing
|
// Stage 1 has no special attack; the skill manager does nothing
|
||||||
const auto packets = Capture([&] { bossScript.SpiderSkillManager(boss, true); });
|
const auto packets = Capture([&] { bossScript.SpiderSkillManager(boss, true); });
|
||||||
|
|||||||
Reference in New Issue
Block a user