mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
feat(combat): Die carries the killing attack's smash direction
Live filled Die's direction_relative_angle_xz, _angle_y and _force on 11,606 of 19,428 deaths. The values are the dir_angle_xz, dir_angle_y (degrees, sent as radians) and dir_force parameters of the BasicAttack that dealt the killing blow: per killing skill the triple is constant (skill 1210: -25 degrees, force 12 on 540 of 549 kills; skill 10: force 7 on 1,830 of 1,835), and deaths with no killer (5,948) never have one. BasicAttack is the only behavior with these parameters apart from one Grab. The client passes them to PerformSmashableDeath, which throws the smashable's pieces that way; DLU sent 0. The radians are rounded the way live did (-25 is 0xbedf66f3, 20 is 0x3eb2b8c3). Not covered: use_caster_velocity (one behavior, racing). Check: smash crates and enemies with different weapons; the pieces fly off to the side/up per weapon instead of straight out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ void BasicAttackBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bi
|
|||||||
auto* destroyableComponent = entity->GetComponent<DestroyableComponent>();
|
auto* destroyableComponent = entity->GetComponent<DestroyableComponent>();
|
||||||
if (destroyableComponent != nullptr) {
|
if (destroyableComponent != nullptr) {
|
||||||
PlayFx(u"onhit", entity->GetObjectID()); //This damage animation doesn't seem to play consistently
|
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
|
//Handle player damage cooldown
|
||||||
if (entity->IsPlayer() && !this->m_DontApplyImmune) {
|
if (entity->IsPlayer() && !this->m_DontApplyImmune) {
|
||||||
@@ -119,7 +119,7 @@ void BasicAttackBehavior::DoHandleBehavior(BehaviorContext* context, RakNet::Bit
|
|||||||
auto previousArmor = destroyableComponent->GetArmor();
|
auto previousArmor = destroyableComponent->GetArmor();
|
||||||
auto previousHealth = destroyableComponent->GetHealth();
|
auto previousHealth = destroyableComponent->GetHealth();
|
||||||
PlayFx(u"onhit", targetEntity->GetObjectID());
|
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{};
|
uint8_t successState{};
|
||||||
@@ -204,7 +204,7 @@ void BasicAttackBehavior::DoBehaviorCalculation(BehaviorContext* context, RakNet
|
|||||||
const auto damage = this->m_MinDamage;
|
const auto damage = this->m_MinDamage;
|
||||||
|
|
||||||
PlayFx(u"onhit", targetEntity->GetObjectID(), 1);
|
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);
|
context->ScheduleUpdate(branch.target);
|
||||||
|
|
||||||
const uint32_t armorDamageDealt = previousArmor - destroyableComponent->GetArmor();
|
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.
|
// 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;
|
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_OnSuccess = GetAction("on_success");
|
||||||
|
|
||||||
this->m_OnFailArmor = GetAction("on_fail_armor");
|
this->m_OnFailArmor = GetAction("on_fail_armor");
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "Behavior.h"
|
#include "Behavior.h"
|
||||||
|
#include "DestroyableComponent.h"
|
||||||
|
|
||||||
class BasicAttackBehavior final : public Behavior
|
class BasicAttackBehavior final : public Behavior
|
||||||
{
|
{
|
||||||
@@ -54,6 +55,9 @@ private:
|
|||||||
|
|
||||||
uint32_t m_MaxDamage;
|
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_OnSuccess;
|
||||||
|
|
||||||
Behavior* m_OnFailArmor;
|
Behavior* m_OnFailArmor;
|
||||||
|
|||||||
@@ -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<double>(3.14159265358979323846f);
|
||||||
|
return static_cast<float>(static_cast<double>(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) {
|
if (GetHealth() <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -649,7 +654,7 @@ void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Smash(source, eKillType::VIOLENT, u"", skillID);
|
Smash(source, eKillType::VIOLENT, u"", skillID, deathDirection);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DestroyableComponent::Subscribe(LWOOBJID scriptObjId, CppScripts::Script* scriptToAdd) {
|
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;
|
if (m_IsDead) return;
|
||||||
|
|
||||||
//check if hardcore mode is enabled
|
//check if hardcore mode is enabled
|
||||||
@@ -751,6 +756,9 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType
|
|||||||
die.bClientDeath = false;
|
die.bClientDeath = false;
|
||||||
die.bSpawnLoot = true;
|
die.bSpawnLoot = true;
|
||||||
die.deathType = deathType;
|
die.deathType = deathType;
|
||||||
|
die.directionRelative_AngleXZ = deathDirection.angleXZ;
|
||||||
|
die.directionRelative_AngleY = deathDirection.angleY;
|
||||||
|
die.directionRelative_Force = deathDirection.force;
|
||||||
die.killType = killType;
|
die.killType = killType;
|
||||||
die.killerID = source;
|
die.killerID = source;
|
||||||
die.lootOwnerID = source;
|
die.lootOwnerID = source;
|
||||||
|
|||||||
@@ -21,6 +21,21 @@ namespace CppScripts {
|
|||||||
}; //! namespace CppScripts
|
}; //! namespace CppScripts
|
||||||
enum class eStateChangeType : uint32_t;
|
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
|
* Represents the stats of an entity, for example its health, imagination and armor. Also handles factions, which
|
||||||
* indicate which enemies this entity has.
|
* indicate which enemies this entity has.
|
||||||
@@ -410,8 +425,9 @@ public:
|
|||||||
* @param source the attacker that caused this damage
|
* @param source the attacker that caused this damage
|
||||||
* @param skillID the skill that damaged this entity
|
* @param skillID the skill that damaged this entity
|
||||||
* @param echo whether or not to serialize the damage
|
* @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
|
* Smashes this entity, notifying all clients
|
||||||
@@ -419,8 +435,9 @@ public:
|
|||||||
* @param skillID the skill that killed this entity
|
* @param skillID the skill that killed this entity
|
||||||
* @param killType the way this entity was killed, determines if a client animation is played
|
* @param killType the way this entity was killed, determines if a client animation is played
|
||||||
* @param deathType the animation to play when killed
|
* @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,
|
* Brings entity back to life: tells every client (Resurrect), then, once the respawn animation is done,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "PacketTestUtils.h"
|
#include "PacketTestUtils.h"
|
||||||
#include "dCommonVars.h"
|
#include "dCommonVars.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -616,3 +617,52 @@ TEST_F(DestroyableTest, PlayerDieDoesNotClaimAClientDeath) {
|
|||||||
EXPECT_FALSE(die->bClientDeath);
|
EXPECT_FALSE(die->bClientDeath);
|
||||||
player.SetCharacter(nullptr);
|
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<DestroyableComponent>(-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<GameMessages::Die>(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<GameMessages::Die>(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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user