mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
MovementAIComponent::SetDestination cuts an enemy's path (chasing, tethering, wandering) where it first walks into a wall its collision group can't cross, so enemies no longer walk through the Sentinel camp walls, the Crux Prime and property navmesh carvers, or the enemy blocking volumes. With nothing left to walk the enemy stays put. Patrols along a level path, and movers without combat AI, are left alone. Adds a scenario test: an enemy chasing across an enemy only volume and across a carver stops in front of it; the clear threat wall is a trigger, not a wall. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
697 lines
22 KiB
C++
697 lines
22 KiB
C++
#include "MovementAIComponent.h"
|
|
|
|
#include <utility>
|
|
#include <cmath>
|
|
|
|
#include "ControllablePhysicsComponent.h"
|
|
#include "BaseCombatAIComponent.h"
|
|
#include "dpCommon.h"
|
|
#include "dpWorld.h"
|
|
#include "EntityManager.h"
|
|
#include "SimplePhysicsComponent.h"
|
|
#include "CDClientManager.h"
|
|
#include "Game.h"
|
|
#include "dZoneManager.h"
|
|
|
|
#include "CDComponentsRegistryTable.h"
|
|
#include "QuickBuildComponent.h"
|
|
#include "CDPhysicsComponentTable.h"
|
|
#include "Amf3.h"
|
|
|
|
#include "dNavMesh.h"
|
|
#include "eWaypointCommandType.h"
|
|
#include "StringifiedEnum.h"
|
|
#include "SkillComponent.h"
|
|
#include "GeneralUtils.h"
|
|
#include "RenderComponent.h"
|
|
#include "InventoryComponent.h"
|
|
|
|
namespace {
|
|
/**
|
|
* Cache of all lots and their respective speeds
|
|
*/
|
|
std::map<LOT, float> m_PhysicsSpeedCache;
|
|
|
|
// Half the height of a character: how far a short knockback may drop onto the ground (the client uses its
|
|
// character's height / 2)
|
|
constexpr float KNOCKBACK_MAX_DROP = 2.0f;
|
|
|
|
// The client's gravity when WorldConfig has none (0x015fd9a8)
|
|
constexpr float DEFAULT_GRAVITY = 9.8f;
|
|
}
|
|
|
|
MovementAIComponent::MovementAIComponent(Entity* parent, const int32_t componentID, MovementAIInfo info) : Component(parent, componentID) {
|
|
m_Info = info;
|
|
m_AtFinalWaypoint = true;
|
|
|
|
m_BaseCombatAI = m_Parent->GetComponent<BaseCombatAIComponent>();
|
|
|
|
//Try and fix the insane values:
|
|
if (m_Info.wanderRadius > 5.0f) m_Info.wanderRadius *= 0.5f;
|
|
if (m_Info.wanderRadius > 8.0f) m_Info.wanderRadius = 8.0f;
|
|
if (m_Info.wanderSpeed > 0.5f) m_Info.wanderSpeed *= 0.5f;
|
|
|
|
m_BaseSpeed = GetBaseSpeed(m_Parent->GetLOT());
|
|
|
|
m_NextWaypoint = m_Parent->GetPosition();
|
|
m_Acceleration = 0.4f;
|
|
m_PullingToPoint = false;
|
|
m_PullPoint = NiPoint3Constant::ZERO;
|
|
m_HaltDistance = 0;
|
|
m_TimeToTravel = 0;
|
|
m_TimeTravelled = 0;
|
|
m_CurrentSpeed = 0;
|
|
m_MaxSpeed = 0;
|
|
m_LockRotation = false;
|
|
m_Path = nullptr;
|
|
m_SourcePosition = m_Parent->GetPosition();
|
|
m_Paused = false;
|
|
m_SavedVelocity = NiPoint3Constant::ZERO;
|
|
m_IsBounced = false;
|
|
|
|
RegisterMsg(&MovementAIComponent::OnGetObjectReportInfo);
|
|
|
|
SetPath(m_Parent->GetVarAsString(u"attached_path"));
|
|
}
|
|
|
|
void MovementAIComponent::SetPath(const std::string pathName) {
|
|
m_Path = Game::zoneManager->GetZone()->GetPath(pathName);
|
|
if (!pathName.empty()) LOG("WARNING: %s path %s", m_Path ? "Found" : "Failed to find", pathName.c_str());
|
|
if (!m_Path) return;
|
|
SetMaxSpeed(1);
|
|
SetCurrentSpeed(m_BaseSpeed);
|
|
SetPath(m_Path->pathWaypoints);
|
|
}
|
|
|
|
void MovementAIComponent::Pause() {
|
|
if (m_Paused) return;
|
|
m_Paused = true;
|
|
// A stun doesn't stop a knockback mid air; the entity stays put once it lands
|
|
if (IsKnockedBack()) {
|
|
m_SavedVelocity = NiPoint3Constant::ZERO;
|
|
return;
|
|
}
|
|
SetPosition(ApproximateLocation());
|
|
m_SavedVelocity = GetVelocity();
|
|
SetVelocity(NiPoint3Constant::ZERO);
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
|
|
void MovementAIComponent::Resume() {
|
|
if (!m_Paused) return;
|
|
m_Paused = false;
|
|
if (IsKnockedBack()) return;
|
|
SetVelocity(m_SavedVelocity);
|
|
m_SavedVelocity = NiPoint3Constant::ZERO;
|
|
SetRotation(QuatUtils::LookAt(m_Parent->GetPosition(), m_NextWaypoint));
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
|
|
void MovementAIComponent::Update(const float deltaTime) {
|
|
if (IsKnockedBack()) {
|
|
UpdateKnockback(deltaTime);
|
|
return;
|
|
}
|
|
|
|
if (m_Paused) return;
|
|
|
|
auto* const quickBuildComponent = m_Parent->GetComponent<QuickBuildComponent>();
|
|
if (quickBuildComponent && quickBuildComponent->GetState() != eQuickBuildState::COMPLETED) return;
|
|
|
|
if (m_PullingToPoint) {
|
|
const auto source = GetCurrentWaypoint();
|
|
|
|
const auto speed = deltaTime * 2.5f;
|
|
|
|
NiPoint3 velocity = (m_PullPoint - source) * speed;
|
|
|
|
SetPosition(source + velocity);
|
|
|
|
if (Vector3::DistanceSquared(m_Parent->GetPosition(), m_PullPoint) < std::pow(2, 2)) {
|
|
m_PullingToPoint = false;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Are we done?
|
|
if (AtFinalWaypoint()) return;
|
|
|
|
if (m_HaltDistance > 0) {
|
|
// Prevent us from hugging the target
|
|
if (Vector3::DistanceSquared(ApproximateLocation(), GetDestination()) < std::pow(m_HaltDistance, 2)) {
|
|
Stop();
|
|
return;
|
|
}
|
|
}
|
|
|
|
m_TimeTravelled += deltaTime;
|
|
|
|
const auto approxPos = ApproximateLocation();
|
|
SetPosition(approxPos);
|
|
// Set the AIs new home based on where our current waypoint is IF wehave a path, we're idle, not out of combat (grace period if you left the radius),
|
|
// and we're not tethering to home, that way we can return to this
|
|
// when resuming the pathing after losing aggro while moving the aggro hitbox with us
|
|
const bool idleNotResetting = m_Path && m_BaseCombatAI && m_BaseCombatAI->GetState() == AiState::idle && !m_BaseCombatAI->GetOutOfCombat() && !m_BaseCombatAI->GetIsTethering();
|
|
if (idleNotResetting) {
|
|
m_BaseCombatAI->SetStartingPosition(approxPos);
|
|
}
|
|
|
|
if (m_TimeTravelled < m_TimeToTravel) return;
|
|
m_TimeTravelled = 0.0f;
|
|
|
|
const auto source = GetCurrentWaypoint();
|
|
|
|
SetPosition(source);
|
|
m_SourcePosition = source;
|
|
|
|
if (m_Acceleration > 0 && m_BaseSpeed > 0 && AdvanceWaypointIndex()) // Do we have another waypoint to seek?
|
|
{
|
|
m_NextWaypoint = GetCurrentWaypoint();
|
|
if (m_NextWaypoint == source) {
|
|
m_TimeToTravel = 0.0f;
|
|
} else {
|
|
m_CurrentSpeed = std::min(m_CurrentSpeed + m_Acceleration, m_MaxSpeed);
|
|
|
|
// scale speed based on base speed, and by any speed buffs or slows on this entity
|
|
auto speed = m_CurrentSpeed * m_BaseSpeed;
|
|
const auto* const controllablePhysics = m_Parent->GetComponent<ControllablePhysicsComponent>();
|
|
if (controllablePhysics && controllablePhysics->GetSpeedMultiplier() > 0.0f) speed *= controllablePhysics->GetSpeedMultiplier();
|
|
|
|
const auto delta = m_NextWaypoint - source;
|
|
|
|
// Normalize the vector
|
|
const auto length = delta.Length();
|
|
if (length > 0.0f) {
|
|
SetVelocity((delta / length) * speed);
|
|
}
|
|
|
|
// Calclute the time it will take to reach the next waypoint with the current speed
|
|
m_TimeTravelled = 0.0f;
|
|
m_TimeToTravel = length / speed;
|
|
|
|
SetRotation(QuatUtils::LookAt(source, m_NextWaypoint));
|
|
}
|
|
} else {
|
|
// Only try to renew or continue the path if we're in the idle or spawn state and we actually have a combatAI component
|
|
if (!m_BaseCombatAI || (idleNotResetting)) {
|
|
// Check if there are more waypoints in the queue, if so set our next destination to the next waypoint
|
|
const auto waypointNum = m_IsBounced ? m_CurrentPath.size() : m_CurrentPathWaypointCount - m_CurrentPath.size() - 1;
|
|
RunWaypointCommands(waypointNum);
|
|
if (m_CurrentPath.empty()) {
|
|
if (m_Path) {
|
|
if (m_Path->pathBehavior == PathBehavior::Loop) {
|
|
SetPath(m_Path->pathWaypoints);
|
|
} else if (m_Path->pathBehavior == PathBehavior::Bounce) {
|
|
m_IsBounced = !m_IsBounced;
|
|
std::vector<PathWaypoint> waypoints = m_Path->pathWaypoints;
|
|
if (m_IsBounced) std::ranges::reverse(waypoints);
|
|
SetPath(waypoints);
|
|
} else if (m_Path->pathBehavior == PathBehavior::Once) {
|
|
// In this case we intended to follow a path and once we've followed it we camp there, otherwise we'd just wander home again.
|
|
Stop();
|
|
return;
|
|
}
|
|
} else {
|
|
Stop();
|
|
if (m_FollowedTarget != LWOOBJID_EMPTY) {
|
|
GameMessages::GetPosition getPos;
|
|
if (!getPos.Send(m_FollowedTarget)) {
|
|
LOG("Target %llu does not exist anymore to follow", m_FollowedTarget);
|
|
m_FollowedTarget = LWOOBJID_EMPTY;
|
|
} else {
|
|
SetDestination(getPos.pos);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
} else {
|
|
SetDestination(m_CurrentPath.top().position);
|
|
|
|
m_CurrentPath.pop();
|
|
}
|
|
} else {
|
|
Stop();
|
|
}
|
|
}
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
|
|
const MovementAIInfo& MovementAIComponent::GetInfo() const {
|
|
return m_Info;
|
|
}
|
|
|
|
bool MovementAIComponent::AdvanceWaypointIndex() {
|
|
if (m_PathIndex >= m_InterpolatedWaypoints.size()) {
|
|
return false;
|
|
}
|
|
|
|
m_PathIndex++;
|
|
|
|
return true;
|
|
}
|
|
|
|
NiPoint3 MovementAIComponent::GetCurrentWaypoint() const {
|
|
return m_PathIndex >= m_InterpolatedWaypoints.size() ? m_Parent->GetPosition() : m_InterpolatedWaypoints[m_PathIndex];
|
|
}
|
|
|
|
NiPoint3 MovementAIComponent::ApproximateLocation() const {
|
|
auto source = m_SourcePosition;
|
|
if (AtFinalWaypoint()) return m_Parent->GetPosition();
|
|
|
|
auto destination = m_NextWaypoint;
|
|
|
|
auto percentageToWaypoint = m_TimeToTravel > 0 ? m_TimeTravelled / m_TimeToTravel : 0;
|
|
|
|
auto approximation = source + ((destination - source) * percentageToWaypoint);
|
|
|
|
if (dpWorld::IsLoaded()) {
|
|
approximation.y = dpWorld::GetNavMesh()->GetHeightAtPoint(approximation);
|
|
}
|
|
|
|
return approximation;
|
|
}
|
|
|
|
bool MovementAIComponent::Warp(const NiPoint3& point) {
|
|
m_Knockback.Cancel();
|
|
m_DestinationAfterKnockback.reset();
|
|
Stop();
|
|
|
|
NiPoint3 destination = point;
|
|
|
|
if (dpWorld::IsLoaded()) {
|
|
destination.y = dpWorld::GetNavMesh()->GetHeightAtPoint(point);
|
|
|
|
if (std::abs(destination.y - point.y) > 3) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
SetPosition(destination);
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
return true;
|
|
}
|
|
|
|
void MovementAIComponent::Stop() {
|
|
m_DestinationAfterKnockback.reset();
|
|
if (AtFinalWaypoint()) return;
|
|
|
|
SetPosition(ApproximateLocation());
|
|
|
|
SetVelocity(NiPoint3Constant::ZERO);
|
|
|
|
m_TimeToTravel = 0;
|
|
m_TimeTravelled = 0;
|
|
|
|
m_AtFinalWaypoint = true;
|
|
|
|
m_InterpolatedWaypoints.clear();
|
|
while (!m_CurrentPath.empty()) m_CurrentPath.pop();
|
|
m_CurrentPathWaypointCount = 0;
|
|
|
|
m_PathIndex = 0;
|
|
|
|
m_CurrentSpeed = 0;
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
|
|
void MovementAIComponent::PullToPoint(const NiPoint3& point) {
|
|
m_Knockback.Cancel();
|
|
Stop();
|
|
|
|
m_PullingToPoint = true;
|
|
m_PullPoint = point;
|
|
}
|
|
|
|
void MovementAIComponent::SetPath(std::vector<PathWaypoint> path) {
|
|
if (path.empty()) return;
|
|
while (!m_CurrentPath.empty()) m_CurrentPath.pop();
|
|
std::for_each(path.rbegin(), path.rend() - 1, [this](const PathWaypoint& point) {
|
|
this->m_CurrentPath.push(point);
|
|
});
|
|
|
|
m_CurrentPathWaypointCount = path.size();
|
|
SetDestination(path.front().position);
|
|
}
|
|
|
|
float MovementAIComponent::GetBaseSpeed(LOT lot) {
|
|
// Check if the lot is in the cache
|
|
const auto& it = m_PhysicsSpeedCache.find(lot);
|
|
|
|
if (it != m_PhysicsSpeedCache.end()) {
|
|
return it->second;
|
|
}
|
|
|
|
CDComponentsRegistryTable* componentRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
|
|
CDPhysicsComponentTable* physicsComponentTable = CDClientManager::GetTable<CDPhysicsComponentTable>();
|
|
|
|
int32_t componentID;
|
|
CDPhysicsComponent* physicsComponent = nullptr;
|
|
|
|
componentID = componentRegistryTable->GetByIDAndType(lot, eReplicaComponentType::CONTROLLABLE_PHYSICS, -1);
|
|
|
|
if (componentID == -1) {
|
|
componentID = componentRegistryTable->GetByIDAndType(lot, eReplicaComponentType::SIMPLE_PHYSICS, -1);
|
|
}
|
|
|
|
physicsComponent = physicsComponentTable->GetByID(componentID);
|
|
|
|
// Client defaults speed to 10 and if the speed is also null in the table, it defaults to 10.
|
|
float speed = physicsComponent != nullptr ? physicsComponent->speed : 10.0f;
|
|
|
|
float delta = fabs(speed) - 1.0f;
|
|
|
|
if (delta <= std::numeric_limits<float>::epsilon()) speed = 10.0f;
|
|
|
|
m_PhysicsSpeedCache[lot] = speed;
|
|
|
|
return speed;
|
|
}
|
|
|
|
float MovementAIComponent::GetRemainingPathDistance() const {
|
|
if (m_InterpolatedWaypoints.empty() || m_PathIndex >= m_InterpolatedWaypoints.size()) {
|
|
return 0.0f;
|
|
}
|
|
|
|
float total = NiPoint3::Distance(m_Parent->GetPosition(), m_InterpolatedWaypoints[m_PathIndex]);
|
|
for (size_t i = m_PathIndex; i + 1 < m_InterpolatedWaypoints.size(); ++i) {
|
|
total += NiPoint3::Distance(m_InterpolatedWaypoints[i], m_InterpolatedWaypoints[i + 1]);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
void MovementAIComponent::SetPosition(const NiPoint3& value) {
|
|
m_Parent->SetPosition(value);
|
|
}
|
|
|
|
void MovementAIComponent::SetRotation(const NiQuaternion& value) {
|
|
if (!m_LockRotation) m_Parent->SetRotation(value);
|
|
}
|
|
|
|
NiPoint3 MovementAIComponent::GetVelocity() const {
|
|
auto* controllablePhysicsComponent = m_Parent->GetComponent<ControllablePhysicsComponent>();
|
|
|
|
if (controllablePhysicsComponent != nullptr) {
|
|
return controllablePhysicsComponent->GetVelocity();
|
|
}
|
|
|
|
auto* simplePhysicsComponent = m_Parent->GetComponent<SimplePhysicsComponent>();
|
|
|
|
if (simplePhysicsComponent != nullptr) {
|
|
return simplePhysicsComponent->GetVelocity();
|
|
}
|
|
|
|
return NiPoint3Constant::ZERO;
|
|
|
|
}
|
|
|
|
void MovementAIComponent::SetVelocity(const NiPoint3& value) {
|
|
auto* controllablePhysicsComponent = m_Parent->GetComponent<ControllablePhysicsComponent>();
|
|
|
|
if (controllablePhysicsComponent != nullptr) {
|
|
controllablePhysicsComponent->SetVelocity(value);
|
|
|
|
return;
|
|
}
|
|
|
|
auto* simplePhysicsComponent = m_Parent->GetComponent<SimplePhysicsComponent>();
|
|
|
|
if (simplePhysicsComponent != nullptr) {
|
|
simplePhysicsComponent->SetVelocity(value);
|
|
}
|
|
}
|
|
|
|
void MovementAIComponent::SetDestination(const NiPoint3 destination) {
|
|
if (m_PullingToPoint) return;
|
|
if (IsKnockedBack()) {
|
|
m_DestinationAfterKnockback = destination;
|
|
return;
|
|
}
|
|
|
|
const auto location = ApproximateLocation();
|
|
|
|
if (!AtFinalWaypoint()) {
|
|
SetPosition(location);
|
|
}
|
|
|
|
m_SourcePosition = location;
|
|
|
|
std::vector<NiPoint3> computedPath;
|
|
if (dpWorld::IsLoaded()) {
|
|
computedPath = dpWorld::GetNavMesh()->GetPath(m_Parent->GetPosition(), destination, m_Info.wanderSpeed);
|
|
}
|
|
|
|
// Somehow failed
|
|
if (computedPath.empty()) {
|
|
// Than take 10 points between the current position and the destination and make that the path
|
|
|
|
auto start = location;
|
|
|
|
auto delta = destination - start;
|
|
|
|
auto step = delta / 10.0f;
|
|
|
|
for (int i = 0; i < 10; i++) {
|
|
start += step;
|
|
|
|
computedPath.push_back(start);
|
|
}
|
|
}
|
|
|
|
// The path is walked without collision, so an enemy's stops at walls it can't cross (navmesh carvers, enemy only
|
|
// blockers); with nothing left to walk it stays where it is. A patrol along its level path is left as designed.
|
|
const bool patrolling = m_Path && m_BaseCombatAI && m_BaseCombatAI->GetState() == AiState::idle;
|
|
if (m_BaseCombatAI && !patrolling) {
|
|
computedPath = dpWorld::ClampPath(m_Parent->GetPosition(), std::move(computedPath), static_cast<uint32_t>(m_Parent->GetCollisionGroup()));
|
|
if (computedPath.empty()) computedPath.push_back(m_Parent->GetPosition());
|
|
}
|
|
|
|
m_InterpolatedWaypoints.clear();
|
|
|
|
// Simply path
|
|
for (auto& point : computedPath) {
|
|
if (dpWorld::IsLoaded()) {
|
|
point.y = dpWorld::GetNavMesh()->GetHeightAtPoint(point);
|
|
}
|
|
|
|
m_InterpolatedWaypoints.push_back(point);
|
|
}
|
|
|
|
m_PathIndex = 0;
|
|
|
|
m_TimeTravelled = 0;
|
|
m_TimeToTravel = 0;
|
|
|
|
m_AtFinalWaypoint = false;
|
|
}
|
|
|
|
NiPoint3 MovementAIComponent::GetDestination() const {
|
|
return m_InterpolatedWaypoints.empty() ? m_Parent->GetPosition() : m_InterpolatedWaypoints.back();
|
|
}
|
|
|
|
void MovementAIComponent::SetMaxSpeed(const float value) {
|
|
if (value == m_MaxSpeed) return;
|
|
m_MaxSpeed = value;
|
|
m_Acceleration = value / 5.0f;
|
|
}
|
|
|
|
void MovementAIComponent::RunWaypointCommands(uint32_t waypointNum) {
|
|
m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum);
|
|
|
|
if (!m_Path || waypointNum >= m_Path->pathWaypoints.size()) return;
|
|
const auto& commands = m_Path->pathWaypoints[waypointNum].commands;
|
|
for (const auto& [command, data] : commands) {
|
|
LOG_DEBUG("%s %s %s", StringifiedEnum::ToString(command).data(), m_Path->pathName.c_str(), data.c_str());
|
|
const auto dataSplit = GeneralUtils::SplitString(data, ',');
|
|
switch (command) {
|
|
case eWaypointCommandType::INVALID: break;
|
|
case eWaypointCommandType::BOUNCE: break;
|
|
case eWaypointCommandType::STOP: Pause(); break;
|
|
case eWaypointCommandType::GROUP_EMOTE: break;
|
|
case eWaypointCommandType::SET_VARIABLE: break; // Empty in the client
|
|
case eWaypointCommandType::CAST_SKILL: {
|
|
const auto skill = GeneralUtils::TryParse<uint32_t>(data);
|
|
if (skill) {
|
|
auto* const skillComponent = m_Parent->GetComponent<SkillComponent>();
|
|
if (skillComponent) skillComponent->CastSkill(skill.value());
|
|
}
|
|
break;
|
|
}
|
|
case eWaypointCommandType::EQUIP_INVENTORY: {
|
|
auto* const inventoryComponent = m_Parent->GetComponent<InventoryComponent>();
|
|
if (inventoryComponent) {
|
|
// items should always exist
|
|
auto* const item = inventoryComponent->GetInventory(eInventoryType::ITEMS)->FindItemBySlot(0);
|
|
if (item) inventoryComponent->EquipItem(item);
|
|
}
|
|
break;
|
|
}
|
|
case eWaypointCommandType::UNEQUIP_INVENTORY: {
|
|
auto* const inventoryComponent = m_Parent->GetComponent<InventoryComponent>();
|
|
if (inventoryComponent) {
|
|
// items should always exist
|
|
auto* const item = inventoryComponent->GetInventory(eInventoryType::ITEMS)->FindItemBySlot(0);
|
|
if (item) inventoryComponent->UnEquipItem(item);
|
|
}
|
|
break;
|
|
}
|
|
case eWaypointCommandType::DELAY: {
|
|
// Pause(GeneralUtils::TryParse<float>(data).value_or(0.0f));
|
|
break;
|
|
}
|
|
case eWaypointCommandType::EMOTE: {
|
|
// m_Delay = RenderComponent::GetAnimationTime(m_Parent, data);
|
|
// const auto emoteID = GeneralUtils::TryParse<uint32_t>(data);
|
|
// if (emoteID) GameMessages::SendPlayEmote(m_Parent->GetObjectID(), emoteID.value(), LWOOBJID_EMPTY, UNASSIGNED_SYSTEM_ADDRESS);
|
|
break;
|
|
}
|
|
case eWaypointCommandType::TELEPORT: break;
|
|
case eWaypointCommandType::PATH_SPEED: m_BaseSpeed = GetBaseSpeed(m_Parent->GetLOT()) * GeneralUtils::TryParse<float>(data).value_or(1.0f); break;
|
|
case eWaypointCommandType::REMOVE_NPC: break;
|
|
case eWaypointCommandType::CHANGE_WAYPOINT: SetPath(dataSplit[0]); break;
|
|
case eWaypointCommandType::DELETE_SELF: break;
|
|
case eWaypointCommandType::KILL_SELF: m_Parent->Smash(); break;
|
|
case eWaypointCommandType::SPAWN_OBJECT: break;
|
|
case eWaypointCommandType::PLAY_SOUND: break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool MovementAIComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
|
|
|
|
auto& movementInfo = reportInfo.info->PushDebug("MovementAI");
|
|
if (m_Path) {
|
|
movementInfo.PushDebug<AMFStringValue>("Path") = m_Path->pathName;
|
|
}
|
|
|
|
auto& movementAiInfo = movementInfo.PushDebug("Movement AI Info");
|
|
movementAiInfo.PushDebug<AMFStringValue>("Movement Type") = m_Info.movementType;
|
|
movementAiInfo.PushDebug<AMFDoubleValue>("Wander Radius") = m_Info.wanderRadius;
|
|
movementAiInfo.PushDebug<AMFDoubleValue>("Wander Speed") = m_Info.wanderSpeed;
|
|
movementAiInfo.PushDebug<AMFDoubleValue>("Wander Chance") = m_Info.wanderChance;
|
|
movementAiInfo.PushDebug<AMFDoubleValue>("Wander Delay Min") = m_Info.wanderDelayMin;
|
|
movementAiInfo.PushDebug<AMFDoubleValue>("Wander Delay Max") = m_Info.wanderDelayMax;
|
|
|
|
auto& speedInfo = movementInfo.PushDebug("Speed Info");
|
|
speedInfo.PushDebug<AMFDoubleValue>("Base Speed") = m_BaseSpeed;
|
|
speedInfo.PushDebug<AMFDoubleValue>("Max Speed") = m_MaxSpeed;
|
|
speedInfo.PushDebug<AMFDoubleValue>("Current Speed") = m_CurrentSpeed;
|
|
speedInfo.PushDebug<AMFDoubleValue>("Acceleration") = m_Acceleration;
|
|
|
|
movementInfo.PushDebug<AMFDoubleValue>("Halt Distance") = m_HaltDistance;
|
|
movementInfo.PushDebug<AMFDoubleValue>("Time To Travel") = m_TimeToTravel;
|
|
movementInfo.PushDebug<AMFDoubleValue>("Time Travelled") = m_TimeTravelled;
|
|
movementInfo.PushDebug<AMFBoolValue>("Lock Rotation") = m_LockRotation;
|
|
movementInfo.PushDebug<AMFBoolValue>("Paused") = m_Paused;
|
|
movementInfo.PushDebug<AMFDoubleValue>("Pulling To Point") = m_PullingToPoint;
|
|
movementInfo.PushDebug<AMFBoolValue>("At Final Waypoint") = m_AtFinalWaypoint;
|
|
|
|
auto& pullPointInfo = movementInfo.PushDebug("Pull Point").PushDebug(m_PullPoint);
|
|
|
|
// movementInfo.PushDebug<AMFDoubleValue>("Delay") = m_Delay;
|
|
|
|
auto& waypoints = movementInfo.PushDebug("Interpolated Waypoints");
|
|
int i = 0;
|
|
for (const auto& point : m_InterpolatedWaypoints) {
|
|
waypoints.PushDebug("Waypoint " + std::to_string(++i)).PushDebug(point);
|
|
}
|
|
|
|
i = 0;
|
|
auto& currentPath = movementInfo.PushDebug("Current Path");
|
|
auto pathCopy = m_CurrentPath; // Copy to avoid modifying the original stack
|
|
while (!pathCopy.empty()) {
|
|
const auto& waypoint = pathCopy.top();
|
|
currentPath.PushDebug("Waypoint " + std::to_string(++i)).PushDebug(waypoint.position);
|
|
|
|
pathCopy.pop();
|
|
}
|
|
|
|
movementInfo.PushDebug<AMFStringValue>("Followed Target") = std::to_string(m_FollowedTarget);
|
|
|
|
return true;
|
|
}
|
|
|
|
void MovementAIComponent::FollowTarget(const LWOOBJID target) {
|
|
if (target == LWOOBJID_EMPTY) {
|
|
m_FollowedTarget = target;
|
|
return;
|
|
}
|
|
GameMessages::GetPosition getPos;
|
|
if (!getPos.Send(target)) {
|
|
LOG("Tried to follow target %llu but they don't exist", target);
|
|
m_FollowedTarget = LWOOBJID_EMPTY;
|
|
return;
|
|
}
|
|
|
|
m_FollowedTarget = target;
|
|
SetMaxSpeed(1.0f);
|
|
m_CurrentSpeed = 1.0f;
|
|
SetDestination(getPos.pos);
|
|
}
|
|
|
|
void MovementAIComponent::Knockback(const NiPoint3& vector) {
|
|
// Whatever we were walking to is forgotten, like a knocked back character losing its movement input
|
|
m_PullingToPoint = false;
|
|
Stop();
|
|
|
|
const auto position = m_Parent->GetPosition();
|
|
m_KnockbackStartHeight = position.y;
|
|
|
|
auto* const controllablePhysics = m_Parent->GetComponent<ControllablePhysicsComponent>();
|
|
const auto ground = [this](const NiPoint3& point) { return GetGroundHeight(point); };
|
|
|
|
if (!m_Knockback.Start(position, vector)) {
|
|
// Too weak to launch: just shoved along the ground
|
|
SetPosition(dpKnockback::Nudge(position, vector, KNOCKBACK_MAX_DROP, ground));
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
return;
|
|
}
|
|
|
|
SetPosition(m_Knockback.GetPosition());
|
|
SetVelocity(m_Knockback.GetVelocity());
|
|
if (controllablePhysics) controllablePhysics->SetIsOnGround(false);
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
|
|
void MovementAIComponent::UpdateKnockback(const float deltaTime) {
|
|
auto* const controllablePhysics = m_Parent->GetComponent<ControllablePhysicsComponent>();
|
|
|
|
auto gravity = Game::zoneManager ? Game::zoneManager->GetWorldConfig().peGravityValue : 0.0f;
|
|
if (gravity <= 0.0f) gravity = DEFAULT_GRAVITY;
|
|
if (controllablePhysics && controllablePhysics->GetGravityScale() > 0.0f) gravity *= controllablePhysics->GetGravityScale();
|
|
|
|
const bool flying = m_Knockback.Step(deltaTime, gravity, [this](const NiPoint3& point) { return GetGroundHeight(point); });
|
|
|
|
if (flying) {
|
|
SetPosition(m_Knockback.GetPosition());
|
|
SetVelocity(m_Knockback.GetVelocity());
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
return;
|
|
}
|
|
|
|
// Landed: put it back on the navmesh so pathing picks up from a walkable spot
|
|
auto landing = m_Knockback.GetPosition();
|
|
if (dpWorld::IsLoaded()) landing = dpWorld::GetNavMesh()->NearestPoint(landing);
|
|
SetPosition(landing);
|
|
SetVelocity(NiPoint3Constant::ZERO);
|
|
if (controllablePhysics) controllablePhysics->SetIsOnGround(true);
|
|
m_SourcePosition = landing;
|
|
m_NextWaypoint = landing;
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
if (m_DestinationAfterKnockback) {
|
|
const auto destination = *m_DestinationAfterKnockback;
|
|
m_DestinationAfterKnockback.reset();
|
|
if (!m_Paused) SetDestination(destination);
|
|
}
|
|
}
|
|
|
|
float MovementAIComponent::GetGroundHeight(const NiPoint3& point) const {
|
|
if (dpWorld::IsLoaded()) return dpWorld::GetNavMesh()->GetHeightAtPoint(point);
|
|
return m_KnockbackStartHeight;
|
|
}
|