mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
Volumes on the server touched far more than in the client: - An enemy's body in the physics world was a sphere the size of its aggro radius, so trigger and damage volumes caught enemies from far away (Cavalry Hill enemies taking damage on spawn). It is now the enemy's own radius and collision group from its physics component. - Trigger volumes ignored their collision group and caught everything. dpEntity now filters with the client's collision filter (PeCollisionFilter, 1.10.64 0x00fb6940, group table from 0x00fcf9a0): POI walls ignore enemies, threat clearing walls ignore players, and so on. The aggro sensor keeps seeing only players. - Rotated boxes were tested as the axis aligned box around them, which for a turned wall covers a big square (the AG survival boundary). Sphere and point tests now use the box's own axes. Proximity monitors take an optional collision group like live's SetProximityRadius; the AM shield generators use live's (10 finds enemies, 1 finds players). Fixes #1127 Refs #1971 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
971 lines
27 KiB
C++
971 lines
27 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 "EffectsMessages.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"
|
|
#include "dpCollisionFilter.h"
|
|
|
|
namespace {
|
|
// The collision groups of players and enemies in PhysicsComponent
|
|
constexpr uint32_t PLAYER_COLLISION_GROUP = 10;
|
|
constexpr uint32_t ENEMY_COLLISION_GROUP = 12;
|
|
// standard_enemy.hkx's radius
|
|
constexpr float DEFAULT_BODY_RADIUS = 1.7f;
|
|
}
|
|
|
|
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
|
|
*/
|
|
|
|
// The enemy's own body, which other volumes (triggers, damage volumes, proximity monitors) see: its group and
|
|
// radius from its physics component, like its character controller in the client. It used to be a sphere of
|
|
// the aggro radius, so volumes caught enemies from far away (e.g. the Cavalry Hill damage volumes, #1127).
|
|
uint32_t collisionGroup = ENEMY_COLLISION_GROUP;
|
|
float bodyRadius = DEFAULT_BODY_RADIUS;
|
|
|
|
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 = static_cast<uint32_t>(info->collisionGroup);
|
|
if (info->playerRadius > 0.0f) bodyRadius = info->playerRadius;
|
|
}
|
|
}
|
|
|
|
// The aggro sensor only needs to see players, as before; targets are picked by faction afterwards
|
|
m_dpEntity = new dpEntity(m_Parent->GetObjectID(), m_AggroRadius);
|
|
m_dpEntityEnemy = new dpEntity(m_Parent->GetObjectID(), bodyRadius, false);
|
|
|
|
m_dpEntity->SetCollisionGroup(dpCollisionFilter::GROUP_MASK | (1u << (PLAYER_COLLISION_GROUP - 1)));
|
|
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;
|
|
m_ForcedTetherTime -= deltaTime;
|
|
if (m_ForcedTetherTime >= 0) return;
|
|
GameMessages::StopFXEffect(m_Parent->GetObjectID(), true, "tether").Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
m_TetherEffectActive = false;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Flying from a knockback: pick a destination again once we land
|
|
if (m_MovementAI->IsKnockedBack()) 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);
|
|
// Don't clobber the pursuit speed set by TetherLogic while the tether is still active,
|
|
// otherwise the entity slows to wander speed and takes far longer than m_TetherTime to
|
|
// return to its spawn point.
|
|
if (m_MovementAI && !m_TetherEffectActive) m_MovementAI->SetMaxSpeed(1.0f);
|
|
}
|
|
|
|
if (!hasSkillToCast) return;
|
|
|
|
if (m_Target == LWOOBJID_EMPTY) {
|
|
SetAiState(AiState::idle);
|
|
if (m_MovementAI && !m_TetherEffectActive) 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();
|
|
|
|
// A target that died is no longer worth attacking, drop it and look for someone else.
|
|
if (target != nullptr && target->GetIsDead()) {
|
|
SetThreat(m_Target, 0.0f);
|
|
target = nullptr;
|
|
m_DirtyThreat = true;
|
|
}
|
|
|
|
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 || entity->GetIsDead()) {
|
|
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 || entity->GetIsDead()) {
|
|
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;
|
|
GameMessages::NotifyCombatAIStateChange stateMsg;
|
|
stateMsg.prevState = this->m_State;
|
|
stateMsg.newState = newState;
|
|
m_Parent->HandleMsg(stateMsg);
|
|
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>();
|
|
|
|
const bool applyTetherEffect = destroyableComponent != nullptr && destroyableComponent->HasFaction(4);
|
|
|
|
if (applyTetherEffect) {
|
|
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::PlayFXEffect(m_Parent->GetObjectID(), 6270, u"tether", "tether").Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
m_TetherEffectActive = true;
|
|
}
|
|
|
|
// Speed towards start position
|
|
if (m_MovementAI != nullptr) {
|
|
m_MovementAI->SetHaltDistance(0);
|
|
m_MovementAI->SetMaxSpeed(m_PursuitSpeed);
|
|
m_MovementAI->SetCurrentSpeed(m_PursuitSpeed);
|
|
m_MovementAI->SetDestination(m_StartPosition);
|
|
}
|
|
|
|
if (applyTetherEffect) {
|
|
// Use the actual navmesh path length (populated by SetDestination above) instead of the
|
|
// straight-line distance, otherwise the tether timer expires before the entity can walk
|
|
// around obstacles back to its spawn point.
|
|
const auto distance = m_MovementAI != nullptr
|
|
? m_MovementAI->GetRemainingPathDistance()
|
|
: NiPoint3::Distance(m_Parent->GetPosition(), m_StartPosition);
|
|
const auto speed = m_PursuitSpeed * MovementAIComponent::GetBaseSpeed(m_Parent->GetLOT());
|
|
m_TetherTime = speed > 0.0f ? distance / speed : 0.0f;
|
|
m_ForcedTetherTime = m_TetherTime;
|
|
}
|
|
}
|
|
|
|
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", "LWOOBJID") = 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;
|
|
|
|
cmptType.PushDebug("Tether Point").PushDebug(m_StartPosition);
|
|
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;
|
|
}
|