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
This commit is contained in:
David Markowitz
2025-08-01 01:09:16 -07:00
committed by GitHub
parent c9e95839ee
commit c083f21e44
11 changed files with 111 additions and 6 deletions

View File

@@ -30,6 +30,7 @@
#include "CharacterComponent.h" #include "CharacterComponent.h"
#include "PossessableComponent.h" #include "PossessableComponent.h"
#include "PossessorComponent.h" #include "PossessorComponent.h"
#include "ModelComponent.h"
#include "InventoryComponent.h" #include "InventoryComponent.h"
#include "dZoneManager.h" #include "dZoneManager.h"
#include "WorldConfig.h" #include "WorldConfig.h"
@@ -82,6 +83,7 @@ DestroyableComponent::DestroyableComponent(Entity* parent) : Component(parent) {
m_DamageCooldownTimer = 0.0f; m_DamageCooldownTimer = 0.0f;
RegisterMsg<GetObjectReportInfo>(this, &DestroyableComponent::OnGetObjectReportInfo); RegisterMsg<GetObjectReportInfo>(this, &DestroyableComponent::OnGetObjectReportInfo);
RegisterMsg<GameMessages::SetFaction>(this, &DestroyableComponent::OnSetFaction);
} }
DestroyableComponent::~DestroyableComponent() { DestroyableComponent::~DestroyableComponent() {
@@ -579,6 +581,14 @@ void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32
return; return;
} }
// Client does the same check, so we're doing it too
auto* const modelComponent = m_Parent->GetComponent<ModelComponent>();
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 this entity has damage reduction, reduce the damage to a minimum of 1
if (m_DamageReduction > 0 && damage > 0) { if (m_DamageReduction > 0 && damage > 0) {
if (damage > m_DamageReduction) { if (damage > m_DamageReduction) {
@@ -1089,3 +1099,11 @@ bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
return true; return true;
} }
bool DestroyableComponent::OnSetFaction(GameMessages::GameMsg& msg) {
auto& modifyFaction = static_cast<GameMessages::SetFaction&>(msg);
m_DirtyHealth = true;
Game::entityManager->SerializeEntity(m_Parent);
SetFaction(modifyFaction.factionID, modifyFaction.bIgnoreChecks);
return true;
}

View File

@@ -469,6 +469,7 @@ public:
void DoHardcoreModeDrops(const LWOOBJID source); void DoHardcoreModeDrops(const LWOOBJID source);
bool OnGetObjectReportInfo(GameMessages::GameMsg& msg); bool OnGetObjectReportInfo(GameMessages::GameMsg& msg);
bool OnSetFaction(GameMessages::GameMsg& msg);
static Implementation<bool, const Entity*> IsEnemyImplentation; static Implementation<bool, const Entity*> IsEnemyImplentation;
static Implementation<bool, const Entity*> IsFriendImplentation; static Implementation<bool, const Entity*> IsFriendImplentation;

View File

@@ -15,14 +15,15 @@
#include "DluAssert.h" #include "DluAssert.h"
ModelComponent::ModelComponent(Entity* parent) : Component(parent) { ModelComponent::ModelComponent(Entity* parent) : Component(parent) {
using namespace GameMessages;
m_OriginalPosition = m_Parent->GetDefaultPosition(); m_OriginalPosition = m_Parent->GetDefaultPosition();
m_OriginalRotation = m_Parent->GetDefaultRotation(); m_OriginalRotation = m_Parent->GetDefaultRotation();
m_IsPaused = false; m_IsPaused = false;
m_NumListeningInteract = 0; m_NumListeningInteract = 0;
m_userModelID = m_Parent->GetVarAs<LWOOBJID>(u"userModelID"); m_userModelID = m_Parent->GetVarAs<LWOOBJID>(u"userModelID");
RegisterMsg(MessageType::Game::REQUEST_USE, this, &ModelComponent::OnRequestUse); RegisterMsg<RequestUse>(this, &ModelComponent::OnRequestUse);
RegisterMsg(MessageType::Game::RESET_MODEL_TO_DEFAULTS, this, &ModelComponent::OnResetModelToDefaults); RegisterMsg<ResetModelToDefaults>(this, &ModelComponent::OnResetModelToDefaults);
} }
bool ModelComponent::OnResetModelToDefaults(GameMessages::GameMsg& msg) { bool ModelComponent::OnResetModelToDefaults(GameMessages::GameMsg& msg) {
@@ -40,6 +41,14 @@ bool ModelComponent::OnResetModelToDefaults(GameMessages::GameMsg& msg) {
m_Speed = 3.0f; m_Speed = 3.0f;
m_NumListeningInteract = 0; m_NumListeningInteract = 0;
m_NumActiveUnSmash = 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; m_Dirty = true;
Game::entityManager->SerializeEntity(GetParent()); Game::entityManager->SerializeEntity(GetParent());
@@ -297,3 +306,35 @@ void ModelComponent::SetVelocity(const NiPoint3& velocity) const {
void ModelComponent::OnChatMessageReceived(const std::string& sMessage) { void ModelComponent::OnChatMessageReceived(const std::string& sMessage) {
for (auto& behavior : m_Behaviors) behavior.OnChatMessageReceived(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();
}
}

View File

@@ -146,11 +146,21 @@ public:
void OnChatMessageReceived(const std::string& sMessage); void OnChatMessageReceived(const std::string& sMessage);
void OnHit();
// Sets the speed of the model // Sets the speed of the model
void SetSpeed(const float newSpeed) { m_Speed = newSpeed; } void SetSpeed(const float newSpeed) { m_Speed = newSpeed; }
// Whether or not to restart at the end of the frame // Whether or not to restart at the end of the frame
void RestartAtEndOfFrame() { m_RestartAtEndOfFrame = true; } 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: private:
// Loads a behavior from the database. // Loads a behavior from the database.
@@ -168,6 +178,9 @@ private:
// The number of strips listening for a RequestUse GM to come in. // The number of strips listening for a RequestUse GM to come in.
uint32_t m_NumListeningInteract{}; 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. // Whether or not the model is paused and should reject all interactions regarding behaviors.
bool m_IsPaused{}; bool m_IsPaused{};
/** /**

View File

@@ -863,5 +863,13 @@ namespace GameMessages {
NiPoint3 pos{}; NiPoint3 pos{};
}; };
struct SetFaction : public GameMsg {
SetFaction() : GameMsg(MessageType::Game::SET_FACTION) {}
int32_t factionID{};
bool bIgnoreChecks{ false };
};
}; };
#endif // GAMEMESSAGES_H #endif // GAMEMESSAGES_H

View File

@@ -184,3 +184,7 @@ void PropertyBehavior::Update(float deltaTime, ModelComponent& modelComponent) {
void PropertyBehavior::OnChatMessageReceived(const std::string& sMessage) { void PropertyBehavior::OnChatMessageReceived(const std::string& sMessage) {
for (auto& state : m_States | std::views::values) state.OnChatMessageReceived(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();
}

View File

@@ -42,6 +42,7 @@ public:
void Update(float deltaTime, ModelComponent& modelComponent); void Update(float deltaTime, ModelComponent& modelComponent);
void OnChatMessageReceived(const std::string& sMessage); void OnChatMessageReceived(const std::string& sMessage);
void OnHit();
private: private:
// The current active behavior state. Behaviors can only be in ONE state at a time. // The current active behavior state. Behaviors can only be in ONE state at a time.

View File

@@ -170,3 +170,7 @@ void State::Update(float deltaTime, ModelComponent& modelComponent) {
void State::OnChatMessageReceived(const std::string& sMessage) { void State::OnChatMessageReceived(const std::string& sMessage) {
for (auto& strip : m_Strips) strip.OnChatMessageReceived(sMessage); for (auto& strip : m_Strips) strip.OnChatMessageReceived(sMessage);
} }
void State::OnHit() {
for (auto& strip : m_Strips) strip.OnHit();
}

View File

@@ -24,6 +24,7 @@ public:
void Update(float deltaTime, ModelComponent& modelComponent); void Update(float deltaTime, ModelComponent& modelComponent);
void OnChatMessageReceived(const std::string& sMessage); void OnChatMessageReceived(const std::string& sMessage);
void OnHit();
private: private:
// The strips contained within this state. // The strips contained within this state.

View File

@@ -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() { void Strip::IncrementAction() {
if (m_Actions.empty()) return; if (m_Actions.empty()) return;
m_NextActionIndex++; m_NextActionIndex++;
@@ -259,6 +269,8 @@ void Strip::RemoveStates(ModelComponent& modelComponent) const {
if (prevActionType == "OnInteract") { if (prevActionType == "OnInteract") {
modelComponent.RemoveInteract(); modelComponent.RemoveInteract();
Game::entityManager->SerializeEntity(modelComponent.GetParent()); Game::entityManager->SerializeEntity(modelComponent.GetParent());
} else if (prevActionType == "OnAttack") {
modelComponent.RemoveAttack();
} else if (prevActionType == "UnSmash") { } else if (prevActionType == "UnSmash") {
modelComponent.RemoveUnSmash(); modelComponent.RemoveUnSmash();
} }
@@ -336,13 +348,14 @@ void Strip::Update(float deltaTime, ModelComponent& modelComponent) {
if (m_NextActionIndex == 0) { if (m_NextActionIndex == 0) {
if (nextAction.GetType() == "OnInteract") { if (nextAction.GetType() == "OnInteract") {
modelComponent.AddInteract(); modelComponent.AddInteract();
Game::entityManager->SerializeEntity(entity);
m_WaitingForAction = true;
} else if (nextAction.GetType() == "OnChat") { } else if (nextAction.GetType() == "OnChat") {
Game::entityManager->SerializeEntity(entity); // logic here if needed
m_WaitingForAction = true; } else if (nextAction.GetType() == "OnAttack") {
modelComponent.AddAttack();
} }
Game::entityManager->SerializeEntity(entity);
m_WaitingForAction = true;
} else { // should be a normal block } else { // should be a normal block
ProcNormalAction(deltaTime, modelComponent); ProcNormalAction(deltaTime, modelComponent);
} }

View File

@@ -42,6 +42,7 @@ public:
bool HasMinimumActions() const { return m_Actions.size() >= 2; } bool HasMinimumActions() const { return m_Actions.size() >= 2; }
void OnChatMessageReceived(const std::string& sMessage); void OnChatMessageReceived(const std::string& sMessage);
void OnHit();
private: private:
// Indicates this Strip is waiting for an action to be taken upon it to progress to its actions // Indicates this Strip is waiting for an action to be taken upon it to progress to its actions
bool m_WaitingForAction{ false }; bool m_WaitingForAction{ false };