From 8d5c0c7cad6cc3de45e0b4eba8a1e0a0b55a6ed0 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Thu, 2 Jul 2026 14:08:59 -0500 Subject: [PATCH] Path finding (first pass) --- src/dusk/rpg/cutscene/cutscene.h | 29 +++++++++++ src/dusk/rpg/cutscene/cutscenepause.h | 12 +++-- src/dusk/rpg/cutscene/cutscenesystem.c | 3 ++ src/dusk/rpg/cutscene/cutscenesystem.h | 1 + src/dusk/rpg/cutscene/item/cutsceneitem.c | 26 ++++++++++ src/dusk/rpg/cutscene/item/cutsceneitem.h | 17 ++++++- src/dusk/rpg/cutscene/scene/testcutscene.h | 8 +-- src/dusk/rpg/entity/CMakeLists.txt | 1 + src/dusk/rpg/entity/entitypathstep.c | 57 ++++++++++++++++++++++ src/dusk/rpg/entity/entitypathstep.h | 26 ++++++++++ src/dusk/rpg/entity/npc/npc.c | 5 +- src/dusk/rpg/entity/npc/npcpath.c | 27 +++++----- src/dusk/rpg/entity/npc/npcpath.h | 8 +++ src/dusk/rpg/entity/player.c | 5 +- src/dusk/rpg/rpg.c | 11 ++--- 15 files changed, 199 insertions(+), 37 deletions(-) create mode 100644 src/dusk/rpg/entity/entitypathstep.c create mode 100644 src/dusk/rpg/entity/entitypathstep.h diff --git a/src/dusk/rpg/cutscene/cutscene.h b/src/dusk/rpg/cutscene/cutscene.h index 5cbdf27d..42ab40e9 100644 --- a/src/dusk/rpg/cutscene/cutscene.h +++ b/src/dusk/rpg/cutscene/cutscene.h @@ -41,6 +41,32 @@ typedef struct cutscene_s { #define CUTSCENE_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) \ { \ .type = CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT, \ @@ -64,3 +90,6 @@ typedef struct cutscene_s { #define CUTSCENE_FADE_FROM_WHITE(DURATION) \ CUTSCENE_FADE(COLOR_WHITE, COLOR_TRANSPARENT_WHITE, DURATION, EASING_LINEAR) + +#define CUTSCENE_SET_PAUSE(FLAGS) \ + { .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) } diff --git a/src/dusk/rpg/cutscene/cutscenepause.h b/src/dusk/rpg/cutscene/cutscenepause.h index 735ae3e6..222d28c0 100644 --- a/src/dusk/rpg/cutscene/cutscenepause.h +++ b/src/dusk/rpg/cutscene/cutscenepause.h @@ -10,11 +10,15 @@ typedef uint8_t cutscenepause_t; +#define CUTSCENE_PAUSE_NONE ((cutscenepause_t)0) #define CUTSCENE_PAUSE_NPC ((cutscenepause_t)(1 << 0)) #define CUTSCENE_PAUSE_PLAYER ((cutscenepause_t)(1 << 1)) #define CUTSCENE_PAUSE_WORLD ((cutscenepause_t)(1 << 2)) -#define CUTSCENE_PAUSE_DEFAULT \ - ((cutscenepause_t)(CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER)) -#define CUTSCENE_PAUSE_ALL \ - ((cutscenepause_t)(CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER | CUTSCENE_PAUSE_WORLD)) +#define CUTSCENE_PAUSE_DEFAULT ((cutscenepause_t)( \ + CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER \ +)) + +#define CUTSCENE_PAUSE_ALL ((cutscenepause_t)( \ + CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER | CUTSCENE_PAUSE_WORLD \ +)) diff --git a/src/dusk/rpg/cutscene/cutscenesystem.c b/src/dusk/rpg/cutscene/cutscenesystem.c index 29dc8ce2..a1cedca4 100644 --- a/src/dusk/rpg/cutscene/cutscenesystem.c +++ b/src/dusk/rpg/cutscene/cutscenesystem.c @@ -16,6 +16,7 @@ void cutsceneSystemInit() { void cutsceneSystemStartCutscene(const cutscene_t *cutscene) { CUTSCENE_SYSTEM.scene = cutscene; + CUTSCENE_SYSTEM.pause = cutscene->pause; CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so start wraps. cutsceneSystemNext(); } @@ -38,6 +39,7 @@ void cutsceneSystemNext() { ) { CUTSCENE_SYSTEM.scene = NULL; CUTSCENE_SYSTEM.currentItem = 0xFF; + CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE; return; } @@ -56,4 +58,5 @@ const cutsceneitem_t * cutsceneSystemGetCurrentItem() { void cutsceneSystemDispose() { CUTSCENE_SYSTEM.scene = NULL; CUTSCENE_SYSTEM.currentItem = 0xFF; + CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE; } \ No newline at end of file diff --git a/src/dusk/rpg/cutscene/cutscenesystem.h b/src/dusk/rpg/cutscene/cutscenesystem.h index 9579a392..8d0540b4 100644 --- a/src/dusk/rpg/cutscene/cutscenesystem.h +++ b/src/dusk/rpg/cutscene/cutscenesystem.h @@ -11,6 +11,7 @@ typedef struct { const cutscene_t *scene; uint8_t currentItem; + cutscenepause_t pause; // Data (used by the current item). cutsceneitemdata_t data; diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.c b/src/dusk/rpg/cutscene/item/cutsceneitem.c index 1c0f0007..c2e6b7a5 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.c +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.c @@ -11,6 +11,7 @@ #include "time/time.h" #include "ui/rpg/uitextboxmain.h" #include "rpg/entity/entity.h" +#include "rpg/entity/entitypathstep.h" void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) { switch(item->type) { @@ -37,6 +38,10 @@ void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) { ); break; + case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO: + data->entityWalkTo.currentIndex = 0; + break; + case CUTSCENE_ITEM_TYPE_FADE: uiFullboxTransition( &UI_FULLBOX_OVER, @@ -47,6 +52,10 @@ void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) { ); break; + case CUTSCENE_ITEM_TYPE_SET_PAUSE: + CUTSCENE_SYSTEM.pause = item->setPause; + break; + default: break; } @@ -68,6 +77,19 @@ void cutsceneItemUpdate(const cutsceneitem_t *item, cutsceneitemdata_t *data) { cutsceneSystemNext(); 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: if( UI_FULLBOX_OVER.duration > 0.0f && @@ -76,6 +98,10 @@ void cutsceneItemUpdate(const cutsceneitem_t *item, cutsceneitemdata_t *data) { cutsceneSystemNext(); break; + case CUTSCENE_ITEM_TYPE_SET_PAUSE: + cutsceneSystemNext(); + break; + default: break; } diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.h b/src/dusk/rpg/cutscene/item/cutsceneitem.h index 7fb1b548..4964982a 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.h @@ -10,6 +10,7 @@ #include "cutscenecallback.h" #include "cutscenetext.h" #include "cutscenefade.h" +#include "rpg/cutscene/cutscenepause.h" #include "rpg/overworld/worldpos.h" typedef struct cutscene_s cutscene_t; @@ -21,7 +22,9 @@ typedef enum { CUTSCENE_ITEM_TYPE_WAIT, CUTSCENE_ITEM_TYPE_CUTSCENE, 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; typedef struct cutsceneitem_s { @@ -37,12 +40,24 @@ typedef struct cutsceneitem_s { uint8_t entityIndex; worldpos_t target; } entityTeleport; + struct { + uint8_t entityIndex; + const worldpos_t *positions; + uint8_t count; + bool_t walkAround; + } entityWalkTo; cutscenefade_t fade; + cutscenepause_t setPause; }; } cutsceneitem_t; +typedef struct { + uint8_t currentIndex; +} cutsceneentitywalktodata_t; + typedef union { cutscenewaitdata_t wait; + cutsceneentitywalktodata_t entityWalkTo; } cutsceneitemdata_t; /** diff --git a/src/dusk/rpg/cutscene/scene/testcutscene.h b/src/dusk/rpg/cutscene/scene/testcutscene.h index 9c950eca..b8690d7a 100755 --- a/src/dusk/rpg/cutscene/scene/testcutscene.h +++ b/src/dusk/rpg/cutscene/scene/testcutscene.h @@ -15,8 +15,8 @@ CUTSCENE(TEST_ONE, DEFAULT, CUTSCENE(TEST_TWO, DEFAULT, CUTSCENE_TEXT("Test Two."), - CUTSCENE_FADE_TO_BLACK(1.0f), - CUTSCENE_ENTITY_TELEPORT(0, 20, 0, 0), - CUTSCENE_WAIT(1.0f), - CUTSCENE_FADE_FROM_BLACK(1.0f), + CUTSCENE_SET_PAUSE(CUTSCENE_PAUSE_NONE), + CUTSCENE_WAIT(5.0f), + CUTSCENE_TEXT("Hello world") + // CUTSCENE_ENTITY_WALK_TO(0, 0, 0, 0), ); \ No newline at end of file diff --git a/src/dusk/rpg/entity/CMakeLists.txt b/src/dusk/rpg/entity/CMakeLists.txt index 0cf1f321..7aa0960c 100644 --- a/src/dusk/rpg/entity/CMakeLists.txt +++ b/src/dusk/rpg/entity/CMakeLists.txt @@ -8,6 +8,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC entity.c entitydir.c + entitypathstep.c player.c ) diff --git a/src/dusk/rpg/entity/entitypathstep.c b/src/dusk/rpg/entity/entitypathstep.c new file mode 100644 index 00000000..003b03a9 --- /dev/null +++ b/src/dusk/rpg/entity/entitypathstep.c @@ -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; +} diff --git a/src/dusk/rpg/entity/entitypathstep.h b/src/dusk/rpg/entity/entitypathstep.h new file mode 100644 index 00000000..1608cae8 --- /dev/null +++ b/src/dusk/rpg/entity/entitypathstep.h @@ -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 +); diff --git a/src/dusk/rpg/entity/npc/npc.c b/src/dusk/rpg/entity/npc/npc.c index eebd80a2..05175828 100644 --- a/src/dusk/rpg/entity/npc/npc.c +++ b/src/dusk/rpg/entity/npc/npc.c @@ -50,10 +50,7 @@ void npcSetMoveType(entity_t *entity, const npcmovetype_t moveType) { void npcMovement(entity_t *entity) { assertNotNull(entity, "Entity pointer cannot be NULL"); - if( - CUTSCENE_SYSTEM.scene != NULL && - (CUTSCENE_SYSTEM.scene->pause & CUTSCENE_PAUSE_NPC) - ) return; + if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_NPC) return; npc_t *npc = &entity->data.npc; if(npc->interactState != NPC_INTERACT_STATE_NONE) return; diff --git a/src/dusk/rpg/entity/npc/npcpath.c b/src/dusk/rpg/entity/npc/npcpath.c index e6f235b8..9d6ea47d 100644 --- a/src/dusk/rpg/entity/npc/npcpath.c +++ b/src/dusk/rpg/entity/npc/npcpath.c @@ -7,7 +7,9 @@ #include "npc.h" #include "rpg/entity/entity.h" +#include "rpg/entity/entitypathstep.h" #include "rpg/overworld/worldpos.h" +#include "assert/assert.h" void npcPathInit(npc_t *npc) { npcpath_t *path = &npc->moveData.path; @@ -15,6 +17,15 @@ void npcPathInit(npc_t *npc) { 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) { npcpath_t *path = &entity->data.npc.moveData.path; if(path->count == 0) return; @@ -22,23 +33,11 @@ void npcPathMovement(entity_t *entity) { // Advance past any waypoints already reached (including the current one). worldpos_t *target = &path->positions[path->index]; - if(worldPosIsEqual(entity->position, *target)) { + if(entityPathStep(entity, *target, false)) { 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); } - - 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); } diff --git a/src/dusk/rpg/entity/npc/npcpath.h b/src/dusk/rpg/entity/npc/npcpath.h index 5af51d73..d6469168 100644 --- a/src/dusk/rpg/entity/npc/npcpath.h +++ b/src/dusk/rpg/entity/npc/npcpath.h @@ -28,6 +28,14 @@ typedef struct { */ 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. * diff --git a/src/dusk/rpg/entity/player.c b/src/dusk/rpg/entity/player.c index 91516226..07ce68c8 100644 --- a/src/dusk/rpg/entity/player.c +++ b/src/dusk/rpg/entity/player.c @@ -24,10 +24,7 @@ bool_t playerCanInteract(entity_t *entity) { void playerInput(entity_t *entity) { assertNotNull(entity, "Entity pointer cannot be NULL"); - if( - CUTSCENE_SYSTEM.scene != NULL && - (CUTSCENE_SYSTEM.scene->pause & CUTSCENE_PAUSE_PLAYER) - ) return; + if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_PLAYER) return; // Toggle settings on pause if(uiSettingsIsOpen() && inputPressed(INPUT_ACTION_PAUSE)) { diff --git a/src/dusk/rpg/rpg.c b/src/dusk/rpg/rpg.c index 5dba6e44..9b17a0a1 100644 --- a/src/dusk/rpg/rpg.c +++ b/src/dusk/rpg/rpg.c @@ -7,6 +7,7 @@ #include "rpg.h" #include "entity/entity.h" +#include "rpg/entity/npc/npcpath.h" #include "rpg/overworld/map.h" #include "rpg/cutscene/cutscenesystem.h" #include "rpg/cutscene/scene/testcutscene.h" @@ -57,12 +58,10 @@ errorret_t rpgInit(void) { if(ci != -1) entitySetChunk(npc, (uint8_t)ci); } - // npcpath_t *path = &npc->data.npc.moveData.path; - // path->positions[0] = (worldpos_t){ 3, 3, 0 }; - // path->positions[1] = (worldpos_t){ 10, 3, 0 }; - // path->positions[2] = (worldpos_t){ 10, 10, 0 }; - // path->positions[3] = (worldpos_t){ 3, 10, 0 }; - // path->count = 4; + npcPathAddNode(&npc->data.npc, (worldpos_t){ 4, 4, 0 }); + npcPathAddNode(&npc->data.npc, (worldpos_t){ 10, 10, 1 }); + npcPathAddNode(&npc->data.npc, (worldpos_t){ 4, 4, 0 }); + npcPathAddNode(&npc->data.npc, (worldpos_t){ 10, 10, 1 }); // All Good! errorOk();