#include "CharacterState.h" #include "LocationState.h" #include #include 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(HOME_DRIFT_CHECK_INTERVAL); constexpr float TARGET_REPLAN_THRESHOLD = 0.25f; fp TARGET_REPLAN_THRESHOLD_SQUARED = static_cast(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(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 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(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(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(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(location.navigationMaps.size())) { walkTarget = Vec3fp(position.x, static_cast(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(0.f); } walkTarget = pathWaypoints.back(); return; } walkTarget = Vec3fp(position.x, static_cast(0.f), position.z); if (!isPlayer) scheduleNextNavigationRetry(); } void CharacterState::forceReplan( const LocationNetworkState& location, const std::string& entityId) { const Vec3fp normalizedTarget( requestedWalkTarget.x, static_cast(0.f), requestedWalkTarget.z); if (location.activeNavigationIndex < 0 || location.activeNavigationIndex >= static_cast(location.navigationMaps.size())) { walkTarget = Vec3fp(position.x, static_cast(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(0.f); } walkTarget = pathWaypoints.back(); return; } walkTarget = Vec3fp(position.x, static_cast(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(animationFrame) < 20 || (currentState != AnimationState::ACTION_ATTACK && currentState != AnimationState::ACTION_ATTACK_2)) { return; } const auto index = static_cast(currentState); animationFrame = index < animationStartFrames.size() ? static_cast(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(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(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(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(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(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(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(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(PI); while (angleDiff > PIfp) angleDiff -= static_cast(2.f) * PIfp; while (angleDiff < -PIfp) angleDiff += static_cast(2.f) * PIfp; const fp rotationStep = rotationSpeed * static_cast(deltaMs) / static_cast(1000.f); if (fpm::abs(angleDiff) <= rotationStep) { facingAngle = targetFacingAngle; faceTargetId.clear(); } else { facingAngle += angleDiff > static_cast(0.f) ? rotationStep : -rotationStep; } if (!isPlayer && hp > 0.f && battle_state == 0 && maxHomeDriftSquared > static_cast(0.f)) { homeDriftCheckTimer -= static_cast(deltaMs) / static_cast(1000.f); if (homeDriftCheckTimer <= static_cast (0.f)) { homeDriftCheckTimer = HOME_DRIFT_CHECK_INTERVAL_FP; Vec3fp toHome = homePosition - position; toHome.y = static_cast(0.f); if (!isMoving() && toHome.normSquared() > maxHomeDriftSquared) { events.navigationRequested = true; events.navigationTarget = homePosition; } } } if (resetAnim) { resetAnim = false; const auto index = static_cast(currentState); animationFrame = index < animationStartFrames.size() ? static_cast(animationStartFrames[index]) : 0.f; } const int previousFrame = static_cast(animationFrame); animationFrame += static_cast(deltaMs) / 24.f; if (previousFrame < 19 && static_cast(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(0.f); const fp lengthSquared = events.attackDirection.normSquared(); if (lengthSquared > static_cast(0.001f)) { const fp length = fpm::sqrt(lengthSquared); //FP SQRT events.attackDirection = events.attackDirection / length; } else { events.attackDirection = Vec3fp{}; } } showWeapon = (static_cast(animationFrame) >= 20 && currentState == AnimationState::STAND_TO_ACTION) || (static_cast(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(currentState); const int frameCount = animationIndex < animationFrameCounts.size() ? animationFrameCounts[animationIndex] : 1; if (frameCount > 1 && static_cast(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(currentState); animationFrame = nextIndex < animationStartFrames.size() ? static_cast(animationStartFrames[nextIndex]) : 0.f; } return events; } void CharacterState::save(nlohmann::json& out) const { out["position"] = { static_cast(position.x), static_cast(position.y), static_cast(position.z) }; out["facingAngle"] = static_cast(facingAngle); out["targetFacingAngle"] = static_cast(targetFacingAngle); out["hp"] = hp; out["enabled"] = enabled; out["battle_state"] = battle_state; out["currentState"] = static_cast(currentState); out["attack_cooldown"] = attack_cooldown; out["showWeapon"] = showWeapon; out["attackTargetId"] = attackTargetId; out["faceTargetId"] = faceTargetId; out["homePosition"] = { static_cast(homePosition.x), static_cast(homePosition.y), static_cast(homePosition.z) }; out["walkTarget"] = { static_cast(walkTarget.x), static_cast(walkTarget.y), static_cast(walkTarget.z) }; out["requestedWalkTarget"] = { static_cast(requestedWalkTarget.x), static_cast(requestedWalkTarget.y), static_cast(requestedWalkTarget.z) }; nlohmann::json wpArr = nlohmann::json::array(); for (const auto& wp : pathWaypoints) { wpArr.push_back({ static_cast(wp.x), static_cast(wp.y), static_cast(wp.z) }); } out["pathWaypoints"] = std::move(wpArr); out["currentWaypointIndex"] = currentWaypointIndex; out["homeDriftCheckTimer"] = static_cast(homeDriftCheckTimer); out["movementBlockedTicks"] = movementBlockedTicks; out["navigationRetryTicks"] = navigationRetryTicks; out["movedLastTick"] = movedLastTick; out["navigationRetryCooldownTicks"] = navigationRetryCooldownTicks; out["navigationRetryCooldownIndex"] = navigationRetryCooldownIndex; out["animationFrame"] = animationFrame; out["visibleRange"] = static_cast(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(), j[1].get(), j[2].get() }; return def; }; auto readVec3fp = [](const nlohmann::json& j, Vec3fp def) -> Vec3fp { if (j.is_array() && j.size() == 3) return Vec3fp{ static_cast(j[0].get()), static_cast(j[1].get()), static_cast(j[2].get()) }; return def; }; if (in.contains("position")) { position = readVec3fp(in["position"], position); } facingAngle = static_cast(in.value("facingAngle", static_cast(facingAngle))); targetFacingAngle = static_cast(in.value("targetFacingAngle", static_cast(targetFacingAngle))); hp = in.value("hp", hp); visibleRange = static_cast(in.value("visibleRange", static_cast(visibleRange))); enabled = in.value("enabled", enabled); battle_state = in.value("battle_state", battle_state); currentState = static_cast(in.value("currentState", static_cast(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(in.value("homeDriftCheckTimer", static_cast(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