#include "BaseCombatAIComponent.h" #include #include #include #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 "SkillComponent.h" #include "QuickBuildComponent.h" #include "DestroyableComponent.h" #include "Metrics.hpp" #include "CDComponentsRegistryTable.h" #include "CDPhysicsComponentTable.h" BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const uint32_t componentId) : Component(parent) { m_Target = LWOOBJID_EMPTY; m_ComponentId = componentId; SetAiState(AiState::Spawn); m_Timer = 1.0f; m_StartPosition = parent->GetPosition(); m_Disabled = false; m_SkillEntries = {}; m_MovementAI = nullptr; m_SoftTimer = 5.0f; m_dpEntity = nullptr; m_dpEntityEnemy = nullptr; } void BaseCombatAIComponent::LoadTemplateData() { //Grab the aggro information from BaseCombatAI: auto componentQuery = CDClientDatabase::CreatePreppedStmt( "SELECT aggroRadius, tetherSpeed, pursuitSpeed, softTetherRadius, hardTetherRadius FROM BaseCombatAIComponent WHERE id = ?;"); componentQuery.bind(1, m_ComponentId); 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(); /* * Find skills */ auto skillQuery = CDClientDatabase::CreatePreppedStmt( "SELECT skillID, cooldown, behaviorID FROM SkillBehavior WHERE skillID IN (SELECT skillID FROM ObjectSkills WHERE objectTemplate = ?);"); skillQuery.bind(1, m_ParentEntity->GetLOT()); auto result = skillQuery.execQuery(); while (!result.eof()) { const auto skillId = static_cast(result.getIntField(0)); const auto abilityCooldown = static_cast(result.getFloatField(1)); const auto behaviorId = static_cast(result.getIntField(2)); auto* behavior = Behavior::CreateBehavior(behaviorId); m_SkillEntries.push_back(AiSkillEntry(skillId, 0, abilityCooldown, behavior)); result.nextRow(); } } void BaseCombatAIComponent::LoadConfigData() { // 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_ParentEntity) { auto aggroRadius = m_ParentEntity->GetVar(u"aggroRadius"); m_AggroRadius = aggroRadius != 0 ? aggroRadius : m_AggroRadius; auto tetherRadius = m_ParentEntity->GetVar(u"tetherRadius"); m_HardTetherRadius = tetherRadius != 0 ? tetherRadius : m_HardTetherRadius; } } void BaseCombatAIComponent::Startup() { Stun(1.0f); // Add physics int32_t collisionGroup = (COLLISION_GROUP_DYNAMIC | COLLISION_GROUP_ENEMY); auto* componentRegistryTable = CDClientManager::Instance().GetTable(); if (!componentRegistryTable) return; auto componentID = componentRegistryTable->GetByIDAndType(m_ParentEntity->GetLOT(), eReplicaComponentType::CONTROLLABLE_PHYSICS); auto* physicsComponentTable = CDClientManager::Instance().GetTable(); if (!physicsComponentTable) return; auto* info = physicsComponentTable->GetByID(componentID); if (info) collisionGroup = info->bStatic ? COLLISION_GROUP_NEUTRAL : info->collisionGroup; // Why are these new'd here and then deleted by the dpworld?? //Create a phantom physics volume so we can detect when we're aggro'd. m_dpEntity = new dpEntity(m_ParentEntity->GetObjectID(), m_AggroRadius); m_dpEntityEnemy = new dpEntity(m_ParentEntity->GetObjectID(), m_AggroRadius, false); m_dpEntity->SetCollisionGroup(collisionGroup); m_dpEntityEnemy->SetCollisionGroup(collisionGroup); m_dpEntity->SetPosition(m_ParentEntity->GetPosition()); m_dpEntityEnemy->SetPosition(m_ParentEntity->GetPosition()); dpWorld::Instance().AddEntity(m_dpEntity); dpWorld::Instance().AddEntity(m_dpEntityEnemy); } BaseCombatAIComponent::~BaseCombatAIComponent() { if (m_dpEntity) dpWorld::Instance().RemoveEntity(m_dpEntity); if (m_dpEntityEnemy) dpWorld::Instance().RemoveEntity(m_dpEntityEnemy); m_MovementAI = nullptr; m_dpEntity = nullptr; m_dpEntityEnemy = nullptr; } void BaseCombatAIComponent::Update(const float deltaTime) { //First, we need to process physics: if (!m_dpEntity) return; m_dpEntity->SetPosition(m_ParentEntity->GetPosition()); //make sure our position is synced with our dpEntity m_dpEntityEnemy->SetPosition(m_ParentEntity->GetPosition()); //Process enter events for (auto en : m_dpEntity->GetNewObjects()) { m_ParentEntity->OnCollisionPhantom(en->GetObjectID()); } //Process exit events for (auto en : m_dpEntity->GetRemovedObjects()) { m_ParentEntity->OnCollisionLeavePhantom(en->GetObjectID()); } // Check if we should stop the tether effect if (m_TetherEffectActive) { m_TetherTime -= deltaTime; const auto& info = m_MovementAI->GetInfo(); if (m_Target != LWOOBJID_EMPTY || (NiPoint3::DistanceSquared( m_StartPosition, m_ParentEntity->GetPosition()) < 20 * 20 && m_TetherTime <= 0) ) { GameMessages::SendStopFXEffect(m_ParentEntity, true, "tether"); m_TetherEffectActive = false; } } if (m_SoftTimer <= 0.0f) { EntityManager::Instance()->SerializeEntity(m_ParentEntity); m_SoftTimer = 5.0f; } else { m_SoftTimer -= deltaTime; } if (m_Disabled || m_ParentEntity->IsDead()) return; bool stunnedThisFrame = m_Stunned; CalculateCombat(deltaTime); // Putting this here for now if (m_StartPosition == NiPoint3::ZERO) { m_StartPosition = m_ParentEntity->GetPosition(); } if (!m_MovementAI) { m_MovementAI = m_ParentEntity->GetComponent(); if (!m_MovementAI) 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: Game::logger->Log("BaseCombatAIComponent", "Entity %i is in an invalid state %i", m_ParentEntity->GetLOT(), m_State); 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_ParentEntity->GetComponent(); if (rebuild != nullptr) { const auto state = rebuild->GetState(); if (state != eRebuildState::COMPLETED) { return; } } auto* skillComponent = m_ParentEntity->GetComponent(); if (!skillComponent) 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_ParentEntity->GetComponent(); 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) { EntityManager::Instance()->SerializeEntity(m_ParentEntity); } GameMessages::SendPlayFXEffect(m_ParentEntity->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->SetSpeed(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 = EntityManager::Instance()->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 deadThreats{}; for (const auto& threatTarget : m_ThreatEntries) { auto* entity = EntityManager::Instance()->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 BaseCombatAIComponent::GetTargetWithinAggroRange() const { std::vector targets; for (auto id : m_ParentEntity->GetTargetsInPhantom()) { auto* other = EntityManager::Instance()->GetEntity(id); const auto distance = Vector3::DistanceSquared(m_ParentEntity->GetPosition(), other->GetPosition()); if (distance > m_AggroRadius * m_AggroRadius) continue; targets.push_back(id); } return targets; } void BaseCombatAIComponent::Serialize(RakNet::BitStream* outBitStream, bool bIsInitialUpdate, unsigned int& flags) { 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; EntityManager::Instance()->SerializeEntity(m_ParentEntity); } bool BaseCombatAIComponent::IsEnemy(LWOOBJID target) const { auto* entity = EntityManager::Instance()->GetEntity(target); if (!entity) { Game::logger->Log("BaseCombatAIComponent", "Invalid entity for checking validity (%llu)!", target); return false; } auto* destroyable = entity->GetComponent(); if (!destroyable) return false; auto* referenceDestroyable = m_ParentEntity->GetComponent(); if (!referenceDestroyable) { Game::logger->Log("BaseCombatAIComponent", "Invalid reference destroyable component on (%llu)!", m_ParentEntity->GetObjectID()); return false; } auto* quickbuild = entity->GetComponent(); if (quickbuild) { const auto state = quickbuild->GetState(); if (state != eRebuildState::COMPLETED) { return false; } } auto enemyList = referenceDestroyable->GetEnemyFactionsIDs(); auto candidateList = destroyable->GetFactionIDs(); for (const 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; EntityManager::Instance()->SerializeEntity(m_ParentEntity); } Entity* BaseCombatAIComponent::GetTargetEntity() const { return EntityManager::Instance()->GetEntity(m_Target); } void BaseCombatAIComponent::Taunt(LWOOBJID offender, float threat) { // Can't taunt self if (offender == m_ParentEntity->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(info.wanderDelayMax); m_Timer = (div == 0 ? 0 : GeneralUtils::GenerateRandomNumber(0, div)) + info.wanderDelayMin; //set a random timer to stay put. const float radius = info.wanderRadius * sqrt(static_cast(GeneralUtils::GenerateRandomNumber(0, 1))); //our wander radius + a bit of random range const float theta = ((static_cast(GeneralUtils::GenerateRandomNumber(0, 1)) * 2 * PI)); const NiPoint3 delta = { radius * cos(theta), 0, radius * sin(theta) }; auto destination = m_StartPosition + delta; if (dpWorld::Instance().IsLoaded()) { destination.y = dpWorld::Instance().GetNavMesh()->GetHeightAtPoint(destination); } if (Vector3::DistanceSquared(destination, m_MovementAI->GetCurrentPosition()) < 2 * 2) { m_MovementAI->Stop(); return; } m_MovementAI->SetSpeed(m_TetherSpeed); m_MovementAI->SetDestination(destination); m_Timer += (m_MovementAI->GetCurrentPosition().x - destination.x) / m_TetherSpeed; } void BaseCombatAIComponent::OnAggro() { if (m_Target == LWOOBJID_EMPTY) return; auto* target = GetTargetEntity(); if (!target) return; m_MovementAI->SetHaltDistance(m_AttackRadius); NiPoint3 targetPos = target->GetPosition(); NiPoint3 currentPos = m_MovementAI->GetCurrentPosition(); // 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->SetSpeed(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->SetSpeed(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->SetSpeed(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->SetSpeed(m_PursuitSpeed); m_MovementAI->SetDestination(targetPos); } m_Timer += 0.5f; } void BaseCombatAIComponent::Stun(const float time) { if (m_StunImmune || m_StunTime > time) return; m_StunTime = time; m_Stunned = true; } void BaseCombatAIComponent::LookAt(const NiPoint3& point) { if (m_Stunned) return; m_ParentEntity->SetRotation(NiQuaternion::LookAt(m_ParentEntity->GetPosition(), point)); } void BaseCombatAIComponent::Sleep() { m_dpEntity->SetSleeping(true); m_dpEntityEnemy->SetSleeping(true); } void BaseCombatAIComponent::Wake() { m_dpEntity->SetSleeping(false); m_dpEntityEnemy->SetSleeping(false); }