physics stuff

This commit is contained in:
2026-07-17 19:07:32 -05:00
parent 0bc80d5df3
commit 5f08337726
38 changed files with 945 additions and 634 deletions
+2 -1
View File
@@ -10,4 +10,5 @@ dusktest(test_rpg.c)
# Subdirs
add_subdirectory(overworld)
add_subdirectory(physics)
add_subdirectory(physics)
add_subdirectory(entity)
+12
View File
@@ -0,0 +1,12 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(dusktest)
# Tests
dusktest(test_entitydir.c)
dusktest(test_entity.c)
# Subdirs
+105
View File
@@ -0,0 +1,105 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "util/memory.h"
#include "rpg/entity/entity.h"
static void testEntitiesReset(void) {
memoryZero(ENTITIES, sizeof(ENTITIES));
}
static void testEntityPlace(
const uint8_t index,
const entitytype_t type,
const vec3 position,
const entitydir_t direction
) {
entityInit(&ENTITIES[index], type);
ENTITIES[index].direction = direction;
const vec3 extents = ENTITY_PHYSICS_EXTENTS_DEFAULT;
physicsBodyInit(&ENTITIES[index].body, position, extents);
}
static void test_entityGetFacingFindsEntityAhead(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
testEntityPlace(1, ENTITY_TYPE_NPC, (vec3){ 1.0f, 0.0f, 0.0f },
ENTITY_DIR_NORTH);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_ptr_equal(result, &ENTITIES[1]);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityGetFacingIgnoresEntityBehind(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
testEntityPlace(1, ENTITY_TYPE_NPC, (vec3){ -2.0f, 0.0f, 0.0f },
ENTITY_DIR_NORTH);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_null(result);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityGetFacingIgnoresEntityToSide(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
testEntityPlace(1, ENTITY_TYPE_NPC, (vec3){ 0.0f, 3.0f, 0.0f },
ENTITY_DIR_NORTH);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_null(result);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityGetFacingReturnsNullWhenNothingInRange(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_null(result);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityGetFacingPicksNearest(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
// Farther candidate placed at the lower array index, to prove the
// result is chosen by distance, not array/insertion order.
testEntityPlace(1, ENTITY_TYPE_NPC, (vec3){ 1.5f, 0.0f, 0.0f },
ENTITY_DIR_NORTH);
testEntityPlace(2, ENTITY_TYPE_NPC, (vec3){ 1.0f, 0.0f, 0.0f },
ENTITY_DIR_NORTH);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_ptr_equal(result, &ENTITIES[2]);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_entityGetFacingFindsEntityAhead),
cmocka_unit_test(test_entityGetFacingIgnoresEntityBehind),
cmocka_unit_test(test_entityGetFacingIgnoresEntityToSide),
cmocka_unit_test(test_entityGetFacingReturnsNullWhenNothingInRange),
cmocka_unit_test(test_entityGetFacingPicksNearest),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+84
View File
@@ -0,0 +1,84 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "util/memory.h"
#include "rpg/entity/entitydir.h"
static void test_entityDirToVec2(void **state) {
vec2 out;
entityDirToVec2(ENTITY_DIR_NORTH, out);
assert_float_equal(out[0], 0.0f, 0.0001f);
assert_float_equal(out[1], 1.0f, 0.0001f);
entityDirToVec2(ENTITY_DIR_EAST, out);
assert_float_equal(out[0], 1.0f, 0.0001f);
assert_float_equal(out[1], 0.0f, 0.0001f);
entityDirToVec2(ENTITY_DIR_SOUTH, out);
assert_float_equal(out[0], 0.0f, 0.0001f);
assert_float_equal(out[1], -1.0f, 0.0001f);
entityDirToVec2(ENTITY_DIR_WEST, out);
assert_float_equal(out[0], -1.0f, 0.0001f);
assert_float_equal(out[1], 0.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityDirFromVec2Cardinal(void **state) {
assert_int_equal(
entityDirFromVec2((vec2){ 0.0f, 1.0f }), ENTITY_DIR_NORTH
);
assert_int_equal(
entityDirFromVec2((vec2){ 1.0f, 0.0f }), ENTITY_DIR_EAST
);
assert_int_equal(
entityDirFromVec2((vec2){ 0.0f, -1.0f }), ENTITY_DIR_SOUTH
);
assert_int_equal(
entityDirFromVec2((vec2){ -1.0f, 0.0f }), ENTITY_DIR_WEST
);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityDirFromVec2DominantAxis(void **state) {
// |x| > |y| picks east/west regardless of y's sign.
assert_int_equal(
entityDirFromVec2((vec2){ 2.0f, 1.0f }), ENTITY_DIR_EAST
);
assert_int_equal(
entityDirFromVec2((vec2){ -2.0f, -1.0f }), ENTITY_DIR_WEST
);
// |y| >= |x| picks north/south.
assert_int_equal(
entityDirFromVec2((vec2){ 1.0f, 2.0f }), ENTITY_DIR_NORTH
);
assert_int_equal(
entityDirFromVec2((vec2){ -1.0f, -3.0f }), ENTITY_DIR_SOUTH
);
// Exact diagonal ties break toward north/south.
assert_int_equal(
entityDirFromVec2((vec2){ 0.7f, 0.7f }), ENTITY_DIR_NORTH
);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_entityDirToVec2),
cmocka_unit_test(test_entityDirFromVec2Cardinal),
cmocka_unit_test(test_entityDirFromVec2DominantAxis),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+1
View File
@@ -6,5 +6,6 @@
include(dusktest)
# Tests
dusktest(test_maparea.c)
# Subdirs
+126
View File
@@ -0,0 +1,126 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "util/memory.h"
#include "rpg/overworld/maparea.h"
static uint32_t TEST_ENTER_COUNT;
static uint32_t TEST_STEP_COUNT;
static uint32_t TEST_EXIT_COUNT;
static void testAreaCallback(entity_t *entity, const uint8_t trigger) {
if(trigger == MAP_TRIGGER_ENTER) TEST_ENTER_COUNT++;
else if(trigger == MAP_TRIGGER_STEP) TEST_STEP_COUNT++;
else if(trigger == MAP_TRIGGER_EXIT) TEST_EXIT_COUNT++;
}
static void testMapAreaReset(void) {
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
TEST_ENTER_COUNT = 0;
TEST_STEP_COUNT = 0;
TEST_EXIT_COUNT = 0;
}
static entity_t testEntityAt(const entitytype_t type, const worldpos_t pos) {
entity_t entity;
memoryZero(&entity, sizeof(entity));
entity.id = 0;
entity.type = type;
entity.position = pos;
return entity;
}
static void test_mapAreaCheckEntityFiresEnterAndExit(void **state) {
testMapAreaReset();
mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
testAreaCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
);
entity_t entity = testEntityAt(ENTITY_TYPE_PLAYER, (worldpos_t){ 10, 10, 0 });
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_ENTER_COUNT, 0);
entity.position = (worldpos_t){ 2, 2, 0 };
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_ENTER_COUNT, 1);
assert_int_equal(TEST_STEP_COUNT, 0);
entity.position = (worldpos_t){ 10, 10, 0 };
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_EXIT_COUNT, 1);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_mapAreaCheckEntityStepFiresOncePerNewTile(void **state) {
testMapAreaReset();
mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
testAreaCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
);
entity_t entity = testEntityAt(ENTITY_TYPE_PLAYER, (worldpos_t){ 1, 1, 0 });
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_ENTER_COUNT, 1);
assert_int_equal(TEST_STEP_COUNT, 0);
// Same tile, checked repeatedly (simulating multiple frames without the
// entity moving) - STEP must not fire.
mapAreaCheckEntity(&entity);
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_STEP_COUNT, 0);
// Moves to a new tile, still inside - STEP fires once.
entity.position = (worldpos_t){ 2, 1, 0 };
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_STEP_COUNT, 1);
// Same new tile again over multiple calls - no additional STEP.
mapAreaCheckEntity(&entity);
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_STEP_COUNT, 1);
// Another new tile - STEP fires again.
entity.position = (worldpos_t){ 3, 1, 0 };
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_STEP_COUNT, 2);
assert_int_equal(TEST_ENTER_COUNT, 1);
assert_int_equal(TEST_EXIT_COUNT, 0);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_mapAreaCheckEntityNotifyFlagsRespected(void **state) {
testMapAreaReset();
mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
testAreaCallback, MAP_AREA_NOTIFY_PLAYER, MAP_TRIGGER_ALL
);
entity_t npc = testEntityAt(ENTITY_TYPE_NPC, (worldpos_t){ 1, 1, 0 });
mapAreaCheckEntity(&npc);
assert_int_equal(TEST_ENTER_COUNT, 0);
entity_t player = testEntityAt(ENTITY_TYPE_PLAYER, (worldpos_t){ 1, 1, 0 });
mapAreaCheckEntity(&player);
assert_int_equal(TEST_ENTER_COUNT, 1);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_mapAreaCheckEntityFiresEnterAndExit),
cmocka_unit_test(test_mapAreaCheckEntityStepFiresOncePerNewTile),
cmocka_unit_test(test_mapAreaCheckEntityNotifyFlagsRespected),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+97 -9
View File
@@ -61,7 +61,7 @@ static void test_physicsWorldStepStraightLineNoObstacles(void **state) {
const uint32_t steps = 10;
for(uint32_t i = 0; i < steps; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
}
assert_float_equal(body.position[0], 1.0f * steps * DUSK_TIME_STEP, 0.001f);
@@ -89,7 +89,7 @@ static void test_physicsWorldStepBlockedHorizontally(void **state) {
body.velocity[0] = 5.0f;
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
}
assert_float_equal(body.position[0], 2.0f, 0.0001f);
@@ -97,7 +97,7 @@ static void test_physicsWorldStepBlockedHorizontally(void **state) {
// Further steps must not push it past the wall.
for(uint32_t i = 0; i < 5; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_float_equal(body.position[0], 2.0f, 0.0001f);
}
@@ -117,7 +117,7 @@ static void test_physicsWorldStepGravitySettlesOnFloor(void **state) {
physicsBodyInit(&body, position, extents);
for(uint32_t i = 0; i < 200; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_true(body.position[2] >= -0.0001f);
}
@@ -142,7 +142,7 @@ static void test_physicsWorldStepFallsThroughHole(void **state) {
for(uint32_t i = 0; i < 50; i++) {
const float_t previousZ = body.position[2];
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_true(body.position[2] < previousZ);
assert_false(body.grounded);
}
@@ -163,7 +163,7 @@ static void test_physicsWorldStepTerminalVelocityClamp(void **state) {
physicsBodyInit(&body, position, extents);
for(uint32_t i = 0; i < 300; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_true(fabsf(body.velocity[2]) <= world.terminalVelocity + 0.0001f);
}
@@ -186,7 +186,7 @@ static void test_physicsWorldStepBlockedByCeiling(void **state) {
body.velocity[2] = 1.0f;
for(uint32_t i = 0; i < 200; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
}
assert_float_equal(body.position[2], 1.0f, 0.0001f);
@@ -215,7 +215,7 @@ static void test_physicsWorldStepMultiColumnFootprint(void **state) {
body.velocity[0] = 5.0f;
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
}
assert_float_equal(body.position[0], 1.0f, 0.0001f);
@@ -242,7 +242,7 @@ static void test_physicsWorldStepRampTreatedAsFlatGround(void **state) {
body.velocity[0] = 5.0f;
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_float_equal(body.position[2], 0.0f, 0.0001f);
}
@@ -252,6 +252,91 @@ static void test_physicsWorldStepRampTreatedAsFlatGround(void **state) {
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepBlockedByOtherBody(void **state) {
testMapReset();
for(worldunit_t x = 0; x < 10; x++) {
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
}
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsbody_t other;
physicsBodyInit(&other, (vec3){ 5.0f, 0.0f, 0.0f }, extents);
physicsbody_t body;
physicsBodyInit(&body, (vec3){ 0.0f, 0.0f, 0.0f }, extents);
body.velocity[0] = 5.0f;
physicsbody_t *others[] = { &other };
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, others, 1);
}
// Other occupies [5,6) - body (1 unit wide) should stop exactly
// touching it, at x=4.
assert_float_equal(body.position[0], 4.0f, 0.0001f);
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
assert_float_equal(other.position[0], 5.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepBlockedByNearestOfMultipleBodies(
void **state
) {
testMapReset();
for(worldunit_t x = 0; x < 20; x++) {
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
}
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsbody_t nearOther;
physicsBodyInit(&nearOther, (vec3){ 5.0f, 0.0f, 0.0f }, extents);
physicsbody_t farOther;
physicsBodyInit(&farOther, (vec3){ 15.0f, 0.0f, 0.0f }, extents);
physicsbody_t body;
physicsBodyInit(&body, (vec3){ 0.0f, 0.0f, 0.0f }, extents);
body.velocity[0] = 5.0f;
physicsbody_t *others[] = { &farOther, &nearOther };
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, others, 2);
}
assert_float_equal(body.position[0], 4.0f, 0.0001f);
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldResolveBodyOverlapIgnoresSelfAndNull(
void **state
) {
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsbody_t body;
physicsBodyInit(&body, (vec3){ 0.0f, 0.0f, 0.0f }, extents);
body.velocity[0] = 1.0f;
physicsbody_t *others[] = { &body, NULL };
physicsWorldResolveBodyOverlap(&body, 0, others, 2);
assert_float_equal(body.position[0], 0.0f, 0.0001f);
assert_float_equal(body.velocity[0], 1.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_physicsWorldStepStraightLineNoObstacles),
@@ -262,6 +347,9 @@ int main(void) {
cmocka_unit_test(test_physicsWorldStepBlockedByCeiling),
cmocka_unit_test(test_physicsWorldStepMultiColumnFootprint),
cmocka_unit_test(test_physicsWorldStepRampTreatedAsFlatGround),
cmocka_unit_test(test_physicsWorldStepBlockedByOtherBody),
cmocka_unit_test(test_physicsWorldStepBlockedByNearestOfMultipleBodies),
cmocka_unit_test(test_physicsWorldResolveBodyOverlapIgnoresSelfAndNull),
};
return cmocka_run_group_tests(tests, NULL, NULL);