feat: Property behaviors partially functional (#1759)

* most of gameplay tab works

* smash unsmash and wait working

* Add pausing of models and behaviors

* working basic behaviors

* play sound functioning

* add resetting

* Fix asynchronous actions executing other strips actions

* Add comments, remove dead code etc.

* Skip Smashes if they coincide with a UnSmash

Remove debug logs

Comment on return
This commit is contained in:
David Markowitz
2025-05-05 00:17:39 -07:00
committed by GitHub
parent 841b754b01
commit 3ebc6709db
13 changed files with 465 additions and 64 deletions

View File

@@ -4,9 +4,13 @@
#include "BehaviorStates.h"
#include "ControlBehaviorMsgs.h"
#include "tinyxml2.h"
#include "ModelComponent.h"
#include <ranges>
PropertyBehavior::PropertyBehavior() {
m_LastEditedState = BehaviorState::HOME_STATE;
m_ActiveState = BehaviorState::HOME_STATE;
}
template<>
@@ -84,6 +88,17 @@ void PropertyBehavior::HandleMsg(AddMessage& msg) {
isLoot = m_BehaviorId != 7965;
};
template<>
void PropertyBehavior::HandleMsg(GameMessages::RequestUse& msg) {
m_States[m_ActiveState].HandleMsg(msg);
}
template<>
void PropertyBehavior::HandleMsg(GameMessages::ResetModelToDefaults& msg) {
m_ActiveState = BehaviorState::HOME_STATE;
for (auto& state : m_States | std::views::values) state.HandleMsg(msg);
}
void PropertyBehavior::SendBehaviorListToClient(AMFArrayValue& args) const {
args.Insert("id", std::to_string(m_BehaviorId));
args.Insert("name", m_Name);
@@ -153,3 +168,7 @@ void PropertyBehavior::Deserialize(const tinyxml2::XMLElement& behavior) {
m_States[static_cast<BehaviorState>(stateId)].Deserialize(*stateElement);
}
}
void PropertyBehavior::Update(float deltaTime, ModelComponent& modelComponent) {
for (auto& state : m_States | std::views::values) state.Update(deltaTime, modelComponent);
}

View File

@@ -10,6 +10,7 @@ namespace tinyxml2 {
enum class BehaviorState : uint32_t;
class AMFArrayValue;
class ModelComponent;
/**
* Represents the Entity of a Property Behavior and holds data associated with the behavior
@@ -31,7 +32,12 @@ public:
void Serialize(tinyxml2::XMLElement& behavior) const;
void Deserialize(const tinyxml2::XMLElement& behavior);
void Update(float deltaTime, ModelComponent& modelComponent);
private:
// The current active behavior state. Behaviors can only be in ONE state at a time.
BehaviorState m_ActiveState;
// The states this behavior has.
std::map<BehaviorState, State> m_States;

View File

@@ -117,6 +117,16 @@ void State::HandleMsg(MigrateActionsMessage& msg) {
}
};
template<>
void State::HandleMsg(GameMessages::RequestUse& msg) {
for (auto& strip : m_Strips) strip.HandleMsg(msg);
}
template<>
void State::HandleMsg(GameMessages::ResetModelToDefaults& msg) {
for (auto& strip : m_Strips) strip.HandleMsg(msg);
}
bool State::IsEmpty() const {
for (const auto& strip : m_Strips) {
if (!strip.IsEmpty()) return false;
@@ -152,3 +162,7 @@ void State::Deserialize(const tinyxml2::XMLElement& state) {
strip.Deserialize(*stripElement);
}
}
void State::Update(float deltaTime, ModelComponent& modelComponent) {
for (auto& strip : m_Strips) strip.Update(deltaTime, modelComponent);
}

View File

@@ -8,6 +8,7 @@ namespace tinyxml2 {
}
class AMFArrayValue;
class ModelComponent;
class State {
public:
@@ -19,7 +20,11 @@ public:
void Serialize(tinyxml2::XMLElement& state) const;
void Deserialize(const tinyxml2::XMLElement& state);
void Update(float deltaTime, ModelComponent& modelComponent);
private:
// The strips contained within this state.
std::vector<Strip> m_Strips;
};

View File

@@ -3,6 +3,11 @@
#include "Amf3.h"
#include "ControlBehaviorMsgs.h"
#include "tinyxml2.h"
#include "dEntity/EntityInfo.h"
#include "ModelComponent.h"
#include "PlayerManager.h"
#include "DluAssert.h"
template <>
void Strip::HandleMsg(AddStripMessage& msg) {
@@ -75,7 +80,138 @@ void Strip::HandleMsg(MigrateActionsMessage& msg) {
} else {
m_Actions.insert(m_Actions.begin() + msg.GetDstActionIndex(), msg.GetMigratedActions().begin(), msg.GetMigratedActions().end());
}
};
}
template<>
void Strip::HandleMsg(GameMessages::RequestUse& msg) {
if (m_PausedTime > 0.0f) return;
if (m_Actions[m_NextActionIndex].GetType() == "OnInteract") {
IncrementAction();
m_WaitingForAction = false;
}
}
template<>
void Strip::HandleMsg(GameMessages::ResetModelToDefaults& msg) {
m_WaitingForAction = false;
m_PausedTime = 0.0f;
m_NextActionIndex = 0;
}
void Strip::IncrementAction() {
if (m_Actions.empty()) return;
m_NextActionIndex++;
m_NextActionIndex %= m_Actions.size();
}
void Strip::Spawn(LOT lot, Entity& entity) {
EntityInfo info{};
info.lot = lot;
info.pos = entity.GetPosition();
info.rot = NiQuaternionConstant::IDENTITY;
info.spawnerID = entity.GetObjectID();
Game::entityManager->ConstructEntity(Game::entityManager->CreateEntity(info, nullptr, &entity));
}
// Spawns a specific drop for all
void Strip::SpawnDrop(LOT dropLOT, Entity& entity) {
for (auto* const player : PlayerManager::GetAllPlayers()) {
GameMessages::SendDropClientLoot(player, entity.GetObjectID(), dropLOT, 0, entity.GetPosition());
}
}
void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent) {
auto& entity = *modelComponent.GetParent();
auto& nextAction = GetNextAction();
auto number = nextAction.GetValueParameterDouble();
auto numberAsInt = static_cast<int32_t>(number);
auto nextActionType = GetNextAction().GetType();
if (nextActionType == "SpawnStromling") {
Spawn(10495, entity); // Stromling property
} else if (nextActionType == "SpawnPirate") {
Spawn(10497, entity); // Maelstrom Pirate property
} else if (nextActionType == "SpawnRonin") {
Spawn(10498, entity); // Dark Ronin property
} else if (nextActionType == "DropImagination") {
for (; numberAsInt > 0; numberAsInt--) SpawnDrop(935, entity); // 1 Imagination powerup
} else if (nextActionType == "DropHealth") {
for (; numberAsInt > 0; numberAsInt--) SpawnDrop(177, entity); // 1 Life powerup
} else if (nextActionType == "DropArmor") {
for (; numberAsInt > 0; numberAsInt--) SpawnDrop(6431, entity); // 1 Armor powerup
} else if (nextActionType == "Smash") {
if (!modelComponent.IsUnSmashing()) {
GameMessages::Smash smash{};
smash.target = entity.GetObjectID();
smash.killerID = entity.GetObjectID();
smash.Send(UNASSIGNED_SYSTEM_ADDRESS);
}
} else if (nextActionType == "UnSmash") {
GameMessages::UnSmash unsmash{};
unsmash.target = entity.GetObjectID();
unsmash.duration = number;
unsmash.builderID = LWOOBJID_EMPTY;
unsmash.Send(UNASSIGNED_SYSTEM_ADDRESS);
modelComponent.AddUnSmash();
m_PausedTime = number;
} else if (nextActionType == "Wait") {
m_PausedTime = number;
} else if (nextActionType == "PlaySound") {
GameMessages::PlayBehaviorSound sound;
sound.target = modelComponent.GetParent()->GetObjectID();
sound.soundID = numberAsInt;
sound.Send(UNASSIGNED_SYSTEM_ADDRESS);
} else {
static std::set<std::string> g_WarnedActions;
if (!g_WarnedActions.contains(nextActionType.data())) {
LOG("Tried to play action (%s) which is not supported.", nextActionType.data());
g_WarnedActions.insert(nextActionType.data());
}
return;
}
IncrementAction();
}
// Decrement references to the previous state if we have progressed to the next one.
void Strip::RemoveStates(ModelComponent& modelComponent) const {
const auto& prevAction = GetPreviousAction();
const auto prevActionType = prevAction.GetType();
if (prevActionType == "OnInteract") {
modelComponent.RemoveInteract();
Game::entityManager->SerializeEntity(modelComponent.GetParent());
} else if (prevActionType == "UnSmash") {
modelComponent.RemoveUnSmash();
}
}
void Strip::Update(float deltaTime, ModelComponent& modelComponent) {
m_PausedTime -= deltaTime;
if (m_PausedTime > 0.0f) return;
m_PausedTime = 0.0f;
if (m_WaitingForAction) return;
auto& entity = *modelComponent.GetParent();
auto& nextAction = GetNextAction();
RemoveStates(modelComponent);
// Check for starting blocks and if not a starting block proc this blocks action
if (m_NextActionIndex == 0) {
if (nextAction.GetType() == "OnInteract") {
modelComponent.AddInteract();
Game::entityManager->SerializeEntity(entity);
m_WaitingForAction = true;
}
} else { // should be a normal block
ProcNormalAction(deltaTime, modelComponent);
}
}
void Strip::SendBehaviorBlocksToClient(AMFArrayValue& args) const {
m_Position.SendBehaviorBlocksToClient(args);
@@ -106,3 +242,13 @@ void Strip::Deserialize(const tinyxml2::XMLElement& strip) {
action.Deserialize(*actionElement);
}
}
const Action& Strip::GetNextAction() const {
DluAssert(m_NextActionIndex < m_Actions.size()); return m_Actions[m_NextActionIndex];
}
const Action& Strip::GetPreviousAction() const {
DluAssert(m_NextActionIndex < m_Actions.size());
size_t index = m_NextActionIndex == 0 ? m_Actions.size() - 1 : m_NextActionIndex - 1;
return m_Actions[index];
}

View File

@@ -11,6 +11,7 @@ namespace tinyxml2 {
}
class AMFArrayValue;
class ModelComponent;
class Strip {
public:
@@ -22,8 +23,30 @@ public:
void Serialize(tinyxml2::XMLElement& strip) const;
void Deserialize(const tinyxml2::XMLElement& strip);
const Action& GetNextAction() const;
const Action& GetPreviousAction() const;
void IncrementAction();
void Spawn(LOT object, Entity& entity);
void Update(float deltaTime, ModelComponent& modelComponent);
void SpawnDrop(LOT dropLOT, Entity& entity);
void ProcNormalAction(float deltaTime, ModelComponent& modelComponent);
void RemoveStates(ModelComponent& modelComponent) const;
private:
// Indicates this Strip is waiting for an action to be taken upon it to progress to its actions
bool m_WaitingForAction{ false };
// The amount of time this strip is paused for. Any interactions with this strip should be bounced if this is greater than 0.
float m_PausedTime{ 0.0f };
// The index of the next action to be played. This should always be within range of [0, m_Actions.size()).
size_t m_NextActionIndex{ 0 };
// The list of actions to be executed on this behavior.
std::vector<Action> m_Actions;
// The location of this strip on the UGBehaviorEditor UI
StripUiPosition m_Position;
};