Remove rpg, ui, editor and scene subfolders; split scripts by platform
Delete src/dusk/rpg, src/dusk/ui, the animation/keyframe modules, the scene subfolders, the chunk asset loader and ./editor, and strip the dependent save slot, scene type, engine and test code so the main binary and tests build again. Add scene/node.c/.h skeleton and move build scripts into scripts/<platform>/. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
3948607b1e
commit
31ee7989de
464 files changed
+70
-33009
No files matched your search
@@ -10,8 +10,6 @@ add_subdirectory(console)
|
||||
add_subdirectory(error)
|
||||
add_subdirectory(thread)
|
||||
add_subdirectory(display)
|
||||
add_subdirectory(rpg)
|
||||
add_subdirectory(item)
|
||||
add_subdirectory(save)
|
||||
add_subdirectory(time)
|
||||
add_subdirectory(util)
|
||||
@@ -7,5 +7,3 @@ include(dusktest)
|
||||
|
||||
# Tests
|
||||
dusktest(test_easing.c)
|
||||
dusktest(test_keyframe.c)
|
||||
dusktest(test_animation.c)
|
||||
@@ -1,443 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "animation/animation.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
typedef struct {
|
||||
uint16_t updateCount;
|
||||
uint16_t loopCount;
|
||||
uint16_t completeCount;
|
||||
float_t lastValues[8];
|
||||
} animationtestcallbacks_t;
|
||||
|
||||
static void testOnUpdate(
|
||||
const uint16_t layer, const float_t value, void *user
|
||||
) {
|
||||
animationtestcallbacks_t *cb = (animationtestcallbacks_t *)user;
|
||||
cb->updateCount++;
|
||||
cb->lastValues[layer] = value;
|
||||
}
|
||||
|
||||
static void testOnLoop(void *user) {
|
||||
((animationtestcallbacks_t *)user)->loopCount++;
|
||||
}
|
||||
|
||||
static void testOnComplete(void *user) {
|
||||
((animationtestcallbacks_t *)user)->completeCount++;
|
||||
}
|
||||
|
||||
static void animationTestInit(
|
||||
animation_t *anim,
|
||||
animationtestcallbacks_t *cb,
|
||||
keyframe_t *keyframes,
|
||||
uint16_t *keyframeCounts,
|
||||
const uint16_t layerCount
|
||||
) {
|
||||
memoryZero(cb, sizeof(animationtestcallbacks_t));
|
||||
animationInit(anim, keyframes, keyframeCounts, layerCount);
|
||||
anim->user = cb;
|
||||
anim->onUpdate = testOnUpdate;
|
||||
anim->onLoop = testOnLoop;
|
||||
anim->onComplete = testOnComplete;
|
||||
}
|
||||
|
||||
static void test_animationInitSingleLayerDuration(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 },
|
||||
};
|
||||
uint16_t keyframeCounts[1] = { 3 };
|
||||
animation_t anim;
|
||||
|
||||
animationInit(&anim, keyframes, keyframeCounts, 1);
|
||||
|
||||
assert_float_equal(anim.duration, 2.0f, 0.0001f);
|
||||
assert_int_equal(anim.layerCount, 1);
|
||||
assert_float_equal(anim.time, 0.0f, 0.0001f);
|
||||
assert_int_equal(anim.flags, 0);
|
||||
}
|
||||
|
||||
static void test_animationInitMultiLayerDurationIsMax(void **state) {
|
||||
// Both layers must share the same keyframe count - animationInit/
|
||||
// animationGetLayerValue flatten the array as layer * keyframeCounts[layer],
|
||||
// which only produces the correct offset when every layer's count matches.
|
||||
keyframe_t keyframes[4] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 3.0f, .value = 300.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[2] = { 2, 2 };
|
||||
animation_t anim;
|
||||
|
||||
animationInit(&anim, keyframes, keyframeCounts, 2);
|
||||
|
||||
// Layer 1's last keyframe (time=3) is later than layer 0's (time=1).
|
||||
assert_float_equal(anim.duration, 3.0f, 0.0001f);
|
||||
}
|
||||
|
||||
static void test_animationInitNullAsserts(void **state) {
|
||||
keyframe_t keyframes[1] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[1] = { 1 };
|
||||
animation_t anim;
|
||||
|
||||
expect_assert_failure(animationInit(NULL, keyframes, keyframeCounts, 1));
|
||||
expect_assert_failure(animationInit(&anim, NULL, keyframeCounts, 1));
|
||||
expect_assert_failure(animationInit(&anim, keyframes, NULL, 1));
|
||||
}
|
||||
|
||||
static void test_animationInitZeroLayerCountAsserts(void **state) {
|
||||
keyframe_t keyframes[1] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[1] = { 1 };
|
||||
animation_t anim;
|
||||
|
||||
expect_assert_failure(animationInit(&anim, keyframes, keyframeCounts, 0));
|
||||
}
|
||||
|
||||
static void test_animationInitUnsortedKeyframesAsserts(void **state) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[1] = { 2 };
|
||||
animation_t anim;
|
||||
|
||||
expect_assert_failure(animationInit(&anim, keyframes, keyframeCounts, 1));
|
||||
}
|
||||
|
||||
static void test_animationGetLayerValueSamplesEachLayerIndependently(
|
||||
void **state
|
||||
) {
|
||||
keyframe_t keyframes[4] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 2.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[2] = { 2, 2 };
|
||||
animation_t anim;
|
||||
|
||||
animationInit(&anim, keyframes, keyframeCounts, 2);
|
||||
anim.time = 1.0f;
|
||||
|
||||
assert_float_equal(animationGetLayerValue(&anim, 0), 10.0f, 0.0001f);
|
||||
assert_float_equal(animationGetLayerValue(&anim, 1), 50.0f, 0.0001f);
|
||||
}
|
||||
|
||||
static void test_animationGetLayerValueOutOfBoundsAsserts(void **state) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[1] = { 2 };
|
||||
animation_t anim;
|
||||
|
||||
animationInit(&anim, keyframes, keyframeCounts, 1);
|
||||
expect_assert_failure(animationGetLayerValue(&anim, 1));
|
||||
}
|
||||
|
||||
static void test_animationUpdateAdvancesTimeAndCallsOnUpdate(void **state) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[1] = { 2 };
|
||||
animation_t anim;
|
||||
animationtestcallbacks_t cb;
|
||||
|
||||
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
|
||||
animationUpdate(&anim, 0.5f);
|
||||
|
||||
assert_float_equal(anim.time, 0.5f, 0.0001f);
|
||||
assert_int_equal(cb.updateCount, 1);
|
||||
assert_float_equal(cb.lastValues[0], 5.0f, 0.0001f);
|
||||
assert_int_equal(cb.completeCount, 0);
|
||||
}
|
||||
|
||||
static void test_animationUpdatePlainPlayStopsAtEndAndFiresOnceOnly(
|
||||
void **state
|
||||
) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[1] = { 2 };
|
||||
animation_t anim;
|
||||
animationtestcallbacks_t cb;
|
||||
|
||||
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
|
||||
|
||||
// Overshoot the duration in a single update.
|
||||
animationUpdate(&anim, 5.0f);
|
||||
assert_float_equal(anim.time, 2.0f, 0.0001f);
|
||||
assert_float_equal(cb.lastValues[0], 20.0f, 0.0001f);
|
||||
assert_int_equal(cb.completeCount, 1);
|
||||
|
||||
// Further updates must not clamp past the end or refire onComplete.
|
||||
animationUpdate(&anim, 1.0f);
|
||||
assert_float_equal(anim.time, 2.0f, 0.0001f);
|
||||
assert_int_equal(cb.completeCount, 1);
|
||||
}
|
||||
|
||||
static void test_animationUpdateExactLandingOnEndFiresOnComplete(void **state) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[1] = { 2 };
|
||||
animation_t anim;
|
||||
animationtestcallbacks_t cb;
|
||||
|
||||
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
|
||||
|
||||
// deltaTime exactly matches the remaining distance to the boundary.
|
||||
animationUpdate(&anim, 2.0f);
|
||||
assert_float_equal(anim.time, 2.0f, 0.0001f);
|
||||
assert_int_equal(cb.completeCount, 1);
|
||||
}
|
||||
|
||||
static void test_animationUpdateLoopWrapsAndFiresOnLoop(void **state) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[1] = { 2 };
|
||||
animation_t anim;
|
||||
animationtestcallbacks_t cb;
|
||||
|
||||
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
|
||||
anim.flags = ANIMATION_FLAG_LOOP;
|
||||
|
||||
// 2.5 durations worth of time: wraps twice, lands at 1.0 into the third.
|
||||
animationUpdate(&anim, 5.0f);
|
||||
|
||||
assert_float_equal(anim.time, 1.0f, 0.0001f);
|
||||
assert_int_equal(cb.loopCount, 2);
|
||||
assert_int_equal(cb.completeCount, 0);
|
||||
}
|
||||
|
||||
static void test_animationUpdateLoopStopEndCompletesInsteadOfWrapping(
|
||||
void **state
|
||||
) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[1] = { 2 };
|
||||
animation_t anim;
|
||||
animationtestcallbacks_t cb;
|
||||
|
||||
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
|
||||
anim.flags = ANIMATION_FLAG_LOOP | ANIMATION_FLAG_STOP_END;
|
||||
|
||||
animationUpdate(&anim, 5.0f);
|
||||
|
||||
assert_float_equal(anim.time, 2.0f, 0.0001f);
|
||||
assert_int_equal(cb.loopCount, 0);
|
||||
assert_int_equal(cb.completeCount, 1);
|
||||
}
|
||||
|
||||
static void test_animationUpdateReversePlaysBackward(void **state) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[1] = { 2 };
|
||||
animation_t anim;
|
||||
animationtestcallbacks_t cb;
|
||||
|
||||
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
|
||||
anim.flags = ANIMATION_FLAG_REVERSE;
|
||||
anim.time = anim.duration;
|
||||
|
||||
animationUpdate(&anim, 0.5f);
|
||||
|
||||
assert_float_equal(anim.time, 1.5f, 0.0001f);
|
||||
assert_float_equal(cb.lastValues[0], 15.0f, 0.0001f);
|
||||
}
|
||||
|
||||
static void test_animationUpdateReverseStopsAtBeginning(void **state) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[1] = { 2 };
|
||||
animation_t anim;
|
||||
animationtestcallbacks_t cb;
|
||||
|
||||
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
|
||||
anim.flags = ANIMATION_FLAG_REVERSE;
|
||||
anim.time = anim.duration;
|
||||
|
||||
animationUpdate(&anim, 10.0f);
|
||||
|
||||
assert_float_equal(anim.time, 0.0f, 0.0001f);
|
||||
assert_float_equal(cb.lastValues[0], 0.0f, 0.0001f);
|
||||
assert_int_equal(cb.completeCount, 1);
|
||||
}
|
||||
|
||||
static void test_animationUpdatePingpongBouncesForeverWithoutStopFlags(
|
||||
void **state
|
||||
) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[1] = { 2 };
|
||||
animation_t anim;
|
||||
animationtestcallbacks_t cb;
|
||||
|
||||
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
|
||||
anim.flags = ANIMATION_FLAG_PINGPONG;
|
||||
|
||||
// 0 -> 2 (2s consumed), reflect, backward 1s more -> time=1, going backward.
|
||||
animationUpdate(&anim, 3.0f);
|
||||
assert_float_equal(anim.time, 1.0f, 0.0001f);
|
||||
assert_true((anim.flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) != 0);
|
||||
assert_int_equal(cb.completeCount, 0);
|
||||
|
||||
// Continue backward to 0 (1s), reflect, forward 1s more -> time=1, forward.
|
||||
animationUpdate(&anim, 2.0f);
|
||||
assert_float_equal(anim.time, 1.0f, 0.0001f);
|
||||
assert_true((anim.flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) == 0);
|
||||
assert_int_equal(cb.completeCount, 0);
|
||||
}
|
||||
|
||||
static void test_animationUpdatePingpongStopEndCompletesAtEnd(void **state) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[1] = { 2 };
|
||||
animation_t anim;
|
||||
animationtestcallbacks_t cb;
|
||||
|
||||
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
|
||||
anim.flags = ANIMATION_FLAG_PINGPONG | ANIMATION_FLAG_STOP_END;
|
||||
|
||||
animationUpdate(&anim, 10.0f);
|
||||
|
||||
assert_float_equal(anim.time, 2.0f, 0.0001f);
|
||||
assert_int_equal(cb.completeCount, 1);
|
||||
}
|
||||
|
||||
static void test_animationUpdatePingpongStopBeginningCompletesAtStart(
|
||||
void **state
|
||||
) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[1] = { 2 };
|
||||
animation_t anim;
|
||||
animationtestcallbacks_t cb;
|
||||
|
||||
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1);
|
||||
anim.flags = ANIMATION_FLAG_PINGPONG | ANIMATION_FLAG_STOP_BEGINNING;
|
||||
anim.time = anim.duration;
|
||||
anim.flags |= ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD;
|
||||
|
||||
animationUpdate(&anim, 10.0f);
|
||||
|
||||
assert_float_equal(anim.time, 0.0f, 0.0001f);
|
||||
assert_int_equal(cb.completeCount, 1);
|
||||
}
|
||||
|
||||
static void test_animationUpdateMultiLayerCallsOnUpdatePerLayer(void **state) {
|
||||
keyframe_t keyframes[4] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 2.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[2] = { 2, 2 };
|
||||
animation_t anim;
|
||||
animationtestcallbacks_t cb;
|
||||
|
||||
animationTestInit(&anim, &cb, keyframes, keyframeCounts, 2);
|
||||
animationUpdate(&anim, 1.0f);
|
||||
|
||||
assert_int_equal(cb.updateCount, 2);
|
||||
assert_float_equal(cb.lastValues[0], 10.0f, 0.0001f);
|
||||
assert_float_equal(cb.lastValues[1], 50.0f, 0.0001f);
|
||||
}
|
||||
|
||||
static void test_animationUpdateNullAsserts(void **state) {
|
||||
expect_assert_failure(animationUpdate(NULL, 1.0f));
|
||||
}
|
||||
|
||||
static void test_animationUpdateNegativeDeltaTimeAsserts(void **state) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[1] = { 2 };
|
||||
animation_t anim;
|
||||
|
||||
animationInit(&anim, keyframes, keyframeCounts, 1);
|
||||
expect_assert_failure(animationUpdate(&anim, -1.0f));
|
||||
}
|
||||
|
||||
static void test_animationInitZeroDurationAsserts(void **state) {
|
||||
keyframe_t keyframes[1] = {
|
||||
{ .time = 0.0f, .value = 5.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[1] = { 1 };
|
||||
animation_t anim;
|
||||
|
||||
expect_assert_failure(animationInit(&anim, keyframes, keyframeCounts, 1));
|
||||
}
|
||||
|
||||
static void test_animationUpdateLoopAndPingpongTogetherAsserts(void **state) {
|
||||
keyframe_t keyframes[2] = {
|
||||
{ .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR },
|
||||
{ .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR },
|
||||
};
|
||||
uint16_t keyframeCounts[1] = { 2 };
|
||||
animation_t anim;
|
||||
|
||||
animationInit(&anim, keyframes, keyframeCounts, 1);
|
||||
anim.flags = ANIMATION_FLAG_LOOP | ANIMATION_FLAG_PINGPONG;
|
||||
expect_assert_failure(animationUpdate(&anim, 0.1f));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_animationInitSingleLayerDuration),
|
||||
cmocka_unit_test(test_animationInitMultiLayerDurationIsMax),
|
||||
cmocka_unit_test(test_animationInitNullAsserts),
|
||||
cmocka_unit_test(test_animationInitZeroLayerCountAsserts),
|
||||
cmocka_unit_test(test_animationInitUnsortedKeyframesAsserts),
|
||||
cmocka_unit_test(test_animationGetLayerValueSamplesEachLayerIndependently),
|
||||
cmocka_unit_test(test_animationGetLayerValueOutOfBoundsAsserts),
|
||||
cmocka_unit_test(test_animationUpdateAdvancesTimeAndCallsOnUpdate),
|
||||
cmocka_unit_test(test_animationUpdatePlainPlayStopsAtEndAndFiresOnceOnly),
|
||||
cmocka_unit_test(test_animationUpdateExactLandingOnEndFiresOnComplete),
|
||||
cmocka_unit_test(test_animationUpdateLoopWrapsAndFiresOnLoop),
|
||||
cmocka_unit_test(test_animationUpdateLoopStopEndCompletesInsteadOfWrapping),
|
||||
cmocka_unit_test(test_animationUpdateReversePlaysBackward),
|
||||
cmocka_unit_test(test_animationUpdateReverseStopsAtBeginning),
|
||||
cmocka_unit_test(
|
||||
test_animationUpdatePingpongBouncesForeverWithoutStopFlags
|
||||
),
|
||||
cmocka_unit_test(test_animationUpdatePingpongStopEndCompletesAtEnd),
|
||||
cmocka_unit_test(test_animationUpdatePingpongStopBeginningCompletesAtStart),
|
||||
cmocka_unit_test(test_animationUpdateMultiLayerCallsOnUpdatePerLayer),
|
||||
cmocka_unit_test(test_animationUpdateNullAsserts),
|
||||
cmocka_unit_test(test_animationUpdateNegativeDeltaTimeAsserts),
|
||||
cmocka_unit_test(test_animationInitZeroDurationAsserts),
|
||||
cmocka_unit_test(test_animationUpdateLoopAndPingpongTogetherAsserts),
|
||||
};
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
include(dusktest)
|
||||
|
||||
# Tests
|
||||
dusktest(test_inventory.c)
|
||||
@@ -1,433 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "rpg/item/inventory.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
static itemid_t ITEM_ID_POTION;
|
||||
static itemid_t ITEM_ID_POTATO;
|
||||
static itemid_t ITEM_ID_APPLE;
|
||||
|
||||
// Registers three test items (POTION as MEDICINE, POTATO/APPLE as FOOD)
|
||||
// directly into the global ITEM registry -- these tests exercise the
|
||||
// inventory/backpack layer only, not the JSON-driven itemInit() pipeline.
|
||||
static int_t itemFixtureSetup(void **state) {
|
||||
memoryZero(&ITEM, sizeof(ITEM));
|
||||
ITEM.typeCount = ITEM_TYPE_NULL + 1;
|
||||
ITEM.defCount = ITEM_ID_NULL + 1;
|
||||
|
||||
const itemtype_t medicine = itemTypeGetOrCreate("MEDICINE");
|
||||
const itemtype_t food = itemTypeGetOrCreate("FOOD");
|
||||
|
||||
ITEM_ID_POTION = ITEM.defCount++;
|
||||
ITEM.defs[ITEM_ID_POTION].type = medicine;
|
||||
|
||||
ITEM_ID_POTATO = ITEM.defCount++;
|
||||
ITEM.defs[ITEM_ID_POTATO].type = food;
|
||||
|
||||
ITEM_ID_APPLE = ITEM.defCount++;
|
||||
ITEM.defs[ITEM_ID_APPLE].type = food;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void test_inventoryInit(void **state) {
|
||||
|
||||
|
||||
inventorystack_t storage[5];
|
||||
inventory_t inventory;
|
||||
|
||||
inventoryInit(&inventory, storage, 5);
|
||||
|
||||
// Should setup struct
|
||||
assert_non_null(inventory.storage);
|
||||
assert_int_equal(inventory.storageSize, 5);
|
||||
|
||||
// Should zero the item ids.
|
||||
for(size_t i = 0; i < inventory.storageSize; i++) {
|
||||
assert_int_equal(inventory.storage[i].item, ITEM_ID_NULL);
|
||||
assert_int_equal(inventory.storage[i].quantity, 0);
|
||||
}
|
||||
|
||||
// Should fail when given NULL inventory pointer
|
||||
expect_assert_failure(inventoryInit(NULL, storage, 5));
|
||||
|
||||
// Should fail when given NULL storage pointer
|
||||
expect_assert_failure(inventoryInit(&inventory, NULL, 5));
|
||||
|
||||
// Should fail when given zero storage size
|
||||
expect_assert_failure(inventoryInit(&inventory, storage, 0));
|
||||
}
|
||||
|
||||
static void test_inventoryItemExists(void **state) {
|
||||
|
||||
|
||||
inventorystack_t storage[5];
|
||||
inventory_t inventory;
|
||||
inventoryInit(&inventory, storage, 5);
|
||||
|
||||
// Initially should not exist
|
||||
assert_false(inventoryItemExists(&inventory, ITEM_ID_POTION));
|
||||
|
||||
// Add item
|
||||
inventorySet(&inventory, ITEM_ID_POTION, 3);
|
||||
assert_true(inventoryItemExists(&inventory, ITEM_ID_POTION));
|
||||
|
||||
// Remove item
|
||||
inventorySet(&inventory, ITEM_ID_POTION, 0);
|
||||
assert_false(inventoryItemExists(&inventory, ITEM_ID_POTION));
|
||||
|
||||
// Should fail when given NULL inventory pointer
|
||||
expect_assert_failure(inventoryItemExists(NULL, ITEM_ID_POTION));
|
||||
|
||||
// Should fail when given NULL storage pointer
|
||||
inventory.storage = NULL;
|
||||
expect_assert_failure(inventoryItemExists(&inventory, ITEM_ID_POTION));
|
||||
inventory.storage = storage;
|
||||
|
||||
// Should fail when given zero storage size
|
||||
inventory.storageSize = 0;
|
||||
expect_assert_failure(inventoryItemExists(&inventory, ITEM_ID_POTION));
|
||||
inventory.storageSize = 5;
|
||||
|
||||
// Should fail when given ITEM_ID_NULL
|
||||
expect_assert_failure(inventoryItemExists(&inventory, ITEM_ID_NULL));
|
||||
|
||||
// Should fail if item has zero quantity somehow
|
||||
inventorySet(&inventory, ITEM_ID_POTION, 3);
|
||||
inventory.storage[0].quantity = 0;
|
||||
expect_assert_failure(inventoryItemExists(&inventory, ITEM_ID_POTION));
|
||||
}
|
||||
|
||||
static void test_inventorySet(void **state) {
|
||||
|
||||
|
||||
inventorystack_t storage[5];
|
||||
inventory_t inventory;
|
||||
inventoryInit(&inventory, storage, 5);
|
||||
|
||||
// Set item quantity
|
||||
inventorySet(&inventory, ITEM_ID_POTION, 4);
|
||||
assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
|
||||
assert_int_equal(inventory.storage[0].quantity, 4);
|
||||
|
||||
// Update item quantity
|
||||
inventorySet(&inventory, ITEM_ID_POTION, 2);
|
||||
assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
|
||||
assert_int_equal(inventory.storage[0].quantity, 2);
|
||||
|
||||
// Remove item by setting quantity to 0
|
||||
inventorySet(&inventory, ITEM_ID_POTION, 0);
|
||||
assert_int_equal(inventory.storage[0].item, ITEM_ID_NULL);
|
||||
|
||||
// Setting multiple items should not cause issues
|
||||
inventorySet(&inventory, ITEM_ID_POTION, 1);
|
||||
inventorySet(&inventory, ITEM_ID_POTATO, 5);
|
||||
assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
|
||||
assert_int_equal(inventory.storage[0].quantity, 1);
|
||||
assert_int_equal(inventory.storage[1].item, ITEM_ID_POTATO);
|
||||
assert_int_equal(inventory.storage[1].quantity, 5);
|
||||
assert_int_equal(inventory.storage[2].item, ITEM_ID_NULL);
|
||||
|
||||
// Setting early item to 0 should shift others down
|
||||
inventorySet(&inventory, ITEM_ID_POTION, 0);// Item 0 removed
|
||||
assert_int_equal(inventory.storage[0].item, ITEM_ID_POTATO);
|
||||
assert_int_equal(inventory.storage[0].quantity, 5);
|
||||
assert_int_equal(inventory.storage[1].item, ITEM_ID_NULL);
|
||||
|
||||
// Setting non-existing item should add it
|
||||
inventorySet(&inventory, ITEM_ID_POTION, 3);
|
||||
assert_int_equal(inventory.storage[1].item, ITEM_ID_POTION);
|
||||
assert_int_equal(inventory.storage[1].quantity, 3);
|
||||
|
||||
// Should fail when given NULL inventory pointer
|
||||
expect_assert_failure(inventorySet(NULL, ITEM_ID_POTION, 3));
|
||||
|
||||
// Should fail when given NULL storage pointer
|
||||
inventory.storage = NULL;
|
||||
expect_assert_failure(inventorySet(&inventory, ITEM_ID_POTION, 3));
|
||||
inventory.storage = storage;
|
||||
|
||||
// Should fail when given zero storage size
|
||||
inventory.storageSize = 0;
|
||||
expect_assert_failure(inventorySet(&inventory, ITEM_ID_POTION, 3));
|
||||
inventory.storageSize = 5;
|
||||
|
||||
// Should fail when given ITEM_ID_NULL
|
||||
expect_assert_failure(inventorySet(&inventory, ITEM_ID_NULL, 3));
|
||||
}
|
||||
|
||||
static void test_inventoryAdd(void **state) {
|
||||
|
||||
|
||||
inventorystack_t storage[5];
|
||||
inventory_t inventory;
|
||||
inventoryInit(&inventory, storage, 5);
|
||||
|
||||
// Add item quantity
|
||||
inventoryAdd(&inventory, ITEM_ID_POTION, 4);
|
||||
assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
|
||||
assert_int_equal(inventory.storage[0].quantity, 4);
|
||||
|
||||
// Add more to existing item
|
||||
inventoryAdd(&inventory, ITEM_ID_POTION, 3);
|
||||
assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
|
||||
assert_int_equal(inventory.storage[0].quantity, 7);
|
||||
|
||||
// Adding item that would overflow should assert
|
||||
expect_assert_failure(inventoryAdd(&inventory, ITEM_ID_POTION, 250));
|
||||
|
||||
// Should fail when given NULL inventory pointer
|
||||
expect_assert_failure(inventoryAdd(NULL, ITEM_ID_POTION, 3));
|
||||
|
||||
// Should fail when given NULL storage pointer
|
||||
inventory.storage = NULL;
|
||||
expect_assert_failure(inventoryAdd(&inventory, ITEM_ID_POTION, 3));
|
||||
inventory.storage = storage;
|
||||
|
||||
// Should fail when given zero storage size
|
||||
inventory.storageSize = 0;
|
||||
expect_assert_failure(inventoryAdd(&inventory, ITEM_ID_POTION, 3));
|
||||
inventory.storageSize = 5;
|
||||
|
||||
// Should fail when given ITEM_ID_NULL
|
||||
expect_assert_failure(inventoryAdd(&inventory, ITEM_ID_NULL, 3));
|
||||
}
|
||||
|
||||
static void test_inventoryRemove(void **state) {
|
||||
|
||||
|
||||
inventorystack_t storage[5];
|
||||
inventory_t inventory;
|
||||
inventoryInit(&inventory, storage, 5);
|
||||
|
||||
// Add item and then remove it
|
||||
inventorySet(&inventory, ITEM_ID_POTION, 5);
|
||||
inventoryRemove(&inventory, ITEM_ID_POTION);
|
||||
assert_int_equal(inventory.storage[0].item, ITEM_ID_NULL);
|
||||
|
||||
// Removing non-existing item should do nothing
|
||||
inventoryRemove(&inventory, ITEM_ID_POTION);
|
||||
assert_int_equal(inventory.storage[0].item, ITEM_ID_NULL);
|
||||
|
||||
// Should fail when given NULL inventory pointer
|
||||
expect_assert_failure(inventoryRemove(NULL, ITEM_ID_POTION));
|
||||
|
||||
// Should fail when given NULL storage pointer
|
||||
inventory.storage = NULL;
|
||||
expect_assert_failure(inventoryRemove(&inventory, ITEM_ID_POTION));
|
||||
inventory.storage = storage;
|
||||
|
||||
// Should fail when given zero storage size
|
||||
inventory.storageSize = 0;
|
||||
expect_assert_failure(inventoryRemove(&inventory, ITEM_ID_POTION));
|
||||
inventory.storageSize = 5;
|
||||
|
||||
// Should fail when given ITEM_ID_NULL
|
||||
expect_assert_failure(inventoryRemove(&inventory, ITEM_ID_NULL));
|
||||
}
|
||||
|
||||
static void test_inventoryGetCount(void **state) {
|
||||
|
||||
|
||||
inventorystack_t storage[5];
|
||||
inventory_t inventory;
|
||||
inventoryInit(&inventory, storage, 5);
|
||||
|
||||
// Initially should be zero
|
||||
assert_int_equal(inventoryGetCount(&inventory, ITEM_ID_POTION), 0);
|
||||
|
||||
// Add item and check count
|
||||
inventorySet(&inventory, ITEM_ID_POTION, 4);
|
||||
assert_int_equal(inventoryGetCount(&inventory, ITEM_ID_POTION), 4);
|
||||
|
||||
// Update item quantity and check count
|
||||
inventorySet(&inventory, ITEM_ID_POTION, 2);
|
||||
assert_int_equal(inventoryGetCount(&inventory, ITEM_ID_POTION), 2);
|
||||
|
||||
// Remove item and check count
|
||||
inventorySet(&inventory, ITEM_ID_POTION, 0);
|
||||
assert_int_equal(inventoryGetCount(&inventory, ITEM_ID_POTION), 0);
|
||||
|
||||
// Should fail when given NULL inventory pointer
|
||||
expect_assert_failure(inventoryGetCount(NULL, ITEM_ID_POTION));
|
||||
|
||||
// Should fail when given NULL storage pointer
|
||||
inventory.storage = NULL;
|
||||
expect_assert_failure(inventoryGetCount(&inventory, ITEM_ID_POTION));
|
||||
inventory.storage = storage;
|
||||
|
||||
// Should fail when given zero storage size
|
||||
inventory.storageSize = 0;
|
||||
expect_assert_failure(inventoryGetCount(&inventory, ITEM_ID_POTION));
|
||||
inventory.storageSize = 5;
|
||||
|
||||
// Should fail when given ITEM_ID_NULL
|
||||
expect_assert_failure(inventoryGetCount(&inventory, ITEM_ID_NULL));
|
||||
}
|
||||
|
||||
static void test_inventoryIsFull(void **state) {
|
||||
|
||||
|
||||
inventorystack_t storage[2];
|
||||
inventory_t inventory;
|
||||
inventoryInit(&inventory, storage, 2);
|
||||
|
||||
// Initially should not be full
|
||||
assert_false(inventoryIsFull(&inventory));
|
||||
|
||||
// Fill inventory
|
||||
inventorySet(&inventory, ITEM_ID_POTION, 1);
|
||||
inventorySet(&inventory, ITEM_ID_POTATO, 1);
|
||||
assert_true(inventoryIsFull(&inventory));
|
||||
|
||||
// Remove one item
|
||||
inventoryRemove(&inventory, ITEM_ID_POTION);
|
||||
assert_false(inventoryIsFull(&inventory));
|
||||
|
||||
// Add one item back
|
||||
inventorySet(&inventory, ITEM_ID_POTION, 1);
|
||||
assert_true(inventoryIsFull(&inventory));
|
||||
|
||||
// Should fail when given NULL inventory pointer
|
||||
expect_assert_failure(inventoryIsFull(NULL));
|
||||
|
||||
// Should fail when given NULL storage pointer
|
||||
inventory.storage = NULL;
|
||||
expect_assert_failure(inventoryIsFull(&inventory));
|
||||
inventory.storage = storage;
|
||||
|
||||
// Should fail when given zero storage size
|
||||
inventory.storageSize = 0;
|
||||
expect_assert_failure(inventoryIsFull(&inventory));
|
||||
inventory.storageSize = 2;
|
||||
}
|
||||
|
||||
static void test_inventoryItemFull(void **state) {
|
||||
|
||||
|
||||
inventorystack_t storage[2];
|
||||
inventory_t inventory;
|
||||
inventoryInit(&inventory, storage, 2);
|
||||
|
||||
// Initially should not be full
|
||||
assert_false(inventoryItemFull(&inventory, ITEM_ID_POTION));
|
||||
|
||||
// Add some potions, but not too many.
|
||||
inventorySet(&inventory, ITEM_ID_POTION, 100);
|
||||
assert_false(inventoryItemFull(&inventory, ITEM_ID_POTION));
|
||||
|
||||
// Fill with potions
|
||||
inventorySet(&inventory, ITEM_ID_POTION, ITEM_STACK_QUANTITY_MAX);
|
||||
assert_true(inventoryItemFull(&inventory, ITEM_ID_POTION));
|
||||
|
||||
// Add potatoes, should not affect potions
|
||||
inventorySet(&inventory, ITEM_ID_POTATO, 50);
|
||||
assert_true(inventoryItemFull(&inventory, ITEM_ID_POTION));
|
||||
assert_false(inventoryItemFull(&inventory, ITEM_ID_POTATO));
|
||||
|
||||
// Remove some potions
|
||||
inventorySet(&inventory, ITEM_ID_POTION, 200);
|
||||
assert_false(inventoryItemFull(&inventory, ITEM_ID_POTION));
|
||||
|
||||
// Fill with potatoes
|
||||
inventorySet(&inventory, ITEM_ID_POTATO, ITEM_STACK_QUANTITY_MAX);
|
||||
assert_false(inventoryItemFull(&inventory, ITEM_ID_POTION));
|
||||
assert_true(inventoryItemFull(&inventory, ITEM_ID_POTATO));
|
||||
|
||||
// Should fail when given NULL inventory pointer
|
||||
expect_assert_failure(inventoryItemFull(NULL, ITEM_ID_POTION));
|
||||
|
||||
// Should fail when given NULL storage pointer
|
||||
inventory.storage = NULL;
|
||||
expect_assert_failure(inventoryItemFull(&inventory, ITEM_ID_POTION));
|
||||
inventory.storage = storage;
|
||||
|
||||
// Should fail when given zero storage size
|
||||
inventory.storageSize = 0;
|
||||
expect_assert_failure(inventoryItemFull(&inventory, ITEM_ID_POTION));
|
||||
inventory.storageSize = 2;
|
||||
}
|
||||
|
||||
static void test_inventorySort(void **state) {
|
||||
|
||||
|
||||
inventorystack_t storage[5];
|
||||
inventory_t inventory;
|
||||
inventoryInit(&inventory, storage, 5);
|
||||
|
||||
// Add items out of order
|
||||
inventorySet(&inventory, ITEM_ID_POTATO, 3);
|
||||
inventorySet(&inventory, ITEM_ID_APPLE, 5);
|
||||
inventorySet(&inventory, ITEM_ID_POTION, 2);
|
||||
|
||||
// Sort by ID
|
||||
inventorySort(&inventory, INVENTORY_SORT_BY_ID, false);
|
||||
|
||||
// Check order
|
||||
assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
|
||||
assert_int_equal(inventory.storage[1].item, ITEM_ID_POTATO);
|
||||
assert_int_equal(inventory.storage[2].item, ITEM_ID_APPLE);
|
||||
|
||||
// Sort by ID reverse
|
||||
inventorySort(&inventory, INVENTORY_SORT_BY_ID, true);
|
||||
assert_int_equal(inventory.storage[0].item, ITEM_ID_APPLE);
|
||||
assert_int_equal(inventory.storage[1].item, ITEM_ID_POTATO);
|
||||
assert_int_equal(inventory.storage[2].item, ITEM_ID_POTION);
|
||||
|
||||
// Sort by type ascending - POTION (MEDICINE) sorts before POTATO/APPLE
|
||||
// (both FOOD). The two FOOD items compare equal, so their relative order
|
||||
// isn't guaranteed (qsort is not stable) - only the type grouping is.
|
||||
inventorySort(&inventory, INVENTORY_SORT_BY_TYPE, false);
|
||||
assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION);
|
||||
assert_true(
|
||||
(inventory.storage[1].item == ITEM_ID_POTATO &&
|
||||
inventory.storage[2].item == ITEM_ID_APPLE) ||
|
||||
(inventory.storage[1].item == ITEM_ID_APPLE &&
|
||||
inventory.storage[2].item == ITEM_ID_POTATO)
|
||||
);
|
||||
|
||||
// Sort by type reverse - FOOD items first (order unspecified), POTION last.
|
||||
inventorySort(&inventory, INVENTORY_SORT_BY_TYPE, true);
|
||||
assert_int_equal(inventory.storage[2].item, ITEM_ID_POTION);
|
||||
assert_true(
|
||||
(inventory.storage[0].item == ITEM_ID_POTATO &&
|
||||
inventory.storage[1].item == ITEM_ID_APPLE) ||
|
||||
(inventory.storage[0].item == ITEM_ID_APPLE &&
|
||||
inventory.storage[1].item == ITEM_ID_POTATO)
|
||||
);
|
||||
|
||||
// Should fail when given NULL inventory pointer
|
||||
expect_assert_failure(inventorySort(NULL, INVENTORY_SORT_BY_ID, false));
|
||||
|
||||
// Should fail when given NULL storage pointer
|
||||
inventory.storage = NULL;
|
||||
expect_assert_failure(inventorySort(&inventory, INVENTORY_SORT_BY_ID, false));
|
||||
inventory.storage = storage;
|
||||
|
||||
// Should fail when given zero storage size
|
||||
inventory.storageSize = 0;
|
||||
expect_assert_failure(inventorySort(&inventory, INVENTORY_SORT_BY_ID, false));
|
||||
inventory.storageSize = 5;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_inventoryInit),
|
||||
cmocka_unit_test(test_inventoryItemExists),
|
||||
cmocka_unit_test(test_inventorySet),
|
||||
cmocka_unit_test(test_inventoryAdd),
|
||||
cmocka_unit_test(test_inventoryRemove),
|
||||
cmocka_unit_test(test_inventoryGetCount),
|
||||
cmocka_unit_test(test_inventoryIsFull),
|
||||
cmocka_unit_test(test_inventoryItemFull),
|
||||
cmocka_unit_test(test_inventorySort),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, itemFixtureSetup, NULL);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
include(dusktest)
|
||||
|
||||
# Tests
|
||||
dusktest(test_rpg.c)
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(overworld)
|
||||
add_subdirectory(battle)
|
||||
add_subdirectory(entity)
|
||||
add_subdirectory(cutscene)
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
include(dusktest)
|
||||
|
||||
# Tests
|
||||
dusktest(test_battle.c)
|
||||
@@ -1,271 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
|
||||
// TEMPORARY: hardcoded pending a real moves.jsonc-backed registry lookup.
|
||||
#define BATTLE_MOVE_ID_ATTACK ((battlemoveid_t)1)
|
||||
|
||||
static battlefighter_t *addFighter(
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const uint16_t attack,
|
||||
const uint16_t defense,
|
||||
const uint16_t speed,
|
||||
const uint16_t healthMax
|
||||
) {
|
||||
const battlefighterstats_t stats = {
|
||||
.attack = attack, .defense = defense, .speed = speed
|
||||
};
|
||||
return battleAddFighter(team, controller, stats, healthMax, 0);
|
||||
}
|
||||
|
||||
static void test_battleStartEntersOpeningThenPreRound(void **state) {
|
||||
|
||||
|
||||
battleInit();
|
||||
addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 10, 0, 10, 20
|
||||
);
|
||||
addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 10, 0, 5, 20
|
||||
);
|
||||
|
||||
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_OPENING);
|
||||
|
||||
// OPENING only transitions to PRE_ROUND; it doesn't run PRE_ROUND's logic
|
||||
// in the same call.
|
||||
battleUpdate();
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_PRE_ROUND);
|
||||
assert_int_equal(BATTLE.executionCount, 0);
|
||||
|
||||
// PRE_ROUND builds the execution order and moves on to selection.
|
||||
battleUpdate();
|
||||
assert_int_equal(BATTLE.executionCount, 2);
|
||||
assert_true(
|
||||
BATTLE.state == BATTLE_STATE_PLAYER_SELECTION ||
|
||||
BATTLE.state == BATTLE_STATE_AI_SELECTION
|
||||
);
|
||||
}
|
||||
|
||||
static void test_battlePlayerSelectionAdvancesAndSkipsDecidedFighters(
|
||||
void **state
|
||||
) {
|
||||
|
||||
|
||||
battleInit();
|
||||
battlefighter_t *ally1 = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 100, 20
|
||||
);
|
||||
battlefighter_t *ally2 = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20
|
||||
);
|
||||
battlefighter_t *enemy = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 50, 20
|
||||
);
|
||||
|
||||
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
|
||||
battleUpdate();// OPENING -> PRE_ROUND
|
||||
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION, positions the cursor
|
||||
|
||||
// ally1 is fastest, so it's first to decide.
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_PLAYER_SELECTION);
|
||||
assert_ptr_equal(battleGetCurrentFighter(), ally1);
|
||||
|
||||
// A forced move (as a scripted-battle cutscene item would push) marks
|
||||
// ally2 as already decided, so PLAYER_SELECTION must skip it.
|
||||
battleQueueMove(ally2->id, BATTLE_MOVE_ID_ATTACK, enemy->id);
|
||||
|
||||
battlePlayerAttack(enemy->id);
|
||||
assert_int_equal(BATTLE.moves[ally1->id].move, BATTLE_MOVE_ID_ATTACK);
|
||||
assert_int_equal(BATTLE.moves[ally1->id].targets[0], enemy->id);
|
||||
|
||||
// ally2 already had a decision queued, and enemy is AI-controlled, so
|
||||
// there's nothing left for PLAYER_SELECTION -- it should have moved on.
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_AI_SELECTION);
|
||||
}
|
||||
|
||||
static void test_battleAiSelectionAutoQueuesUndecidedAiFighters(void **state) {
|
||||
|
||||
|
||||
battleInit();
|
||||
battlefighter_t *ally = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20
|
||||
);
|
||||
battlefighter_t *enemy = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 5, 20
|
||||
);
|
||||
|
||||
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
|
||||
battleUpdate();// OPENING -> PRE_ROUND
|
||||
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
|
||||
|
||||
battlePlayerAttack(enemy->id);// ally decides, selection moves to AI
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_AI_SELECTION);
|
||||
|
||||
battleUpdate();// AI_SELECTION auto-decides for enemy
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_MOVES_EXECUTING);
|
||||
assert_int_equal(BATTLE.moves[enemy->id].move, BATTLE_MOVE_ID_ATTACK);
|
||||
assert_int_equal(BATTLE.moves[enemy->id].targets[0], ally->id);
|
||||
}
|
||||
|
||||
static void test_battleMovesExecutingResolvesInSpeedOrderAndFizzles(
|
||||
void **state
|
||||
) {
|
||||
|
||||
|
||||
battleInit();
|
||||
// ally1 is fast enough to kill enemy1 before enemy1 or ally2 act. enemy2
|
||||
// keeps the battle alive so execution continues to ally2's now-moot
|
||||
// attack against the already-dead enemy1.
|
||||
battlefighter_t *ally1 = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 50, 0, 100, 20
|
||||
);
|
||||
battlefighter_t *ally2 = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 50, 0, 10, 20
|
||||
);
|
||||
battlefighter_t *enemy1 = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 1, 0, 50, 10
|
||||
);
|
||||
battlefighter_t *enemy2 = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 1, 0, 1, 100
|
||||
);
|
||||
|
||||
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
|
||||
battleUpdate();// OPENING -> PRE_ROUND
|
||||
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION (ally1 first, fastest)
|
||||
|
||||
assert_ptr_equal(battleGetCurrentFighter(), ally1);
|
||||
battlePlayerAttack(enemy1->id);
|
||||
assert_ptr_equal(battleGetCurrentFighter(), ally2);
|
||||
battlePlayerAttack(enemy1->id);// both allies target enemy1
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_AI_SELECTION);
|
||||
|
||||
battleUpdate();// AI_SELECTION
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_MOVES_EXECUTING);
|
||||
|
||||
// Execution order: ally1 (100), enemy1 (50), ally2 (10), enemy2 (1).
|
||||
battleUpdate();// ally1 kills enemy1
|
||||
assert_false(battleFighterIsAlive(enemy1));
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_MOVES_EXECUTING);// enemy2 alive
|
||||
|
||||
battleUpdate();// enemy1's own (now dead) turn is skipped
|
||||
battleUpdate();// ally2's attack against the dead enemy1 fizzles silently
|
||||
assert_int_equal(enemy1->health, 0);// no double-kill, no underflow
|
||||
|
||||
battleUpdate();// enemy2 acts
|
||||
battleUpdate();// all 4 slots processed -> POST_ROUND
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_POST_ROUND);
|
||||
|
||||
battleUpdate();// POST_ROUND -> PRE_ROUND, round advances
|
||||
assert_int_equal(BATTLE.round, 2);
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_PRE_ROUND);
|
||||
}
|
||||
|
||||
static void test_battleWinEndsInStateEnded(void **state) {
|
||||
|
||||
|
||||
battleInit();
|
||||
battlefighter_t *ally = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 50, 0, 10, 20
|
||||
);
|
||||
battlefighter_t *enemy = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 1, 0, 5, 10
|
||||
);
|
||||
|
||||
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
|
||||
battleUpdate();// OPENING -> PRE_ROUND
|
||||
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
|
||||
|
||||
battlePlayerAttack(enemy->id);// selection -> AI_SELECTION
|
||||
battleUpdate();// AI_SELECTION -> MOVES_EXECUTING
|
||||
battleUpdate();// ally kills the only enemy
|
||||
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_ENDED);
|
||||
assert_int_equal(BATTLE.result, BATTLE_RESULT_WIN);
|
||||
}
|
||||
|
||||
static void test_battleLossEndsInStateEnded(void **state) {
|
||||
|
||||
|
||||
battleInit();
|
||||
battlefighter_t *ally = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 1, 0, 5, 10
|
||||
);
|
||||
battlefighter_t *enemy = addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 50, 0, 10, 20
|
||||
);
|
||||
|
||||
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
|
||||
battleUpdate();// OPENING -> PRE_ROUND
|
||||
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
|
||||
|
||||
battlePlayerAttack(enemy->id);// selection -> AI_SELECTION
|
||||
battleUpdate();// AI_SELECTION -> MOVES_EXECUTING
|
||||
battleUpdate();// ally attacks first (irrelevant to outcome)
|
||||
battleUpdate();// enemy kills the only ally
|
||||
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_ENDED);
|
||||
assert_int_equal(BATTLE.result, BATTLE_RESULT_LOSS);
|
||||
}
|
||||
|
||||
static void test_battleFleeEndsInStateEndedImmediately(void **state) {
|
||||
|
||||
|
||||
battleInit();
|
||||
addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20
|
||||
);
|
||||
addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 5, 20
|
||||
);
|
||||
|
||||
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
|
||||
battleUpdate();// OPENING -> PRE_ROUND
|
||||
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
|
||||
|
||||
battlePlayerFlee();
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_ENDED);
|
||||
assert_int_equal(BATTLE.result, BATTLE_RESULT_FLED);
|
||||
}
|
||||
|
||||
static void test_battleFleeUnavailableIsIgnored(void **state) {
|
||||
|
||||
|
||||
battleInit();
|
||||
addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20
|
||||
);
|
||||
addFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 5, 20
|
||||
);
|
||||
|
||||
battleStart(BATTLE_ENCOUNTER_REGULAR, false);
|
||||
battleUpdate();// OPENING -> PRE_ROUND
|
||||
battleUpdate();// PRE_ROUND -> PLAYER_SELECTION
|
||||
|
||||
battlePlayerFlee();
|
||||
assert_int_equal(BATTLE.state, BATTLE_STATE_PLAYER_SELECTION);
|
||||
assert_int_equal(BATTLE.result, BATTLE_RESULT_NONE);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_battleStartEntersOpeningThenPreRound),
|
||||
cmocka_unit_test(test_battlePlayerSelectionAdvancesAndSkipsDecidedFighters),
|
||||
cmocka_unit_test(test_battleAiSelectionAutoQueuesUndecidedAiFighters),
|
||||
cmocka_unit_test(test_battleMovesExecutingResolvesInSpeedOrderAndFizzles),
|
||||
cmocka_unit_test(test_battleWinEndsInStateEnded),
|
||||
cmocka_unit_test(test_battleLossEndsInStateEnded),
|
||||
cmocka_unit_test(test_battleFleeEndsInStateEndedImmediately),
|
||||
cmocka_unit_test(test_battleFleeUnavailableIsIgnored),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
include(dusktest)
|
||||
|
||||
# Tests
|
||||
dusktest(test_cutscene.c)
|
||||
dusktest(test_cutscenesystem.c)
|
||||
dusktest(test_cutscenecontrol.c)
|
||||
dusktest(test_cutscenemaparea.c)
|
||||
@@ -1,84 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "rpg/cutscene/cutscene.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
|
||||
// ============================================================
|
||||
// cutsceneRemoveFront
|
||||
// ============================================================
|
||||
|
||||
static cutsceneitem_t MARKER_ITEM(const char_t *name) {
|
||||
cutsceneitem_t item;
|
||||
memoryZero(&item, sizeof(item));
|
||||
item.type = CUTSCENE_ITEM_TYPE_MARKER;
|
||||
stringCopy(item.marker.name, name, CUTSCENE_MARKER_NAME_MAX - 1);
|
||||
return item;
|
||||
}
|
||||
|
||||
static void test_cutsceneRemoveFrontShiftsRemainderToIndexZero(
|
||||
void **state
|
||||
) {
|
||||
|
||||
cutsceneitem_t items[4] = {
|
||||
MARKER_ITEM("a"), MARKER_ITEM("b"), MARKER_ITEM("c"), MARKER_ITEM("d")
|
||||
};
|
||||
cutscene_t scene = { .items = items, .itemCount = 4, .itemsMax = 4 };
|
||||
|
||||
cutsceneRemoveFront(&scene, 2);
|
||||
|
||||
assert_int_equal(scene.itemCount, 2);
|
||||
assert_string_equal(scene.items[0].marker.name, "c");
|
||||
assert_string_equal(scene.items[1].marker.name, "d");
|
||||
}
|
||||
|
||||
static void test_cutsceneRemoveFrontAllItemsLeavesEmptyScene(void **state) {
|
||||
|
||||
cutsceneitem_t items[3] = {
|
||||
MARKER_ITEM("a"), MARKER_ITEM("b"), MARKER_ITEM("c")
|
||||
};
|
||||
cutscene_t scene = { .items = items, .itemCount = 3, .itemsMax = 3 };
|
||||
|
||||
cutsceneRemoveFront(&scene, 3);
|
||||
|
||||
assert_int_equal(scene.itemCount, 0);
|
||||
}
|
||||
|
||||
static void test_cutsceneRemoveFrontZeroIsNoop(void **state) {
|
||||
|
||||
cutsceneitem_t items[2] = { MARKER_ITEM("a"), MARKER_ITEM("b") };
|
||||
cutscene_t scene = { .items = items, .itemCount = 2, .itemsMax = 2 };
|
||||
|
||||
cutsceneRemoveFront(&scene, 0);
|
||||
|
||||
assert_int_equal(scene.itemCount, 2);
|
||||
assert_string_equal(scene.items[0].marker.name, "a");
|
||||
assert_string_equal(scene.items[1].marker.name, "b");
|
||||
}
|
||||
|
||||
static void test_cutsceneRemoveFrontAssertsWhenCountExceedsItemCount(
|
||||
void **state
|
||||
) {
|
||||
|
||||
cutsceneitem_t items[2] = { MARKER_ITEM("a"), MARKER_ITEM("b") };
|
||||
cutscene_t scene = { .items = items, .itemCount = 2, .itemsMax = 2 };
|
||||
|
||||
expect_assert_failure(cutsceneRemoveFront(&scene, 3));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_cutsceneRemoveFrontShiftsRemainderToIndexZero),
|
||||
cmocka_unit_test(test_cutsceneRemoveFrontAllItemsLeavesEmptyScene),
|
||||
cmocka_unit_test(test_cutsceneRemoveFrontZeroIsNoop),
|
||||
cmocka_unit_test(test_cutsceneRemoveFrontAssertsWhenCountExceedsItemCount),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "time/time.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
static void test_cutsceneWaitCompletesAfterItsDuration(void **state) {
|
||||
|
||||
cutsceneitem_t item = { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f };
|
||||
cutsceneitemdata_t data;
|
||||
memoryZero(&data, sizeof(data));
|
||||
|
||||
cutsceneWaitStart(&item, &data);
|
||||
|
||||
TIME.delta = 0.5f;
|
||||
assert_false(cutsceneWaitUpdate(&item, &data));
|
||||
|
||||
TIME.delta = 0.6f;
|
||||
assert_true(cutsceneWaitUpdate(&item, &data));
|
||||
}
|
||||
|
||||
static void *lastUserData;
|
||||
static uint8_t callbackCallCount;
|
||||
|
||||
static void recordCallback(void *userData) {
|
||||
lastUserData = userData;
|
||||
callbackCallCount++;
|
||||
}
|
||||
|
||||
static void test_cutsceneCallbackFiresOnStartWithNullUserData(void **state) {
|
||||
|
||||
callbackCallCount = 0;
|
||||
lastUserData = (void *)0x1;
|
||||
|
||||
cutsceneitem_t item =
|
||||
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordCallback };
|
||||
cutsceneitemdata_t data;
|
||||
|
||||
cutsceneCallbackStart(&item, &data);
|
||||
assert_int_equal(callbackCallCount, 1);
|
||||
assert_null(lastUserData);
|
||||
|
||||
// Callback items always complete immediately -- the effect already
|
||||
// happened in Start, not Update.
|
||||
assert_true(cutsceneCallbackUpdate(&item, &data));
|
||||
assert_int_equal(callbackCallCount, 1);// Update doesn't fire it again
|
||||
}
|
||||
|
||||
static void test_cutsceneCallbackNullIsNoop(void **state) {
|
||||
|
||||
callbackCallCount = 0;
|
||||
cutsceneitem_t item =
|
||||
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = NULL };
|
||||
cutsceneitemdata_t data;
|
||||
|
||||
cutsceneCallbackStart(&item, &data);// should not crash
|
||||
assert_int_equal(callbackCallCount, 0);
|
||||
}
|
||||
|
||||
static void test_cutsceneSetPauseAppliesImmediately(void **state) {
|
||||
|
||||
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
|
||||
cutsceneitem_t item = {
|
||||
.type = CUTSCENE_ITEM_TYPE_SET_PAUSE,
|
||||
.setPause = CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_BATTLE
|
||||
};
|
||||
cutsceneitemdata_t data;
|
||||
|
||||
cutsceneSetPauseStart(&item, &data);
|
||||
assert_int_equal(
|
||||
CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_BATTLE
|
||||
);
|
||||
assert_true(cutsceneSetPauseUpdate(&item, &data));
|
||||
}
|
||||
|
||||
static void test_cutsceneConcurrentCompletesOnceAllChildrenDo(void **state) {
|
||||
|
||||
cutsceneitem_t item = {
|
||||
.type = CUTSCENE_ITEM_TYPE_CONCURRENT,
|
||||
.concurrent = {
|
||||
.items = (const cutsceneitem_t[]){
|
||||
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 0.2f },
|
||||
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 0.5f }
|
||||
},
|
||||
.count = 2
|
||||
}
|
||||
};
|
||||
cutsceneitemdata_t data;
|
||||
memoryZero(&data, sizeof(data));
|
||||
|
||||
cutsceneConcurrentStart(&item, &data);
|
||||
|
||||
TIME.delta = 0.3f;
|
||||
// Child 0 (0.2s) elapses; child 1 (0.5s) still has 0.2s left.
|
||||
assert_false(cutsceneConcurrentUpdate(&item, &data));
|
||||
|
||||
TIME.delta = 0.3f;
|
||||
// Child 1 now elapses too.
|
||||
assert_true(cutsceneConcurrentUpdate(&item, &data));
|
||||
}
|
||||
|
||||
static void test_cutsceneConcurrentDoesNotReUpdateFinishedChildren(
|
||||
void **state
|
||||
) {
|
||||
|
||||
cutsceneitem_t item = {
|
||||
.type = CUTSCENE_ITEM_TYPE_CONCURRENT,
|
||||
.concurrent = {
|
||||
.items = (const cutsceneitem_t[]){
|
||||
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 0.1f },
|
||||
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 10.0f }
|
||||
},
|
||||
.count = 2
|
||||
}
|
||||
};
|
||||
cutsceneitemdata_t data;
|
||||
memoryZero(&data, sizeof(data));
|
||||
|
||||
cutsceneConcurrentStart(&item, &data);
|
||||
|
||||
TIME.delta = 0.2f;
|
||||
cutsceneConcurrentUpdate(&item, &data);// child 0 finishes
|
||||
|
||||
// If child 0 were re-updated, its stored wait value would keep dropping
|
||||
// further below zero -- not observable directly, but the overall result
|
||||
// must not falsely report done just because child 0 keeps completing.
|
||||
TIME.delta = 0.2f;
|
||||
assert_false(cutsceneConcurrentUpdate(&item, &data));
|
||||
}
|
||||
|
||||
static void test_cutsceneConcurrentCannotNest(void **state) {
|
||||
|
||||
cutsceneitem_t outer = {
|
||||
.type = CUTSCENE_ITEM_TYPE_CONCURRENT,
|
||||
.concurrent = {
|
||||
.items = (const cutsceneitem_t[]){
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_CONCURRENT,
|
||||
.concurrent = {
|
||||
.items = (const cutsceneitem_t[]){
|
||||
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f }
|
||||
},
|
||||
.count = 1
|
||||
}
|
||||
}
|
||||
},
|
||||
.count = 1
|
||||
}
|
||||
};
|
||||
cutsceneitemdata_t data;
|
||||
|
||||
expect_assert_failure(cutsceneConcurrentStart(&outer, &data));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_cutsceneWaitCompletesAfterItsDuration),
|
||||
cmocka_unit_test(test_cutsceneCallbackFiresOnStartWithNullUserData),
|
||||
cmocka_unit_test(test_cutsceneCallbackNullIsNoop),
|
||||
cmocka_unit_test(test_cutsceneSetPauseAppliesImmediately),
|
||||
cmocka_unit_test(test_cutsceneConcurrentCompletesOnceAllChildrenDo),
|
||||
cmocka_unit_test(test_cutsceneConcurrentDoesNotReUpdateFinishedChildren),
|
||||
cmocka_unit_test(test_cutsceneConcurrentCannotNest),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "rpg/overworld/maparea.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
static void resetMapAreas(void) {
|
||||
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
|
||||
}
|
||||
|
||||
static void test_cutsceneMapAreaAddCreatesAreaAndStoresLastCreated(
|
||||
void **state
|
||||
) {
|
||||
|
||||
resetMapAreas();
|
||||
cutsceneitem_t item = {
|
||||
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
|
||||
.mapAreaAdd = {
|
||||
.min = { 0, 0, 0 }, .max = { 5, 5, 0 },
|
||||
.callback = mapAreaNoopCallback,
|
||||
.notify = MAP_AREA_NOTIFY_ALL, .trigger = MAP_TRIGGER_ALL
|
||||
}
|
||||
};
|
||||
cutsceneitemdata_t data;
|
||||
|
||||
cutsceneMapAreaAddStart(&item, &data);
|
||||
assert_true(cutsceneMapAreaAddUpdate(&item, &data));
|
||||
|
||||
uint8_t id = CUTSCENE_SYSTEM.areaLastCreated;
|
||||
assert_ptr_equal(MAP_AREAS[id].callback, mapAreaNoopCallback);
|
||||
assert_int_equal(MAP_AREAS[id].max.x, 5);
|
||||
}
|
||||
|
||||
static void test_cutsceneMapAreaRemoveClearsSlot(void **state) {
|
||||
|
||||
resetMapAreas();
|
||||
uint8_t id = mapAreaAdd(
|
||||
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
|
||||
mapAreaNoopCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
|
||||
);
|
||||
|
||||
cutsceneitem_t item = {
|
||||
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE,
|
||||
.mapAreaRemove = { .areaId = id }
|
||||
};
|
||||
cutsceneitemdata_t data;
|
||||
|
||||
cutsceneMapAreaRemoveStart(&item, &data);
|
||||
assert_true(cutsceneMapAreaRemoveUpdate(&item, &data));
|
||||
assert_null(MAP_AREAS[id].callback);
|
||||
}
|
||||
|
||||
static void test_cutsceneMapAreaRemoveResolvesLastCreatedSentinel(
|
||||
void **state
|
||||
) {
|
||||
|
||||
resetMapAreas();
|
||||
cutsceneitem_t addItem = {
|
||||
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
|
||||
.mapAreaAdd = {
|
||||
.min = { 0, 0, 0 }, .max = { 5, 5, 0 },
|
||||
.callback = mapAreaNoopCallback,
|
||||
.notify = MAP_AREA_NOTIFY_ALL, .trigger = MAP_TRIGGER_ALL
|
||||
}
|
||||
};
|
||||
cutsceneitemdata_t data;
|
||||
cutsceneMapAreaAddStart(&addItem, &data);
|
||||
uint8_t id = CUTSCENE_SYSTEM.areaLastCreated;
|
||||
|
||||
cutsceneitem_t removeItem = {
|
||||
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE,
|
||||
.mapAreaRemove = { .areaId = CUTSCENE_REF_AREA_LAST_CREATED }
|
||||
};
|
||||
cutsceneMapAreaRemoveStart(&removeItem, &data);
|
||||
assert_null(MAP_AREAS[id].callback);
|
||||
}
|
||||
|
||||
static void test_cutsceneMapAreaWaitCompletesWhenTriggerCountChanges(
|
||||
void **state
|
||||
) {
|
||||
|
||||
resetMapAreas();
|
||||
uint8_t id = mapAreaAdd(
|
||||
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
|
||||
mapAreaNoopCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
|
||||
);
|
||||
|
||||
cutsceneitem_t item = {
|
||||
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
|
||||
.mapAreaWait = { .areaIds = { id }, .count = 1 }
|
||||
};
|
||||
cutsceneitemdata_t data;
|
||||
|
||||
cutsceneMapAreaWaitStart(&item, &data);
|
||||
assert_false(cutsceneMapAreaWaitUpdate(&item, &data));
|
||||
|
||||
MAP_AREAS[id].triggerCount++;// simulates the area's callback firing
|
||||
assert_true(cutsceneMapAreaWaitUpdate(&item, &data));
|
||||
}
|
||||
|
||||
static void test_cutsceneMapAreaWaitCompletesWhenAnyWatchedAreaChanges(
|
||||
void **state
|
||||
) {
|
||||
|
||||
resetMapAreas();
|
||||
uint8_t idA = mapAreaAdd(
|
||||
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
|
||||
mapAreaNoopCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
|
||||
);
|
||||
uint8_t idB = mapAreaAdd(
|
||||
(worldpos_t){ 10, 10, 0 }, (worldpos_t){ 15, 15, 0 },
|
||||
mapAreaNoopCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
|
||||
);
|
||||
|
||||
cutsceneitem_t item = {
|
||||
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
|
||||
.mapAreaWait = { .areaIds = { idA, idB }, .count = 2 }
|
||||
};
|
||||
cutsceneitemdata_t data;
|
||||
|
||||
cutsceneMapAreaWaitStart(&item, &data);
|
||||
assert_false(cutsceneMapAreaWaitUpdate(&item, &data));
|
||||
|
||||
MAP_AREAS[idB].triggerCount++;// only the second watched area fires
|
||||
assert_true(cutsceneMapAreaWaitUpdate(&item, &data));
|
||||
}
|
||||
|
||||
static void test_cutsceneMapAreaWaitResolvesLastCreatedSentinel(
|
||||
void **state
|
||||
) {
|
||||
|
||||
resetMapAreas();
|
||||
cutsceneitem_t addItem = {
|
||||
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
|
||||
.mapAreaAdd = {
|
||||
.min = { 0, 0, 0 }, .max = { 5, 5, 0 },
|
||||
.callback = mapAreaNoopCallback,
|
||||
.notify = MAP_AREA_NOTIFY_ALL, .trigger = MAP_TRIGGER_ALL
|
||||
}
|
||||
};
|
||||
cutsceneitemdata_t data;
|
||||
cutsceneMapAreaAddStart(&addItem, &data);
|
||||
uint8_t id = CUTSCENE_SYSTEM.areaLastCreated;
|
||||
|
||||
cutsceneitem_t waitItem = {
|
||||
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
|
||||
.mapAreaWait = { .areaIds = { CUTSCENE_REF_AREA_LAST_CREATED }, .count = 1 }
|
||||
};
|
||||
cutsceneMapAreaWaitStart(&waitItem, &data);
|
||||
assert_false(cutsceneMapAreaWaitUpdate(&waitItem, &data));
|
||||
|
||||
MAP_AREAS[id].triggerCount++;
|
||||
assert_true(cutsceneMapAreaWaitUpdate(&waitItem, &data));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_cutsceneMapAreaAddCreatesAreaAndStoresLastCreated),
|
||||
cmocka_unit_test(test_cutsceneMapAreaRemoveClearsSlot),
|
||||
cmocka_unit_test(test_cutsceneMapAreaRemoveResolvesLastCreatedSentinel),
|
||||
cmocka_unit_test(test_cutsceneMapAreaWaitCompletesWhenTriggerCountChanges),
|
||||
cmocka_unit_test(
|
||||
test_cutsceneMapAreaWaitCompletesWhenAnyWatchedAreaChanges
|
||||
),
|
||||
cmocka_unit_test(test_cutsceneMapAreaWaitResolvesLastCreatedSentinel),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -1,583 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "time/time.h"
|
||||
#include "util/string.h"
|
||||
|
||||
static uint8_t callbackFired;
|
||||
|
||||
static void recordCallback(void *userData) {
|
||||
callbackFired++;
|
||||
}
|
||||
|
||||
// INNER is jumped into by OUTER's second item, never returning -- this is
|
||||
// the "nested cutscene is a one-way jump" behavior: cutsceneCutsceneStart
|
||||
// replaces CUTSCENE_SYSTEM.scene outright, and the callback item's effect
|
||||
// fires in Start, not Update, so it fires the same frame the jump happens.
|
||||
static cutsceneitem_t CUTSCENE_TEST_INNER_ITEMS[] = {
|
||||
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordCallback }
|
||||
};
|
||||
static cutscene_t CUTSCENE_TEST_INNER = {
|
||||
.items = CUTSCENE_TEST_INNER_ITEMS,
|
||||
.itemCount = sizeof(CUTSCENE_TEST_INNER_ITEMS) / sizeof(cutsceneitem_t),
|
||||
.itemsMax = sizeof(CUTSCENE_TEST_INNER_ITEMS) / sizeof(cutsceneitem_t),
|
||||
.pause = CUTSCENE_PAUSE_NONE
|
||||
};
|
||||
|
||||
static cutsceneitem_t CUTSCENE_TEST_OUTER_ITEMS[] = {
|
||||
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 0.5f },
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_CUTSCENE,
|
||||
.cutsceneRef = { .cutscene = &CUTSCENE_TEST_INNER }
|
||||
}
|
||||
};
|
||||
static cutscene_t CUTSCENE_TEST_OUTER = {
|
||||
.items = CUTSCENE_TEST_OUTER_ITEMS,
|
||||
.itemCount = sizeof(CUTSCENE_TEST_OUTER_ITEMS) / sizeof(cutsceneitem_t),
|
||||
.itemsMax = sizeof(CUTSCENE_TEST_OUTER_ITEMS) / sizeof(cutsceneitem_t),
|
||||
.pause = CUTSCENE_PAUSE_DEFAULT
|
||||
};
|
||||
|
||||
static void test_cutsceneSystemStartSetsUpInitialItem(void **state) {
|
||||
|
||||
cutsceneSystemInit();
|
||||
|
||||
cutsceneSystemStartCutscene(&CUTSCENE_TEST_OUTER);
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_OUTER);
|
||||
assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0);
|
||||
assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_DEFAULT);
|
||||
}
|
||||
|
||||
static void test_cutsceneSystemNestedCutsceneIsOneWayJump(void **state) {
|
||||
|
||||
cutsceneSystemInit();
|
||||
callbackFired = 0;
|
||||
cutsceneSystemStartCutscene(&CUTSCENE_TEST_OUTER);
|
||||
|
||||
TIME.delta = 0.1f;
|
||||
cutsceneSystemUpdate();// wait not elapsed yet
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_OUTER);
|
||||
assert_int_equal(callbackFired, 0);
|
||||
|
||||
// The wait elapses, advancing to the nested-cutscene item, which jumps
|
||||
// straight into INNER and starts its first item (the callback) -- all
|
||||
// within this single update call.
|
||||
TIME.delta = 1.0f;
|
||||
cutsceneSystemUpdate();
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INNER);
|
||||
assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0);
|
||||
assert_int_equal(callbackFired, 1);
|
||||
|
||||
// INNER's only item (the callback) always reports complete -- one more
|
||||
// update ends the whole cutscene.
|
||||
cutsceneSystemUpdate();
|
||||
assert_null(CUTSCENE_SYSTEM.scene);
|
||||
assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0xFF);
|
||||
assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NONE);
|
||||
}
|
||||
|
||||
static void test_cutsceneSystemUpdateIsNoopWithNoActiveCutscene(void **state) {
|
||||
|
||||
cutsceneSystemInit();
|
||||
|
||||
cutsceneSystemUpdate();// should not crash
|
||||
assert_null(CUTSCENE_SYSTEM.scene);
|
||||
}
|
||||
|
||||
static cutsceneitem_t CUTSCENE_TEST_SINGLE_WAIT_ITEMS[] = {
|
||||
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f }
|
||||
};
|
||||
static cutscene_t CUTSCENE_TEST_SINGLE_WAIT = {
|
||||
.items = CUTSCENE_TEST_SINGLE_WAIT_ITEMS,
|
||||
.itemCount = sizeof(CUTSCENE_TEST_SINGLE_WAIT_ITEMS) / sizeof(cutsceneitem_t),
|
||||
.itemsMax = sizeof(CUTSCENE_TEST_SINGLE_WAIT_ITEMS) / sizeof(cutsceneitem_t),
|
||||
.pause = CUTSCENE_PAUSE_NONE
|
||||
};
|
||||
|
||||
static void test_cutsceneSystemStartWithSetsInteractEntities(void **state) {
|
||||
|
||||
cutsceneSystemInit();
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER);
|
||||
entityInit(&ENTITIES[1], ENTITY_TYPE_NPC);
|
||||
|
||||
cutsceneSystemStartCutsceneWith(
|
||||
&CUTSCENE_TEST_SINGLE_WAIT, &ENTITIES[0], &ENTITIES[1]
|
||||
);
|
||||
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.refs.entityInteract, &ENTITIES[0]);
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.refs.entityInteracted, &ENTITIES[1]);
|
||||
assert_ptr_equal(
|
||||
cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACT), &ENTITIES[0]
|
||||
);
|
||||
assert_ptr_equal(
|
||||
cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACTED), &ENTITIES[1]
|
||||
);
|
||||
|
||||
TIME.delta = 2.0f;
|
||||
cutsceneSystemUpdate();// ends the cutscene
|
||||
assert_null(CUTSCENE_SYSTEM.refs.entityInteract);// reset on end
|
||||
assert_null(CUTSCENE_SYSTEM.refs.entityInteracted);
|
||||
}
|
||||
|
||||
static void test_cutsceneSystemGetEntitySentinelsRequireBeingSet(
|
||||
void **state
|
||||
) {
|
||||
|
||||
cutsceneSystemInit();
|
||||
|
||||
expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACT));
|
||||
expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACTED));
|
||||
expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_LAST_CREATED));
|
||||
expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_LAST_REF));
|
||||
}
|
||||
|
||||
static void test_cutsceneSystemGetEntityDirectIndexUpdatesLastRef(
|
||||
void **state
|
||||
) {
|
||||
|
||||
cutsceneSystemInit();
|
||||
entityInit(&ENTITIES[2], ENTITY_TYPE_NPC);
|
||||
|
||||
entity_t *resolved = cutsceneSystemGetEntity(2);
|
||||
assert_ptr_equal(resolved, &ENTITIES[2]);
|
||||
// Resolving by direct index also updates LAST_REF.
|
||||
assert_ptr_equal(
|
||||
cutsceneSystemGetEntity(CUTSCENE_ENTITY_LAST_REF), &ENTITIES[2]
|
||||
);
|
||||
}
|
||||
|
||||
static void test_cutsceneSystemGetAreaId(void **state) {
|
||||
|
||||
cutsceneSystemInit();
|
||||
// cutsceneSystemInit() zero-inits the field -- only actually starting a
|
||||
// cutscene sets it to the "nothing created yet" sentinel.
|
||||
CUTSCENE_SYSTEM.refs.areaLastCreated = CUTSCENE_REF_AREA_LAST_CREATED;
|
||||
|
||||
expect_assert_failure(cutsceneSystemGetAreaId(CUTSCENE_REF_AREA_LAST_CREATED));
|
||||
assert_int_equal(cutsceneSystemGetAreaId(5), 5);// direct IDs pass through
|
||||
|
||||
CUTSCENE_SYSTEM.refs.areaLastCreated = 3;
|
||||
assert_int_equal(cutsceneSystemGetAreaId(CUTSCENE_REF_AREA_LAST_CREATED), 3);
|
||||
}
|
||||
|
||||
static void test_cutsceneSystemGetTextMiniId(void **state) {
|
||||
|
||||
cutsceneSystemInit();
|
||||
CUTSCENE_SYSTEM.refs.textMiniLastCreated = CUTSCENE_REF_TEXT_MINI_LAST_CREATED;
|
||||
|
||||
expect_assert_failure(
|
||||
cutsceneSystemGetTextMiniId(CUTSCENE_REF_TEXT_MINI_LAST_CREATED)
|
||||
);
|
||||
assert_int_equal(cutsceneSystemGetTextMiniId(4), 4);
|
||||
|
||||
CUTSCENE_SYSTEM.refs.textMiniLastCreated = 1;
|
||||
assert_int_equal(
|
||||
cutsceneSystemGetTextMiniId(CUTSCENE_REF_TEXT_MINI_LAST_CREATED), 1
|
||||
);
|
||||
}
|
||||
|
||||
static void test_cutsceneSystemDisposeResetsState(void **state) {
|
||||
|
||||
cutsceneSystemInit();
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER);
|
||||
cutsceneSystemStartCutsceneWith(
|
||||
&CUTSCENE_TEST_SINGLE_WAIT, &ENTITIES[0], &ENTITIES[0]
|
||||
);
|
||||
|
||||
cutsceneSystemDispose();
|
||||
assert_null(CUTSCENE_SYSTEM.scene);
|
||||
assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0xFF);
|
||||
assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NONE);
|
||||
assert_null(CUTSCENE_SYSTEM.refs.entityInteract);
|
||||
assert_null(CUTSCENE_SYSTEM.refs.entityInteracted);
|
||||
}
|
||||
|
||||
// --- loadedFile survival across cutsceneSystemPrepare -------------------
|
||||
//
|
||||
// cutsceneCutsceneResolve (and cutsceneSystemLoad) stamp
|
||||
// CUTSCENE_SYSTEM.loadedFile with what they just parsed loadedScene from
|
||||
// - cutsceneSystemGoTo needs that to survive so it can reload the same source
|
||||
// later. cutsceneCutsceneResolve's caller stamps it BEFORE calling
|
||||
// cutsceneSystemStartCutscene(&CUTSCENE_SYSTEM.loadedScene), so
|
||||
// cutsceneSystemPrepare must not blindly clear loadedFile on every
|
||||
// start - only when switching away from loadedScene to some other
|
||||
// cutscene_t entirely (see cutsceneSystemPrepare's doc comment). This
|
||||
// regression-tests the exact bug hit via initial.jsonc's CUTSCENE item
|
||||
// into main_menu.jsonc: cutsceneCutsceneResolve stamped loadedFile, then
|
||||
// the immediately following cutsceneSystemStartCutscene call wiped it
|
||||
// straight back out, so a later cutsceneSystemGoTo on the still-running
|
||||
// cutscene had nothing to reload from.
|
||||
static void test_cutsceneSystemPrepareKeepsLoadedFileWhenReenteringLoadedScene(
|
||||
void **state
|
||||
) {
|
||||
|
||||
cutsceneSystemInit();
|
||||
|
||||
// Simulate cutsceneCutsceneResolve: populate loadedScene/loadedItems
|
||||
// and stamp loadedFile with its source, before the cutscene has
|
||||
// actually started running.
|
||||
CUTSCENE_SYSTEM.loadedItems[0] =
|
||||
(cutsceneitem_t){ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f };
|
||||
CUTSCENE_SYSTEM.loadedScene = (cutscene_t){
|
||||
.items = CUTSCENE_SYSTEM.loadedItems,
|
||||
.itemCount = 1,
|
||||
.itemsMax = CUTSCENE_LOADED_ITEMS_MAX,
|
||||
.pause = CUTSCENE_PAUSE_NONE
|
||||
};
|
||||
stringCopy(
|
||||
CUTSCENE_SYSTEM.loadedFile, "cutscenes/main_menu.jsonc",
|
||||
ASSET_FILE_NAME_MAX - 1
|
||||
);
|
||||
|
||||
cutsceneSystemStartCutscene(&CUTSCENE_SYSTEM.loadedScene);
|
||||
|
||||
assert_string_equal(
|
||||
CUTSCENE_SYSTEM.loadedFile, "cutscenes/main_menu.jsonc"
|
||||
);
|
||||
|
||||
// Starting a genuinely different cutscene, though, must still
|
||||
// invalidate it - there's nothing left to reload it from.
|
||||
cutsceneSystemStartCutscene(&CUTSCENE_TEST_SINGLE_WAIT);
|
||||
|
||||
assert_string_equal(CUTSCENE_SYSTEM.loadedFile, "");
|
||||
}
|
||||
|
||||
// --- CUTSCENE_INSERT ---------------------------------------------------
|
||||
//
|
||||
// INSERT now splices its target's items directly into the running
|
||||
// scene's own array (see cutsceneSystemInsertCutscene) rather than
|
||||
// swapping CUTSCENE_SYSTEM.scene to a different cutscene_t and pushing a
|
||||
// return frame - CUTSCENE_SYSTEM.scene therefore stays the same outer
|
||||
// cutscene throughout every test below. cutsceneSystemNext pops each
|
||||
// already-run item off the front of the array as it advances (see
|
||||
// CUTSCENE_SYSTEM.currentItem's doc comment), so itemCount reflects only
|
||||
// what's left to run at any given point, not a running total of
|
||||
// everything ever spliced in - the itemCount assertions below are sized
|
||||
// accordingly. Since the running scene's own array is what gets appended
|
||||
// into, any outer cutscene used with INSERT must still declare its items
|
||||
// array (and .itemsMax) with enough spare capacity for whatever ends up
|
||||
// spliced into it at once, including transitively through any nested
|
||||
// INSERT.
|
||||
|
||||
static uint8_t insertOrderLog[8];
|
||||
static uint8_t insertOrderLogCount;
|
||||
|
||||
static void recordOrder0(void *userData) { insertOrderLog[insertOrderLogCount++] = 0; }
|
||||
static void recordOrder1(void *userData) { insertOrderLog[insertOrderLogCount++] = 1; }
|
||||
static void recordOrder2(void *userData) { insertOrderLog[insertOrderLogCount++] = 2; }
|
||||
static void recordOrderA(void *userData) { insertOrderLog[insertOrderLogCount++] = 3; }
|
||||
static void recordOrderB(void *userData) { insertOrderLog[insertOrderLogCount++] = 4; }
|
||||
|
||||
static cutsceneitem_t CUTSCENE_TEST_INSERT_SNIPPET_ITEMS[] = {
|
||||
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA },
|
||||
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 2.0f },
|
||||
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderB }
|
||||
};
|
||||
static cutscene_t CUTSCENE_TEST_INSERT_SNIPPET = {
|
||||
.items = CUTSCENE_TEST_INSERT_SNIPPET_ITEMS,
|
||||
.itemCount =
|
||||
sizeof(CUTSCENE_TEST_INSERT_SNIPPET_ITEMS) / sizeof(cutsceneitem_t),
|
||||
.itemsMax =
|
||||
sizeof(CUTSCENE_TEST_INSERT_SNIPPET_ITEMS) / sizeof(cutsceneitem_t),
|
||||
.pause = CUTSCENE_PAUSE_NONE
|
||||
};
|
||||
|
||||
// 5 items to start, +3 spare slots for CUTSCENE_TEST_INSERT_SNIPPET's items.
|
||||
static cutsceneitem_t CUTSCENE_TEST_INSERT_OUTER_ITEMS[8] = {
|
||||
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 },
|
||||
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f },
|
||||
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder1 },
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_INSERT,
|
||||
.insert = { .cutscene = &CUTSCENE_TEST_INSERT_SNIPPET }
|
||||
},
|
||||
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder2 }
|
||||
};
|
||||
static cutscene_t CUTSCENE_TEST_INSERT_OUTER = {
|
||||
.items = CUTSCENE_TEST_INSERT_OUTER_ITEMS,
|
||||
.itemCount = 5,
|
||||
.itemsMax = sizeof(CUTSCENE_TEST_INSERT_OUTER_ITEMS) / sizeof(cutsceneitem_t),
|
||||
.pause = CUTSCENE_PAUSE_NONE
|
||||
};
|
||||
|
||||
static void test_cutsceneInsertSplicesItemsInPlaceThenResumes(void **state) {
|
||||
|
||||
insertOrderLogCount = 0;
|
||||
cutsceneSystemInit();
|
||||
cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_OUTER);
|
||||
|
||||
TIME.delta = 1.5f;// elapses the outer WAIT(1.0)
|
||||
cutsceneSystemUpdate();// order0 done -> starts the outer WAIT
|
||||
cutsceneSystemUpdate();// the WAIT elapses -> starts order1
|
||||
cutsceneSystemUpdate();// order1 done -> starts the INSERT, cascading into orderA
|
||||
|
||||
// The splice happened in place - CUTSCENE_SYSTEM.scene never changed.
|
||||
// order0/the WAIT/order1/the INSERT item itself have all since been
|
||||
// popped off the front, leaving the snippet's 3 items (orderA now
|
||||
// running, its WAIT, orderB) plus order2.
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_OUTER);
|
||||
assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 4);
|
||||
|
||||
TIME.delta = 2.5f;// elapses the snippet's WAIT(2.0)
|
||||
cutsceneSystemUpdate();// orderA done -> starts snippet's WAIT
|
||||
cutsceneSystemUpdate();// snippet's WAIT elapses -> starts orderB
|
||||
cutsceneSystemUpdate();// orderB done -> continues on to order2
|
||||
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_OUTER);
|
||||
|
||||
cutsceneSystemUpdate();// order2 done -> cutscene ends naturally
|
||||
assert_null(CUTSCENE_SYSTEM.scene);
|
||||
|
||||
const uint8_t expected[] = { 0, 1, 3, 4, 2 };
|
||||
assert_int_equal(insertOrderLogCount, sizeof(expected));
|
||||
assert_memory_equal(insertOrderLog, expected, sizeof(expected));
|
||||
}
|
||||
|
||||
static cutsceneitem_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS[] = {
|
||||
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA }
|
||||
};
|
||||
static cutscene_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET = {
|
||||
.items = CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS,
|
||||
.itemCount = sizeof(CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS) /
|
||||
sizeof(cutsceneitem_t),
|
||||
.itemsMax = sizeof(CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS) /
|
||||
sizeof(cutsceneitem_t),
|
||||
.pause = CUTSCENE_PAUSE_NONE
|
||||
};
|
||||
|
||||
// 2 items to start, +1 spare slot for the snippet's single item.
|
||||
static cutsceneitem_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER_ITEMS[3] = {
|
||||
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 },
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_INSERT,
|
||||
.insert = {
|
||||
.cutscene = &CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET
|
||||
}
|
||||
}
|
||||
};
|
||||
static cutscene_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER = {
|
||||
.items = CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER_ITEMS,
|
||||
.itemCount = 2,
|
||||
.itemsMax = sizeof(CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER_ITEMS) /
|
||||
sizeof(cutsceneitem_t),
|
||||
.pause = CUTSCENE_PAUSE_NONE
|
||||
};
|
||||
|
||||
static void test_cutsceneInsertAsLastItemEndsCutsceneNaturally(
|
||||
void **state
|
||||
) {
|
||||
|
||||
insertOrderLogCount = 0;
|
||||
cutsceneSystemInit();
|
||||
cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER);
|
||||
|
||||
cutsceneSystemUpdate();// order0 done -> starts INSERT, cascades to orderA
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER);
|
||||
// order0 and the INSERT item itself have both been popped off the
|
||||
// front by now, leaving just orderA (currently running).
|
||||
assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 1);
|
||||
|
||||
// orderA is the snippet's only item, and the snippet was spliced in as
|
||||
// the outer's last item -- finishing it should reach the natural end
|
||||
// of the whole cutscene, not get stuck.
|
||||
cutsceneSystemUpdate();
|
||||
assert_null(CUTSCENE_SYSTEM.scene);
|
||||
|
||||
const uint8_t expected[] = { 0, 3 };
|
||||
assert_int_equal(insertOrderLogCount, sizeof(expected));
|
||||
assert_memory_equal(insertOrderLog, expected, sizeof(expected));
|
||||
}
|
||||
|
||||
static cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS[] = {
|
||||
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA }
|
||||
};
|
||||
static cutscene_t CUTSCENE_TEST_INSERT_NESTED_INNER = {
|
||||
.items = CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS,
|
||||
.itemCount =
|
||||
sizeof(CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS) / sizeof(cutsceneitem_t),
|
||||
.itemsMax =
|
||||
sizeof(CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS) / sizeof(cutsceneitem_t),
|
||||
.pause = CUTSCENE_PAUSE_NONE
|
||||
};
|
||||
|
||||
// INSERT is MID's first item, so entering MID (from OUTER) cascades
|
||||
// straight through into INNER within the same Start chain, one level
|
||||
// deeper than a single insert. MID is only ever read from as a splice
|
||||
// source (see CUTSCENE_TEST_INSERT_NESTED_OUTER) - it's never itself the
|
||||
// running scene, so it needs no spare capacity of its own.
|
||||
static cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS[] = {
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_INSERT,
|
||||
.insert = { .cutscene = &CUTSCENE_TEST_INSERT_NESTED_INNER }
|
||||
},
|
||||
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderB }
|
||||
};
|
||||
static cutscene_t CUTSCENE_TEST_INSERT_NESTED_MID = {
|
||||
.items = CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS,
|
||||
.itemCount =
|
||||
sizeof(CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS) / sizeof(cutsceneitem_t),
|
||||
.itemsMax =
|
||||
sizeof(CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS) / sizeof(cutsceneitem_t),
|
||||
.pause = CUTSCENE_PAUSE_NONE
|
||||
};
|
||||
|
||||
// 3 items to start, +2 spare for MID's items, +1 more for MID's own
|
||||
// nested INSERT(INNER) once MID's items become part of this array too.
|
||||
static cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_OUTER_ITEMS[6] = {
|
||||
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 },
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_INSERT,
|
||||
.insert = { .cutscene = &CUTSCENE_TEST_INSERT_NESTED_MID }
|
||||
},
|
||||
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder2 }
|
||||
};
|
||||
static cutscene_t CUTSCENE_TEST_INSERT_NESTED_OUTER = {
|
||||
.items = CUTSCENE_TEST_INSERT_NESTED_OUTER_ITEMS,
|
||||
.itemCount = 3,
|
||||
.itemsMax = sizeof(CUTSCENE_TEST_INSERT_NESTED_OUTER_ITEMS) /
|
||||
sizeof(cutsceneitem_t),
|
||||
.pause = CUTSCENE_PAUSE_NONE
|
||||
};
|
||||
|
||||
static void test_cutsceneInsertNestsThroughMultipleLevels(void **state) {
|
||||
|
||||
insertOrderLogCount = 0;
|
||||
cutsceneSystemInit();
|
||||
cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_NESTED_OUTER);
|
||||
|
||||
// order0 done -> starts INSERT(MID), cascading through INSERT(INNER) ->
|
||||
// orderA, all within this one Update call's Start chain - both splices
|
||||
// land in OUTER's own array.
|
||||
cutsceneSystemUpdate();
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_NESTED_OUTER);
|
||||
// order0 and both INSERT items themselves have all been popped off the
|
||||
// front by now, leaving orderA (currently running), orderB, order2.
|
||||
assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 3);
|
||||
|
||||
cutsceneSystemUpdate();// orderA done -> continues on to orderB
|
||||
cutsceneSystemUpdate();// orderB done -> continues on to order2
|
||||
cutsceneSystemUpdate();// order2 done -> ends naturally
|
||||
assert_null(CUTSCENE_SYSTEM.scene);
|
||||
|
||||
const uint8_t expected[] = { 0, 3, 4, 2 };
|
||||
assert_int_equal(insertOrderLogCount, sizeof(expected));
|
||||
assert_memory_equal(insertOrderLog, expected, sizeof(expected));
|
||||
}
|
||||
|
||||
static cutsceneitem_t CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET_ITEMS[] = {
|
||||
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA }
|
||||
};
|
||||
static cutscene_t CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET = {
|
||||
.items = CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET_ITEMS,
|
||||
.itemCount = 1,
|
||||
.itemsMax = 1,
|
||||
.pause = CUTSCENE_PAUSE_NONE
|
||||
};
|
||||
|
||||
// No spare capacity at all - itemsMax equals itemCount, leaving no room
|
||||
// to splice anything into it.
|
||||
static cutsceneitem_t CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER_ITEMS[1] = {
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_INSERT,
|
||||
.insert = { .cutscene = &CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET }
|
||||
}
|
||||
};
|
||||
static cutscene_t CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER = {
|
||||
.items = CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER_ITEMS,
|
||||
.itemCount = 1,
|
||||
.itemsMax = 1,
|
||||
.pause = CUTSCENE_PAUSE_NONE
|
||||
};
|
||||
|
||||
static void test_cutsceneInsertAssertsWhenExceedingItemsMax(void **state) {
|
||||
|
||||
cutsceneSystemInit();
|
||||
|
||||
expect_assert_failure(
|
||||
cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER)
|
||||
);
|
||||
}
|
||||
|
||||
static void noopInsertCallback(void *userData) {}
|
||||
|
||||
static cutsceneitem_t CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS[] = {
|
||||
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = noopInsertCallback }
|
||||
};
|
||||
static cutscene_t CUTSCENE_TEST_INSERT_PERSIST_SNIPPET = {
|
||||
.items = CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS,
|
||||
.itemCount = sizeof(CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS) /
|
||||
sizeof(cutsceneitem_t),
|
||||
.itemsMax = sizeof(CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS) /
|
||||
sizeof(cutsceneitem_t),
|
||||
.pause = CUTSCENE_PAUSE_ALL
|
||||
};
|
||||
|
||||
// 1 item to start, +1 spare slot for the snippet's single item.
|
||||
static cutsceneitem_t CUTSCENE_TEST_INSERT_PERSIST_OUTER_ITEMS[2] = {
|
||||
{
|
||||
.type = CUTSCENE_ITEM_TYPE_INSERT,
|
||||
.insert = { .cutscene = &CUTSCENE_TEST_INSERT_PERSIST_SNIPPET }
|
||||
}
|
||||
};
|
||||
static cutscene_t CUTSCENE_TEST_INSERT_PERSIST_OUTER = {
|
||||
.items = CUTSCENE_TEST_INSERT_PERSIST_OUTER_ITEMS,
|
||||
.itemCount = 1,
|
||||
.itemsMax = sizeof(CUTSCENE_TEST_INSERT_PERSIST_OUTER_ITEMS) /
|
||||
sizeof(cutsceneitem_t),
|
||||
.pause = CUTSCENE_PAUSE_NPC
|
||||
};
|
||||
|
||||
static void test_cutsceneInsertDoesNotResetPauseOrInteractEntities(
|
||||
void **state
|
||||
) {
|
||||
|
||||
cutsceneSystemInit();
|
||||
entityInit(&ENTITIES[3], ENTITY_TYPE_PLAYER);
|
||||
entityInit(&ENTITIES[4], ENTITY_TYPE_NPC);
|
||||
|
||||
cutsceneSystemStartCutsceneWith(
|
||||
&CUTSCENE_TEST_INSERT_PERSIST_OUTER, &ENTITIES[3], &ENTITIES[4]
|
||||
);
|
||||
|
||||
// The INSERT item's Start already spliced the snippet into place and
|
||||
// cascaded into it by now - and since the INSERT item itself has since
|
||||
// been popped off the front, only the snippet's one item is left.
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_PERSIST_OUTER);
|
||||
assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 1);
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.refs.entityInteract, &ENTITIES[3]);
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.refs.entityInteracted, &ENTITIES[4]);
|
||||
// The snippet declares ALL, but INSERT doesn't apply it -- the outer
|
||||
// cutscene's NPC pause, already in effect, stands.
|
||||
assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NPC);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_cutsceneSystemStartSetsUpInitialItem),
|
||||
cmocka_unit_test(test_cutsceneSystemNestedCutsceneIsOneWayJump),
|
||||
cmocka_unit_test(test_cutsceneSystemUpdateIsNoopWithNoActiveCutscene),
|
||||
cmocka_unit_test(test_cutsceneSystemStartWithSetsInteractEntities),
|
||||
cmocka_unit_test(test_cutsceneSystemGetEntitySentinelsRequireBeingSet),
|
||||
cmocka_unit_test(test_cutsceneSystemGetEntityDirectIndexUpdatesLastRef),
|
||||
cmocka_unit_test(test_cutsceneSystemGetAreaId),
|
||||
cmocka_unit_test(test_cutsceneSystemGetTextMiniId),
|
||||
cmocka_unit_test(test_cutsceneSystemDisposeResetsState),
|
||||
cmocka_unit_test(
|
||||
test_cutsceneSystemPrepareKeepsLoadedFileWhenReenteringLoadedScene
|
||||
),
|
||||
cmocka_unit_test(test_cutsceneInsertSplicesItemsInPlaceThenResumes),
|
||||
cmocka_unit_test(test_cutsceneInsertAsLastItemEndsCutsceneNaturally),
|
||||
cmocka_unit_test(test_cutsceneInsertNestsThroughMultipleLevels),
|
||||
cmocka_unit_test(test_cutsceneInsertAssertsWhenExceedingItemsMax),
|
||||
cmocka_unit_test(test_cutsceneInsertDoesNotResetPauseOrInteractEntities),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
include(dusktest)
|
||||
|
||||
# Tests
|
||||
dusktest(test_entity.c)
|
||||
target_sources(test_entity PRIVATE entitytestfixture.c)
|
||||
|
||||
dusktest(test_npc.c)
|
||||
target_sources(test_npc PRIVATE entitytestfixture.c)
|
||||
|
||||
dusktest(test_entityinteract.c)
|
||||
target_sources(test_entityinteract PRIVATE entitytestfixture.c)
|
||||
|
||||
dusktest(test_entityitem.c)
|
||||
target_sources(test_entityitem PRIVATE entitytestfixture.c)
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "entitytestfixture.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
#include "rpg/overworld/tile.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
|
||||
void entityTestFixtureReset(void) {
|
||||
memoryZero(ENTITIES, sizeof(ENTITIES));
|
||||
|
||||
memoryZero(&MAP, sizeof(map_t));
|
||||
stringCopy(MAP.name, "test", MAP_NAME_MAX);
|
||||
MAP.chunkPosition = (chunkpos_t){ 0, 0, 0 };
|
||||
|
||||
chunk_t *chunk = &MAP.chunks[0];
|
||||
chunk->position = (chunkpos_t){ 0, 0, 0 };
|
||||
for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) {
|
||||
chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND, .z = 0 };
|
||||
}
|
||||
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
||||
chunk->entities[i] = 0xFF;
|
||||
}
|
||||
|
||||
MAP.chunkOrder[0] = chunk;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/entity/entity.h"
|
||||
|
||||
/**
|
||||
* Resets ENTITIES[] to all-empty and builds a single minimal, synchronously
|
||||
* "loaded" chunk at chunk position (0, 0, 0) on the MAP global, entirely
|
||||
* TILE_SHAPE_GROUND at local Z 0. Sufficient for entityWalk/Turn/Run, chunk
|
||||
* bookkeeping, and NPC movement tests -- does not exercise the real async
|
||||
* chunk-loading pipeline (mapChunkLoad).
|
||||
*
|
||||
* World positions with x/y in [0, CHUNK_WIDTH-1]/[0, CHUNK_HEIGHT-1] and
|
||||
* z == 0 fall within the fixture chunk. Individual tests may override
|
||||
* specific columns afterward (e.g. to place a ramp).
|
||||
*/
|
||||
void entityTestFixtureReset(void);
|
||||
@@ -1,300 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "entitytestfixture.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
#include "rpg/overworld/worldpos.h"
|
||||
|
||||
static void setTile(
|
||||
const worldpos_t pos, const tileshape_t shape, const uint8_t z
|
||||
) {
|
||||
chunktileindex_t index = worldPosToChunkTileIndex(&pos);
|
||||
MAP.chunks[0].tiles[index] = (tile_t){ .shape = shape, .z = z };
|
||||
}
|
||||
|
||||
static void test_entityInit(void **state) {
|
||||
|
||||
entityTestFixtureReset();
|
||||
|
||||
entityInit(&ENTITIES[3], ENTITY_TYPE_ITEM);
|
||||
assert_int_equal(ENTITIES[3].id, 3);
|
||||
assert_int_equal(ENTITIES[3].type, ENTITY_TYPE_ITEM);
|
||||
assert_int_equal(ENTITIES[3].chunkIndex, 0xFF);
|
||||
assert_int_equal(ENTITIES[3].globalId, ENTITY_GLOBAL_ID_NULL);
|
||||
// entityItemInit wires the interact callback -- confirms init dispatch ran.
|
||||
assert_int_equal(ENTITIES[3].interact.type, ENTITY_INTERACT_CALLBACK);
|
||||
|
||||
expect_assert_failure(entityInit(NULL, ENTITY_TYPE_ITEM));
|
||||
expect_assert_failure(entityInit(&ENTITIES[0], ENTITY_TYPE_NULL));
|
||||
expect_assert_failure(entityInit(&ENTITIES[0], ENTITY_TYPE_COUNT));
|
||||
|
||||
entity_t offBounds;
|
||||
expect_assert_failure(entityInit(&offBounds, ENTITY_TYPE_ITEM));
|
||||
}
|
||||
|
||||
static void test_entityGetAvailable(void **state) {
|
||||
|
||||
entityTestFixtureReset();
|
||||
|
||||
assert_int_equal(entityGetAvailable(), 0);
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
|
||||
assert_int_equal(entityGetAvailable(), 1);
|
||||
entityInit(&ENTITIES[1], ENTITY_TYPE_NPC);
|
||||
assert_int_equal(entityGetAvailable(), 2);
|
||||
}
|
||||
|
||||
static void test_entityGetAt(void **state) {
|
||||
|
||||
entityTestFixtureReset();
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
|
||||
ENTITIES[0].position = (worldpos_t){ 3, 4, 0 };
|
||||
|
||||
assert_ptr_equal(entityGetAt((worldpos_t){ 3, 4, 0 }), &ENTITIES[0]);
|
||||
assert_null(entityGetAt((worldpos_t){ 3, 4, 1 }));
|
||||
assert_null(entityGetAt((worldpos_t){ 0, 0, 0 }));
|
||||
}
|
||||
|
||||
static void test_entityGetByGlobalId(void **state) {
|
||||
|
||||
entityTestFixtureReset();
|
||||
|
||||
entityInit(&ENTITIES[2], ENTITY_TYPE_NPC);
|
||||
ENTITIES[2].globalId = 42;
|
||||
|
||||
assert_ptr_equal(entityGetByGlobalId(42), &ENTITIES[2]);
|
||||
assert_null(entityGetByGlobalId(43));
|
||||
assert_null(entityGetByGlobalId(ENTITY_GLOBAL_ID_NULL));
|
||||
}
|
||||
|
||||
static void test_entityCanUnload(void **state) {
|
||||
|
||||
entityTestFixtureReset();
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
|
||||
ENTITIES[0].globalId = ENTITY_GLOBAL_ID_START - 1;
|
||||
assert_true(entityCanUnload(&ENTITIES[0]));
|
||||
|
||||
ENTITIES[0].globalId = ENTITY_GLOBAL_ID_START;
|
||||
assert_false(entityCanUnload(&ENTITIES[0]));
|
||||
}
|
||||
|
||||
static void test_entityCanTurnWalkRun(void **state) {
|
||||
|
||||
entityTestFixtureReset();
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
|
||||
assert_true(entityCanTurn(&ENTITIES[0]));
|
||||
assert_true(entityCanWalk(&ENTITIES[0]));
|
||||
assert_true(entityCanRun(&ENTITIES[0]));
|
||||
|
||||
ENTITIES[0].animation = ENTITY_ANIM_WALK;
|
||||
assert_false(entityCanTurn(&ENTITIES[0]));
|
||||
assert_false(entityCanWalk(&ENTITIES[0]));
|
||||
assert_false(entityCanRun(&ENTITIES[0]));
|
||||
|
||||
ENTITIES[0].animation = ENTITY_ANIM_IDLE;
|
||||
ENTITIES[0].walkEndCooldown = 0.1f;
|
||||
assert_false(entityCanTurn(&ENTITIES[0]));// cooldown blocks turning only
|
||||
assert_true(entityCanWalk(&ENTITIES[0]));
|
||||
}
|
||||
|
||||
static void test_entityTurn(void **state) {
|
||||
|
||||
entityTestFixtureReset();
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
|
||||
entityTurn(&ENTITIES[0], ENTITY_DIR_EAST);
|
||||
assert_int_equal(ENTITIES[0].direction, ENTITY_DIR_EAST);
|
||||
assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_TURN);
|
||||
|
||||
// Can't turn again mid-turn.
|
||||
ENTITIES[0].direction = ENTITY_DIR_NORTH;
|
||||
entityTurn(&ENTITIES[0], ENTITY_DIR_WEST);
|
||||
assert_int_equal(ENTITIES[0].direction, ENTITY_DIR_NORTH);
|
||||
}
|
||||
|
||||
static void test_entityWalkOnOpenGround(void **state) {
|
||||
|
||||
entityTestFixtureReset();
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
|
||||
ENTITIES[0].position = (worldpos_t){ 5, 5, 0 };
|
||||
|
||||
entityWalk(&ENTITIES[0], ENTITY_DIR_NORTH);
|
||||
assert_int_equal(ENTITIES[0].position.x, 5);
|
||||
assert_int_equal(ENTITIES[0].position.y, 6);
|
||||
assert_int_equal(ENTITIES[0].position.z, 0);
|
||||
assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_WALK);
|
||||
assert_int_equal(ENTITIES[0].direction, ENTITY_DIR_NORTH);
|
||||
// entityUpdateChunk should have assigned it to the fixture chunk.
|
||||
assert_int_equal(ENTITIES[0].chunkIndex, 0);
|
||||
assert_int_equal(MAP.chunks[0].entities[0], 0);
|
||||
}
|
||||
|
||||
static void test_entityWalkBlockedAtChunkEdge(void **state) {
|
||||
|
||||
entityTestFixtureReset();
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
|
||||
ENTITIES[0].position = (worldpos_t){ 0, 0, 0 };
|
||||
|
||||
// West/south of the loaded chunk is unloaded (TILE_NULL) -- blocked.
|
||||
entityWalk(&ENTITIES[0], ENTITY_DIR_WEST);
|
||||
assert_int_equal(ENTITIES[0].position.x, 0);
|
||||
assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_IDLE);
|
||||
}
|
||||
|
||||
static void test_entityWalkBlockedByOtherEntity(void **state) {
|
||||
|
||||
entityTestFixtureReset();
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
|
||||
ENTITIES[0].position = (worldpos_t){ 5, 5, 0 };
|
||||
|
||||
entityInit(&ENTITIES[1], ENTITY_TYPE_NPC);
|
||||
ENTITIES[1].position = (worldpos_t){ 5, 6, 0 };// directly north
|
||||
|
||||
entityWalk(&ENTITIES[0], ENTITY_DIR_NORTH);
|
||||
assert_int_equal(ENTITIES[0].position.x, 5);
|
||||
assert_int_equal(ENTITIES[0].position.y, 5);
|
||||
assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_IDLE);
|
||||
}
|
||||
|
||||
static void test_entityWalkCannotWhileNotIdle(void **state) {
|
||||
|
||||
entityTestFixtureReset();
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
|
||||
ENTITIES[0].position = (worldpos_t){ 5, 5, 0 };
|
||||
ENTITIES[0].animation = ENTITY_ANIM_WALK;
|
||||
|
||||
entityWalk(&ENTITIES[0], ENTITY_DIR_NORTH);
|
||||
assert_int_equal(ENTITIES[0].position.y, 5);// unchanged
|
||||
}
|
||||
|
||||
// Ramp at (5,5) facing north, raised GROUND at (5,6)/z1 -- walking north
|
||||
// off the ramp raises the entity by one Z layer, and walking back south
|
||||
// from the raised tile falls back down onto the ramp.
|
||||
static void setUpRamp(void) {
|
||||
setTile((worldpos_t){ 5, 5, 0 }, TILE_SHAPE_RAMP_NORTH, 0);
|
||||
setTile((worldpos_t){ 5, 6, 0 }, TILE_SHAPE_GROUND, 1);
|
||||
}
|
||||
|
||||
static void test_entityWalkUpRamp(void **state) {
|
||||
|
||||
entityTestFixtureReset();
|
||||
setUpRamp();
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
|
||||
ENTITIES[0].position = (worldpos_t){ 5, 5, 0 };
|
||||
|
||||
entityWalk(&ENTITIES[0], ENTITY_DIR_NORTH);
|
||||
assert_int_equal(ENTITIES[0].position.x, 5);
|
||||
assert_int_equal(ENTITIES[0].position.y, 6);
|
||||
assert_int_equal(ENTITIES[0].position.z, 1);
|
||||
}
|
||||
|
||||
static void test_entityWalkFallDownRamp(void **state) {
|
||||
|
||||
entityTestFixtureReset();
|
||||
setUpRamp();
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
|
||||
ENTITIES[0].position = (worldpos_t){ 5, 6, 1 };
|
||||
|
||||
entityWalk(&ENTITIES[0], ENTITY_DIR_SOUTH);
|
||||
assert_int_equal(ENTITIES[0].position.x, 5);
|
||||
assert_int_equal(ENTITIES[0].position.y, 5);
|
||||
assert_int_equal(ENTITIES[0].position.z, 0);
|
||||
}
|
||||
|
||||
static void test_entityWalkCannotClimbRampFromTheSide(void **state) {
|
||||
|
||||
entityTestFixtureReset();
|
||||
setUpRamp();
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
|
||||
ENTITIES[0].position = (worldpos_t){ 5, 5, 0 };
|
||||
|
||||
// Only NORTH climbs this ramp -- EAST off the ramp tile is just blocked
|
||||
// ground movement, not a climb.
|
||||
entityWalk(&ENTITIES[0], ENTITY_DIR_EAST);
|
||||
assert_int_equal(ENTITIES[0].position.x, 6);
|
||||
assert_int_equal(ENTITIES[0].position.z, 0);
|
||||
}
|
||||
|
||||
static void test_entityRun(void **state) {
|
||||
|
||||
entityTestFixtureReset();
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
|
||||
ENTITIES[0].position = (worldpos_t){ 5, 5, 0 };
|
||||
|
||||
entityRun(&ENTITIES[0], ENTITY_DIR_NORTH);
|
||||
assert_int_equal(ENTITIES[0].position.y, 6);
|
||||
assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_RUN);
|
||||
}
|
||||
|
||||
static void test_entitySetChunkAndUpdateChunk(void **state) {
|
||||
|
||||
entityTestFixtureReset();
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
|
||||
entitySetChunk(&ENTITIES[0], 0);
|
||||
assert_int_equal(ENTITIES[0].chunkIndex, 0);
|
||||
assert_int_equal(MAP.chunks[0].entities[0], 0);
|
||||
|
||||
entitySetChunk(&ENTITIES[0], 0xFF);
|
||||
assert_int_equal(ENTITIES[0].chunkIndex, 0xFF);
|
||||
assert_int_equal(MAP.chunks[0].entities[0], 0xFF);
|
||||
|
||||
ENTITIES[0].position = (worldpos_t){ 3, 3, 0 };
|
||||
entityUpdateChunk(&ENTITIES[0]);
|
||||
assert_int_equal(ENTITIES[0].chunkIndex, 0);
|
||||
}
|
||||
|
||||
static void test_entityPositionSet(void **state) {
|
||||
|
||||
entityTestFixtureReset();
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
|
||||
ENTITIES[0].animation = ENTITY_ANIM_WALK;
|
||||
ENTITIES[0].walkEndCooldown = 5;
|
||||
|
||||
entityPositionSet(&ENTITIES[0], (worldpos_t){ 7, 8, 0 });
|
||||
assert_int_equal(ENTITIES[0].position.x, 7);
|
||||
assert_int_equal(ENTITIES[0].position.y, 8);
|
||||
assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_IDLE);
|
||||
assert_int_equal(ENTITIES[0].walkEndCooldown, 0);
|
||||
assert_int_equal(ENTITIES[0].chunkIndex, 0);// updated as a side effect
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_entityInit),
|
||||
cmocka_unit_test(test_entityGetAvailable),
|
||||
cmocka_unit_test(test_entityGetAt),
|
||||
cmocka_unit_test(test_entityGetByGlobalId),
|
||||
cmocka_unit_test(test_entityCanUnload),
|
||||
cmocka_unit_test(test_entityCanTurnWalkRun),
|
||||
cmocka_unit_test(test_entityTurn),
|
||||
cmocka_unit_test(test_entityWalkOnOpenGround),
|
||||
cmocka_unit_test(test_entityWalkBlockedAtChunkEdge),
|
||||
cmocka_unit_test(test_entityWalkBlockedByOtherEntity),
|
||||
cmocka_unit_test(test_entityWalkCannotWhileNotIdle),
|
||||
cmocka_unit_test(test_entityWalkUpRamp),
|
||||
cmocka_unit_test(test_entityWalkFallDownRamp),
|
||||
cmocka_unit_test(test_entityWalkCannotClimbRampFromTheSide),
|
||||
cmocka_unit_test(test_entityRun),
|
||||
cmocka_unit_test(test_entitySetChunkAndUpdateChunk),
|
||||
cmocka_unit_test(test_entityPositionSet),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "entitytestfixture.h"
|
||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||
#include "ui/focus/uifocus.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
|
||||
static cutsceneitem_t CUTSCENE_TEST_INTERACT_ITEMS[] = {
|
||||
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f }
|
||||
};
|
||||
static cutscene_t CUTSCENE_TEST_INTERACT = {
|
||||
.items = CUTSCENE_TEST_INTERACT_ITEMS,
|
||||
.itemCount = 1,
|
||||
.itemsMax = 1,
|
||||
.pause = CUTSCENE_PAUSE_NONE
|
||||
};
|
||||
|
||||
static entity_t *player;
|
||||
static entity_t *target;
|
||||
|
||||
static void resetInteractFixture(void) {
|
||||
entityTestFixtureReset();
|
||||
uiFocusInit();
|
||||
uiTextboxMainInit();
|
||||
uiTextboxMainFocusClosed(NULL);// force-clear stale focus from a prior test
|
||||
cutsceneSystemInit();
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER);
|
||||
entityInit(&ENTITIES[1], ENTITY_TYPE_NPC);
|
||||
player = &ENTITIES[0];
|
||||
target = &ENTITIES[1];
|
||||
player->direction = ENTITY_DIR_NORTH;
|
||||
}
|
||||
|
||||
static void test_entityInteractWithNull(void **state) {
|
||||
|
||||
resetInteractFixture();
|
||||
|
||||
target->interact.type = ENTITY_INTERACT_NULL;
|
||||
entityInteractWith(player, target);// should not crash or change anything
|
||||
assert_int_equal(target->interact.type, ENTITY_INTERACT_NULL);
|
||||
}
|
||||
|
||||
static void test_entityInteractWithPrintTurnsNpcToFacePlayer(void **state) {
|
||||
|
||||
resetInteractFixture();
|
||||
|
||||
target->interact.type = ENTITY_INTERACT_PRINT;
|
||||
target->interact.data.message = "Hello!";
|
||||
|
||||
entityInteractWith(player, target);
|
||||
|
||||
assert_true(uiTextboxMainIsActive());
|
||||
assert_int_equal(
|
||||
target->data.npc.interactState, NPC_INTERACT_STATE_CONVERSING
|
||||
);
|
||||
// NPC turns to face the opposite of the player's facing direction.
|
||||
assert_int_equal(target->direction, entityDirGetOpposite(player->direction));
|
||||
}
|
||||
|
||||
static entity_t *callbackPlayerArg;
|
||||
static entity_t *callbackTargetArg;
|
||||
static uint8_t callbackCount;
|
||||
|
||||
static void recordInteractCallback(entity_t *p, entity_t *t) {
|
||||
callbackPlayerArg = p;
|
||||
callbackTargetArg = t;
|
||||
callbackCount++;
|
||||
}
|
||||
|
||||
static void test_entityInteractWithCallback(void **state) {
|
||||
|
||||
resetInteractFixture();
|
||||
callbackCount = 0;
|
||||
|
||||
target->interact.type = ENTITY_INTERACT_CALLBACK;
|
||||
target->interact.data.callback = recordInteractCallback;
|
||||
|
||||
entityInteractWith(player, target);
|
||||
|
||||
assert_int_equal(callbackCount, 1);
|
||||
assert_ptr_equal(callbackPlayerArg, player);
|
||||
assert_ptr_equal(callbackTargetArg, target);
|
||||
}
|
||||
|
||||
static void test_entityInteractWithCallbackRequiresNonNull(void **state) {
|
||||
|
||||
resetInteractFixture();
|
||||
|
||||
target->interact.type = ENTITY_INTERACT_CALLBACK;
|
||||
target->interact.data.callback = NULL;
|
||||
|
||||
expect_assert_failure(entityInteractWith(player, target));
|
||||
}
|
||||
|
||||
static void test_entityInteractWithCutsceneStartsIt(void **state) {
|
||||
|
||||
resetInteractFixture();
|
||||
|
||||
target->interact.type = ENTITY_INTERACT_CUTSCENE;
|
||||
target->interact.data.cutscene = &CUTSCENE_TEST_INTERACT;
|
||||
|
||||
entityInteractWith(player, target);
|
||||
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INTERACT);
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.entityInteract, player);
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.entityInteracted, target);
|
||||
}
|
||||
|
||||
static void test_entityInteractWithRequiresNonNullEntities(void **state) {
|
||||
|
||||
resetInteractFixture();
|
||||
|
||||
expect_assert_failure(entityInteractWith(NULL, target));
|
||||
expect_assert_failure(entityInteractWith(player, NULL));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_entityInteractWithNull),
|
||||
cmocka_unit_test(test_entityInteractWithPrintTurnsNpcToFacePlayer),
|
||||
cmocka_unit_test(test_entityInteractWithCallback),
|
||||
cmocka_unit_test(test_entityInteractWithCallbackRequiresNonNull),
|
||||
cmocka_unit_test(test_entityInteractWithCutsceneStartsIt),
|
||||
cmocka_unit_test(test_entityInteractWithRequiresNonNullEntities),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "entitytestfixture.h"
|
||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||
#include "ui/focus/uifocus.h"
|
||||
|
||||
// An arbitrary, non-null itemid_t -- entityItemSet just stores/reports it
|
||||
// verbatim, it doesn't need a real item registered in ITEM.
|
||||
#define TEST_ITEM_ID ((itemid_t)1)
|
||||
|
||||
static entity_t *setUpItemEntity(void) {
|
||||
entityTestFixtureReset();
|
||||
uiFocusInit();
|
||||
uiTextboxMainInit();
|
||||
uiTextboxMainFocusClosed(NULL);// force-clear stale focus from a prior test
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_ITEM);
|
||||
return &ENTITIES[0];
|
||||
}
|
||||
|
||||
static void test_entityItemInit(void **state) {
|
||||
|
||||
entity_t *item = setUpItemEntity();
|
||||
|
||||
assert_int_equal(item->interact.type, ENTITY_INTERACT_CALLBACK);
|
||||
assert_ptr_equal(item->interact.data.callback, entityItemInteract);
|
||||
}
|
||||
|
||||
static void test_entityItemSet(void **state) {
|
||||
|
||||
entity_t *item = setUpItemEntity();
|
||||
|
||||
entityItemSet(item, TEST_ITEM_ID, 3);
|
||||
assert_int_equal(item->data.item.item, TEST_ITEM_ID);
|
||||
assert_int_equal(item->data.item.quantity, 3);
|
||||
assert_false(item->data.item.collected);
|
||||
}
|
||||
|
||||
// entityItemInteract itself is NOT covered here: it unconditionally calls
|
||||
// itemGive(), which calls itemGetName(), which dereferences
|
||||
// LOCALE.entry->data.locale -- a real locale asset populated only by the
|
||||
// async asset-loading pipeline (see localemanager.h/assetlocaleloader.h).
|
||||
// That's exactly the asset-loading dependency this test pass scoped out;
|
||||
// faking it cheaply isn't possible without hand-building the loader's
|
||||
// internal hash format, which is more coupling than it's worth. Backpack
|
||||
// bookkeeping itself is already covered by test/item/test_inventory.c-style
|
||||
// tests at the inventory/backpack layer.
|
||||
|
||||
static void test_entityItemMovementWaitsForCollectionAndTextbox(void **state) {
|
||||
|
||||
entity_t *item = setUpItemEntity();
|
||||
|
||||
// Not collected yet -- stays.
|
||||
entityItemMovement(item);
|
||||
assert_int_equal(item->type, ENTITY_TYPE_ITEM);
|
||||
|
||||
// Collected (set directly -- see note above on why entityItemInteract
|
||||
// itself isn't driven here), but a message is still showing -- stays.
|
||||
item->data.item.collected = true;
|
||||
uiTextboxMainSetText("Picked up!");
|
||||
entityItemMovement(item);
|
||||
assert_int_equal(item->type, ENTITY_TYPE_ITEM);
|
||||
|
||||
// Once the textbox is dismissed, the entity despawns.
|
||||
uiFocusPop();
|
||||
assert_false(uiTextboxMainIsActive());
|
||||
entityItemMovement(item);
|
||||
assert_int_equal(item->type, ENTITY_TYPE_NULL);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_entityItemInit),
|
||||
cmocka_unit_test(test_entityItemSet),
|
||||
cmocka_unit_test(test_entityItemMovementWaitsForCollectionAndTextbox),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "entitytestfixture.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "time/time.h"
|
||||
|
||||
static entity_t *setUpNpc(const worldpos_t position) {
|
||||
entityTestFixtureReset();
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_NPC);
|
||||
ENTITIES[0].position = position;
|
||||
return &ENTITIES[0];
|
||||
}
|
||||
|
||||
static void test_npcSetMoveType(void **state) {
|
||||
|
||||
entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 });
|
||||
|
||||
npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_TURN);
|
||||
assert_int_equal(npc->data.npc.moveType, NPC_MOVE_TYPE_RANDOM_TURN);
|
||||
// Init ran: timer was seeded within the default frequency range.
|
||||
assert_true(npc->data.npc.moveData.randomTurn.timer > 0.0f);
|
||||
}
|
||||
|
||||
static void test_npcRandomTurnMovementFiresOnceTimerElapses(void **state) {
|
||||
|
||||
entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 });
|
||||
npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_TURN);
|
||||
npc->data.npc.moveData.randomTurn.timer = 0.05f;
|
||||
|
||||
TIME.delta = 0.01f;
|
||||
npcRandomTurnMovement(npc);
|
||||
assert_int_equal(npc->animation, ENTITY_ANIM_IDLE);// timer not elapsed yet
|
||||
|
||||
TIME.delta = 1.0f;
|
||||
npcRandomTurnMovement(npc);
|
||||
assert_int_equal(npc->animation, ENTITY_ANIM_TURN);
|
||||
// Timer is reseeded, not left at/below zero.
|
||||
assert_true(npc->data.npc.moveData.randomTurn.timer > 0.0f);
|
||||
}
|
||||
|
||||
static void test_npcRandomWalkMovementFiresOnceTimerElapses(void **state) {
|
||||
|
||||
entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 });
|
||||
npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_WALK);
|
||||
npc->data.npc.moveData.randomWalk.timer = 0.05f;
|
||||
|
||||
TIME.delta = 0.01f;
|
||||
npcRandomWalkMovement(npc);
|
||||
assert_int_equal(npc->animation, ENTITY_ANIM_IDLE);
|
||||
|
||||
TIME.delta = 1.0f;
|
||||
npcRandomWalkMovement(npc);
|
||||
// Open ground on all sides -- whichever direction is chosen, it moves.
|
||||
assert_int_equal(npc->animation, ENTITY_ANIM_WALK);
|
||||
}
|
||||
|
||||
static void test_npcRandomTurnAndWalkMovementRunsBothIndependently(
|
||||
void **state
|
||||
) {
|
||||
|
||||
entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 });
|
||||
npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_TURN_AND_WALK);
|
||||
npcrandomturnandwalk_t *tw = &npc->data.npc.moveData.randomTurnAndWalk;
|
||||
tw->turn.timer = 999.0f;// don't fire this tick
|
||||
tw->walk.timer = 0.05f;// fires this tick
|
||||
|
||||
TIME.delta = 1.0f;
|
||||
npcRandomTurnAndWalkMovement(npc);
|
||||
assert_int_equal(npc->animation, ENTITY_ANIM_WALK);
|
||||
assert_true(tw->turn.timer > 900.0f);// unaffected by the walk firing
|
||||
}
|
||||
|
||||
static void test_npcMovementGatedByCutscenePause(void **state) {
|
||||
|
||||
entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 });
|
||||
npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_WALK);
|
||||
npc->data.npc.moveData.randomWalk.timer = 0.0f;
|
||||
TIME.delta = 1.0f;
|
||||
|
||||
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NPC;
|
||||
npcMovement(npc);
|
||||
assert_int_equal(npc->animation, ENTITY_ANIM_IDLE);// blocked by pause
|
||||
|
||||
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
|
||||
npcMovement(npc);
|
||||
assert_int_equal(npc->animation, ENTITY_ANIM_WALK);
|
||||
}
|
||||
|
||||
static void test_npcMovementGatedByInteractState(void **state) {
|
||||
|
||||
entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 });
|
||||
npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_WALK);
|
||||
npc->data.npc.moveData.randomWalk.timer = 0.0f;
|
||||
TIME.delta = 1.0f;
|
||||
|
||||
npc->data.npc.interactState = NPC_INTERACT_STATE_CONVERSING;
|
||||
npcMovement(npc);
|
||||
assert_int_equal(npc->animation, ENTITY_ANIM_IDLE);// blocked while conversing
|
||||
|
||||
npc->data.npc.interactState = NPC_INTERACT_STATE_NONE;
|
||||
npcMovement(npc);
|
||||
assert_int_equal(npc->animation, ENTITY_ANIM_WALK);
|
||||
}
|
||||
|
||||
static void test_npcPathMovementFollowsAndLoopsWaypoints(void **state) {
|
||||
|
||||
entity_t *npc = setUpNpc((worldpos_t){ 5, 5, 0 });
|
||||
npcSetMoveType(npc, NPC_MOVE_TYPE_PATH);
|
||||
npcpath_t *path = &npc->data.npc.moveData.path;
|
||||
npcPathAddNode(&npc->data.npc, (worldpos_t){ 5, 6, 0 });
|
||||
npcPathAddNode(&npc->data.npc, (worldpos_t){ 5, 5, 0 });
|
||||
|
||||
npcPathMovement(npc);// steps toward waypoint 0
|
||||
assert_int_equal(npc->position.x, 5);
|
||||
assert_int_equal(npc->position.y, 6);
|
||||
assert_int_equal(path->index, 0);// not yet considered "arrived"
|
||||
|
||||
npc->animation = ENTITY_ANIM_IDLE;// simulate the walk animation finishing
|
||||
npcPathMovement(npc);// arrives at waypoint 0, advances, steps toward wp 1
|
||||
assert_int_equal(path->index, 1);
|
||||
assert_int_equal(npc->position.x, 5);
|
||||
assert_int_equal(npc->position.y, 5);
|
||||
}
|
||||
|
||||
static void test_npcPathMovementNoopWithEmptyPath(void **state) {
|
||||
|
||||
entity_t *npc = setUpNpc((worldpos_t){ 5, 5, 0 });
|
||||
npcSetMoveType(npc, NPC_MOVE_TYPE_PATH);
|
||||
|
||||
npcPathMovement(npc);
|
||||
assert_int_equal(npc->position.x, 5);
|
||||
assert_int_equal(npc->position.y, 5);
|
||||
assert_int_equal(npc->animation, ENTITY_ANIM_IDLE);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_npcSetMoveType),
|
||||
cmocka_unit_test(test_npcRandomTurnMovementFiresOnceTimerElapses),
|
||||
cmocka_unit_test(test_npcRandomWalkMovementFiresOnceTimerElapses),
|
||||
cmocka_unit_test(test_npcRandomTurnAndWalkMovementRunsBothIndependently),
|
||||
cmocka_unit_test(test_npcMovementGatedByCutscenePause),
|
||||
cmocka_unit_test(test_npcMovementGatedByInteractState),
|
||||
cmocka_unit_test(test_npcPathMovementFollowsAndLoopsWaypoints),
|
||||
cmocka_unit_test(test_npcPathMovementNoopWithEmptyPath),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
include(dusktest)
|
||||
|
||||
# Tests
|
||||
dusktest(test_maparea.c)
|
||||
|
||||
# Subdirs
|
||||
@@ -1,217 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "rpg/overworld/maparea.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
static uint8_t callbackCount;
|
||||
static uint8_t lastTrigger;
|
||||
|
||||
static void recordCallback(entity_t *entity, const uint8_t trigger) {
|
||||
callbackCount++;
|
||||
lastTrigger = trigger;
|
||||
}
|
||||
|
||||
static void resetMapAreas(void) {
|
||||
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
|
||||
callbackCount = 0;
|
||||
lastTrigger = 0;
|
||||
}
|
||||
|
||||
static void test_mapAreaInit(void **state) {
|
||||
|
||||
|
||||
maparea_t area;
|
||||
const worldpos_t min = { 5, 5, 0 };
|
||||
const worldpos_t max = { 0, 0, 0 };
|
||||
|
||||
// min/max should be normalized regardless of argument order.
|
||||
mapAreaInit(
|
||||
&area, min, max, recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
|
||||
);
|
||||
assert_int_equal(area.min.x, 0);
|
||||
assert_int_equal(area.min.y, 0);
|
||||
assert_int_equal(area.max.x, 5);
|
||||
assert_int_equal(area.max.y, 5);
|
||||
assert_int_equal(area.triggerCount, 0);
|
||||
|
||||
expect_assert_failure(
|
||||
mapAreaInit(&area, min, max, NULL, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL)
|
||||
);
|
||||
}
|
||||
|
||||
static void test_mapAreaIsInside(void **state) {
|
||||
|
||||
|
||||
maparea_t area;
|
||||
mapAreaInit(
|
||||
&area, (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 10, 10, 0 },
|
||||
recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
|
||||
);
|
||||
|
||||
assert_true(mapAreaIsInside(&area, (worldpos_t){ 5, 5, 0 }));
|
||||
assert_true(mapAreaIsInside(&area, (worldpos_t){ 0, 0, 0 }));// inclusive min
|
||||
assert_true(
|
||||
mapAreaIsInside(&area, (worldpos_t){ 10, 10, 0 })
|
||||
);// inclusive max
|
||||
assert_false(mapAreaIsInside(&area, (worldpos_t){ 11, 5, 0 }));
|
||||
assert_false(mapAreaIsInside(&area, (worldpos_t){ 5, 5, 1 }));
|
||||
}
|
||||
|
||||
static void test_mapAreaShouldNotify(void **state) {
|
||||
|
||||
|
||||
maparea_t area;
|
||||
mapAreaInit(
|
||||
&area, (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 10, 10, 0 },
|
||||
recordCallback, MAP_AREA_NOTIFY_PLAYER, MAP_TRIGGER_ALL
|
||||
);
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER);
|
||||
entityInit(&ENTITIES[1], ENTITY_TYPE_NPC);
|
||||
entityInit(&ENTITIES[2], ENTITY_TYPE_ITEM);
|
||||
|
||||
assert_true(mapAreaShouldNotify(&area, &ENTITIES[0]));
|
||||
assert_false(mapAreaShouldNotify(&area, &ENTITIES[1]));// not notified
|
||||
assert_false(mapAreaShouldNotify(&area, &ENTITIES[2]));// item never notifies
|
||||
|
||||
area.notify = MAP_AREA_NOTIFY_ALL;
|
||||
assert_true(mapAreaShouldNotify(&area, &ENTITIES[1]));
|
||||
}
|
||||
|
||||
static void test_mapAreaAddAndRemove(void **state) {
|
||||
|
||||
resetMapAreas();
|
||||
|
||||
uint8_t id = mapAreaAdd(
|
||||
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
|
||||
recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
|
||||
);
|
||||
assert_int_equal(id, 0);
|
||||
assert_ptr_equal(MAP_AREAS[0].callback, recordCallback);
|
||||
|
||||
// Adding again should reuse the next free slot, not the same one.
|
||||
uint8_t id2 = mapAreaAdd(
|
||||
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
|
||||
recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
|
||||
);
|
||||
assert_int_equal(id2, 1);
|
||||
|
||||
mapAreaRemove(id);
|
||||
assert_null(MAP_AREAS[0].callback);// slot freed
|
||||
|
||||
// Removing frees the slot for reuse.
|
||||
uint8_t id3 = mapAreaAdd(
|
||||
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
|
||||
recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
|
||||
);
|
||||
assert_int_equal(id3, 0);
|
||||
}
|
||||
|
||||
static void test_mapAreaCheckEntityTriggersEnterStepExit(void **state) {
|
||||
|
||||
resetMapAreas();
|
||||
|
||||
mapAreaAdd(
|
||||
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
|
||||
recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
|
||||
);
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER);
|
||||
ENTITIES[0].position = (worldpos_t){ 10, 10, 0 };// outside
|
||||
|
||||
// Outside -> outside: no callback.
|
||||
mapAreaCheckEntity(&ENTITIES[0]);
|
||||
assert_int_equal(callbackCount, 0);
|
||||
assert_int_equal(MAP_AREAS[0].triggerCount, 0);
|
||||
|
||||
// Outside -> inside: ENTER.
|
||||
ENTITIES[0].position = (worldpos_t){ 2, 2, 0 };
|
||||
mapAreaCheckEntity(&ENTITIES[0]);
|
||||
assert_int_equal(callbackCount, 1);
|
||||
assert_int_equal(lastTrigger, MAP_TRIGGER_ENTER);
|
||||
assert_int_equal(MAP_AREAS[0].triggerCount, 1);
|
||||
|
||||
// Inside -> inside: STEP, every subsequent call.
|
||||
mapAreaCheckEntity(&ENTITIES[0]);
|
||||
assert_int_equal(callbackCount, 2);
|
||||
assert_int_equal(lastTrigger, MAP_TRIGGER_STEP);
|
||||
|
||||
mapAreaCheckEntity(&ENTITIES[0]);
|
||||
assert_int_equal(callbackCount, 3);
|
||||
assert_int_equal(lastTrigger, MAP_TRIGGER_STEP);
|
||||
|
||||
// Inside -> outside: EXIT.
|
||||
ENTITIES[0].position = (worldpos_t){ 10, 10, 0 };
|
||||
mapAreaCheckEntity(&ENTITIES[0]);
|
||||
assert_int_equal(callbackCount, 4);
|
||||
assert_int_equal(lastTrigger, MAP_TRIGGER_EXIT);
|
||||
}
|
||||
|
||||
static void test_mapAreaCheckEntityRespectsTriggerMask(void **state) {
|
||||
|
||||
resetMapAreas();
|
||||
|
||||
// Only interested in ENTER, not STEP or EXIT.
|
||||
mapAreaAdd(
|
||||
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
|
||||
recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ENTER
|
||||
);
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER);
|
||||
ENTITIES[0].position = (worldpos_t){ 2, 2, 0 };
|
||||
|
||||
mapAreaCheckEntity(&ENTITIES[0]);// ENTER: fires
|
||||
assert_int_equal(callbackCount, 1);
|
||||
|
||||
mapAreaCheckEntity(&ENTITIES[0]);// STEP: masked out
|
||||
assert_int_equal(callbackCount, 1);
|
||||
|
||||
ENTITIES[0].position = (worldpos_t){ 10, 10, 0 };
|
||||
mapAreaCheckEntity(&ENTITIES[0]);// EXIT: masked out
|
||||
assert_int_equal(callbackCount, 1);
|
||||
}
|
||||
|
||||
static void test_mapAreaCanUnload(void **state) {
|
||||
|
||||
resetMapAreas();
|
||||
memoryZero(&MAP, sizeof(map_t));
|
||||
|
||||
maparea_t area;
|
||||
mapAreaInit(
|
||||
&area, (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
|
||||
recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
|
||||
);
|
||||
|
||||
// No chunk overlaps the area's bounds (all chunks default to position 0,
|
||||
// 0, 0 after memoryZero -- move them far away first).
|
||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||
MAP.chunks[i].position = (chunkpos_t){ 100, 100, 100 };
|
||||
}
|
||||
assert_true(mapAreaCanUnload(&area));
|
||||
|
||||
// One chunk overlapping the area's bounds blocks unload.
|
||||
MAP.chunks[0].position = (chunkpos_t){ 0, 0, 0 };
|
||||
assert_false(mapAreaCanUnload(&area));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_mapAreaInit),
|
||||
cmocka_unit_test(test_mapAreaIsInside),
|
||||
cmocka_unit_test(test_mapAreaShouldNotify),
|
||||
cmocka_unit_test(test_mapAreaAddAndRemove),
|
||||
cmocka_unit_test(test_mapAreaCheckEntityTriggersEnterStepExit),
|
||||
cmocka_unit_test(test_mapAreaCheckEntityRespectsTriggerMask),
|
||||
cmocka_unit_test(test_mapAreaCanUnload),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
// Add RPG tests here in the future
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -10,9 +10,6 @@
|
||||
|
||||
#include "savetestfixture.h"
|
||||
#include "assert/assert.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include <ftw.h>
|
||||
@@ -25,51 +22,6 @@ static char_t SAVE_TEST_FIXTURE_HOME[PATH_MAX];
|
||||
static char_t SAVE_TEST_FIXTURE_REAL_HOME[PATH_MAX];
|
||||
static bool_t SAVE_TEST_FIXTURE_HAD_REAL_HOME;
|
||||
|
||||
static const char_t *SAVE_TEST_FIXTURE_PARTY_JSON =
|
||||
"{\"members\":{\"hero\":{"
|
||||
"\"attack\":10,\"defense\":5,\"magic\":0,\"speed\":10,\"luck\":0,"
|
||||
"\"healthMax\":30,\"mpMax\":10"
|
||||
"}}}";
|
||||
|
||||
static yyjson_doc *SAVE_TEST_FIXTURE_PARTY_DOC = NULL;
|
||||
|
||||
errorret_t saveTestFixturePartyJsonLoadSyncStub(assetloading_t *loading) {
|
||||
loading->entry->data.json = SAVE_TEST_FIXTURE_PARTY_DOC;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveTestFixturePartyJsonDisposeStub(assetentry_t *entry) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void saveTestFixtureStubPartyAsset(void) {
|
||||
memoryZero(&ASSET, sizeof(ASSET));
|
||||
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
|
||||
threadMutexInit(&ASSET.loading[i].mutex);
|
||||
}
|
||||
|
||||
if(SAVE_TEST_FIXTURE_PARTY_DOC == NULL) {
|
||||
SAVE_TEST_FIXTURE_PARTY_DOC = yyjson_read(
|
||||
SAVE_TEST_FIXTURE_PARTY_JSON, strlen(SAVE_TEST_FIXTURE_PARTY_JSON), 0
|
||||
);
|
||||
assertNotNull(
|
||||
SAVE_TEST_FIXTURE_PARTY_DOC, "Failed to parse party JSON fixture"
|
||||
);
|
||||
}
|
||||
|
||||
ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_JSON].loadSync =
|
||||
saveTestFixturePartyJsonLoadSyncStub;
|
||||
ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_JSON].dispose =
|
||||
saveTestFixturePartyJsonDisposeStub;
|
||||
}
|
||||
|
||||
void saveTestFixtureUnstubPartyAsset(void) {
|
||||
if(SAVE_TEST_FIXTURE_PARTY_DOC == NULL) return;
|
||||
yyjson_doc_free(SAVE_TEST_FIXTURE_PARTY_DOC);
|
||||
SAVE_TEST_FIXTURE_PARTY_DOC = NULL;
|
||||
}
|
||||
|
||||
static int_t saveTestFixtureRemoveEntry(
|
||||
const char *path,
|
||||
const struct stat *statBuffer,
|
||||
@@ -97,8 +49,6 @@ int saveTestFixtureSetup(void **state) {
|
||||
|
||||
assertTrue(setenv("HOME", SAVE_TEST_FIXTURE_HOME, 1) == 0, "setenv failed");
|
||||
|
||||
saveTestFixtureStubPartyAsset();
|
||||
|
||||
errorret_t ret = saveInit();
|
||||
assertTrue(errorIsOk(ret), "saveInit failed in test fixture");
|
||||
|
||||
@@ -106,7 +56,6 @@ int saveTestFixtureSetup(void **state) {
|
||||
}
|
||||
|
||||
int saveTestFixtureTeardown(void **state) {
|
||||
saveTestFixtureUnstubPartyAsset();
|
||||
|
||||
nftw(
|
||||
SAVE_TEST_FIXTURE_HOME, saveTestFixtureRemoveEntry, 16,
|
||||
|
||||
@@ -36,20 +36,3 @@ int saveTestFixtureTeardown(void **state);
|
||||
*/
|
||||
void saveTestFixtureBlockSaveDirectory(void);
|
||||
|
||||
/**
|
||||
* Stubs the JSON asset loader's sync callback to hand back a small
|
||||
* in-memory party.jsonc-shaped doc instead of doing a real file/zip read -
|
||||
* saveInit()/saveSlotInit() seed the party roster via partyInit(), which
|
||||
* loads party.jsonc through the real asset system, and there's no real
|
||||
* asset bundle available under the test harness. No thread, no real I/O.
|
||||
* Resets ASSET to a fresh, otherwise-empty state each call, so it's safe
|
||||
* to call once per test. Called automatically by saveTestFixtureSetup.
|
||||
*/
|
||||
void saveTestFixtureStubPartyAsset(void);
|
||||
|
||||
/**
|
||||
* Frees the in-memory doc created by saveTestFixtureStubPartyAsset. Safe
|
||||
* to call even if that function was never called. Called automatically
|
||||
* by saveTestFixtureTeardown.
|
||||
*/
|
||||
void saveTestFixtureUnstubPartyAsset(void);
|
||||
@@ -15,19 +15,6 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// saveSlotInit() seeds the party roster via partyInit(), which loads
|
||||
// party.jsonc through the real asset system - stub it out (see
|
||||
// savetestfixture.h) rather than needing a real asset bundle here.
|
||||
static int assetGroupSetup(void **state) {
|
||||
saveTestFixtureStubPartyAsset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int assetGroupTeardown(void **state) {
|
||||
saveTestFixtureUnstubPartyAsset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Helpers - wrap the writeInit/readInit macros (which can only be used
|
||||
// inside a function returning errorret_t) so tests can call them plainly.
|
||||
@@ -156,12 +143,6 @@ static void test_saveSlotReadJSON_roundTrip(void **state) {
|
||||
SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name)
|
||||
);
|
||||
SAVE.slot.cachedData.playerLevel = 42;
|
||||
// An in-use slot (non-empty name) requires a non-empty map name too -
|
||||
// see saveSlotNewGame/requireStrMin("mapName", 1) in saveSlotVer1ReadJSON.
|
||||
stringCopy(
|
||||
SAVE.slot.cachedData.mapName, "overworld",
|
||||
sizeof(SAVE.slot.cachedData.mapName)
|
||||
);
|
||||
|
||||
char_t *json;
|
||||
size_t len;
|
||||
@@ -178,7 +159,6 @@ static void test_saveSlotReadJSON_roundTrip(void **state) {
|
||||
|
||||
assert_true(stringEquals(read.cachedData.name, "Hero"));
|
||||
assert_int_equal(read.cachedData.playerLevel, 42);
|
||||
assert_true(stringEquals(read.cachedData.mapName, "overworld"));
|
||||
assert_true(read.cachedData.time.time == writtenTime.time);
|
||||
|
||||
// version/dataType are struct-only bookkeeping, never serialized - the
|
||||
@@ -199,33 +179,18 @@ static void test_saveSlotReadJSON_blankSlot_shortCircuits(void **state) {
|
||||
|
||||
// No "name" key (or an empty one) means a blank/never-used slot (see
|
||||
// saveSlotInUse) - the reader returns right after reading that, without
|
||||
// needing or validating time/mapName/partyMembers/partyOrder at all, so
|
||||
// needing or validating time at all, so
|
||||
// this succeeds despite omitting fields that would otherwise be required.
|
||||
errorret_t ret = slotFromJSON(&slot, "{\"version\":1}");
|
||||
assert_true(errorIsOk(ret));
|
||||
|
||||
assert_int_equal(slot.cachedData.name[0], '\0');
|
||||
assert_int_equal(slot.cachedData.mapName[0], '\0');
|
||||
// Not readInt32's own default of 1 - the early return happens before
|
||||
// playerLevel is ever read, leaving the zero-init saveSlotCurrentReadJSON
|
||||
// seeds ver1 with (see the memoryZero in saveSlotCurrentReadJSON).
|
||||
assert_int_equal(slot.cachedData.playerLevel, 0);
|
||||
}
|
||||
|
||||
static void test_saveSlotReadJSON_inUseMissingMapName_errors(void **state) {
|
||||
saveslot_t slot;
|
||||
|
||||
// "name" is present (in use) but "mapName" is missing - a real save
|
||||
// (via saveSlotNewGame) always has both, so this is corrupt.
|
||||
errorret_t ret = slotFromJSON(
|
||||
&slot,
|
||||
"{\"version\":1,\"name\":\"Hero\","
|
||||
"\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}"
|
||||
);
|
||||
assert_true(errorIsNotOk(ret));
|
||||
errorCatch(ret);
|
||||
}
|
||||
|
||||
static void test_saveSlotReadJSON_missingTimeKey_errors(void **state) {
|
||||
saveslot_t slot;
|
||||
|
||||
@@ -302,7 +267,6 @@ int main(void) {
|
||||
cmocka_unit_test(test_saveSlotReadJSON_nullAsserts),
|
||||
cmocka_unit_test(test_saveSlotReadJSON_roundTrip),
|
||||
cmocka_unit_test(test_saveSlotReadJSON_blankSlot_shortCircuits),
|
||||
cmocka_unit_test(test_saveSlotReadJSON_inUseMissingMapName_errors),
|
||||
cmocka_unit_test(test_saveSlotReadJSON_missingTimeKey_errors),
|
||||
cmocka_unit_test(test_saveSlotReadJSON_nameTooLong_errors),
|
||||
cmocka_unit_test(test_saveSlotReadJSON_nonObjectRoot_errors),
|
||||
@@ -310,5 +274,5 @@ int main(void) {
|
||||
cmocka_unit_test(test_saveSlotReadJSON_versionMismatch_errors),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, assetGroupSetup, assetGroupTeardown);
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
Reference in new issue
Block a user