Skip to content

Commit

Permalink
Adding damage cooldown/"invincibility frames" as in Live (#1276)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jadebenn committed Nov 12, 2023
1 parent 2c9a983 commit 411dce7
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 44 deletions.
26 changes: 24 additions & 2 deletions dGame/dBehaviors/BasicAttackBehavior.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -13,8 +15,15 @@ void BasicAttackBehavior::Handle(BehaviorContext* context, RakNet::BitStream* bi

auto* destroyableComponent = entity->GetComponent<DestroyableComponent>();
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);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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;

Expand Down
14 changes: 8 additions & 6 deletions dGame/dBehaviors/BasicAttackBehavior.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ class BasicAttackBehavior final : public Behavior
/**
* @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.
Expand All @@ -27,13 +27,13 @@ class BasicAttackBehavior final : public Behavior
/**
* @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.
Expand All @@ -44,10 +44,12 @@ class BasicAttackBehavior final : public Behavior
* @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;
Expand Down
47 changes: 29 additions & 18 deletions dGame/dComponents/DestroyableComponent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<CharacterComponent>();
if (!thisCharacterComponent) return false;
auto* otherCharacterComponent = other->GetComponent<CharacterComponent>();
Expand Down Expand Up @@ -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<CharacterComponent>();
auto* inventoryComponent = m_Parent->GetComponent<InventoryComponent>();
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -945,7 +956,7 @@ void DestroyableComponent::AddOnHitCallback(const std::function<void(Entity*)>&
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:
Expand All @@ -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;
Expand Down
38 changes: 27 additions & 11 deletions dGame/dComponents/DestroyableComponent.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class DestroyableComponent : public Component {
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;
Expand Down Expand Up @@ -166,6 +167,11 @@ class DestroyableComponent : public Component {
*/
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
Expand Down Expand Up @@ -406,18 +412,23 @@ class DestroyableComponent : public Component {
);

// 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;};

int32_t GetDeathBehavior() const { return m_DeathBehavior; }
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; };

// 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
Expand Down Expand Up @@ -605,6 +616,11 @@ class DestroyableComponent : public Component {
* 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
32 changes: 25 additions & 7 deletions dGame/dUtilities/SlashCommandHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<DestroyableComponent>();

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<BuffComponent>();

Expand Down Expand Up @@ -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<SkillComponent>();
if (skillComponent){
if (skillComponent) {
uint32_t skillId;

if (!GeneralUtils::TryParse(args[0], skillId)) {
Expand All @@ -1860,7 +1878,7 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit
uint32_t skillId;
int slot;
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
if (inventoryComponent){
if (inventoryComponent) {
if (!GeneralUtils::TryParse(args[0], slot)) {
ChatPackets::SendSystemMessage(sysAddr, u"Error getting slot.");
return;
Expand All @@ -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");
}
}
Expand All @@ -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<DestroyableComponent>();
if (destroyableComponent){
if (destroyableComponent) {
int32_t faction;

if (!GeneralUtils::TryParse(args[0], faction)) {
Expand All @@ -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<DestroyableComponent>();
if (destroyableComponent){
if (destroyableComponent) {
int32_t faction;

if (!GeneralUtils::TryParse(args[0], faction)) {
Expand All @@ -1908,7 +1926,7 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit

if (chatCommand == "getfactions" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER) {
auto* destroyableComponent = entity->GetComponent<DestroyableComponent>();
if (destroyableComponent){
if (destroyableComponent) {
ChatPackets::SendSystemMessage(sysAddr, u"Friendly factions:");
for (const auto entry : destroyableComponent->GetFactionIDs()) {
ChatPackets::SendSystemMessage(sysAddr, (GeneralUtils::to_u16string(entry)));
Expand Down
Loading

0 comments on commit 411dce7

Please sign in to comment.