mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-07-30 17:29:14 -04:00
Funnel every ship-facing path through TransportManager::transportModelBowOffset (facing = direction of travel + fixed per-model bow offset), replacing the scattered per-entry 180-degree hull corrections. berthRunsParallel() names the side-on dock routes, and buildTaxiSegmentSpline is extracted static/testable so the cyclic-wrap behavior is unit-covered without a DBC.
413 lines
16 KiB
C++
413 lines
16 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.dockYaw = 0.0f;
|
|
t.hasDockYaw = 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: world-coordinate WMO faces along the server-space route", "[transport_animator][transport]") {
|
|
TransportAnimator animator;
|
|
CatmullRomSpline spline({
|
|
{0, glm::vec3(0.0f, 0.0f, 0.0f)},
|
|
{1000, glm::vec3(10.0f, 0.0f, 0.0f)},
|
|
{2000, glm::vec3(20.0f, 0.0f, 0.0f)},
|
|
});
|
|
PathEntry path(std::move(spline), 300, false, true, true);
|
|
auto t = makeTransport();
|
|
t.basePosition = glm::vec3(0.0f);
|
|
t.isM2 = false;
|
|
|
|
animator.evaluateAndApply(t, path, 500);
|
|
|
|
// canonical +X is server +Y; model-local +X is the ship's forward axis.
|
|
const glm::quat expected = glm::angleAxis(
|
|
glm::half_pi<float>(), 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: exact ship dwell restores authored dock facing", "[transport_animator][transport]") {
|
|
TransportAnimator animator;
|
|
CatmullRomSpline spline({
|
|
{0, glm::vec3(-10.0f, 0.0f, 0.0f)},
|
|
{1000, glm::vec3(0.0f, 0.0f, 0.0f)},
|
|
{61000, glm::vec3(0.0f, 0.0f, 0.0f)},
|
|
{62000, glm::vec3(10.0f, 0.0f, 0.0f)},
|
|
});
|
|
PathEntry path(std::move(spline), 301, false, true, true);
|
|
auto t = makeTransport();
|
|
t.basePosition = glm::vec3(0.0f);
|
|
t.rotation = glm::angleAxis(0.75f, glm::vec3(0.0f, 0.0f, 1.0f));
|
|
t.dockYaw = -0.4f;
|
|
t.hasDockYaw = true;
|
|
|
|
animator.evaluateAndApply(t, path, 30000);
|
|
|
|
REQUIRE(t.position == glm::vec3(0.0f));
|
|
const glm::quat expected = glm::angleAxis(
|
|
t.dockYaw, glm::vec3(0.0f, 0.0f, 1.0f));
|
|
REQUIRE(t.rotation.w == Catch::Approx(expected.w));
|
|
REQUIRE(t.rotation.z == Catch::Approx(expected.z));
|
|
}
|
|
|
|
TEST_CASE("Animator: Bravery holds side-on at its dock dwell", "[transport_animator][transport]") {
|
|
TransportAnimator animator;
|
|
CatmullRomSpline spline({
|
|
{0, glm::vec3(0.0f, 0.0f, 0.0f)},
|
|
{10000, glm::vec3(100.0f, 0.0f, 0.0f)},
|
|
{70000, glm::vec3(100.0f, 0.0f, 0.0f)},
|
|
{80000, glm::vec3(200.0f, 0.0f, 0.0f)},
|
|
});
|
|
PathEntry path(std::move(spline), 176310u, false, true, true);
|
|
auto t = makeTransport(1, 176310u);
|
|
t.entry = 176310u;
|
|
t.displayId = 3015u; // Bravery-class hull: authored bow-forward, no +PI correction
|
|
t.basePosition = glm::vec3(0.0f);
|
|
t.isM2 = false;
|
|
// Deliberately unrelated live server yaw. Docking must not depend on the
|
|
// orientation snapshot received when this transport happened to load.
|
|
t.dockYaw = glm::pi<float>();
|
|
t.hasDockYaw = true;
|
|
|
|
animator.evaluateAndApply(t, path, 30000);
|
|
|
|
// Keep the authored pier node so the gangway reaches the dock.
|
|
REQUIRE(t.position.x == Catch::Approx(100.0f));
|
|
REQUIRE(t.position.y == Catch::Approx(0.0f));
|
|
// Model 3015 is bow-forward, so route yaw is PI/2 (no hull correction). Bravery's
|
|
// TaxiPath runs parallel to the pier, so retaining that heading stops it broadside.
|
|
const glm::quat expectedDock = glm::angleAxis(
|
|
glm::half_pi<float>(),
|
|
glm::vec3(0.0f, 0.0f, 1.0f));
|
|
REQUIRE(t.rotation.w == Catch::Approx(expectedDock.w).margin(0.001f));
|
|
REQUIRE(t.rotation.z == Catch::Approx(expectedDock.z).margin(0.001f));
|
|
|
|
t.dockYaw = -0.35f;
|
|
t.hasDockYaw = false;
|
|
animator.evaluateAndApply(t, path, 30000);
|
|
REQUIRE(t.rotation.w == Catch::Approx(expectedDock.w).margin(0.001f));
|
|
REQUIRE(t.rotation.z == Catch::Approx(expectedDock.z).margin(0.001f));
|
|
}
|
|
|
|
TEST_CASE("Animator: affected ship hulls face their direction of travel",
|
|
"[transport_animator][transport]") {
|
|
// The hull bow correction is keyed by MODEL (displayId), not entry. Models 7087 and
|
|
// 7446 are bow-reversed (+PI); model 3015 (Bravery) is bow-forward (no correction).
|
|
struct ShipCase { uint32_t entry; uint32_t displayId; bool bowReversed; };
|
|
for (const ShipCase& sc : {
|
|
ShipCase{176310u, 3015u, false}, // The Bravery
|
|
ShipCase{176244u, 7087u, true}, // The Moonspray
|
|
ShipCase{181646u, 7087u, true}, // Elune's Blessing
|
|
ShipCase{190536u, 7446u, true}, // Kraken-class icebreaker
|
|
}) {
|
|
TransportAnimator animator;
|
|
CatmullRomSpline spline({
|
|
{0, glm::vec3(0.0f, 0.0f, 0.0f)},
|
|
{1000, glm::vec3(10.0f, 0.0f, 0.0f)},
|
|
{2000, glm::vec3(20.0f, 0.0f, 0.0f)},
|
|
});
|
|
PathEntry path(std::move(spline), sc.entry, false, false, true);
|
|
auto t = makeTransport(1, sc.entry);
|
|
t.entry = sc.entry;
|
|
t.displayId = sc.displayId;
|
|
t.basePosition = glm::vec3(0.0f);
|
|
t.isM2 = false;
|
|
|
|
animator.evaluateAndApply(t, path, 500);
|
|
|
|
const float expectedYaw = glm::half_pi<float>() +
|
|
(sc.bowReversed ? glm::pi<float>() : 0.0f);
|
|
const glm::quat expected = glm::angleAxis(
|
|
expectedYaw, glm::vec3(0.0f, 0.0f, 1.0f));
|
|
INFO("entry=" << sc.entry << " displayId=" << sc.displayId);
|
|
REQUIRE(t.rotation.w == Catch::Approx(expected.w).margin(0.001f));
|
|
REQUIRE(t.rotation.z == Catch::Approx(expected.z).margin(0.001f));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Animator: Kraken retains arrival heading throughout dock dwell",
|
|
"[transport_animator][transport]") {
|
|
TransportAnimator animator;
|
|
CatmullRomSpline spline({
|
|
{0, glm::vec3(0.0f, 0.0f, 0.0f)},
|
|
{10000, glm::vec3(100.0f, 0.0f, 0.0f)},
|
|
{70000, glm::vec3(100.0f, 0.0f, 0.0f)},
|
|
{80000, glm::vec3(200.0f, 0.0f, 0.0f)},
|
|
});
|
|
PathEntry path(std::move(spline), 190536u, false, true, true);
|
|
auto t = makeTransport(1, 190536u);
|
|
t.entry = 190536u;
|
|
t.displayId = 7446u; // Icebreaker hull: excluded from spawn-yaw restore at dwell
|
|
t.basePosition = glm::vec3(0.0f);
|
|
t.isM2 = false;
|
|
t.dockYaw = glm::half_pi<float>();
|
|
t.hasDockYaw = true;
|
|
|
|
animator.evaluateAndApply(t, path, 5000);
|
|
const glm::quat arrivalRotation = t.rotation;
|
|
animator.evaluateAndApply(t, path, 30000);
|
|
|
|
REQUIRE(t.rotation.w == Catch::Approx(arrivalRotation.w).margin(0.001f));
|
|
REQUIRE(t.rotation.z == Catch::Approx(arrivalRotation.z).margin(0.001f));
|
|
}
|
|
|
|
TEST_CASE("Animator: Moonspray holds side-on at its dock dwell", "[transport_animator][transport]") {
|
|
TransportAnimator animator;
|
|
CatmullRomSpline spline({
|
|
{0, glm::vec3(0.0f, 0.0f, 0.0f)},
|
|
{10000, glm::vec3(100.0f, 0.0f, 0.0f)},
|
|
{70000, glm::vec3(100.0f, 0.0f, 0.0f)},
|
|
{80000, glm::vec3(200.0f, 0.0f, 0.0f)},
|
|
});
|
|
PathEntry path(std::move(spline), 176244u, false, true, true);
|
|
auto t = makeTransport(1, 176244u);
|
|
t.entry = 176244u;
|
|
t.displayId = 7087u;
|
|
t.basePosition = glm::vec3(0.0f);
|
|
t.isM2 = false;
|
|
// Deliberately wrong bow-first server yaw: Moonspray keeps the berth-parallel route yaw.
|
|
t.dockYaw = glm::pi<float>() - 0.1f;
|
|
t.hasDockYaw = true;
|
|
|
|
animator.evaluateAndApply(t, path, 30000);
|
|
|
|
REQUIRE(t.position.x == Catch::Approx(100.0f));
|
|
REQUIRE(t.position.y == Catch::Approx(0.0f));
|
|
const glm::quat expectedDock = glm::angleAxis(
|
|
glm::half_pi<float>() + glm::pi<float>(),
|
|
glm::vec3(0.0f, 0.0f, 1.0f));
|
|
REQUIRE(t.rotation.w == Catch::Approx(expectedDock.w).margin(0.001f));
|
|
REQUIRE(t.rotation.z == Catch::Approx(expectedDock.z).margin(0.001f));
|
|
}
|
|
|
|
TEST_CASE("Animator: Elune's Blessing holds side-on at its dock dwell", "[transport_animator][transport]") {
|
|
TransportAnimator animator;
|
|
CatmullRomSpline spline({
|
|
{0, glm::vec3(0.0f, 0.0f, 0.0f)},
|
|
{10000, glm::vec3(100.0f, 0.0f, 0.0f)},
|
|
{70000, glm::vec3(100.0f, 0.0f, 0.0f)},
|
|
{80000, glm::vec3(200.0f, 0.0f, 0.0f)},
|
|
});
|
|
PathEntry path(std::move(spline), 181646u, false, true, true);
|
|
auto t = makeTransport(1, 181646u);
|
|
t.entry = 181646u; // Elune's Blessing
|
|
t.displayId = 7087u; // Same model family as Moonspray
|
|
t.basePosition = glm::vec3(0.0f);
|
|
t.isM2 = false;
|
|
t.dockYaw = glm::pi<float>();
|
|
t.hasDockYaw = true;
|
|
|
|
animator.evaluateAndApply(t, path, 30000);
|
|
|
|
const glm::quat expectedDock = glm::angleAxis(
|
|
glm::half_pi<float>() + glm::pi<float>(),
|
|
glm::vec3(0.0f, 0.0f, 1.0f));
|
|
REQUIRE(t.rotation.w == Catch::Approx(expectedDock.w).margin(0.001f));
|
|
REQUIRE(t.rotation.z == Catch::Approx(expectedDock.z).margin(0.001f));
|
|
}
|
|
|
|
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));
|
|
}
|