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