#include "GameDependencies.h" #include #include "BitStream.h" #include "DestroyableComponent.h" #include "Entity.h" #include "eReplicaComponentType.h" #include "eStateChangeType.h" #include "Character.h" #include "CharacterComponent.h" #include "BaseCombatAIComponent.h" #include "CDClientDatabase.h" #include "CombatMessages.h" #include "dGameMessagesTests/GameMessageTestUtils.h" #include "PacketTestUtils.h" #include "dCommonVars.h" #include #include #include namespace { // The first game message with this ID among captured packets, read into T. template std::optional FindGameMessage(const std::vector& packets, const MessageType::Game id) { for (const auto& packet : packets) { RakNet::BitStream bitStream(const_cast(packet.bytes.data()), packet.bytes.size(), false); bitStream.IgnoreBytes(8); // CLIENT GAME_MSG header LWOOBJID target{}; MessageType::Game messageID{}; if (!bitStream.Read(target) || !bitStream.Read(messageID) || messageID != id) continue; T msg; if (!msg.Deserialize(bitStream)) continue; msg.target = target; return msg; } return std::nullopt; } } class DestroyableTest : public GameDependenciesTest { protected: Entity* baseEntity; DestroyableComponent* destroyableComponent; RakNet::BitStream bitStream; uint32_t flags = 0; void SetUp() override { SetUpDependencies(); baseEntity = new Entity(15, GameDependenciesTest::info); destroyableComponent = baseEntity->AddComponent(-1); // Initialize some values to be not default destroyableComponent->SetMaxHealth(12345.0f); destroyableComponent->SetHealth(23); destroyableComponent->SetMaxArmor(14.0f); destroyableComponent->SetArmor(7); destroyableComponent->SetMaxImagination(14000.0f); destroyableComponent->SetImagination(6000); destroyableComponent->SetIsSmashable(true); destroyableComponent->SetExplodeFactor(1.1f); destroyableComponent->AddFactionNoLookup(-1); destroyableComponent->AddFactionNoLookup(6); } void TearDown() override { delete baseEntity; TearDownDependencies(); } }; TEST_F(DestroyableTest, PlacementNewAddComponentTest) { ASSERT_NE(destroyableComponent, nullptr); ASSERT_EQ(destroyableComponent->GetArmor(), 7); baseEntity->AddComponent(-1); ASSERT_NE(baseEntity->GetComponent(), nullptr); ASSERT_EQ(destroyableComponent->GetArmor(), 0); } /** * Test Construction of a DestroyableComponent */ TEST_F(DestroyableTest, DestroyableComponentSerializeConstructionTest) { destroyableComponent->Serialize(bitStream, true); // Assert that the full number of bits are present ASSERT_EQ(bitStream.GetNumberOfUnreadBits(), 748); { // Now read in the full serialized construction BitStream bool optionStatusImmunityInfo{}; uint32_t ImmuneToBasicAttackCount{}; uint32_t ImmuneToDamageOverTimeCount{}; uint32_t ImmuneToKnockbackCount{}; uint32_t ImmuneToInterruptCount{}; uint32_t ImmuneToSpeedCount{}; uint32_t ImmuneToImaginationGainCount{}; uint32_t ImmuneToImaginationLossCount{}; uint32_t ImmuneToQuickbuildInterruptCount{}; uint32_t ImmuneToPullToPointCount{}; bool optionStatsInfo{}; uint32_t currentHealth{}; float maxHealth{}; uint32_t currentArmor{}; float maxArmor{}; uint32_t currentImagination{}; float maxImagination{}; uint32_t damageAbsorptionPoints{}; bool hasImmunity{}; bool isGmImmune{}; bool isShielded{}; float actualMaxHealth{}; float actualMaxArmor{}; float actualMaxImagination{}; uint32_t factionsSize{}; std::vector factions{}; bool isSmashable{}; bool isDead{}; bool isSmashed{}; bool isModuleAssembly{}; bool optionExplodeFactor{}; float explodeFactor{}; bool optionIsOnThreatList{}; bool isThreatened{}; bitStream.Read(optionStatusImmunityInfo); bitStream.Read(ImmuneToBasicAttackCount); bitStream.Read(ImmuneToDamageOverTimeCount); bitStream.Read(ImmuneToKnockbackCount); bitStream.Read(ImmuneToInterruptCount); bitStream.Read(ImmuneToSpeedCount); bitStream.Read(ImmuneToImaginationGainCount); bitStream.Read(ImmuneToImaginationLossCount); bitStream.Read(ImmuneToQuickbuildInterruptCount); bitStream.Read(ImmuneToPullToPointCount); bitStream.Read(optionStatsInfo); bitStream.Read(currentHealth); bitStream.Read(maxHealth); bitStream.Read(currentArmor); bitStream.Read(maxArmor); bitStream.Read(currentImagination); bitStream.Read(maxImagination); bitStream.Read(damageAbsorptionPoints); bitStream.Read(hasImmunity); bitStream.Read(isGmImmune); bitStream.Read(isShielded); bitStream.Read(actualMaxHealth); bitStream.Read(actualMaxArmor); bitStream.Read(actualMaxImagination); bitStream.Read(factionsSize); for (uint32_t i = 0; i < factionsSize; i++) { int32_t factionID{}; bitStream.Read(factionID); factions.push_back(factionID); } bitStream.Read(isSmashable); // This is an option later and also a flag at this spot bitStream.Read(isDead); bitStream.Read(isSmashed); // if IsSmashable is true, read the next bits. bitStream.Read(isModuleAssembly); bitStream.Read(optionExplodeFactor); bitStream.Read(explodeFactor); bitStream.Read(optionIsOnThreatList); bitStream.Read(isThreatened); EXPECT_EQ(optionStatusImmunityInfo, true); EXPECT_EQ(ImmuneToBasicAttackCount, 0); EXPECT_EQ(ImmuneToDamageOverTimeCount, 0); EXPECT_EQ(ImmuneToKnockbackCount, 0); EXPECT_EQ(ImmuneToInterruptCount, 0); EXPECT_EQ(ImmuneToSpeedCount, 0); EXPECT_EQ(ImmuneToImaginationGainCount, 0); EXPECT_EQ(ImmuneToImaginationLossCount, 0); EXPECT_EQ(ImmuneToQuickbuildInterruptCount, 0); EXPECT_EQ(ImmuneToPullToPointCount, 0); EXPECT_EQ(optionStatsInfo, true); EXPECT_EQ(currentHealth, 23); EXPECT_EQ(maxHealth, 12345.0f); EXPECT_EQ(currentArmor, 7); EXPECT_EQ(maxArmor, 14.0f); EXPECT_EQ(currentImagination, 6000); EXPECT_EQ(maxImagination, 14000.0f); EXPECT_EQ(damageAbsorptionPoints, 0.0f); EXPECT_EQ(hasImmunity, false); EXPECT_EQ(isGmImmune, false); EXPECT_EQ(isShielded, false); EXPECT_EQ(actualMaxHealth, 12345.0f); EXPECT_EQ(actualMaxArmor, 14.0f); EXPECT_EQ(actualMaxImagination, 14000.0f); EXPECT_EQ(factionsSize, 2); EXPECT_NE(std::find(factions.begin(), factions.end(), -1), factions.end()); EXPECT_NE(std::find(factions.begin(), factions.end(), 6), factions.end()); EXPECT_EQ(isSmashable, true); EXPECT_EQ(isDead, false); EXPECT_EQ(isSmashed, false); EXPECT_EQ(isSmashable, true); // For the sake of readability with the struct viewers, we will test this twice since its used as an option here, but as a bool above. EXPECT_EQ(isModuleAssembly, false); EXPECT_EQ(optionExplodeFactor, true); EXPECT_EQ(explodeFactor, 1.1f); EXPECT_EQ(optionIsOnThreatList, true); EXPECT_EQ(isThreatened, false); } bitStream.Reset(); } /** * Test serialization of a DestroyableComponent */ TEST_F(DestroyableTest, DestroyableComponentSerializeTest) { bitStream.Reset(); // Initialize some values to be not default so we can test a full serialization destroyableComponent->SetMaxHealth(1233.0f); // Now we test a serialization for correctness. destroyableComponent->Serialize(bitStream, false); ASSERT_EQ(bitStream.GetNumberOfUnreadBits(), 422); { // Now read in the full serialized BitStream bool optionStatsInfo{}; uint32_t currentHealth{}; float maxHealth{}; uint32_t currentArmor{}; float maxArmor{}; uint32_t currentImagination{}; float maxImagination{}; uint32_t damageAbsorptionPoints{}; bool hasImmunity{}; bool isGmImmune{}; bool isShielded{}; float actualMaxHealth{}; float actualMaxArmor{}; float actualMaxImagination{}; uint32_t factionsSize{}; std::vector factions{}; bool isSmashable{}; bool optionIsOnThreatList{}; bitStream.Read(optionStatsInfo); bitStream.Read(currentHealth); bitStream.Read(maxHealth); bitStream.Read(currentArmor); bitStream.Read(maxArmor); bitStream.Read(currentImagination); bitStream.Read(maxImagination); bitStream.Read(damageAbsorptionPoints); bitStream.Read(hasImmunity); bitStream.Read(isGmImmune); bitStream.Read(isShielded); bitStream.Read(actualMaxHealth); bitStream.Read(actualMaxArmor); bitStream.Read(actualMaxImagination); bitStream.Read(factionsSize); for (uint32_t i = 0; i < factionsSize; i++) { int32_t factionID{}; bitStream.Read(factionID); factions.push_back(factionID); } bitStream.Read(isSmashable); bitStream.Read(optionIsOnThreatList); EXPECT_EQ(optionStatsInfo, true); EXPECT_EQ(currentHealth, 23); EXPECT_EQ(maxHealth, 1233.0f); EXPECT_EQ(currentArmor, 7); EXPECT_EQ(maxArmor, 14.0f); EXPECT_EQ(currentImagination, 6000); EXPECT_EQ(maxImagination, 14000.0f); EXPECT_EQ(damageAbsorptionPoints, 0.0f); EXPECT_EQ(hasImmunity, false); EXPECT_EQ(isGmImmune, false); EXPECT_EQ(isShielded, false); EXPECT_EQ(actualMaxHealth, 1233.0f); EXPECT_EQ(actualMaxArmor, 14.0f); EXPECT_EQ(actualMaxImagination, 14000.0f); EXPECT_EQ(factionsSize, 2); EXPECT_NE(std::find(factions.begin(), factions.end(), -1), factions.end()); EXPECT_NE(std::find(factions.begin(), factions.end(), 6), factions.end()); EXPECT_EQ(isSmashable, true); EXPECT_EQ(optionIsOnThreatList, false); // Always zero for now on serialization } } /** * Test the Damage method of DestroyableComponent */ TEST_F(DestroyableTest, DestroyableComponentDamageTest) { // Do some actions destroyableComponent->SetMaxHealth(100.0f); destroyableComponent->SetHealth(100); destroyableComponent->SetMaxArmor(0.0f); destroyableComponent->Damage(10, LWOOBJID_EMPTY); // Check that we take damage ASSERT_EQ(destroyableComponent->GetHealth(), 90); // Check that if we have armor, we take the correct amount of damage destroyableComponent->SetMaxArmor(10.0f); destroyableComponent->SetArmor(5); destroyableComponent->Damage(10, LWOOBJID_EMPTY); ASSERT_EQ(destroyableComponent->GetHealth(), 85); // Check that if we have damage absorption we take the correct damage destroyableComponent->SetDamageToAbsorb(10); destroyableComponent->Damage(9, LWOOBJID_EMPTY); ASSERT_EQ(destroyableComponent->GetHealth(), 85); ASSERT_EQ(destroyableComponent->GetDamageToAbsorb(), 1); destroyableComponent->Damage(6, LWOOBJID_EMPTY); ASSERT_EQ(destroyableComponent->GetHealth(), 80); // Check that we take the correct reduced damage if we take reduced damage destroyableComponent->SetDamageReduction(2); destroyableComponent->Damage(7, LWOOBJID_EMPTY); ASSERT_EQ(destroyableComponent->GetHealth(), 75); destroyableComponent->Damage(2, LWOOBJID_EMPTY); ASSERT_EQ(destroyableComponent->GetHealth(), 74); ASSERT_EQ(destroyableComponent->GetDamageReduction(), 2); destroyableComponent->SetDamageReduction(0); // Check that blocking works destroyableComponent->SetAttacksToBlock(1); destroyableComponent->Damage(UINT32_MAX, LWOOBJID_EMPTY); ASSERT_EQ(destroyableComponent->GetHealth(), 74); destroyableComponent->Damage(4, LWOOBJID_EMPTY); ASSERT_EQ(destroyableComponent->GetHealth(), 70); // Check that immunity works destroyableComponent->SetIsImmune(true); destroyableComponent->Damage(UINT32_MAX, LWOOBJID_EMPTY); ASSERT_EQ(destroyableComponent->GetHealth(), 70); ASSERT_TRUE(destroyableComponent->IsImmune()); destroyableComponent->SetIsImmune(false); destroyableComponent->SetIsGMImmune(true); destroyableComponent->Damage(UINT32_MAX, LWOOBJID_EMPTY); ASSERT_EQ(destroyableComponent->GetHealth(), 70); ASSERT_TRUE(destroyableComponent->IsImmune()); destroyableComponent->SetIsGMImmune(false); // Check knockback immunity destroyableComponent->SetIsShielded(true); ASSERT_TRUE(destroyableComponent->IsKnockbackImmune()); // Finally deal enough damage to kill the Entity destroyableComponent->Damage(71, LWOOBJID_EMPTY); ASSERT_EQ(destroyableComponent->GetHealth(), 0); // Now lets heal some stats destroyableComponent->Heal(15); ASSERT_EQ(destroyableComponent->GetHealth(), 15); destroyableComponent->Heal(15000); ASSERT_EQ(destroyableComponent->GetHealth(), 100); destroyableComponent->Repair(10); ASSERT_EQ(destroyableComponent->GetArmor(), 10); destroyableComponent->Repair(15000); ASSERT_EQ(destroyableComponent->GetArmor(), 10); destroyableComponent->SetMaxImagination(100.0f); destroyableComponent->SetImagination(0); destroyableComponent->Imagine(99); ASSERT_EQ(destroyableComponent->GetImagination(), 99); destroyableComponent->Imagine(4); ASSERT_EQ(destroyableComponent->GetImagination(), 100); } TEST_F(DestroyableTest, DestroyableComponentFactionTest) { ASSERT_TRUE(destroyableComponent->HasFaction(-1)); ASSERT_TRUE(destroyableComponent->HasFaction(6)); } TEST_F(DestroyableTest, DestroyableComponentValiditiyTest) { auto* enemyEntity = new Entity(19, info); enemyEntity->AddComponent(-1)->AddFactionNoLookup(16); destroyableComponent->AddEnemyFaction(16); EXPECT_TRUE(destroyableComponent->IsEnemy(enemyEntity)); EXPECT_FALSE(destroyableComponent->IsFriend(enemyEntity)); delete enemyEntity; } TEST_F(DestroyableTest, DestroyableComponentImmunityTest) { // assert to show that they are empty ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack()); ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime()); ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback()); ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss()); ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint()); // set them all to true (count 1) and check destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, true, true, true, true, true, true, true, true, true); ASSERT_TRUE(destroyableComponent->GetImmuneToBasicAttack()); ASSERT_TRUE(destroyableComponent->GetImmuneToDamageOverTime()); ASSERT_TRUE(destroyableComponent->GetImmuneToKnockback()); ASSERT_TRUE(destroyableComponent->GetImmuneToInterrupt()); ASSERT_TRUE(destroyableComponent->GetImmuneToSpeed()); ASSERT_TRUE(destroyableComponent->GetImmuneToImaginationGain()); ASSERT_TRUE(destroyableComponent->GetImmuneToImaginationLoss()); ASSERT_TRUE(destroyableComponent->GetImmuneToQuickbuildInterrupt()); ASSERT_TRUE(destroyableComponent->GetImmuneToPullToPoint()); // remove them to check that they get removed properly destroyableComponent->SetStatusImmunity(eStateChangeType::POP, true, true, true, true, true, true, true, true, true); ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack()); ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime()); ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback()); ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss()); ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint()); // should not crash to remove them again destroyableComponent->SetStatusImmunity(eStateChangeType::POP, true, true, true, true, true, true, true, true, true); ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack()); ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime()); ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback()); ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss()); ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint()); // just do one destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, true); ASSERT_TRUE(destroyableComponent->GetImmuneToBasicAttack()); ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime()); ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback()); ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss()); ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint()); // now stack it to 2 on basic attack destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, true); ASSERT_TRUE(destroyableComponent->GetImmuneToBasicAttack()); ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime()); ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback()); ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss()); ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint()); // remove one and still shoudl be true destroyableComponent->SetStatusImmunity(eStateChangeType::POP, true); ASSERT_TRUE(destroyableComponent->GetImmuneToBasicAttack()); ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime()); ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback()); ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss()); ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint()); // go back to 0 destroyableComponent->SetStatusImmunity(eStateChangeType::POP, true); ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack()); ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime()); ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback()); ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss()); ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint()); // check individual ones now destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, true, false, false, false, false, false, false, false, false); ASSERT_TRUE(destroyableComponent->GetImmuneToBasicAttack()); ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime()); ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback()); ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss()); ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint()); destroyableComponent->SetStatusImmunity(eStateChangeType::POP, true, false, false, false, false, false, false, false, false); destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, false, true, false, false, false, false, false, false, false); ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack()); ASSERT_TRUE(destroyableComponent->GetImmuneToDamageOverTime()); ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback()); ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss()); ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint()); destroyableComponent->SetStatusImmunity(eStateChangeType::POP, false, true, false, false, false, false, false, false, false); destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, false, false, true, false, false, false, false, false, false); ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack()); ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime()); ASSERT_TRUE(destroyableComponent->GetImmuneToKnockback()); ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss()); ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint()); destroyableComponent->SetStatusImmunity(eStateChangeType::POP, false, false, true, false, false, false, false, false, false); destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, false, false, false, true, false, false, false, false, false); ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack()); ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime()); ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback()); ASSERT_TRUE(destroyableComponent->GetImmuneToInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss()); ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint()); destroyableComponent->SetStatusImmunity(eStateChangeType::POP, false, false, false, true, false, false, false, false, false); destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, false, false, false, false, true, false, false, false, false); ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack()); ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime()); ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback()); ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt()); ASSERT_TRUE(destroyableComponent->GetImmuneToSpeed()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss()); ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint()); destroyableComponent->SetStatusImmunity(eStateChangeType::POP, false, false, false, false, true, false, false, false, false); destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, false, false, false, false, false, true, false, false, false); ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack()); ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime()); ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback()); ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed()); ASSERT_TRUE(destroyableComponent->GetImmuneToImaginationGain()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss()); ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint()); destroyableComponent->SetStatusImmunity(eStateChangeType::POP, false, false, false, false, false, true, false, false, false); destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, false, false, false, false, false, false, true, false, false); ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack()); ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime()); ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback()); ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain()); ASSERT_TRUE(destroyableComponent->GetImmuneToImaginationLoss()); ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint()); destroyableComponent->SetStatusImmunity(eStateChangeType::POP, false, false, false, false, false, false, true, false, false); destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, false, false, false, false, false, false, false, true, false); ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack()); ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime()); ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback()); ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss()); ASSERT_TRUE(destroyableComponent->GetImmuneToQuickbuildInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToPullToPoint()); destroyableComponent->SetStatusImmunity(eStateChangeType::POP, false, false, false, false, false, false, false, true, false); destroyableComponent->SetStatusImmunity(eStateChangeType::PUSH, false, false, false, false, false, false, false, false, true); ASSERT_FALSE(destroyableComponent->GetImmuneToBasicAttack()); ASSERT_FALSE(destroyableComponent->GetImmuneToDamageOverTime()); ASSERT_FALSE(destroyableComponent->GetImmuneToKnockback()); ASSERT_FALSE(destroyableComponent->GetImmuneToInterrupt()); ASSERT_FALSE(destroyableComponent->GetImmuneToSpeed()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationGain()); ASSERT_FALSE(destroyableComponent->GetImmuneToImaginationLoss()); ASSERT_FALSE(destroyableComponent->GetImmuneToQuickbuildInterrupt()); ASSERT_TRUE(destroyableComponent->GetImmuneToPullToPoint()); destroyableComponent->SetStatusImmunity(eStateChangeType::POP, false, false, false, false, false, false, false, false, true); } /** * Test the Damage cooldown timer of DestroyableComponent */ TEST_F(DestroyableTest, DestroyableComponentDamageCooldownTest) { // Test the damage immune timer state (anything above 0.0f) destroyableComponent->SetDamageCooldownTimer(1.0f); EXPECT_FLOAT_EQ(destroyableComponent->GetDamageCooldownTimer(), 1.0f); ASSERT_TRUE(destroyableComponent->IsCooldownImmune()); // Test that the Update() function correctly decrements the damage cooldown timer destroyableComponent->Update(0.5f); EXPECT_FLOAT_EQ(destroyableComponent->GetDamageCooldownTimer(), 0.5f); ASSERT_TRUE(destroyableComponent->IsCooldownImmune()); // Test the non damage immune timer state (anything below or equal to 0.0f) destroyableComponent->SetDamageCooldownTimer(0.0f); EXPECT_FLOAT_EQ(destroyableComponent->GetDamageCooldownTimer(), 0.0f); ASSERT_FALSE(destroyableComponent->IsCooldownImmune()); } // Live Die never sets client_death (19,428 live Die, 452 of them players). TEST_F(DestroyableTest, DieDoesNotClaimAClientDeath) { destroyableComponent->SetMaxArmor(0.0f); const auto sent = PacketTestUtils::Capture([&] { destroyableComponent->Damage(UINT32_MAX, LWOOBJID_EMPTY); }); const auto die = FindGameMessage(sent, MessageType::Game::DIE); ASSERT_TRUE(die.has_value()); EXPECT_EQ(die->target, baseEntity->GetObjectID()); EXPECT_FALSE(die->bClientDeath); EXPECT_TRUE(die->bSpawnLoot); } TEST_F(DestroyableTest, PlayerDieDoesNotClaimAClientDeath) { auto playerInfo = info; playerInfo.lot = 1; Entity player(0x1000000000000001LL, playerInfo); Character character(1, nullptr); player.SetCharacter(&character); character.SetEntity(&player); SystemAddress address; address.binaryAddress = 0x0100007f; address.port = 2003; player.AddComponent(-1, &character, address); auto* destroyable = player.AddComponent(-1); destroyable->SetMaxHealth(4.0f); destroyable->SetHealth(4); ASSERT_TRUE(player.IsPlayer()); const auto sent = PacketTestUtils::Capture([&] { destroyable->Smash(LWOOBJID_EMPTY); }); const auto die = FindGameMessage(sent, MessageType::Game::DIE); ASSERT_TRUE(die.has_value()); EXPECT_EQ(die->target, player.GetObjectID()); EXPECT_FALSE(die->bClientDeath); player.SetCharacter(nullptr); } // BasicAttack's dir_angle_* are degrees; live's Die carries them as these exact floats. TEST_F(DestroyableTest, DeathDirectionDegreesMatchLiveBits) { const auto bits = [](float value) { uint32_t out; std::memcpy(&out, &value, sizeof(out)); return out; }; EXPECT_EQ(bits(DeathDirection::DegreesToRadians(-25.0f)), 0xbedf66f3u); EXPECT_EQ(bits(DeathDirection::DegreesToRadians(25.0f)), 0x3edf66f3u); EXPECT_EQ(bits(DeathDirection::DegreesToRadians(20.0f)), 0x3eb2b8c3u); EXPECT_EQ(bits(DeathDirection::DegreesToRadians(-20.0f)), 0xbeb2b8c3u); EXPECT_EQ(bits(DeathDirection::DegreesToRadians(75.0f)), 0x3fa78d36u); EXPECT_EQ(bits(DeathDirection::DegreesToRadians(80.0f)), 0x3fb2b8c3u); EXPECT_EQ(bits(DeathDirection::DegreesToRadians(15.0f)), 0x3e860a92u); EXPECT_EQ(bits(DeathDirection::DegreesToRadians(45.0f)), 0x3f490fdbu); EXPECT_EQ(DeathDirection::DegreesToRadians(0.0f), 0.0f); } // The killing blow's direction goes into Die. Bytes: a live Die from a -25 degree, force 12 BasicAttack with the // object IDs replaced. TEST_F(DestroyableTest, KillingBlowDirectionIsSentInDie) { Entity target(0x0102030405060708LL, info); auto* destroyable = target.AddComponent(-1); destroyable->SetMaxHealth(4.0f); destroyable->SetHealth(4); const DeathDirection direction{ DeathDirection::DegreesToRadians(-25.0f), 0.0f, 12.0f }; const LWOOBJID killer = 0x1000000000000001LL; auto sent = PacketTestUtils::Capture([&] { destroyable->Damage(1, killer, 0, true, direction); }); EXPECT_FALSE(FindGameMessage(sent, MessageType::Game::DIE).has_value()); // not a killing blow sent = PacketTestUtils::Capture([&] { destroyable->Damage(3, killer, 0, true, direction); }); bool found = false; for (const auto& packet : sent) { if (packet.bytes.size() < 18 || packet.bytes[16] != 0x25 || packet.bytes[17] != 0x00) continue; found = true; EXPECT_PACKET_EQ(PacketTestUtils::FromHex( "53 05 00 0c 00 00 00 00 08 07 06 05 04 03 02 01 25 00 40 00 00 00 3c d9 b7 ef 80 00 00 00 00 00 10 10 " "40 20 00 00 00 00 00 02 10 10 00 00 00 00 00 01 00", 404), PacketTestUtils::FromCapture(packet)); } EXPECT_TRUE(found); } // Deaths that are not a BasicAttack killing blow carry no direction. TEST_F(DestroyableTest, SmashWithoutAttackHasNoDirection) { const auto sent = PacketTestUtils::Capture([&] { destroyableComponent->Smash(0x1000000000000001LL); }); const auto die = FindGameMessage(sent, MessageType::Game::DIE); ASSERT_TRUE(die.has_value()); EXPECT_EQ(die->directionRelative_AngleXZ, 0.0f); EXPECT_EQ(die->directionRelative_AngleY, 0.0f); EXPECT_EQ(die->directionRelative_Force, 0.0f); } // Live stunned every dead enemy in the packet right after its Die; bytes are a live SetStunned with the ID replaced. TEST_F(DestroyableTest, DeadEnemyIsStunnedAfterDie) { if (!CDClientDatabase::isConnected) { CDClientDatabase::Connect(":memory:"); for (const auto* table : { "ComponentsRegistry (id INTEGER, component_type INTEGER, component_id INTEGER)", "BaseCombatAIComponent (id INTEGER, aggroRadius REAL, tetherSpeed REAL, pursuitSpeed REAL, softTetherRadius REAL, hardTetherRadius REAL, minRoundLength REAL, maxRoundLength REAL, combatRoundLength REAL)", "ObjectSkills (objectTemplate INTEGER, skillID INTEGER, castOnType INTEGER, AICombatWeight INTEGER)", "SkillBehavior (skillID INTEGER, behaviorID INTEGER)", }) { CDClientDatabase::ExecuteDML(std::string("CREATE TABLE ") + table + ";"); } } Entity enemy(0x0102030405060708LL, info); enemy.AddComponent(-1); auto* destroyable = enemy.AddComponent(-1); destroyable->SetMaxHealth(4.0f); destroyable->SetHealth(4); const auto sent = PacketTestUtils::Capture([&] { destroyable->Smash(LWOOBJID_EMPTY); }); const auto ids = GameMessageTestUtils::SentGameMessageIds(sent); const auto die = std::find(ids.begin(), ids.end(), MessageType::Game::DIE); ASSERT_NE(die, ids.end()); ASSERT_NE(die + 1, ids.end()); EXPECT_EQ(*(die + 1), MessageType::Game::SET_STUNNED); const auto stuns = GameMessageTestUtils::SentGameMessages(sent); ASSERT_EQ(stuns.size(), 1u); EXPECT_PACKET_EQ(PacketTestUtils::FromHex("53 05 00 0c 00 00 00 00 08 07 06 05 04 03 02 01 c6 00 00 00 00 00 40 50 80", 193), GameMessageTestUtils::StructPacket(stuns[0])); } TEST_F(DestroyableTest, DeadSmashableIsNotStunned) { const auto sent = PacketTestUtils::Capture([&] { destroyableComponent->Smash(LWOOBJID_EMPTY); }); EXPECT_TRUE(GameMessageTestUtils::SentGameMessages(sent).empty()); }