diff --git a/dGame/dComponents/BaseCombatAIComponent.cpp b/dGame/dComponents/BaseCombatAIComponent.cpp index b97211b8..ed4bf70d 100644 --- a/dGame/dComponents/BaseCombatAIComponent.cpp +++ b/dGame/dComponents/BaseCombatAIComponent.cpp @@ -210,8 +210,10 @@ void BaseCombatAIComponent::Update(const float deltaTime) { } if (stunnedThisFrame) { - m_MovementAI->Stop(); + if (!m_MovementAI->IsPaused()) m_MovementAI->Pause(); + // in this case we just become unstunned so check if we paused and resume if we did + if (!m_Stunned && m_MovementAI->IsPaused()) m_MovementAI->Resume(); return; } @@ -317,12 +319,14 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) { SetAiState(AiState::aggro); } else { SetAiState(AiState::idle); + if (m_MovementAI) m_MovementAI->SetMaxSpeed(1.0f); } if (!hasSkillToCast) return; if (m_Target == LWOOBJID_EMPTY) { SetAiState(AiState::idle); + if (m_MovementAI) m_MovementAI->SetMaxSpeed(1.0f); return; } @@ -618,6 +622,11 @@ void BaseCombatAIComponent::Wander() { return; } + // If we have a path to follow we should almost certainly do that instead of wandering. + if (m_MovementAI->HasPath()) { + return; + } + m_MovementAI->SetHaltDistance(0); const auto& info = m_MovementAI->GetInfo(); @@ -862,12 +871,12 @@ bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReport // roundInfo.PushDebug("Combat Start Delay") = m_CombatStartDelay; std::string curState; switch (m_State) { - case idle: curState = "Idling"; break; - case aggro: curState = "Aggroed"; break; - case tether: curState = "Returning to Tether"; break; - case spawn: curState = "Spawn"; break; - case dead: curState = "Dead"; break; - default: curState = "Unknown or Undefined"; break; + case idle: curState = "Idling"; break; + case aggro: curState = "Aggroed"; break; + case tether: curState = "Returning to Tether"; break; + case spawn: curState = "Spawn"; break; + case dead: curState = "Dead"; break; + default: curState = "Unknown or Undefined"; break; } cmptType.PushDebug("Current Combat State") = curState; diff --git a/dGame/dComponents/BaseCombatAIComponent.h b/dGame/dComponents/BaseCombatAIComponent.h index 95eb75ce..3485ac16 100644 --- a/dGame/dComponents/BaseCombatAIComponent.h +++ b/dGame/dComponents/BaseCombatAIComponent.h @@ -236,6 +236,8 @@ public: bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); + void SetStartingPosition(const NiPoint3& pos) { m_StartPosition = pos; } + private: /** * Returns the current target or the target that currently is the largest threat to this entity diff --git a/dGame/dComponents/MovementAIComponent.cpp b/dGame/dComponents/MovementAIComponent.cpp index 3519712a..2cdf0987 100644 --- a/dGame/dComponents/MovementAIComponent.cpp +++ b/dGame/dComponents/MovementAIComponent.cpp @@ -19,6 +19,12 @@ #include "Amf3.h" #include "dNavMesh.h" +#include "eWaypointCommandType.h" +#include "StringifiedEnum.h" +#include "SkillComponent.h" +#include "GeneralUtils.h" +#include "RenderComponent.h" +#include "InventoryComponent.h" namespace { /** @@ -60,7 +66,7 @@ MovementAIComponent::MovementAIComponent(Entity* parent, const int32_t component RegisterMsg(&MovementAIComponent::OnGetObjectReportInfo); - if (!m_Parent->GetComponent()) SetPath(m_Parent->GetVarAsString(u"attached_path")); + SetPath(m_Parent->GetVarAsString(u"attached_path")); } void MovementAIComponent::SetPath(const std::string pathName) { @@ -162,6 +168,7 @@ void MovementAIComponent::Update(const float deltaTime) { } else { // Check if there are more waypoints in the queue, if so set our next destination to the next waypoint const auto waypointNum = m_IsBounced ? m_CurrentPath.size() : m_CurrentPathWaypointCount - m_CurrentPath.size() - 1; + RunWaypointCommands(waypointNum); if (m_CurrentPath.empty()) { if (m_Path) { if (m_Path->pathBehavior == PathBehavior::Loop) { @@ -172,17 +179,16 @@ void MovementAIComponent::Update(const float deltaTime) { if (m_IsBounced) std::ranges::reverse(waypoints); SetPath(waypoints); } else if (m_Path->pathBehavior == PathBehavior::Once) { - m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum); + // In this case we intended to follow a path and once we've followed it we camp there, otherwise we'd just wander home again. + m_BaseCombatAI->SetStartingPosition(m_SourcePosition); Stop(); return; } } else { - m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum); Stop(); return; } } else { - m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum); SetDestination(m_CurrentPath.top().position); m_CurrentPath.pop(); @@ -423,7 +429,69 @@ NiPoint3 MovementAIComponent::GetDestination() const { void MovementAIComponent::SetMaxSpeed(const float value) { if (value == m_MaxSpeed) return; m_MaxSpeed = value; - m_Acceleration = value / 5; + m_Acceleration = value / 5.0f; +} + +void MovementAIComponent::RunWaypointCommands(uint32_t waypointNum) { + m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum); + + if (!m_Path || waypointNum >= m_Path->pathWaypoints.size()) return; + const auto& commands = m_Path->pathWaypoints[waypointNum].commands; + for (const auto& [command, data] : commands) { + LOG_DEBUG("%s %s %s", StringifiedEnum::ToString(command).data(), m_Path->pathName.c_str(), data.c_str()); + const auto dataSplit = GeneralUtils::SplitString(data, ','); + switch (command) { + case eWaypointCommandType::INVALID: break; + case eWaypointCommandType::BOUNCE: break; + case eWaypointCommandType::STOP: Pause(); break; + case eWaypointCommandType::GROUP_EMOTE: break; + case eWaypointCommandType::SET_VARIABLE: break; // Empty in the client + case eWaypointCommandType::CAST_SKILL: { + const auto skill = GeneralUtils::TryParse(data); + if (skill) { + auto* const skillComponent = m_Parent->GetComponent(); + if (skillComponent) skillComponent->CastSkill(skill.value()); + } + break; + } + case eWaypointCommandType::EQUIP_INVENTORY: { + auto* const inventoryComponent = m_Parent->GetComponent(); + if (inventoryComponent) { + // items should always exist + auto* const item = inventoryComponent->GetInventory(eInventoryType::ITEMS)->FindItemBySlot(0); + inventoryComponent->EquipItem(item); + } + break; + } + case eWaypointCommandType::UNEQUIP_INVENTORY: { + auto* const inventoryComponent = m_Parent->GetComponent(); + if (inventoryComponent) { + // items should always exist + auto* const item = inventoryComponent->GetInventory(eInventoryType::ITEMS)->FindItemBySlot(0); + inventoryComponent->UnEquipItem(item); + } + break; + } + // case eWaypointCommandType::DELAY: { + // Pause(GeneralUtils::TryParse(data).value_or(0.0f)); + // break; + // } + case eWaypointCommandType::EMOTE: { + // m_Delay = RenderComponent::GetAnimationTime(m_Parent, data); + // const auto emoteID = GeneralUtils::TryParse(data); + // if (emoteID) GameMessages::SendPlayEmote(m_Parent->GetObjectID(), emoteID.value(), LWOOBJID_EMPTY, UNASSIGNED_SYSTEM_ADDRESS); + // break; + } + case eWaypointCommandType::TELEPORT: break; + case eWaypointCommandType::PATH_SPEED: m_BaseSpeed = GetBaseSpeed(m_Parent->GetLOT()) * GeneralUtils::TryParse(data).value_or(1.0f); break; + case eWaypointCommandType::REMOVE_NPC: break; + case eWaypointCommandType::CHANGE_WAYPOINT: SetPath(dataSplit[0]); break; + case eWaypointCommandType::DELETE_SELF: break; + case eWaypointCommandType::KILL_SELF: m_Parent->Smash(); break; + case eWaypointCommandType::SPAWN_OBJECT: break; + case eWaypointCommandType::PLAY_SOUND: break; + } + } } bool MovementAIComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) { diff --git a/dGame/dComponents/MovementAIComponent.h b/dGame/dComponents/MovementAIComponent.h index 2a095716..af7da7f0 100644 --- a/dGame/dComponents/MovementAIComponent.h +++ b/dGame/dComponents/MovementAIComponent.h @@ -212,8 +212,16 @@ public: bool IsPaused() const { return m_Paused; } bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); + + bool HasPath() const { return m_Path != nullptr; } private: + /** + * @brief + * Runs the commands on a waypoint if a path exists + */ + void RunWaypointCommands(uint32_t waypointNum); + /** * Sets the current position of the entity * @param value the position to set diff --git a/dZoneManager/Zone.cpp b/dZoneManager/Zone.cpp index 893d9598..6ebd5509 100644 --- a/dZoneManager/Zone.cpp +++ b/dZoneManager/Zone.cpp @@ -101,15 +101,23 @@ void Zone::LoadZoneIntoMemory() { m_Paths.reserve(pathCount); for (uint32_t i = 0; i < pathCount; ++i) LoadPath(file); - for (Path path : m_Paths) { + for (const Path& path : m_Paths) { if (path.pathType != PathType::Spawner) continue; - SpawnerInfo info = SpawnerInfo(); - for (PathWaypoint waypoint : path.pathWaypoints) { + SpawnerInfo info{}; + for (int i = 0; i < path.pathWaypoints.size(); i++) { + const auto& waypoint = path.pathWaypoints[i]; SpawnerNode* node = new SpawnerNode(); node->position = waypoint.position; node->rotation = waypoint.rotation; node->nodeID = 0; - node->config = waypoint.config; + node->config = path.pathWaypoints[0].config; + // All spawner waypoints get the config data of the first waypoint, but then we + // overwrite settings on this waypoint if we have another one defined of the same name + if (i != 0) { + for (const auto& [key, value] : waypoint.config) { + node->config.ParseInsert(value->GetString()); + } + } for (const auto& data : waypoint.config | std::views::values) { if (!data) continue;