From 4900f11cf1f6ee9de8c6333e1126a15b7b557d85 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 10:16:02 -0500 Subject: [PATCH] fix(scripts): the Spider Queen screams from the mountain when a spiderling dies (issue 307) The zone script never handed the boss her landing target and scream emitter: ZoneAgProperty::ProcessGroupObjects was empty and nothing answered the boss client script's "QueryZoneScript" event. As the live scripts: the boss asks the zone ("RetrieveZoneData"), the zone stores the first object of Land_Target and Spider_Scream on her as LandingTarget and ScreamEmitter (looking again every 0.3 s until they are spawned), and each spiderling death sends NotifyClientObject "EmitScream" with the emitter, which the boss's client script plays as the scream. The landing skill and camera shake now come from the landing target, not the boss. Check in game: AG Spider Queen (property or instance): kill a spiderling: the scream plays from the mountain; when she comes back down, the landing blast hits around the landing spot and the camera shakes. Co-Authored-By: Claude Opus 5.5 --- .../Enemy/AG/BossSpiderQueenEnemyServer.cpp | 45 +++++---- .../Enemy/AG/BossSpiderQueenEnemyServer.h | 6 ++ .../Property/AG_Small/EnemySpiderSpawner.cpp | 9 ++ .../Map/Property/AG_Small/ZoneAgProperty.cpp | 27 +++++- .../Map/Property/AG_Small/ZoneAgProperty.h | 2 +- tests/dGameTests/CMakeLists.txt | 1 + tests/dGameTests/SpiderQueenTests.cpp | 95 +++++++++++++++++++ 7 files changed, 161 insertions(+), 24 deletions(-) create mode 100644 tests/dGameTests/SpiderQueenTests.cpp diff --git a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp index 879ae3c48..65cb8be7c 100644 --- a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp +++ b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.cpp @@ -75,6 +75,21 @@ void BossSpiderQueenEnemyServer::OnProximityUpdate(Entity* self, Entity* enterin self->SetVar(u"player_count", playerCount); } +void BossSpiderQueenEnemyServer::OnFireEventServerSide(Entity* self, Entity* sender, std::string args, int32_t param1, int32_t param2, int32_t param3) { + if (args != "QueryZoneScript") return; + + // The zone script sets SpiderEggNetworkID, LandingTarget and ScreamEmitter on the boss + auto* const zoneControl = Game::entityManager->GetZoneControlEntity(); + if (zoneControl) zoneControl->OnFireEventServerSide(self, "RetrieveZoneData"); +} + +void BossSpiderQueenEnemyServer::OnNotifyObject(Entity* self, Entity* sender, const std::string& name, int32_t param1, int32_t param2) { + if (name != "SpiderlingDied" || !sender || sender->GetLOT() != SpiderlingID) return; + + // The boss's client script plays the mountain scream from the emitter it is given + GameMessages::NotifyClientObject(self->GetObjectID(), u"EmitScream", 0, 0, self->GetVar(u"ScreamEmitter"), "").Send(UNASSIGNED_SYSTEM_ADDRESS); +} + void BossSpiderQueenEnemyServer::OnDie(Entity* self, Entity* killer) { if (Game::zoneManager->GetZoneID().GetMapID() == instanceZoneID && killer) { for (const auto& player : PlayerManager::GetAllPlayers()) { @@ -512,30 +527,20 @@ void BossSpiderQueenEnemyServer::OnTimerDone(Entity* self, const std::string tim landingTarget:PlayEmbeddedEffectOnAllClientsNearObject{radius = 100, fromObjectID = landingTarget, effectName = "camshake-bridge"} }*/ - auto landingTarget = self->GetI64(u"LandingTarget"); - auto landingEntity = Game::entityManager->GetEntity(landingTarget); - - auto* skillComponent = self->GetComponent(); - - if (skillComponent != nullptr) { - skillComponent->CalculateBehavior(bossLandingSkill, 37739, LWOOBJID_EMPTY); - } - + // The landing target casts the landing skill and shakes the camera, not the boss + auto* const landingEntity = Game::entityManager->GetEntity(self->GetVar(u"LandingTarget")); if (landingEntity) { - auto* landingSkill = landingEntity->GetComponent(); + auto* const landingSkill = landingEntity->GetComponent(); + if (landingSkill) landingSkill->CalculateBehavior(bossLandingSkill, 37739, LWOOBJID_EMPTY, true); - if (landingSkill != nullptr) { - landingSkill->CalculateBehavior(bossLandingSkill, 37739, LWOOBJID_EMPTY, true); - } + GameMessages::PlayEmbeddedEffectOnAllClientsNearObject embeddedEffect; + embeddedEffect.target = landingEntity->GetObjectID(); + embeddedEffect.effectName = u"camshake-bridge"; + embeddedEffect.fromObjectID = landingEntity->GetObjectID(); + embeddedEffect.radius = 100.0f; + embeddedEffect.Send(UNASSIGNED_SYSTEM_ADDRESS); } - GameMessages::PlayEmbeddedEffectOnAllClientsNearObject embeddedEffect; - embeddedEffect.target = self->GetObjectID(); - embeddedEffect.effectName = u"camshake-bridge"; - embeddedEffect.fromObjectID = self->GetObjectID(); - embeddedEffect.radius = 100.0f; - embeddedEffect.Send(UNASSIGNED_SYSTEM_ADDRESS); - } else if (timerName == "AdvanceComplete") { GameMessages::NotifyClientObject(self->GetObjectID(), u"SetColGroup", 11, 0, 0, "").Send(UNASSIGNED_SYSTEM_ADDRESS); diff --git a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h index 0f975abe8..9f1a4fc8d 100644 --- a/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h +++ b/dScripts/02_server/Enemy/AG/BossSpiderQueenEnemyServer.h @@ -48,6 +48,12 @@ public: void OnProximityUpdate(Entity* self, Entity* entering, std::string name, std::string status); + // The boss's client script asks for the zone data ("QueryZoneScript") once the boss renders + void OnFireEventServerSide(Entity* self, Entity* sender, std::string args, int32_t param1, int32_t param2, int32_t param3) override; + + // "SpiderlingDied" from the spiderlings the eggs hatch (live: notifyDie on the spiderling) + void OnNotifyObject(Entity* self, Entity* sender, const std::string& name, int32_t param1, int32_t param2) override; + private: void ToggleAttacking(Entity& self, bool on); //Regular variables: diff --git a/dScripts/02_server/Map/Property/AG_Small/EnemySpiderSpawner.cpp b/dScripts/02_server/Map/Property/AG_Small/EnemySpiderSpawner.cpp index 4b5e74fe1..fd0ee59f0 100644 --- a/dScripts/02_server/Map/Property/AG_Small/EnemySpiderSpawner.cpp +++ b/dScripts/02_server/Map/Property/AG_Small/EnemySpiderSpawner.cpp @@ -53,6 +53,15 @@ void EnemySpiderSpawner::OnTimerDone(Entity* self, std::string timerName) { if (newEntity) { Game::entityManager->ConstructEntity(newEntity); newEntity->GetGroups().push_back("BabySpider"); + + // The Spider Queen screams from the mountain when one of her spiderlings dies + const auto spawnOwner = self->GetI64(u"SpawnOwner"); + const auto spiderlingID = newEntity->GetObjectID(); + newEntity->AddDieCallback([spawnOwner, spiderlingID]() { + auto* const spiderBoss = Game::entityManager->GetEntity(spawnOwner); + auto* const spiderling = Game::entityManager->GetEntity(spiderlingID); + if (spiderBoss && spiderling) spiderBoss->GetScript()->OnNotifyObject(spiderBoss, spiderling, "SpiderlingDied"); + }); } self->ScheduleKillAfterUpdate(); diff --git a/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.cpp b/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.cpp index fb89b36c4..80dea74c4 100644 --- a/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.cpp +++ b/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.cpp @@ -1,4 +1,5 @@ #include "ZoneAgProperty.h" +#include "GeneralUtils.h" #include "EntityManager.h" #include "Character.h" #include "Entity.h" @@ -113,7 +114,25 @@ void ZoneAgProperty::LoadProperty(Entity* self) { ActivateSpawner(self->GetVar(MailboxSpawner)); } -void ZoneAgProperty::ProcessGroupObjects(Entity* self, std::string group) { +void ZoneAgProperty::ProcessGroupObjects(Entity* self, const std::string& group) { + // Hands the Spider Queen the first object of the group: the landing target casts her landing skill, + // the mountain emitter plays her scream + std::u16string varName; + if (group == self->GetVar(LandTargetGroup)) varName = u"LandingTarget"; + else if (group == self->GetVar(SpiderScreamGroup)) varName = u"ScreamEmitter"; + else return; + + auto* const spiderBoss = Game::entityManager->GetEntity(self->GetVar(u"SpiderBossID")); + if (!spiderBoss) return; + + const auto objects = Game::entityManager->GetEntitiesInGroup(group); + if (objects.empty()) { + // Not spawned yet: look again shortly + self->AddTimer("ProcessGroupObj_" + GeneralUtils::UTF16ToWTF8(varName), 0.3f); + return; + } + + spiderBoss->SetVar(varName, objects.front()->GetObjectID()); } void ZoneAgProperty::SpawnSpots(Entity* self) { @@ -290,8 +309,10 @@ void ZoneAgProperty::BaseTimerDone(Entity* self, const std::string& timerName) { DestroySpawner(self->GetVar(FXManagerSpawner)); self->SetVar(u"FXObjectGone", true); - } else if (timerName == "ProcessGroupObj") { - // TODO + } else if (timerName == "ProcessGroupObj_LandingTarget") { + ProcessGroupObjects(self, self->GetVar(LandTargetGroup)); + } else if (timerName == "ProcessGroupObj_ScreamEmitter") { + ProcessGroupObjects(self, self->GetVar(SpiderScreamGroup)); } } diff --git a/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.h b/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.h index b1720783d..578f60a99 100644 --- a/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.h +++ b/dScripts/02_server/Map/Property/AG_Small/ZoneAgProperty.h @@ -30,7 +30,7 @@ public: void LoadInstance(Entity* self); void LoadProperty(Entity* self); - void ProcessGroupObjects(Entity* self, std::string group); + void ProcessGroupObjects(Entity* self, const std::string& group); void SpawnSpots(Entity* self) override; void KillSpots(Entity* self) override; diff --git a/tests/dGameTests/CMakeLists.txt b/tests/dGameTests/CMakeLists.txt index 99a3766b4..d3179832d 100644 --- a/tests/dGameTests/CMakeLists.txt +++ b/tests/dGameTests/CMakeLists.txt @@ -22,6 +22,7 @@ set(DGAMETEST_SOURCES "HolidayEventsTests.cpp" "SetCurrencySourceTests.cpp" "LootDropPositionTests.cpp" + "SpiderQueenTests.cpp" ) add_subdirectory(dComponentsTests) diff --git a/tests/dGameTests/SpiderQueenTests.cpp b/tests/dGameTests/SpiderQueenTests.cpp new file mode 100644 index 000000000..990eafbdf --- /dev/null +++ b/tests/dGameTests/SpiderQueenTests.cpp @@ -0,0 +1,95 @@ +#include + +#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> 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& groups) { + info.lot = lot; + auto* const entity = entities.emplace_back(std::make_unique(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(u"SpiderBossID"), BOSS); + EXPECT_EQ(boss->GetVar(u"LandingTarget"), LAND_TARGET); + EXPECT_EQ(boss->GetVar(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(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(u"ScreamEmitter"), SCREAM_EMITTER); +} + +TEST_F(SpiderQueenTest, ASpiderlingDeathMakesTheBossScreamFromTheMountain) { + boss->SetVar(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(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()); +}