From c083f21e44d0fcba42bad5c7720b1a2fb3c6c142 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Fri, 1 Aug 2025 01:09:16 -0700 Subject: [PATCH] feat: OnAttack behavior (#1853) Adds the `OnAttack` property behavior starting node. Tested that having the node allows the model to be attacked to trigger the start of behaviors --- dGame/dComponents/DestroyableComponent.cpp | 18 ++++++++ dGame/dComponents/DestroyableComponent.h | 1 + dGame/dComponents/ModelComponent.cpp | 45 ++++++++++++++++++- dGame/dComponents/ModelComponent.h | 13 ++++++ dGame/dGameMessages/GameMessages.h | 8 ++++ dGame/dPropertyBehaviors/PropertyBehavior.cpp | 4 ++ dGame/dPropertyBehaviors/PropertyBehavior.h | 1 + dGame/dPropertyBehaviors/State.cpp | 4 ++ dGame/dPropertyBehaviors/State.h | 1 + dGame/dPropertyBehaviors/Strip.cpp | 21 +++++++-- dGame/dPropertyBehaviors/Strip.h | 1 + 11 files changed, 111 insertions(+), 6 deletions(-) diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index 4c0e75bd..39d91045 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -30,6 +30,7 @@ #include "CharacterComponent.h" #include "PossessableComponent.h" #include "PossessorComponent.h" +#include "ModelComponent.h" #include "InventoryComponent.h" #include "dZoneManager.h" #include "WorldConfig.h" @@ -82,6 +83,7 @@ DestroyableComponent::DestroyableComponent(Entity* parent) : Component(parent) { m_DamageCooldownTimer = 0.0f; RegisterMsg(this, &DestroyableComponent::OnGetObjectReportInfo); + RegisterMsg(this, &DestroyableComponent::OnSetFaction); } DestroyableComponent::~DestroyableComponent() { @@ -579,6 +581,14 @@ void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32 return; } + // Client does the same check, so we're doing it too + auto* const modelComponent = m_Parent->GetComponent(); + if (modelComponent) { + modelComponent->OnHit(); + // Don't actually deal the damage so the model doesn't die + return; + } + // If this entity has damage reduction, reduce the damage to a minimum of 1 if (m_DamageReduction > 0 && damage > 0) { if (damage > m_DamageReduction) { @@ -1089,3 +1099,11 @@ bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) { return true; } + +bool DestroyableComponent::OnSetFaction(GameMessages::GameMsg& msg) { + auto& modifyFaction = static_cast(msg); + m_DirtyHealth = true; + Game::entityManager->SerializeEntity(m_Parent); + SetFaction(modifyFaction.factionID, modifyFaction.bIgnoreChecks); + return true; +} diff --git a/dGame/dComponents/DestroyableComponent.h b/dGame/dComponents/DestroyableComponent.h index a89b3eab..7ec9bc44 100644 --- a/dGame/dComponents/DestroyableComponent.h +++ b/dGame/dComponents/DestroyableComponent.h @@ -469,6 +469,7 @@ public: void DoHardcoreModeDrops(const LWOOBJID source); bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); + bool OnSetFaction(GameMessages::GameMsg& msg); static Implementation IsEnemyImplentation; static Implementation IsFriendImplentation; diff --git a/dGame/dComponents/ModelComponent.cpp b/dGame/dComponents/ModelComponent.cpp index cc0f2d2a..6066fa4b 100644 --- a/dGame/dComponents/ModelComponent.cpp +++ b/dGame/dComponents/ModelComponent.cpp @@ -15,14 +15,15 @@ #include "DluAssert.h" ModelComponent::ModelComponent(Entity* parent) : Component(parent) { + using namespace GameMessages; m_OriginalPosition = m_Parent->GetDefaultPosition(); m_OriginalRotation = m_Parent->GetDefaultRotation(); m_IsPaused = false; m_NumListeningInteract = 0; m_userModelID = m_Parent->GetVarAs(u"userModelID"); - RegisterMsg(MessageType::Game::REQUEST_USE, this, &ModelComponent::OnRequestUse); - RegisterMsg(MessageType::Game::RESET_MODEL_TO_DEFAULTS, this, &ModelComponent::OnResetModelToDefaults); + RegisterMsg(this, &ModelComponent::OnRequestUse); + RegisterMsg(this, &ModelComponent::OnResetModelToDefaults); } bool ModelComponent::OnResetModelToDefaults(GameMessages::GameMsg& msg) { @@ -40,6 +41,14 @@ bool ModelComponent::OnResetModelToDefaults(GameMessages::GameMsg& msg) { m_Speed = 3.0f; m_NumListeningInteract = 0; m_NumActiveUnSmash = 0; + + m_NumActiveAttack = 0; + GameMessages::SetFaction set{}; + set.target = m_Parent->GetObjectID(); + set.factionID = -1; // Default faction for smashables + set.bIgnoreChecks = true; // Remove the attack faction + set.Send(); + m_Dirty = true; Game::entityManager->SerializeEntity(GetParent()); @@ -297,3 +306,35 @@ void ModelComponent::SetVelocity(const NiPoint3& velocity) const { void ModelComponent::OnChatMessageReceived(const std::string& sMessage) { for (auto& behavior : m_Behaviors) behavior.OnChatMessageReceived(sMessage); } + +void ModelComponent::OnHit() { + for (auto& behavior : m_Behaviors) { + behavior.OnHit(); + } +} + +void ModelComponent::AddAttack() { + LOG_DEBUG("Adding attack %i", m_NumActiveAttack); + m_Dirty = true; + if (m_NumActiveAttack == 0) { + GameMessages::SetFaction set{}; + set.target = m_Parent->GetObjectID(); + set.factionID = 6; // Default faction for smashables + set.Send(); + } + m_NumActiveAttack++; +} + +void ModelComponent::RemoveAttack() { + LOG_DEBUG("Removing attack %i", m_NumActiveAttack); + DluAssert(m_NumActiveAttack > 0); + m_Dirty = true; + m_NumActiveAttack--; + if (m_NumActiveAttack == 0) { + GameMessages::SetFaction set{}; + set.target = m_Parent->GetObjectID(); + set.factionID = -1; // Default faction for smashables + set.bIgnoreChecks = true; // Remove the attack faction + set.Send(); + } +} diff --git a/dGame/dComponents/ModelComponent.h b/dGame/dComponents/ModelComponent.h index c081e469..e5fe83a0 100644 --- a/dGame/dComponents/ModelComponent.h +++ b/dGame/dComponents/ModelComponent.h @@ -146,11 +146,21 @@ public: void OnChatMessageReceived(const std::string& sMessage); + void OnHit(); + // Sets the speed of the model void SetSpeed(const float newSpeed) { m_Speed = newSpeed; } // Whether or not to restart at the end of the frame void RestartAtEndOfFrame() { m_RestartAtEndOfFrame = true; } + + // Increments the number of strips listening for an attack. + // If this is the first strip adding an attack, it will set the factions to the correct values. + void AddAttack(); + + // Decrements the number of strips listening for an attack. + // If this is the last strip removing an attack, it will reset the factions to the default of -1. + void RemoveAttack(); private: // Loads a behavior from the database. @@ -168,6 +178,9 @@ private: // The number of strips listening for a RequestUse GM to come in. uint32_t m_NumListeningInteract{}; + // The number of strips listening for an attack. + uint32_t m_NumActiveAttack{}; + // Whether or not the model is paused and should reject all interactions regarding behaviors. bool m_IsPaused{}; /** diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index 83f7ea9f..cf6e7adf 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -863,5 +863,13 @@ namespace GameMessages { NiPoint3 pos{}; }; + + struct SetFaction : public GameMsg { + SetFaction() : GameMsg(MessageType::Game::SET_FACTION) {} + + int32_t factionID{}; + + bool bIgnoreChecks{ false }; + }; }; #endif // GAMEMESSAGES_H diff --git a/dGame/dPropertyBehaviors/PropertyBehavior.cpp b/dGame/dPropertyBehaviors/PropertyBehavior.cpp index b1b1a30f..0eb3f9df 100644 --- a/dGame/dPropertyBehaviors/PropertyBehavior.cpp +++ b/dGame/dPropertyBehaviors/PropertyBehavior.cpp @@ -184,3 +184,7 @@ void PropertyBehavior::Update(float deltaTime, ModelComponent& modelComponent) { void PropertyBehavior::OnChatMessageReceived(const std::string& sMessage) { for (auto& state : m_States | std::views::values) state.OnChatMessageReceived(sMessage); } + +void PropertyBehavior::OnHit() { + for (auto& state : m_States | std::views::values) state.OnHit(); +} diff --git a/dGame/dPropertyBehaviors/PropertyBehavior.h b/dGame/dPropertyBehaviors/PropertyBehavior.h index 49d75d1a..f6a6be10 100644 --- a/dGame/dPropertyBehaviors/PropertyBehavior.h +++ b/dGame/dPropertyBehaviors/PropertyBehavior.h @@ -42,6 +42,7 @@ public: void Update(float deltaTime, ModelComponent& modelComponent); void OnChatMessageReceived(const std::string& sMessage); + void OnHit(); private: // The current active behavior state. Behaviors can only be in ONE state at a time. diff --git a/dGame/dPropertyBehaviors/State.cpp b/dGame/dPropertyBehaviors/State.cpp index 7f95c2a0..5a2828e4 100644 --- a/dGame/dPropertyBehaviors/State.cpp +++ b/dGame/dPropertyBehaviors/State.cpp @@ -170,3 +170,7 @@ void State::Update(float deltaTime, ModelComponent& modelComponent) { void State::OnChatMessageReceived(const std::string& sMessage) { for (auto& strip : m_Strips) strip.OnChatMessageReceived(sMessage); } + +void State::OnHit() { + for (auto& strip : m_Strips) strip.OnHit(); +} diff --git a/dGame/dPropertyBehaviors/State.h b/dGame/dPropertyBehaviors/State.h index 580b647d..a8d03ba7 100644 --- a/dGame/dPropertyBehaviors/State.h +++ b/dGame/dPropertyBehaviors/State.h @@ -24,6 +24,7 @@ public: void Update(float deltaTime, ModelComponent& modelComponent); void OnChatMessageReceived(const std::string& sMessage); + void OnHit(); private: // The strips contained within this state. diff --git a/dGame/dPropertyBehaviors/Strip.cpp b/dGame/dPropertyBehaviors/Strip.cpp index ff3bb0e3..a923691e 100644 --- a/dGame/dPropertyBehaviors/Strip.cpp +++ b/dGame/dPropertyBehaviors/Strip.cpp @@ -118,6 +118,16 @@ void Strip::OnChatMessageReceived(const std::string& sMessage) { } } +void Strip::OnHit() { + if (m_PausedTime > 0.0f || !HasMinimumActions()) return; + + const auto& nextAction = GetNextAction(); + if (nextAction.GetType() == "OnAttack") { + IncrementAction(); + m_WaitingForAction = false; + } +} + void Strip::IncrementAction() { if (m_Actions.empty()) return; m_NextActionIndex++; @@ -259,6 +269,8 @@ void Strip::RemoveStates(ModelComponent& modelComponent) const { if (prevActionType == "OnInteract") { modelComponent.RemoveInteract(); Game::entityManager->SerializeEntity(modelComponent.GetParent()); + } else if (prevActionType == "OnAttack") { + modelComponent.RemoveAttack(); } else if (prevActionType == "UnSmash") { modelComponent.RemoveUnSmash(); } @@ -336,13 +348,14 @@ void Strip::Update(float deltaTime, ModelComponent& modelComponent) { if (m_NextActionIndex == 0) { if (nextAction.GetType() == "OnInteract") { modelComponent.AddInteract(); - Game::entityManager->SerializeEntity(entity); - m_WaitingForAction = true; } else if (nextAction.GetType() == "OnChat") { - Game::entityManager->SerializeEntity(entity); - m_WaitingForAction = true; + // logic here if needed + } else if (nextAction.GetType() == "OnAttack") { + modelComponent.AddAttack(); } + Game::entityManager->SerializeEntity(entity); + m_WaitingForAction = true; } else { // should be a normal block ProcNormalAction(deltaTime, modelComponent); } diff --git a/dGame/dPropertyBehaviors/Strip.h b/dGame/dPropertyBehaviors/Strip.h index aee437cd..1a61afd5 100644 --- a/dGame/dPropertyBehaviors/Strip.h +++ b/dGame/dPropertyBehaviors/Strip.h @@ -42,6 +42,7 @@ public: bool HasMinimumActions() const { return m_Actions.size() >= 2; } void OnChatMessageReceived(const std::string& sMessage); + void OnHit(); private: // Indicates this Strip is waiting for an action to be taken upon it to progress to its actions bool m_WaitingForAction{ false };