physics stuff
This commit is contained in:
@@ -12,7 +12,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
player.c
|
player.c
|
||||||
)
|
)
|
||||||
|
|
||||||
add_subdirectory(anim)
|
|
||||||
add_subdirectory(interact)
|
add_subdirectory(interact)
|
||||||
add_subdirectory(npc)
|
add_subdirectory(npc)
|
||||||
add_subdirectory(item)
|
add_subdirectory(item)
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
# Copyright (c) 2026 Dominic Masters
|
|
||||||
#
|
|
||||||
# This software is released under the MIT License.
|
|
||||||
# https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|
||||||
PUBLIC
|
|
||||||
entityanim.c
|
|
||||||
entityanimidle.c
|
|
||||||
entityanimturn.c
|
|
||||||
entityanimwalk.c
|
|
||||||
entityanimrun.c
|
|
||||||
)
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "rpg/entity/entity.h"
|
|
||||||
#include "rpg/overworld/map.h"
|
|
||||||
#include "rpg/overworld/tile.h"
|
|
||||||
#include "time/time.h"
|
|
||||||
#include "entityanimwalk.h"
|
|
||||||
|
|
||||||
const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT] = {
|
|
||||||
[ENTITY_ANIM_IDLE] = { entityAnimIdleUpdate },
|
|
||||||
[ENTITY_ANIM_TURN] = { entityAnimTurnUpdate },
|
|
||||||
[ENTITY_ANIM_WALK] = { entityAnimWalkUpdate },
|
|
||||||
[ENTITY_ANIM_RUN] = { entityAnimRunUpdate },
|
|
||||||
};
|
|
||||||
|
|
||||||
float_t entityAnimTileZOffset(const worldpos_t pos) {
|
|
||||||
return tileShapeIsRamp(mapGetTile(pos).shape) ? 0.5f : 0.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
void entityAnimUpdate(entity_t *entity) {
|
|
||||||
if(entity->animation != ENTITY_ANIM_IDLE) {
|
|
||||||
entity->animTime -= TIME.delta;
|
|
||||||
if(entity->animTime <= 0) {
|
|
||||||
if(
|
|
||||||
entity->animation == ENTITY_ANIM_WALK ||
|
|
||||||
entity->animation == ENTITY_ANIM_RUN
|
|
||||||
) {
|
|
||||||
entity->walkEndCooldown = ENTITY_ANIM_WALK_TURN_COOLDOWN;
|
|
||||||
}
|
|
||||||
entity->animation = ENTITY_ANIM_IDLE;
|
|
||||||
entity->animTime = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(entity->walkEndCooldown > 0) {
|
|
||||||
entity->walkEndCooldown -= TIME.delta;
|
|
||||||
if(entity->walkEndCooldown < 0) entity->walkEndCooldown = 0;
|
|
||||||
}
|
|
||||||
ENTITY_ANIM_CALLBACKS[entity->animation].update(entity);
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "dusk.h"
|
|
||||||
#include "entityanimidle.h"
|
|
||||||
#include "entityanimturn.h"
|
|
||||||
#include "entityanimwalk.h"
|
|
||||||
#include "entityanimrun.h"
|
|
||||||
|
|
||||||
typedef struct entity_s entity_t;
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
ENTITY_ANIM_IDLE,
|
|
||||||
ENTITY_ANIM_TURN,
|
|
||||||
ENTITY_ANIM_WALK,
|
|
||||||
ENTITY_ANIM_RUN,
|
|
||||||
ENTITY_ANIM_COUNT
|
|
||||||
} entityanim_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
/** Updates the render position for this animation state. */
|
|
||||||
void (*update)(entity_t *entity);
|
|
||||||
} entityanimcallback_t;
|
|
||||||
|
|
||||||
extern const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the entity animation timer and render position.
|
|
||||||
*
|
|
||||||
* @param entity Pointer to the entity to update.
|
|
||||||
*/
|
|
||||||
void entityAnimUpdate(entity_t *entity);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns 0.5 if the tile at pos is a ramp, 0.0 otherwise.
|
|
||||||
* Used to lift entity render position to mid-ramp height.
|
|
||||||
*
|
|
||||||
* @param pos World position to sample.
|
|
||||||
* @returns float_t The Z offset to apply.
|
|
||||||
*/
|
|
||||||
float_t entityAnimTileZOffset(const worldpos_t pos);
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "rpg/entity/entity.h"
|
|
||||||
#include "entityanim.h"
|
|
||||||
|
|
||||||
void entityAnimIdleUpdate(entity_t *entity) {
|
|
||||||
entity->renderPosition[0] = (float_t)entity->position.x;
|
|
||||||
entity->renderPosition[1] = (float_t)entity->position.y;
|
|
||||||
entity->renderPosition[2] = (
|
|
||||||
(float_t)entity->position.z + entityAnimTileZOffset(entity->position)
|
|
||||||
) * WORLD_LAYER_HEIGHT;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "dusk.h"
|
|
||||||
|
|
||||||
typedef struct entity_s entity_t;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates render position for the idle animation state.
|
|
||||||
*
|
|
||||||
* @param entity Pointer to the entity to update.
|
|
||||||
*/
|
|
||||||
void entityAnimIdleUpdate(entity_t *entity);
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "rpg/entity/entity.h"
|
|
||||||
#include "entityanim.h"
|
|
||||||
|
|
||||||
void entityAnimRunUpdate(entity_t *entity) {
|
|
||||||
float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_RUN_DURATION);
|
|
||||||
float_t zFrom = (float_t)entity->lastPosition.z
|
|
||||||
+ entityAnimTileZOffset(entity->lastPosition);
|
|
||||||
float_t zTo = (float_t)entity->position.z
|
|
||||||
+ entityAnimTileZOffset(entity->position);
|
|
||||||
entity->renderPosition[0] = (float_t)entity->lastPosition.x + t * (
|
|
||||||
(float_t)entity->position.x - (float_t)entity->lastPosition.x
|
|
||||||
);
|
|
||||||
entity->renderPosition[1] = (float_t)entity->lastPosition.y + t * (
|
|
||||||
(float_t)entity->position.y - (float_t)entity->lastPosition.y
|
|
||||||
);
|
|
||||||
entity->renderPosition[2] = (zFrom + t * (zTo - zFrom)) * WORLD_LAYER_HEIGHT;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "dusk.h"
|
|
||||||
#include "time/time.h"
|
|
||||||
|
|
||||||
typedef struct entity_s entity_t;
|
|
||||||
|
|
||||||
#define ENTITY_ANIM_RUN_DURATION TIME_TICKS_TO_TIME(6)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates render position for the run animation state.
|
|
||||||
*
|
|
||||||
* @param entity Pointer to the entity to update.
|
|
||||||
*/
|
|
||||||
void entityAnimRunUpdate(entity_t *entity);
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "rpg/entity/entity.h"
|
|
||||||
#include "entityanim.h"
|
|
||||||
|
|
||||||
void entityAnimTurnUpdate(entity_t *entity) {
|
|
||||||
entity->renderPosition[0] = (float_t)entity->position.x;
|
|
||||||
entity->renderPosition[1] = (float_t)entity->position.y;
|
|
||||||
entity->renderPosition[2] = (
|
|
||||||
(float_t)entity->position.z + entityAnimTileZOffset(entity->position)
|
|
||||||
) * WORLD_LAYER_HEIGHT;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "dusk.h"
|
|
||||||
#include "time/time.h"
|
|
||||||
|
|
||||||
typedef struct entity_s entity_t;
|
|
||||||
|
|
||||||
#define ENTITY_ANIM_TURN_DURATION TIME_TICKS_TO_TIME(4)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates render position for the turn animation state.
|
|
||||||
*
|
|
||||||
* @param entity Pointer to the entity to update.
|
|
||||||
*/
|
|
||||||
void entityAnimTurnUpdate(entity_t *entity);
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "rpg/entity/entity.h"
|
|
||||||
#include "entityanim.h"
|
|
||||||
|
|
||||||
void entityAnimWalkUpdate(entity_t *entity) {
|
|
||||||
float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_WALK_DURATION);
|
|
||||||
float_t zFrom = (float_t)entity->lastPosition.z
|
|
||||||
+ entityAnimTileZOffset(entity->lastPosition);
|
|
||||||
float_t zTo = (float_t)entity->position.z
|
|
||||||
+ entityAnimTileZOffset(entity->position);
|
|
||||||
entity->renderPosition[0] = (float_t)entity->lastPosition.x + t * (
|
|
||||||
(float_t)entity->position.x - (float_t)entity->lastPosition.x
|
|
||||||
);
|
|
||||||
entity->renderPosition[1] = (float_t)entity->lastPosition.y + t * (
|
|
||||||
(float_t)entity->position.y - (float_t)entity->lastPosition.y
|
|
||||||
);
|
|
||||||
entity->renderPosition[2] = (zFrom + t * (zTo - zFrom)) * WORLD_LAYER_HEIGHT;
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "dusk.h"
|
|
||||||
#include "time/time.h"
|
|
||||||
|
|
||||||
typedef struct entity_s entity_t;
|
|
||||||
|
|
||||||
#define ENTITY_ANIM_WALK_DURATION TIME_TICKS_TO_TIME(12)
|
|
||||||
#define ENTITY_ANIM_WALK_TURN_COOLDOWN TIME_TICKS_TO_TIME(4)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates render position for the walk animation state.
|
|
||||||
*
|
|
||||||
* @param entity Pointer to the entity to update.
|
|
||||||
*/
|
|
||||||
void entityAnimWalkUpdate(entity_t *entity);
|
|
||||||
+118
-190
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2025 Dominic Masters
|
* Copyright (c) 2025 Dominic Masters
|
||||||
*
|
*
|
||||||
* This software is released under the MIT License.
|
* This software is released under the MIT License.
|
||||||
* https://opensource.org/licenses/MIT
|
* https://opensource.org/licenses/MIT
|
||||||
*/
|
*/
|
||||||
@@ -8,14 +8,14 @@
|
|||||||
#include "entity.h"
|
#include "entity.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "time/time.h"
|
|
||||||
#include "util/math.h"
|
#include "util/math.h"
|
||||||
|
#include "time/time.h"
|
||||||
#include "rpg/overworld/map.h"
|
#include "rpg/overworld/map.h"
|
||||||
#include "rpg/overworld/maparea.h"
|
#include "rpg/overworld/maparea.h"
|
||||||
#include "rpg/overworld/chunk.h"
|
#include "rpg/overworld/chunk.h"
|
||||||
#include "rpg/overworld/tile.h"
|
|
||||||
|
|
||||||
entity_t ENTITIES[ENTITY_COUNT];
|
entity_t ENTITIES[ENTITY_COUNT];
|
||||||
|
physicsworld_t ENTITY_PHYSICS_WORLD;
|
||||||
|
|
||||||
void entityInit(entity_t *entity, const entitytype_t type) {
|
void entityInit(entity_t *entity, const entitytype_t type) {
|
||||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||||
@@ -32,6 +32,9 @@ void entityInit(entity_t *entity, const entitytype_t type) {
|
|||||||
entity->type = type;
|
entity->type = type;
|
||||||
entity->chunkIndex = 0xFF;
|
entity->chunkIndex = 0xFF;
|
||||||
|
|
||||||
|
const vec3 extents = ENTITY_PHYSICS_EXTENTS_DEFAULT;
|
||||||
|
physicsBodyInit(&entity->body, (vec3){ 0.0f, 0.0f, 0.0f }, extents);
|
||||||
|
|
||||||
if(ENTITY_CALLBACKS[type].init != NULL) ENTITY_CALLBACKS[type].init(entity);
|
if(ENTITY_CALLBACKS[type].init != NULL) ENTITY_CALLBACKS[type].init(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,212 +43,115 @@ void entityUpdate(entity_t *entity) {
|
|||||||
assertTrue(entity->type < ENTITY_TYPE_COUNT, "Invalid entity type");
|
assertTrue(entity->type < ENTITY_TYPE_COUNT, "Invalid entity type");
|
||||||
assertTrue(entity->type != ENTITY_TYPE_NULL, "Cannot have NULL entity type");
|
assertTrue(entity->type != ENTITY_TYPE_NULL, "Cannot have NULL entity type");
|
||||||
|
|
||||||
// What state is the entity in?
|
|
||||||
entityAnimUpdate(entity);
|
|
||||||
|
|
||||||
// Movement code.
|
|
||||||
if(ENTITY_CALLBACKS[entity->type].movement != NULL) {
|
if(ENTITY_CALLBACKS[entity->type].movement != NULL) {
|
||||||
ENTITY_CALLBACKS[entity->type].movement(entity);
|
ENTITY_CALLBACKS[entity->type].movement(entity);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
bool_t entityCanTurn(entity_t *entity) {
|
physicsbody_t *others[ENTITY_COUNT];
|
||||||
return entity->animation == ENTITY_ANIM_IDLE &&
|
uint32_t othersCount = 0;
|
||||||
entity->walkEndCooldown <= 0;
|
for(uint8_t i = 0; i < ENTITY_COUNT; i++) {
|
||||||
}
|
if(ENTITIES[i].type == ENTITY_TYPE_NULL) continue;
|
||||||
|
if(&ENTITIES[i] == entity) continue;
|
||||||
|
others[othersCount++] = &ENTITIES[i].body;
|
||||||
|
}
|
||||||
|
|
||||||
bool_t entityCanWalk(entity_t *entity) {
|
physicsWorldStep(
|
||||||
return entity->animation == ENTITY_ANIM_IDLE;
|
&ENTITY_PHYSICS_WORLD, &entity->body, TIME.delta, others, othersCount
|
||||||
}
|
);
|
||||||
|
entitySyncFromPhysics(entity);
|
||||||
bool_t entityCanRun(entity_t *entity) {
|
|
||||||
return entity->animation == ENTITY_ANIM_IDLE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t entityCanUnload(entity_t *entity) {
|
bool_t entityCanUnload(entity_t *entity) {
|
||||||
return entity->globalId < ENTITY_GLOBAL_ID_START;
|
return entity->globalId < ENTITY_GLOBAL_ID_START;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void entityMove(
|
||||||
|
entity_t *entity, const vec2 direction, const bool_t running
|
||||||
|
) {
|
||||||
|
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||||
|
|
||||||
|
const float_t magSq =
|
||||||
|
direction[0] * direction[0] + direction[1] * direction[1];
|
||||||
|
if(magSq <= ENTITY_MOVE_DEADZONE * ENTITY_MOVE_DEADZONE) {
|
||||||
|
entity->body.velocity[0] = 0.0f;
|
||||||
|
entity->body.velocity[1] = 0.0f;
|
||||||
|
entity->animation = ENTITY_ANIM_IDLE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float_t mag = sqrtf(magSq);
|
||||||
|
const float_t clampedMag = mathMin(mag, 1.0f);
|
||||||
|
const float_t speed = running ? ENTITY_MOVE_RUN_SPEED : ENTITY_MOVE_WALK_SPEED;
|
||||||
|
|
||||||
|
entity->body.velocity[0] = (direction[0] / mag) * clampedMag * speed;
|
||||||
|
entity->body.velocity[1] = (direction[1] / mag) * clampedMag * speed;
|
||||||
|
entity->direction = entityDirFromVec2(direction);
|
||||||
|
entity->animation = running ? ENTITY_ANIM_RUN : ENTITY_ANIM_WALK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityStop(entity_t *entity) {
|
||||||
|
const vec2 zero = { 0.0f, 0.0f };
|
||||||
|
entityMove(entity, zero, false);
|
||||||
|
}
|
||||||
|
|
||||||
void entityTurn(entity_t *entity, const entitydir_t direction) {
|
void entityTurn(entity_t *entity, const entitydir_t direction) {
|
||||||
if(!entityCanTurn(entity)) return;
|
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||||
entity->direction = direction;
|
entity->direction = direction;
|
||||||
entity->animation = ENTITY_ANIM_TURN;
|
|
||||||
entity->animTime = ENTITY_ANIM_TURN_DURATION;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityWalk(entity_t *entity, const entitydir_t direction) {
|
void entityWalk(entity_t *entity, const entitydir_t direction) {
|
||||||
if(!entityCanWalk(entity)) return;
|
vec2 dirVec;
|
||||||
// TODO: Animation, delay, etc.
|
entityDirToVec2(direction, dirVec);
|
||||||
entity->direction = direction;
|
entityMove(entity, dirVec, false);
|
||||||
|
|
||||||
// Where are we moving?
|
|
||||||
worldpos_t newPos = entity->position;
|
|
||||||
worldunits_t relX, relY;
|
|
||||||
{
|
|
||||||
entityDirGetRelative(direction, &relX, &relY);
|
|
||||||
newPos.x += relX;
|
|
||||||
newPos.y += relY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get tile under foot
|
|
||||||
tile_t tileCurrent = mapGetTile(entity->position);
|
|
||||||
tile_t tileNew = mapGetTile(newPos);
|
|
||||||
bool_t fall = false;
|
|
||||||
bool_t raise = false;
|
|
||||||
|
|
||||||
// Are we walking up a ramp?
|
|
||||||
if(
|
|
||||||
tileShapeIsRamp(tileCurrent.shape) &&
|
|
||||||
(
|
|
||||||
// Can only walk UP the direction the ramp faces.
|
|
||||||
(direction+TILE_SHAPE_RAMP_NORTH) == tileCurrent.shape ||
|
|
||||||
// If diagonal ramp, can go up one of two ways only. Inner ramps
|
|
||||||
// share the same allowed directions as their outer counterparts.
|
|
||||||
(
|
|
||||||
(
|
|
||||||
(
|
|
||||||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST ||
|
|
||||||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER
|
|
||||||
) &&
|
|
||||||
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
(
|
|
||||||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST ||
|
|
||||||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER
|
|
||||||
) &&
|
|
||||||
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
(
|
|
||||||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST ||
|
|
||||||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER
|
|
||||||
) &&
|
|
||||||
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
(
|
|
||||||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST ||
|
|
||||||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER
|
|
||||||
) &&
|
|
||||||
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
// Must be able to walk up.
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
tile_t tileNewSaved = tileNew;
|
|
||||||
tileNew = TILE_NULL;
|
|
||||||
worldpos_t abovePos = newPos;
|
|
||||||
abovePos.z += 1;
|
|
||||||
tile_t tileAbove = mapGetTile(abovePos);
|
|
||||||
|
|
||||||
if(
|
|
||||||
tileAbove.shape != TILE_SHAPE_NULL &&
|
|
||||||
tileShapeIsWalkable(tileAbove.shape)
|
|
||||||
) {
|
|
||||||
raise = true;
|
|
||||||
} else {
|
|
||||||
tileNew = tileNewSaved;
|
|
||||||
}
|
|
||||||
} else if(tileNew.shape == TILE_SHAPE_NULL && newPos.z > 0) {
|
|
||||||
// Falling down?
|
|
||||||
worldpos_t belowPos = newPos;
|
|
||||||
belowPos.z -= 1;
|
|
||||||
tile_t tileBelow = mapGetTile(belowPos);
|
|
||||||
if(
|
|
||||||
tileBelow.shape != TILE_SHAPE_NULL &&
|
|
||||||
tileShapeIsRamp(tileBelow.shape) &&
|
|
||||||
(
|
|
||||||
// This handles regular cardinal ramps
|
|
||||||
(
|
|
||||||
entityDirGetOpposite(direction)+TILE_SHAPE_RAMP_NORTH
|
|
||||||
) == tileBelow.shape ||
|
|
||||||
// This handles diagonal ramps. Inner ramps share the same
|
|
||||||
// allowed directions as their outer counterparts.
|
|
||||||
(
|
|
||||||
(
|
|
||||||
(
|
|
||||||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST ||
|
|
||||||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER
|
|
||||||
) &&
|
|
||||||
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
(
|
|
||||||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST ||
|
|
||||||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER
|
|
||||||
) &&
|
|
||||||
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
(
|
|
||||||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST ||
|
|
||||||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER
|
|
||||||
) &&
|
|
||||||
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
(
|
|
||||||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST ||
|
|
||||||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER
|
|
||||||
) &&
|
|
||||||
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// We will fall to this tile.
|
|
||||||
fall = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Can we walk here?
|
|
||||||
if(!raise && !fall && !tileShapeIsWalkable(tileNew.shape)) return;// Blocked
|
|
||||||
|
|
||||||
// Raise/fall must be applied before checking for blocking entities,
|
|
||||||
// otherwise the check compares against the wrong z-level.
|
|
||||||
if(raise) {
|
|
||||||
newPos.z += 1;
|
|
||||||
} else if(fall) {
|
|
||||||
newPos.z -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entity in way?
|
|
||||||
entity_t *other = ENTITIES;
|
|
||||||
do {
|
|
||||||
if(other == entity) continue;
|
|
||||||
if(other->type == ENTITY_TYPE_NULL) continue;
|
|
||||||
if(!worldPosIsEqual(other->position, newPos)) continue;
|
|
||||||
return;// Blocked
|
|
||||||
} while(++other, other < &ENTITIES[ENTITY_COUNT]);
|
|
||||||
|
|
||||||
entity->lastPosition = entity->position;
|
|
||||||
entity->position = newPos;
|
|
||||||
entity->animation = ENTITY_ANIM_WALK;
|
|
||||||
entity->animTime = ENTITY_ANIM_WALK_DURATION;// TODO: Running vs walking
|
|
||||||
entityUpdateChunk(entity);
|
|
||||||
mapAreaCheckEntity(entity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityRun(entity_t *entity, const entitydir_t direction) {
|
void entityRun(entity_t *entity, const entitydir_t direction) {
|
||||||
if(!entityCanRun(entity)) return;
|
vec2 dirVec;
|
||||||
entityWalk(entity, direction);
|
entityDirToVec2(direction, dirVec);
|
||||||
if(entity->animation == ENTITY_ANIM_WALK) {
|
entityMove(entity, dirVec, true);
|
||||||
entity->animation = ENTITY_ANIM_RUN;
|
|
||||||
entity->animTime = ENTITY_ANIM_RUN_DURATION;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entity_t * entityGetAt(const worldpos_t position) {
|
entity_t * entityGetFacing(entity_t *entity, const float_t range) {
|
||||||
|
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||||
|
|
||||||
|
vec2 dir;
|
||||||
|
entityDirToVec2(entity->direction, dir);
|
||||||
|
|
||||||
|
vec3 min, max;
|
||||||
|
physicsBodyGetBounds(&entity->body, min, max);
|
||||||
|
|
||||||
|
const vec3 probeMin = {
|
||||||
|
min[0] + dir[0] * range, min[1] + dir[1] * range, min[2]
|
||||||
|
};
|
||||||
|
const vec3 probeMax = {
|
||||||
|
max[0] + dir[0] * range, max[1] + dir[1] * range, max[2]
|
||||||
|
};
|
||||||
|
|
||||||
|
entity_t *best = NULL;
|
||||||
|
float_t bestDistSq = 0.0f;
|
||||||
|
|
||||||
entity_t *ent = ENTITIES;
|
entity_t *ent = ENTITIES;
|
||||||
do {
|
do {
|
||||||
if(ent->type == ENTITY_TYPE_NULL) continue;
|
if(ent->type == ENTITY_TYPE_NULL) continue;
|
||||||
if(!worldPosIsEqual(ent->position, position)) continue;
|
if(ent == entity) continue;
|
||||||
return ent;
|
|
||||||
|
vec3 oMin, oMax;
|
||||||
|
physicsBodyGetBounds(&ent->body, oMin, oMax);
|
||||||
|
|
||||||
|
if(probeMin[0] >= oMax[0] || probeMax[0] <= oMin[0]) continue;
|
||||||
|
if(probeMin[1] >= oMax[1] || probeMax[1] <= oMin[1]) continue;
|
||||||
|
if(probeMin[2] >= oMax[2] || probeMax[2] <= oMin[2]) continue;
|
||||||
|
|
||||||
|
const float_t dx = ent->body.position[0] - entity->body.position[0];
|
||||||
|
const float_t dy = ent->body.position[1] - entity->body.position[1];
|
||||||
|
const float_t distSq = dx * dx + dy * dy;
|
||||||
|
|
||||||
|
if(best != NULL && distSq >= bestDistSq) continue;
|
||||||
|
best = ent;
|
||||||
|
bestDistSq = distSq;
|
||||||
} while(++ent, ent < &ENTITIES[ENTITY_COUNT]);
|
} while(++ent, ent < &ENTITIES[ENTITY_COUNT]);
|
||||||
|
|
||||||
return NULL;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
entity_t * entityGetByGlobalId(const entityglobalid_t globalId) {
|
entity_t * entityGetByGlobalId(const entityglobalid_t globalId) {
|
||||||
@@ -270,12 +176,14 @@ uint8_t entityGetAvailable() {
|
|||||||
|
|
||||||
void entityPositionSet(entity_t *entity, const worldpos_t pos) {
|
void entityPositionSet(entity_t *entity, const worldpos_t pos) {
|
||||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||||
entity->lastPosition = pos;
|
|
||||||
entity->position = pos;
|
const vec3 floatPos = {
|
||||||
|
(float_t)pos.x, (float_t)pos.y, (float_t)pos.z
|
||||||
|
};
|
||||||
|
const vec3 extents = ENTITY_PHYSICS_EXTENTS_DEFAULT;
|
||||||
|
physicsBodyInit(&entity->body, floatPos, extents);
|
||||||
entity->animation = ENTITY_ANIM_IDLE;
|
entity->animation = ENTITY_ANIM_IDLE;
|
||||||
entity->animTime = 0;
|
entitySyncFromPhysics(entity);
|
||||||
entity->walkEndCooldown = 0;
|
|
||||||
entityUpdateChunk(entity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
|
void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
|
||||||
@@ -297,11 +205,14 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
|
|||||||
if(chunkIndex != 0xFF) {
|
if(chunkIndex != 0xFF) {
|
||||||
chunk_t *next = mapGetChunk(chunkIndex);
|
chunk_t *next = mapGetChunk(chunkIndex);
|
||||||
if(next != NULL) {
|
if(next != NULL) {
|
||||||
|
bool_t inserted = false;
|
||||||
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
||||||
if(next->entities[i] != 0xFF) continue;
|
if(next->entities[i] != 0xFF) continue;
|
||||||
next->entities[i] = entity->id;
|
next->entities[i] = entity->id;
|
||||||
|
inserted = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
assertTrue(inserted, "Chunk entity slot overflow");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,5 +223,22 @@ void entityUpdateChunk(entity_t *entity) {
|
|||||||
chunkpos_t cp;
|
chunkpos_t cp;
|
||||||
worldPosToChunkPos(&entity->position, &cp);
|
worldPosToChunkPos(&entity->position, &cp);
|
||||||
chunkindex_t ci = mapGetChunkIndexAt(cp);
|
chunkindex_t ci = mapGetChunkIndexAt(cp);
|
||||||
if(ci != -1) entitySetChunk(entity, (uint8_t)ci);
|
if(ci == -1 || ci == entity->chunkIndex) return;
|
||||||
}
|
entitySetChunk(entity, (uint8_t)ci);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entitySyncFromPhysics(entity_t *entity) {
|
||||||
|
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||||
|
|
||||||
|
entity->position = (worldpos_t){
|
||||||
|
(worldunit_t)floorf(entity->body.position[0]),
|
||||||
|
(worldunit_t)floorf(entity->body.position[1]),
|
||||||
|
(worldunit_t)floorf(entity->body.position[2])
|
||||||
|
};
|
||||||
|
|
||||||
|
glm_vec3_copy(entity->body.position, entity->renderPosition);
|
||||||
|
entity->renderPosition[2] *= WORLD_LAYER_HEIGHT;
|
||||||
|
|
||||||
|
entityUpdateChunk(entity);
|
||||||
|
mapAreaCheckEntity(entity);
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,10 +7,11 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "entitydir.h"
|
#include "entitydir.h"
|
||||||
#include "anim/entityanim.h"
|
|
||||||
#include "interact/entityinteract.h"
|
#include "interact/entityinteract.h"
|
||||||
#include "entitytype.h"
|
#include "entitytype.h"
|
||||||
#include "npc/npc.h"
|
#include "npc/npc.h"
|
||||||
|
#include "rpg/physics/physicsbody.h"
|
||||||
|
#include "rpg/physics/physicsworld.h"
|
||||||
|
|
||||||
typedef struct map_s map_t;
|
typedef struct map_s map_t;
|
||||||
|
|
||||||
@@ -20,6 +21,29 @@ typedef uint16_t entityglobalid_t;
|
|||||||
#define ENTITY_GLOBAL_ID_START 1
|
#define ENTITY_GLOBAL_ID_START 1
|
||||||
#define ENTITY_GLOBAL_ID_PLAYER 1
|
#define ENTITY_GLOBAL_ID_PLAYER 1
|
||||||
|
|
||||||
|
// Default collision box for every entity's physics body - matches the old
|
||||||
|
// system's exact one-tile footprint.
|
||||||
|
#define ENTITY_PHYSICS_EXTENTS_DEFAULT { 1.0f, 1.0f, 1.0f }
|
||||||
|
|
||||||
|
// Movement speeds, in grid units per second - chosen to match the feel of
|
||||||
|
// the old fixed-duration one-tile-per-step system (12 ticks/tile walking,
|
||||||
|
// 6 ticks/tile running, at DUSK_TIME_STEP = 16ms).
|
||||||
|
#define ENTITY_MOVE_WALK_SPEED 5.2083f
|
||||||
|
#define ENTITY_MOVE_RUN_SPEED 10.4167f
|
||||||
|
|
||||||
|
// Movement vectors below this magnitude are treated as no movement.
|
||||||
|
#define ENTITY_MOVE_DEADZONE 0.1f
|
||||||
|
|
||||||
|
// How far in front of an entity entityGetFacing probes for a target.
|
||||||
|
#define ENTITY_INTERACT_RANGE 1.0f
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ENTITY_ANIM_IDLE,
|
||||||
|
ENTITY_ANIM_WALK,
|
||||||
|
ENTITY_ANIM_RUN,
|
||||||
|
ENTITY_ANIM_COUNT
|
||||||
|
} entityanim_t;
|
||||||
|
|
||||||
typedef struct entity_s {
|
typedef struct entity_s {
|
||||||
uint8_t id;
|
uint8_t id;
|
||||||
entityglobalid_t globalId;
|
entityglobalid_t globalId;
|
||||||
@@ -28,13 +52,18 @@ typedef struct entity_s {
|
|||||||
|
|
||||||
// Movement
|
// Movement
|
||||||
entitydir_t direction;
|
entitydir_t direction;
|
||||||
|
physicsbody_t body;
|
||||||
|
|
||||||
|
// Derived each frame from body.position (floored) - kept for systems
|
||||||
|
// that still assume an integer grid position (chunk membership, map
|
||||||
|
// area triggers, entity-at-position queries).
|
||||||
worldpos_t position;
|
worldpos_t position;
|
||||||
worldpos_t lastPosition;
|
|
||||||
|
// Derived each frame from body.position - mirrors the physics position
|
||||||
|
// into render/world-float space (z scaled by WORLD_LAYER_HEIGHT).
|
||||||
vec3 renderPosition;
|
vec3 renderPosition;
|
||||||
|
|
||||||
entityanim_t animation;
|
entityanim_t animation;
|
||||||
float_t animTime;
|
|
||||||
float_t walkEndCooldown;
|
|
||||||
|
|
||||||
entityinteract_t interact;
|
entityinteract_t interact;
|
||||||
|
|
||||||
@@ -43,6 +72,9 @@ typedef struct entity_s {
|
|||||||
|
|
||||||
extern entity_t ENTITIES[ENTITY_COUNT];
|
extern entity_t ENTITIES[ENTITY_COUNT];
|
||||||
|
|
||||||
|
// Shared physics world every entity's body steps against.
|
||||||
|
extern physicsworld_t ENTITY_PHYSICS_WORLD;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes an entity structure.
|
* Initializes an entity structure.
|
||||||
*
|
*
|
||||||
@@ -58,30 +90,6 @@ void entityInit(entity_t *entity, const entitytype_t type);
|
|||||||
*/
|
*/
|
||||||
void entityUpdate(entity_t *entity);
|
void entityUpdate(entity_t *entity);
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the entity is in a state where it can turn.
|
|
||||||
*
|
|
||||||
* @param entity Pointer to the entity to check.
|
|
||||||
* @returns True if the entity can turn.
|
|
||||||
*/
|
|
||||||
bool_t entityCanTurn(entity_t *entity);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the entity is in a state where it can walk.
|
|
||||||
*
|
|
||||||
* @param entity Pointer to the entity to check.
|
|
||||||
* @returns True if the entity can walk.
|
|
||||||
*/
|
|
||||||
bool_t entityCanWalk(entity_t *entity);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the entity is in a state where it can run.
|
|
||||||
*
|
|
||||||
* @param entity Pointer to the entity to check.
|
|
||||||
* @returns True if the entity can run.
|
|
||||||
*/
|
|
||||||
bool_t entityCanRun(entity_t *entity);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the entity is allowed to be unloaded. By default this is
|
* Returns true if the entity is allowed to be unloaded. By default this is
|
||||||
* true for entities whose global ID falls within the randomly assigned
|
* true for entities whose global ID falls within the randomly assigned
|
||||||
@@ -93,7 +101,29 @@ bool_t entityCanRun(entity_t *entity);
|
|||||||
bool_t entityCanUnload(entity_t *entity);
|
bool_t entityCanUnload(entity_t *entity);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turn an entity to face a new direction.
|
* Moves an entity continuously in a direction, at walking or running
|
||||||
|
* speed. Sets the entity's facing to the nearest cardinal direction that
|
||||||
|
* matches the movement vector. Must be called every tick the entity
|
||||||
|
* should keep moving - unlike the old tile-stepping system this does not
|
||||||
|
* complete a move on its own; call entityStop to stop.
|
||||||
|
*
|
||||||
|
* @param entity Pointer to the entity to move.
|
||||||
|
* @param direction Movement vector, magnitude 0-1 (values longer than 1
|
||||||
|
* are clamped to 1, so diagonals aren't faster than cardinals).
|
||||||
|
* @param running Whether to move at running speed instead of walking.
|
||||||
|
*/
|
||||||
|
void entityMove(entity_t *entity, const vec2 direction, const bool_t running);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops an entity's horizontal movement (equivalent to
|
||||||
|
* entityMove(entity, {0, 0}, false)).
|
||||||
|
*
|
||||||
|
* @param entity Pointer to the entity to stop.
|
||||||
|
*/
|
||||||
|
void entityStop(entity_t *entity);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn an entity to face a new direction, instantly, without moving it.
|
||||||
*
|
*
|
||||||
* @param entity Pointer to the entity to turn.
|
* @param entity Pointer to the entity to turn.
|
||||||
* @param direction The direction to face.
|
* @param direction The direction to face.
|
||||||
@@ -101,7 +131,10 @@ bool_t entityCanUnload(entity_t *entity);
|
|||||||
void entityTurn(entity_t *entity, const entitydir_t direction);
|
void entityTurn(entity_t *entity, const entitydir_t direction);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make an entity walk in a direction.
|
* Makes an entity walk continuously in a cardinal direction, at walking
|
||||||
|
* speed. Convenience wrapper over entityMove for callers that only think
|
||||||
|
* in terms of the 4 cardinal directions. Must be called every tick the
|
||||||
|
* entity should keep moving.
|
||||||
*
|
*
|
||||||
* @param entity Pointer to the entity to make walk.
|
* @param entity Pointer to the entity to make walk.
|
||||||
* @param direction The direction to walk in.
|
* @param direction The direction to walk in.
|
||||||
@@ -109,7 +142,8 @@ void entityTurn(entity_t *entity, const entitydir_t direction);
|
|||||||
void entityWalk(entity_t *entity, const entitydir_t direction);
|
void entityWalk(entity_t *entity, const entitydir_t direction);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make an entity run in a direction.
|
* Makes an entity walk continuously in a cardinal direction, at running
|
||||||
|
* speed. See entityWalk.
|
||||||
*
|
*
|
||||||
* @param entity Pointer to the entity to make run.
|
* @param entity Pointer to the entity to make run.
|
||||||
* @param direction The direction to run in.
|
* @param direction The direction to run in.
|
||||||
@@ -117,13 +151,16 @@ void entityWalk(entity_t *entity, const entitydir_t direction);
|
|||||||
void entityRun(entity_t *entity, const entitydir_t direction);
|
void entityRun(entity_t *entity, const entitydir_t direction);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the entity at a specific world position.
|
* Finds the closest other entity whose bounds overlap a probe box
|
||||||
|
* projected out from the given entity's own bounds, along its current
|
||||||
|
* facing direction. Used for interaction targeting - continuous-position
|
||||||
|
* aware, unlike an exact tile match.
|
||||||
*
|
*
|
||||||
* @param map Pointer to the map to check.
|
* @param entity Pointer to the entity to probe from.
|
||||||
* @param pos The world position to check.
|
* @param range Distance, in grid units, to project the probe box.
|
||||||
* @return Pointer to the entity at the position, or NULL if none.
|
* @return Pointer to the closest overlapping entity, or NULL if none.
|
||||||
*/
|
*/
|
||||||
entity_t *entityGetAt(const worldpos_t pos);
|
entity_t *entityGetFacing(entity_t *entity, const float_t range);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the entity with the given global ID, if one is currently loaded.
|
* Gets the entity with the given global ID, if one is currently loaded.
|
||||||
@@ -164,4 +201,13 @@ void entityUpdateChunk(entity_t *entity);
|
|||||||
* @param entity Pointer to the entity to move.
|
* @param entity Pointer to the entity to move.
|
||||||
* @param pos The world position to place the entity at.
|
* @param pos The world position to place the entity at.
|
||||||
*/
|
*/
|
||||||
void entityPositionSet(entity_t *entity, const worldpos_t pos);
|
void entityPositionSet(entity_t *entity, const worldpos_t pos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives position and renderPosition from the entity's physics body, and
|
||||||
|
* refreshes chunk membership and map area triggers to match. Called once
|
||||||
|
* per entity per frame, after the physics step.
|
||||||
|
*
|
||||||
|
* @param entity Pointer to the entity to sync.
|
||||||
|
*/
|
||||||
|
void entitySyncFromPhysics(entity_t *entity);
|
||||||
@@ -48,4 +48,27 @@ void entityDirGetRelative(
|
|||||||
*outY = 0;
|
*outY = 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityDirToVec2(const entitydir_t dir, vec2 out) {
|
||||||
|
assertValidEntityDir(dir, "Invalid direction provided");
|
||||||
|
assertNotNull(out, "Output vector cannot be NULL");
|
||||||
|
|
||||||
|
worldunits_t relX, relY;
|
||||||
|
entityDirGetRelative(dir, &relX, &relY);
|
||||||
|
out[0] = (float_t)relX;
|
||||||
|
out[1] = (float_t)relY;
|
||||||
|
}
|
||||||
|
|
||||||
|
entitydir_t entityDirFromVec2(const vec2 direction) {
|
||||||
|
assertNotNull(direction, "Direction vector cannot be NULL");
|
||||||
|
assertTrue(
|
||||||
|
direction[0] != 0.0f || direction[1] != 0.0f,
|
||||||
|
"Direction vector cannot be zero"
|
||||||
|
);
|
||||||
|
|
||||||
|
if(fabsf(direction[0]) > fabsf(direction[1])) {
|
||||||
|
return direction[0] > 0.0f ? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
|
||||||
|
}
|
||||||
|
return direction[1] > 0.0f ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
|
||||||
}
|
}
|
||||||
@@ -44,11 +44,30 @@ entitydir_t entityDirGetOpposite(const entitydir_t dir);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the relative x and y offsets for a given direction.
|
* Gets the relative x and y offsets for a given direction.
|
||||||
*
|
*
|
||||||
* @param dir The direction to get offsets for.
|
* @param dir The direction to get offsets for.
|
||||||
* @param relX Pointer to store the relative x offset.
|
* @param relX Pointer to store the relative x offset.
|
||||||
* @param relY Pointer to store the relative y offset.
|
* @param relY Pointer to store the relative y offset.
|
||||||
*/
|
*/
|
||||||
void entityDirGetRelative(
|
void entityDirGetRelative(
|
||||||
const entitydir_t dir, worldunits_t *relX, worldunits_t *relY
|
const entitydir_t dir, worldunits_t *relX, worldunits_t *relY
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a cardinal direction to a unit 2D vector, using the same axis
|
||||||
|
* convention as entityDirGetRelative (north = +y, east = +x).
|
||||||
|
*
|
||||||
|
* @param dir The direction to convert.
|
||||||
|
* @param out Output unit vector for the direction.
|
||||||
|
*/
|
||||||
|
void entityDirToVec2(const entitydir_t dir, vec2 out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quantizes a 2D direction vector to the nearest cardinal direction, by
|
||||||
|
* comparing the magnitude of its x and y components.
|
||||||
|
*
|
||||||
|
* @param direction The direction vector to quantize. Must not be a zero
|
||||||
|
* vector.
|
||||||
|
* @return The nearest cardinal direction.
|
||||||
|
*/
|
||||||
|
entitydir_t entityDirFromVec2(const vec2 direction);
|
||||||
@@ -9,6 +9,10 @@
|
|||||||
#include "entitydir.h"
|
#include "entitydir.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
// Distance, in grid units, within which the entity is considered to have
|
||||||
|
// arrived at its path target.
|
||||||
|
#define ENTITY_PATH_ARRIVE_EPSILON 0.05f
|
||||||
|
|
||||||
bool_t entityPathStep(
|
bool_t entityPathStep(
|
||||||
entity_t *entity,
|
entity_t *entity,
|
||||||
const worldpos_t target,
|
const worldpos_t target,
|
||||||
@@ -24,36 +28,44 @@ bool_t entityPathStep(
|
|||||||
"Entity pointer is out of bounds"
|
"Entity pointer is out of bounds"
|
||||||
);
|
);
|
||||||
|
|
||||||
if(worldPosIsEqual(entity->position, target) && entityCanWalk(entity)) {
|
const float_t dx = (float_t)target.x - entity->body.position[0];
|
||||||
|
const float_t dy = (float_t)target.y - entity->body.position[1];
|
||||||
|
|
||||||
|
if(
|
||||||
|
fabsf(dx) <= ENTITY_PATH_ARRIVE_EPSILON &&
|
||||||
|
fabsf(dy) <= ENTITY_PATH_ARRIVE_EPSILON
|
||||||
|
) {
|
||||||
|
entityStop(entity);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if(!entityCanWalk(entity)) return false;
|
|
||||||
|
|
||||||
entitydir_t dir;
|
entitydir_t dir;
|
||||||
if(entity->position.x != target.x) {
|
bool_t horizontal;
|
||||||
dir = entity->position.x < target.x ? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
|
if(fabsf(dx) > fabsf(dy)) {
|
||||||
} else if(entity->position.y != target.y) {
|
dir = dx > 0.0f ? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
|
||||||
dir = entity->position.y < target.y ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
|
horizontal = true;
|
||||||
} else {
|
} else {
|
||||||
dir = entity->position.z < target.z ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
|
dir = dy > 0.0f ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
|
||||||
|
horizontal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Was the entity trying to walk on this axis last tick, but ended up not
|
||||||
|
// moving (velocity clamped by a collision)? If so, treat it as blocked.
|
||||||
|
if(walkAround) {
|
||||||
|
const uint8_t axis = horizontal ? 0 : 1;
|
||||||
|
const bool_t blockedLastTick =
|
||||||
|
entity->animation != ENTITY_ANIM_IDLE &&
|
||||||
|
fabsf(entity->body.velocity[axis]) <= ENTITY_MOVE_DEADZONE;
|
||||||
|
|
||||||
|
if(blockedLastTick) {
|
||||||
|
const entitydir_t alt = horizontal
|
||||||
|
? (dy >= 0.0f ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH)
|
||||||
|
: (dx >= 0.0f ? ENTITY_DIR_EAST : ENTITY_DIR_WEST);
|
||||||
|
entityWalk(entity, alt);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entityWalk(entity, dir);
|
entityWalk(entity, dir);
|
||||||
|
|
||||||
if(walkAround && entity->animation == ENTITY_ANIM_IDLE) {
|
|
||||||
// Primary direction blocked - try perpendicular axes.
|
|
||||||
entitydir_t alt, altOpp;
|
|
||||||
if(dir == ENTITY_DIR_EAST || dir == ENTITY_DIR_WEST) {
|
|
||||||
alt = entity->position.y <= target.y
|
|
||||||
? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
|
|
||||||
} else {
|
|
||||||
alt = entity->position.x <= target.x
|
|
||||||
? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
|
|
||||||
}
|
|
||||||
altOpp = entityDirGetOpposite(alt);
|
|
||||||
entityWalk(entity, alt);
|
|
||||||
if(entity->animation == ENTITY_ANIM_IDLE) entityWalk(entity, altOpp);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,15 +9,17 @@
|
|||||||
#include "entity.h"
|
#include "entity.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to walk the entity one tile toward target. Prefers resolving X
|
* Continuously walks the entity toward target's X/Y position (Z is left to
|
||||||
* first, then Y, then Z. Does nothing if the entity cannot currently walk.
|
* gravity/ground collision, not deliberate path-stepping). Prefers closing
|
||||||
* When walkAround is true and the preferred direction is blocked by an entity,
|
* whichever of X/Y currently differs most. Must be called every tick until
|
||||||
* tries perpendicular directions to navigate around it.
|
* it returns true. When walkAround is true and the last tick's movement on
|
||||||
|
* the current axis was blocked (its velocity on that axis reads as zero
|
||||||
|
* despite currently walking), tries a perpendicular direction instead.
|
||||||
*
|
*
|
||||||
* @param entity Pointer to the entity to move.
|
* @param entity Pointer to the entity to move.
|
||||||
* @param target The world position to move toward.
|
* @param target The world position to move toward (Z is ignored).
|
||||||
* @param walkAround Whether to try perpendicular directions when blocked.
|
* @param walkAround Whether to try a perpendicular direction when blocked.
|
||||||
* @returns true if the entity is already at target, false otherwise.
|
* @returns true once the entity is within arrival distance of target.
|
||||||
*/
|
*/
|
||||||
bool_t entityPathStep(
|
bool_t entityPathStep(
|
||||||
entity_t *entity,
|
entity_t *entity,
|
||||||
|
|||||||
@@ -29,15 +29,9 @@ void npcPathAddNode(npc_t *npc, const worldpos_t pos) {
|
|||||||
void npcPathMovement(entity_t *entity) {
|
void npcPathMovement(entity_t *entity) {
|
||||||
npcpath_t *path = &entity->data.npc.moveData.path;
|
npcpath_t *path = &entity->data.npc.moveData.path;
|
||||||
if(path->count == 0) return;
|
if(path->count == 0) return;
|
||||||
if(!entityCanWalk(entity)) return;
|
|
||||||
|
|
||||||
// Advance past any waypoints already reached (including the current one).
|
const worldpos_t target = path->positions[path->index];
|
||||||
worldpos_t *target = &path->positions[path->index];
|
if(entityPathStep(entity, target, false)) {
|
||||||
if(entityPathStep(entity, *target, false)) {
|
|
||||||
path->index = (path->index + 1) % path->count;
|
path->index = (path->index + 1) % path->count;
|
||||||
target = &path->positions[path->index];
|
|
||||||
// New target is the same tile - nothing to do this tick
|
|
||||||
if(worldPosIsEqual(entity->position, *target)) return;
|
|
||||||
entityPathStep(entity, *target, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,5 +23,5 @@ void npcRandomTurnMovement(entity_t *entity) {
|
|||||||
turn->timer -= TIME.delta;
|
turn->timer -= TIME.delta;
|
||||||
if(turn->timer > 0.0f) return;
|
if(turn->timer > 0.0f) return;
|
||||||
turn->timer = randomFloat(turn->frequencyMin, turn->frequencyMax);
|
turn->timer = randomFloat(turn->frequencyMin, turn->frequencyMax);
|
||||||
if(entityCanTurn(entity)) entityTurn(entity, (entitydir_t)(rand() % 4));
|
entityTurn(entity, (entitydir_t)(rand() % 4));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,14 +16,24 @@ void npcRandomWalkInit(npc_t *npc) {
|
|||||||
walk->frequencyMin = NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT;
|
walk->frequencyMin = NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT;
|
||||||
walk->frequencyMax = NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT;
|
walk->frequencyMax = NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT;
|
||||||
walk->timer = randomFloat(walk->frequencyMin, walk->frequencyMax);
|
walk->timer = randomFloat(walk->frequencyMin, walk->frequencyMax);
|
||||||
|
walk->moveDuration = 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
void npcRandomWalkMovement(entity_t *entity) {
|
void npcRandomWalkMovement(entity_t *entity) {
|
||||||
npcrandomwalk_t *walk = &entity->data.npc.moveData.randomWalk;
|
npcrandomwalk_t *walk = &entity->data.npc.moveData.randomWalk;
|
||||||
|
|
||||||
|
if(walk->moveDuration > 0.0f) {
|
||||||
|
walk->moveDuration -= TIME.delta;
|
||||||
|
entityWalk(entity, walk->direction);
|
||||||
|
if(walk->moveDuration <= 0.0f) entityStop(entity);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
walk->timer -= TIME.delta;
|
walk->timer -= TIME.delta;
|
||||||
if(walk->timer > 0.0f) return;
|
if(walk->timer > 0.0f) return;
|
||||||
walk->timer = randomFloat(walk->frequencyMin, walk->frequencyMax);
|
walk->timer = randomFloat(walk->frequencyMin, walk->frequencyMax);
|
||||||
if(entityCanWalk(entity)) entityWalk(entity, (entitydir_t)(rand() % 4));
|
walk->direction = (entitydir_t)(rand() % 4);
|
||||||
|
walk->moveDuration = NPC_RANDOM_WALK_MOVE_DURATION_DEFAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
void npcRandomTurnAndWalkInit(npc_t *npc) {
|
void npcRandomTurnAndWalkInit(npc_t *npc) {
|
||||||
@@ -32,21 +42,30 @@ void npcRandomTurnAndWalkInit(npc_t *npc) {
|
|||||||
tw->walk.frequencyMin = NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT;
|
tw->walk.frequencyMin = NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT;
|
||||||
tw->walk.frequencyMax = NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT;
|
tw->walk.frequencyMax = NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT;
|
||||||
tw->walk.timer = randomFloat(tw->walk.frequencyMin, tw->walk.frequencyMax);
|
tw->walk.timer = randomFloat(tw->walk.frequencyMin, tw->walk.frequencyMax);
|
||||||
|
tw->walk.moveDuration = 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
void npcRandomTurnAndWalkMovement(entity_t *entity) {
|
void npcRandomTurnAndWalkMovement(entity_t *entity) {
|
||||||
npcrandomturnandwalk_t *tw = &entity->data.npc.moveData.randomTurnAndWalk;
|
npcrandomturnandwalk_t *tw = &entity->data.npc.moveData.randomTurnAndWalk;
|
||||||
|
|
||||||
|
if(tw->walk.moveDuration > 0.0f) {
|
||||||
|
tw->walk.moveDuration -= TIME.delta;
|
||||||
|
entityWalk(entity, tw->walk.direction);
|
||||||
|
if(tw->walk.moveDuration <= 0.0f) entityStop(entity);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
tw->turn.timer -= TIME.delta;
|
tw->turn.timer -= TIME.delta;
|
||||||
if(tw->turn.timer <= 0.0f) {
|
if(tw->turn.timer <= 0.0f) {
|
||||||
tw->turn.timer = randomFloat(tw->turn.frequencyMin, tw->turn.frequencyMax);
|
tw->turn.timer = randomFloat(tw->turn.frequencyMin, tw->turn.frequencyMax);
|
||||||
if(entityCanTurn(entity)) entityTurn(entity, (entitydir_t)(rand() % 4));
|
entityTurn(entity, (entitydir_t)(rand() % 4));
|
||||||
}
|
}
|
||||||
|
|
||||||
tw->walk.timer -= TIME.delta;
|
tw->walk.timer -= TIME.delta;
|
||||||
if(tw->walk.timer <= 0.0f) {
|
if(tw->walk.timer <= 0.0f) {
|
||||||
tw->walk.timer = randomFloat(tw->walk.frequencyMin, tw->walk.frequencyMax);
|
tw->walk.timer = randomFloat(tw->walk.frequencyMin, tw->walk.frequencyMax);
|
||||||
if(entityCanWalk(entity)) entityWalk(entity, (entitydir_t)(rand() % 4));
|
tw->walk.direction = (entitydir_t)(rand() % 4);
|
||||||
|
tw->walk.moveDuration = NPC_RANDOM_WALK_MOVE_DURATION_DEFAULT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,15 +8,25 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "dusk.h"
|
#include "dusk.h"
|
||||||
#include "npcturn.h"
|
#include "npcturn.h"
|
||||||
|
#include "rpg/entity/entitydir.h"
|
||||||
|
|
||||||
/** Default min/max seconds between NPC random-walk ticks. */
|
/** Default min/max seconds between NPC random-walk ticks. */
|
||||||
#define NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT 2.0f
|
#define NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT 2.0f
|
||||||
#define NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT 5.0f
|
#define NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT 5.0f
|
||||||
|
|
||||||
|
/** How long a random-walk move lasts once triggered, roughly one tile's
|
||||||
|
* worth of travel at walking speed. */
|
||||||
|
#define NPC_RANDOM_WALK_MOVE_DURATION_DEFAULT 0.2f
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
float_t frequencyMin;
|
float_t frequencyMin;
|
||||||
float_t frequencyMax;
|
float_t frequencyMax;
|
||||||
float_t timer;
|
float_t timer;
|
||||||
|
|
||||||
|
// In-progress move state - while moveDuration > 0, movement keeps
|
||||||
|
// walking in direction every tick.
|
||||||
|
float_t moveDuration;
|
||||||
|
entitydir_t direction;
|
||||||
} npcrandomwalk_t;
|
} npcrandomwalk_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|||||||
@@ -25,8 +25,11 @@ bool_t playerCanInteract(entity_t *entity) {
|
|||||||
|
|
||||||
void playerInput(entity_t *entity) {
|
void playerInput(entity_t *entity) {
|
||||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||||
if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_PLAYER) return;
|
if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_PLAYER) {
|
||||||
|
entityStop(entity);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Toggle game menu on pause
|
// Toggle game menu on pause
|
||||||
if(uiGameMenuIsOpen() && inputPressed(INPUT_ACTION_PAUSE)) {
|
if(uiGameMenuIsOpen() && inputPressed(INPUT_ACTION_PAUSE)) {
|
||||||
uiGameMenuClose();
|
uiGameMenuClose();
|
||||||
@@ -38,63 +41,27 @@ void playerInput(entity_t *entity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Can player act?
|
// Can player act?
|
||||||
if(UI_FOCUS.count > 0) return;
|
if(UI_FOCUS.count > 0) {
|
||||||
|
entityStop(entity);
|
||||||
// Determine direction player may be trying to face/go.
|
return;
|
||||||
const playerinputdirmap_t *dirMap;
|
|
||||||
|
|
||||||
// First pass, we want to prefer a button relative to what the player is
|
|
||||||
// already facing, e.g. if they hold DOWN and start moving down, then tap
|
|
||||||
// right we want to prefer down over right.
|
|
||||||
|
|
||||||
// Determine current dirmap based on facing direction.
|
|
||||||
dirMap = PLAYER_INPUT_DIR_MAP;
|
|
||||||
do {
|
|
||||||
if(dirMap->direction != entity->direction) continue;
|
|
||||||
break;
|
|
||||||
} while((++dirMap)->action != 0xFF);
|
|
||||||
|
|
||||||
// Are we holding that button?
|
|
||||||
if(!inputIsDown(dirMap->action)) {
|
|
||||||
// No, so we need to check all directions.
|
|
||||||
dirMap = PLAYER_INPUT_DIR_MAP;
|
|
||||||
do {
|
|
||||||
if(!inputIsDown(dirMap->action)) continue;
|
|
||||||
break;
|
|
||||||
} while((++dirMap)->action != 0xFF);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trying to direct?
|
// Analog/free-angle movement vector from the 4 directional actions -
|
||||||
if(dirMap->action != 0xFF) {
|
// already normalized so diagonals aren't faster than cardinals, and
|
||||||
// Is the player attempting to turn? That is, is the player idle and has the
|
// preserves real analog magnitude on platforms that bind an actual
|
||||||
// turn lockout passed?
|
// gamepad stick to these actions.
|
||||||
if(entityCanTurn(entity)) {
|
vec2 moveDir;
|
||||||
if(entity->direction != dirMap->direction) {
|
inputAngle2D(
|
||||||
entityTurn(entity, dirMap->direction);
|
INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT,
|
||||||
return;
|
INPUT_ACTION_DOWN, INPUT_ACTION_UP,
|
||||||
}
|
moveDir
|
||||||
}
|
);
|
||||||
|
|
||||||
bool_t running = inputIsDown(INPUT_ACTION_CANCEL);
|
entityMove(entity, moveDir, inputIsDown(INPUT_ACTION_CANCEL));
|
||||||
if(running && entityCanRun(entity)) {
|
|
||||||
entityRun(entity, dirMap->direction);
|
|
||||||
} else if(entityCanWalk(entity)) {
|
|
||||||
entityWalk(entity, dirMap->direction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interaction
|
// Interaction
|
||||||
if(inputPressed(INPUT_ACTION_ACCEPT) && playerCanInteract(entity)) {
|
if(inputPressed(INPUT_ACTION_ACCEPT) && playerCanInteract(entity)) {
|
||||||
worldunit_t x, y, z;
|
entity_t *target = entityGetFacing(entity, ENTITY_INTERACT_RANGE);
|
||||||
{
|
|
||||||
worldunits_t relX, relY;
|
|
||||||
entityDirGetRelative(entity->direction, &relX, &relY);
|
|
||||||
x = entity->position.x + relX;
|
|
||||||
y = entity->position.y + relY;
|
|
||||||
z = entity->position.z;
|
|
||||||
}
|
|
||||||
|
|
||||||
entity_t *target = entityGetAt((worldpos_t){ x, y, z });
|
|
||||||
if(target == NULL) return;
|
if(target == NULL) return;
|
||||||
entityInteractWith(entity, target);
|
entityInteractWith(entity, target);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,20 +15,6 @@ typedef struct {
|
|||||||
void *nothing;
|
void *nothing;
|
||||||
} player_t;
|
} player_t;
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
inputaction_t action;
|
|
||||||
entitydir_t direction;
|
|
||||||
} playerinputdirmap_t;
|
|
||||||
|
|
||||||
static const playerinputdirmap_t PLAYER_INPUT_DIR_MAP[] = {
|
|
||||||
{ INPUT_ACTION_UP, ENTITY_DIR_NORTH },
|
|
||||||
{ INPUT_ACTION_DOWN, ENTITY_DIR_SOUTH },
|
|
||||||
{ INPUT_ACTION_LEFT, ENTITY_DIR_WEST },
|
|
||||||
{ INPUT_ACTION_RIGHT, ENTITY_DIR_EAST },
|
|
||||||
|
|
||||||
{ 0xFF, 0xFF }
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes a player entity.
|
* Initializes a player entity.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -168,6 +168,14 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Placeholder-fill with flat ground while the real data streams in
|
||||||
|
// asynchronously - entities standing on this chunk (subject to gravity)
|
||||||
|
// would otherwise fall through the zeroed/empty tiles until
|
||||||
|
// mapChunkLoaded() replaces them with the real data.
|
||||||
|
for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) {
|
||||||
|
chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND };
|
||||||
|
}
|
||||||
|
|
||||||
assertTrue(
|
assertTrue(
|
||||||
MAP.loadQueueCount < MAP_CHUNK_COUNT,
|
MAP.loadQueueCount < MAP_CHUNK_COUNT,
|
||||||
"Chunk load queue overflow"
|
"Chunk load queue overflow"
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ void mapAreaInit(
|
|||||||
area->triggerCount = 0;
|
area->triggerCount = 0;
|
||||||
|
|
||||||
memorySet(area->entities, 0, sizeof(area->entities));
|
memorySet(area->entities, 0, sizeof(area->entities));
|
||||||
|
memorySet(area->lastStepPosition, 0, sizeof(area->lastStepPosition));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t mapAreaIsInside(const maparea_t *area, const worldpos_t position) {
|
bool_t mapAreaIsInside(const maparea_t *area, const worldpos_t position) {
|
||||||
@@ -132,9 +133,13 @@ void mapAreaCheckEntity(entity_t *entity) {
|
|||||||
|
|
||||||
if(isInside && !wasInside) {
|
if(isInside && !wasInside) {
|
||||||
area->entities[entity->id] = 1;
|
area->entities[entity->id] = 1;
|
||||||
|
area->lastStepPosition[entity->id] = entity->position;
|
||||||
trigger = MAP_TRIGGER_ENTER;
|
trigger = MAP_TRIGGER_ENTER;
|
||||||
} else if(isInside && wasInside) {
|
} else if(isInside && wasInside) {
|
||||||
trigger = MAP_TRIGGER_STEP;
|
if(!worldPosIsEqual(entity->position, area->lastStepPosition[entity->id])) {
|
||||||
|
area->lastStepPosition[entity->id] = entity->position;
|
||||||
|
trigger = MAP_TRIGGER_STEP;
|
||||||
|
}
|
||||||
} else if(!isInside && wasInside) {
|
} else if(!isInside && wasInside) {
|
||||||
area->entities[entity->id] = 0;
|
area->entities[entity->id] = 0;
|
||||||
trigger = MAP_TRIGGER_EXIT;
|
trigger = MAP_TRIGGER_EXIT;
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ typedef struct maparea_s maparea_t;
|
|||||||
#define MAP_AREA_NOTIFY_NPC (1 << 1)
|
#define MAP_AREA_NOTIFY_NPC (1 << 1)
|
||||||
#define MAP_AREA_NOTIFY_ALL (MAP_AREA_NOTIFY_PLAYER | MAP_AREA_NOTIFY_NPC)
|
#define MAP_AREA_NOTIFY_ALL (MAP_AREA_NOTIFY_PLAYER | MAP_AREA_NOTIFY_NPC)
|
||||||
|
|
||||||
|
// Fires once per newly-entered tile while an entity remains inside the
|
||||||
|
// area (not once per frame) - see mapAreaCheckEntity.
|
||||||
#define MAP_TRIGGER_STEP (1 << 0)
|
#define MAP_TRIGGER_STEP (1 << 0)
|
||||||
#define MAP_TRIGGER_ENTER (1 << 1)
|
#define MAP_TRIGGER_ENTER (1 << 1)
|
||||||
#define MAP_TRIGGER_EXIT (1 << 2)
|
#define MAP_TRIGGER_EXIT (1 << 2)
|
||||||
@@ -39,6 +41,11 @@ typedef struct maparea_s {
|
|||||||
uint8_t trigger;
|
uint8_t trigger;
|
||||||
uint8_t entities[ENTITY_COUNT];
|
uint8_t entities[ENTITY_COUNT];
|
||||||
|
|
||||||
|
// The floored tile position ENTER or STEP last fired at, per entity.
|
||||||
|
// Used to fire STEP only once per newly-entered tile rather than
|
||||||
|
// every frame the entity remains inside the area.
|
||||||
|
worldpos_t lastStepPosition[ENTITY_COUNT];
|
||||||
|
|
||||||
// Incremented every time this area's callback is invoked. Lets other
|
// Incremented every time this area's callback is invoked. Lets other
|
||||||
// systems (e.g. cutscenes) detect "has this area fired since I last
|
// systems (e.g. cutscenes) detect "has this area fired since I last
|
||||||
// checked" without needing to be the callback themselves.
|
// checked" without needing to be the callback themselves.
|
||||||
@@ -143,8 +150,10 @@ void mapAreaRemove(const uint8_t id);
|
|||||||
* Checks every active map area against an entity's current position,
|
* Checks every active map area against an entity's current position,
|
||||||
* for areas whose notify flags include the entity's type. Invokes the
|
* for areas whose notify flags include the entity's type. Invokes the
|
||||||
* area's callback with MAP_TRIGGER_ENTER the frame the entity first
|
* area's callback with MAP_TRIGGER_ENTER the frame the entity first
|
||||||
* becomes inside, MAP_TRIGGER_STEP on every subsequent frame it remains
|
* becomes inside, MAP_TRIGGER_STEP the frame it moves into a different
|
||||||
* inside, and MAP_TRIGGER_EXIT the frame it leaves.
|
* tile while remaining inside, and MAP_TRIGGER_EXIT the frame it leaves.
|
||||||
|
* Safe (and expected) to call every frame - trigger conditions are
|
||||||
|
* edge-detected against the entity's tile position, not the frame rate.
|
||||||
*
|
*
|
||||||
* @param entity Pointer to the entity to check.
|
* @param entity Pointer to the entity to check.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ void physicsWorldInit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void physicsWorldStep(
|
void physicsWorldStep(
|
||||||
const physicsworld_t *world, physicsbody_t *body, const float_t dt
|
const physicsworld_t *world,
|
||||||
|
physicsbody_t *body,
|
||||||
|
const float_t dt,
|
||||||
|
physicsbody_t * const *others,
|
||||||
|
const uint32_t othersCount
|
||||||
) {
|
) {
|
||||||
assertNotNull(world, "world must not be null");
|
assertNotNull(world, "world must not be null");
|
||||||
assertNotNull(body, "body must not be null");
|
assertNotNull(body, "body must not be null");
|
||||||
@@ -41,13 +45,17 @@ void physicsWorldStep(
|
|||||||
body->velocity[2], -world->terminalVelocity, world->terminalVelocity
|
body->velocity[2], -world->terminalVelocity, world->terminalVelocity
|
||||||
);
|
);
|
||||||
|
|
||||||
physicsWorldResolveAxisX(world, body, dt);
|
physicsWorldResolveAxisX(world, body, dt, others, othersCount);
|
||||||
physicsWorldResolveAxisY(world, body, dt);
|
physicsWorldResolveAxisY(world, body, dt, others, othersCount);
|
||||||
physicsWorldResolveAxisZ(world, body, dt);
|
physicsWorldResolveAxisZ(world, body, dt, others, othersCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void physicsWorldResolveAxisX(
|
void physicsWorldResolveAxisX(
|
||||||
const physicsworld_t *world, physicsbody_t *body, const float_t dt
|
const physicsworld_t *world,
|
||||||
|
physicsbody_t *body,
|
||||||
|
const float_t dt,
|
||||||
|
physicsbody_t * const *others,
|
||||||
|
const uint32_t othersCount
|
||||||
) {
|
) {
|
||||||
assertNotNull(world, "world must not be null");
|
assertNotNull(world, "world must not be null");
|
||||||
assertNotNull(body, "body must not be null");
|
assertNotNull(body, "body must not be null");
|
||||||
@@ -83,6 +91,7 @@ void physicsWorldResolveAxisX(
|
|||||||
if(blocked) {
|
if(blocked) {
|
||||||
body->position[0] = (float_t)col - body->extents[0];
|
body->position[0] = (float_t)col - body->extents[0];
|
||||||
body->velocity[0] = 0.0f;
|
body->velocity[0] = 0.0f;
|
||||||
|
physicsWorldResolveBodyOverlap(body, 0, others, othersCount);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,16 +117,23 @@ void physicsWorldResolveAxisX(
|
|||||||
if(blocked) {
|
if(blocked) {
|
||||||
body->position[0] = (float_t)(col + 1);
|
body->position[0] = (float_t)(col + 1);
|
||||||
body->velocity[0] = 0.0f;
|
body->velocity[0] = 0.0f;
|
||||||
|
physicsWorldResolveBodyOverlap(body, 0, others, othersCount);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body->position[0] += vx * dt;
|
body->position[0] += vx * dt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
physicsWorldResolveBodyOverlap(body, 0, others, othersCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void physicsWorldResolveAxisY(
|
void physicsWorldResolveAxisY(
|
||||||
const physicsworld_t *world, physicsbody_t *body, const float_t dt
|
const physicsworld_t *world,
|
||||||
|
physicsbody_t *body,
|
||||||
|
const float_t dt,
|
||||||
|
physicsbody_t * const *others,
|
||||||
|
const uint32_t othersCount
|
||||||
) {
|
) {
|
||||||
assertNotNull(world, "world must not be null");
|
assertNotNull(world, "world must not be null");
|
||||||
assertNotNull(body, "body must not be null");
|
assertNotNull(body, "body must not be null");
|
||||||
@@ -153,6 +169,7 @@ void physicsWorldResolveAxisY(
|
|||||||
if(blocked) {
|
if(blocked) {
|
||||||
body->position[1] = (float_t)row - body->extents[1];
|
body->position[1] = (float_t)row - body->extents[1];
|
||||||
body->velocity[1] = 0.0f;
|
body->velocity[1] = 0.0f;
|
||||||
|
physicsWorldResolveBodyOverlap(body, 1, others, othersCount);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,16 +195,23 @@ void physicsWorldResolveAxisY(
|
|||||||
if(blocked) {
|
if(blocked) {
|
||||||
body->position[1] = (float_t)(row + 1);
|
body->position[1] = (float_t)(row + 1);
|
||||||
body->velocity[1] = 0.0f;
|
body->velocity[1] = 0.0f;
|
||||||
|
physicsWorldResolveBodyOverlap(body, 1, others, othersCount);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body->position[1] += vy * dt;
|
body->position[1] += vy * dt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
physicsWorldResolveBodyOverlap(body, 1, others, othersCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void physicsWorldResolveAxisZ(
|
void physicsWorldResolveAxisZ(
|
||||||
const physicsworld_t *world, physicsbody_t *body, const float_t dt
|
const physicsworld_t *world,
|
||||||
|
physicsbody_t *body,
|
||||||
|
const float_t dt,
|
||||||
|
physicsbody_t * const *others,
|
||||||
|
const uint32_t othersCount
|
||||||
) {
|
) {
|
||||||
assertNotNull(world, "world must not be null");
|
assertNotNull(world, "world must not be null");
|
||||||
assertNotNull(body, "body must not be null");
|
assertNotNull(body, "body must not be null");
|
||||||
@@ -244,4 +268,40 @@ void physicsWorldResolveAxisZ(
|
|||||||
body->position[2] += vz * dt;
|
body->position[2] += vz * dt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
physicsWorldResolveBodyOverlap(body, 2, others, othersCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
void physicsWorldResolveBodyOverlap(
|
||||||
|
physicsbody_t *body,
|
||||||
|
const uint8_t axis,
|
||||||
|
physicsbody_t * const *others,
|
||||||
|
const uint32_t othersCount
|
||||||
|
) {
|
||||||
|
assertNotNull(body, "body must not be null");
|
||||||
|
assertTrue(axis < 3, "axis must be 0, 1 or 2");
|
||||||
|
|
||||||
|
const float_t v = body->velocity[axis];
|
||||||
|
if(v == 0.0f) return;
|
||||||
|
|
||||||
|
for(uint32_t i = 0; i < othersCount; i++) {
|
||||||
|
physicsbody_t *other = others[i];
|
||||||
|
if(other == NULL || other == body) continue;
|
||||||
|
|
||||||
|
vec3 min, max;
|
||||||
|
physicsBodyGetBounds(body, min, max);
|
||||||
|
vec3 oMin, oMax;
|
||||||
|
physicsBodyGetBounds(other, oMin, oMax);
|
||||||
|
|
||||||
|
if(min[0] >= oMax[0] || max[0] <= oMin[0]) continue;
|
||||||
|
if(min[1] >= oMax[1] || max[1] <= oMin[1]) continue;
|
||||||
|
if(min[2] >= oMax[2] || max[2] <= oMin[2]) continue;
|
||||||
|
|
||||||
|
if(v > 0.0f) {
|
||||||
|
body->position[axis] -= max[axis] - oMin[axis];
|
||||||
|
} else {
|
||||||
|
body->position[axis] += oMax[axis] - min[axis];
|
||||||
|
}
|
||||||
|
body->velocity[axis] = 0.0f;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ void physicsWorldInit(
|
|||||||
* Advances a body by one timestep: applies gravity, integrates velocity
|
* Advances a body by one timestep: applies gravity, integrates velocity
|
||||||
* into position (semi-implicit Euler), and resolves collisions against
|
* into position (semi-implicit Euler), and resolves collisions against
|
||||||
* the tile map one axis at a time (X, then Y, then Z), clamping position
|
* the tile map one axis at a time (X, then Y, then Z), clamping position
|
||||||
* and zeroing velocity on any axis that hits a tile boundary.
|
* and zeroing velocity on any axis that hits a tile boundary. After each
|
||||||
|
* axis's tile resolution, also resolves overlap against any given other
|
||||||
|
* bodies (see physicsWorldResolveBodyOverlap) - so another body is
|
||||||
|
* treated as solid too, not just the tile map.
|
||||||
*
|
*
|
||||||
* Known limitations, deliberately out of scope for this very basic pass:
|
* Known limitations, deliberately out of scope for this very basic pass:
|
||||||
* - No ramp/slope support - ramp tiles are treated as flat walkable
|
* - No ramp/slope support - ramp tiles are treated as flat walkable
|
||||||
@@ -52,28 +55,44 @@ void physicsWorldInit(
|
|||||||
* - A single step can tunnel through an intervening solid Z layer if
|
* - A single step can tunnel through an intervening solid Z layer if
|
||||||
* velocity.z * dt exceeds one grid unit; terminalVelocity bounds this
|
* velocity.z * dt exceeds one grid unit; terminalVelocity bounds this
|
||||||
* but does not eliminate it for very small/thin floors.
|
* but does not eliminate it for very small/thin floors.
|
||||||
|
* - Body-vs-body resolution only reacts to the axis currently being
|
||||||
|
* resolved - two bodies already overlapping on every axis, with no
|
||||||
|
* velocity on any of them, are never proactively separated.
|
||||||
*
|
*
|
||||||
* @param world Physics world configuration.
|
* @param world Physics world configuration.
|
||||||
* @param body The body to step. Its position/velocity/grounded fields are
|
* @param body The body to step. Its position/velocity/grounded fields are
|
||||||
* updated in place.
|
* updated in place.
|
||||||
* @param dt Timestep, in seconds (use DUSK_TIME_STEP for the fixed step).
|
* @param dt Timestep, in seconds (use DUSK_TIME_STEP for the fixed step).
|
||||||
|
* @param others Array of pointers to other bodies to treat as solid.
|
||||||
|
* Pass NULL (with othersCount 0) to resolve against the tile map only.
|
||||||
|
* @param othersCount Number of entries in others.
|
||||||
*/
|
*/
|
||||||
void physicsWorldStep(
|
void physicsWorldStep(
|
||||||
const physicsworld_t *world, physicsbody_t *body, const float_t dt
|
const physicsworld_t *world,
|
||||||
|
physicsbody_t *body,
|
||||||
|
const float_t dt,
|
||||||
|
physicsbody_t * const *others,
|
||||||
|
const uint32_t othersCount
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the body's movement along the X axis for this step, clamping
|
* Resolves the body's movement along the X axis for this step, clamping
|
||||||
* position and zeroing velocity.x if a non-walkable column blocks the
|
* position and zeroing velocity.x if a non-walkable column, or another
|
||||||
* move. Declared publicly as an internal step helper, not a stable public
|
* body, blocks the move. Declared publicly as an internal step helper,
|
||||||
* API on its own.
|
* not a stable public API on its own.
|
||||||
*
|
*
|
||||||
* @param world Physics world configuration.
|
* @param world Physics world configuration.
|
||||||
* @param body The body to resolve.
|
* @param body The body to resolve.
|
||||||
* @param dt Timestep, in seconds.
|
* @param dt Timestep, in seconds.
|
||||||
|
* @param others Array of pointers to other bodies to treat as solid.
|
||||||
|
* @param othersCount Number of entries in others.
|
||||||
*/
|
*/
|
||||||
void physicsWorldResolveAxisX(
|
void physicsWorldResolveAxisX(
|
||||||
const physicsworld_t *world, physicsbody_t *body, const float_t dt
|
const physicsworld_t *world,
|
||||||
|
physicsbody_t *body,
|
||||||
|
const float_t dt,
|
||||||
|
physicsbody_t * const *others,
|
||||||
|
const uint32_t othersCount
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,9 +102,15 @@ void physicsWorldResolveAxisX(
|
|||||||
* @param world Physics world configuration.
|
* @param world Physics world configuration.
|
||||||
* @param body The body to resolve.
|
* @param body The body to resolve.
|
||||||
* @param dt Timestep, in seconds.
|
* @param dt Timestep, in seconds.
|
||||||
|
* @param others Array of pointers to other bodies to treat as solid.
|
||||||
|
* @param othersCount Number of entries in others.
|
||||||
*/
|
*/
|
||||||
void physicsWorldResolveAxisY(
|
void physicsWorldResolveAxisY(
|
||||||
const physicsworld_t *world, physicsbody_t *body, const float_t dt
|
const physicsworld_t *world,
|
||||||
|
physicsbody_t *body,
|
||||||
|
const float_t dt,
|
||||||
|
physicsbody_t * const *others,
|
||||||
|
const uint32_t othersCount
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,7 +121,34 @@ void physicsWorldResolveAxisY(
|
|||||||
* @param world Physics world configuration.
|
* @param world Physics world configuration.
|
||||||
* @param body The body to resolve.
|
* @param body The body to resolve.
|
||||||
* @param dt Timestep, in seconds.
|
* @param dt Timestep, in seconds.
|
||||||
|
* @param others Array of pointers to other bodies to treat as solid.
|
||||||
|
* @param othersCount Number of entries in others.
|
||||||
*/
|
*/
|
||||||
void physicsWorldResolveAxisZ(
|
void physicsWorldResolveAxisZ(
|
||||||
const physicsworld_t *world, physicsbody_t *body, const float_t dt
|
const physicsworld_t *world,
|
||||||
|
physicsbody_t *body,
|
||||||
|
const float_t dt,
|
||||||
|
physicsbody_t * const *others,
|
||||||
|
const uint32_t othersCount
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the body's overlap against a set of other bodies along a
|
||||||
|
* single axis: if the body's bounds fully overlap (on all 3 axes)
|
||||||
|
* another body's bounds, pushes the body back along the given axis by
|
||||||
|
* the overlap depth on that axis, in the opposite direction of its
|
||||||
|
* current velocity on that axis, and zeroes that velocity component.
|
||||||
|
* Does nothing if the body has no velocity on that axis, since there is
|
||||||
|
* then no direction to know which way to push it out.
|
||||||
|
*
|
||||||
|
* @param body The body to resolve.
|
||||||
|
* @param axis The axis to resolve overlap on (0 = x, 1 = y, 2 = z).
|
||||||
|
* @param others Array of pointers to other bodies to check against.
|
||||||
|
* @param othersCount Number of entries in others.
|
||||||
|
*/
|
||||||
|
void physicsWorldResolveBodyOverlap(
|
||||||
|
physicsbody_t *body,
|
||||||
|
const uint8_t axis,
|
||||||
|
physicsbody_t * const *others,
|
||||||
|
const uint32_t othersCount
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ errorret_t rpgInit(void) {
|
|||||||
memoryZero(ENTITIES, sizeof(ENTITIES));
|
memoryZero(ENTITIES, sizeof(ENTITIES));
|
||||||
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
|
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
|
||||||
|
|
||||||
|
physicsWorldInit(
|
||||||
|
&ENTITY_PHYSICS_WORLD,
|
||||||
|
PHYSICS_WORLD_GRAVITY_DEFAULT,
|
||||||
|
PHYSICS_WORLD_TERMINAL_VELOCITY_DEFAULT
|
||||||
|
);
|
||||||
|
|
||||||
backpackInit();
|
backpackInit();
|
||||||
cutsceneSystemInit();
|
cutsceneSystemInit();
|
||||||
|
|
||||||
|
|||||||
@@ -44,5 +44,21 @@ errorret_t uiPlayerPosDraw() {
|
|||||||
errorChain(textDraw(
|
errorChain(textDraw(
|
||||||
(float_t)SCREEN.scanX, y, text, COLOR_GREEN, &FONT_DEFAULT
|
(float_t)SCREEN.scanX, y, text, COLOR_GREEN, &FONT_DEFAULT
|
||||||
));
|
));
|
||||||
|
|
||||||
|
char_t velocityText[64];
|
||||||
|
snprintf(
|
||||||
|
velocityText,
|
||||||
|
sizeof(velocityText),
|
||||||
|
"vel %.2f,%.2f,%.2f",
|
||||||
|
player->body.velocity[0],
|
||||||
|
player->body.velocity[1],
|
||||||
|
player->body.velocity[2]
|
||||||
|
);
|
||||||
|
|
||||||
|
float_t velocityY = y + (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||||
|
errorChain(textDraw(
|
||||||
|
(float_t)SCREEN.scanX, velocityY, velocityText, COLOR_GREEN, &FONT_DEFAULT
|
||||||
|
));
|
||||||
|
|
||||||
return spriteBatchFlush();
|
return spriteBatchFlush();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ dusktest(test_rpg.c)
|
|||||||
|
|
||||||
# Subdirs
|
# Subdirs
|
||||||
add_subdirectory(overworld)
|
add_subdirectory(overworld)
|
||||||
add_subdirectory(physics)
|
add_subdirectory(physics)
|
||||||
|
add_subdirectory(entity)
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -6,5 +6,6 @@
|
|||||||
include(dusktest)
|
include(dusktest)
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
|
dusktest(test_maparea.c)
|
||||||
|
|
||||||
# Subdirs
|
# Subdirs
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -61,7 +61,7 @@ static void test_physicsWorldStepStraightLineNoObstacles(void **state) {
|
|||||||
|
|
||||||
const uint32_t steps = 10;
|
const uint32_t steps = 10;
|
||||||
for(uint32_t i = 0; i < steps; i++) {
|
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);
|
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;
|
body.velocity[0] = 5.0f;
|
||||||
|
|
||||||
for(uint32_t i = 0; i < 60; i++) {
|
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);
|
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.
|
// Further steps must not push it past the wall.
|
||||||
for(uint32_t i = 0; i < 5; i++) {
|
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);
|
assert_float_equal(body.position[0], 2.0f, 0.0001f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ static void test_physicsWorldStepGravitySettlesOnFloor(void **state) {
|
|||||||
physicsBodyInit(&body, position, extents);
|
physicsBodyInit(&body, position, extents);
|
||||||
|
|
||||||
for(uint32_t i = 0; i < 200; i++) {
|
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);
|
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++) {
|
for(uint32_t i = 0; i < 50; i++) {
|
||||||
const float_t previousZ = body.position[2];
|
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_true(body.position[2] < previousZ);
|
||||||
assert_false(body.grounded);
|
assert_false(body.grounded);
|
||||||
}
|
}
|
||||||
@@ -163,7 +163,7 @@ static void test_physicsWorldStepTerminalVelocityClamp(void **state) {
|
|||||||
physicsBodyInit(&body, position, extents);
|
physicsBodyInit(&body, position, extents);
|
||||||
|
|
||||||
for(uint32_t i = 0; i < 300; i++) {
|
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);
|
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;
|
body.velocity[2] = 1.0f;
|
||||||
|
|
||||||
for(uint32_t i = 0; i < 200; i++) {
|
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);
|
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;
|
body.velocity[0] = 5.0f;
|
||||||
|
|
||||||
for(uint32_t i = 0; i < 60; i++) {
|
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);
|
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;
|
body.velocity[0] = 5.0f;
|
||||||
|
|
||||||
for(uint32_t i = 0; i < 60; i++) {
|
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);
|
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);
|
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) {
|
int main(void) {
|
||||||
const struct CMUnitTest tests[] = {
|
const struct CMUnitTest tests[] = {
|
||||||
cmocka_unit_test(test_physicsWorldStepStraightLineNoObstacles),
|
cmocka_unit_test(test_physicsWorldStepStraightLineNoObstacles),
|
||||||
@@ -262,6 +347,9 @@ int main(void) {
|
|||||||
cmocka_unit_test(test_physicsWorldStepBlockedByCeiling),
|
cmocka_unit_test(test_physicsWorldStepBlockedByCeiling),
|
||||||
cmocka_unit_test(test_physicsWorldStepMultiColumnFootprint),
|
cmocka_unit_test(test_physicsWorldStepMultiColumnFootprint),
|
||||||
cmocka_unit_test(test_physicsWorldStepRampTreatedAsFlatGround),
|
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);
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||||
|
|||||||
Reference in New Issue
Block a user