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,19 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
include(dusktest)
|
||||
|
||||
# Tests
|
||||
dusktest(test_entity.c)
|
||||
target_sources(test_entity PRIVATE entitytestfixture.c)
|
||||
|
||||
dusktest(test_npc.c)
|
||||
target_sources(test_npc PRIVATE entitytestfixture.c)
|
||||
|
||||
dusktest(test_entityinteract.c)
|
||||
target_sources(test_entityinteract PRIVATE entitytestfixture.c)
|
||||
|
||||
dusktest(test_entityitem.c)
|
||||
target_sources(test_entityitem PRIVATE entitytestfixture.c)
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "entitytestfixture.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
#include "rpg/overworld/tile.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
void entityTestFixtureReset(void) {
|
||||
memoryZero(ENTITIES, sizeof(ENTITIES));
|
||||
|
||||
memoryZero(&MAP, sizeof(map_t));
|
||||
MAP.loaded = true;
|
||||
MAP.chunkPosition = (chunkpos_t){ 0, 0, 0 };
|
||||
|
||||
chunk_t *chunk = &MAP.chunks[0];
|
||||
chunk->position = (chunkpos_t){ 0, 0, 0 };
|
||||
for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) {
|
||||
chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND, .z = 0 };
|
||||
}
|
||||
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
||||
chunk->entities[i] = 0xFF;
|
||||
}
|
||||
|
||||
MAP.chunkOrder[0] = chunk;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/entity/entity.h"
|
||||
|
||||
/**
|
||||
* Resets ENTITIES[] to all-empty and builds a single minimal, synchronously
|
||||
* "loaded" chunk at chunk position (0, 0, 0) on the MAP global, entirely
|
||||
* TILE_SHAPE_GROUND at local Z 0. Sufficient for entityWalk/Turn/Run, chunk
|
||||
* bookkeeping, and NPC movement tests -- does not exercise the real async
|
||||
* chunk-loading pipeline (mapChunkLoad).
|
||||
*
|
||||
* World positions with x/y in [0, CHUNK_WIDTH-1]/[0, CHUNK_HEIGHT-1] and
|
||||
* z == 0 fall within the fixture chunk. Individual tests may override
|
||||
* specific columns afterward (e.g. to place a ramp).
|
||||
*/
|
||||
void entityTestFixtureReset(void);
|
||||
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 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 "ui/rpg/textbox/uitextboxmain.h"
|
||||
#include "ui/focus/uifocus.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
|
||||
static const cutsceneitem_t CUTSCENE_TEST_INTERACT_ITEMS[] = {
|
||||
CUTSCENE_WAIT(1.0f)
|
||||
};
|
||||
static const cutscene_t CUTSCENE_TEST_INTERACT = {
|
||||
.items = CUTSCENE_TEST_INTERACT_ITEMS,
|
||||
.itemCount = 1,
|
||||
.pause = CUTSCENE_PAUSE_NONE,
|
||||
.dataSize = 0
|
||||
};
|
||||
|
||||
static entity_t *player;
|
||||
static entity_t *target;
|
||||
|
||||
static void resetInteractFixture(void) {
|
||||
entityTestFixtureReset();
|
||||
uiFocusInit();
|
||||
uiTextboxMainInit();
|
||||
uiTextboxMainFocusClosed(NULL);// force-clear stale focus from a prior test
|
||||
cutsceneSystemInit();
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER);
|
||||
entityInit(&ENTITIES[1], ENTITY_TYPE_NPC);
|
||||
player = &ENTITIES[0];
|
||||
target = &ENTITIES[1];
|
||||
player->direction = ENTITY_DIR_NORTH;
|
||||
}
|
||||
|
||||
static void test_entityInteractWithNull(void **state) {
|
||||
|
||||
resetInteractFixture();
|
||||
|
||||
target->interact.type = ENTITY_INTERACT_NULL;
|
||||
entityInteractWith(player, target);// should not crash or change anything
|
||||
assert_int_equal(target->interact.type, ENTITY_INTERACT_NULL);
|
||||
}
|
||||
|
||||
static void test_entityInteractWithPrintTurnsNpcToFacePlayer(void **state) {
|
||||
|
||||
resetInteractFixture();
|
||||
|
||||
target->interact.type = ENTITY_INTERACT_PRINT;
|
||||
target->interact.data.message = "Hello!";
|
||||
|
||||
entityInteractWith(player, target);
|
||||
|
||||
assert_true(uiTextboxMainIsActive());
|
||||
assert_int_equal(target->data.npc.interactState, NPC_INTERACT_STATE_CONVERSING);
|
||||
// NPC turns to face the opposite of the player's facing direction.
|
||||
assert_int_equal(target->direction, entityDirGetOpposite(player->direction));
|
||||
}
|
||||
|
||||
static entity_t *callbackPlayerArg;
|
||||
static entity_t *callbackTargetArg;
|
||||
static uint8_t callbackCount;
|
||||
|
||||
static void recordInteractCallback(entity_t *p, entity_t *t) {
|
||||
callbackPlayerArg = p;
|
||||
callbackTargetArg = t;
|
||||
callbackCount++;
|
||||
}
|
||||
|
||||
static void test_entityInteractWithCallback(void **state) {
|
||||
|
||||
resetInteractFixture();
|
||||
callbackCount = 0;
|
||||
|
||||
target->interact.type = ENTITY_INTERACT_CALLBACK;
|
||||
target->interact.data.callback = recordInteractCallback;
|
||||
|
||||
entityInteractWith(player, target);
|
||||
|
||||
assert_int_equal(callbackCount, 1);
|
||||
assert_ptr_equal(callbackPlayerArg, player);
|
||||
assert_ptr_equal(callbackTargetArg, target);
|
||||
}
|
||||
|
||||
static void test_entityInteractWithCallbackRequiresNonNull(void **state) {
|
||||
|
||||
resetInteractFixture();
|
||||
|
||||
target->interact.type = ENTITY_INTERACT_CALLBACK;
|
||||
target->interact.data.callback = NULL;
|
||||
|
||||
expect_assert_failure(entityInteractWith(player, target));
|
||||
}
|
||||
|
||||
static void test_entityInteractWithCutsceneStartsIt(void **state) {
|
||||
|
||||
resetInteractFixture();
|
||||
|
||||
target->interact.type = ENTITY_INTERACT_CUTSCENE;
|
||||
target->interact.data.cutscene = &CUTSCENE_TEST_INTERACT;
|
||||
|
||||
entityInteractWith(player, target);
|
||||
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INTERACT);
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.entityInteract, player);
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.entityInteracted, target);
|
||||
}
|
||||
|
||||
static void test_entityInteractWithRequiresNonNullEntities(void **state) {
|
||||
|
||||
resetInteractFixture();
|
||||
|
||||
expect_assert_failure(entityInteractWith(NULL, target));
|
||||
expect_assert_failure(entityInteractWith(player, NULL));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_entityInteractWithNull),
|
||||
cmocka_unit_test(test_entityInteractWithPrintTurnsNpcToFacePlayer),
|
||||
cmocka_unit_test(test_entityInteractWithCallback),
|
||||
cmocka_unit_test(test_entityInteractWithCallbackRequiresNonNull),
|
||||
cmocka_unit_test(test_entityInteractWithCutsceneStartsIt),
|
||||
cmocka_unit_test(test_entityInteractWithRequiresNonNullEntities),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 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 "ui/rpg/textbox/uitextboxmain.h"
|
||||
#include "ui/focus/uifocus.h"
|
||||
|
||||
static entity_t *setUpItemEntity(void) {
|
||||
entityTestFixtureReset();
|
||||
uiFocusInit();
|
||||
uiTextboxMainInit();
|
||||
uiTextboxMainFocusClosed(NULL);// force-clear stale focus from a prior test
|
||||
|
||||
entityInit(&ENTITIES[0], ENTITY_TYPE_ITEM);
|
||||
return &ENTITIES[0];
|
||||
}
|
||||
|
||||
static void test_entityItemInit(void **state) {
|
||||
|
||||
entity_t *item = setUpItemEntity();
|
||||
|
||||
assert_int_equal(item->interact.type, ENTITY_INTERACT_CALLBACK);
|
||||
assert_ptr_equal(item->interact.data.callback, entityItemInteract);
|
||||
}
|
||||
|
||||
static void test_entityItemSet(void **state) {
|
||||
|
||||
entity_t *item = setUpItemEntity();
|
||||
|
||||
entityItemSet(item, ITEM_ID_POTION, 3);
|
||||
assert_int_equal(item->data.item.item, ITEM_ID_POTION);
|
||||
assert_int_equal(item->data.item.quantity, 3);
|
||||
assert_false(item->data.item.collected);
|
||||
}
|
||||
|
||||
// entityItemInteract itself is NOT covered here: it unconditionally calls
|
||||
// itemGive(), which calls itemGetName(), which dereferences
|
||||
// LOCALE.entry->data.locale -- a real locale asset populated only by the
|
||||
// async asset-loading pipeline (see localemanager.h/assetlocaleloader.h).
|
||||
// That's exactly the asset-loading dependency this test pass scoped out;
|
||||
// faking it cheaply isn't possible without hand-building the loader's
|
||||
// internal hash format, which is more coupling than it's worth. Backpack
|
||||
// bookkeeping itself is already covered by test/item/test_inventory.c-style
|
||||
// tests at the inventory/backpack layer.
|
||||
|
||||
static void test_entityItemMovementWaitsForCollectionAndTextbox(void **state) {
|
||||
|
||||
entity_t *item = setUpItemEntity();
|
||||
|
||||
// Not collected yet -- stays.
|
||||
entityItemMovement(item);
|
||||
assert_int_equal(item->type, ENTITY_TYPE_ITEM);
|
||||
|
||||
// Collected (set directly -- see note above on why entityItemInteract
|
||||
// itself isn't driven here), but a message is still showing -- stays.
|
||||
item->data.item.collected = true;
|
||||
uiTextboxMainSetText("Picked up!");
|
||||
entityItemMovement(item);
|
||||
assert_int_equal(item->type, ENTITY_TYPE_ITEM);
|
||||
|
||||
// Once the textbox is dismissed, the entity despawns.
|
||||
uiFocusPop();
|
||||
assert_false(uiTextboxMainIsActive());
|
||||
entityItemMovement(item);
|
||||
assert_int_equal(item->type, ENTITY_TYPE_NULL);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_entityItemInit),
|
||||
cmocka_unit_test(test_entityItemSet),
|
||||
cmocka_unit_test(test_entityItemMovementWaitsForCollectionAndTextbox),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -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