Files
DarkflameServer/tests/dGameTests/SpiderQueenTests.cpp
Aaron Kimbrell 300932c657 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>
2026-09-29 10:52:54 -05:00

178 lines
6.9 KiB
C++

#include <gtest/gtest.h>
#include "GameDependencies.h"
#include "Entity.h"
#include "ObjectMessages.h"
#include "dGameMessagesTests/GameMessageTestUtils.h"
#include "02_server/Enemy/AG/BossSpiderQueenEnemyServer.h"
#include "02_server/Map/Property/AG_Small/ZoneAgProperty.h"
// The AG property's Spider Queen fight (L_ZONE_AG_PROPERTY.lua, L_BOSS_SPIDER_QUEEN_ENEMY_SERVER.lua).
using namespace GameMessageTestUtils;
class SpiderQueenTest : public GameDependenciesTest {
protected:
static constexpr LWOOBJID ZONE = 70368744177662;
static constexpr LWOOBJID BOSS = 0x1000;
static constexpr LWOOBJID LAND_TARGET = 0x1001;
static constexpr LWOOBJID SCREAM_EMITTER = 0x1002;
static constexpr LWOOBJID SPIDERLING = 0x1003;
std::vector<std::unique_ptr<Entity>> entities;
Entity* zone = nullptr;
Entity* boss = nullptr;
ZoneAgProperty zoneScript;
BossSpiderQueenEnemyServer bossScript;
void SetUp() override {
SetUpDependencies();
zone = Add(ZONE, 0, {});
boss = Add(BOSS, 14381, { "SpiderBoss" });
zoneScript.SetGameVariables(zone);
}
void TearDown() override {
for (const auto& entity : entities) Game::entityManager->_removeEntity(entity->GetObjectID());
entities.clear();
TearDownDependencies();
}
Entity* Add(const LWOOBJID id, const LOT lot, const std::vector<std::string>& groups) {
info.lot = lot;
auto* const entity = entities.emplace_back(std::make_unique<Entity>(id, info)).get();
entity->GetGroups() = groups;
Game::entityManager->_addEntity(entity);
return entity;
}
};
TEST_F(SpiderQueenTest, ZoneHandsTheBossItsLandingTargetAndScreamEmitter) {
Add(LAND_TARGET, 14382, { "Land_Target" });
Add(SCREAM_EMITTER, 14383, { "Spider_Scream" });
zoneScript.BaseOnFireEventServerSide(zone, boss, "RetrieveZoneData");
EXPECT_EQ(zone->GetVar<LWOOBJID>(u"SpiderBossID"), BOSS);
EXPECT_EQ(boss->GetVar<LWOOBJID>(u"LandingTarget"), LAND_TARGET);
EXPECT_EQ(boss->GetVar<LWOOBJID>(u"ScreamEmitter"), SCREAM_EMITTER);
zone->Update(0.0f);
EXPECT_FALSE(zone->HasTimer("ProcessGroupObj_LandingTarget"));
EXPECT_FALSE(zone->HasTimer("ProcessGroupObj_ScreamEmitter"));
}
TEST_F(SpiderQueenTest, ZoneLooksAgainWhileTheObjectsAreNotSpawned) {
zoneScript.BaseOnFireEventServerSide(zone, boss, "RetrieveZoneData");
EXPECT_EQ(boss->GetVar<LWOOBJID>(u"LandingTarget"), LWOOBJID_EMPTY);
zone->Update(0.0f); // starts the pending timers
EXPECT_TRUE(zone->HasTimer("ProcessGroupObj_LandingTarget"));
EXPECT_TRUE(zone->HasTimer("ProcessGroupObj_ScreamEmitter"));
Add(SCREAM_EMITTER, 14383, { "Spider_Scream" });
zoneScript.OnTimerDone(zone, "ProcessGroupObj_ScreamEmitter");
EXPECT_EQ(boss->GetVar<LWOOBJID>(u"ScreamEmitter"), SCREAM_EMITTER);
}
TEST_F(SpiderQueenTest, ASpiderlingDeathMakesTheBossScreamFromTheMountain) {
boss->SetVar<LWOOBJID>(u"ScreamEmitter", SCREAM_EMITTER);
auto* const spiderling = Add(SPIDERLING, 16197, { "BabySpider" });
const auto packets = Capture([&] { bossScript.OnNotifyObject(boss, spiderling, "SpiderlingDied", 0, 0); });
const auto sent = SentGameMessages<GameMessages::NotifyClientObject>(packets);
ASSERT_EQ(sent.size(), 1u);
EXPECT_EQ(sent[0].target, BOSS);
EXPECT_EQ(sent[0].name, u"EmitScream");
EXPECT_EQ(sent[0].paramObj, SCREAM_EMITTER);
EXPECT_TRUE(packets[0].broadcast);
}
TEST_F(SpiderQueenTest, OnlySpiderlingsMakeTheBossScream) {
auto* const other = Add(SPIDERLING, 16196, {});
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"));
}
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) {
// 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"));
}