Roadmap and physics

This commit is contained in:
2026-07-17 13:09:58 -05:00
parent 2432443ea6
commit 0bc80d5df3
12 changed files with 868 additions and 3 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ add_subdirectory(error)
add_subdirectory(network)
add_subdirectory(thread)
add_subdirectory(display)
# add_subdirectory(rpg)
add_subdirectory(rpg)
# add_subdirectory(item)
add_subdirectory(time)
add_subdirectory(util)
+2 -1
View File
@@ -9,4 +9,5 @@ include(dusktest)
dusktest(test_rpg.c)
# Subdirs
add_subdirectory(overworld)
add_subdirectory(overworld)
add_subdirectory(physics)
+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_physicsbody.c)
dusktest(test_physicsworld.c)
# Subdirs
+78
View File
@@ -0,0 +1,78 @@
/**
* 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/physics/physicsbody.h"
static void test_physicsBodyInit(void **state) {
physicsbody_t body;
const vec3 position = { 1.0f, 2.0f, 3.0f };
const vec3 extents = { 1.0f, 2.0f, 3.0f };
physicsBodyInit(&body, position, extents);
assert_float_equal(body.position[0], 1.0f, 0.0001f);
assert_float_equal(body.position[1], 2.0f, 0.0001f);
assert_float_equal(body.position[2], 3.0f, 0.0001f);
assert_float_equal(body.extents[0], 1.0f, 0.0001f);
assert_float_equal(body.extents[1], 2.0f, 0.0001f);
assert_float_equal(body.extents[2], 3.0f, 0.0001f);
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
assert_float_equal(body.velocity[1], 0.0f, 0.0001f);
assert_float_equal(body.velocity[2], 0.0f, 0.0001f);
assert_false(body.grounded);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsBodyGetBounds(void **state) {
physicsbody_t body;
const vec3 position = { 2.0f, 3.0f, 4.0f };
const vec3 extents = { 1.0f, 2.0f, 0.5f };
physicsBodyInit(&body, position, extents);
vec3 min, max;
physicsBodyGetBounds(&body, min, max);
assert_float_equal(min[0], 2.0f, 0.0001f);
assert_float_equal(min[1], 3.0f, 0.0001f);
assert_float_equal(min[2], 4.0f, 0.0001f);
assert_float_equal(max[0], 3.0f, 0.0001f);
assert_float_equal(max[1], 5.0f, 0.0001f);
assert_float_equal(max[2], 4.5f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsBodyGetBoundsNegativeCoordinates(void **state) {
physicsbody_t body;
const vec3 position = { -5.0f, -1.5f, -2.0f };
const vec3 extents = { 2.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
vec3 min, max;
physicsBodyGetBounds(&body, min, max);
assert_float_equal(min[0], -5.0f, 0.0001f);
assert_float_equal(min[1], -1.5f, 0.0001f);
assert_float_equal(min[2], -2.0f, 0.0001f);
assert_float_equal(max[0], -3.0f, 0.0001f);
assert_float_equal(max[1], -0.5f, 0.0001f);
assert_float_equal(max[2], -1.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_physicsBodyInit),
cmocka_unit_test(test_physicsBodyGetBounds),
cmocka_unit_test(test_physicsBodyGetBoundsNegativeCoordinates),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+268
View File
@@ -0,0 +1,268 @@
/**
* 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 "time/time.h"
#include "rpg/physics/physicsbody.h"
#include "rpg/physics/physicsworld.h"
#include "rpg/overworld/map.h"
#include "rpg/overworld/tile.h"
#include "rpg/overworld/tileshape.h"
#include "rpg/overworld/worldpos.h"
static void testMapReset(void) {
memoryZero(&MAP, sizeof(map_t));
MAP.loaded = true;
MAP.chunkPosition = (chunkpos_t){ 0, 0, 0 };
// Push every chunk but the first out of the loaded window, so only
// MAP.chunks[0] (positioned at the origin below) resolves through
// mapRebuildChunkOrder - otherwise every chunk would default to
// position (0,0,0) too and collide on the same chunk order slot.
for(chunkindex_t i = 1; i < MAP_CHUNK_COUNT; i++) {
MAP.chunks[i].position = (chunkpos_t){ 100, 100, 100 };
}
MAP.chunks[0].position = (chunkpos_t){ 0, 0, 0 };
mapRebuildChunkOrder();
}
static void testMapSetTile(
const worldunit_t x,
const worldunit_t y,
const worldunit_t z,
const tileshape_t shape
) {
const worldpos_t pos = { x, y, z };
const chunktileindex_t index = worldPosToChunkTileIndex(&pos);
const uint8_t localZ = worldPosToChunkLocalZ(&pos);
MAP.chunks[0].tiles[index] = (tile_t){ .shape = shape, .z = localZ };
}
static void test_physicsWorldStepStraightLineNoObstacles(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);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 0.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
body.velocity[0] = 1.0f;
const uint32_t steps = 10;
for(uint32_t i = 0; i < steps; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
}
assert_float_equal(body.position[0], 1.0f * steps * DUSK_TIME_STEP, 0.001f);
assert_float_equal(body.position[1], 0.0f, 0.0001f);
assert_float_equal(body.position[2], 0.0f, 0.0001f);
assert_float_equal(body.velocity[0], 1.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepBlockedHorizontally(void **state) {
testMapReset();
testMapSetTile(0, 0, 0, TILE_SHAPE_GROUND);
testMapSetTile(1, 0, 0, TILE_SHAPE_GROUND);
testMapSetTile(2, 0, 0, TILE_SHAPE_GROUND);
// x = 3 left as TILE_SHAPE_NULL, acting as a wall.
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 0.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
body.velocity[0] = 5.0f;
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
}
assert_float_equal(body.position[0], 2.0f, 0.0001f);
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
// Further steps must not push it past the wall.
for(uint32_t i = 0; i < 5; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
assert_float_equal(body.position[0], 2.0f, 0.0001f);
}
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepGravitySettlesOnFloor(void **state) {
testMapReset();
testMapSetTile(0, 0, 0, TILE_SHAPE_GROUND);
physicsworld_t world;
physicsWorldInit(&world, 20.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 3.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
for(uint32_t i = 0; i < 200; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
assert_true(body.position[2] >= -0.0001f);
}
assert_float_equal(body.position[2], 0.0f, 0.0001f);
assert_float_equal(body.velocity[2], 0.0f, 0.0001f);
assert_true(body.grounded);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepFallsThroughHole(void **state) {
testMapReset();
// No tiles set anywhere - every column is a hole.
physicsworld_t world;
physicsWorldInit(&world, 20.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 5.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
for(uint32_t i = 0; i < 50; i++) {
const float_t previousZ = body.position[2];
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
assert_true(body.position[2] < previousZ);
assert_false(body.grounded);
}
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepTerminalVelocityClamp(void **state) {
testMapReset();
// No tiles set anywhere - every column is a hole.
physicsworld_t world;
physicsWorldInit(&world, 20.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 5.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
for(uint32_t i = 0; i < 300; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
assert_true(fabsf(body.velocity[2]) <= world.terminalVelocity + 0.0001f);
}
assert_float_equal(fabsf(body.velocity[2]), world.terminalVelocity, 0.01f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepBlockedByCeiling(void **state) {
testMapReset();
testMapSetTile(0, 0, 2, TILE_SHAPE_GROUND);
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 0.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
body.velocity[2] = 1.0f;
for(uint32_t i = 0; i < 200; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
}
assert_float_equal(body.position[2], 1.0f, 0.0001f);
assert_float_equal(body.velocity[2], 0.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepMultiColumnFootprint(void **state) {
testMapReset();
for(worldunit_t x = 0; x < 6; x++) {
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
}
testMapSetTile(0, 1, 0, TILE_SHAPE_GROUND);
testMapSetTile(1, 1, 0, TILE_SHAPE_GROUND);
// x = 2, y = 1 left as TILE_SHAPE_NULL, blocking only the second row
// spanned by the body's 2-unit-deep footprint.
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 0.0f };
const vec3 extents = { 1.0f, 2.0f, 1.0f };
physicsBodyInit(&body, position, extents);
body.velocity[0] = 5.0f;
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
}
assert_float_equal(body.position[0], 1.0f, 0.0001f);
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepRampTreatedAsFlatGround(void **state) {
testMapReset();
testMapSetTile(0, 0, 0, TILE_SHAPE_GROUND);
testMapSetTile(1, 0, 0, TILE_SHAPE_RAMP_EAST);
for(worldunit_t x = 2; x < 6; x++) {
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
}
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 0.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
body.velocity[0] = 5.0f;
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
assert_float_equal(body.position[2], 0.0f, 0.0001f);
}
assert_float_equal(body.velocity[0], 5.0f, 0.0001f);
assert_true(body.position[0] > 1.5f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_physicsWorldStepStraightLineNoObstacles),
cmocka_unit_test(test_physicsWorldStepBlockedHorizontally),
cmocka_unit_test(test_physicsWorldStepGravitySettlesOnFloor),
cmocka_unit_test(test_physicsWorldStepFallsThroughHole),
cmocka_unit_test(test_physicsWorldStepTerminalVelocityClamp),
cmocka_unit_test(test_physicsWorldStepBlockedByCeiling),
cmocka_unit_test(test_physicsWorldStepMultiColumnFootprint),
cmocka_unit_test(test_physicsWorldStepRampTreatedAsFlatGround),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}