582 lines
24 KiB
C++
582 lines
24 KiB
C++
#include "CharacterState.h"
|
||
#include "LocationState.h"
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
|
||
namespace FRG {
|
||
|
||
namespace {
|
||
// Tick counts are authoritative and therefore independent of render rate.
|
||
constexpr float ATTACK_COOLDOWN_TIME = 1.6f;
|
||
constexpr float HOME_DRIFT_CHECK_INTERVAL = 5.0f;
|
||
fp HOME_DRIFT_CHECK_INTERVAL_FP = static_cast<fp>(HOME_DRIFT_CHECK_INTERVAL);
|
||
constexpr float TARGET_REPLAN_THRESHOLD = 0.25f;
|
||
|
||
fp TARGET_REPLAN_THRESHOLD_SQUARED = static_cast<fp>(TARGET_REPLAN_THRESHOLD * TARGET_REPLAN_THRESHOLD);
|
||
|
||
|
||
|
||
constexpr float PI = 3.14159265358979323846f;
|
||
constexpr int BLOCKED_REPLAN_TICKS = 3;
|
||
|
||
std::uint32_t stableIdHash(const std::string& value)
|
||
{
|
||
std::uint32_t hash = 2166136261u;
|
||
for (const unsigned char byte : value) {
|
||
hash ^= byte;
|
||
hash *= 16777619u;
|
||
}
|
||
return hash;
|
||
}
|
||
}
|
||
|
||
void CharacterState::configureAsPlayer() {
|
||
walkSpeed = static_cast<fp>(3.0f);
|
||
rotationSpeed = static_cast < fp>(8.0f);
|
||
modelScale = 1.f;
|
||
modelCorrectionRotation = Eigen::Quaternionf(
|
||
Eigen::AngleAxisf(PI, Eigen::Vector3f::UnitY()) *
|
||
Eigen::AngleAxisf(-PI * 0.5f, Eigen::Vector3f::UnitX()));
|
||
canAttack = true;
|
||
isPlayer = true;
|
||
}
|
||
|
||
void CharacterState::configureNavigationRetryCooldowns(
|
||
const std::string& entityId, std::vector<int> cooldownTicks)
|
||
{
|
||
if (cooldownTicks.empty()) {
|
||
cooldownTicks = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
|
||
}
|
||
for (int& ticks : cooldownTicks)
|
||
ticks = (std::max)(1, (std::min)(600, ticks));
|
||
|
||
std::uint32_t state = stableIdHash(entityId);
|
||
for (std::size_t i = cooldownTicks.size(); i > 1; --i) {
|
||
state = state * 1664525u + 1013904223u;
|
||
const std::size_t other = state % i;
|
||
std::swap(cooldownTicks[i - 1], cooldownTicks[other]);
|
||
}
|
||
navigationRetryCooldownTicks = std::move(cooldownTicks);
|
||
navigationRetryCooldownIndex = 0;
|
||
}
|
||
|
||
void CharacterState::scheduleNextNavigationRetry()
|
||
{
|
||
if (navigationRetryCooldownTicks.empty())
|
||
configureNavigationRetryCooldowns(npcName);
|
||
navigationRetryTicks = navigationRetryCooldownTicks[
|
||
navigationRetryCooldownIndex % navigationRetryCooldownTicks.size()];
|
||
navigationRetryCooldownIndex =
|
||
(navigationRetryCooldownIndex + 1) %
|
||
navigationRetryCooldownTicks.size();
|
||
}
|
||
|
||
bool CharacterState::isMoving() const {
|
||
Vec3fp toTarget = walkTarget - position;
|
||
toTarget.y = static_cast<fp>(0.f);
|
||
return !pathWaypoints.empty() || toTarget.normSquared() > kWalkThreshold_SQUARED();
|
||
}
|
||
|
||
Vec3fp CharacterState::getCurrentNavigationTarget() const {
|
||
if (!pathWaypoints.empty() && currentWaypointIndex < pathWaypoints.size()) {
|
||
return pathWaypoints[currentWaypointIndex];
|
||
}
|
||
return walkTarget;
|
||
}
|
||
|
||
void CharacterState::stopInPlace() {
|
||
walkTarget = Vec3fp(position.x, static_cast<fp>(0.f), position.z);
|
||
requestedWalkTarget = walkTarget;
|
||
pathWaypoints.clear();
|
||
currentWaypointIndex = 0;
|
||
onArrivedCallbackName.clear();
|
||
movementBlockedTicks = 0;
|
||
navigationRetryTicks = 0;
|
||
velocity = Vec2fp(fp(0), fp(0));
|
||
preferredVelocity = Vec2fp(fp(0), fp(0));
|
||
}
|
||
|
||
void CharacterState::setTarget(
|
||
const Vec3fp& target,
|
||
std::string callbackName,
|
||
const LocationNetworkState& location,
|
||
const std::string& entityId)
|
||
{
|
||
|
||
|
||
const Vec3fp normalizedTarget(target.x, static_cast<fp>(0.f), target.z);
|
||
const bool sameRequestedTarget =
|
||
(normalizedTarget - requestedWalkTarget).normSquared() <= TARGET_REPLAN_THRESHOLD_SQUARED;
|
||
const bool alreadyMovingToTarget =
|
||
!pathWaypoints.empty() ||
|
||
(walkTarget - normalizedTarget).normSquared() <= TARGET_REPLAN_THRESHOLD_SQUARED;
|
||
const bool stoppedAfterFailedPath =
|
||
pathWaypoints.empty() &&
|
||
(walkTarget - position).normSquared() <= TARGET_REPLAN_THRESHOLD_SQUARED;
|
||
if (callbackName.empty() && sameRequestedTarget &&
|
||
(alreadyMovingToTarget || stoppedAfterFailedPath)) {
|
||
return;
|
||
}
|
||
|
||
requestedWalkTarget = normalizedTarget;
|
||
onArrivedCallbackName = std::move(callbackName);
|
||
movementBlockedTicks = 0;
|
||
navigationRetryTicks = 0;
|
||
|
||
// В setTarget и forceReplan:
|
||
if (location.activeNavigationIndex < 0 ||
|
||
location.activeNavigationIndex >= static_cast<int>(location.navigationMaps.size())) {
|
||
walkTarget = Vec3fp(position.x, static_cast<fp>(0.f), position.z);
|
||
return;
|
||
}
|
||
|
||
pathWaypoints = location.navigationMaps[location.activeNavigationIndex]
|
||
.findPathToNearest(this->position, normalizedTarget);
|
||
|
||
// Сразу целимся во второй элемент (индекс 1), так как индекс 0 — это точка старта
|
||
currentWaypointIndex = (pathWaypoints.size() > 1) ? 1 : 0;
|
||
|
||
if (!pathWaypoints.empty()) {
|
||
for (Vec3fp& waypoint : pathWaypoints)
|
||
{
|
||
waypoint.y = static_cast<fp>(0.f);
|
||
}
|
||
walkTarget = pathWaypoints.back();
|
||
return;
|
||
}
|
||
|
||
walkTarget = Vec3fp(position.x, static_cast<fp>(0.f), position.z);
|
||
if (!isPlayer) scheduleNextNavigationRetry();
|
||
}
|
||
|
||
void CharacterState::forceReplan(
|
||
const LocationNetworkState& location,
|
||
const std::string& entityId)
|
||
{
|
||
const Vec3fp normalizedTarget(
|
||
requestedWalkTarget.x, static_cast<fp>(0.f), requestedWalkTarget.z);
|
||
|
||
if (location.activeNavigationIndex < 0 ||
|
||
location.activeNavigationIndex >= static_cast<int>(location.navigationMaps.size())) {
|
||
walkTarget = Vec3fp(position.x, static_cast<fp>(0.f), position.z);
|
||
return;
|
||
}
|
||
|
||
pathWaypoints = location.navigationMaps[location.activeNavigationIndex]
|
||
.findPathToNearest(this->position, normalizedTarget);
|
||
|
||
currentWaypointIndex = (pathWaypoints.size() > 1) ? 1 : 0;
|
||
movementBlockedTicks = 0;
|
||
navigationRetryTicks = 0;
|
||
|
||
if (!pathWaypoints.empty()) {
|
||
for (Vec3fp& waypoint : pathWaypoints) {
|
||
waypoint.y = static_cast<fp>(0.f);
|
||
}
|
||
walkTarget = pathWaypoints.back();
|
||
return;
|
||
}
|
||
|
||
walkTarget = Vec3fp(position.x, static_cast<fp>(0.f), position.z);
|
||
if (!isPlayer) scheduleNextNavigationRetry();
|
||
}
|
||
|
||
void CharacterState::stopAndRotateToTarget(std::string characterId)
|
||
{
|
||
stopInPlace();
|
||
faceTargetId = std::move(characterId);
|
||
}
|
||
|
||
void CharacterState::resetPlayerAfterDeath()
|
||
{
|
||
if (!isPlayer) return;
|
||
currentState = AnimationState::STAND;
|
||
resetAnim = true;
|
||
showWeapon = false;
|
||
battle_state = 0;
|
||
attack = 0;
|
||
}
|
||
|
||
void CharacterState::tryAttackCombo()
|
||
{
|
||
if (static_cast<int>(animationFrame) < 20 ||
|
||
(currentState != AnimationState::ACTION_ATTACK &&
|
||
currentState != AnimationState::ACTION_ATTACK_2)) {
|
||
return;
|
||
}
|
||
const auto index = static_cast<std::size_t>(currentState);
|
||
animationFrame = index < animationStartFrames.size()
|
||
? static_cast<float>(animationStartFrames[index]) : 0.f;
|
||
}
|
||
|
||
void CharacterState::setHp(float newHp) {
|
||
hp = newHp;
|
||
if (hp < 0.f) hp = 0.f;
|
||
}
|
||
|
||
void CharacterState::setAnimationMetadata(
|
||
AnimationState state, int startFrame, int frameCount)
|
||
{
|
||
const auto index = static_cast<std::size_t>(state);
|
||
if (index < animationFrameCounts.size()) {
|
||
animationStartFrames[index] = (std::max)(0, startFrame);
|
||
animationFrameCounts[index] = (std::max)(1, frameCount);
|
||
}
|
||
}
|
||
|
||
CharacterUpdateEvents CharacterState::update(
|
||
std::int64_t deltaMs, const CharacterUpdateContext& context)
|
||
{
|
||
CharacterUpdateEvents events;
|
||
movedLastTick = false;
|
||
if (!enabled) return events;
|
||
bool navigationPausedThisTick = false;
|
||
|
||
const CharacterObservation* attackTarget = context.attackTarget;
|
||
const CharacterObservation* faceTarget = context.faceTarget;
|
||
if (!attackTarget || attackTarget->hp <= 0.f) {
|
||
battle_state = 0;
|
||
attack = 0;
|
||
attackTargetId.clear();
|
||
} else {
|
||
const fp distanceSquared = (attackTarget->position - position).normSquared();
|
||
if (distanceSquared >= visibleRange* visibleRange) {
|
||
if ((attackTarget->position - requestedWalkTarget).normSquared() >
|
||
TARGET_REPLAN_THRESHOLD_SQUARED) {
|
||
events.navigationRequested = true;
|
||
events.navigationTarget = attackTarget->position;
|
||
}
|
||
battle_state = 0;
|
||
} else if (distanceSquared >= static_cast<fp>(1.25f*1.25f)) {
|
||
if ((attackTarget->position - requestedWalkTarget).normSquared() > TARGET_REPLAN_THRESHOLD_SQUARED) {
|
||
events.navigationRequested = true;
|
||
events.navigationTarget = attackTarget->position;
|
||
}
|
||
battle_state = 0;
|
||
} else {
|
||
battle_state = 1;
|
||
velocity = Vec2fp(fp(0), fp(0));
|
||
}
|
||
}
|
||
|
||
const Vec3fp activeTarget = getCurrentNavigationTarget();
|
||
|
||
preferredVelocity = Vec2fp(fp(0), fp(0));
|
||
|
||
if (hp > 0.f && battle_state == 0 && !pathWaypoints.empty() &&
|
||
(currentState == AnimationState::WALK || currentState == AnimationState::STAND))
|
||
{
|
||
// Если мы уже достигли промежуточного waypoint'а, сразу переключаемся на следующий в этом же тике
|
||
while (!pathWaypoints.empty() && currentWaypointIndex + 1 < pathWaypoints.size()) {
|
||
Vec3fp diff = pathWaypoints[currentWaypointIndex] - position;
|
||
diff.y = fp(0);
|
||
if (diff.normSquared() <= kWalkThreshold_SQUARED()) {
|
||
++currentWaypointIndex;
|
||
}
|
||
else {
|
||
break;
|
||
}
|
||
}
|
||
|
||
const Vec3fp currentTarget = getCurrentNavigationTarget();
|
||
Vec3fp toTarget = currentTarget - position;
|
||
toTarget.y = fp(0);
|
||
const fp distanceSquared = toTarget.normSquared();
|
||
|
||
if (distanceSquared > kWalkThreshold_SQUARED()) {
|
||
const fp distance = fpm::sqrt(distanceSquared);
|
||
const fp dtSec = static_cast<fp>(deltaMs) / fp(1000);
|
||
|
||
fp desiredSpeed = distance / dtSec;
|
||
if (desiredSpeed > walkSpeed) {
|
||
desiredSpeed = walkSpeed;
|
||
}
|
||
|
||
preferredVelocity = Vec2fp(
|
||
(toTarget.x / distance) * desiredSpeed,
|
||
(toTarget.z / distance) * desiredSpeed
|
||
);
|
||
|
||
// Если стартуем с места — сразу целим поворот на точку,
|
||
// а в движении угол скорректирует Фаза 3 по реальному вектору ORCA (newVel)
|
||
if (velocity.lengthSq() <= static_cast<fp>(0.05f)) {
|
||
targetFacingAngle = fpm::atan2(toTarget.x, -toTarget.z);
|
||
}
|
||
}
|
||
else {
|
||
// Дошли до ФИНАЛЬНОЙ точки маршрута
|
||
velocity = Vec2fp(fp(0), fp(0));
|
||
if (currentState == AnimationState::WALK) {
|
||
currentState = AnimationState::STAND;
|
||
resetAnim = true;
|
||
}
|
||
const bool completedPath = !pathWaypoints.empty();
|
||
pathWaypoints.clear();
|
||
currentWaypointIndex = 0;
|
||
|
||
if (completedPath &&
|
||
(requestedWalkTarget - position).normSquared() > TARGET_REPLAN_THRESHOLD_SQUARED &&
|
||
!isPlayer) {
|
||
scheduleNextNavigationRetry();
|
||
}
|
||
if (completedPath && !onArrivedCallbackName.empty()) {
|
||
events.arrivedCallbackName = onArrivedCallbackName;
|
||
onArrivedCallbackName.clear();
|
||
}
|
||
if (faceTarget) {
|
||
Vec3fp toFace = faceTarget->position - position;
|
||
toFace.y = fp(0);
|
||
const fp distanceToFace = fpm::sqrt(toFace.normSquared());
|
||
targetFacingAngle = distanceToFace > static_cast<fp>(1e-3f)
|
||
? fpm::atan2(toFace.x / distanceToFace, -toFace.z / distanceToFace)
|
||
: facingAngle;
|
||
}
|
||
else if (!faceTargetId.empty()) {
|
||
faceTargetId.clear();
|
||
}
|
||
}
|
||
}
|
||
else if (hp > 0.f && battle_state == 0 && pathWaypoints.empty() &&
|
||
(currentState == AnimationState::WALK || currentState == AnimationState::STAND))
|
||
{
|
||
// Маршрута нет — стоим на месте и при необходимости поворачиваемся к faceTarget
|
||
velocity = Vec2fp(fp(0), fp(0));
|
||
if (currentState == AnimationState::WALK) {
|
||
currentState = AnimationState::STAND;
|
||
resetAnim = true;
|
||
}
|
||
if (faceTarget) {
|
||
Vec3fp toFace = faceTarget->position - position;
|
||
toFace.y = fp(0);
|
||
const fp distanceToFace = fpm::sqrt(toFace.normSquared());
|
||
targetFacingAngle = distanceToFace > static_cast<fp>(1e-3f)
|
||
? fpm::atan2(toFace.x / distanceToFace, -toFace.z / distanceToFace)
|
||
: facingAngle;
|
||
}
|
||
else if (!faceTargetId.empty()) {
|
||
faceTargetId.clear();
|
||
}
|
||
}
|
||
|
||
|
||
if (!isPlayer && !navigationPausedThisTick && pathWaypoints.empty() &&
|
||
(requestedWalkTarget - position).normSquared() > TARGET_REPLAN_THRESHOLD_SQUARED &&
|
||
!events.navigationRequested) {
|
||
if (navigationRetryTicks > 0) --navigationRetryTicks;
|
||
if (navigationRetryTicks == 0) {
|
||
events.navigationRequested = true;
|
||
events.forceNavigationReplan = true;
|
||
events.navigationTarget = requestedWalkTarget;
|
||
}
|
||
}
|
||
|
||
if (hp <= 0.f) {
|
||
if (currentState != AnimationState::ACTION_TO_DEATH &&
|
||
currentState != AnimationState::DEATH_IDLE) {
|
||
currentState = AnimationState::ACTION_TO_DEATH;
|
||
resetAnim = true;
|
||
attackTargetId.clear();
|
||
}
|
||
}
|
||
else if (battle_state == 1 && attackTarget) {
|
||
targetFacingAngle = fpm::atan2(
|
||
attackTarget->position.x - position.x, -(attackTarget->position.z - position.z));
|
||
|
||
if (currentState == AnimationState::STAND || currentState == AnimationState::WALK) {
|
||
currentState = AnimationState::STAND_TO_ACTION;
|
||
resetAnim = true;
|
||
}
|
||
if (canAttack && attack == 0 && attack_cooldown < 0.f &&
|
||
currentState == AnimationState::ACTION_IDLE) {
|
||
attack = 1;
|
||
attack_cooldown = ATTACK_COOLDOWN_TIME;
|
||
}
|
||
if (canAttack && attack_cooldown >= 0.f)
|
||
attack_cooldown -= static_cast<float>(deltaMs) / 1000.f;
|
||
if (attack == 1 && currentState == AnimationState::ACTION_IDLE &&
|
||
attackTarget->hp > 0.f) {
|
||
currentState = AnimationState::ACTION_ATTACK;
|
||
resetAnim = true;
|
||
}
|
||
} else if (currentState == AnimationState::STAND_TO_ACTION ||
|
||
currentState == AnimationState::ACTION_IDLE ||
|
||
currentState == AnimationState::ACTION_ATTACK) {
|
||
currentState = AnimationState::ACTION_TO_STAND;
|
||
resetAnim = true;
|
||
}
|
||
|
||
fp angleDiff = targetFacingAngle - facingAngle;
|
||
const fp PIfp = static_cast<fp>(PI);
|
||
while (angleDiff > PIfp) angleDiff -= static_cast<fp>(2.f) * PIfp;
|
||
while (angleDiff < -PIfp) angleDiff += static_cast<fp>(2.f) * PIfp;
|
||
const fp rotationStep = rotationSpeed * static_cast<fp>(deltaMs) / static_cast<fp>(1000.f);
|
||
if (fpm::abs(angleDiff) <= rotationStep) {
|
||
facingAngle = targetFacingAngle;
|
||
faceTargetId.clear();
|
||
} else {
|
||
facingAngle += angleDiff > static_cast<fp>(0.f) ? rotationStep : -rotationStep;
|
||
}
|
||
|
||
if (!isPlayer && hp > 0.f && battle_state == 0 && maxHomeDriftSquared > static_cast<fp>(0.f)) {
|
||
homeDriftCheckTimer -= static_cast<fp>(deltaMs) / static_cast<fp>(1000.f);
|
||
if (homeDriftCheckTimer <= static_cast <fp>(0.f)) {
|
||
homeDriftCheckTimer = HOME_DRIFT_CHECK_INTERVAL_FP;
|
||
Vec3fp toHome = homePosition - position;
|
||
toHome.y = static_cast<fp>(0.f);
|
||
if (!isMoving() && toHome.normSquared() > maxHomeDriftSquared) {
|
||
events.navigationRequested = true;
|
||
events.navigationTarget = homePosition;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (resetAnim) {
|
||
resetAnim = false;
|
||
const auto index = static_cast<std::size_t>(currentState);
|
||
animationFrame = index < animationStartFrames.size()
|
||
? static_cast<float>(animationStartFrames[index]) : 0.f;
|
||
}
|
||
const int previousFrame = static_cast<int>(animationFrame);
|
||
animationFrame += static_cast<float>(deltaMs) / 24.f;
|
||
|
||
if (previousFrame < 19 && static_cast<int>(animationFrame) >= 19 &&
|
||
(currentState == AnimationState::ACTION_ATTACK ||
|
||
currentState == AnimationState::ACTION_ATTACK_2) &&
|
||
attackTarget) {
|
||
events.attackImpact = true;
|
||
events.attackTargetId = attackTargetId;
|
||
events.attackDirection = attackTarget->position - position;
|
||
events.attackDirection.y = static_cast<fp>(0.f);
|
||
const fp lengthSquared = events.attackDirection.normSquared();
|
||
if (lengthSquared > static_cast<fp>(0.001f))
|
||
{
|
||
const fp length = fpm::sqrt(lengthSquared); //FP SQRT
|
||
|
||
events.attackDirection = events.attackDirection / length;
|
||
}
|
||
else
|
||
{
|
||
events.attackDirection = Vec3fp{};
|
||
}
|
||
}
|
||
|
||
showWeapon =
|
||
(static_cast<int>(animationFrame) >= 20 && currentState == AnimationState::STAND_TO_ACTION) ||
|
||
(static_cast<int>(animationFrame) <= 32 && currentState == AnimationState::ACTION_TO_STAND) ||
|
||
currentState == AnimationState::ACTION_ATTACK_2 ||
|
||
currentState == AnimationState::ACTION_ATTACK ||
|
||
currentState == AnimationState::ACTION_IDLE;
|
||
|
||
const auto animationIndex = static_cast<std::size_t>(currentState);
|
||
const int frameCount = animationIndex < animationFrameCounts.size()
|
||
? animationFrameCounts[animationIndex] : 1;
|
||
if (frameCount > 1 && static_cast<int>(animationFrame) >= frameCount - 1) {
|
||
if (currentState == AnimationState::STAND_TO_ACTION)
|
||
currentState = AnimationState::ACTION_IDLE;
|
||
else if (currentState == AnimationState::ACTION_TO_STAND)
|
||
currentState = AnimationState::STAND;
|
||
else if (currentState == AnimationState::ACTION_ATTACK ||
|
||
currentState == AnimationState::ACTION_ATTACK_2) {
|
||
currentState = AnimationState::ACTION_IDLE;
|
||
attack = 0;
|
||
} else if (currentState == AnimationState::ACTION_TO_DEATH) {
|
||
currentState = AnimationState::DEATH_IDLE;
|
||
events.deathAnimationComplete = true;
|
||
}
|
||
const auto nextIndex = static_cast<std::size_t>(currentState);
|
||
animationFrame = nextIndex < animationStartFrames.size()
|
||
? static_cast<float>(animationStartFrames[nextIndex]) : 0.f;
|
||
}
|
||
|
||
return events;
|
||
}
|
||
|
||
void CharacterState::save(nlohmann::json& out) const
|
||
{
|
||
out["position"] = { static_cast<double>(position.x), static_cast<double>(position.y), static_cast<double>(position.z) };
|
||
out["facingAngle"] = static_cast<double>(facingAngle);
|
||
out["targetFacingAngle"] = static_cast<double>(targetFacingAngle);
|
||
out["hp"] = hp;
|
||
out["enabled"] = enabled;
|
||
out["battle_state"] = battle_state;
|
||
out["currentState"] = static_cast<int>(currentState);
|
||
out["attack_cooldown"] = attack_cooldown;
|
||
out["showWeapon"] = showWeapon;
|
||
out["attackTargetId"] = attackTargetId;
|
||
out["faceTargetId"] = faceTargetId;
|
||
out["homePosition"] = { static_cast<double>(homePosition.x), static_cast<double>(homePosition.y), static_cast<double>(homePosition.z) };
|
||
out["walkTarget"] = { static_cast<double>(walkTarget.x), static_cast<double>(walkTarget.y), static_cast<double>(walkTarget.z) };
|
||
out["requestedWalkTarget"] = { static_cast<double>(requestedWalkTarget.x), static_cast<double>(requestedWalkTarget.y), static_cast<double>(requestedWalkTarget.z) };
|
||
nlohmann::json wpArr = nlohmann::json::array();
|
||
for (const auto& wp : pathWaypoints) {
|
||
wpArr.push_back({ static_cast<double>(wp.x), static_cast<double>(wp.y), static_cast<double>(wp.z) });
|
||
}
|
||
out["pathWaypoints"] = std::move(wpArr);
|
||
out["currentWaypointIndex"] = currentWaypointIndex;
|
||
out["homeDriftCheckTimer"] = static_cast<double>(homeDriftCheckTimer);
|
||
out["movementBlockedTicks"] = movementBlockedTicks;
|
||
out["navigationRetryTicks"] = navigationRetryTicks;
|
||
out["movedLastTick"] = movedLastTick;
|
||
out["navigationRetryCooldownTicks"] = navigationRetryCooldownTicks;
|
||
out["navigationRetryCooldownIndex"] = navigationRetryCooldownIndex;
|
||
|
||
out["animationFrame"] = animationFrame;
|
||
out["visibleRange"] = static_cast<double>(visibleRange);
|
||
if (!onArrivedCallbackName.empty())
|
||
out["onArrivedCallbackName"] = onArrivedCallbackName;
|
||
}
|
||
|
||
void CharacterState::load(const nlohmann::json& in)
|
||
{
|
||
auto readVec3 = [](const nlohmann::json& j, Eigen::Vector3f def) -> Eigen::Vector3f {
|
||
if (j.is_array() && j.size() == 3)
|
||
return { j[0].get<float>(), j[1].get<float>(), j[2].get<float>() };
|
||
return def;
|
||
};
|
||
|
||
auto readVec3fp = [](const nlohmann::json& j, Vec3fp def) -> Vec3fp {
|
||
if (j.is_array() && j.size() == 3)
|
||
return Vec3fp{ static_cast<fp>(j[0].get<double>()), static_cast<fp>(j[1].get<double>()), static_cast<fp>(j[2].get<double>()) };
|
||
return def;
|
||
};
|
||
|
||
if (in.contains("position")) {
|
||
position = readVec3fp(in["position"], position);
|
||
}
|
||
facingAngle = static_cast<fp>(in.value("facingAngle", static_cast<double>(facingAngle)));
|
||
targetFacingAngle = static_cast<fp>(in.value("targetFacingAngle", static_cast<double>(targetFacingAngle)));
|
||
hp = in.value("hp", hp);
|
||
visibleRange = static_cast<fp>(in.value("visibleRange", static_cast<double>(visibleRange)));
|
||
enabled = in.value("enabled", enabled);
|
||
battle_state = in.value("battle_state", battle_state);
|
||
currentState = static_cast<AnimationState>(in.value("currentState", static_cast<int>(currentState)));
|
||
attack_cooldown = in.value("attack_cooldown", attack_cooldown);
|
||
showWeapon = in.value("showWeapon", showWeapon);
|
||
attackTargetId = in.value("attackTargetId", attackTargetId);
|
||
faceTargetId = in.value("faceTargetId", faceTargetId);
|
||
if (in.contains("homePosition")) homePosition = readVec3fp(in["homePosition"], homePosition);
|
||
if (in.contains("walkTarget")) walkTarget = readVec3fp(in["walkTarget"], walkTarget);
|
||
if (in.contains("requestedWalkTarget")) requestedWalkTarget = readVec3fp(in["requestedWalkTarget"], requestedWalkTarget);
|
||
pathWaypoints.clear();
|
||
if (in.contains("pathWaypoints") && in["pathWaypoints"].is_array()) {
|
||
for (const auto& wp : in["pathWaypoints"]) {
|
||
pathWaypoints.push_back(readVec3fp(wp, Vec3fp()));
|
||
}
|
||
}
|
||
currentWaypointIndex = in.value("currentWaypointIndex", currentWaypointIndex);
|
||
homeDriftCheckTimer = static_cast<fp>(in.value("homeDriftCheckTimer", static_cast<double>(homeDriftCheckTimer)));
|
||
movementBlockedTicks = in.value("movementBlockedTicks", movementBlockedTicks);
|
||
navigationRetryTicks = in.value("navigationRetryTicks", navigationRetryTicks);
|
||
movedLastTick = in.value("movedLastTick", movedLastTick);
|
||
navigationRetryCooldownTicks = in.value(
|
||
"navigationRetryCooldownTicks", navigationRetryCooldownTicks);
|
||
navigationRetryCooldownIndex = in.value(
|
||
"navigationRetryCooldownIndex", navigationRetryCooldownIndex);
|
||
if (!navigationRetryCooldownTicks.empty())
|
||
navigationRetryCooldownIndex %= navigationRetryCooldownTicks.size();
|
||
animationFrame = in.value("animationFrame", animationFrame);
|
||
onArrivedCallbackName = in.value("onArrivedCallbackName", std::string());
|
||
}
|
||
|
||
} // namespace FRG
|