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
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
Reference in New Issue
Block a user