Files
EVEngine/test/climbing_vault.cpp
T

196 lines
9.3 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "animation/AnimClip.h"
#include "animation/AnimPlayer.h"
#include "animation/AnimSkeleton.h"
#include "climbing/Climbing.h"
#include "climbing/ClimbingAnimation.h"
#include "physics/Body3D.h"
#include "physics/Physics.h"
#include "physics/World3D.h"
#include <cmath>
#include <cstdint>
#include <memory>
#include <vector>
namespace {
eve::climbing::ClimbingActionDefinition vaultAction(std::string id, float minHeight, float maxHeight,
int selectionBias = 0) {
eve::climbing::ClimbingActionDefinition action;
action.id = std::move(id);
action.kind = eve::climbing::ClimbingActionKind::Vault;
action.minHeight = minHeight;
action.maxHeight = maxHeight;
action.minSpeed = 3.f;
action.duration = eve::Duration::fromNanoseconds(600000000);
action.landingForward = 0.75f;
action.apexHeight = 0.55f;
action.selectionBias = selectionBias;
action.maxTranslationWarpPerTick = 0.2f;
action.horizontalWarpBudget = 1.2f;
action.verticalWarpBudget = 1.2f;
action.requiredNotifies = {"contact.left_hand", "land"};
return action;
}
struct VaultWorld {
VaultWorld() {
world.reset(eve::physics::Physics::create()->newWorld3D(0.f, 0.f, 0.f, false));
player = world->newBody("kinematic", 0.f, 0.9f, 0.f);
player->newCapsuleShape(1.8f, 0.3f);
obstacle = world->newBody("static", 0.f, 0.3f, 1.f);
obstacle->newBoxShape(1.35f, 0.6f, 0.65f);
}
std::unique_ptr<eve::physics::World3D> world;
eve::physics::Body3D* player = nullptr;
eve::physics::Body3D* obstacle = nullptr;
};
struct VaultRun {
std::vector<eve::climbing::ClimbingAdvance> advances;
std::vector<eve::climbing::ClimbingEvent> events;
bool sawConstraint = false;
bool landingClear = false;
std::uint64_t replayHash = 1469598103934665603ull;
};
void hashValue(std::uint64_t& hash, std::int64_t value) {
const auto bits = static_cast<std::uint64_t>(value);
for (int byte = 0; byte < 8; ++byte) {
hash ^= (bits >> (byte * 8)) & 0xffu;
hash *= 1099511628211ull;
}
}
std::int64_t millimeters(float value) {
return static_cast<std::int64_t>(std::llround(static_cast<double>(value) * 1000.0));
}
VaultRun runVault() {
VaultWorld fixture;
eve::climbing::ClimbingRuntime runtime;
eve::climbing::ClimbingProfileDefinition profile;
profile.maxWarpResidual = 5.f;
profile.actions.push_back(vaultAction("parkour:vault_low", 0.4f, 0.78f));
profile.actions.push_back(vaultAction("parkour:vault_high", 0.79f, 1.2f, 1));
REQUIRE(runtime.setProfile(profile).ok());
eve::animation::AnimSkeleton skeleton;
const int root = skeleton.addBone("root", -1);
eve::animation::AnimClip clip("vault_low");
clip.setDuration(0.6f);
clip.setLoop(false);
clip.addPositionKey(root, 0.f, 0.f, 0.f, 0.f);
clip.addPositionKey(root, 0.6f, 0.f, 0.6f, 1.8f);
clip.addEvent(0.15f, "contact.left_hand");
clip.addEvent(0.55f, "land");
REQUIRE(runtime.validateAnimationBinding("parkour:vault_low", clip).ok());
const eve::climbing::ClimbingPose pose{{0.f, 0.f, 0.f}, {0.f, 0.f, 1.f}, 4.5f, fixture.player->getId(), 0.f, true};
auto candidates = runtime.probe(*fixture.world, pose);
REQUIRE(candidates.ok());
REQUIRE_EQ(candidates.value().size(), std::size_t{1});
CHECK_EQ(candidates.value().front().actionId, std::string("parkour:vault_low"));
CHECK(candidates.value().front().obstacleBody == fixture.obstacle->runtimeHandle());
REQUIRE(runtime.tryBegin(*fixture.world, pose, eve::SimulationTick(100)).ok());
eve::animation::AnimPlayer player(&skeleton);
REQUIRE(eve::climbing::beginClimbingAnimation(player, clip, root).ok());
VaultRun output;
for (std::uint64_t frame = 1; frame <= 6; ++frame) {
const eve::SimulationStep step{eve::SimulationTick(100 + frame), eve::Duration::fromNanoseconds(100000000)};
auto animationFrame = eve::climbing::advanceClimbingAnimation(player, step, {0.f, 0.f, 1.f});
REQUIRE(animationFrame.ok());
auto advanced = runtime.advance(*fixture.world, step, animationFrame.value().motion);
REQUIRE(advanced.ok());
output.sawConstraint = output.sawConstraint || advanced.value().constrained ||
std::fabs(advanced.value().desiredDelta.x - advanced.value().actualDelta.x) > 1e-4f ||
std::fabs(advanced.value().desiredDelta.y - advanced.value().actualDelta.y) > 1e-4f ||
std::fabs(advanced.value().desiredDelta.z - advanced.value().actualDelta.z) > 1e-4f;
hashValue(output.replayHash, millimeters(advanced.value().feet.x));
hashValue(output.replayHash, millimeters(advanced.value().feet.y));
hashValue(output.replayHash, millimeters(advanced.value().feet.z));
output.advances.push_back(std::move(advanced).takeValue());
}
auto events = runtime.drainEvents();
REQUIRE(events.ok());
output.events = std::move(events).takeValue();
for (const auto& event : output.events) {
hashValue(output.replayHash, static_cast<std::int64_t>(event.kind));
hashValue(output.replayHash, static_cast<std::int64_t>(event.tick.value()));
}
const auto& landing = output.advances.back().feet;
eve::physics::QueryFilter3D filter;
filter.ignoredBodyId = fixture.player->getId();
auto overlap = fixture.world->queryCapsuleOwned(landing.x, landing.y + profile.capsuleRadius, landing.z, landing.x,
landing.y + profile.capsuleHeight - profile.capsuleRadius,
landing.z, profile.capsuleRadius, filter);
REQUIRE(overlap.ok());
output.landingClear = overlap.value().bodyCount == 0;
return output;
}
} // namespace
TEST_CASE("climbing.vault.runningJumpUsesLowVaultAndProducesStableConstrainedLanding") {
const VaultRun first = runVault();
REQUIRE_EQ(first.advances.size(), std::size_t{6});
CHECK(first.sawConstraint);
CHECK(first.landingClear);
CHECK_EQ(static_cast<int>(first.advances.back().phase), static_cast<int>(eve::climbing::ClimbingPhase::Completed));
REQUIRE_EQ(first.events.size(), std::size_t{4});
CHECK_EQ(static_cast<int>(first.events[0].kind), static_cast<int>(eve::climbing::ClimbingEventKind::Started));
CHECK_EQ(static_cast<int>(first.events[1].kind),
static_cast<int>(eve::climbing::ClimbingEventKind::ContactLeftHand));
CHECK_EQ(static_cast<int>(first.events[2].kind), static_cast<int>(eve::climbing::ClimbingEventKind::Landed));
CHECK_EQ(static_cast<int>(first.events[3].kind), static_cast<int>(eve::climbing::ClimbingEventKind::Completed));
const VaultRun replay = runVault();
CHECK_EQ(replay.replayHash, first.replayHash);
}
TEST_CASE("climbing.vault.destroyedTargetCancelsWithStaleEvidence") {
VaultWorld fixture;
eve::climbing::ClimbingRuntime runtime;
auto low = vaultAction("parkour:vault_low", 0.4f, 0.78f);
eve::climbing::ClimbingProfileDefinition profile;
profile.maxWarpResidual = 5.f;
profile.actions.push_back(low);
profile.actions.push_back(vaultAction("parkour:vault_high", 0.79f, 1.2f, 1));
REQUIRE(runtime.setProfile(profile).ok());
eve::animation::AnimSkeleton skeleton;
const int root = skeleton.addBone("root", -1);
eve::animation::AnimClip clip("vault_low_stale");
clip.setDuration(0.6f);
clip.setLoop(false);
clip.addPositionKey(root, 0.f, 0.f, 0.f, 0.f);
clip.addPositionKey(root, 0.6f, 0.f, 0.6f, 1.8f);
clip.addEvent(0.15f, "contact.left_hand");
clip.addEvent(0.55f, "land");
REQUIRE(runtime.validateAnimationBinding("parkour:vault_low", clip).ok());
const eve::climbing::ClimbingPose pose{{0.f, 0.f, 0.f}, {0.f, 0.f, 1.f}, 4.5f, fixture.player->getId(), 0.f, true};
auto candidates = runtime.probe(*fixture.world, pose);
REQUIRE(candidates.ok());
REQUIRE_EQ(candidates.value().size(), std::size_t{1});
CHECK(candidates.value().front().obstacleBody == fixture.obstacle->runtimeHandle());
REQUIRE(runtime.tryBegin(*fixture.world, pose, eve::SimulationTick(5)).ok());
fixture.world->destroyBody(fixture.obstacle);
fixture.obstacle = nullptr;
auto advanced = runtime.advance(*fixture.world, {eve::SimulationTick(6), eve::Duration::fromNanoseconds(16666667)});
CHECK(!advanced.ok());
CHECK_EQ(static_cast<int>(runtime.phase()), static_cast<int>(eve::climbing::ClimbingPhase::Cancelled));
CHECK_EQ(runtime.inspect().terminalCode, std::string("climbing.anchor.stale"));
auto events = runtime.drainEvents();
REQUIRE(events.ok());
REQUIRE_EQ(events.value().size(), std::size_t{2});
CHECK_EQ(static_cast<int>(events.value().back().kind),
static_cast<int>(eve::climbing::ClimbingEventKind::Cancelled));
}