Files
YourWishes 1bd73d69fe 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
2026-08-06 12:58:07 -05:00

181 lines
7.0 KiB
C

/**
* 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);
}