mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-08-03 03:23:35 -04:00
The conversion between the renderer's character yaw and the game side's canonical yaw was written as 180 - c. It should be c + 90, which is not a choice but a consequence: canonicalToRender swaps x and y, and canonical yaw is atan2(-dy, dx), so a direction at canonical yaw c has render components (-sin c, cos c) and a render yaw of c + 90. A mirror and a rotation agree at exactly one heading. Every user of the pair was wrong in the same way, so nothing looked out of place while values only moved between renderer and game. It showed when one crossed to the server: orientation. The arc checks were run against a heading that was mirrored, which is why Smite reported a target as not in front while the character plainly faced it, and why casting appeared to face the wrong way once the combat auto-turn was routed through the same conversion. Correcting the pair puts the auto-turn back where it was — going through canonical now produces exactly the render angle it computed directly — and sends the server a heading that matches what is drawn. The test derives the expected render yaw from the direction vector rather than restating the formula, so it fails if the two conventions ever disagree again.
84 lines
4.1 KiB
C++
84 lines
4.1 KiB
C++
#include <catch_amalgamated.hpp>
|
|
#include "rendering/movement_limits.hpp"
|
|
#include "core/coordinates.hpp"
|
|
|
|
#include <cmath>
|
|
|
|
TEST_CASE("stock hill climbing limits are shared by all surfaces") {
|
|
using namespace wowee::rendering::movement;
|
|
REQUIRE(kMaxWalkableSlopeDegrees == 50.0f);
|
|
REQUIRE(isWalkableNormal(kMinWalkableNormalZ));
|
|
REQUIRE_FALSE(isWalkableNormal(kMinWalkableNormalZ - 0.001f));
|
|
REQUIRE(isReachableStep(kMaxStepUp));
|
|
REQUIRE_FALSE(isReachableStep(kMaxStepUp + 0.001f));
|
|
}
|
|
|
|
// A walkable slope can rise faster than the step-up budget allows for, which is
|
|
// why grounding cannot rely on the budget alone. At the steepest walkable angle
|
|
// a mounted player crosses more ground per frame than kMaxStepUp covers as soon
|
|
// as the frame runs long — and the floor selection rejects any surface above
|
|
// feet + budget as unreachable, so the terrain under a climbing player stops
|
|
// counting as ground and they sink into the hill.
|
|
TEST_CASE("a walkable slope out-climbs the step-up budget in a long frame") {
|
|
using namespace wowee::rendering::movement;
|
|
|
|
// tan(50 degrees), the rise per unit travelled along the steepest slope a
|
|
// player may walk up.
|
|
constexpr float kSteepestRisePerYard = 1.19175f;
|
|
|
|
auto riseOverFrame = [](float speedYardsPerSec, float frameSeconds) {
|
|
return speedYardsPerSec * frameSeconds * kSteepestRisePerYard;
|
|
};
|
|
|
|
// A smooth frame stays well inside the budget at every travel speed.
|
|
CHECK(riseOverFrame(7.0f, 1.0f / 60.0f) < kMaxStepUp); // running
|
|
CHECK(riseOverFrame(14.0f, 1.0f / 60.0f) < kMaxStepUp); // epic mount
|
|
|
|
// A slow frame does not. This is the case that put the player inside the
|
|
// hill, so grounding has to recover from penetration rather than assume it
|
|
// cannot happen.
|
|
CHECK(riseOverFrame(14.0f, 1.0f / 20.0f) > kMaxStepUp);
|
|
}
|
|
|
|
// Facing crosses two representations: the renderer holds the character's yaw in
|
|
// degrees, the game side holds canonical yaw in radians, and the frame loop
|
|
// converts render → game every frame. Both directions of that conversion were
|
|
// hand-written at four call sites, two of them inverting the other two from
|
|
// memory. If they ever disagree, facing a target writes one value and the next
|
|
// frame reads back another — which is how a cast could be accepted and then
|
|
// fail the server's arc check a second and a half later.
|
|
TEST_CASE("character yaw and canonical yaw convert back to each other") {
|
|
using namespace wowee::core::coords;
|
|
|
|
for (float deg = -720.0f; deg <= 720.0f; deg += 7.5f) {
|
|
const float canonical = characterYawDegToCanonical(deg);
|
|
const float back = canonicalToCharacterYawDeg(canonical);
|
|
// Round-trips to the same heading, allowing for full turns.
|
|
float delta = std::fmod(std::fabs(back - deg), 360.0f);
|
|
if (delta > 180.0f) delta = 360.0f - delta;
|
|
INFO("degrees: " << deg << " canonical: " << canonical << " back: " << back);
|
|
CHECK(delta < 0.01f);
|
|
CHECK(canonical >= -PI - 0.001f);
|
|
CHECK(canonical <= PI + 0.001f);
|
|
}
|
|
|
|
// Render yaw is canonical + 90: canonicalToRender swaps x and y, and
|
|
// canonical yaw is atan2(-dy, dx), so a heading of canonical 0 (north) has
|
|
// render components (0, 1) and a render yaw of 90.
|
|
CHECK(canonicalToCharacterYawDeg(0.0f) == Catch::Approx(90.0f).margin(1e-4));
|
|
CHECK(characterYawDegToCanonical(90.0f) == Catch::Approx(0.0f).margin(1e-5));
|
|
|
|
// And it must agree with taking the angle of the render direction directly,
|
|
// which is how the combat auto-turn and the camera both produce it.
|
|
for (float canon = -3.0f; canon <= 3.0f; canon += 0.25f) {
|
|
const glm::vec3 dirCanonical(std::cos(canon), -std::sin(canon), 0.0f);
|
|
const glm::vec3 dirRender = canonicalToRender(dirCanonical);
|
|
const float renderYawDeg =
|
|
std::atan2(dirRender.y, dirRender.x) * (180.0f / PI);
|
|
float diff = std::fmod(std::fabs(renderYawDeg - canonicalToCharacterYawDeg(canon)), 360.0f);
|
|
if (diff > 180.0f) diff = 360.0f - diff;
|
|
INFO("canonical: " << canon);
|
|
CHECK(diff < 0.01f);
|
|
}
|
|
}
|