From 5dba7d5136d89982c7c381ac37f4994298d9489e Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 10:43:06 -0500 Subject: [PATCH] fix(scripts): Spider Queen rain of fire hits two spots in each outer ring (issue 2016) The live boss script's rain of fire takes every target of the first ROF target group and ROFImpactCnt (2) different random targets of each other group. The server took one per group, so the rain was sparser than live. The impacts are now picked like the live script (without repeats inside a group). Check in game: AG Spider Queen stage 3; the rain of fire lands on the centre ring and on two spots in each outer ring. Co-Authored-By: Claude Opus 5.5 --- .../Enemy/AG/BossSpiderQueenEnemyServer.cpp | 44 +++++++++++-------- .../Enemy/AG/BossSpiderQueenEnemyServer.h | 7 ++- tests/dGameTests/SpiderQueenTests.cpp | 7 +++ 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp index e66772cd0..20bad6535 100644 --- a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp +++ b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp @@ -352,31 +352,20 @@ void BossSpiderQueenEnemyServer::RunRainOfFire(Entity* self) { ToggleForSpecial(self, true); SetRainOfFireStun(self, true); - impactList.clear(); - - auto index = 0u; + std::vector> targetGroups; for (const auto& rofGroup : ROFTargetGroupIDTable) { - const auto spawners = Game::zoneManager->GetSpawnersInGroup(rofGroup); - - std::vector spawned; - - for (auto* spawner : spawners) { + auto& spawned = targetGroups.emplace_back(); + for (auto* spawner : Game::zoneManager->GetSpawnersInGroup(rofGroup)) { for (const auto* node : spawner->m_Info.nodes) { spawned.insert(spawned.end(), node->entities.begin(), node->entities.end()); } } - - if (index == 0) { - impactList.insert(impactList.end(), spawned.begin(), spawned.end()); - } else if (!spawned.empty()) { - const auto randomIndex = GeneralUtils::GenerateRandomNumber(0, spawned.size() - 1); - - impactList.push_back(spawned[randomIndex]); - } - - index++; } + impactList = PickRainOfFireImpacts(targetGroups, [](const size_t count) { + return GeneralUtils::GenerateRandomNumber(0, count - 1); + }); + const auto animTime = PlayAnimAndReturnTime(self, spiderROFAnim); self->AddTimer("StartROF", animTime); @@ -504,6 +493,25 @@ std::vector BossSpiderQueenEnemyServer::BuildRapidFireTargets(const st return attackTargets; } +std::vector BossSpiderQueenEnemyServer::PickRainOfFireImpacts(const std::vector>& groups, + const std::function& randomIndex) { + std::vector impacts; + for (size_t i = 0; i < groups.size(); i++) { + if (i == 0) { + impacts.insert(impacts.end(), groups[i].begin(), groups[i].end()); + continue; + } + + auto remaining = groups[i]; + for (size_t picked = 0; picked < ROFImpactCnt && !remaining.empty(); picked++) { + const auto index = randomIndex(remaining.size()); + impacts.push_back(remaining[index]); + remaining.erase(remaining.begin() + static_cast(index)); + } + } + return impacts; +} + void BossSpiderQueenEnemyServer::OnZoneVolumeEntered(Entity* self, Entity* player, const std::string& volumeGroup) { if (!player || !player->IsPlayer()) return; diff --git a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h index a5495314f..5575b4753 100644 --- a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h +++ b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h @@ -65,6 +65,11 @@ public: static std::vector BuildRapidFireTargets(const std::string& primaryZone, bool clockwise, const std::function(const std::string& group)>& groupTargets); + // The rain of fire's impacts: every target of the first group and ROFImpactCnt different random targets of each + // other group. randomIndex(n) returns a number in [0, n). + static std::vector PickRainOfFireImpacts(const std::vector>& groups, + const std::function& randomIndex); + // 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); @@ -99,7 +104,7 @@ private: const std::vector spiderWaveCntTable = { 2, 3 }; //The number of Dark Spiderling enemies to spawn per indexed wave number - const int ROFImpactCnt = 2; //The number of ROF impacts in each quadrant of the arena selected at random + static constexpr size_t ROFImpactCnt = 2; //The number of ROF impacts in each quadrant of the arena selected at random const LOT SpiderlingID = 16197; //Reference obj ID for hatched Spiderlings diff --git a/tests/dGameTests/SpiderQueenTests.cpp b/tests/dGameTests/SpiderQueenTests.cpp index 2b98f9325..d049be259 100644 --- a/tests/dGameTests/SpiderQueenTests.cpp +++ b/tests/dGameTests/SpiderQueenTests.cpp @@ -149,6 +149,13 @@ TEST_F(SpiderQueenTest, RapidFireSweepsOverTheEdgeBetweenZonesEightAndOne) { EXPECT_EQ(BossSpiderQueenEnemyServer::BuildRapidFireTargets("Zone2Vol", true, ArenaGroup), Ids({ 1, 2, 3, 4, 5, 6, 7 })); } +TEST_F(SpiderQueenTest, RainOfFireTakesTheFirstGroupAndTwoOfEachOther) { + const std::vector> groups = { { 1, 2, 3 }, { 10, 11, 12, 13 }, { 20 }, {}, { 40, 41 } }; + // Always the last remaining target + const auto impacts = BossSpiderQueenEnemyServer::PickRainOfFireImpacts(groups, [](const size_t count) { return count - 1; }); + EXPECT_EQ(impacts, (std::vector{ 1, 2, 3, 13, 12, 20, 41, 40 })); +} + TEST_F(SpiderQueenTest, ZoneVolumesTellTheBossWhereThePlayerIs) { for (const auto& group : { "Zone1Vol", "Zone2Vol", "Zone3Vol", "Zone4Vol", "Zone5Vol", "Zone6Vol", "Zone7Vol", "Zone8Vol", "AggroVol", "TeleVol" }) { Add(0x2000 + static_cast(entities.size()), 14400, { group });