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()); +}