diff --git a/dGame/dBehaviors/BasicAttackBehavior.cpp b/dGame/dBehaviors/BasicAttackBehavior.cpp index e265a034a..eeca2c3dd 100644 --- a/dGame/dBehaviors/BasicAttackBehavior.cpp +++ b/dGame/dBehaviors/BasicAttackBehavior.cpp @@ -16,7 +16,7 @@ void BasicAttackBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bi auto* destroyableComponent = entity->GetComponent(); if (destroyableComponent != nullptr) { PlayFx(u"onhit", entity->GetObjectID()); //This damage animation doesn't seem to play consistently - destroyableComponent->Damage(this->m_MaxDamage, context->originator, context->skillID); + destroyableComponent->Damage(this->m_MaxDamage, context->originator, context->skillID, true, m_DeathDirection); //Handle player damage cooldown if (entity->IsPlayer() && !this->m_DontApplyImmune) { @@ -119,7 +119,7 @@ void BasicAttackBehavior::DoHandleBehavior(BehaviorContext* context, RakNet::Bit auto previousArmor = destroyableComponent->GetArmor(); auto previousHealth = destroyableComponent->GetHealth(); PlayFx(u"onhit", targetEntity->GetObjectID()); - destroyableComponent->Damage(totalDamageDealt, context->originator, context->skillID); + destroyableComponent->Damage(totalDamageDealt, context->originator, context->skillID, true, m_DeathDirection); } uint8_t successState{}; @@ -204,7 +204,7 @@ void BasicAttackBehavior::DoBehaviorCalculation(BehaviorContext* context, RakNet const auto damage = this->m_MinDamage; PlayFx(u"onhit", targetEntity->GetObjectID(), 1); - destroyableComponent->Damage(damage, context->originator, context->skillID, false); + destroyableComponent->Damage(damage, context->originator, context->skillID, false, m_DeathDirection); context->ScheduleUpdate(branch.target); const uint32_t armorDamageDealt = previousArmor - destroyableComponent->GetArmor(); @@ -262,6 +262,10 @@ void BasicAttackBehavior::Load() { // The client sets the minimum damage to maximum, so we'll do the same. These are usually the same value anyways. if (this->m_MinDamage < this->m_MaxDamage) this->m_MinDamage = this->m_MaxDamage; + m_DeathDirection.angleXZ = DeathDirection::DegreesToRadians(GetFloat("dir_angle_xz")); + m_DeathDirection.angleY = DeathDirection::DegreesToRadians(GetFloat("dir_angle_y")); + m_DeathDirection.force = GetFloat("dir_force"); + this->m_OnSuccess = GetAction("on_success"); this->m_OnFailArmor = GetAction("on_fail_armor"); diff --git a/dGame/dBehaviors/BasicAttackBehavior.h b/dGame/dBehaviors/BasicAttackBehavior.h index 3cde23fde..1073b002d 100644 --- a/dGame/dBehaviors/BasicAttackBehavior.h +++ b/dGame/dBehaviors/BasicAttackBehavior.h @@ -1,5 +1,6 @@ #pragma once #include "Behavior.h" +#include "DestroyableComponent.h" class BasicAttackBehavior final : public Behavior { @@ -54,6 +55,9 @@ private: uint32_t m_MaxDamage; + // dir_angle_xz, dir_angle_y and dir_force: sent in Die when this attack kills its target. + DeathDirection m_DeathDirection; + Behavior* m_OnSuccess; Behavior* m_OnFailArmor; diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index 61eefb833..7f675eba9 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -548,7 +548,12 @@ void DestroyableComponent::Repair(const uint32_t armor) { } -void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32_t skillID, bool echo) { +float DeathDirection::DegreesToRadians(const float degrees) { + constexpr double PI_AS_FLOAT = static_cast(3.14159265358979323846f); + return static_cast(static_cast(degrees) * PI_AS_FLOAT / 180.0); +} + +void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32_t skillID, bool echo, const DeathDirection& deathDirection) { if (GetHealth() <= 0) { return; } @@ -649,7 +654,7 @@ void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32 return; } - Smash(source, eKillType::VIOLENT, u"", skillID); + Smash(source, eKillType::VIOLENT, u"", skillID, deathDirection); } void DestroyableComponent::Subscribe(LWOOBJID scriptObjId, CppScripts::Script* scriptToAdd) { @@ -675,7 +680,7 @@ void DestroyableComponent::NotifySubscribers(Entity* attacker, uint32_t damage) } } -void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType, const std::u16string& deathType, uint32_t skillID) { +void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType, const std::u16string& deathType, uint32_t skillID, const DeathDirection& deathDirection) { if (m_IsDead) return; //check if hardcore mode is enabled @@ -751,6 +756,9 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType die.bClientDeath = false; die.bSpawnLoot = true; die.deathType = deathType; + die.directionRelative_AngleXZ = deathDirection.angleXZ; + die.directionRelative_AngleY = deathDirection.angleY; + die.directionRelative_Force = deathDirection.force; die.killType = killType; die.killerID = source; die.lootOwnerID = source; diff --git a/dGame/dComponents/DestroyableComponent.h b/dGame/dComponents/DestroyableComponent.h index ff54f6fce..1104ce344 100644 --- a/dGame/dComponents/DestroyableComponent.h +++ b/dGame/dComponents/DestroyableComponent.h @@ -21,6 +21,21 @@ namespace CppScripts { }; //! namespace CppScripts enum class eStateChangeType : uint32_t; +/** + * Which way a smashed object flies apart: Die's direction_relative_angle_xz / _y (radians) and _force. + * Live fills it from the BasicAttack that dealt the killing blow (its dir_angle_xz, dir_angle_y and dir_force), + * and leaves it 0 for any other death. + */ +struct DeathDirection { + float angleXZ{}; + float angleY{}; + float force{}; + + // BasicAttack stores its angles in degrees. Converted the way live did, bit for bit: in double with pi + // rounded to float, then rounded to float (-25 degrees is -0.43633232 and 20 degrees 0.34906587 in captures). + static float DegreesToRadians(float degrees); +}; + /** * Represents the stats of an entity, for example its health, imagination and armor. Also handles factions, which * indicate which enemies this entity has. @@ -410,8 +425,9 @@ public: * @param source the attacker that caused this damage * @param skillID the skill that damaged this entity * @param echo whether or not to serialize the damage + * @param deathDirection how the entity flies apart if this damage kills it */ - void Damage(uint32_t damage, LWOOBJID source, uint32_t skillID = 0, bool echo = true); + void Damage(uint32_t damage, LWOOBJID source, uint32_t skillID = 0, bool echo = true, const DeathDirection& deathDirection = {}); /** * Smashes this entity, notifying all clients @@ -419,8 +435,9 @@ public: * @param skillID the skill that killed this entity * @param killType the way this entity was killed, determines if a client animation is played * @param deathType the animation to play when killed + * @param deathDirection how the entity flies apart */ - void Smash(LWOOBJID source, eKillType killType = eKillType::VIOLENT, const std::u16string& deathType = u"", uint32_t skillID = 0); + void Smash(LWOOBJID source, eKillType killType = eKillType::VIOLENT, const std::u16string& deathType = u"", uint32_t skillID = 0, const DeathDirection& deathDirection = {}); /** * Brings entity back to life: tells every client (Resurrect), then, once the respawn animation is done, diff --git a/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp b/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp index 3f3137777..30f4dea77 100644 --- a/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp +++ b/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp @@ -12,6 +12,7 @@ #include "PacketTestUtils.h" #include "dCommonVars.h" +#include #include namespace { @@ -616,3 +617,52 @@ TEST_F(DestroyableTest, PlayerDieDoesNotClaimAClientDeath) { 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); +}