Files
dusk/test/rpg/entity/test_entity.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

299 lines
9.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 "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);
}