mirror of
https://github.com/DarkflameUniverse/DarkflameServer
synced 2024-08-30 18:43:58 +00:00
06e7d57e0d
* chore: Remove dpEntity pointers from collision checking * Update fn documentation in ProximityMonitorComponent.h * use more idiomatic method to calculate vector index * feedback * missed a ranges::find replacement * adjust for feedback. last changes tonight. * okay, also remove unneeded include. then sleep. * for real tho * update to use unordered_set instead of set
808 lines
19 KiB
C++
808 lines
19 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 "DestroyableComponent.h"
|
|
|
|
#include <algorithm>
|
|
#include <sstream>
|
|
#include <vector>
|
|
|
|
#include "SkillComponent.h"
|
|
#include "QuickBuildComponent.h"
|
|
#include "DestroyableComponent.h"
|
|
#include "Metrics.hpp"
|
|
#include "CDComponentsRegistryTable.h"
|
|
#include "CDPhysicsComponentTable.h"
|
|
#include "dNavMesh.h"
|
|
|
|
BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const uint32_t id): Component(parent) {
|
|
m_Target = LWOOBJID_EMPTY;
|
|
SetAiState(AiState::spawn);
|
|
m_Timer = 1.0f;
|
|
m_StartPosition = parent->GetPosition();
|
|
m_MovementAI = nullptr;
|
|
m_Disabled = false;
|
|
m_SkillEntries = {};
|
|
m_SoftTimer = 5.0f;
|
|
|
|
//Grab the aggro information from BaseCombatAI:
|
|
auto componentQuery = CDClientDatabase::CreatePreppedStmt(
|
|
"SELECT aggroRadius, tetherSpeed, pursuitSpeed, softTetherRadius, hardTetherRadius FROM BaseCombatAIComponent WHERE id = ?;");
|
|
componentQuery.bind(1, static_cast<int>(id));
|
|
|
|
auto componentResult = componentQuery.execQuery();
|
|
|
|
if (!componentResult.eof()) {
|
|
if (!componentResult.fieldIsNull(0))
|
|
m_AggroRadius = componentResult.getFloatField(0);
|
|
|
|
if (!componentResult.fieldIsNull(1))
|
|
m_TetherSpeed = componentResult.getFloatField(1);
|
|
|
|
if (!componentResult.fieldIsNull(2))
|
|
m_PursuitSpeed = componentResult.getFloatField(2);
|
|
|
|
if (!componentResult.fieldIsNull(3))
|
|
m_SoftTetherRadius = componentResult.getFloatField(3);
|
|
|
|
if (!componentResult.fieldIsNull(4))
|
|
m_HardTetherRadius = componentResult.getFloatField(4);
|
|
}
|
|
|
|
componentResult.finalize();
|
|
|
|
// 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;
|
|
}
|
|
|
|
/*
|
|
* 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()));
|
|
|
|
auto result = skillQuery.execQuery();
|
|
|
|
while (!result.eof()) {
|
|
const auto skillId = static_cast<uint32_t>(result.getIntField(0));
|
|
|
|
const auto abilityCooldown = static_cast<float>(result.getFloatField(1));
|
|
|
|
const auto behaviorId = static_cast<uint32_t>(result.getIntField(2));
|
|
|
|
auto* behavior = Behavior::CreateBehavior(behaviorId);
|
|
|
|
std::stringstream behaviorQuery;
|
|
|
|
AiSkillEntry entry = { skillId, 0, abilityCooldown, behavior };
|
|
|
|
m_SkillEntries.push_back(entry);
|
|
|
|
result.nextRow();
|
|
}
|
|
|
|
Stun(1.0f);
|
|
|
|
/*
|
|
* Add physics
|
|
*/
|
|
|
|
int32_t collisionGroup = (COLLISION_GROUP_DYNAMIC | COLLISION_GROUP_ENEMY);
|
|
|
|
CDComponentsRegistryTable* componentRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
|
|
auto componentID = componentRegistryTable->GetByIDAndType(parent->GetLOT(), eReplicaComponentType::CONTROLLABLE_PHYSICS);
|
|
|
|
CDPhysicsComponentTable* physicsComponentTable = CDClientManager::GetTable<CDPhysicsComponentTable>();
|
|
|
|
if (physicsComponentTable != nullptr) {
|
|
auto* info = physicsComponentTable->GetByID(componentID);
|
|
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;
|
|
}
|
|
}
|
|
|
|
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) {
|
|
m_MovementAI->Stop();
|
|
|
|
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;
|
|
for (auto& entry : m_SkillEntries) {
|
|
if (entry.cooldown > 0.0f) {
|
|
entry.cooldown -= deltaTime;
|
|
} else {
|
|
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) {
|
|
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);
|
|
}
|
|
|
|
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 (!hasSkillToCast) return;
|
|
|
|
if (m_Target == LWOOBJID_EMPTY) {
|
|
SetAiState(AiState::idle);
|
|
|
|
return;
|
|
}
|
|
|
|
auto* target = GetTargetEntity();
|
|
|
|
if (target != nullptr) {
|
|
LookAt(target->GetPosition());
|
|
}
|
|
|
|
for (auto i = 0; i < m_SkillEntries.size(); ++i) {
|
|
auto entry = m_SkillEntries.at(i);
|
|
|
|
if (entry.cooldown > 0) {
|
|
continue;
|
|
}
|
|
|
|
const auto result = skillComponent->CalculateBehavior(entry.skillId, entry.behavior->m_behaviorId, LWOOBJID_EMPTY);
|
|
|
|
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;
|
|
|
|
m_SkillEntries[i] = entry;
|
|
|
|
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);
|
|
|
|
const auto distance = Vector3::DistanceSquared(m_Parent->GetPosition(), other->GetPosition());
|
|
|
|
if (distance > m_AggroRadius * m_AggroRadius) 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_DirtyThreat = true;
|
|
}
|
|
|
|
void BaseCombatAIComponent::Wander() {
|
|
if (!m_MovementAI->AtFinalWaypoint()) {
|
|
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) {
|
|
if (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(NiQuaternion::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);
|
|
}
|