mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
The client only simulates knockbacks on the character it controls (LWOControllablePhysComponent::msgKnockback); enemies and NPCs are drawn where the server puts them, so a knockback on them did nothing and scripts stunned them instead. MovementAIComponent::Knockback now flies the object the way the client flies its character: a vector longer than 5 lifts it 0.5, throws it with that velocity under WorldConfig gravity (times its gravity scale) until it lands on the navmesh, no sooner than 250ms later; a shorter one moves it by the vector. Walls taller than a step stop sideways motion, the landing is put back onto the navmesh, and pathing and the combat AI wait until it lands (destinations set mid air are walked to afterwards). Position and velocity go out in the normal serialization every tick. KnockbackBehavior builds the vector like the client's Cast (strength capped at 300, angle as elevation, relative, caster and ignore_self) and knocks back server moved targets that aren't immune, both when the server casts and when a client's skill hits. The blocked bit it writes now also answers for the target's knockback immunity, so a player whose client hasn't caught up with a Personal Fortress isn't knocked out of it. The AM shield generators knock enemies back like live instead of stunning them. Fixes #257 Refs #185 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
156 lines
6.1 KiB
C++
156 lines
6.1 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include <cmath>
|
|
|
|
#include "dpKnockback.h"
|
|
|
|
namespace {
|
|
constexpr float GRAVITY = 57.0f; // WorldConfig.pegravityvalue
|
|
constexpr float TICK = 1.0f / 30.0f;
|
|
|
|
const dpKnockback::GroundQuery flatGround = [](const NiPoint3&) { return 0.0f; };
|
|
|
|
// Flies the arc to the end, returns how long it took
|
|
float Fly(dpKnockback::Arc& arc, const dpKnockback::GroundQuery& ground, float tick = TICK) {
|
|
float time = 0.0f;
|
|
while (arc.Step(tick, GRAVITY, ground)) {
|
|
time += tick;
|
|
if (time > 20.0f) break;
|
|
}
|
|
return time + tick;
|
|
}
|
|
}
|
|
|
|
TEST(KnockbackTests, VectorMatchesTheClient) {
|
|
// Flat direction away from the source, raised by angle degrees
|
|
const auto vector = dpKnockback::ComputeVector(NiPoint3(10.0f, 3.0f, 0.0f), 45.0f, 20.0f);
|
|
EXPECT_NEAR(vector.x, 20.0f / std::sqrt(2.0f), 1e-3f);
|
|
EXPECT_NEAR(vector.y, 20.0f / std::sqrt(2.0f), 1e-3f);
|
|
EXPECT_NEAR(vector.z, 0.0f, 1e-5f);
|
|
|
|
// The dragon's slam: angle 65, strength 55
|
|
const auto slam = dpKnockback::ComputeVector(NiPoint3(0.0f, 0.0f, -4.0f), 65.0f, 55.0f);
|
|
EXPECT_NEAR(slam.Length(), 55.0f, 1e-3f);
|
|
EXPECT_NEAR(std::atan2(slam.y, -slam.z) * 180.0f / 3.14159265f, 65.0f, 1e-2f);
|
|
}
|
|
|
|
TEST(KnockbackTests, NinetyDegreesIsStraightUp) {
|
|
const auto up = dpKnockback::ComputeVector(NiPoint3(5.0f, 0.0f, 5.0f), 90.0f, 40.0f);
|
|
EXPECT_EQ(up, NiPoint3(0.0f, 40.0f, 0.0f));
|
|
|
|
// Without a direction the client still knocks straight up for anything near 90
|
|
EXPECT_EQ(dpKnockback::ComputeVector(NiPoint3Constant::ZERO, 89.5f, 10.0f), NiPoint3(0.0f, 10.0f, 0.0f));
|
|
}
|
|
|
|
TEST(KnockbackTests, StrengthIsCapped) {
|
|
const auto vector = dpKnockback::ComputeVector(NiPoint3(1.0f, 0.0f, 0.0f), 0.0f, 1000.0f);
|
|
EXPECT_NEAR(vector.x, dpKnockback::MAX_STRENGTH, 1e-3f);
|
|
EXPECT_NEAR(vector.y, 0.0f, 1e-5f);
|
|
}
|
|
|
|
TEST(KnockbackTests, ShortVectorsDontLaunch) {
|
|
dpKnockback::Arc arc;
|
|
EXPECT_FALSE(arc.Start(NiPoint3(1.0f, 2.0f, 3.0f), NiPoint3(3.0f, 0.0f, 4.0f))); // length 5
|
|
EXPECT_FALSE(arc.IsActive());
|
|
|
|
// Moved by the vector and dropped onto the ground when it's close
|
|
const auto moved = dpKnockback::Nudge(NiPoint3(0.0f, 0.5f, 0.0f), NiPoint3(3.0f, 0.0f, 0.0f), 2.0f, flatGround);
|
|
EXPECT_EQ(moved, NiPoint3(3.0f, 0.0f, 0.0f));
|
|
|
|
// Ground too far below (a ledge): stays put, like the client
|
|
const auto ledge = dpKnockback::Nudge(NiPoint3(0.0f, 10.0f, 0.0f), NiPoint3(3.0f, 0.0f, 0.0f), 2.0f, flatGround);
|
|
EXPECT_EQ(ledge, NiPoint3(0.0f, 10.0f, 0.0f));
|
|
}
|
|
|
|
TEST(KnockbackTests, ArcFollowsBallistics) {
|
|
// Straight up at 57 u/s under 57 u/s^2 rises for 1s, peaks at 28.5 above the 0.5 lift, lands after ~2s
|
|
dpKnockback::Arc arc;
|
|
ASSERT_TRUE(arc.Start(NiPoint3Constant::ZERO, NiPoint3(0.0f, 57.0f, 0.0f)));
|
|
EXPECT_FLOAT_EQ(arc.GetPosition().y, dpKnockback::LAUNCH_LIFT);
|
|
|
|
float peak = 0.0f;
|
|
float time = 0.0f;
|
|
while (arc.Step(TICK, GRAVITY, flatGround)) {
|
|
time += TICK;
|
|
peak = std::max(peak, arc.GetPosition().y);
|
|
}
|
|
EXPECT_NEAR(peak, 28.5f + dpKnockback::LAUNCH_LIFT, 1.0f);
|
|
EXPECT_NEAR(time, 2.0f, 0.1f);
|
|
EXPECT_FLOAT_EQ(arc.GetPosition().y, 0.0f);
|
|
EXPECT_EQ(arc.GetVelocity(), NiPoint3Constant::ZERO);
|
|
}
|
|
|
|
TEST(KnockbackTests, HorizontalDistanceMatchesFlightTime) {
|
|
// The shield generator: 50 back, 15 up
|
|
dpKnockback::Arc arc;
|
|
ASSERT_TRUE(arc.Start(NiPoint3Constant::ZERO, NiPoint3(-50.0f, 15.0f, 0.0f)));
|
|
const auto time = Fly(arc, flatGround);
|
|
|
|
// t = (vy + sqrt(vy^2 + 2 g lift)) / g
|
|
const float expected = (15.0f + std::sqrt(15.0f * 15.0f + 2.0f * GRAVITY * dpKnockback::LAUNCH_LIFT)) / GRAVITY;
|
|
EXPECT_NEAR(time, expected, 2 * TICK);
|
|
EXPECT_NEAR(arc.GetPosition().x, -50.0f * expected, 50.0f * 2 * TICK);
|
|
EXPECT_FLOAT_EQ(arc.GetPosition().y, 0.0f);
|
|
}
|
|
|
|
TEST(KnockbackTests, TickRateDoesntChangeTheLanding) {
|
|
dpKnockback::Arc slow;
|
|
dpKnockback::Arc fast;
|
|
ASSERT_TRUE(slow.Start(NiPoint3Constant::ZERO, NiPoint3(20.0f, 30.0f, 10.0f)));
|
|
ASSERT_TRUE(fast.Start(NiPoint3Constant::ZERO, NiPoint3(20.0f, 30.0f, 10.0f)));
|
|
Fly(slow, flatGround, 0.1f);
|
|
Fly(fast, flatGround, 1.0f / 120.0f);
|
|
EXPECT_NEAR(slow.GetPosition().x, fast.GetPosition().x, 0.5f);
|
|
EXPECT_NEAR(slow.GetPosition().z, fast.GetPosition().z, 0.5f);
|
|
}
|
|
|
|
TEST(KnockbackTests, FlatKnockbackLastsTheMinimumTime) {
|
|
// Mostly sideways: lands from the 0.5 lift almost at once but the client keeps the state 250ms
|
|
dpKnockback::Arc arc;
|
|
ASSERT_TRUE(arc.Start(NiPoint3Constant::ZERO, NiPoint3(0.0f, 0.0f, 40.0f)));
|
|
while (arc.Step(1.0f / 100.0f, GRAVITY, flatGround)) {}
|
|
EXPECT_GE(arc.GetElapsed(), dpKnockback::MIN_AIRBORNE_TIME);
|
|
EXPECT_LT(arc.GetElapsed(), dpKnockback::MIN_AIRBORNE_TIME + 0.02f);
|
|
EXPECT_NEAR(arc.GetPosition().z, 40.0f * arc.GetElapsed(), 0.5f);
|
|
EXPECT_FLOAT_EQ(arc.GetPosition().y, 0.0f);
|
|
}
|
|
|
|
TEST(KnockbackTests, WallsStopSidewaysMotion) {
|
|
// A 10 tall wall at x = 5
|
|
const dpKnockback::GroundQuery wall = [](const NiPoint3& point) { return point.x >= 5.0f ? 10.0f : 0.0f; };
|
|
dpKnockback::Arc arc;
|
|
ASSERT_TRUE(arc.Start(NiPoint3Constant::ZERO, NiPoint3(40.0f, 10.0f, 0.0f)));
|
|
Fly(arc, wall);
|
|
EXPECT_LT(arc.GetPosition().x, 5.0f);
|
|
EXPECT_FLOAT_EQ(arc.GetPosition().y, 0.0f);
|
|
}
|
|
|
|
TEST(KnockbackTests, LandsOnLowerGround) {
|
|
// Knocked off a 5 high ledge at x = 2
|
|
const dpKnockback::GroundQuery ledge = [](const NiPoint3& point) { return point.x < 2.0f ? 5.0f : 0.0f; };
|
|
dpKnockback::Arc arc;
|
|
ASSERT_TRUE(arc.Start(NiPoint3(0.0f, 5.0f, 0.0f), NiPoint3(10.0f, 5.0f, 0.0f)));
|
|
Fly(arc, ledge);
|
|
EXPECT_GT(arc.GetPosition().x, 2.0f);
|
|
EXPECT_FLOAT_EQ(arc.GetPosition().y, 0.0f);
|
|
}
|
|
|
|
TEST(KnockbackTests, StepsUpOntoLowRises) {
|
|
// A 0.5 step at x = 1 is walked onto, not treated as a wall
|
|
const dpKnockback::GroundQuery step = [](const NiPoint3& point) { return point.x >= 1.0f ? 0.5f : 0.0f; };
|
|
dpKnockback::Arc arc;
|
|
ASSERT_TRUE(arc.Start(NiPoint3Constant::ZERO, NiPoint3(20.0f, 0.0f, 0.0f)));
|
|
Fly(arc, step);
|
|
EXPECT_GT(arc.GetPosition().x, 1.0f);
|
|
EXPECT_FLOAT_EQ(arc.GetPosition().y, 0.5f);
|
|
}
|
|
|
|
TEST(KnockbackTests, BottomlessFallsGiveUp) {
|
|
const dpKnockback::GroundQuery nothing = [](const NiPoint3&) { return -100000.0f; };
|
|
dpKnockback::Arc arc;
|
|
ASSERT_TRUE(arc.Start(NiPoint3Constant::ZERO, NiPoint3(0.0f, 10.0f, 0.0f)));
|
|
Fly(arc, nothing);
|
|
EXPECT_FALSE(arc.IsActive());
|
|
EXPECT_NEAR(arc.GetElapsed(), dpKnockback::MAX_AIRBORNE_TIME, 0.05f);
|
|
}
|