mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-06-20 13:44:21 +00:00
* 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
934 lines
25 KiB
C++
934 lines
25 KiB
C++
#include "BaseCombatAIComponent.h"
|
|
#include "BitStream.h"
|
|
|
|
#include "Entity.h"
|
|
#include "EntityManager.h"
|
|
#include "ControllablePhysicsComponent.h"
|
|
#include "MovementAIComponent.h"
|
|
#include "dpWorld.h"
|
|
|
|
#include "GameMessages.h"
|
|
#include "dServer.h"
|
|
#include "Game.h"
|
|
|
|
#include "CDClientDatabase.h"
|
|
#include "CDClientManager.h"
|
|
#include "CDObjectSkillsTable.h"
|
|
#include "CDSkillBehaviorTable.h"
|
|
#include "DestroyableComponent.h"
|
|
|
|
#include <algorithm>
|
|
#include <ranges>
|
|
#include <sstream>
|
|
#include <vector>
|
|
|
|
#include "SkillComponent.h"
|
|
#include "QuickBuildComponent.h"
|
|
#include "DestroyableComponent.h"
|
|
#include "CDComponentsRegistryTable.h"
|
|
#include "CDPhysicsComponentTable.h"
|
|
#include "dNavMesh.h"
|
|
#include "Amf3.h"
|
|
|
|
BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
|
|
RegisterMsg(&BaseCombatAIComponent::MsgGetObjectReportInfo);
|
|
m_Target = LWOOBJID_EMPTY;
|
|
m_DirtyStateOrTarget = true;
|
|
m_State = AiState::spawn;
|
|
m_Timer = 1.0f;
|
|
m_StartPosition = parent->GetPosition();
|
|
m_MovementAI = nullptr;
|
|
m_Disabled = false;
|
|
m_SkillEntries = {};
|
|
m_SoftTimer = 5.0f;
|
|
m_ForcedTetherTime = 0.0f;
|
|
|
|
//Grab the aggro information from BaseCombatAI:
|
|
auto componentQuery = CDClientDatabase::CreatePreppedStmt(
|
|
"SELECT aggroRadius, tetherSpeed, pursuitSpeed, softTetherRadius, hardTetherRadius, minRoundLength, maxRoundLength, combatRoundLength FROM BaseCombatAIComponent WHERE id = ?;");
|
|
componentQuery.bind(1, static_cast<int>(componentID));
|
|
|
|
auto componentResult = componentQuery.execQuery();
|
|
|
|
if (!componentResult.eof()) {
|
|
if (!componentResult.fieldIsNull("aggroRadius"))
|
|
m_AggroRadius = componentResult.getFloatField("aggroRadius");
|
|
|
|
if (!componentResult.fieldIsNull("tetherSpeed"))
|
|
m_TetherSpeed = componentResult.getFloatField("tetherSpeed");
|
|
|
|
if (!componentResult.fieldIsNull("pursuitSpeed"))
|
|
m_PursuitSpeed = componentResult.getFloatField("pursuitSpeed");
|
|
|
|
if (!componentResult.fieldIsNull("softTetherRadius"))
|
|
m_SoftTetherRadius = componentResult.getFloatField("softTetherRadius");
|
|
|
|
if (!componentResult.fieldIsNull("hardTetherRadius"))
|
|
m_HardTetherRadius = componentResult.getFloatField("hardTetherRadius");
|
|
|
|
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.
|
|
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
|
|
*/
|
|
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;
|
|
|
|
const auto abilityCooldown = skillBehavior.cooldown;
|
|
|
|
const auto behaviorId = skillBehavior.behaviorID;
|
|
|
|
const auto combatWeight = objectSkill.AICombatWeight;
|
|
|
|
auto* behavior = Behavior::CreateBehavior(behaviorId);
|
|
|
|
AiSkillEntry entry = { .skillId = skillId, .cooldown = 0.0f, .abilityCooldown = abilityCooldown, .behavior = behavior, .combatWeight = combatWeight };
|
|
|
|
m_SkillEntries.push_back(entry);
|
|
}
|
|
}
|
|
|
|
Stun(1.0f);
|
|
|
|
/*
|
|
* Add physics
|
|
*/
|
|
|
|
int32_t collisionGroup = (COLLISION_GROUP_DYNAMIC | COLLISION_GROUP_ENEMY);
|
|
|
|
CDComponentsRegistryTable* componentRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
|
|
const auto controllablePhysicsID = componentRegistryTable->GetByIDAndType(parent->GetLOT(), eReplicaComponentType::CONTROLLABLE_PHYSICS);
|
|
|
|
CDPhysicsComponentTable* physicsComponentTable = CDClientManager::GetTable<CDPhysicsComponentTable>();
|
|
|
|
if (physicsComponentTable != nullptr) {
|
|
auto* info = physicsComponentTable->GetByID(controllablePhysicsID);
|
|
if (info != nullptr) {
|
|
collisionGroup = info->bStatic ? COLLISION_GROUP_NEUTRAL : info->collisionGroup;
|
|
}
|
|
}
|
|
|
|
//Create a phantom physics volume so we can detect when we're aggro'd.
|
|
m_dpEntity = new dpEntity(m_Parent->GetObjectID(), m_AggroRadius);
|
|
m_dpEntityEnemy = new dpEntity(m_Parent->GetObjectID(), m_AggroRadius, false);
|
|
|
|
m_dpEntity->SetCollisionGroup(collisionGroup);
|
|
m_dpEntityEnemy->SetCollisionGroup(collisionGroup);
|
|
|
|
m_dpEntity->SetPosition(m_Parent->GetPosition());
|
|
m_dpEntityEnemy->SetPosition(m_Parent->GetPosition());
|
|
|
|
dpWorld::AddEntity(m_dpEntity);
|
|
dpWorld::AddEntity(m_dpEntityEnemy);
|
|
|
|
}
|
|
|
|
BaseCombatAIComponent::~BaseCombatAIComponent() {
|
|
if (m_dpEntity)
|
|
dpWorld::RemoveEntity(m_dpEntity);
|
|
|
|
if (m_dpEntityEnemy)
|
|
dpWorld::RemoveEntity(m_dpEntityEnemy);
|
|
}
|
|
|
|
void BaseCombatAIComponent::Update(const float deltaTime) {
|
|
//First, we need to process physics:
|
|
if (!m_dpEntity) return;
|
|
|
|
m_dpEntity->SetPosition(m_Parent->GetPosition()); //make sure our position is synced with our dpEntity
|
|
m_dpEntityEnemy->SetPosition(m_Parent->GetPosition());
|
|
|
|
//Process enter events
|
|
for (const auto id : m_dpEntity->GetNewObjects()) {
|
|
m_Parent->OnCollisionPhantom(id);
|
|
}
|
|
|
|
//Process exit events
|
|
for (const auto id : m_dpEntity->GetRemovedObjects()) {
|
|
m_Parent->OnCollisionLeavePhantom(id);
|
|
}
|
|
|
|
// Check if we should stop the tether effect
|
|
if (m_TetherEffectActive) {
|
|
m_TetherTime -= deltaTime;
|
|
if (m_Target != LWOOBJID_EMPTY || (NiPoint3::DistanceSquared(
|
|
m_StartPosition,
|
|
m_Parent->GetPosition()) < 20 * 20 && m_TetherTime <= 0)
|
|
) {
|
|
GameMessages::SendStopFXEffect(m_Parent, true, "tether");
|
|
m_TetherEffectActive = false;
|
|
}
|
|
m_ForcedTetherTime -= deltaTime;
|
|
if (m_ForcedTetherTime >= 0) return;
|
|
}
|
|
|
|
for (auto entry = m_RemovedThreatList.begin(); entry != m_RemovedThreatList.end();) {
|
|
entry->second -= deltaTime;
|
|
if (entry->second <= 0.0f) {
|
|
entry = m_RemovedThreatList.erase(entry);
|
|
} else {
|
|
++entry;
|
|
}
|
|
}
|
|
|
|
if (m_SoftTimer <= 0.0f) {
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
m_SoftTimer = 5.0f;
|
|
} else {
|
|
m_SoftTimer -= deltaTime;
|
|
}
|
|
|
|
if (m_Disabled || m_Parent->GetIsDead())
|
|
return;
|
|
bool stunnedThisFrame = m_Stunned;
|
|
CalculateCombat(deltaTime); // Putting this here for now
|
|
|
|
if (m_StartPosition == NiPoint3Constant::ZERO) {
|
|
m_StartPosition = m_Parent->GetPosition();
|
|
}
|
|
|
|
m_MovementAI = m_Parent->GetComponent<MovementAIComponent>();
|
|
|
|
if (m_MovementAI == nullptr) {
|
|
return;
|
|
}
|
|
|
|
if (stunnedThisFrame) {
|
|
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;
|
|
}
|
|
|
|
if (m_Timer > 0.0f) {
|
|
m_Timer -= deltaTime;
|
|
return;
|
|
}
|
|
|
|
switch (m_State) {
|
|
case AiState::spawn:
|
|
Stun(2.0f);
|
|
SetAiState(AiState::idle);
|
|
break;
|
|
|
|
case AiState::idle:
|
|
Wander();
|
|
break;
|
|
|
|
case AiState::aggro:
|
|
OnAggro();
|
|
break;
|
|
|
|
case AiState::tether:
|
|
OnTether();
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
bool hadRemainingDowntime = m_SkillTime > 0.0f;
|
|
if (m_SkillTime > 0.0f) m_SkillTime -= deltaTime;
|
|
|
|
auto* rebuild = m_Parent->GetComponent<QuickBuildComponent>();
|
|
|
|
if (rebuild != nullptr) {
|
|
const auto state = rebuild->GetState();
|
|
|
|
if (state != eQuickBuildState::COMPLETED) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
auto* skillComponent = m_Parent->GetComponent<SkillComponent>();
|
|
|
|
if (skillComponent == nullptr) {
|
|
return;
|
|
}
|
|
|
|
skillComponent->CalculateUpdate(deltaTime);
|
|
|
|
if (m_Disabled) return;
|
|
|
|
if (m_Stunned) {
|
|
m_StunTime -= deltaTime;
|
|
|
|
if (m_StunTime > 0.0f) {
|
|
return;
|
|
}
|
|
m_StunTime = 0.0f;
|
|
m_Stunned = false;
|
|
}
|
|
|
|
if (m_Stunned || hadRemainingDowntime) return;
|
|
|
|
auto newTarget = FindTarget();
|
|
|
|
// Tether - reset enemy
|
|
if (m_Target != LWOOBJID_EMPTY && newTarget == LWOOBJID_EMPTY) {
|
|
m_OutOfCombat = true;
|
|
m_OutOfCombatTime = 1.0f;
|
|
} else if (newTarget != LWOOBJID_EMPTY) {
|
|
m_OutOfCombat = false;
|
|
m_OutOfCombatTime = 0.0f;
|
|
}
|
|
|
|
if (!m_TetherEffectActive && m_OutOfCombat && (m_OutOfCombatTime -= deltaTime) <= 0) {
|
|
TetherLogic();
|
|
|
|
m_OutOfCombat = false;
|
|
m_OutOfCombatTime = 0.0f;
|
|
}
|
|
|
|
SetTarget(newTarget);
|
|
|
|
if (m_Target != LWOOBJID_EMPTY) {
|
|
if (m_State == AiState::idle) {
|
|
m_Timer = 0;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
auto* target = GetTargetEntity();
|
|
|
|
if (target != nullptr) {
|
|
LookAt(target->GetPosition());
|
|
}
|
|
|
|
// Roll to find which skill we'll try to cast
|
|
auto randomizedWeight = GeneralUtils::GenerateRandomNumber<int32_t>(0, maxSkillWeights);
|
|
|
|
for (auto& entry : m_SkillEntries) {
|
|
// Skill isn't cooled off yet
|
|
if (entry.cooldown > 0.0f) {
|
|
continue;
|
|
}
|
|
|
|
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) {
|
|
m_MovementAI->Stop();
|
|
}
|
|
|
|
SetAiState(AiState::aggro);
|
|
|
|
m_Timer = 0;
|
|
|
|
m_SkillTime = result.skillTime;
|
|
|
|
entry.cooldown = entry.abilityCooldown + m_SkillTime;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
LWOOBJID BaseCombatAIComponent::FindTarget() {
|
|
NiPoint3 reference = m_StartPosition;
|
|
|
|
if (m_MovementAI) reference = m_MovementAI->ApproximateLocation();
|
|
|
|
auto* target = GetTargetEntity();
|
|
|
|
if (target != nullptr && !m_DirtyThreat) {
|
|
const auto targetPosition = target->GetPosition();
|
|
|
|
if (Vector3::DistanceSquared(targetPosition, m_StartPosition) < m_HardTetherRadius * m_HardTetherRadius) {
|
|
return m_Target;
|
|
}
|
|
|
|
return LWOOBJID_EMPTY;
|
|
}
|
|
|
|
auto possibleTargets = GetTargetWithinAggroRange();
|
|
|
|
if (possibleTargets.empty() && m_ThreatEntries.empty()) {
|
|
m_DirtyThreat = false;
|
|
|
|
return LWOOBJID_EMPTY;
|
|
}
|
|
|
|
Entity* optimalTarget = nullptr;
|
|
float biggestThreat = 0;
|
|
|
|
for (const auto& entry : possibleTargets) {
|
|
auto* entity = Game::entityManager->GetEntity(entry);
|
|
|
|
if (entity == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
const auto targetPosition = entity->GetPosition();
|
|
|
|
const auto threat = GetThreat(entry);
|
|
|
|
const auto maxDistanceSquared = m_HardTetherRadius * m_HardTetherRadius;
|
|
|
|
if (Vector3::DistanceSquared(targetPosition, m_StartPosition) > maxDistanceSquared) {
|
|
if (threat > 0) {
|
|
SetThreat(entry, 0);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (threat > biggestThreat) {
|
|
biggestThreat = threat;
|
|
optimalTarget = entity;
|
|
|
|
continue;
|
|
}
|
|
|
|
const auto proximityThreat = -(Vector3::DistanceSquared(targetPosition, reference) - maxDistanceSquared) / 100; // Proximity threat takes last priority
|
|
|
|
if (proximityThreat > biggestThreat) {
|
|
biggestThreat = proximityThreat;
|
|
optimalTarget = entity;
|
|
}
|
|
}
|
|
|
|
if (!m_DirtyThreat) {
|
|
if (optimalTarget == nullptr) {
|
|
return LWOOBJID_EMPTY;
|
|
} else {
|
|
return optimalTarget->GetObjectID();
|
|
}
|
|
}
|
|
|
|
std::vector<LWOOBJID> deadThreats{};
|
|
|
|
for (const auto& threatTarget : m_ThreatEntries) {
|
|
auto* entity = Game::entityManager->GetEntity(threatTarget.first);
|
|
|
|
if (entity == nullptr) {
|
|
deadThreats.push_back(threatTarget.first);
|
|
|
|
continue;
|
|
}
|
|
|
|
const auto targetPosition = entity->GetPosition();
|
|
|
|
if (Vector3::DistanceSquared(targetPosition, m_StartPosition) > m_HardTetherRadius * m_HardTetherRadius) {
|
|
deadThreats.push_back(threatTarget.first);
|
|
|
|
continue;
|
|
}
|
|
|
|
if (threatTarget.second > biggestThreat) {
|
|
optimalTarget = entity;
|
|
biggestThreat = threatTarget.second;
|
|
}
|
|
}
|
|
|
|
for (const auto& deadThreat : deadThreats) {
|
|
SetThreat(deadThreat, 0);
|
|
}
|
|
|
|
m_DirtyThreat = false;
|
|
|
|
if (optimalTarget == nullptr) {
|
|
return LWOOBJID_EMPTY;
|
|
} else {
|
|
return optimalTarget->GetObjectID();
|
|
}
|
|
}
|
|
|
|
std::vector<LWOOBJID> BaseCombatAIComponent::GetTargetWithinAggroRange() const {
|
|
std::vector<LWOOBJID> targets;
|
|
|
|
for (auto id : m_Parent->GetTargetsInPhantom()) {
|
|
auto* other = Game::entityManager->GetEntity(id);
|
|
if (!other) continue;
|
|
|
|
const auto distance = Vector3::DistanceSquared(m_Parent->GetPosition(), other->GetPosition());
|
|
|
|
if (distance > m_AggroRadius * m_AggroRadius || m_RemovedThreatList.contains(id)) continue;
|
|
|
|
targets.push_back(id);
|
|
}
|
|
|
|
return targets;
|
|
}
|
|
|
|
bool BaseCombatAIComponent::IsMech() {
|
|
switch (m_Parent->GetLOT()) {
|
|
case 6253:
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
void BaseCombatAIComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {
|
|
outBitStream.Write(m_DirtyStateOrTarget || bIsInitialUpdate);
|
|
if (m_DirtyStateOrTarget || bIsInitialUpdate) {
|
|
outBitStream.Write(m_State);
|
|
outBitStream.Write(m_Target);
|
|
m_DirtyStateOrTarget = false;
|
|
}
|
|
}
|
|
|
|
void BaseCombatAIComponent::SetAiState(AiState newState) {
|
|
if (newState == this->m_State) return;
|
|
this->m_State = newState;
|
|
m_DirtyStateOrTarget = true;
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
|
|
bool BaseCombatAIComponent::IsEnemy(LWOOBJID target) const {
|
|
auto* entity = Game::entityManager->GetEntity(target);
|
|
|
|
if (entity == nullptr) {
|
|
LOG("Invalid entity for checking validity (%llu)!", target);
|
|
|
|
return false;
|
|
}
|
|
|
|
auto* destroyable = entity->GetComponent<DestroyableComponent>();
|
|
|
|
if (destroyable == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
auto* referenceDestroyable = m_Parent->GetComponent<DestroyableComponent>();
|
|
|
|
if (referenceDestroyable == nullptr) {
|
|
LOG("Invalid reference destroyable component on (%llu)!", m_Parent->GetObjectID());
|
|
|
|
return false;
|
|
}
|
|
|
|
auto* quickbuild = entity->GetComponent<QuickBuildComponent>();
|
|
|
|
if (quickbuild != nullptr) {
|
|
const auto state = quickbuild->GetState();
|
|
|
|
if (state != eQuickBuildState::COMPLETED) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
auto enemyList = referenceDestroyable->GetEnemyFactionsIDs();
|
|
|
|
auto candidateList = destroyable->GetFactionIDs();
|
|
|
|
for (auto value : candidateList) {
|
|
if (std::find(enemyList.begin(), enemyList.end(), value) != enemyList.end()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void BaseCombatAIComponent::SetTarget(const LWOOBJID target) {
|
|
if (this->m_Target == target) return;
|
|
m_Target = target;
|
|
m_DirtyStateOrTarget = true;
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
|
|
Entity* BaseCombatAIComponent::GetTargetEntity() const {
|
|
return Game::entityManager->GetEntity(m_Target);
|
|
}
|
|
|
|
void BaseCombatAIComponent::Taunt(LWOOBJID offender, float threat) {
|
|
// Can't taunt self
|
|
if (offender == m_Parent->GetObjectID())
|
|
return;
|
|
|
|
m_ThreatEntries[offender] += threat;
|
|
m_DirtyThreat = true;
|
|
}
|
|
|
|
float BaseCombatAIComponent::GetThreat(LWOOBJID offender) {
|
|
const auto pair = m_ThreatEntries.find(offender);
|
|
|
|
if (pair == m_ThreatEntries.end()) return 0;
|
|
|
|
return pair->second;
|
|
}
|
|
|
|
void BaseCombatAIComponent::SetThreat(LWOOBJID offender, float threat) {
|
|
if (threat == 0) {
|
|
m_ThreatEntries.erase(offender);
|
|
} else {
|
|
m_ThreatEntries[offender] = threat;
|
|
}
|
|
|
|
m_DirtyThreat = true;
|
|
}
|
|
|
|
const NiPoint3& BaseCombatAIComponent::GetStartPosition() const {
|
|
return m_StartPosition;
|
|
}
|
|
|
|
void BaseCombatAIComponent::ClearThreat() {
|
|
m_ThreatEntries.clear();
|
|
m_Target = LWOOBJID_EMPTY;
|
|
|
|
m_DirtyThreat = true;
|
|
}
|
|
|
|
void BaseCombatAIComponent::Wander() {
|
|
if (!m_MovementAI->AtFinalWaypoint()) {
|
|
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();
|
|
|
|
const auto div = static_cast<int>(info.wanderDelayMax);
|
|
m_Timer = (div == 0 ? 0 : GeneralUtils::GenerateRandomNumber<int>(0, div)) + info.wanderDelayMin; //set a random timer to stay put.
|
|
|
|
const float radius = info.wanderRadius * sqrt(static_cast<double>(GeneralUtils::GenerateRandomNumber<float>(0, 1))); //our wander radius + a bit of random range
|
|
const float theta = ((static_cast<double>(GeneralUtils::GenerateRandomNumber<float>(0, 1)) * 2 * PI));
|
|
|
|
const NiPoint3 delta =
|
|
{
|
|
radius * cos(theta),
|
|
0,
|
|
radius * sin(theta)
|
|
};
|
|
|
|
auto destination = m_StartPosition + delta;
|
|
|
|
if (dpWorld::IsLoaded()) {
|
|
destination.y = dpWorld::GetNavMesh()->GetHeightAtPoint(destination);
|
|
}
|
|
|
|
if (Vector3::DistanceSquared(destination, m_MovementAI->GetParent()->GetPosition()) < 2 * 2) {
|
|
m_MovementAI->Stop();
|
|
|
|
return;
|
|
}
|
|
|
|
m_MovementAI->SetMaxSpeed(m_TetherSpeed);
|
|
|
|
m_MovementAI->SetDestination(destination);
|
|
|
|
m_Timer += (m_MovementAI->GetParent()->GetPosition().x - destination.x) / m_TetherSpeed;
|
|
}
|
|
|
|
void BaseCombatAIComponent::OnAggro() {
|
|
if (m_Target == LWOOBJID_EMPTY) return;
|
|
|
|
auto* target = GetTargetEntity();
|
|
|
|
if (target == nullptr) {
|
|
return;
|
|
}
|
|
|
|
m_MovementAI->SetHaltDistance(m_AttackRadius);
|
|
|
|
NiPoint3 targetPos = target->GetPosition();
|
|
NiPoint3 currentPos = m_MovementAI->GetParent()->GetPosition();
|
|
|
|
// If the player's position is within range, attack
|
|
if (Vector3::DistanceSquared(currentPos, targetPos) <= m_AttackRadius * m_AttackRadius) {
|
|
m_MovementAI->Stop();
|
|
} else if (Vector3::DistanceSquared(m_StartPosition, targetPos) > m_HardTetherRadius * m_HardTetherRadius) //Return to spawn if we're too far
|
|
{
|
|
m_MovementAI->SetMaxSpeed(m_PursuitSpeed);
|
|
|
|
m_MovementAI->SetDestination(m_StartPosition);
|
|
} else //Chase the player's new position
|
|
{
|
|
if (IsMech() && Vector3::DistanceSquared(targetPos, currentPos) > m_AttackRadius * m_AttackRadius * 3 * 3) return;
|
|
|
|
m_MovementAI->SetMaxSpeed(m_PursuitSpeed);
|
|
|
|
m_MovementAI->SetDestination(targetPos);
|
|
|
|
SetAiState(AiState::tether);
|
|
}
|
|
|
|
m_Timer += 0.5f;
|
|
}
|
|
|
|
void BaseCombatAIComponent::OnTether() {
|
|
auto* target = GetTargetEntity();
|
|
|
|
if (target == nullptr) {
|
|
return;
|
|
}
|
|
|
|
m_MovementAI->SetHaltDistance(m_AttackRadius);
|
|
|
|
NiPoint3 targetPos = target->GetPosition();
|
|
NiPoint3 currentPos = m_MovementAI->ApproximateLocation();
|
|
|
|
if (Vector3::DistanceSquared(currentPos, targetPos) <= m_AttackRadius * m_AttackRadius) {
|
|
m_MovementAI->Stop();
|
|
} else if (Vector3::DistanceSquared(m_StartPosition, targetPos) > m_HardTetherRadius * m_HardTetherRadius) //Return to spawn if we're too far
|
|
{
|
|
m_MovementAI->SetMaxSpeed(m_PursuitSpeed);
|
|
|
|
m_MovementAI->SetDestination(m_StartPosition);
|
|
|
|
SetAiState(AiState::aggro);
|
|
} else {
|
|
if (IsMech() && Vector3::DistanceSquared(targetPos, currentPos) > m_AttackRadius * m_AttackRadius * 3 * 3) return;
|
|
|
|
m_MovementAI->SetMaxSpeed(m_PursuitSpeed);
|
|
|
|
m_MovementAI->SetDestination(targetPos);
|
|
}
|
|
|
|
m_Timer += 0.5f;
|
|
}
|
|
|
|
bool BaseCombatAIComponent::GetStunned() const {
|
|
return m_Stunned;
|
|
}
|
|
|
|
void BaseCombatAIComponent::SetStunned(const bool value) {
|
|
m_Stunned = value;
|
|
}
|
|
|
|
bool BaseCombatAIComponent::GetStunImmune() const {
|
|
return m_StunImmune;
|
|
}
|
|
|
|
void BaseCombatAIComponent::SetStunImmune(bool value) {
|
|
m_StunImmune = value;
|
|
}
|
|
|
|
float BaseCombatAIComponent::GetTetherSpeed() const {
|
|
return m_TetherSpeed;
|
|
}
|
|
|
|
void BaseCombatAIComponent::SetTetherSpeed(float value) {
|
|
m_TetherSpeed = value;
|
|
}
|
|
|
|
void BaseCombatAIComponent::Stun(const float time, const bool force) {
|
|
if (!force && (m_StunImmune || m_StunTime > time)) {
|
|
return;
|
|
}
|
|
|
|
m_StunTime = time;
|
|
|
|
m_Stunned = true;
|
|
}
|
|
|
|
float BaseCombatAIComponent::GetAggroRadius() const {
|
|
return m_AggroRadius;
|
|
}
|
|
|
|
void BaseCombatAIComponent::SetAggroRadius(const float value) {
|
|
m_AggroRadius = value;
|
|
}
|
|
|
|
void BaseCombatAIComponent::LookAt(const NiPoint3& point) {
|
|
if (m_Stunned) {
|
|
return;
|
|
}
|
|
|
|
m_Parent->SetRotation(QuatUtils::LookAt(m_Parent->GetPosition(), point));
|
|
}
|
|
|
|
void BaseCombatAIComponent::SetDisabled(bool value) {
|
|
m_Disabled = value;
|
|
}
|
|
|
|
bool BaseCombatAIComponent::GetDistabled() const {
|
|
return m_Disabled;
|
|
}
|
|
|
|
void BaseCombatAIComponent::Sleep() {
|
|
m_dpEntity->SetSleeping(true);
|
|
m_dpEntityEnemy->SetSleeping(true);
|
|
}
|
|
|
|
void BaseCombatAIComponent::Wake() {
|
|
m_dpEntity->SetSleeping(false);
|
|
m_dpEntityEnemy->SetSleeping(false);
|
|
}
|
|
|
|
void BaseCombatAIComponent::TetherLogic() {
|
|
auto* destroyableComponent = m_Parent->GetComponent<DestroyableComponent>();
|
|
|
|
if (destroyableComponent != nullptr && destroyableComponent->HasFaction(4)) {
|
|
auto serilizationRequired = false;
|
|
|
|
if (destroyableComponent->GetHealth() != destroyableComponent->GetMaxHealth()) {
|
|
destroyableComponent->SetHealth(destroyableComponent->GetMaxHealth());
|
|
|
|
serilizationRequired = true;
|
|
}
|
|
|
|
if (destroyableComponent->GetArmor() != destroyableComponent->GetMaxArmor()) {
|
|
destroyableComponent->SetArmor(destroyableComponent->GetMaxArmor());
|
|
|
|
serilizationRequired = true;
|
|
}
|
|
|
|
if (serilizationRequired) {
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
|
|
GameMessages::SendPlayFXEffect(m_Parent->GetObjectID(), 6270, u"tether", "tether");
|
|
|
|
m_TetherEffectActive = true;
|
|
|
|
m_TetherTime = 3.0f;
|
|
}
|
|
|
|
// Speed towards start position
|
|
if (m_MovementAI != nullptr) {
|
|
m_MovementAI->SetHaltDistance(0);
|
|
m_MovementAI->SetMaxSpeed(m_PursuitSpeed);
|
|
m_MovementAI->SetDestination(m_StartPosition);
|
|
}
|
|
}
|
|
|
|
void BaseCombatAIComponent::ForceTether() {
|
|
SetTarget(LWOOBJID_EMPTY);
|
|
m_ThreatEntries.clear();
|
|
TetherLogic();
|
|
m_ForcedTetherTime = m_TetherTime;
|
|
|
|
SetAiState(AiState::aggro);
|
|
}
|
|
|
|
void BaseCombatAIComponent::IgnoreThreat(const LWOOBJID threat, const float value) {
|
|
m_RemovedThreatList[threat] = value;
|
|
SetThreat(threat, 0.0f);
|
|
m_Target = LWOOBJID_EMPTY;
|
|
}
|
|
|
|
bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
|
|
using enum AiState;
|
|
auto& cmptType = reportInfo.info->PushDebug("Base Combat AI");
|
|
cmptType.PushDebug<AMFIntValue>("Component ID") = GetComponentID();
|
|
auto& targetInfo = cmptType.PushDebug("Current Target Info");
|
|
targetInfo.PushDebug<AMFStringValue>("Current Target ID") = std::to_string(m_Target);
|
|
// if (m_Target != LWOOBJID_EMPTY) {
|
|
// LWOGameMessages::ObjGetName nameMsg(m_CurrentTarget);
|
|
// SEND_GAMEOBJ_MSG(nameMsg);
|
|
// if (!nameMsg.msg.name.empty()) targetInfo.PushDebug("Name") = nameMsg.msg.name;
|
|
// }
|
|
|
|
auto& roundInfo = cmptType.PushDebug("Round Info");
|
|
// roundInfo.PushDebug<AMFDoubleValue>("Combat Round Time") = m_CombatRoundLength;
|
|
// roundInfo.PushDebug<AMFDoubleValue>("Minimum Time") = m_MinRoundLength;
|
|
// roundInfo.PushDebug<AMFDoubleValue>("Maximum Time") = m_MaxRoundLength;
|
|
// roundInfo.PushDebug<AMFDoubleValue>("Selected Time") = m_SelectedTime;
|
|
// roundInfo.PushDebug<AMFDoubleValue>("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;
|
|
}
|
|
cmptType.PushDebug<AMFStringValue>("Current Combat State") = curState;
|
|
|
|
//switch (m_CombatBehaviorType) {
|
|
// case 0: curState = "Passive"; break;
|
|
// case 1: curState = "Aggressive"; break;
|
|
// case 2: curState = "Passive (Turret)"; break;
|
|
// case 3: curState = "Aggressive (Turret)"; break;
|
|
// default: curState = "Unknown or Undefined"; break;
|
|
//}
|
|
//cmptType.PushDebug("Current Combat Behavior State") = curState;
|
|
|
|
//switch (m_CombatRole) {
|
|
// case 0: curState = "Melee"; break;
|
|
// case 1: curState = "Ranged"; break;
|
|
// case 2: curState = "Support"; break;
|
|
// default: curState = "Unknown or Undefined"; break;
|
|
//}
|
|
//cmptType.PushDebug("Current Combat Role") = curState;
|
|
|
|
auto& tetherPoint = cmptType.PushDebug("Tether Point");
|
|
tetherPoint.PushDebug<AMFDoubleValue>("X") = m_StartPosition.x;
|
|
tetherPoint.PushDebug<AMFDoubleValue>("Y") = m_StartPosition.y;
|
|
tetherPoint.PushDebug<AMFDoubleValue>("Z") = m_StartPosition.z;
|
|
cmptType.PushDebug<AMFDoubleValue>("Hard Tether Radius") = m_HardTetherRadius;
|
|
cmptType.PushDebug<AMFDoubleValue>("Soft Tether Radius") = m_SoftTetherRadius;
|
|
cmptType.PushDebug<AMFDoubleValue>("Aggro Radius") = m_AggroRadius;
|
|
cmptType.PushDebug<AMFDoubleValue>("Tether Speed") = m_TetherSpeed;
|
|
cmptType.PushDebug<AMFDoubleValue>("Aggro Speed") = m_TetherSpeed;
|
|
// cmptType.PushDebug<AMFDoubleValue>("Specified Min Range") = m_SpecificMinRange;
|
|
// cmptType.PushDebug<AMFDoubleValue>("Specified Max Range") = m_SpecificMaxRange;
|
|
auto& threats = cmptType.PushDebug("Target Threats");
|
|
for (const auto& [id, threat] : m_ThreatEntries) {
|
|
threats.PushDebug<AMFDoubleValue>(std::to_string(id)) = threat;
|
|
}
|
|
|
|
auto& ignoredThreats = cmptType.PushDebug("Temp Ignored Threats");
|
|
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;
|
|
}
|