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
This commit is contained in:
David Markowitz
2026-06-18 23:27:49 -07:00
committed by GitHub
parent ce9d4e823c
commit 56504d9447
7 changed files with 74 additions and 10 deletions

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);
for (auto* behavior : this->m_behaviors) {
behavior->Calculate(context, bit_stream, branch);
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

@@ -350,7 +350,7 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
continue;
}
const auto result = skillComponent->CalculateBehavior(entry.skillId, entry.behavior->m_behaviorId, LWOOBJID_EMPTY);
const auto result = skillComponent->CalculateBehavior(entry.skillId, entry.behavior->m_behaviorId, GetTarget());
if (result.success) {
if (m_MovementAI != nullptr) {
@@ -759,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;
}

View File

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

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

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