Files
shadow-over-bishkek001/src/CharacterState.cpp
T

582 lines
24 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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