From cf1e8a54c6c944f17e6baf8529146ef60e5a818b Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Fri, 2 Oct 2026 09:11:20 -0500 Subject: [PATCH] fix(destroyable): damage reductions add up, as the client The damage reduction behavior set the reduction to its amount and back to 0 when it ended, so a second reduction replaced the first and ending either removed both. The client's behavior sends AddDamageReduction (DamageReductionBehavior::Cast) and the destroyable adds it to the current reduction; removing takes off that amount, never below 0. Co-Authored-By: Claude Opus 5.5 --- dGame/dBehaviors/DamageReductionBehavior.cpp | 5 +++-- dGame/dComponents/DestroyableComponent.cpp | 8 ++++++++ dGame/dComponents/DestroyableComponent.h | 5 +++++ .../dComponentsTests/DestroyableComponentTests.cpp | 12 ++++++++++++ 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/dGame/dBehaviors/DamageReductionBehavior.cpp b/dGame/dBehaviors/DamageReductionBehavior.cpp index 62b70a52b..0874a9ec2 100644 --- a/dGame/dBehaviors/DamageReductionBehavior.cpp +++ b/dGame/dBehaviors/DamageReductionBehavior.cpp @@ -22,7 +22,8 @@ void DamageReductionBehavior::Handle(BehaviorContext* context, RakNet::BitStream return; } - destroyable->SetDamageReduction(m_ReductionAmount); + // DamageReductionBehavior::Cast (0x004fdbb0) sends AddDamageReduction: it adds to any other reduction + destroyable->AddDamageReduction(m_ReductionAmount); context->RegisterTimerBehavior(this, branch, target->GetObjectID()); } @@ -46,7 +47,7 @@ void DamageReductionBehavior::Timer(BehaviorContext* context, BehaviorBranchCont return; } - destroyable->SetDamageReduction(0); + destroyable->RemoveDamageReduction(m_ReductionAmount); } void DamageReductionBehavior::Load() { diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index 88dc24d64..7955ce687 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -444,6 +444,14 @@ void DestroyableComponent::SetDamageReduction(int32_t value) { m_DamageReduction = value; } +void DestroyableComponent::AddDamageReduction(const int32_t value) { + SetDamageReduction(std::max(m_DamageReduction + value, 0)); +} + +void DestroyableComponent::RemoveDamageReduction(const int32_t value) { + SetDamageReduction(std::max(m_DamageReduction - value, 0)); +} + void DestroyableComponent::SetIsImmune(bool value) { m_DirtyHealth = true; m_ImmuneToBasicAttackCount = value ? 1 : 0; diff --git a/dGame/dComponents/DestroyableComponent.h b/dGame/dComponents/DestroyableComponent.h index 1ab62b1c8..a6bf02950 100644 --- a/dGame/dComponents/DestroyableComponent.h +++ b/dGame/dComponents/DestroyableComponent.h @@ -175,6 +175,11 @@ public: */ void SetDamageReduction(int32_t value); + // The client's AddDamageReduction / RemoveDamageReduction: reductions from several buffs add up, and removing one + // takes off only its own amount (never below 0) + void AddDamageReduction(int32_t value); + void RemoveDamageReduction(int32_t value); + /** * Returns the current damage reduction value * @return the current damage reduction value diff --git a/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp b/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp index a41d6d742..c2c421e19 100644 --- a/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp +++ b/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp @@ -706,3 +706,15 @@ TEST_F(DestroyableTest, DeadSmashableIsNotStunned) { const auto sent = PacketTestUtils::Capture([&] { destroyableComponent->Smash(LWOOBJID_EMPTY); }); EXPECT_TRUE(GameMessageTestUtils::SentGameMessages(sent).empty()); } + +// The client's AddDamageReduction / RemoveDamageReduction: two buffs add up, ending one leaves the other's +TEST_F(DestroyableTest, DamageReductionsAddUp) { + destroyableComponent->SetDamageReduction(0); + destroyableComponent->AddDamageReduction(2); + destroyableComponent->AddDamageReduction(3); + EXPECT_EQ(destroyableComponent->GetDamageReduction(), 5); + destroyableComponent->RemoveDamageReduction(2); + EXPECT_EQ(destroyableComponent->GetDamageReduction(), 3); + destroyableComponent->RemoveDamageReduction(10); + EXPECT_EQ(destroyableComponent->GetDamageReduction(), 0); +}