Files
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

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