From 411dce7457fcede6ad2439a44e7f924cfc34fb0f Mon Sep 17 00:00:00 2001 From: jadebenn Date: Sun, 12 Nov 2023 05:53:03 -0600 Subject: [PATCH] Adding damage cooldown/"invincibility frames" as in Live (#1276) * Added cooldown handling * Made most of the logs hidden outside of debug mode * removed weird submodule * kill this phantom submodule * updated to reflect reviewed feedback * Added IsCooldownImmune() method to DestroyableComponent * friggin typo * Implemented non-pending changes and added cooldown immunity functions to DestroyableComponentTests * add trailing linebreak * another typo :( * flipped cooldown test order (not leaving immune) * Clean up comment and add DestroyableComponent test --- dGame/dBehaviors/BasicAttackBehavior.cpp | 26 +++++++++- dGame/dBehaviors/BasicAttackBehavior.h | 14 +++--- dGame/dComponents/DestroyableComponent.cpp | 47 ++++++++++++------- dGame/dComponents/DestroyableComponent.h | 36 ++++++++++---- dGame/dUtilities/SlashCommandHandler.cpp | 32 ++++++++++--- docs/Commands.md | 1 + .../DestroyableComponentTests.cpp | 19 ++++++++ 7 files changed, 132 insertions(+), 43 deletions(-) diff --git a/dGame/dBehaviors/BasicAttackBehavior.cpp b/dGame/dBehaviors/BasicAttackBehavior.cpp index 2bd6ce41..36615a9f 100644 --- a/dGame/dBehaviors/BasicAttackBehavior.cpp +++ b/dGame/dBehaviors/BasicAttackBehavior.cpp @@ -3,6 +3,8 @@ #include "Game.h" #include "Logger.h" #include "EntityManager.h" +#include "dZoneManager.h" +#include "WorldConfig.h" #include "DestroyableComponent.h" #include "BehaviorContext.h" #include "eBasicAttackSuccessTypes.h" @@ -13,8 +15,15 @@ void BasicAttackBehavior::Handle(BehaviorContext* context, RakNet::BitStream* bi auto* destroyableComponent = entity->GetComponent(); if (destroyableComponent != nullptr) { - PlayFx(u"onhit", entity->GetObjectID()); + PlayFx(u"onhit", entity->GetObjectID()); //This damage animation doesn't seem to play consistently destroyableComponent->Damage(this->m_MaxDamage, context->originator, context->skillID); + + //Handle player damage cooldown + if (entity->IsPlayer() && !this->m_DontApplyImmune) { + const float immunityTime = Game::zoneManager->GetWorldConfig()->globalImmunityTime; + destroyableComponent->SetDamageCooldownTimer(immunityTime); + LOG_DEBUG("Target targetEntity %llu took damage, setting damage cooldown timer to %f s", branch.target, immunityTime); + } } this->m_OnSuccess->Handle(context, bitStream, branch); @@ -72,6 +81,7 @@ void BasicAttackBehavior::DoHandleBehavior(BehaviorContext* context, RakNet::Bit } if (isImmune) { + LOG_DEBUG("Target targetEntity %llu is immune!", branch.target); this->m_OnFailImmune->Handle(context, bitStream, branch); return; } @@ -178,11 +188,15 @@ void BasicAttackBehavior::DoBehaviorCalculation(BehaviorContext* context, RakNet return; } - const bool isImmune = destroyableComponent->IsImmune(); + const float immunityTime = Game::zoneManager->GetWorldConfig()->globalImmunityTime; + LOG_DEBUG("Damage cooldown timer currently %f s", destroyableComponent->GetDamageCooldownTimer()); + + const bool isImmune = (destroyableComponent->IsImmune()) || (destroyableComponent->IsCooldownImmune()); bitStream->Write(isImmune); if (isImmune) { + LOG_DEBUG("Target targetEntity %llu is immune!", branch.target); this->m_OnFailImmune->Calculate(context, bitStream, branch); return; } @@ -203,6 +217,12 @@ void BasicAttackBehavior::DoBehaviorCalculation(BehaviorContext* context, RakNet bitStream->Write(isSuccess); + //Handle player damage cooldown + if (isSuccess && targetEntity->IsPlayer() && !this->m_DontApplyImmune) { + destroyableComponent->SetDamageCooldownTimer(immunityTime); + LOG_DEBUG("Target targetEntity %llu took damage, setting damage cooldown timer to %f s", branch.target, immunityTime); + } + eBasicAttackSuccessTypes successState = eBasicAttackSuccessTypes::FAILIMMUNE; if (isSuccess) { if (healthDamageDealt >= 1) { @@ -236,6 +256,8 @@ void BasicAttackBehavior::DoBehaviorCalculation(BehaviorContext* context, RakNet } void BasicAttackBehavior::Load() { + this->m_DontApplyImmune = GetBoolean("dont_apply_immune"); + this->m_MinDamage = GetInt("min damage"); if (this->m_MinDamage == 0) this->m_MinDamage = 1; diff --git a/dGame/dBehaviors/BasicAttackBehavior.h b/dGame/dBehaviors/BasicAttackBehavior.h index f6e3fa28..6525c343 100644 --- a/dGame/dBehaviors/BasicAttackBehavior.h +++ b/dGame/dBehaviors/BasicAttackBehavior.h @@ -10,14 +10,14 @@ public: /** * @brief Reads a 16bit short from the bitStream and when the actual behavior handling finishes with all of its branches, the bitStream * is then offset to after the allocated bits for this stream. - * + * */ void DoHandleBehavior(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch); /** * @brief Handles a client initialized Basic Attack Behavior cast to be deserialized and verified on the server. - * - * @param context The Skill's Behavior context. All behaviors in the same tree share the same context + * + * @param context The Skill's Behavior context. All behaviors in the same tree share the same context * @param bitStream The bitStream to deserialize. BitStreams will always check their bounds before reading in a behavior * and will fail gracefully if an overread is detected. * @param branch The context of this specific branch of the Skill Behavior. Changes based on which branch you are going down. @@ -27,13 +27,13 @@ public: /** * @brief Writes a 16bit short to the bitStream and when the actual behavior calculation finishes with all of its branches, the number * of bits used is then written to where the 16bit short initially was. - * + * */ void Calculate(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch) override; /** * @brief Calculates a server initialized Basic Attack Behavior cast to be serialized to the client - * + * * @param context The Skill's Behavior context. All behaviors in the same tree share the same context * @param bitStream The bitStream to serialize to. * @param branch The context of this specific branch of the Skill Behavior. Changes based on which branch you are going down. @@ -44,10 +44,12 @@ public: * @brief Loads this Behaviors parameters from the database. For this behavior specifically: * max and min damage will always be the same. If min is less than max, they are both set to max. * If an action is not in the database, then no action is taken for that result. - * + * */ void Load() override; private: + bool m_DontApplyImmune; + uint32_t m_MinDamage; uint32_t m_MaxDamage; diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index 1cf1da40..e98bc33b 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -73,6 +73,8 @@ DestroyableComponent::DestroyableComponent(Entity* parent) : Component(parent) { m_ImmuneToQuickbuildInterruptCount = 0; m_ImmuneToPullToPointCount = 0; m_DeathBehavior = -1; + + m_DamageCooldownTimer = 0.0f; } DestroyableComponent::~DestroyableComponent() { @@ -179,6 +181,10 @@ void DestroyableComponent::Serialize(RakNet::BitStream* outBitStream, bool bIsIn } } +void DestroyableComponent::Update(float deltaTime) { + m_DamageCooldownTimer -= deltaTime; +} + void DestroyableComponent::LoadFromXml(tinyxml2::XMLDocument* doc) { tinyxml2::XMLElement* dest = doc->FirstChildElement("obj")->FirstChildElement("dest"); if (!dest) { @@ -409,7 +415,7 @@ void DestroyableComponent::AddFaction(const int32_t factionID, const bool ignore } bool DestroyableComponent::IsEnemy(const Entity* other) const { - if (m_Parent->IsPlayer() && other->IsPlayer()){ + if (m_Parent->IsPlayer() && other->IsPlayer()) { auto* thisCharacterComponent = m_Parent->GetComponent(); if (!thisCharacterComponent) return false; auto* otherCharacterComponent = other->GetComponent(); @@ -464,6 +470,10 @@ bool DestroyableComponent::IsImmune() const { return m_IsGMImmune || m_ImmuneToBasicAttackCount > 0; } +bool DestroyableComponent::IsCooldownImmune() const { + return m_DamageCooldownTimer > 0.0f; +} + bool DestroyableComponent::IsKnockbackImmune() const { auto* characterComponent = m_Parent->GetComponent(); auto* inventoryComponent = m_Parent->GetComponent(); @@ -546,7 +556,8 @@ void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32 return; } - if (IsImmune()) { + if (IsImmune() || IsCooldownImmune()) { + LOG_DEBUG("Target targetEntity %llu is immune!", m_Parent->GetObjectID()); //Immune is succesfully proc'd return; } @@ -634,9 +645,9 @@ void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32 } //check if hardcore mode is enabled - if (Game::entityManager->GetHardcoreMode()) { + if (Game::entityManager->GetHardcoreMode()) { DoHardcoreModeDrops(source); - } + } Smash(source, eKillType::VIOLENT, u"", skillID); } @@ -796,16 +807,16 @@ void DestroyableComponent::SetFaction(int32_t factionID, bool ignoreChecks) { } void DestroyableComponent::SetStatusImmunity( - const eStateChangeType state, - const bool bImmuneToBasicAttack, - const bool bImmuneToDamageOverTime, - const bool bImmuneToKnockback, - const bool bImmuneToInterrupt, - const bool bImmuneToSpeed, - const bool bImmuneToImaginationGain, - const bool bImmuneToImaginationLoss, - const bool bImmuneToQuickbuildInterrupt, - const bool bImmuneToPullToPoint) { + const eStateChangeType state, + const bool bImmuneToBasicAttack, + const bool bImmuneToDamageOverTime, + const bool bImmuneToKnockback, + const bool bImmuneToInterrupt, + const bool bImmuneToSpeed, + const bool bImmuneToImaginationGain, + const bool bImmuneToImaginationLoss, + const bool bImmuneToQuickbuildInterrupt, + const bool bImmuneToPullToPoint) { if (state == eStateChangeType::POP) { if (bImmuneToBasicAttack && m_ImmuneToBasicAttackCount > 0) m_ImmuneToBasicAttackCount -= 1; @@ -818,7 +829,7 @@ void DestroyableComponent::SetStatusImmunity( if (bImmuneToQuickbuildInterrupt && m_ImmuneToQuickbuildInterruptCount > 0) m_ImmuneToQuickbuildInterruptCount -= 1; if (bImmuneToPullToPoint && m_ImmuneToPullToPointCount > 0) m_ImmuneToPullToPointCount -= 1; - } else if (state == eStateChangeType::PUSH){ + } else if (state == eStateChangeType::PUSH) { if (bImmuneToBasicAttack) m_ImmuneToBasicAttackCount += 1; if (bImmuneToDamageOverTime) m_ImmuneToDamageOverTimeCount += 1; if (bImmuneToKnockback) m_ImmuneToKnockbackCount += 1; @@ -945,7 +956,7 @@ void DestroyableComponent::AddOnHitCallback(const std::function& m_OnHitCallbacks.push_back(callback); } -void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source){ +void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) { //check if this is a player: if (m_Parent->IsPlayer()) { //remove hardcore_lose_uscore_on_death_percent from the player's uscore: @@ -963,9 +974,9 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source){ if (inventory) { //get the items inventory: auto items = inventory->GetInventory(eInventoryType::ITEMS); - if (items){ + if (items) { auto itemMap = items->GetItems(); - if (!itemMap.empty()){ + if (!itemMap.empty()) { for (const auto& item : itemMap) { //drop the item: if (!item.second) continue; diff --git a/dGame/dComponents/DestroyableComponent.h b/dGame/dComponents/DestroyableComponent.h index 52d4be5a..b81ab9f3 100644 --- a/dGame/dComponents/DestroyableComponent.h +++ b/dGame/dComponents/DestroyableComponent.h @@ -24,6 +24,7 @@ public: DestroyableComponent(Entity* parentEntity); ~DestroyableComponent() override; + void Update(float deltaTime) override; void Serialize(RakNet::BitStream* outBitStream, bool bIsInitialUpdate) override; void LoadFromXml(tinyxml2::XMLDocument* doc) override; void UpdateXml(tinyxml2::XMLDocument* doc) override; @@ -166,6 +167,11 @@ public: */ bool IsImmune() const; + /** + * @return whether this entity is currently immune to attacks due to a damage cooldown period + */ + bool IsCooldownImmune() const; + /** * Sets if this entity has GM immunity, making it not killable * @param value the GM immunity of this entity @@ -406,18 +412,23 @@ public: ); // Getters for status immunities - const bool GetImmuneToBasicAttack() {return m_ImmuneToBasicAttackCount > 0;}; - const bool GetImmuneToDamageOverTime() {return m_ImmuneToDamageOverTimeCount > 0;}; - const bool GetImmuneToKnockback() {return m_ImmuneToKnockbackCount > 0;}; - const bool GetImmuneToInterrupt() {return m_ImmuneToInterruptCount > 0;}; - const bool GetImmuneToSpeed() {return m_ImmuneToSpeedCount > 0;}; - const bool GetImmuneToImaginationGain() {return m_ImmuneToImaginationGainCount > 0;}; - const bool GetImmuneToImaginationLoss() {return m_ImmuneToImaginationLossCount > 0;}; - const bool GetImmuneToQuickbuildInterrupt() {return m_ImmuneToQuickbuildInterruptCount > 0;}; - const bool GetImmuneToPullToPoint() {return m_ImmuneToPullToPointCount > 0;}; + const bool GetImmuneToBasicAttack() { return m_ImmuneToBasicAttackCount > 0; }; + const bool GetImmuneToDamageOverTime() { return m_ImmuneToDamageOverTimeCount > 0; }; + const bool GetImmuneToKnockback() { return m_ImmuneToKnockbackCount > 0; }; + const bool GetImmuneToInterrupt() { return m_ImmuneToInterruptCount > 0; }; + const bool GetImmuneToSpeed() { return m_ImmuneToSpeedCount > 0; }; + const bool GetImmuneToImaginationGain() { return m_ImmuneToImaginationGainCount > 0; }; + const bool GetImmuneToImaginationLoss() { return m_ImmuneToImaginationLossCount > 0; }; + const bool GetImmuneToQuickbuildInterrupt() { return m_ImmuneToQuickbuildInterruptCount > 0; }; + const bool GetImmuneToPullToPoint() { return m_ImmuneToPullToPointCount > 0; }; - int32_t GetDeathBehavior() const { return m_DeathBehavior; } + // Damage cooldown setters/getters + void SetDamageCooldownTimer(float value) { m_DamageCooldownTimer = value; } + float GetDamageCooldownTimer() { return m_DamageCooldownTimer; } + + // Death behavior setters/getters void SetDeathBehavior(int32_t value) { m_DeathBehavior = value; } + int32_t GetDeathBehavior() const { return m_DeathBehavior; } /** * Utility to reset all stats to the default stats based on items and completed missions @@ -605,6 +616,11 @@ private: * Death behavior type. If 0, the client plays a death animation as opposed to a smash animation. */ int32_t m_DeathBehavior; + + /** + * Damage immunity cooldown timer. Set to a value that then counts down to create a damage cooldown for players + */ + float m_DamageCooldownTimer; }; #endif // DESTROYABLECOMPONENT_H diff --git a/dGame/dUtilities/SlashCommandHandler.cpp b/dGame/dUtilities/SlashCommandHandler.cpp index af44d543..47037071 100644 --- a/dGame/dUtilities/SlashCommandHandler.cpp +++ b/dGame/dUtilities/SlashCommandHandler.cpp @@ -247,7 +247,7 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit } if (chatCommand == "credits" || chatCommand == "info") { - const auto& customText = chatCommand == "credits" ? VanityUtilities::ParseMarkdown((BinaryPathFinder::GetBinaryDir() / "vanity/CREDITS.md").string()) : VanityUtilities::ParseMarkdown((BinaryPathFinder::GetBinaryDir() / "vanity/INFO.md").string()); + const auto& customText = chatCommand == "credits" ? VanityUtilities::ParseMarkdown((BinaryPathFinder::GetBinaryDir() / "vanity/CREDITS.md").string()) : VanityUtilities::ParseMarkdown((BinaryPathFinder::GetBinaryDir() / "vanity/INFO.md").string()); { AMFArrayValue args; @@ -1490,6 +1490,24 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit return; } + //Testing basic attack immunity + if (chatCommand == "attackimmune" && args.size() >= 1 && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER) { + auto* destroyableComponent = entity->GetComponent(); + + int32_t state = false; + + if (!GeneralUtils::TryParse(args[0], state)) { + ChatPackets::SendSystemMessage(sysAddr, u"Invalid state."); + return; + } + + if (destroyableComponent != nullptr) { + destroyableComponent->SetIsImmune(state); + } + + return; + } + if (chatCommand == "buff" && args.size() >= 2 && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER) { auto* buffComponent = entity->GetComponent(); @@ -1843,7 +1861,7 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit if (chatCommand == "castskill" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 1) { auto* skillComponent = entity->GetComponent(); - if (skillComponent){ + if (skillComponent) { uint32_t skillId; if (!GeneralUtils::TryParse(args[0], skillId)) { @@ -1860,7 +1878,7 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit uint32_t skillId; int slot; auto* inventoryComponent = entity->GetComponent(); - if (inventoryComponent){ + if (inventoryComponent) { if (!GeneralUtils::TryParse(args[0], slot)) { ChatPackets::SendSystemMessage(sysAddr, u"Error getting slot."); return; @@ -1869,7 +1887,7 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit ChatPackets::SendSystemMessage(sysAddr, u"Error getting skill."); return; } else { - if(inventoryComponent->SetSkill(slot, skillId)) ChatPackets::SendSystemMessage(sysAddr, u"Set skill to slot successfully"); + if (inventoryComponent->SetSkill(slot, skillId)) ChatPackets::SendSystemMessage(sysAddr, u"Set skill to slot successfully"); else ChatPackets::SendSystemMessage(sysAddr, u"Set skill to slot failed"); } } @@ -1878,7 +1896,7 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit if (chatCommand == "setfaction" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 1) { auto* destroyableComponent = entity->GetComponent(); - if (destroyableComponent){ + if (destroyableComponent) { int32_t faction; if (!GeneralUtils::TryParse(args[0], faction)) { @@ -1893,7 +1911,7 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit if (chatCommand == "addfaction" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 1) { auto* destroyableComponent = entity->GetComponent(); - if (destroyableComponent){ + if (destroyableComponent) { int32_t faction; if (!GeneralUtils::TryParse(args[0], faction)) { @@ -1908,7 +1926,7 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit if (chatCommand == "getfactions" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER) { auto* destroyableComponent = entity->GetComponent(); - if (destroyableComponent){ + if (destroyableComponent) { ChatPackets::SendSystemMessage(sysAddr, u"Friendly factions:"); for (const auto entry : destroyableComponent->GetFactionIDs()) { ChatPackets::SendSystemMessage(sysAddr, (GeneralUtils::to_u16string(entry))); diff --git a/docs/Commands.md b/docs/Commands.md index ea81ff56..71a3da43 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -25,6 +25,7 @@ |ban|`/ban `|Bans a user from the server.|4| |approveproperty|`/approveproperty`|Approves the property the player is currently visiting.|5| |mute|`/mute (days) (hours)`|Mute player for the given amount of time. If no time is given, the mute is indefinite.|6| +|attackimmune|`/attackimmune `|Sets the character's immunity to basic attacks state, where value can be one of "1", to make yourself immune to basic attack damage, or "0" to undo.|8| |gmimmune|`/gmimmunve `|Sets the character's GMImmune state, where value can be one of "1", to make yourself immune to damage, or "0" to undo.|8| |gminvis|`/gminvis`|Toggles invisibility for the character, though it's currently a bit buggy. Requires nonzero GM Level for the character, but the account must have a GM level of 8.|8| |setname|`/setname `|Sets a temporary name for your player. The name resets when you log out.|8| diff --git a/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp b/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp index acb90352..ff37f154 100644 --- a/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp +++ b/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp @@ -536,3 +536,22 @@ TEST_F(DestroyableTest, DestroyableComponentImmunityTest) { } +/** + * 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()); +}