Path finding (first pass)

This commit is contained in:
2026-07-02 14:08:59 -05:00
parent a8271e01bd
commit 8d5c0c7cad
15 changed files with 199 additions and 37 deletions
+29
View File
@@ -41,6 +41,32 @@ typedef struct cutscene_s {
#define CUTSCENE_CALLBACK(CALLBACK) \ #define CUTSCENE_CALLBACK(CALLBACK) \
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = CALLBACK } { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = CALLBACK }
// Single-destination walk: entity walks directly to (X, Y, Z).
// walkAround=true: tries perpendicular directions when blocked by an entity.
#define CUTSCENE_ENTITY_WALK_TO(ENTITY_INDEX, X, Y, Z) \
{ \
.type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO, \
.entityWalkTo = { \
.entityIndex = ENTITY_INDEX, \
.positions = (const worldpos_t[]){ { X, Y, Z } }, \
.count = 1, \
.walkAround = true \
} \
}
// Multi-waypoint walk: generates a named positions array then references it.
#define CUTSCENE_ENTITY_WALK_PATH(NAME, ENTITY_INDEX, ...) \
static const worldpos_t CUTSCENE_##NAME##_POSITIONS[] = { __VA_ARGS__ }; \
static const cutsceneitem_t CUTSCENE_##NAME = { \
.type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO, \
.entityWalkTo = { \
.entityIndex = ENTITY_INDEX, \
.positions = CUTSCENE_##NAME##_POSITIONS, \
.count = sizeof(CUTSCENE_##NAME##_POSITIONS) / sizeof(worldpos_t), \
.walkAround = true \
} \
}
#define CUTSCENE_ENTITY_TELEPORT(ENTITY_INDEX, X, Y, Z) \ #define CUTSCENE_ENTITY_TELEPORT(ENTITY_INDEX, X, Y, Z) \
{ \ { \
.type = CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT, \ .type = CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT, \
@@ -64,3 +90,6 @@ typedef struct cutscene_s {
#define CUTSCENE_FADE_FROM_WHITE(DURATION) \ #define CUTSCENE_FADE_FROM_WHITE(DURATION) \
CUTSCENE_FADE(COLOR_WHITE, COLOR_TRANSPARENT_WHITE, DURATION, EASING_LINEAR) CUTSCENE_FADE(COLOR_WHITE, COLOR_TRANSPARENT_WHITE, DURATION, EASING_LINEAR)
#define CUTSCENE_SET_PAUSE(FLAGS) \
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
+8 -4
View File
@@ -10,11 +10,15 @@
typedef uint8_t cutscenepause_t; typedef uint8_t cutscenepause_t;
#define CUTSCENE_PAUSE_NONE ((cutscenepause_t)0)
#define CUTSCENE_PAUSE_NPC ((cutscenepause_t)(1 << 0)) #define CUTSCENE_PAUSE_NPC ((cutscenepause_t)(1 << 0))
#define CUTSCENE_PAUSE_PLAYER ((cutscenepause_t)(1 << 1)) #define CUTSCENE_PAUSE_PLAYER ((cutscenepause_t)(1 << 1))
#define CUTSCENE_PAUSE_WORLD ((cutscenepause_t)(1 << 2)) #define CUTSCENE_PAUSE_WORLD ((cutscenepause_t)(1 << 2))
#define CUTSCENE_PAUSE_DEFAULT \ #define CUTSCENE_PAUSE_DEFAULT ((cutscenepause_t)( \
((cutscenepause_t)(CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER)) CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER \
#define CUTSCENE_PAUSE_ALL \ ))
((cutscenepause_t)(CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER | CUTSCENE_PAUSE_WORLD))
#define CUTSCENE_PAUSE_ALL ((cutscenepause_t)( \
CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER | CUTSCENE_PAUSE_WORLD \
))
+3
View File
@@ -16,6 +16,7 @@ void cutsceneSystemInit() {
void cutsceneSystemStartCutscene(const cutscene_t *cutscene) { void cutsceneSystemStartCutscene(const cutscene_t *cutscene) {
CUTSCENE_SYSTEM.scene = cutscene; CUTSCENE_SYSTEM.scene = cutscene;
CUTSCENE_SYSTEM.pause = cutscene->pause;
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so start wraps. CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so start wraps.
cutsceneSystemNext(); cutsceneSystemNext();
} }
@@ -38,6 +39,7 @@ void cutsceneSystemNext() {
) { ) {
CUTSCENE_SYSTEM.scene = NULL; CUTSCENE_SYSTEM.scene = NULL;
CUTSCENE_SYSTEM.currentItem = 0xFF; CUTSCENE_SYSTEM.currentItem = 0xFF;
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
return; return;
} }
@@ -56,4 +58,5 @@ const cutsceneitem_t * cutsceneSystemGetCurrentItem() {
void cutsceneSystemDispose() { void cutsceneSystemDispose() {
CUTSCENE_SYSTEM.scene = NULL; CUTSCENE_SYSTEM.scene = NULL;
CUTSCENE_SYSTEM.currentItem = 0xFF; CUTSCENE_SYSTEM.currentItem = 0xFF;
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
} }
+1
View File
@@ -11,6 +11,7 @@
typedef struct { typedef struct {
const cutscene_t *scene; const cutscene_t *scene;
uint8_t currentItem; uint8_t currentItem;
cutscenepause_t pause;
// Data (used by the current item). // Data (used by the current item).
cutsceneitemdata_t data; cutsceneitemdata_t data;
+26
View File
@@ -11,6 +11,7 @@
#include "time/time.h" #include "time/time.h"
#include "ui/rpg/uitextboxmain.h" #include "ui/rpg/uitextboxmain.h"
#include "rpg/entity/entity.h" #include "rpg/entity/entity.h"
#include "rpg/entity/entitypathstep.h"
void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) { void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
switch(item->type) { switch(item->type) {
@@ -37,6 +38,10 @@ void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
); );
break; break;
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO:
data->entityWalkTo.currentIndex = 0;
break;
case CUTSCENE_ITEM_TYPE_FADE: case CUTSCENE_ITEM_TYPE_FADE:
uiFullboxTransition( uiFullboxTransition(
&UI_FULLBOX_OVER, &UI_FULLBOX_OVER,
@@ -47,6 +52,10 @@ void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
); );
break; break;
case CUTSCENE_ITEM_TYPE_SET_PAUSE:
CUTSCENE_SYSTEM.pause = item->setPause;
break;
default: default:
break; break;
} }
@@ -68,6 +77,19 @@ void cutsceneItemUpdate(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
cutsceneSystemNext(); cutsceneSystemNext();
break; break;
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO: {
uint8_t i = data->entityWalkTo.currentIndex;
entity_t *e = &ENTITIES[item->entityWalkTo.entityIndex];
if(!entityPathStep(e, item->entityWalkTo.positions[i], item->entityWalkTo.walkAround)) return;
i++;
if(i < item->entityWalkTo.count) {
data->entityWalkTo.currentIndex = i;
return;
}
cutsceneSystemNext();
break;
}
case CUTSCENE_ITEM_TYPE_FADE: case CUTSCENE_ITEM_TYPE_FADE:
if( if(
UI_FULLBOX_OVER.duration > 0.0f && UI_FULLBOX_OVER.duration > 0.0f &&
@@ -76,6 +98,10 @@ void cutsceneItemUpdate(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
cutsceneSystemNext(); cutsceneSystemNext();
break; break;
case CUTSCENE_ITEM_TYPE_SET_PAUSE:
cutsceneSystemNext();
break;
default: default:
break; break;
} }
+16 -1
View File
@@ -10,6 +10,7 @@
#include "cutscenecallback.h" #include "cutscenecallback.h"
#include "cutscenetext.h" #include "cutscenetext.h"
#include "cutscenefade.h" #include "cutscenefade.h"
#include "rpg/cutscene/cutscenepause.h"
#include "rpg/overworld/worldpos.h" #include "rpg/overworld/worldpos.h"
typedef struct cutscene_s cutscene_t; typedef struct cutscene_s cutscene_t;
@@ -21,7 +22,9 @@ typedef enum {
CUTSCENE_ITEM_TYPE_WAIT, CUTSCENE_ITEM_TYPE_WAIT,
CUTSCENE_ITEM_TYPE_CUTSCENE, CUTSCENE_ITEM_TYPE_CUTSCENE,
CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT, CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT,
CUTSCENE_ITEM_TYPE_FADE CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO,
CUTSCENE_ITEM_TYPE_FADE,
CUTSCENE_ITEM_TYPE_SET_PAUSE
} cutsceneitemtype_t; } cutsceneitemtype_t;
typedef struct cutsceneitem_s { typedef struct cutsceneitem_s {
@@ -37,12 +40,24 @@ typedef struct cutsceneitem_s {
uint8_t entityIndex; uint8_t entityIndex;
worldpos_t target; worldpos_t target;
} entityTeleport; } entityTeleport;
struct {
uint8_t entityIndex;
const worldpos_t *positions;
uint8_t count;
bool_t walkAround;
} entityWalkTo;
cutscenefade_t fade; cutscenefade_t fade;
cutscenepause_t setPause;
}; };
} cutsceneitem_t; } cutsceneitem_t;
typedef struct {
uint8_t currentIndex;
} cutsceneentitywalktodata_t;
typedef union { typedef union {
cutscenewaitdata_t wait; cutscenewaitdata_t wait;
cutsceneentitywalktodata_t entityWalkTo;
} cutsceneitemdata_t; } cutsceneitemdata_t;
/** /**
+4 -4
View File
@@ -15,8 +15,8 @@ CUTSCENE(TEST_ONE, DEFAULT,
CUTSCENE(TEST_TWO, DEFAULT, CUTSCENE(TEST_TWO, DEFAULT,
CUTSCENE_TEXT("Test Two."), CUTSCENE_TEXT("Test Two."),
CUTSCENE_FADE_TO_BLACK(1.0f), CUTSCENE_SET_PAUSE(CUTSCENE_PAUSE_NONE),
CUTSCENE_ENTITY_TELEPORT(0, 20, 0, 0), CUTSCENE_WAIT(5.0f),
CUTSCENE_WAIT(1.0f), CUTSCENE_TEXT("Hello world")
CUTSCENE_FADE_FROM_BLACK(1.0f), // CUTSCENE_ENTITY_WALK_TO(0, 0, 0, 0),
); );
+1
View File
@@ -8,6 +8,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
entity.c entity.c
entitydir.c entitydir.c
entitypathstep.c
player.c player.c
) )
+57
View File
@@ -0,0 +1,57 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entitypathstep.h"
#include "entitydir.h"
#include "assert/assert.h"
bool_t entityPathStep(
entity_t *entity,
const worldpos_t target,
bool_t walkAround
) {
assertNotNull(entity, "Entity must not be NULL");
assertTrue(
entity->type != ENTITY_TYPE_NULL,
"Cannot path step a NULL entity type"
);
assertTrue(
entity >= ENTITIES && entity < ENTITIES + ENTITY_COUNT,
"Entity pointer is out of bounds"
);
if(worldPosIsEqual(entity->position, target)) return true;
if(!entityCanWalk(entity)) return false;
entitydir_t dir;
if(entity->position.x != target.x) {
dir = entity->position.x < target.x ? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
} else if(entity->position.y != target.y) {
dir = entity->position.y < target.y ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
} else {
dir = entity->position.z < target.z ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
}
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;
}
+26
View File
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entity.h"
/**
* Attempts to walk the entity one tile toward target. Prefers resolving X
* first, then Y, then Z. Does nothing if the entity cannot currently walk.
* When walkAround is true and the preferred direction is blocked by an entity,
* tries perpendicular directions to navigate around it.
*
* @param entity Pointer to the entity to move.
* @param target The world position to move toward.
* @param walkAround Whether to try perpendicular directions when blocked.
* @returns true if the entity is already at target, false otherwise.
*/
bool_t entityPathStep(
entity_t *entity,
const worldpos_t target,
bool_t walkAround
);
+1 -4
View File
@@ -50,10 +50,7 @@ void npcSetMoveType(entity_t *entity, const npcmovetype_t moveType) {
void npcMovement(entity_t *entity) { void npcMovement(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL"); assertNotNull(entity, "Entity pointer cannot be NULL");
if( if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_NPC) return;
CUTSCENE_SYSTEM.scene != NULL &&
(CUTSCENE_SYSTEM.scene->pause & CUTSCENE_PAUSE_NPC)
) return;
npc_t *npc = &entity->data.npc; npc_t *npc = &entity->data.npc;
if(npc->interactState != NPC_INTERACT_STATE_NONE) return; if(npc->interactState != NPC_INTERACT_STATE_NONE) return;
+13 -14
View File
@@ -7,7 +7,9 @@
#include "npc.h" #include "npc.h"
#include "rpg/entity/entity.h" #include "rpg/entity/entity.h"
#include "rpg/entity/entitypathstep.h"
#include "rpg/overworld/worldpos.h" #include "rpg/overworld/worldpos.h"
#include "assert/assert.h"
void npcPathInit(npc_t *npc) { void npcPathInit(npc_t *npc) {
npcpath_t *path = &npc->moveData.path; npcpath_t *path = &npc->moveData.path;
@@ -15,6 +17,15 @@ void npcPathInit(npc_t *npc) {
path->index = 0; path->index = 0;
} }
void npcPathAddNode(npc_t *npc, const worldpos_t pos) {
assertNotNull(npc, "NPC must not be NULL");
assertTrue(
npc->moveData.path.count < NPC_PATH_COUNT_MAX,
"NPC path is full"
);
npc->moveData.path.positions[npc->moveData.path.count++] = 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;
@@ -22,23 +33,11 @@ void npcPathMovement(entity_t *entity) {
// Advance past any waypoints already reached (including the current one). // Advance past any waypoints already reached (including the current one).
worldpos_t *target = &path->positions[path->index]; worldpos_t *target = &path->positions[path->index];
if(worldPosIsEqual(entity->position, *target)) { if(entityPathStep(entity, *target, false)) {
path->index = (path->index + 1) % path->count; path->index = (path->index + 1) % path->count;
target = &path->positions[path->index]; target = &path->positions[path->index];
// New target is the same tile - nothing to do this tick // New target is the same tile - nothing to do this tick
if(worldPosIsEqual(entity->position, *target)) return; if(worldPosIsEqual(entity->position, *target)) return;
entityPathStep(entity, *target, false);
} }
entitydir_t dir;
worldpos_t pos = entity->position;
if(pos.x != target->x) {
dir = pos.x < target->x ? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
} else if(pos.y != target->y) {
dir = pos.y < target->y ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
} else {
// x and y match but z differs - step to correct z via ramp logic
dir = pos.z < target->z ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
}
entityWalk(entity, dir);
} }
+8
View File
@@ -28,6 +28,14 @@ typedef struct {
*/ */
void npcPathInit(npc_t *npc); void npcPathInit(npc_t *npc);
/**
* Appends a waypoint to the NPC's path. Has no effect if the path is full.
*
* @param npc Pointer to the NPC.
* @param pos The world position to append.
*/
void npcPathAddNode(npc_t *npc, const worldpos_t pos);
/** /**
* Movement tick for an NPC following a path. * Movement tick for an NPC following a path.
* *
+1 -4
View File
@@ -24,10 +24,7 @@ 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( if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_PLAYER) return;
CUTSCENE_SYSTEM.scene != NULL &&
(CUTSCENE_SYSTEM.scene->pause & CUTSCENE_PAUSE_PLAYER)
) return;
// Toggle settings on pause // Toggle settings on pause
if(uiSettingsIsOpen() && inputPressed(INPUT_ACTION_PAUSE)) { if(uiSettingsIsOpen() && inputPressed(INPUT_ACTION_PAUSE)) {
+5 -6
View File
@@ -7,6 +7,7 @@
#include "rpg.h" #include "rpg.h"
#include "entity/entity.h" #include "entity/entity.h"
#include "rpg/entity/npc/npcpath.h"
#include "rpg/overworld/map.h" #include "rpg/overworld/map.h"
#include "rpg/cutscene/cutscenesystem.h" #include "rpg/cutscene/cutscenesystem.h"
#include "rpg/cutscene/scene/testcutscene.h" #include "rpg/cutscene/scene/testcutscene.h"
@@ -57,12 +58,10 @@ errorret_t rpgInit(void) {
if(ci != -1) entitySetChunk(npc, (uint8_t)ci); if(ci != -1) entitySetChunk(npc, (uint8_t)ci);
} }
// npcpath_t *path = &npc->data.npc.moveData.path; npcPathAddNode(&npc->data.npc, (worldpos_t){ 4, 4, 0 });
// path->positions[0] = (worldpos_t){ 3, 3, 0 }; npcPathAddNode(&npc->data.npc, (worldpos_t){ 10, 10, 1 });
// path->positions[1] = (worldpos_t){ 10, 3, 0 }; npcPathAddNode(&npc->data.npc, (worldpos_t){ 4, 4, 0 });
// path->positions[2] = (worldpos_t){ 10, 10, 0 }; npcPathAddNode(&npc->data.npc, (worldpos_t){ 10, 10, 1 });
// path->positions[3] = (worldpos_t){ 3, 10, 0 };
// path->count = 4;
// All Good! // All Good!
errorOk(); errorOk();