You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

525 lines
19 KiB

#include <gtest/gtest.h>
#include "nthread.h"
#include "engine/animationinfo.h"
using namespace devilution;
/**
* @brief Represents a Action in the game_logic or rendering.
*/
struct TestData {
virtual ~TestData() = default;
};
/**
* @brief Represents a call to SetNewAnimation
*/
struct SetNewAnimationData : TestData {
SetNewAnimationData(int numberOfFrames, int delayLen, AnimationDistributionFlags params = AnimationDistributionFlags::None, int numSkippedFrames = 0, int distributeFramesBeforeFrame = 0)
{
_NumberOfFrames = numberOfFrames;
_DelayLen = delayLen;
_Params = params;
_NumSkippedFrames = numSkippedFrames;
_DistributeFramesBeforeFrame = distributeFramesBeforeFrame;
}
int _NumberOfFrames;
int _DelayLen;
AnimationDistributionFlags _Params;
int _NumSkippedFrames;
int _DistributeFramesBeforeFrame;
};
/**
* @brief Represents a GameTick, this includes skipping of Frames (for example because of Fastest Attack Modifier) and ProcessAnimation (which also updates Animation Frame/Count).
*/
struct GameTickData : TestData {
int _ExpectedAnimationFrame;
int _ExpectedAnimationCnt;
int _FramesToSkip;
GameTickData(int expectedAnimationFrame, int expectedAnimationCnt, int framesToSkip = 0)
{
_ExpectedAnimationFrame = expectedAnimationFrame;
_ExpectedAnimationCnt = expectedAnimationCnt;
_FramesToSkip = framesToSkip;
}
};
/**
* @brief Represents a rendering/drawing Action.
* This happens directly after the game_logic (_fProgressToNextGameTick = 0) or if no GameTick is due between two GameTicks (_fProgressToNextGameTick != 0).
*/
struct RenderingData : TestData {
/*
* @brief the progress as a fraction (0.0f to 1.0f) in time to the next game tick
*/
float _fProgressToNextGameTick;
int _ExpectedRenderingFrame;
RenderingData(float fProgressToNextGameTick, int expectedRenderingFrame)
{
this->_fProgressToNextGameTick = fProgressToNextGameTick;
this->_ExpectedRenderingFrame = expectedRenderingFrame;
}
};
/*
* @brief
* This UnitTest tries to simulate the GameLoop (GameTickData) and the Rendering that can happen (RenderingData).
* Rendering can happen more often then GameTicks and at any time between two GameTicks.
* The Animation Distribution Logic must adjust to the Rendering that happens at any give point in time.
*/
void RunAnimationTest(const std::vector<TestData *> &vecTestData)
{
const int pnum = 0;
AnimationInfo animInfo = {};
int currentGameTick = 0;
for (TestData *x : vecTestData) {
auto setNewAnimationData = dynamic_cast<SetNewAnimationData *>(x);
if (setNewAnimationData != nullptr) {
animInfo.SetNewAnimation(nullptr, setNewAnimationData->_NumberOfFrames, setNewAnimationData->_DelayLen, setNewAnimationData->_Params, setNewAnimationData->_NumSkippedFrames, setNewAnimationData->_DistributeFramesBeforeFrame);
}
auto gameTickData = dynamic_cast<GameTickData *>(x);
if (gameTickData != nullptr) {
currentGameTick += 1;
if (gameTickData->_FramesToSkip != 0)
animInfo.CurrentFrame += gameTickData->_FramesToSkip;
animInfo.ProcessAnimation();
EXPECT_EQ(animInfo.CurrentFrame, gameTickData->_ExpectedAnimationFrame);
EXPECT_EQ(animInfo.DelayCounter, gameTickData->_ExpectedAnimationCnt);
}
auto renderingData = dynamic_cast<RenderingData *>(x);
if (renderingData != nullptr) {
gfProgressToNextGameTick = renderingData->_fProgressToNextGameTick;
EXPECT_EQ(animInfo.GetFrameToUseForRendering(), renderingData->_ExpectedRenderingFrame)
<< std::fixed << std::setprecision(2)
<< "ProgressToNextGameTick: " << renderingData->_fProgressToNextGameTick
<< " CurrentFrame: " << animInfo.CurrentFrame
<< " DelayCounter: " << animInfo.DelayCounter
<< " GameTick: " << currentGameTick;
}
}
for (TestData *x : vecTestData) {
delete x;
}
}
TEST(AnimationInfo, AttackSwordWarrior) // ProcessAnimationPending should be considered by distribution logic
{
RunAnimationTest(
{
new SetNewAnimationData(16, 0, AnimationDistributionFlags::ProcessAnimationPending, 0, 9),
// ProcessAnimation directly after StartAttack (in same GameTick). So we don't see any rendering before.
new GameTickData(2, 0),
new RenderingData(0.0f, 1),
new RenderingData(0.3f, 1),
new RenderingData(0.6f, 1),
new RenderingData(0.8f, 1),
new GameTickData(3, 0),
new RenderingData(0.0f, 2),
new RenderingData(0.3f, 2),
new RenderingData(0.6f, 2),
new RenderingData(0.8f, 3),
new GameTickData(4, 0),
new RenderingData(0.0f, 3),
new RenderingData(0.3f, 3),
new RenderingData(0.6f, 3),
new RenderingData(0.8f, 4),
new GameTickData(5, 0),
new RenderingData(0.0f, 4),
new RenderingData(0.3f, 4),
new RenderingData(0.6f, 5),
new RenderingData(0.8f, 5),
new GameTickData(6, 0),
new RenderingData(0.0f, 5),
new RenderingData(0.3f, 5),
new RenderingData(0.6f, 6),
new RenderingData(0.8f, 6),
new GameTickData(7, 0),
new RenderingData(0.0f, 6),
new RenderingData(0.3f, 7),
new RenderingData(0.6f, 7),
new RenderingData(0.8f, 7),
new GameTickData(8, 0),
new RenderingData(0.0f, 7),
new RenderingData(0.3f, 8),
new RenderingData(0.6f, 8),
new RenderingData(0.8f, 8),
// After this GameTick, the Animation Distribution Logic is disabled
new GameTickData(9, 0),
new RenderingData(0.1f, 9),
new GameTickData(10, 0),
new RenderingData(0.4f, 10),
new GameTickData(11, 0),
new RenderingData(0.4f, 11),
new GameTickData(12, 0),
new RenderingData(0.3f, 12),
new GameTickData(13, 0),
new RenderingData(0.0f, 13),
new GameTickData(14, 0),
new RenderingData(0.6f, 14),
new GameTickData(15, 0),
new RenderingData(0.6f, 15),
new GameTickData(16, 0),
new RenderingData(0.6f, 16),
// Animation stopped cause PM_DoAttack would stop the Animation "if (plr[pnum].AnimInfo.CurrentFrame == plr[pnum]._pAFrames) {"
});
}
TEST(AnimationInfo, AttackSwordWarriorWithFastestAttack) // Skipped frames and ProcessAnimationPending should be considered by distribution logic
{
RunAnimationTest(
{
new SetNewAnimationData(16, 0, AnimationDistributionFlags::ProcessAnimationPending, 2, 9),
// ProcessAnimation directly after StartAttack (in same GameTick). So we don't see any rendering before.
new GameTickData(2, 0),
new RenderingData(0.0f, 1),
new RenderingData(0.3f, 1),
new RenderingData(0.6f, 1),
new RenderingData(0.8f, 2),
new GameTickData(3, 0),
new RenderingData(0.0f, 2),
new RenderingData(0.3f, 3),
new RenderingData(0.6f, 3),
new RenderingData(0.8f, 3),
new GameTickData(4, 0),
new RenderingData(0.0f, 4),
new RenderingData(0.3f, 4),
new RenderingData(0.6f, 5),
new RenderingData(0.8f, 5),
new GameTickData(7, 0, 2),
new RenderingData(0.0f, 5),
new RenderingData(0.3f, 6),
new RenderingData(0.6f, 6),
new RenderingData(0.8f, 7),
new GameTickData(8, 0),
new RenderingData(0.0f, 7),
new RenderingData(0.3f, 7),
new RenderingData(0.6f, 8),
new RenderingData(0.8f, 8),
// After this GameTick, the Animation Distribution Logic is disabled
new GameTickData(9, 0),
new RenderingData(0.1f, 9),
new GameTickData(10, 0),
new RenderingData(0.4f, 10),
new GameTickData(11, 0),
new RenderingData(0.4f, 11),
new GameTickData(12, 0),
new RenderingData(0.3f, 12),
new GameTickData(13, 0),
new RenderingData(0.0f, 13),
new GameTickData(14, 0),
new RenderingData(0.6f, 14),
new GameTickData(15, 0),
new RenderingData(0.6f, 15),
new GameTickData(16, 0),
new RenderingData(0.6f, 16),
// Animation stopped cause PM_DoAttack would stop the Animation "if (plr[pnum].AnimInfo.CurrentFrame == plr[pnum]._pAFrames) {"
});
}
/*
* @brief The Warrior make two attacks. The second queued attack cancels the first after the Hit Frame.
*/
TEST(AnimationInfo, AttackSwordWarriorRepeated)
{
RunAnimationTest(
{
new SetNewAnimationData(16, 0, AnimationDistributionFlags::ProcessAnimationPending, 0, 9),
// ProcessAnimation directly after StartAttack (in same GameTick). So we don't see any rendering before.
new GameTickData(2, 0),
new RenderingData(0.0f, 1),
new RenderingData(0.3f, 1),
new RenderingData(0.6f, 1),
new RenderingData(0.8f, 1),
new GameTickData(3, 0),
new RenderingData(0.0f, 2),
new RenderingData(0.3f, 2),
new RenderingData(0.6f, 2),
new RenderingData(0.8f, 3),
new GameTickData(4, 0),
new RenderingData(0.0f, 3),
new RenderingData(0.3f, 3),
new RenderingData(0.6f, 3),
new RenderingData(0.8f, 4),
new GameTickData(5, 0),
new RenderingData(0.0f, 4),
new RenderingData(0.3f, 4),
new RenderingData(0.6f, 5),
new RenderingData(0.8f, 5),
new GameTickData(6, 0),
new RenderingData(0.0f, 5),
new RenderingData(0.3f, 5),
new RenderingData(0.6f, 6),
new RenderingData(0.8f, 6),
new GameTickData(7, 0),
new RenderingData(0.0f, 6),
new RenderingData(0.3f, 7),
new RenderingData(0.6f, 7),
new RenderingData(0.8f, 7),
new GameTickData(8, 0),
new RenderingData(0.0f, 7),
new RenderingData(0.3f, 8),
new RenderingData(0.6f, 8),
new RenderingData(0.8f, 8),
// After this GameTick, the Animation Distribution Logic is disabled
new GameTickData(9, 0),
new RenderingData(0.1f, 9),
new GameTickData(10, 0),
new RenderingData(0.3f, 10),
// Start of repeated attack, cause plr[pnum].AnimInfo.CurrentFrame > plr[myplr]._pAFNum
new SetNewAnimationData(16, 0, static_cast<AnimationDistributionFlags>(AnimationDistributionFlags::ProcessAnimationPending | AnimationDistributionFlags::RepeatedAction), 0, 9),
// ProcessAnimation directly after StartAttack (in same GameTick). So we don't see any rendering before.
new GameTickData(2, 0),
new RenderingData(0.0f, 11),
new RenderingData(0.3f, 11),
new RenderingData(0.6f, 12),
new RenderingData(0.8f, 12),
new GameTickData(3, 0),
new RenderingData(0.0f, 13),
new RenderingData(0.3f, 13),
new RenderingData(0.6f, 14),
new RenderingData(0.8f, 14),
new GameTickData(4, 0),
new RenderingData(0.0f, 15),
new RenderingData(0.3f, 15),
new RenderingData(0.6f, 16),
new RenderingData(0.8f, 16),
new GameTickData(5, 0),
new RenderingData(0.0f, 1),
new RenderingData(0.3f, 1),
new RenderingData(0.6f, 2),
new RenderingData(0.8f, 2),
new GameTickData(6, 0),
new RenderingData(0.0f, 3),
new RenderingData(0.3f, 3),
new RenderingData(0.6f, 4),
new RenderingData(0.8f, 4),
new GameTickData(7, 0),
new RenderingData(0.0f, 5),
new RenderingData(0.3f, 5),
new RenderingData(0.6f, 6),
new RenderingData(0.8f, 6),
new GameTickData(8, 0),
new RenderingData(0.0f, 7),
new RenderingData(0.3f, 7),
new RenderingData(0.6f, 8),
new RenderingData(0.8f, 8),
// After this GameTick, the Animation Distribution Logic is disabled
new GameTickData(9, 0),
new RenderingData(0.1f, 9),
new GameTickData(10, 0),
new RenderingData(0.4f, 10),
new GameTickData(11, 0),
new RenderingData(0.4f, 11),
new GameTickData(12, 0),
new RenderingData(0.3f, 12),
new GameTickData(13, 0),
new RenderingData(0.0f, 13),
new GameTickData(14, 0),
new RenderingData(0.6f, 14),
new GameTickData(15, 0),
new RenderingData(0.6f, 15),
new GameTickData(16, 0),
new RenderingData(0.6f, 16),
// Animation stopped cause PM_DoAttack would stop the Animation "if (plr[pnum].AnimInfo.CurrentFrame == plr[pnum]._pAFrames) {"
});
}
TEST(AnimationInfo, BlockingWarriorNormal) // Ignored delay for last Frame should be considered by distribution logic
{
RunAnimationTest(
{
new SetNewAnimationData(2, 2, AnimationDistributionFlags::SkipsDelayOfLastFrame),
new RenderingData(0.0f, 1),
new RenderingData(0.3f, 1),
new RenderingData(0.6f, 1),
new RenderingData(0.8f, 1),
new GameTickData(1, 1),
new RenderingData(0.0f, 1),
new RenderingData(0.3f, 1),
new RenderingData(0.6f, 1),
new RenderingData(0.8f, 1),
new GameTickData(1, 2),
new RenderingData(0.0f, 2),
new RenderingData(0.3f, 2),
new RenderingData(0.6f, 2),
new RenderingData(0.8f, 2),
new GameTickData(2, 0),
new RenderingData(0.0f, 2),
new RenderingData(0.3f, 2),
new RenderingData(0.6f, 2),
new RenderingData(0.8f, 2),
// Animation stopped cause PM_DoBlock would stop the Animation "if (plr[pnum].AnimInfo.CurrentFrame >= plr[pnum]._pBFrames) {"
});
}
TEST(AnimationInfo, BlockingSorcererWithFastBlock) // Skipped frames and ignored delay for last Frame should be considered by distribution logic
{
RunAnimationTest(
{
new SetNewAnimationData(6, 2, AnimationDistributionFlags::SkipsDelayOfLastFrame, 4),
new RenderingData(0.0f, 1),
new RenderingData(0.3f, 1),
new RenderingData(0.6f, 1),
new RenderingData(0.8f, 2),
new GameTickData(1, 1),
new RenderingData(0.0f, 2),
new RenderingData(0.3f, 2),
new RenderingData(0.6f, 3),
new RenderingData(0.8f, 3),
new GameTickData(1, 2),
new RenderingData(0.0f, 4),
new RenderingData(0.3f, 4),
new RenderingData(0.6f, 4),
new RenderingData(0.8f, 5),
new GameTickData(2, 0),
new RenderingData(0.0f, 5),
new RenderingData(0.3f, 5),
new RenderingData(0.6f, 6),
new RenderingData(0.8f, 6),
// Animation stopped cause PM_DoBlock would stop the Animation "if (plr[pnum].AnimInfo.CurrentFrame >= plr[pnum]._pBFrames) {"
});
}
TEST(AnimationInfo, HitRecoverySorcererZenMode) // Skipped frames and ignored delay for last Frame should be considered by distribution logic
{
RunAnimationTest(
{
new SetNewAnimationData(8, 0, AnimationDistributionFlags::None, 4),
new RenderingData(0.0f, 1),
new RenderingData(0.3f, 1),
new RenderingData(0.6f, 2),
new RenderingData(0.8f, 2),
new GameTickData(3, 0, 1),
new RenderingData(0.0f, 3),
new RenderingData(0.3f, 3),
new RenderingData(0.6f, 4),
new RenderingData(0.8f, 4),
new GameTickData(7, 0, 3),
new RenderingData(0.0f, 5),
new RenderingData(0.3f, 5),
new RenderingData(0.6f, 6),
new RenderingData(0.8f, 6),
new GameTickData(8, 0),
new RenderingData(0.0f, 7),
new RenderingData(0.3f, 7),
new RenderingData(0.6f, 8),
new RenderingData(0.8f, 8),
// Animation stopped cause PM_DoGotHit would stop the Animation "if (plr[pnum].AnimInfo.CurrentFrame >= plr[pnum]._pHFrames) {"
});
}
TEST(AnimationInfo, Stand) // Distribution Logic shouldn't change anything here
{
RunAnimationTest(
{
new SetNewAnimationData(10, 3),
new RenderingData(0.1f, 1),
new GameTickData(1, 1),
new RenderingData(0.6f, 1),
new GameTickData(1, 2),
new RenderingData(0.6f, 1),
new GameTickData(1, 3),
new RenderingData(0.6f, 1),
new GameTickData(2, 0),
new RenderingData(0.6f, 2),
new GameTickData(2, 1),
new RenderingData(0.6f, 2),
new GameTickData(2, 2),
new RenderingData(0.6f, 2),
new GameTickData(2, 3),
new RenderingData(0.6f, 2),
new GameTickData(3, 0),
new RenderingData(0.6f, 3),
new GameTickData(3, 1),
new RenderingData(0.6f, 3),
new GameTickData(3, 2),
new RenderingData(0.6f, 3),
new GameTickData(3, 3),
new RenderingData(0.6f, 3),
new GameTickData(4, 0),
new RenderingData(0.6f, 4),
new GameTickData(4, 1),
new RenderingData(0.6f, 4),
new GameTickData(4, 2),
new RenderingData(0.6f, 4),
new GameTickData(4, 3),
new RenderingData(0.6f, 4),
new GameTickData(5, 0),
new RenderingData(0.6f, 5),
new GameTickData(5, 1),
new RenderingData(0.6f, 5),
new GameTickData(5, 2),
new RenderingData(0.6f, 5),
new GameTickData(5, 3),
new RenderingData(0.6f, 5),
new GameTickData(6, 0),
new RenderingData(0.6f, 6),
new GameTickData(6, 1),
new RenderingData(0.6f, 6),
new GameTickData(6, 2),
new RenderingData(0.6f, 6),
new GameTickData(6, 3),
new RenderingData(0.6f, 6),
new GameTickData(7, 0),
new RenderingData(0.6f, 7),
new GameTickData(7, 1),
new RenderingData(0.6f, 7),
new GameTickData(7, 2),
new RenderingData(0.6f, 7),
new GameTickData(7, 3),
new RenderingData(0.6f, 7),
new GameTickData(8, 0),
new RenderingData(0.6f, 8),
new GameTickData(8, 1),
new RenderingData(0.6f, 8),
new GameTickData(8, 2),
new RenderingData(0.6f, 8),
new GameTickData(8, 3),
new RenderingData(0.6f, 8),
new GameTickData(9, 0),
new RenderingData(0.6f, 9),
new GameTickData(9, 1),
new RenderingData(0.6f, 9),
new GameTickData(9, 2),
new RenderingData(0.6f, 9),
new GameTickData(9, 3),
new RenderingData(0.6f, 9),
new GameTickData(10, 0),
new RenderingData(0.6f, 10),
new GameTickData(10, 1),
new RenderingData(0.6f, 10),
new GameTickData(10, 2),
new RenderingData(0.6f, 10),
new GameTickData(10, 3),
new RenderingData(0.6f, 10),
// Animation starts again
new GameTickData(1, 0),
new RenderingData(0.1f, 1),
new GameTickData(1, 1),
new RenderingData(0.6f, 1),
new GameTickData(1, 2),
new RenderingData(0.6f, 1),
new GameTickData(1, 3),
new RenderingData(0.6f, 1),
});
}