Compare commits

..

6 Commits

Author SHA1 Message Date
David Markowitz
308412f46e fix: enemies snapping to the incorrect position if they had a path and trying to use the path if they were aggro'd to an enemy (#2005)
* fix: enemies snapping to the incorrect position if they had a path

tested that ags enemies no longer snap backwards a large amount

* fix: move the home point so we can aggro correctly
2026-06-19 02:12:47 -07:00
David Markowitz
56504d9447 fix: add range checks to npc combat skill behavior (#2003)
* fix: add range checks to npc combat skill behavior

tested that all enemies now cast skills smartly based on range to targets, and do not cast skills if they are out of range.

fixes an issue where the spider queen could attack you outside the normal range

fixes an issue where entering happy flower caused you to need to restart the client

fixes #965

* feedback
2026-06-19 01:27:49 -05:00
David Markowitz
ce9d4e823c feat: enemies now use weights on their attacks (#2004)
* feat: enemies now use weights on their attacks

tested that 8 times out of 10, in close range, spiders did a web attack instead of a melee attack, vs the prior behavior of always following a pattern

fixes #2002

* feedback
2026-06-19 01:27:14 -05:00
David Markowitz
0f17e1de3b feat: enemy npc pathing (#2000)
* 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

* feedback
2026-06-17 23:07:36 -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
18 changed files with 327 additions and 90 deletions

View File

@@ -38,3 +38,11 @@ std::vector<CDObjectSkills> CDObjectSkillsTable::Query(std::function<bool(CDObje
return data;
}
std::vector<CDObjectSkills> CDObjectSkillsTable::Get(const LOT lot) const {
std::vector<CDObjectSkills> toReturn;
for (const auto& entry : GetEntries()) {
if (entry.objectTemplate == lot) toReturn.push_back(entry);
}
return toReturn;
}

View File

@@ -4,12 +4,13 @@
#include "CDTable.h"
#include <cstdint>
#include <vector>
struct CDObjectSkills {
uint32_t objectTemplate; //!< The LOT of the item
uint32_t skillID; //!< The Skill ID of the object
uint32_t castOnType; //!< ???
uint32_t AICombatWeight; //!< ???
int32_t AICombatWeight; //!< ???
};
class CDObjectSkillsTable : public CDTable<CDObjectSkillsTable, std::vector<CDObjectSkills>> {
@@ -17,5 +18,6 @@ public:
void LoadValuesFromDatabase();
// Queries the table with a custom "where" clause
std::vector<CDObjectSkills> Query(std::function<bool(CDObjectSkills)> predicate);
std::vector<CDObjectSkills> Get(const LOT lot) const;
};

View File

@@ -5,20 +5,40 @@
void NpcCombatSkillBehavior::Calculate(BehaviorContext* context, RakNet::BitStream& bit_stream, BehaviorBranchContext branch) {
context->skillTime = this->m_npcSkillTime;
const auto* const targetEntity = Game::entityManager->GetEntity(branch.target);
const auto* const sourceEntity = Game::entityManager->GetEntity(context->caster);
bool cast = true;
// Check that the target is within the cast range
if (targetEntity && sourceEntity && this->m_maxRange != 0.0f) {
const auto targetPos = targetEntity->GetPosition();
const auto sourcePos = sourceEntity->GetPosition();
const auto distance = NiPoint3::DistanceSquared(targetPos, sourcePos);
cast = distance >= this->m_minRange && distance <= this->m_maxRange;
}
if (cast) {
for (auto* behavior : this->m_behaviors) {
behavior->Calculate(context, bit_stream, branch);
}
} else {
// We failed to find a valid target, do not continue the behavior
context->foundTarget = false;
}
}
void NpcCombatSkillBehavior::Load() {
this->m_npcSkillTime = GetFloat("npc skill time");
this->m_minRange = GetFloat("min range") * 0.9f; // Make the min and max 10% smaller to account for server/client position disagreements
this->m_minRange *= this->m_minRange;
this->m_maxRange = GetFloat("max range") * 0.9f; // Make the min and max 10% smaller to account for server/client position disagreements
this->m_maxRange *= this->m_maxRange;
const auto parameters = GetParameterNames();
for (const auto& parameter : parameters) {
if (parameter.first.rfind("behavior", 0) == 0) {
auto* action = GetAction(parameter.second);
for (const auto& [parameter, value] : parameters) {
if (parameter.rfind("behavior", 0) == 0) {
auto* action = GetAction(value);
this->m_behaviors.push_back(action);
}

View File

@@ -9,6 +9,7 @@ public:
float m_npcSkillTime;
float m_maxRange{};
float m_minRange{};
/*
* Inherited

View File

@@ -13,6 +13,8 @@
#include "CDClientDatabase.h"
#include "CDClientManager.h"
#include "CDObjectSkillsTable.h"
#include "CDSkillBehaviorTable.h"
#include "DestroyableComponent.h"
#include <algorithm>
@@ -43,7 +45,7 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t compo
//Grab the aggro information from BaseCombatAI:
auto componentQuery = CDClientDatabase::CreatePreppedStmt(
"SELECT aggroRadius, tetherSpeed, pursuitSpeed, softTetherRadius, hardTetherRadius FROM BaseCombatAIComponent WHERE id = ?;");
"SELECT aggroRadius, tetherSpeed, pursuitSpeed, softTetherRadius, hardTetherRadius, minRoundLength, maxRoundLength, combatRoundLength FROM BaseCombatAIComponent WHERE id = ?;");
componentQuery.bind(1, static_cast<int>(componentID));
auto componentResult = componentQuery.execQuery();
@@ -63,44 +65,37 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t compo
if (!componentResult.fieldIsNull("hardTetherRadius"))
m_HardTetherRadius = componentResult.getFloatField("hardTetherRadius");
}
componentResult.finalize();
m_MinRoundLength = componentResult.getFloatField("minRoundLength");
m_MaxRoundLength = componentResult.getFloatField("maxRoundLength");
m_CombatRoundLength = componentResult.getFloatField("combatRoundLength");
}
// Get aggro and tether radius from settings and use this if it is present. Only overwrite the
// radii if it is greater than the one in the database.
if (m_Parent) {
auto aggroRadius = m_Parent->GetVar<float>(u"aggroRadius");
m_AggroRadius = aggroRadius != 0 ? aggroRadius : m_AggroRadius;
auto tetherRadius = m_Parent->GetVar<float>(u"tetherRadius");
m_HardTetherRadius = tetherRadius != 0 ? tetherRadius : m_HardTetherRadius;
}
m_AggroRadius = m_Parent->HasVar(u"aggroRadius") ? m_Parent->GetVar<float>(u"aggroRadius") : m_AggroRadius;
m_HardTetherRadius = m_Parent->HasVar(u"tetherRadius") ? m_Parent->GetVar<float>(u"tetherRadius") : m_HardTetherRadius;
/*
* Find skills
*/
auto skillQuery = CDClientDatabase::CreatePreppedStmt(
"SELECT skillID, cooldown, behaviorID FROM SkillBehavior WHERE skillID IN (SELECT skillID FROM ObjectSkills WHERE objectTemplate = ?);");
skillQuery.bind(1, static_cast<int>(parent->GetLOT()));
for (const auto objectSkill : CDClientManager::GetTable<CDObjectSkillsTable>()->Get(parent->GetLOT())) {
const auto skillBehavior = CDClientManager::GetTable<CDSkillBehaviorTable>()->GetSkillByID(objectSkill.skillID);
if (skillBehavior.skillID == objectSkill.skillID) {
const auto skillId = skillBehavior.skillID;
auto result = skillQuery.execQuery();
const auto abilityCooldown = skillBehavior.cooldown;
while (!result.eof()) {
const auto skillId = static_cast<uint32_t>(result.getIntField("skillID"));
const auto behaviorId = skillBehavior.behaviorID;
const auto abilityCooldown = static_cast<float>(result.getFloatField("cooldown"));
const auto behaviorId = static_cast<uint32_t>(result.getIntField("behaviorID"));
const auto combatWeight = objectSkill.AICombatWeight;
auto* behavior = Behavior::CreateBehavior(behaviorId);
std::stringstream behaviorQuery;
AiSkillEntry entry = { skillId, 0, abilityCooldown, behavior };
AiSkillEntry entry = { .skillId = skillId, .cooldown = 0.0f, .abilityCooldown = abilityCooldown, .behavior = behavior, .combatWeight = combatWeight };
m_SkillEntries.push_back(entry);
result.nextRow();
}
}
Stun(1.0f);
@@ -210,8 +205,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;
}
@@ -246,10 +243,12 @@ void BaseCombatAIComponent::Update(const float deltaTime) {
void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
bool hasSkillToCast = false;
int32_t maxSkillWeights = 0;
for (auto& entry : m_SkillEntries) {
if (entry.cooldown > 0.0f) {
entry.cooldown -= deltaTime;
} else {
maxSkillWeights += entry.combatWeight;
hasSkillToCast = true;
}
}
@@ -317,12 +316,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;
}
@@ -333,14 +334,23 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
LookAt(target->GetPosition());
}
for (auto i = 0; i < m_SkillEntries.size(); ++i) {
auto entry = m_SkillEntries.at(i);
// Roll to find which skill we'll try to cast
auto randomizedWeight = GeneralUtils::GenerateRandomNumber<int32_t>(0, maxSkillWeights);
if (entry.cooldown > 0) {
for (auto& entry : m_SkillEntries) {
// Skill isn't cooled off yet
if (entry.cooldown > 0.0f) {
continue;
}
const auto result = skillComponent->CalculateBehavior(entry.skillId, entry.behavior->m_behaviorId, LWOOBJID_EMPTY);
randomizedWeight -= entry.combatWeight;
// if the weight is still greater than 0 continue to the next rolled skill
if (randomizedWeight > 0) {
continue;
}
const auto result = skillComponent->CalculateBehavior(entry.skillId, entry.behavior->m_behaviorId, GetTarget());
if (result.success) {
if (m_MovementAI != nullptr) {
@@ -355,8 +365,6 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
entry.cooldown = entry.abilityCooldown + m_SkillTime;
m_SkillEntries[i] = entry;
break;
}
}
@@ -618,6 +626,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();
@@ -746,8 +759,8 @@ void BaseCombatAIComponent::SetTetherSpeed(float value) {
m_TetherSpeed = value;
}
void BaseCombatAIComponent::Stun(const float time) {
if (m_StunImmune || m_StunTime > time) {
void BaseCombatAIComponent::Stun(const float time, const bool force) {
if (!force && (m_StunImmune || m_StunTime > time)) {
return;
}
@@ -905,8 +918,16 @@ bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReport
}
auto& ignoredThreats = cmptType.PushDebug("Temp Ignored Threats");
for (const auto& [id, threat] : m_ThreatEntries) {
for (const auto& [id, threat] : m_RemovedThreatList) {
ignoredThreats.PushDebug<AMFDoubleValue>(std::to_string(id) + " - Time") = threat;
}
auto& skillInfo = cmptType.PushDebug("Skill Info");
for (const auto& skill : m_SkillEntries) {
auto& skillDebug = skillInfo.PushDebug("Skill ID " + std::to_string(skill.skillId));
skillDebug.PushDebug<AMFDoubleValue>("Cooldown") = skill.cooldown;
skillDebug.PushDebug<AMFDoubleValue>("Ability Cooldown") = skill.abilityCooldown;
skillDebug.PushDebug<AMFIntValue>("AI Combat Weight") = skill.combatWeight;
}
return true;
}

View File

@@ -33,13 +33,15 @@ enum class AiState : uint32_t {
*/
struct AiSkillEntry
{
uint32_t skillId;
uint32_t skillId{};
float cooldown;
float cooldown{};
float abilityCooldown;
float abilityCooldown{};
Behavior* behavior;
Behavior* behavior{};
int32_t combatWeight{};
};
/**
@@ -181,8 +183,9 @@ public:
/**
* Stuns the entity for a certain amount of time, will not work if the entity is stun immune
* @param time the time to stun the entity, if stunnable
* @param force whether or not to force the stun and ignore checks
*/
void Stun(float time);
void Stun(float time, const bool force = false);
/**
* Gets the radius that will cause this entity to get aggro'd, causing a target chase
@@ -236,6 +239,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
@@ -394,9 +399,17 @@ private:
*/
bool m_DirtyStateOrTarget = false;
// Min amount of time to remain as in combat after casting a skill
float m_MinRoundLength = 0.0f;
// max amount of time to remain as in combat after casting a skill
float m_MaxRoundLength = 0.0f;
// The amount of time the entity will be forced to tether for
float m_ForcedTetherTime = 0.0f;
float m_CombatRoundLength = 0.0f;
// The amount of time a removed threat will be ignored for.
std::map<LWOOBJID, float> m_RemovedThreatList;

View File

@@ -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 {
/**
@@ -31,8 +37,6 @@ MovementAIComponent::MovementAIComponent(Entity* parent, const int32_t component
m_Info = info;
m_AtFinalWaypoint = true;
m_BaseCombatAI = nullptr;
m_BaseCombatAI = m_Parent->GetComponent<BaseCombatAIComponent>();
//Try and fix the insane values:
@@ -60,7 +64,7 @@ MovementAIComponent::MovementAIComponent(Entity* parent, const int32_t component
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) {
@@ -125,7 +129,11 @@ void MovementAIComponent::Update(const float deltaTime) {
m_TimeTravelled += deltaTime;
SetPosition(ApproximateLocation());
const auto approxPos = ApproximateLocation();
SetPosition(approxPos);
// Set the AIs new home based on where our current waypoint is IF we're idle, that way we can return to this
// when resuming the pathing after losing aggro while moving the aggro hitbox with us
if (m_BaseCombatAI && m_BaseCombatAI->GetState() == AiState::idle) m_BaseCombatAI->SetStartingPosition(approxPos);
if (m_TimeTravelled < m_TimeToTravel) return;
m_TimeTravelled = 0.0f;
@@ -160,8 +168,11 @@ void MovementAIComponent::Update(const float deltaTime) {
SetRotation(QuatUtils::LookAt(source, m_NextWaypoint));
}
} else {
// Only try to renew or continue the path if we're in the idle or spawn state and we actually have a combatAI component
if (!m_BaseCombatAI || (m_BaseCombatAI && m_BaseCombatAI->GetState() == AiState::idle)) {
// 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,22 +183,21 @@ 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.
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();
}
}
}
Game::entityManager->SerializeEntity(m_Parent);
}
@@ -212,8 +222,7 @@ NiPoint3 MovementAIComponent::GetCurrentWaypoint() const {
NiPoint3 MovementAIComponent::ApproximateLocation() const {
auto source = m_SourcePosition;
if (AtFinalWaypoint()) return source;
if (AtFinalWaypoint()) return m_Parent->GetPosition();
auto destination = m_NextWaypoint;
@@ -423,7 +432,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<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) {
@@ -453,6 +524,7 @@ bool MovementAIComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInf
movementInfo.PushDebug<AMFBoolValue>("Lock Rotation") = m_LockRotation;
movementInfo.PushDebug<AMFBoolValue>("Paused") = m_Paused;
movementInfo.PushDebug<AMFDoubleValue>("Pulling To Point") = m_PullingToPoint;
movementInfo.PushDebug<AMFBoolValue>("At Final Waypoint") = m_AtFinalWaypoint;
auto& pullPointInfo = movementInfo.PushDebug("Pull Point");
pullPointInfo.PushDebug<AMFDoubleValue>("X") = m_PullPoint.x;

View File

@@ -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

View File

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

View File

@@ -1,3 +1,4 @@
set(DSCRIPTS_SOURCES_02_SERVER_DLU
"DLUVanityTeleportingObject.cpp"
"RegisterWithZoneControl.cpp"
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

@@ -16,6 +16,7 @@
#include "eReplicaComponentType.h"
#include "RenderComponent.h"
#include "PlayerManager.h"
#include "eStateChangeType.h"
#include <vector>
@@ -48,10 +49,30 @@ void BossSpiderQueenEnemyServer::OnStartup(Entity* self) {
combat->SetStunImmune(true);
m_CurrentBossStage = 1;
ToggleAttacking(*self, false);
self->SetProximityRadius(65.0f, "AggroRadius");
// Obtain faction and collision group to save for subsequent resets
}
void BossSpiderQueenEnemyServer::OnProximityUpdate(Entity* self, Entity* entering, std::string name, std::string status) {
if (name != "AggroRadius" || !entering || !entering->IsPlayer()) return;
auto playerCount = self->GetVar<int32_t>(u"player_count");
if (status == "ENTER") {
if (playerCount == 0) {
ToggleAttacking(*self, true);
}
playerCount++;
} else if (status == "LEAVE") {
playerCount--;
if (playerCount == 0) {
ToggleAttacking(*self, false);
}
}
self->SetVar<int32_t>(u"player_count", playerCount);
}
void BossSpiderQueenEnemyServer::OnDie(Entity* self, Entity* killer) {
if (Game::zoneManager->GetZoneID().GetMapID() == instanceZoneID && killer) {
for (const auto& player : PlayerManager::GetAllPlayers()) {
@@ -71,6 +92,7 @@ void BossSpiderQueenEnemyServer::OnDie(Entity* self, Entity* killer) {
self->SetPosition({ 10000, 0, 10000 });
Game::entityManager->SerializeEntity(self);
ToggleAttacking(*self, false);
controller->OnFireEventServerSide(self, "ClearProperty");
}
@@ -634,3 +656,19 @@ float BossSpiderQueenEnemyServer::PlayAnimAndReturnTime(Entity* self, const std:
return animTimer;
}
void BossSpiderQueenEnemyServer::ToggleAttacking(Entity& self, bool on) {
const auto stoppedFlag = self.GetVarAs<bool>(u"stoppedFlag");
if (!on) {
if (stoppedFlag) return;
self.SetVar(u"stoppedFlag", true);
combat->Stun(100000.0f, true); // forcibly stun so we stop attacking people trying to put on armor
} else {
if (!stoppedFlag) return;
self.SetVar(u"stoppedFlag", false);
combat->Stun(0.0f, true); // forcibly turn off the stun we put on above
}
}

View File

@@ -46,7 +46,10 @@ public:
void OnTimerDone(Entity* self, std::string timerName) override;
void OnProximityUpdate(Entity* self, Entity* entering, std::string name, std::string status);
private:
void ToggleAttacking(Entity& self, bool on);
//Regular variables:
DestroyableComponent* destroyable = nullptr;
ControllablePhysicsComponent* controllable = nullptr;

View File

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

View File

@@ -339,6 +339,8 @@
#include "ImaginationBackPack.h"
#include "NsWinterRaceServer.h"
#include "RegisterWithZoneControl.h"
#include <map>
#include <string>
#include <functional>
@@ -663,6 +665,7 @@ namespace {
//WBL
{"scripts\\zone\\LUPs\\WBL_generic_zone.lua", []() {return new WblGenericZone();}},
{"scripts\\zone\\LUPs\\Moonbase Intro\\MOONBASE-INTRO_INTRO_CINEMATIC.lua", []() {return new WblGenericZone();}},
//Alpha
{"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\\equipmenttriggers\\L_TRIAL_FACTION_ARMOR_SERVER.lua", []() {return new TrialFactionArmorServer();}},
{"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 = {
@@ -732,6 +736,11 @@ namespace {
"scripts\\zone\\LUPs\\RobotCity Intro\\WBL_RCIntro_InfectedCitizen.lua",
"scripts\\ai\\MINIGAME\\SIEGE\\OBJECTS\\ATTACKER_BOUNCER_SERVER.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;
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;

View File

@@ -1019,6 +1019,7 @@ void HandlePacket(Packet* packet) {
if (user) {
Character* c = user->GetLastUsedChar();
if (c != nullptr) {
if (Game::entityManager->GetEntity(c->GetObjectID())) return;
std::u16string username = GeneralUtils::ASCIIToUTF16(c->GetName());
Game::server->GetReplicaManager()->AddParticipant(packet->systemAddress);

View File

@@ -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 (size_t 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;