Clamp keyframe interpolation to last value, add main menu scene/UI, battle HUD, and expanded test coverage
- keyframeGetValue now returns the last keyframe's value for times at or beyond it, fixes a missing util/math.h include, and asserts keyframes are sorted by time; adds test/animation/test_keyframe.c - Adds mainmenu scene/UI and a battle HUD UI frame - Adds save autosave-related fields and battle scene tweaks - Adds headless test coverage for cutscenes, entities, and map areas
This commit is contained in:
@@ -6,5 +6,6 @@
|
||||
include(dusktest)
|
||||
|
||||
# Tests
|
||||
dusktest(test_maparea.c)
|
||||
|
||||
# Subdirs
|
||||
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
Reference in New Issue
Block a user