Kelsidavis-WoWee/tests/test_transport_components.cpp
Pavel Okhlopkov 39719cac82 refactor: decompose TransportManager and upgrade Entity to CatmullRom splines
TransportManager decomposition:
- Extract TransportClockSync: server clock offset, yaw flip detection,
  velocity bootstrap, client/server mode switching
- Extract TransportAnimator: spline evaluation, Z clamping, orientation
  from server yaw or spline tangent
- Slim TransportManager to thin orchestrator delegating to ClockSync and
  Animator; add pushTransform() helper to deduplicate WMO/M2 renderer calls
- Remove legacy orientationFromSplineTangent (now uses
  CatmullRomSpline::orientationFromTangent)

Entity path following upgrade:
- Replace pathPoints_/pathSegDists_ linear lerp with
  std::optional<CatmullRomSpline> activeSpline_
- startMoveAlongPath builds SplineKeys with distance-proportional timing
- updateMovement evaluates CatmullRomSpline for smooth Catmull-Rom
  interpolation matching server-side creature movement
- Reset activeSpline_ on setPosition/startMoveTo to prevent stale state

Tests:
- Add test_transport_components (9 cases): ClockSync client/server/reverse
  modes, yaw flip detection, Animator position eval, server yaw, Z clamping
- Link spline.cpp into test_entity for CatmullRomSpline dependency

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-11 09:50:38 +03:00

209 lines
8 KiB
C++

// tests/test_transport_components.cpp
// Unit tests for TransportClockSync and TransportAnimator (Phase 3 extractions).
#include <catch_amalgamated.hpp>
#include "game/transport_clock_sync.hpp"
#include "game/transport_animator.hpp"
#include "game/transport_manager.hpp"
#include "game/transport_path_repository.hpp"
#include "math/spline.hpp"
#include <glm/gtc/constants.hpp>
#include <cmath>
using namespace wowee::game;
using namespace wowee::math;
// ── Helper: build a simple circular path ──────────────────────────
static PathEntry makeCirclePath() {
// Circle-ish path with 4 points, 4000ms duration
std::vector<SplineKey> keys = {
{0, glm::vec3(0.0f, 0.0f, 0.0f)},
{1000, glm::vec3(10.0f, 0.0f, 0.0f)},
{2000, glm::vec3(10.0f, 10.0f, 0.0f)},
{3000, glm::vec3(0.0f, 10.0f, 0.0f)},
{4000, glm::vec3(0.0f, 0.0f, 0.0f)},
};
CatmullRomSpline spline(std::move(keys), /*timeClosed=*/true);
return PathEntry(std::move(spline), /*pathId=*/100, /*zOnly=*/false, /*fromDBC=*/true, /*worldCoords=*/false);
}
// ── Helper: create a fresh ActiveTransport ────────────────────────
static ActiveTransport makeTransport(uint64_t guid = 1, uint32_t pathId = 100) {
ActiveTransport t{};
t.guid = guid;
t.pathId = pathId;
t.basePosition = glm::vec3(100.0f, 200.0f, 0.0f);
t.position = glm::vec3(100.0f, 200.0f, 0.0f);
t.rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f);
t.playerOnBoard = false;
t.playerLocalOffset = glm::vec3(0);
t.hasDeckBounds = false;
t.localClockMs = 0;
t.hasServerClock = false;
t.serverClockOffsetMs = 0;
t.useClientAnimation = true;
t.clientAnimationReverse = false;
t.serverYaw = 0.0f;
t.hasServerYaw = false;
t.serverYawFlipped180 = false;
t.serverYawAlignmentScore = 0;
t.lastServerUpdate = 0.0;
t.serverUpdateCount = 0;
t.serverLinearVelocity = glm::vec3(0);
t.serverAngularVelocity = 0.0f;
t.hasServerVelocity = false;
t.allowBootstrapVelocity = true;
t.isM2 = false;
return t;
}
// ══════════════════════════════════════════════════════════════════
// TransportClockSync tests
// ══════════════════════════════════════════════════════════════════
TEST_CASE("ClockSync: client animation advances localClockMs", "[transport_clock_sync]") {
TransportClockSync sync;
auto path = makeCirclePath();
auto t = makeTransport();
t.useClientAnimation = true;
t.hasServerClock = false;
uint32_t pathTimeMs = 0;
bool result = sync.computePathTime(t, path.spline, 1.0, 0.016f, pathTimeMs);
REQUIRE(result);
REQUIRE(t.localClockMs > 0); // Should have advanced
REQUIRE(pathTimeMs == t.localClockMs % path.spline.durationMs());
}
TEST_CASE("ClockSync: server clock mode wraps correctly", "[transport_clock_sync]") {
TransportClockSync sync;
auto path = makeCirclePath();
auto t = makeTransport();
t.hasServerClock = true;
t.serverClockOffsetMs = 500; // Server is 500ms ahead
uint32_t pathTimeMs = 0;
double elapsedTime = 3.7; // 3700ms local → 4200ms server → 200ms wrapped (dur=4000)
bool result = sync.computePathTime(t, path.spline, elapsedTime, 0.016f, pathTimeMs);
REQUIRE(result);
REQUIRE(pathTimeMs == 200);
}
TEST_CASE("ClockSync: strict server mode returns false", "[transport_clock_sync]") {
TransportClockSync sync;
auto path = makeCirclePath();
auto t = makeTransport();
t.useClientAnimation = false;
t.hasServerClock = false;
uint32_t pathTimeMs = 0;
bool result = sync.computePathTime(t, path.spline, 1.0, 0.016f, pathTimeMs);
REQUIRE_FALSE(result);
}
TEST_CASE("ClockSync: reverse client animation decrements", "[transport_clock_sync]") {
TransportClockSync sync;
auto path = makeCirclePath();
auto t = makeTransport();
t.useClientAnimation = true;
t.clientAnimationReverse = true;
t.localClockMs = 2000;
uint32_t pathTimeMs = 0;
bool result = sync.computePathTime(t, path.spline, 1.0, 0.5f, pathTimeMs);
REQUIRE(result);
// localClockMs should have decreased by ~500ms
REQUIRE(t.localClockMs < 2000);
}
TEST_CASE("ClockSync: processServerUpdate sets yaw and rotation", "[transport_clock_sync]") {
TransportClockSync sync;
auto path = makeCirclePath();
auto t = makeTransport();
glm::vec3 pos(105.0f, 205.0f, 1.0f);
float yaw = 1.5f;
sync.processServerUpdate(t, &path, pos, yaw, 10.0);
REQUIRE(t.serverUpdateCount == 1);
REQUIRE(t.hasServerYaw);
REQUIRE(t.serverYaw == Catch::Approx(1.5f));
REQUIRE(t.position == pos);
}
TEST_CASE("ClockSync: yaw flip detection after repeated misaligned updates", "[transport_clock_sync]") {
TransportClockSync sync;
auto path = makeCirclePath();
auto t = makeTransport();
t.useClientAnimation = false;
// Simulate transport moving east (+X) but reporting yaw pointing west (pi)
float westYaw = glm::pi<float>();
glm::vec3 pos(100.0f, 200.0f, 0.0f);
sync.processServerUpdate(t, &path, pos, westYaw, 1.0);
// Send several updates moving east with west-facing yaw
for (int i = 1; i <= 8; i++) {
pos.x += 5.0f;
sync.processServerUpdate(t, &path, pos, westYaw, 1.0 + i * 0.5);
}
// After enough misaligned updates, should have flipped
REQUIRE(t.serverYawFlipped180);
}
// ══════════════════════════════════════════════════════════════════
// TransportAnimator tests
// ══════════════════════════════════════════════════════════════════
TEST_CASE("Animator: evaluateAndApply updates position from spline", "[transport_animator]") {
TransportAnimator animator;
auto path = makeCirclePath();
auto t = makeTransport();
t.hasServerYaw = false;
animator.evaluateAndApply(t, path, 0);
// At t=0, path offset is (0,0,0), so pos = base + (0,0,0) = (100,200,0)
REQUIRE(t.position.x == Catch::Approx(100.0f));
REQUIRE(t.position.y == Catch::Approx(200.0f));
animator.evaluateAndApply(t, path, 1000);
// At t=1000, path offset is (10,0,0), so pos = base + (10,0,0) = (110,200,0)
REQUIRE(t.position.x == Catch::Approx(110.0f));
}
TEST_CASE("Animator: uses server yaw when available", "[transport_animator]") {
TransportAnimator animator;
auto path = makeCirclePath();
auto t = makeTransport();
t.hasServerYaw = true;
t.serverYaw = 1.0f;
t.serverYawFlipped180 = false;
animator.evaluateAndApply(t, path, 500);
// Rotation should be based on serverYaw=1.0, not spline tangent
float expectedYaw = 1.0f;
glm::quat expected = glm::angleAxis(expectedYaw, glm::vec3(0.0f, 0.0f, 1.0f));
REQUIRE(t.rotation.w == Catch::Approx(expected.w).margin(0.01f));
REQUIRE(t.rotation.z == Catch::Approx(expected.z).margin(0.01f));
}
TEST_CASE("Animator: Z clamping on non-world-coord client anim", "[transport_animator]") {
TransportAnimator animator;
// Build a path with a deep negative Z offset
std::vector<SplineKey> keys = {
{0, glm::vec3(0.0f, 0.0f, 0.0f)},
{1000, glm::vec3(5.0f, 0.0f, -50.0f)}, // Deep negative Z
{2000, glm::vec3(10.0f, 0.0f, 0.0f)},
};
CatmullRomSpline spline(std::move(keys), false);
PathEntry path(std::move(spline), 200, false, true, false);
auto t = makeTransport();
t.useClientAnimation = true;
t.serverUpdateCount = 0; // <= 1, so Z clamping applies
animator.evaluateAndApply(t, path, 1000);
// Z should be clamped to >= -2.0 (kMinFallbackZOffset)
REQUIRE(t.position.z >= (t.basePosition.z - 2.0f));
}