1bd73d69fe
- 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
156 lines
5.2 KiB
C
156 lines
5.2 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/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);
|
|
}
|