#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 #include #include #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(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(u"aggroRadius"); m_AggroRadius = aggroRadius != 0 ? aggroRadius : m_AggroRadius; auto tetherRadius = m_Parent->GetVar(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(parent->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); 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(); auto componentID = componentRegistryTable->GetByIDAndType(parent->GetLOT(), eReplicaComponentType::CONTROLLABLE_PHYSICS); CDPhysicsComponentTable* physicsComponentTable = CDClientManager::GetTable(); 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 (auto en : m_dpEntity->GetNewObjects()) { m_Parent->OnCollisionPhantom(en->GetObjectID()); } //Process exit events for (auto en : m_dpEntity->GetRemovedObjects()) { m_Parent->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_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(); 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(); if (rebuild != nullptr) { const auto state = rebuild->GetState(); if (state != eQuickBuildState::COMPLETED) { return; } } auto* skillComponent = m_Parent->GetComponent(); 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(); 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 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 BaseCombatAIComponent::GetTargetWithinAggroRange() const { std::vector 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::Warn("Invalid entity for checking validity ({})!", target); return false; } auto* destroyable = entity->GetComponent(); if (destroyable == nullptr) { return false; } auto* referenceDestroyable = m_Parent->GetComponent(); if (referenceDestroyable == nullptr) { Log::Warn("Invalid reference destroyable component on ({})!", m_Parent->GetObjectID()); return false; } auto* quickbuild = entity->GetComponent(); 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(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::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); }