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