Compare commits

..

4 Commits

Author SHA1 Message Date
David Markowitz
47535f3c3a feedback 2026-06-17 01:56:18 -07:00
David Markowitz
105ddf4e1d feat: enemy npc pathing
they live 🎉
tested that enemies path all around the world should they have a path configured.
tested that the admiral in gf (at the first camp) paths now.
fixes #1546
2026-06-17 01:47:09 -07:00
David Markowitz
c898356eba fix: enemies not interrupting QB's when they do damage (#1998)
tested that stromlings in AG now correctly interrupt quickbuilds if the player takes damage
2026-06-16 09:49:56 -05:00
David Markowitz
79bb48d3bc feat: implement a bunch of basic scripts that don't really do anything (#1999)
* feat: implement a bunch of basic scripts that don't really do anything

None of these do anything noticeable or break anything

* fixes
2026-06-16 09:49:30 -05:00
11 changed files with 157 additions and 20 deletions

View File

@@ -210,8 +210,10 @@ void BaseCombatAIComponent::Update(const float deltaTime) {
} }
if (stunnedThisFrame) { 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; return;
} }
@@ -317,12 +319,14 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
SetAiState(AiState::aggro); SetAiState(AiState::aggro);
} else { } else {
SetAiState(AiState::idle); SetAiState(AiState::idle);
if (m_MovementAI) m_MovementAI->SetMaxSpeed(1.0f);
} }
if (!hasSkillToCast) return; if (!hasSkillToCast) return;
if (m_Target == LWOOBJID_EMPTY) { if (m_Target == LWOOBJID_EMPTY) {
SetAiState(AiState::idle); SetAiState(AiState::idle);
if (m_MovementAI) m_MovementAI->SetMaxSpeed(1.0f);
return; return;
} }
@@ -618,6 +622,11 @@ void BaseCombatAIComponent::Wander() {
return; 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); m_MovementAI->SetHaltDistance(0);
const auto& info = m_MovementAI->GetInfo(); const auto& info = m_MovementAI->GetInfo();
@@ -862,12 +871,12 @@ bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReport
// roundInfo.PushDebug<AMFDoubleValue>("Combat Start Delay") = m_CombatStartDelay; // roundInfo.PushDebug<AMFDoubleValue>("Combat Start Delay") = m_CombatStartDelay;
std::string curState; std::string curState;
switch (m_State) { switch (m_State) {
case idle: curState = "Idling"; break; case idle: curState = "Idling"; break;
case aggro: curState = "Aggroed"; break; case aggro: curState = "Aggroed"; break;
case tether: curState = "Returning to Tether"; break; case tether: curState = "Returning to Tether"; break;
case spawn: curState = "Spawn"; break; case spawn: curState = "Spawn"; break;
case dead: curState = "Dead"; break; case dead: curState = "Dead"; break;
default: curState = "Unknown or Undefined"; break; default: curState = "Unknown or Undefined"; break;
} }
cmptType.PushDebug<AMFStringValue>("Current Combat State") = curState; cmptType.PushDebug<AMFStringValue>("Current Combat State") = curState;

View File

@@ -236,6 +236,8 @@ public:
bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); bool MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
void SetStartingPosition(const NiPoint3& pos) { m_StartPosition = pos; }
private: private:
/** /**
* Returns the current target or the target that currently is the largest threat to this entity * Returns the current target or the target that currently is the largest threat to this entity

View File

@@ -19,6 +19,12 @@
#include "Amf3.h" #include "Amf3.h"
#include "dNavMesh.h" #include "dNavMesh.h"
#include "eWaypointCommandType.h"
#include "StringifiedEnum.h"
#include "SkillComponent.h"
#include "GeneralUtils.h"
#include "RenderComponent.h"
#include "InventoryComponent.h"
namespace { namespace {
/** /**
@@ -60,7 +66,7 @@ MovementAIComponent::MovementAIComponent(Entity* parent, const int32_t component
RegisterMsg(&MovementAIComponent::OnGetObjectReportInfo); RegisterMsg(&MovementAIComponent::OnGetObjectReportInfo);
if (!m_Parent->GetComponent<BaseCombatAIComponent>()) SetPath(m_Parent->GetVarAsString(u"attached_path")); SetPath(m_Parent->GetVarAsString(u"attached_path"));
} }
void MovementAIComponent::SetPath(const std::string pathName) { void MovementAIComponent::SetPath(const std::string pathName) {
@@ -162,6 +168,7 @@ void MovementAIComponent::Update(const float deltaTime) {
} else { } else {
// Check if there are more waypoints in the queue, if so set our next destination to the next waypoint // 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; const auto waypointNum = m_IsBounced ? m_CurrentPath.size() : m_CurrentPathWaypointCount - m_CurrentPath.size() - 1;
RunWaypointCommands(waypointNum);
if (m_CurrentPath.empty()) { if (m_CurrentPath.empty()) {
if (m_Path) { if (m_Path) {
if (m_Path->pathBehavior == PathBehavior::Loop) { if (m_Path->pathBehavior == PathBehavior::Loop) {
@@ -172,17 +179,16 @@ void MovementAIComponent::Update(const float deltaTime) {
if (m_IsBounced) std::ranges::reverse(waypoints); if (m_IsBounced) std::ranges::reverse(waypoints);
SetPath(waypoints); SetPath(waypoints);
} else if (m_Path->pathBehavior == PathBehavior::Once) { } 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.
if (m_BaseCombatAI) m_BaseCombatAI->SetStartingPosition(m_SourcePosition);
Stop(); Stop();
return; return;
} }
} else { } else {
m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum);
Stop(); Stop();
return; return;
} }
} else { } else {
m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum);
SetDestination(m_CurrentPath.top().position); SetDestination(m_CurrentPath.top().position);
m_CurrentPath.pop(); m_CurrentPath.pop();
@@ -423,7 +429,69 @@ NiPoint3 MovementAIComponent::GetDestination() const {
void MovementAIComponent::SetMaxSpeed(const float value) { void MovementAIComponent::SetMaxSpeed(const float value) {
if (value == m_MaxSpeed) return; if (value == m_MaxSpeed) return;
m_MaxSpeed = value; 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<uint32_t>(data);
if (skill) {
auto* const skillComponent = m_Parent->GetComponent<SkillComponent>();
if (skillComponent) skillComponent->CastSkill(skill.value());
}
break;
}
case eWaypointCommandType::EQUIP_INVENTORY: {
auto* const inventoryComponent = m_Parent->GetComponent<InventoryComponent>();
if (inventoryComponent) {
// items should always exist
auto* const item = inventoryComponent->GetInventory(eInventoryType::ITEMS)->FindItemBySlot(0);
if (item) inventoryComponent->EquipItem(item);
}
break;
}
case eWaypointCommandType::UNEQUIP_INVENTORY: {
auto* const inventoryComponent = m_Parent->GetComponent<InventoryComponent>();
if (inventoryComponent) {
// items should always exist
auto* const item = inventoryComponent->GetInventory(eInventoryType::ITEMS)->FindItemBySlot(0);
if (item) inventoryComponent->UnEquipItem(item);
}
break;
}
case eWaypointCommandType::DELAY: {
// Pause(GeneralUtils::TryParse<float>(data).value_or(0.0f));
break;
}
case eWaypointCommandType::EMOTE: {
// m_Delay = RenderComponent::GetAnimationTime(m_Parent, data);
// const auto emoteID = GeneralUtils::TryParse<uint32_t>(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<float>(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) { bool MovementAIComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {

View File

@@ -212,8 +212,16 @@ public:
bool IsPaused() const { return m_Paused; } bool IsPaused() const { return m_Paused; }
bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
bool HasPath() const { return m_Path != nullptr; }
private: private:
/**
* @brief
* Runs the commands on a waypoint if a path exists
*/
void RunWaypointCommands(uint32_t waypointNum);
/** /**
* Sets the current position of the entity * Sets the current position of the entity
* @param value the position to set * @param value the position to set

View File

@@ -962,5 +962,12 @@ namespace GameMessages {
LWOOBJID childID{}; LWOOBJID childID{};
}; };
struct ObjectLoaded : public GameMsg {
ObjectLoaded() : GameMsg(MessageType::Game::OBJECT_LOADED) {}
LWOOBJID objectID{};
LOT lot{};
};
}; };
#endif // GAMEMESSAGES_H #endif // GAMEMESSAGES_H

View File

@@ -1,3 +1,4 @@
set(DSCRIPTS_SOURCES_02_SERVER_DLU set(DSCRIPTS_SOURCES_02_SERVER_DLU
"DLUVanityTeleportingObject.cpp" "DLUVanityTeleportingObject.cpp"
"RegisterWithZoneControl.cpp"
PARENT_SCOPE) PARENT_SCOPE)

View File

@@ -0,0 +1,12 @@
#include "RegisterWithZoneControl.h"
#include "Entity.h"
#include "EntityManager.h"
#include "GameMessages.h"
void RegisterWithZoneControl::OnStartup(Entity* self) {
GameMessages::ObjectLoaded objLoaded;
objLoaded.objectID = self->GetObjectID();
objLoaded.lot = self->GetLOT();
objLoaded.Send(Game::entityManager->GetZoneControlEntity()->GetObjectID());
}

View File

@@ -0,0 +1,14 @@
// Darkflame Universe
// Copyright 2026
#ifndef REGISTERWITHZONECONTROL_H
#define REGISTERWITHZONECONTROL_H
#include "CppScripts.h"
class RegisterWithZoneControl : public CppScripts::Script {
public:
void OnStartup(Entity* self) override;
};
#endif //!REGISTERWITHZONECONTROL_H

View File

@@ -13,7 +13,6 @@ void ResetMissions(Entity& user) {
} }
void OldManNPC::OnUse(Entity* self, Entity* user) { void OldManNPC::OnUse(Entity* self, Entity* user) {
LOG("");
const auto* const missionComponent = user->GetComponent<MissionComponent>(); const auto* const missionComponent = user->GetComponent<MissionComponent>();
if (!missionComponent) return; if (!missionComponent) return;
@@ -24,7 +23,6 @@ void OldManNPC::OnUse(Entity* self, Entity* user) {
} }
const auto missionState = mission->GetMissionState(); const auto missionState = mission->GetMissionState();
LOG("mission state %i", missionState);
if (missionState == eMissionState::AVAILABLE || missionState == eMissionState::COMPLETE_AVAILABLE) { if (missionState == eMissionState::AVAILABLE || missionState == eMissionState::COMPLETE_AVAILABLE) {
ResetMissions(*user); ResetMissions(*user);
} }

View File

@@ -339,6 +339,8 @@
#include "ImaginationBackPack.h" #include "ImaginationBackPack.h"
#include "NsWinterRaceServer.h" #include "NsWinterRaceServer.h"
#include "RegisterWithZoneControl.h"
#include <map> #include <map>
#include <string> #include <string>
#include <functional> #include <functional>
@@ -663,6 +665,7 @@ namespace {
//WBL //WBL
{"scripts\\zone\\LUPs\\WBL_generic_zone.lua", []() {return new WblGenericZone();}}, {"scripts\\zone\\LUPs\\WBL_generic_zone.lua", []() {return new WblGenericZone();}},
{"scripts\\zone\\LUPs\\Moonbase Intro\\MOONBASE-INTRO_INTRO_CINEMATIC.lua", []() {return new WblGenericZone();}},
//Alpha //Alpha
{"scripts\\ai\\FV\\L_TRIGGER_GAS.lua", []() {return new TriggerGas();}}, {"scripts\\ai\\FV\\L_TRIGGER_GAS.lua", []() {return new TriggerGas();}},
@@ -708,7 +711,8 @@ namespace {
{"scripts\\ai\\RACING\\OBJECTS\\VEHICLE_DEATH_TRIGGER_WATER_SERVER.lua", []() {return new VehicleDeathTriggerWaterServer();}}, {"scripts\\ai\\RACING\\OBJECTS\\VEHICLE_DEATH_TRIGGER_WATER_SERVER.lua", []() {return new VehicleDeathTriggerWaterServer();}},
{"scripts\\equipmenttriggers\\L_TRIAL_FACTION_ARMOR_SERVER.lua", []() {return new TrialFactionArmorServer();}}, {"scripts\\equipmenttriggers\\L_TRIAL_FACTION_ARMOR_SERVER.lua", []() {return new TrialFactionArmorServer();}},
{"scripts\\equipmenttriggers\\ImaginationBackPack.lua", []() {return new ImaginationBackPack();}}, {"scripts\\equipmenttriggers\\ImaginationBackPack.lua", []() {return new ImaginationBackPack();}},
{"scripts\\ai\\MINIGAME\\SG_GF\\SERVER\\SG_CANNON_INSTANCE_ACTOR.lua", [](){return new RegisterWithZoneControl();}},
{"scripts\\ai\\MINIGAME\\SG_GF\\SERVER\\SG_CANNON_INSTANCE_EFFECT.lua", [](){return new RegisterWithZoneControl();}},
}; };
std::set<std::string> g_ExcludedScripts = { std::set<std::string> g_ExcludedScripts = {
@@ -732,6 +736,11 @@ namespace {
"scripts\\zone\\LUPs\\RobotCity Intro\\WBL_RCIntro_InfectedCitizen.lua", "scripts\\zone\\LUPs\\RobotCity Intro\\WBL_RCIntro_InfectedCitizen.lua",
"scripts\\ai\\MINIGAME\\SIEGE\\OBJECTS\\ATTACKER_BOUNCER_SERVER.lua", "scripts\\ai\\MINIGAME\\SIEGE\\OBJECTS\\ATTACKER_BOUNCER_SERVER.lua",
"scripts\\ai\\AG\\L_AG_ZONE_PLAYER.lua", "scripts\\ai\\AG\\L_AG_ZONE_PLAYER.lua",
"scripts\\ai\\GENERAL\\L_NPC_GENERIC_MOVEMENT.lua", // Really old alpha script
"scripts\\zone\\LUPs\\DeepFreeze Intro\\WBL_Enemy_Beaver.lua", // Really old alpha script
"scripts\\ai\\GENERAL\\L_NPC_GENERIC_WANDER_SMALL.lua", // Really old alpha script
"scripts\\ai\\NP\\L_NPC_NP_OLD_MAN_SHERLAND.lua", // This NPC doesn't even exist in modern crux, the only place this is used...
"scripts\\02_server\\Map\\General\\L_SIMPLE_MOVER_SWITCH.lua", // This platform does not exist even when moved manually on a client
}; };
}; };
@@ -745,7 +754,8 @@ CppScripts::Script* const CppScripts::GetScript(Entity* parent, const std::strin
Script* script = itrTernary != scriptLoader.cend() ? itrTernary->second() : &InvalidToReturn; Script* script = itrTernary != scriptLoader.cend() ? itrTernary->second() : &InvalidToReturn;
if (script == &InvalidToReturn && !scriptName.empty() && !g_ExcludedScripts.contains(scriptName)) { if (script == &InvalidToReturn && !scriptName.empty() && !g_ExcludedScripts.contains(scriptName)) {
LOG_DEBUG("LOT %i attempted to load CppScript for '%s', but returned InvalidScript.", parent->GetLOT(), scriptName.c_str()); const auto [x, y, z] = parent->GetPosition();
LOG_DEBUG("LOT %i at %f %f %f attempted to load CppScript for '%s', but returned InvalidScript.", parent->GetLOT(), x, y, z, scriptName.c_str());
} }
g_Scripts[scriptName] = script; g_Scripts[scriptName] = script;

View File

@@ -101,15 +101,23 @@ void Zone::LoadZoneIntoMemory() {
m_Paths.reserve(pathCount); m_Paths.reserve(pathCount);
for (uint32_t i = 0; i < pathCount; ++i) LoadPath(file); 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; if (path.pathType != PathType::Spawner) continue;
SpawnerInfo info = SpawnerInfo(); SpawnerInfo info{};
for (PathWaypoint waypoint : path.pathWaypoints) { for (size_t i = 0; i < path.pathWaypoints.size(); i++) {
const auto& waypoint = path.pathWaypoints[i];
SpawnerNode* node = new SpawnerNode(); SpawnerNode* node = new SpawnerNode();
node->position = waypoint.position; node->position = waypoint.position;
node->rotation = waypoint.rotation; node->rotation = waypoint.rotation;
node->nodeID = 0; 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) { for (const auto& data : waypoint.config | std::views::values) {
if (!data) continue; if (!data) continue;