Files
dusk/test/rpg/overworld/test_maparea.c
T
YourWishes 1bd73d69fe Clamp keyframe interpolation to last value, add main menu scene/UI, battle HUD, and expanded test coverage
- keyframeGetValue now returns the last keyframe's value for times at or
  beyond it, fixes a missing util/math.h include, and asserts keyframes are
  sorted by time; adds test/animation/test_keyframe.c
- Adds mainmenu scene/UI and a battle HUD UI frame
- Adds save autosave-related fields and battle scene tweaks
- Adds headless test coverage for cutscenes, entities, and map areas
2026-08-06 12:58:07 -05:00

214 lines
6.3 KiB
C

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