diff --git a/dGame/dUtilities/Loot.cpp b/dGame/dUtilities/Loot.cpp index 4315b3e72..d81f2522d 100644 --- a/dGame/dUtilities/Loot.cpp +++ b/dGame/dUtilities/Loot.cpp @@ -3,6 +3,7 @@ #include "LiveEvents.h" #include +#include #include #include "CDComponentsRegistryTable.h" @@ -97,21 +98,24 @@ std::map RollLootMatrix(uint32_t matrixIndex, float chanceMul return drops; } -// Generates a 'random' final position for the loot drop based on its input spawn position. -void CalcFinalDropPos(GameMessages::DropClientLoot& lootMsg) { - if (lootMsg.spawnPos != NiPoint3Constant::ZERO) { - lootMsg.bUsePosition = true; +void Loot::SetDropPositions(GameMessages::DropClientLoot& lootMsg, const bool sourceIsPlayer, const float angle) { + if (lootMsg.spawnPos == NiPoint3Constant::ZERO) return; - //Calculate where the loot will go: - uint16_t degree = GeneralUtils::GenerateRandomNumber(0, 360); - - double rad = degree * 3.14 / 180; - double sin_v = sin(rad) * 4.2; - double cos_v = cos(rad) * 4.2; - - const auto [x, y, z] = lootMsg.spawnPos; - lootMsg.finalPosition = NiPoint3(static_cast(x + sin_v), y, static_cast(z + cos_v)); + lootMsg.bUsePosition = sourceIsPlayer; + if (lootMsg.currency > 0) { + lootMsg.finalPosition = lootMsg.spawnPos; + return; } + + const auto [x, y, z] = lootMsg.spawnPos; + lootMsg.finalPosition = NiPoint3(x + std::sin(angle) * ITEM_DROP_DISTANCE, y, z + std::cos(angle) * ITEM_DROP_DISTANCE); +} + +// Picks where the loot lands, in a random direction. +void CalcFinalDropPos(GameMessages::DropClientLoot& lootMsg) { + const auto* const source = Game::entityManager->GetEntity(lootMsg.sourceID); + const auto angle = GeneralUtils::GenerateRandomNumber(0.0f, 6.28318531f); + Loot::SetDropPositions(lootMsg, source && source->IsPlayer(), angle); } // Visually drop the loot to all team members, though only the lootMsg.ownerID can pick it up diff --git a/dGame/dUtilities/Loot.h b/dGame/dUtilities/Loot.h index 266e35c6c..bc638eb4c 100644 --- a/dGame/dUtilities/Loot.h +++ b/dGame/dUtilities/Loot.h @@ -27,4 +27,17 @@ namespace Loot { void DropLoot(Entity* player, const LWOOBJID source, uint32_t matrixIndex, uint32_t minCoins, uint32_t maxCoins); void DropItem(Entity& player, GameMessages::DropClientLoot& lootMsg, bool useTeam = false, bool forceFfa = false); void DropActivityLoot(Entity* player, const LWOOBJID source, uint32_t activityID, int32_t rating = 0); + + // How far from where it spawns a dropped item lands, as live dropped them (median 10.0 units, uniform direction). + constexpr float ITEM_DROP_DISTANCE = 10.0f; + + /** + * Fills DropClientLoot's use_position and final_position the way live did (12,501 live DropClientLoot). + * use_position is set only when a player is the source (activity rewards such as the dragon and BONS chests): + * otherwise the client spawns the loot at the source object where it is on the client, and only falls back + * to the spawn position if that object is gone. Coins land where they spawn; items land ITEM_DROP_DISTANCE away + * at the given angle (radians, around Y). + * Nothing is set when there is no spawn position. + */ + void SetDropPositions(GameMessages::DropClientLoot& lootMsg, bool sourceIsPlayer, float angle); }; diff --git a/tests/dGameTests/CMakeLists.txt b/tests/dGameTests/CMakeLists.txt index 03b30e00f..3b9822895 100644 --- a/tests/dGameTests/CMakeLists.txt +++ b/tests/dGameTests/CMakeLists.txt @@ -19,6 +19,7 @@ set(DGAMETEST_SOURCES "SceneGhostingTests.cpp" "HolidayEventsTests.cpp" "SetCurrencySourceTests.cpp" + "LootDropPositionTests.cpp" ) add_subdirectory(dComponentsTests) diff --git a/tests/dGameTests/LootDropPositionTests.cpp b/tests/dGameTests/LootDropPositionTests.cpp new file mode 100644 index 000000000..f000d6993 --- /dev/null +++ b/tests/dGameTests/LootDropPositionTests.cpp @@ -0,0 +1,74 @@ +#include "GameDependencies.h" +#include "dGameMessagesTests/GameMessageTestUtils.h" + +#include "InventoryMessages.h" +#include "Loot.h" + +#include + +#include + +using namespace GameMessageTestUtils; + +// DropClientLoot's use_position and final_position, as live sent them (12,501 live DropClientLoot). +class LootDropPositionTests : public GameDependenciesTest { +protected: + void SetUp() override { SetUpDependencies(); } + void TearDown() override { TearDownDependencies(); } +}; + +// A live coin drop from an enemy with the player, loot and source IDs replaced: use_position false, final +// position equal to the spawn position. +TEST_F(LootDropPositionTests, CoinsLandWhereTheySpawn) { + GameMessages::DropClientLoot lootMsg; + lootMsg.target = 0x1000000000000001LL; + lootMsg.ownerID = 0x1000000000000001LL; + lootMsg.lootID = 0x0000000011223344LL; + lootMsg.sourceID = 0x0102030405060708LL; + lootMsg.currency = 1; + lootMsg.item = LOT_NULL; + lootMsg.spawnPos = NiPoint3(46.63258361816406f, 388.45635986328125f, -108.97083282470703f); + Loot::SetDropPositions(lootMsg, false, 1.0f); + + EXPECT_FALSE(lootMsg.bUsePosition); + EXPECT_EQ(lootMsg.finalPosition, lootMsg.spawnPos); + EXPECT_PACKET_EQ(FromHex( + "53 05 00 0c 00 00 00 00 01 00 00 00 00 00 00 10 1e 00 71 21 ce 90 9a 8e b0 90 c4 7c 76 70 80 40 00 00 3f ff ff ff " + "d1 0c c8 84 40 00 00 00 00 40 00 00 00 00 00 04 02 01 c1 81 41 00 c0 80 78 90 e7 48 4d 47 58 48 62 3e 3b 38 40", 595), StructPacket(lootMsg)); +} + +TEST_F(LootDropPositionTests, ItemsLandTenUnitsAwayOnTheGround) { + for (const float angle : { 0.0f, 1.0f, 3.14159265f, 5.5f }) { + GameMessages::DropClientLoot lootMsg; + lootMsg.item = 935; + lootMsg.spawnPos = NiPoint3(100.0f, 20.0f, -50.0f); + Loot::SetDropPositions(lootMsg, false, angle); + + EXPECT_FALSE(lootMsg.bUsePosition); + EXPECT_EQ(lootMsg.finalPosition.y, 20.0f); + const auto dx = lootMsg.finalPosition.x - 100.0f; + const auto dz = lootMsg.finalPosition.z + 50.0f; + EXPECT_NEAR(std::sqrt(dx * dx + dz * dz), Loot::ITEM_DROP_DISTANCE, 1e-3f); + EXPECT_NEAR(dx, std::sin(angle) * 10.0f, 1e-3f); + EXPECT_NEAR(dz, std::cos(angle) * 10.0f, 1e-3f); + } +} + +TEST_F(LootDropPositionTests, OnlyAPlayerSourceUsesThePosition) { + GameMessages::DropClientLoot lootMsg; + lootMsg.item = 935; + lootMsg.spawnPos = NiPoint3(1.0f, 2.0f, 3.0f); + Loot::SetDropPositions(lootMsg, true, 0.0f); + EXPECT_TRUE(lootMsg.bUsePosition); + + Loot::SetDropPositions(lootMsg, false, 0.0f); + EXPECT_FALSE(lootMsg.bUsePosition); +} + +TEST_F(LootDropPositionTests, NoSpawnPositionLeavesTheMessageAlone) { + GameMessages::DropClientLoot lootMsg; + lootMsg.item = 935; + Loot::SetDropPositions(lootMsg, true, 1.0f); + EXPECT_FALSE(lootMsg.bUsePosition); + EXPECT_EQ(lootMsg.finalPosition, NiPoint3Constant::ZERO); +}