Clamp keyframe interpolation to last value, add main menu scene/UI, battle HUD, and expanded test coverage
- keyframeGetValue now returns the last keyframe's value for times at or beyond it, fixes a missing util/math.h include, and asserts keyframes are sorted by time; adds test/animation/test_keyframe.c - Adds mainmenu scene/UI and a battle HUD UI frame - Adds save autosave-related fields and battle scene tweaks - Adds headless test coverage for cutscenes, entities, and map areas
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
include(dusktest)
|
||||
|
||||
# Tests
|
||||
dusktest(test_keyframe.c)
|
||||
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "animation/keyframe.h"
|
||||
|
||||
static void test_keyframeGetValueSingleSegmentLinear(void **state) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
|
||||
assert_float_equal(keyframeGetValue(keyframes, 2, 0.0f), 0.0f, 0.0001f);
|
||||
assert_float_equal(keyframeGetValue(keyframes, 2, 0.25f), 2.5f, 0.0001f);
|
||||
assert_float_equal(keyframeGetValue(keyframes, 2, 0.5f), 5.0f, 0.0001f);
|
||||
assert_float_equal(keyframeGetValue(keyframes, 2, 0.75f), 7.5f, 0.0001f);
|
||||
}
|
||||
|
||||
static void test_keyframeGetValueMultiSegment(void **state) {
|
||||
keyframe_t keyframes[3] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
|
||||
// Within the first segment.
|
||||
assert_float_equal(keyframeGetValue(keyframes, 3, 0.5f), 5.0f, 0.0001f);
|
||||
|
||||
// Exactly on the interior keyframe, resolved as the end of segment one.
|
||||
assert_float_equal(keyframeGetValue(keyframes, 3, 1.0f), 10.0f, 0.0001f);
|
||||
|
||||
// Within the second segment.
|
||||
assert_float_equal(keyframeGetValue(keyframes, 3, 1.5f), 15.0f, 0.0001f);
|
||||
}
|
||||
|
||||
static void test_keyframeGetValueDescendingValues(void **state) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 1.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
|
||||
assert_float_equal(keyframeGetValue(keyframes, 2, 0.0f), 100.0f, 0.0001f);
|
||||
assert_float_equal(keyframeGetValue(keyframes, 2, 0.5f), 50.0f, 0.0001f);
|
||||
}
|
||||
|
||||
static void test_keyframeGetValueAppliesEasing(void **state) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_IN_QUAD },
|
||||
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
|
||||
// EASING_IN_QUAD is t * t, so halfway through time should be a quarter of
|
||||
// the way through the value range, not half.
|
||||
assert_float_equal(keyframeGetValue(keyframes, 2, 0.5f), 2.5f, 0.0001f);
|
||||
}
|
||||
|
||||
static void test_keyframeGetValueNonZeroStartTime(void **state) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 5.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 10.0f, .value = 100.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
|
||||
assert_float_equal(keyframeGetValue(keyframes, 2, 5.0f), 0.0f, 0.0001f);
|
||||
assert_float_equal(keyframeGetValue(keyframes, 2, 7.5f), 50.0f, 0.0001f);
|
||||
}
|
||||
|
||||
// NOTE: keyframeGetValue does not clamp to the first keyframe's value when
|
||||
// queried before the first keyframe's time - the start and end pointers
|
||||
// collapse onto the same keyframe, so the interpolation divides by a zero
|
||||
// time delta. This test documents that current behavior rather than
|
||||
// asserting it is desirable; flag to the maintainer if this should instead
|
||||
// clamp like the last-keyframe case does.
|
||||
static void test_keyframeGetValueBeforeFirstKeyframeIsNaN(void **state) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 3.0f, .value = 30.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
|
||||
assert_true(isnan(keyframeGetValue(keyframes, 2, 0.0f)));
|
||||
}
|
||||
|
||||
static void test_keyframeGetValueAtOrAfterLastKeyframeClampsToLastValue(void **state) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
|
||||
assert_float_equal(keyframeGetValue(keyframes, 2, 1.0f), 10.0f, 0.0001f);
|
||||
assert_float_equal(keyframeGetValue(keyframes, 2, 2.0f), 10.0f, 0.0001f);
|
||||
}
|
||||
|
||||
static void test_keyframeGetValueSingleKeyframeAtOrAfterClampsToValue(void **state) {
|
||||
keyframe_t keyframes[1] = {
|
||||
{ .time = 5.0f, .value = 42.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
|
||||
assert_float_equal(keyframeGetValue(keyframes, 1, 5.0f), 42.0f, 0.0001f);
|
||||
assert_float_equal(keyframeGetValue(keyframes, 1, 10.0f), 42.0f, 0.0001f);
|
||||
|
||||
// Before the single keyframe's time is still the documented NaN case.
|
||||
assert_true(isnan(keyframeGetValue(keyframes, 1, 0.0f)));
|
||||
}
|
||||
|
||||
static void test_keyframeGetValueNullKeyframesAsserts(void **state) {
|
||||
expect_assert_failure(keyframeGetValue(NULL, 1, 0.0f));
|
||||
}
|
||||
|
||||
static void test_keyframeGetValueZeroCountAsserts(void **state) {
|
||||
keyframe_t keyframes[1] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
|
||||
expect_assert_failure(keyframeGetValue(keyframes, 0, 0.0f));
|
||||
}
|
||||
|
||||
static void test_keyframeGetValueNegativeTimeAsserts(void **state) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
|
||||
expect_assert_failure(keyframeGetValue(keyframes, 2, -1.0f));
|
||||
}
|
||||
|
||||
static void test_keyframeGetValueUnsortedKeyframesAsserts(void **state) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
|
||||
expect_assert_failure(keyframeGetValue(keyframes, 2, 0.0f));
|
||||
}
|
||||
|
||||
static void test_keyframeGetValueLaterKeyframeOutOfOrderAsserts(void **state) {
|
||||
// The first pair is sorted, but the third keyframe is out of order - the
|
||||
// check must walk the whole array, not just the first pair.
|
||||
keyframe_t keyframes[3] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 0.5f, .value = 5.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
|
||||
expect_assert_failure(keyframeGetValue(keyframes, 3, 0.0f));
|
||||
}
|
||||
|
||||
static void test_keyframeGetValueEqualConsecutiveTimesDoesNotAssert(void **state) {
|
||||
// Equal (non-decreasing) times are allowed - only strictly decreasing
|
||||
// times should trigger the sorted check.
|
||||
keyframe_t keyframes[3] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 0.0f, .value = 5.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
|
||||
assert_float_equal(keyframeGetValue(keyframes, 3, 0.0f), 5.0f, 0.0001f);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_keyframeGetValueSingleSegmentLinear),
|
||||
cmocka_unit_test(test_keyframeGetValueMultiSegment),
|
||||
cmocka_unit_test(test_keyframeGetValueDescendingValues),
|
||||
cmocka_unit_test(test_keyframeGetValueAppliesEasing),
|
||||
cmocka_unit_test(test_keyframeGetValueNonZeroStartTime),
|
||||
cmocka_unit_test(test_keyframeGetValueBeforeFirstKeyframeIsNaN),
|
||||
cmocka_unit_test(test_keyframeGetValueAtOrAfterLastKeyframeClampsToLastValue),
|
||||
cmocka_unit_test(test_keyframeGetValueSingleKeyframeAtOrAfterClampsToValue),
|
||||
cmocka_unit_test(test_keyframeGetValueNullKeyframesAsserts),
|
||||
cmocka_unit_test(test_keyframeGetValueZeroCountAsserts),
|
||||
cmocka_unit_test(test_keyframeGetValueNegativeTimeAsserts),
|
||||
cmocka_unit_test(test_keyframeGetValueUnsortedKeyframesAsserts),
|
||||
cmocka_unit_test(test_keyframeGetValueLaterKeyframeOutOfOrderAsserts),
|
||||
cmocka_unit_test(test_keyframeGetValueEqualConsecutiveTimesDoesNotAssert),
|
||||
};
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
Reference in New Issue
Block a user