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