433 lines
13 KiB
C
433 lines
13 KiB
C
/**
|
|
* 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, NULL, 0);
|
|
}
|
|
|
|
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, NULL, 0);
|
|
}
|
|
|
|
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, NULL, 0);
|
|
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, NULL, 0);
|
|
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, NULL, 0);
|
|
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, NULL, 0);
|
|
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, NULL, 0);
|
|
}
|
|
|
|
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, NULL, 0);
|
|
}
|
|
|
|
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_physicsWorldStepClimbsRampGoingUp(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, 1, TILE_SHAPE_GROUND);
|
|
}
|
|
|
|
physicsworld_t world;
|
|
physicsWorldInit(&world, 20.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;
|
|
|
|
for(uint32_t i = 0; i < 400; i++) {
|
|
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
|
|
|
|
// Ground height is driven by the body's center (not its corner
|
|
// position), so a 1-wide body barely touching a taller neighbouring
|
|
// column isn't yanked onto that column's full height - see
|
|
// physicsWorldResolveAxisZ. While the center is over the ramp tile,
|
|
// height should track how far across it smoothly, not snap flat.
|
|
const float_t centerX = body.position[0] + 0.5f;
|
|
if(centerX >= 1.0f && centerX < 2.0f) {
|
|
const float_t expected = centerX - 1.0f;
|
|
assert_float_equal(body.position[2], expected, 0.005f);
|
|
}
|
|
}
|
|
|
|
// Fully across, resting on the elevated ground one layer up.
|
|
assert_float_equal(body.position[0], 5.0f, 0.001f);
|
|
assert_float_equal(body.position[2], 1.0f, 0.005f);
|
|
assert_true(body.grounded);
|
|
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_physicsWorldStepDescendsRampGoingDown(void **state) {
|
|
testMapReset();
|
|
testMapSetTile(0, 0, 1, TILE_SHAPE_GROUND);
|
|
testMapSetTile(1, 0, 0, TILE_SHAPE_RAMP_WEST);
|
|
for(worldunit_t x = 2; x < 6; x++) {
|
|
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
|
|
}
|
|
|
|
physicsworld_t world;
|
|
physicsWorldInit(&world, 20.0f, 40.0f);
|
|
|
|
physicsbody_t body;
|
|
const vec3 position = { 0.0f, 0.0f, 1.0f };
|
|
const vec3 extents = { 1.0f, 1.0f, 1.0f };
|
|
physicsBodyInit(&body, position, extents);
|
|
body.velocity[0] = 1.0f;
|
|
|
|
for(uint32_t i = 0; i < 400; i++) {
|
|
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
|
|
|
|
// Never falls through as a hole while crossing the ramp - descent
|
|
// may lag slightly behind the ideal slope (gravity closes the gap
|
|
// each step, see physicsWorldStep's documented limitation) but must
|
|
// stay close to it, never dropping toward the void below.
|
|
assert_true(body.position[2] >= -0.2f);
|
|
}
|
|
|
|
// Fully across, resting on the lower ground.
|
|
assert_float_equal(body.position[0], 5.0f, 0.001f);
|
|
assert_float_equal(body.position[2], 0.0f, 0.005f);
|
|
assert_true(body.grounded);
|
|
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_physicsWorldStepFlatGroundUnaffectedByRampLogic(
|
|
void **state
|
|
) {
|
|
testMapReset();
|
|
for(worldunit_t x = 0; x < 6; x++) {
|
|
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
|
|
}
|
|
|
|
physicsworld_t world;
|
|
physicsWorldInit(&world, 20.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, NULL, 0);
|
|
assert_float_equal(body.position[2], 0.0f, 0.0001f);
|
|
}
|
|
|
|
assert_true(body.position[0] > 1.5f);
|
|
|
|
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),
|
|
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_physicsWorldStepClimbsRampGoingUp),
|
|
cmocka_unit_test(test_physicsWorldStepDescendsRampGoingDown),
|
|
cmocka_unit_test(test_physicsWorldStepFlatGroundUnaffectedByRampLogic),
|
|
cmocka_unit_test(test_physicsWorldStepBlockedByOtherBody),
|
|
cmocka_unit_test(test_physicsWorldStepBlockedByNearestOfMultipleBodies),
|
|
cmocka_unit_test(test_physicsWorldResolveBodyOverlapIgnoresSelfAndNull),
|
|
};
|
|
|
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
|
}
|