From d326f6c1acecbaa15698269180b5895a6ef9389f Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Fri, 26 Jun 2026 14:21:48 -0500 Subject: [PATCH 1/6] NPC movements --- CLAUDE.md | 23 +++++++ src/dusk/rpg/entity/CMakeLists.txt | 5 +- src/dusk/rpg/entity/entity.c | 3 + src/dusk/rpg/entity/entity.h | 2 +- src/dusk/rpg/entity/entityanim.h | 4 +- src/dusk/rpg/entity/entitytype.h | 11 ++- src/dusk/rpg/entity/npc.c | 31 --------- src/dusk/rpg/entity/npc.h | 37 ---------- src/dusk/rpg/entity/npc/CMakeLists.txt | 13 ++++ src/dusk/rpg/entity/npc/npc.c | 62 +++++++++++++++++ src/dusk/rpg/entity/npc/npc.h | 84 +++++++++++++++++++++++ src/dusk/rpg/entity/npc/npcpath.c | 41 +++++++++++ src/dusk/rpg/entity/npc/npcpath.h | 36 ++++++++++ src/dusk/rpg/entity/npc/npcturn.c | 27 ++++++++ src/dusk/rpg/entity/npc/npcturn.h | 36 ++++++++++ src/dusk/rpg/entity/npc/npcwalk.c | 52 ++++++++++++++ src/dusk/rpg/entity/npc/npcwalk.h | 54 +++++++++++++++ src/dusk/rpg/rpg.c | 8 +++ src/dusk/scene/overworld/sceneoverworld.c | 37 +++++----- src/dusk/util/CMakeLists.txt | 1 + src/dusk/util/random.c | 13 ++++ src/dusk/util/random.h | 18 +++++ 22 files changed, 502 insertions(+), 96 deletions(-) delete mode 100644 src/dusk/rpg/entity/npc.c delete mode 100644 src/dusk/rpg/entity/npc.h create mode 100644 src/dusk/rpg/entity/npc/CMakeLists.txt create mode 100644 src/dusk/rpg/entity/npc/npc.c create mode 100644 src/dusk/rpg/entity/npc/npc.h create mode 100644 src/dusk/rpg/entity/npc/npcpath.c create mode 100644 src/dusk/rpg/entity/npc/npcpath.h create mode 100644 src/dusk/rpg/entity/npc/npcturn.c create mode 100644 src/dusk/rpg/entity/npc/npcturn.h create mode 100644 src/dusk/rpg/entity/npc/npcwalk.c create mode 100644 src/dusk/rpg/entity/npc/npcwalk.h create mode 100644 src/dusk/util/random.c create mode 100644 src/dusk/util/random.h diff --git a/CLAUDE.md b/CLAUDE.md index ed7bea5e..451c4d16 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -423,6 +423,29 @@ above the declaration with no blank line in between. --- +## Color system + +Colors are defined in `src/dusk/display/color.csv` and code-generated +into a `color.h` header by `tools/color/csv/__main__.py`. + +Each row in the CSV has `name,r,g,b,a` with channel values in `[0.0, 1.0]`. +The script emits four `#define` variants per color plus a bare alias: + +``` +COLOR__4B color4b(r8, g8, b8, a8) // default alias target +COLOR__3B color3b(r8, g8, b8) +COLOR__3F color3f(rf, gf, bf) +COLOR__4F color4f(rf, gf, bf, af) +COLOR_ COLOR__4B +``` + +`color_t` is `color4b_t` (four `uint8_t` channels). + +To add a new color, append a row to `color.csv` and rebuild — do not +hand-edit the generated header. + +--- + ## Tests - Tests live in `test/` mirroring `src/dusk/` structure. - Use cmocka; include `dusktest.h`. diff --git a/src/dusk/rpg/entity/CMakeLists.txt b/src/dusk/rpg/entity/CMakeLists.txt index a254c108..8a249fd4 100644 --- a/src/dusk/rpg/entity/CMakeLists.txt +++ b/src/dusk/rpg/entity/CMakeLists.txt @@ -10,6 +10,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} entityanim.c entitydir.c entityinteract.c - npc.c player.c -) \ No newline at end of file +) + +add_subdirectory(npc) \ No newline at end of file diff --git a/src/dusk/rpg/entity/entity.c b/src/dusk/rpg/entity/entity.c index 274ac576..85d00fb6 100644 --- a/src/dusk/rpg/entity/entity.c +++ b/src/dusk/rpg/entity/entity.c @@ -41,6 +41,9 @@ void entityUpdate(entity_t *entity) { entityAnimUpdate(entity); // Movement code. + if(ENTITY_CALLBACKS[entity->type].freeMovement != NULL) { + ENTITY_CALLBACKS[entity->type].freeMovement(entity); + } if( cutsceneModeIsInputAllowed() && ENTITY_CALLBACKS[entity->type].movement != NULL diff --git a/src/dusk/rpg/entity/entity.h b/src/dusk/rpg/entity/entity.h index 6d6a566a..544e341c 100644 --- a/src/dusk/rpg/entity/entity.h +++ b/src/dusk/rpg/entity/entity.h @@ -10,7 +10,7 @@ #include "entityanim.h" #include "entityinteract.h" #include "entitytype.h" -#include "npc.h" +#include "npc/npc.h" typedef struct map_s map_t; diff --git a/src/dusk/rpg/entity/entityanim.h b/src/dusk/rpg/entity/entityanim.h index 91390682..9c9de54e 100644 --- a/src/dusk/rpg/entity/entityanim.h +++ b/src/dusk/rpg/entity/entityanim.h @@ -9,8 +9,8 @@ #include "time/time.h" #define ENTITY_ANIM_TURN_DURATION TIME_TICKS_TO_TIME(2) -#define ENTITY_ANIM_WALK_DURATION TIME_TICKS_TO_TIME(6) -#define ENTITY_ANIM_RUN_DURATION TIME_TICKS_TO_TIME(3) +#define ENTITY_ANIM_WALK_DURATION TIME_TICKS_TO_TIME(10) +#define ENTITY_ANIM_RUN_DURATION TIME_TICKS_TO_TIME(6) typedef struct entity_s entity_t; diff --git a/src/dusk/rpg/entity/entitytype.h b/src/dusk/rpg/entity/entitytype.h index fb67f33c..4cfffe21 100644 --- a/src/dusk/rpg/entity/entitytype.h +++ b/src/dusk/rpg/entity/entitytype.h @@ -7,7 +7,7 @@ #pragma once #include "rpg/entity/player.h" -#include "npc.h" +#include "npc/npc.h" typedef enum { ENTITY_TYPE_NULL, @@ -33,11 +33,17 @@ typedef struct { void (*init)(entity_t *entity); /** - * Movement callback for the entity type. + * Movement callback for the entity type. Gated by cutscene input. * @param entity Pointer to the entity to move. */ void (*movement)(entity_t *entity); + /** + * Free movement callback. Always runs regardless of cutscene state. + * @param entity Pointer to the entity to move. + */ + void (*freeMovement)(entity_t *entity); + /** * Interaction callback for the entity type. * @param player Pointer to the player entity. @@ -58,6 +64,7 @@ static const entitycallback_t ENTITY_CALLBACKS[ENTITY_TYPE_COUNT] = { [ENTITY_TYPE_NPC] = { .init = npcInit, .movement = npcMovement, + .freeMovement = npcFreeMovement, .interact = npcInteract } }; \ No newline at end of file diff --git a/src/dusk/rpg/entity/npc.c b/src/dusk/rpg/entity/npc.c deleted file mode 100644 index ec6548bd..00000000 --- a/src/dusk/rpg/entity/npc.c +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "entity.h" -#include "assert/assert.h" - -#include "rpg/cutscene/scene/testcutscene.h" -#include "rpg/rpgtextbox.h" - -void npcInit(entity_t *entity) { - assertNotNull(entity, "Entity pointer cannot be NULL"); -} - -void npcMovement(entity_t *entity) { - assertNotNull(entity, "Entity pointer cannot be NULL"); -} - -bool_t npcInteract(entity_t *player, entity_t *npc) { - assertNotNull(player, "Player entity pointer cannot be NULL"); - assertNotNull(npc, "NPC entity pointer cannot be NULL"); - - cutsceneSystemStartCutscene(&TEST_CUTSCENE); - - // rpgTextboxShow(RPG_TEXTBOX_POS_BOTTOM, "Hello World!"); - - return false; -}; \ No newline at end of file diff --git a/src/dusk/rpg/entity/npc.h b/src/dusk/rpg/entity/npc.h deleted file mode 100644 index df36f15e..00000000 --- a/src/dusk/rpg/entity/npc.h +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 2025 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; - -typedef struct { - void *nothing; -} npc_t; - -/** - * Initializes an NPC entity. - * - * @param entity Pointer to the entity structure to initialize. - */ -void npcInit(entity_t *entity); - -/** - * Updates an NPC entity. - * - * @param entity Pointer to the entity structure to update. - */ -void npcMovement(entity_t *entity); - -/** - * Handles interaction with an NPC entity. - * - * @param player Pointer to the player entity. - * @param npc Pointer to the NPC entity. - */ -bool_t npcInteract(entity_t *player, entity_t *npc); \ No newline at end of file diff --git a/src/dusk/rpg/entity/npc/CMakeLists.txt b/src/dusk/rpg/entity/npc/CMakeLists.txt new file mode 100644 index 00000000..922a4e1d --- /dev/null +++ b/src/dusk/rpg/entity/npc/CMakeLists.txt @@ -0,0 +1,13 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + npc.c + npcturn.c + npcwalk.c + npcpath.c +) diff --git a/src/dusk/rpg/entity/npc/npc.c b/src/dusk/rpg/entity/npc/npc.c new file mode 100644 index 00000000..a8dcf6a5 --- /dev/null +++ b/src/dusk/rpg/entity/npc/npc.c @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/entity/entity.h" +#include "assert/assert.h" + +#include "rpg/cutscene/scene/testcutscene.h" +#include "rpg/rpgtextbox.h" + +const npcmovecallback_t NPC_MOVE_CALLBACKS[NPC_MOVE_TYPE_COUNT] = { + [NPC_MOVE_TYPE_NULL] = { NULL, NULL }, + [NPC_MOVE_TYPE_RANDOM_TURN] = { npcRandomTurnInit, npcRandomTurnMovement }, + [NPC_MOVE_TYPE_RANDOM_WALK] = { + npcRandomWalkInit, + npcRandomWalkMovement + }, + [NPC_MOVE_TYPE_RANDOM_TURN_AND_WALK] = { + npcRandomTurnAndWalkInit, + npcRandomTurnAndWalkMovement + }, + [NPC_MOVE_TYPE_PATH] = { npcPathInit, npcPathMovement, true }, +}; + +void npcInit(entity_t *entity) { + assertNotNull(entity, "Entity pointer cannot be NULL"); +} + +void npcSetMoveType(entity_t *entity, const npcmovetype_t moveType) { + assertNotNull(entity, "Entity pointer cannot be NULL"); + npc_t *npc = &entity->data.npc; + npc->moveType = moveType; + if(NPC_MOVE_CALLBACKS[moveType].init != NULL) { + NPC_MOVE_CALLBACKS[moveType].init(npc); + } +} + +void npcMovement(entity_t *entity) { + assertNotNull(entity, "Entity pointer cannot be NULL"); + npc_t *npc = &entity->data.npc; + const npcmovecallback_t *cb = &NPC_MOVE_CALLBACKS[npc->moveType]; + if(!cb->alwaysRun && cb->movement != NULL) cb->movement(entity); +} + +void npcFreeMovement(entity_t *entity) { + assertNotNull(entity, "Entity pointer cannot be NULL"); + npc_t *npc = &entity->data.npc; + const npcmovecallback_t *cb = &NPC_MOVE_CALLBACKS[npc->moveType]; + if(cb->alwaysRun && cb->movement != NULL) cb->movement(entity); +} + +bool_t npcInteract(entity_t *player, entity_t *npc) { + assertNotNull(player, "Player entity pointer cannot be NULL"); + assertNotNull(npc, "NPC entity pointer cannot be NULL"); + + cutsceneSystemStartCutscene(&TEST_CUTSCENE); + // rpgTextboxShow(RPG_TEXTBOX_POS_BOTTOM, "Hello World!"); + return false; +} diff --git a/src/dusk/rpg/entity/npc/npc.h b/src/dusk/rpg/entity/npc/npc.h new file mode 100644 index 00000000..244f2b98 --- /dev/null +++ b/src/dusk/rpg/entity/npc/npc.h @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" +#include "npcturn.h" +#include "npcwalk.h" +#include "npcpath.h" + +typedef struct entity_s entity_t; + +typedef enum { + NPC_MOVE_TYPE_NULL, + NPC_MOVE_TYPE_RANDOM_TURN, + NPC_MOVE_TYPE_RANDOM_WALK, + NPC_MOVE_TYPE_RANDOM_TURN_AND_WALK, + NPC_MOVE_TYPE_PATH, + NPC_MOVE_TYPE_COUNT +} npcmovetype_t; + +typedef union { + npcrandomturn_t randomTurn; + npcrandomwalk_t randomWalk; + npcrandomturnandwalk_t randomTurnAndWalk; + npcpath_t path; +} npcmovedata_t; + +typedef struct npc_s { + npcmovetype_t moveType; + npcmovedata_t moveData; +} npc_t; + +typedef struct { + /** Called once when the move type is set. */ + void (*init)(npc_t *npc); + /** Called each movement tick. */ + void (*movement)(entity_t *entity); + /** True if movement runs regardless of cutscene state. */ + bool_t alwaysRun; +} npcmovecallback_t; + +extern const npcmovecallback_t NPC_MOVE_CALLBACKS[NPC_MOVE_TYPE_COUNT]; + +/** + * Initializes an NPC entity. + * + * @param entity Pointer to the entity structure to initialize. + */ +void npcInit(entity_t *entity); + +/** + * Sets the movement type for an NPC entity. + * + * @param entity Pointer to the entity structure. + * @param moveType The movement type to set. + */ +void npcSetMoveType(entity_t *entity, const npcmovetype_t moveType); + +/** + * Movement callback for an NPC entity. Gated by cutscene input. + * + * @param entity Pointer to the entity structure to update. + */ +void npcMovement(entity_t *entity); + +/** + * Free movement callback for an NPC entity. Runs always-run move types + * regardless of cutscene state. + * + * @param entity Pointer to the entity structure to update. + */ +void npcFreeMovement(entity_t *entity); + +/** + * Handles interaction with an NPC entity. + * + * @param player Pointer to the player entity. + * @param npc Pointer to the NPC entity. + */ +bool_t npcInteract(entity_t *player, entity_t *npc); diff --git a/src/dusk/rpg/entity/npc/npcpath.c b/src/dusk/rpg/entity/npc/npcpath.c new file mode 100644 index 00000000..741ea89d --- /dev/null +++ b/src/dusk/rpg/entity/npc/npcpath.c @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "npc.h" +#include "rpg/entity/entity.h" + +void npcPathInit(npc_t *npc) { + npcpath_t *path = &npc->moveData.path; + path->count = 0; + path->index = 0; +} + +void npcPathMovement(entity_t *entity) { + npcpath_t *path = &entity->data.npc.moveData.path; + if(path->count == 0) return; + if(!entityCanWalk(entity)) return; + + worldpos_t *target = &path->positions[path->index]; + + // At this waypoint - advance to the next + if( + entity->position.x == target->x && + entity->position.y == target->y + ) { + path->index = (path->index + 1) % path->count; + return; + } + + entitydir_t dir; + if(entity->position.x != target->x) { + dir = entity->position.x < target->x ? ENTITY_DIR_EAST : ENTITY_DIR_WEST; + } else { + dir = entity->position.y < target->y ? ENTITY_DIR_SOUTH : ENTITY_DIR_NORTH; + } + + entityWalk(entity, dir); +} diff --git a/src/dusk/rpg/entity/npc/npcpath.h b/src/dusk/rpg/entity/npc/npcpath.h new file mode 100644 index 00000000..5af51d73 --- /dev/null +++ b/src/dusk/rpg/entity/npc/npcpath.h @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" +#include "rpg/overworld/worldpos.h" + +typedef struct npc_s npc_t; +typedef struct entity_s entity_t; + +/** Maximum number of waypoints in an NPC path. */ +#define NPC_PATH_COUNT_MAX 8 + +typedef struct { + worldpos_t positions[NPC_PATH_COUNT_MAX]; + uint8_t count; + uint8_t index; +} npcpath_t; + +/** + * Initializes the path movement data for an NPC. + * + * @param npc Pointer to the NPC to initialize. + */ +void npcPathInit(npc_t *npc); + +/** + * Movement tick for an NPC following a path. + * + * @param entity Pointer to the entity to update. + */ +void npcPathMovement(entity_t *entity); diff --git a/src/dusk/rpg/entity/npc/npcturn.c b/src/dusk/rpg/entity/npc/npcturn.c new file mode 100644 index 00000000..63654ab3 --- /dev/null +++ b/src/dusk/rpg/entity/npc/npcturn.c @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "npc.h" +#include "rpg/entity/entity.h" +#include "util/random.h" +#include "time/time.h" +#include + +void npcRandomTurnInit(npc_t *npc) { + npcrandomturn_t *turn = &npc->moveData.randomTurn; + turn->frequencyMin = NPC_RANDOM_TURN_FREQUENCY_MIN_DEFAULT; + turn->frequencyMax = NPC_RANDOM_TURN_FREQUENCY_MAX_DEFAULT; + turn->timer = randomFloat(turn->frequencyMin, turn->frequencyMax); +} + +void npcRandomTurnMovement(entity_t *entity) { + npcrandomturn_t *turn = &entity->data.npc.moveData.randomTurn; + turn->timer -= TIME.delta; + if(turn->timer > 0.0f) return; + turn->timer = randomFloat(turn->frequencyMin, turn->frequencyMax); + if(entityCanTurn(entity)) entityTurn(entity, (entitydir_t)(rand() % 4)); +} diff --git a/src/dusk/rpg/entity/npc/npcturn.h b/src/dusk/rpg/entity/npc/npcturn.h new file mode 100644 index 00000000..3568bbdb --- /dev/null +++ b/src/dusk/rpg/entity/npc/npcturn.h @@ -0,0 +1,36 @@ +/** + * 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 npc_s npc_t; +typedef struct entity_s entity_t; + +/** Default min/max seconds between NPC random-turn ticks. */ +#define NPC_RANDOM_TURN_FREQUENCY_MIN_DEFAULT 2.0f +#define NPC_RANDOM_TURN_FREQUENCY_MAX_DEFAULT 4.0f + +typedef struct { + float_t frequencyMin; + float_t frequencyMax; + float_t timer; +} npcrandomturn_t; + +/** + * Initializes the random-turn movement data for an NPC. + * + * @param npc Pointer to the NPC to initialize. + */ +void npcRandomTurnInit(npc_t *npc); + +/** + * Movement tick for an NPC using random-turn movement. + * + * @param entity Pointer to the entity to update. + */ +void npcRandomTurnMovement(entity_t *entity); diff --git a/src/dusk/rpg/entity/npc/npcwalk.c b/src/dusk/rpg/entity/npc/npcwalk.c new file mode 100644 index 00000000..dc8794ec --- /dev/null +++ b/src/dusk/rpg/entity/npc/npcwalk.c @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "npc.h" +#include "rpg/entity/entity.h" +#include "util/random.h" +#include "time/time.h" +#include + +void npcRandomWalkInit(npc_t *npc) { + npcrandomwalk_t *walk = &npc->moveData.randomWalk; + walk->frequencyMin = NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT; + walk->frequencyMax = NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT; + walk->timer = randomFloat(walk->frequencyMin, walk->frequencyMax); +} + +void npcRandomWalkMovement(entity_t *entity) { + npcrandomwalk_t *walk = &entity->data.npc.moveData.randomWalk; + walk->timer -= TIME.delta; + if(walk->timer > 0.0f) return; + walk->timer = randomFloat(walk->frequencyMin, walk->frequencyMax); + if(entityCanWalk(entity)) entityWalk(entity, (entitydir_t)(rand() % 4)); +} + +void npcRandomTurnAndWalkInit(npc_t *npc) { + npcRandomTurnInit(npc); + npcrandomturnandwalk_t *tw = &npc->moveData.randomTurnAndWalk; + tw->walk.frequencyMin = NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT; + tw->walk.frequencyMax = NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT; + tw->walk.timer = randomFloat(tw->walk.frequencyMin, tw->walk.frequencyMax); +} + +void npcRandomTurnAndWalkMovement(entity_t *entity) { + npcrandomturnandwalk_t *tw = &entity->data.npc.moveData.randomTurnAndWalk; + + tw->turn.timer -= TIME.delta; + if(tw->turn.timer <= 0.0f) { + tw->turn.timer = randomFloat(tw->turn.frequencyMin, tw->turn.frequencyMax); + if(entityCanTurn(entity)) entityTurn(entity, (entitydir_t)(rand() % 4)); + } + + tw->walk.timer -= TIME.delta; + if(tw->walk.timer <= 0.0f) { + tw->walk.timer = randomFloat(tw->walk.frequencyMin, tw->walk.frequencyMax); + if(entityCanWalk(entity)) entityWalk(entity, (entitydir_t)(rand() % 4)); + } +} + diff --git a/src/dusk/rpg/entity/npc/npcwalk.h b/src/dusk/rpg/entity/npc/npcwalk.h new file mode 100644 index 00000000..4a65c4f8 --- /dev/null +++ b/src/dusk/rpg/entity/npc/npcwalk.h @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" +#include "npcturn.h" + +/** Default min/max seconds between NPC random-walk ticks. */ +#define NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT 2.0f +#define NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT 5.0f + +typedef struct { + float_t frequencyMin; + float_t frequencyMax; + float_t timer; +} npcrandomwalk_t; + +typedef struct { + npcrandomturn_t turn; + npcrandomwalk_t walk; +} npcrandomturnandwalk_t; + +/** + * Initializes the random-walk movement data for an NPC. + * + * @param npc Pointer to the NPC to initialize. + */ +void npcRandomWalkInit(npc_t *npc); + +/** + * Movement tick for an NPC using random-walk movement. + * + * @param entity Pointer to the entity to update. + */ +void npcRandomWalkMovement(entity_t *entity); + +/** + * Initializes the random-turn-and-walk movement data for an NPC. + * + * @param npc Pointer to the NPC to initialize. + */ +void npcRandomTurnAndWalkInit(npc_t *npc); + +/** + * Movement tick for an NPC using random-turn-and-walk movement. + * + * @param entity Pointer to the entity to update. + */ +void npcRandomTurnAndWalkMovement(entity_t *entity); + diff --git a/src/dusk/rpg/rpg.c b/src/dusk/rpg/rpg.c index c2e7446d..b4f38c0b 100644 --- a/src/dusk/rpg/rpg.c +++ b/src/dusk/rpg/rpg.c @@ -41,10 +41,18 @@ errorret_t rpgInit(void) { uint8_t npcIndex = entityGetAvailable(); entity_t *npc = &ENTITIES[npcIndex]; entityInit(npc, ENTITY_TYPE_NPC); + npcSetMoveType(npc, NPC_MOVE_TYPE_PATH); npc->position = (worldpos_t){ 3, 3, 0 }; npc->interact.type = ENTITY_INTERACT_PRINT; npc->interact.data.message = "hello world"; + npcpath_t *path = &npc->data.npc.moveData.path; + path->positions[0] = (worldpos_t){ 3, 3, 0 }; + path->positions[1] = (worldpos_t){ 5, 3, 0 }; + path->positions[2] = (worldpos_t){ 5, 5, 0 }; + path->positions[3] = (worldpos_t){ 3, 5, 0 }; + path->count = 4; + // All Good! errorOk(); } diff --git a/src/dusk/scene/overworld/sceneoverworld.c b/src/dusk/scene/overworld/sceneoverworld.c index e75e8def..064a4748 100644 --- a/src/dusk/scene/overworld/sceneoverworld.c +++ b/src/dusk/scene/overworld/sceneoverworld.c @@ -109,35 +109,30 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) { // Entities { - uint8_t spriteCount = 0; - spritebatchsprite_t sprites[ENTITY_COUNT]; for(uint8_t i = 0; i < ENTITY_COUNT; i++) { entity_t *ent = &ENTITIES[i]; if(ent->type == ENTITY_TYPE_NULL) continue; - glm_vec3_copy(ent->renderPosition, sprites[spriteCount].min); - glm_vec3_copy(ent->renderPosition, sprites[spriteCount].max); - glm_vec3_add( - sprites[spriteCount].max, - (vec3){ 1, 1, 0 }, - sprites[spriteCount].max - ); + spritebatchsprite_t sprite; + glm_vec3_copy(ent->renderPosition, sprite.min); + glm_vec3_copy(ent->renderPosition, sprite.max); + glm_vec3_add(sprite.max, (vec3){ 1, 1, 0 }, sprite.max); + glm_vec2_copy((vec2){ 0, 0 }, sprite.uvMin); + glm_vec2_copy((vec2){ 1, 1 }, sprite.uvMax); - glm_vec2_copy((vec2){ 0, 0 }, sprites[spriteCount].uvMin); - glm_vec2_copy((vec2){ 1, 1 }, sprites[spriteCount].uvMax); + color_t color; + switch(ent->direction) { + case ENTITY_DIR_NORTH: color = COLOR_YELLOW; break; + case ENTITY_DIR_EAST: color = COLOR_RED; break; + case ENTITY_DIR_SOUTH: color = COLOR_GREEN; break; + case ENTITY_DIR_WEST: color = COLOR_BLUE; break; + default: color = COLOR_CYAN; break; + } - spriteCount++; - } - - if(spriteCount) { shadermaterial_t material = { - .unlit = { - .color = COLOR_CYAN, - .texture = NULL - } + .unlit = { .color = color, .texture = NULL } }; - // material.unlit.texture = &TEXTURE_TEST; - spriteBatchBuffer(sprites, spriteCount, &SHADER_UNLIT, material); + spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material); spriteBatchFlush(); } } diff --git a/src/dusk/util/CMakeLists.txt b/src/dusk/util/CMakeLists.txt index 1beed776..62fb7902 100644 --- a/src/dusk/util/CMakeLists.txt +++ b/src/dusk/util/CMakeLists.txt @@ -10,6 +10,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} crypt.c endian.c memory.c + random.c string.c math.c sort.c diff --git a/src/dusk/util/random.c b/src/dusk/util/random.c new file mode 100644 index 00000000..ded6b7d0 --- /dev/null +++ b/src/dusk/util/random.c @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "random.h" +#include + +float_t randomFloat(const float_t min, const float_t max) { + return min + ((float_t)rand() / (float_t)RAND_MAX) * (max - min); +} diff --git a/src/dusk/util/random.h b/src/dusk/util/random.h new file mode 100644 index 00000000..3968e2af --- /dev/null +++ b/src/dusk/util/random.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +/** + * Returns a random float between min (inclusive) and max (exclusive). + * + * @param min Lower bound. + * @param max Upper bound. + * @returns A random float in [min, max). + */ +float_t randomFloat(const float_t min, const float_t max); From e53775b97fd9ab1ed93bffa7752da71c54ee0724 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Fri, 26 Jun 2026 14:24:13 -0500 Subject: [PATCH 2/6] Fixed player turn bug --- src/dusk/rpg/entity/player.c | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/dusk/rpg/entity/player.c b/src/dusk/rpg/entity/player.c index 8a8b8265..521b9e59 100644 --- a/src/dusk/rpg/entity/player.c +++ b/src/dusk/rpg/entity/player.c @@ -33,14 +33,25 @@ void playerInput(entity_t *entity) { // Can player act? if(UI_FOCUS.count > 0) return; - // Turn + // Turn - only if not already holding the direction we face const playerinputdirmap_t *dirMap = PLAYER_INPUT_DIR_MAP; + bool_t holdingFaced = false; do { if(!inputIsDown(dirMap->action)) continue; - if(entity->direction == dirMap->direction) continue; - return entityTurn(entity, dirMap->direction); + if(entity->direction != dirMap->direction) continue; + holdingFaced = true; + break; } while((++dirMap)->action != 0xFF); + if(!holdingFaced) { + dirMap = PLAYER_INPUT_DIR_MAP; + do { + if(!inputIsDown(dirMap->action)) continue; + if(entity->direction == dirMap->direction) continue; + return entityTurn(entity, dirMap->direction); + } while((++dirMap)->action != 0xFF); + } + // Walk / Run bool_t running = inputIsDown(INPUT_ACTION_CANCEL); dirMap = PLAYER_INPUT_DIR_MAP; From dd22d6424ac6eae4c3000071ffc4f5cabd6ce87b Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Fri, 26 Jun 2026 14:29:55 -0500 Subject: [PATCH 3/6] testing some performance stuff --- src/dusk/rpg/entity/entityanim.h | 2 +- src/dusk/rpg/entity/npc/npcpath.c | 30 ++++++++++++++++++++---------- src/dusk/rpg/overworld/worldpos.h | 10 +++++----- src/dusk/rpg/rpg.c | 6 +++--- 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/dusk/rpg/entity/entityanim.h b/src/dusk/rpg/entity/entityanim.h index 9c9de54e..c822d605 100644 --- a/src/dusk/rpg/entity/entityanim.h +++ b/src/dusk/rpg/entity/entityanim.h @@ -9,7 +9,7 @@ #include "time/time.h" #define ENTITY_ANIM_TURN_DURATION TIME_TICKS_TO_TIME(2) -#define ENTITY_ANIM_WALK_DURATION TIME_TICKS_TO_TIME(10) +#define ENTITY_ANIM_WALK_DURATION TIME_TICKS_TO_TIME(12) #define ENTITY_ANIM_RUN_DURATION TIME_TICKS_TO_TIME(6) typedef struct entity_s entity_t; diff --git a/src/dusk/rpg/entity/npc/npcpath.c b/src/dusk/rpg/entity/npc/npcpath.c index 741ea89d..2d8e9a36 100644 --- a/src/dusk/rpg/entity/npc/npcpath.c +++ b/src/dusk/rpg/entity/npc/npcpath.c @@ -7,6 +7,7 @@ #include "npc.h" #include "rpg/entity/entity.h" +#include "rpg/overworld/worldpos.h" void npcPathInit(npc_t *npc) { npcpath_t *path = &npc->moveData.path; @@ -19,23 +20,32 @@ void npcPathMovement(entity_t *entity) { if(path->count == 0) return; if(!entityCanWalk(entity)) return; + // Advance past any waypoints already reached (including the current one). worldpos_t *target = &path->positions[path->index]; - - // At this waypoint - advance to the next - if( - entity->position.x == target->x && - entity->position.y == target->y - ) { + if(worldPosIsEqual(entity->position, *target)) { path->index = (path->index + 1) % path->count; - return; + target = &path->positions[path->index]; + // New target is the same tile - nothing to do this tick + if(worldPosIsEqual(entity->position, *target)) return; } entitydir_t dir; - if(entity->position.x != target->x) { - dir = entity->position.x < target->x ? ENTITY_DIR_EAST : ENTITY_DIR_WEST; + 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 { - dir = entity->position.y < target->y ? ENTITY_DIR_SOUTH : ENTITY_DIR_NORTH; + // 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; } + worldpos_t prevPos = entity->position; entityWalk(entity, dir); + + // If entityWalk didn't move us (blocked), skip this waypoint to avoid + // getting stuck. + if(worldPosIsEqual(entity->position, prevPos)) { + path->index = (path->index + 1) % path->count; + } } diff --git a/src/dusk/rpg/overworld/worldpos.h b/src/dusk/rpg/overworld/worldpos.h index bb6be2fe..a4d2d298 100644 --- a/src/dusk/rpg/overworld/worldpos.h +++ b/src/dusk/rpg/overworld/worldpos.h @@ -10,15 +10,15 @@ #define TILE_SIZE_PIXELS 24 -#define CHUNK_WIDTH 16 -#define CHUNK_HEIGHT 16 +#define CHUNK_WIDTH 32 +#define CHUNK_HEIGHT 32 #define CHUNK_DEPTH 8 #define CHUNK_TILE_COUNT (CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH) -#define MAP_CHUNK_WIDTH 4 -#define MAP_CHUNK_HEIGHT 4 -#define MAP_CHUNK_DEPTH 3 +#define MAP_CHUNK_WIDTH 2 +#define MAP_CHUNK_HEIGHT 2 +#define MAP_CHUNK_DEPTH 2 #define MAP_CHUNK_COUNT (MAP_CHUNK_WIDTH * MAP_CHUNK_HEIGHT * MAP_CHUNK_DEPTH) #define ENTITY_COUNT 32 diff --git a/src/dusk/rpg/rpg.c b/src/dusk/rpg/rpg.c index b4f38c0b..128902fc 100644 --- a/src/dusk/rpg/rpg.c +++ b/src/dusk/rpg/rpg.c @@ -48,9 +48,9 @@ errorret_t rpgInit(void) { npcpath_t *path = &npc->data.npc.moveData.path; path->positions[0] = (worldpos_t){ 3, 3, 0 }; - path->positions[1] = (worldpos_t){ 5, 3, 0 }; - path->positions[2] = (worldpos_t){ 5, 5, 0 }; - path->positions[3] = (worldpos_t){ 3, 5, 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; // All Good! From 67010592b88ebcbcf993d10d1b1921e60526cbd4 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Fri, 26 Jun 2026 14:41:30 -0500 Subject: [PATCH 4/6] Fixing some performance --- src/dusk/rpg/entity/npc/npcpath.c | 13 ++++++++++--- src/dusk/rpg/overworld/worldpos.h | 8 ++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/dusk/rpg/entity/npc/npcpath.c b/src/dusk/rpg/entity/npc/npcpath.c index 2d8e9a36..f0b02b6c 100644 --- a/src/dusk/rpg/entity/npc/npcpath.c +++ b/src/dusk/rpg/entity/npc/npcpath.c @@ -8,11 +8,13 @@ #include "npc.h" #include "rpg/entity/entity.h" #include "rpg/overworld/worldpos.h" +#include "time/time.h" void npcPathInit(npc_t *npc) { npcpath_t *path = &npc->moveData.path; path->count = 0; path->index = 0; + path->blockedTimer = 0.0f; } void npcPathMovement(entity_t *entity) { @@ -43,9 +45,14 @@ void npcPathMovement(entity_t *entity) { worldpos_t prevPos = entity->position; entityWalk(entity, dir); - // If entityWalk didn't move us (blocked), skip this waypoint to avoid - // getting stuck. if(worldPosIsEqual(entity->position, prevPos)) { - path->index = (path->index + 1) % path->count; + // Blocked - wait before skipping so temporary blockers can move away. + path->blockedTimer += TIME.delta; + if(path->blockedTimer >= NPC_PATH_BLOCKED_TIMEOUT) { + path->index = (path->index + 1) % path->count; + path->blockedTimer = 0.0f; + } + } else { + path->blockedTimer = 0.0f; } } diff --git a/src/dusk/rpg/overworld/worldpos.h b/src/dusk/rpg/overworld/worldpos.h index a4d2d298..41c803a1 100644 --- a/src/dusk/rpg/overworld/worldpos.h +++ b/src/dusk/rpg/overworld/worldpos.h @@ -10,14 +10,14 @@ #define TILE_SIZE_PIXELS 24 -#define CHUNK_WIDTH 32 -#define CHUNK_HEIGHT 32 +#define CHUNK_WIDTH 16 +#define CHUNK_HEIGHT 14 #define CHUNK_DEPTH 8 #define CHUNK_TILE_COUNT (CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH) -#define MAP_CHUNK_WIDTH 2 -#define MAP_CHUNK_HEIGHT 2 +#define MAP_CHUNK_WIDTH 5 +#define MAP_CHUNK_HEIGHT 3 #define MAP_CHUNK_DEPTH 2 #define MAP_CHUNK_COUNT (MAP_CHUNK_WIDTH * MAP_CHUNK_HEIGHT * MAP_CHUNK_DEPTH) From 88aed11d988bec0328c8d0031fe34e5b8f8749b1 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Fri, 26 Jun 2026 14:42:35 -0500 Subject: [PATCH 5/6] Blocked path --- src/dusk/rpg/entity/npc/npcpath.c | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/dusk/rpg/entity/npc/npcpath.c b/src/dusk/rpg/entity/npc/npcpath.c index f0b02b6c..e6f235b8 100644 --- a/src/dusk/rpg/entity/npc/npcpath.c +++ b/src/dusk/rpg/entity/npc/npcpath.c @@ -8,13 +8,11 @@ #include "npc.h" #include "rpg/entity/entity.h" #include "rpg/overworld/worldpos.h" -#include "time/time.h" void npcPathInit(npc_t *npc) { npcpath_t *path = &npc->moveData.path; path->count = 0; path->index = 0; - path->blockedTimer = 0.0f; } void npcPathMovement(entity_t *entity) { @@ -42,17 +40,5 @@ void npcPathMovement(entity_t *entity) { dir = pos.z < target->z ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH; } - worldpos_t prevPos = entity->position; entityWalk(entity, dir); - - if(worldPosIsEqual(entity->position, prevPos)) { - // Blocked - wait before skipping so temporary blockers can move away. - path->blockedTimer += TIME.delta; - if(path->blockedTimer >= NPC_PATH_BLOCKED_TIMEOUT) { - path->index = (path->index + 1) % path->count; - path->blockedTimer = 0.0f; - } - } else { - path->blockedTimer = 0.0f; - } } From 8181a28557c629fe2e254d0331ca0aadf5c91785 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Fri, 26 Jun 2026 19:42:34 -0500 Subject: [PATCH 6/6] Bunch of stuff done --- assets/chunks/0_0_0.dcf | Bin 0 -> 63500 bytes src/dusk/asset/loader/CMakeLists.txt | 3 +- src/dusk/asset/loader/assetloader.c | 6 + src/dusk/asset/loader/assetloader.h | 5 + src/dusk/asset/loader/chunk/CMakeLists.txt | 10 ++ .../asset/loader/chunk/assetchunkloader.c | 115 ++++++++++++++++++ .../asset/loader/chunk/assetchunkloader.h | 65 ++++++++++ src/dusk/rpg/entity/CMakeLists.txt | 4 +- src/dusk/rpg/entity/anim/CMakeLists.txt | 13 ++ src/dusk/rpg/entity/anim/entityanim.c | 27 ++++ src/dusk/rpg/entity/anim/entityanim.h | 37 ++++++ src/dusk/rpg/entity/anim/entityanimidle.c | 14 +++ src/dusk/rpg/entity/anim/entityanimidle.h | 18 +++ src/dusk/rpg/entity/anim/entityanimrun.c | 21 ++++ src/dusk/rpg/entity/anim/entityanimrun.h | 21 ++++ src/dusk/rpg/entity/anim/entityanimturn.c | 14 +++ src/dusk/rpg/entity/anim/entityanimturn.h | 21 ++++ src/dusk/rpg/entity/anim/entityanimwalk.c | 21 ++++ src/dusk/rpg/entity/anim/entityanimwalk.h | 21 ++++ src/dusk/rpg/entity/entity.c | 30 +++++ src/dusk/rpg/entity/entity.h | 19 ++- src/dusk/rpg/entity/entityanim.c | 41 ------- src/dusk/rpg/entity/entityanim.h | 29 ----- src/dusk/rpg/entity/interact/CMakeLists.txt | 9 ++ .../entity/{ => interact}/entityinteract.c | 2 +- .../entity/{ => interact}/entityinteract.h | 0 src/dusk/rpg/overworld/CMakeLists.txt | 3 +- src/dusk/rpg/overworld/chunk.h | 1 - src/dusk/rpg/overworld/map.c | 103 +++++++--------- src/dusk/rpg/overworld/worldpos.h | 6 +- src/dusk/rpg/rpg.c | 12 ++ src/dusk/scene/overworld/sceneoverworld.c | 24 +++- 32 files changed, 567 insertions(+), 148 deletions(-) create mode 100644 assets/chunks/0_0_0.dcf create mode 100644 src/dusk/asset/loader/chunk/CMakeLists.txt create mode 100644 src/dusk/asset/loader/chunk/assetchunkloader.c create mode 100644 src/dusk/asset/loader/chunk/assetchunkloader.h create mode 100644 src/dusk/rpg/entity/anim/CMakeLists.txt create mode 100644 src/dusk/rpg/entity/anim/entityanim.c create mode 100644 src/dusk/rpg/entity/anim/entityanim.h create mode 100644 src/dusk/rpg/entity/anim/entityanimidle.c create mode 100644 src/dusk/rpg/entity/anim/entityanimidle.h create mode 100644 src/dusk/rpg/entity/anim/entityanimrun.c create mode 100644 src/dusk/rpg/entity/anim/entityanimrun.h create mode 100644 src/dusk/rpg/entity/anim/entityanimturn.c create mode 100644 src/dusk/rpg/entity/anim/entityanimturn.h create mode 100644 src/dusk/rpg/entity/anim/entityanimwalk.c create mode 100644 src/dusk/rpg/entity/anim/entityanimwalk.h delete mode 100644 src/dusk/rpg/entity/entityanim.c delete mode 100644 src/dusk/rpg/entity/entityanim.h create mode 100644 src/dusk/rpg/entity/interact/CMakeLists.txt rename src/dusk/rpg/entity/{ => interact}/entityinteract.c (97%) rename src/dusk/rpg/entity/{ => interact}/entityinteract.h (100%) diff --git a/assets/chunks/0_0_0.dcf b/assets/chunks/0_0_0.dcf new file mode 100644 index 0000000000000000000000000000000000000000..7e7f4803f22dbe82792fc1c203e628610ddc65bc GIT binary patch literal 63500 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 literal 0 HcmV?d00001 diff --git a/src/dusk/asset/loader/CMakeLists.txt b/src/dusk/asset/loader/CMakeLists.txt index 9bca3019..556ccd17 100644 --- a/src/dusk/asset/loader/CMakeLists.txt +++ b/src/dusk/asset/loader/CMakeLists.txt @@ -14,4 +14,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} # Subdirs add_subdirectory(display) add_subdirectory(locale) -add_subdirectory(json) \ No newline at end of file +add_subdirectory(json) +add_subdirectory(chunk) \ No newline at end of file diff --git a/src/dusk/asset/loader/assetloader.c b/src/dusk/asset/loader/assetloader.c index 5987246f..902731b6 100644 --- a/src/dusk/asset/loader/assetloader.c +++ b/src/dusk/asset/loader/assetloader.c @@ -39,4 +39,10 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = { .loadAsync = assetJsonLoaderAsync, .dispose = assetJsonDispose }, + + [ASSET_LOADER_TYPE_CHUNK] = { + .loadSync = assetChunkLoaderSync, + .loadAsync = assetChunkLoaderAsync, + .dispose = assetChunkDispose + }, }; diff --git a/src/dusk/asset/loader/assetloader.h b/src/dusk/asset/loader/assetloader.h index 8324581d..61ba9696 100644 --- a/src/dusk/asset/loader/assetloader.h +++ b/src/dusk/asset/loader/assetloader.h @@ -11,6 +11,7 @@ #include "asset/loader/display/assettilesetloader.h" #include "asset/loader/locale/assetlocaleloader.h" #include "asset/loader/json/assetjsonloader.h" +#include "asset/loader/chunk/assetchunkloader.h" typedef enum { ASSET_LOADER_TYPE_NULL, @@ -20,6 +21,7 @@ typedef enum { ASSET_LOADER_TYPE_TILESET, ASSET_LOADER_TYPE_LOCALE, ASSET_LOADER_TYPE_JSON, + ASSET_LOADER_TYPE_CHUNK, ASSET_LOADER_TYPE_COUNT } assetloadertype_t; @@ -30,6 +32,7 @@ typedef union { assettilesetloaderinput_t tileset; assetlocaleloaderinput_t locale; assetjsonloaderinput_t json; + assetchunkloaderinput_t chunk; } assetloaderinput_t; typedef union { @@ -38,6 +41,7 @@ typedef union { assettilesetloaderloading_t tileset; assetlocaleloaderloading_t locale; assetjsonloaderloading_t json; + assetchunkloaderloading_t chunk; } assetloaderloading_t; typedef union { @@ -46,6 +50,7 @@ typedef union { assettilesetoutput_t tileset; assetlocaleoutput_t locale; assetjsonoutput_t json; + assetchunkoutput_t chunk; } assetloaderoutput_t; typedef struct assetloading_s assetloading_t; diff --git a/src/dusk/asset/loader/chunk/CMakeLists.txt b/src/dusk/asset/loader/chunk/CMakeLists.txt new file mode 100644 index 00000000..64e16290 --- /dev/null +++ b/src/dusk/asset/loader/chunk/CMakeLists.txt @@ -0,0 +1,10 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + assetchunkloader.c +) diff --git a/src/dusk/asset/loader/chunk/assetchunkloader.c b/src/dusk/asset/loader/chunk/assetchunkloader.c new file mode 100644 index 00000000..7b1a09ce --- /dev/null +++ b/src/dusk/asset/loader/chunk/assetchunkloader.c @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "assetchunkloader.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/endian.h" +#include "asset/loader/assetloading.h" +#include "asset/loader/assetentry.h" + +errorret_t assetChunkLoaderAsync(assetloading_t *loading) { + assertNotNull(loading, "Loading cannot be NULL"); + assertNotMainThread("Should be called from an async thread."); + + if(loading->loading.chunk.state != ASSET_CHUNK_LOADING_STATE_READ_FILE) { + errorOk(); + } + + assertNull(loading->loading.chunk.data, "Data already defined?"); + + assetfile_t *file = &loading->loading.chunk.file; + assetLoaderErrorChain(loading, + assetFileInit(file, loading->entry->name, NULL, NULL) + ); + + uint8_t *data = memoryAllocate(file->size); + assetLoaderErrorChain(loading, assetFileOpen(file)); + assetLoaderErrorChain(loading, assetFileRead(file, data, file->size)); + assetLoaderErrorChain(loading, assetFileClose(file)); + assetLoaderErrorChain(loading, assetFileDispose(file)); + assertTrue( + file->lastRead == file->size, + "Failed to read entire chunk file." + ); + + loading->loading.chunk.data = data; + loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_PARSE; + loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; + errorOk(); +} + +errorret_t assetChunkLoaderSync(assetloading_t *loading) { + assertNotNull(loading, "Loading cannot be NULL"); + assertTrue(loading->type == ASSET_LOADER_TYPE_CHUNK, "Invalid type."); + assertIsMainThread("Must be called from the main thread."); + + switch(loading->loading.chunk.state) { + case ASSET_CHUNK_LOADING_STATE_INITIAL: + loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_READ_FILE; + loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC; + errorOk(); + break; + + case ASSET_CHUNK_LOADING_STATE_PARSE: + break; + + default: + errorOk(); + } + + uint8_t *data = loading->loading.chunk.data; + assertNotNull(data, "Chunk data should have been loaded by now."); + + if(data[0] != 'D' || data[1] != 'C' || data[2] != 'F') { + memoryFree(data); + assetLoaderErrorThrow(loading, "Invalid chunk file header"); + } + + uint32_t version = endianLittleToHost32(*(uint32_t *)(data + 4)); + if(version != ASSET_CHUNK_FILE_VERSION) { + memoryFree(data); + assetLoaderErrorThrow( + loading, "Unsupported chunk file version %u", version + ); + } + + assetchunkoutput_t *out = &loading->entry->data.chunk; + size_t offset = 8; + + size_t tileSize = CHUNK_TILE_COUNT * sizeof(tile_t); + memoryCopy(out->tiles, data + offset, tileSize); + offset += tileSize; + + out->vertCount = endianLittleToHost32(*(uint32_t *)(data + offset)); + offset += sizeof(uint32_t); + + assertTrue( + out->vertCount <= CHUNK_VERTEX_COUNT, + "Chunk vertex count exceeds maximum." + ); + + memoryCopy( + out->vertices, + data + offset, + out->vertCount * sizeof(meshvertex_t) + ); + + memoryFree(data); + loading->loading.chunk.data = NULL; + + loading->entry->state = ASSET_ENTRY_STATE_LOADED; + errorOk(); +} + +errorret_t assetChunkDispose(assetentry_t *entry) { + assertNotNull(entry, "Entry cannot be NULL"); + assertTrue(entry->type == ASSET_LOADER_TYPE_CHUNK, "Invalid type."); + assertIsMainThread("Must be called from the main thread."); + + errorOk(); +} diff --git a/src/dusk/asset/loader/chunk/assetchunkloader.h b/src/dusk/asset/loader/chunk/assetchunkloader.h new file mode 100644 index 00000000..44e7636f --- /dev/null +++ b/src/dusk/asset/loader/chunk/assetchunkloader.h @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "asset/assetfile.h" +#include "rpg/overworld/chunk.h" + +#define ASSET_CHUNK_FILE_VERSION 1 + +typedef struct assetloading_s assetloading_t; +typedef struct assetentry_s assetentry_t; + +typedef struct { + void *nothing; +} assetchunkloaderinput_t; + +typedef enum { + ASSET_CHUNK_LOADING_STATE_INITIAL, + ASSET_CHUNK_LOADING_STATE_READ_FILE, + ASSET_CHUNK_LOADING_STATE_PARSE, + ASSET_CHUNK_LOADING_STATE_DONE +} assetchunkloadingstate_t; + +typedef struct { + assetfile_t file; + assetchunkloadingstate_t state; + uint8_t *data; +} assetchunkloaderloading_t; + +typedef struct { + tile_t tiles[CHUNK_TILE_COUNT]; + uint32_t vertCount; + meshvertex_t vertices[CHUNK_VERTEX_COUNT]; +} assetchunkoutput_t; + +/** + * Asynchronous loader for chunk assets. Reads the raw DCF file bytes into + * the loading buffer so the sync phase can parse without blocking the + * main thread on I/O. + * + * @param loading Loading information for the asset being loaded. + * @return Error code indicating success or failure of the load operation. + */ +errorret_t assetChunkLoaderAsync(assetloading_t *loading); + +/** + * Synchronous loader for chunk assets. Validates the DCF binary previously + * read by the async phase and populates the output assetchunkoutput_t. + * + * @param loading Loading information for the asset being loaded. + * @return Error code indicating success or failure of the load operation. + */ +errorret_t assetChunkLoaderSync(assetloading_t *loading); + +/** + * Disposer for chunk assets. + * + * @param entry Asset entry containing the chunk data to dispose. + * @return Error code indicating success or failure of the dispose operation. + */ +errorret_t assetChunkDispose(assetentry_t *entry); diff --git a/src/dusk/rpg/entity/CMakeLists.txt b/src/dusk/rpg/entity/CMakeLists.txt index 8a249fd4..0cf1f321 100644 --- a/src/dusk/rpg/entity/CMakeLists.txt +++ b/src/dusk/rpg/entity/CMakeLists.txt @@ -7,10 +7,10 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC entity.c - entityanim.c entitydir.c - entityinteract.c player.c ) +add_subdirectory(anim) +add_subdirectory(interact) add_subdirectory(npc) \ No newline at end of file diff --git a/src/dusk/rpg/entity/anim/CMakeLists.txt b/src/dusk/rpg/entity/anim/CMakeLists.txt new file mode 100644 index 00000000..54c1015e --- /dev/null +++ b/src/dusk/rpg/entity/anim/CMakeLists.txt @@ -0,0 +1,13 @@ +# 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 +) diff --git a/src/dusk/rpg/entity/anim/entityanim.c b/src/dusk/rpg/entity/anim/entityanim.c new file mode 100644 index 00000000..1d472662 --- /dev/null +++ b/src/dusk/rpg/entity/anim/entityanim.c @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/entity/entity.h" +#include "time/time.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 }, +}; + +void entityAnimUpdate(entity_t *entity) { + if(entity->animation != ENTITY_ANIM_IDLE) { + entity->animTime -= TIME.delta; + if(entity->animTime <= 0) { + entity->animation = ENTITY_ANIM_IDLE; + entity->animTime = 0; + } + } + ENTITY_ANIM_CALLBACKS[entity->animation].update(entity); +} diff --git a/src/dusk/rpg/entity/anim/entityanim.h b/src/dusk/rpg/entity/anim/entityanim.h new file mode 100644 index 00000000..548c289f --- /dev/null +++ b/src/dusk/rpg/entity/anim/entityanim.h @@ -0,0 +1,37 @@ +/** + * 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); diff --git a/src/dusk/rpg/entity/anim/entityanimidle.c b/src/dusk/rpg/entity/anim/entityanimidle.c new file mode 100644 index 00000000..53836258 --- /dev/null +++ b/src/dusk/rpg/entity/anim/entityanimidle.c @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/entity/entity.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; +} diff --git a/src/dusk/rpg/entity/anim/entityanimidle.h b/src/dusk/rpg/entity/anim/entityanimidle.h new file mode 100644 index 00000000..b7bd9a53 --- /dev/null +++ b/src/dusk/rpg/entity/anim/entityanimidle.h @@ -0,0 +1,18 @@ +/** + * 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); diff --git a/src/dusk/rpg/entity/anim/entityanimrun.c b/src/dusk/rpg/entity/anim/entityanimrun.c new file mode 100644 index 00000000..f3ce19d8 --- /dev/null +++ b/src/dusk/rpg/entity/anim/entityanimrun.c @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/entity/entity.h" + +void entityAnimRunUpdate(entity_t *entity) { + float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_RUN_DURATION); + 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] = (float_t)entity->lastPosition.z + t * ( + (float_t)entity->position.z - (float_t)entity->lastPosition.z + ); +} diff --git a/src/dusk/rpg/entity/anim/entityanimrun.h b/src/dusk/rpg/entity/anim/entityanimrun.h new file mode 100644 index 00000000..ecace434 --- /dev/null +++ b/src/dusk/rpg/entity/anim/entityanimrun.h @@ -0,0 +1,21 @@ +/** + * 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); diff --git a/src/dusk/rpg/entity/anim/entityanimturn.c b/src/dusk/rpg/entity/anim/entityanimturn.c new file mode 100644 index 00000000..feab189b --- /dev/null +++ b/src/dusk/rpg/entity/anim/entityanimturn.c @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/entity/entity.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; +} diff --git a/src/dusk/rpg/entity/anim/entityanimturn.h b/src/dusk/rpg/entity/anim/entityanimturn.h new file mode 100644 index 00000000..aa765dfd --- /dev/null +++ b/src/dusk/rpg/entity/anim/entityanimturn.h @@ -0,0 +1,21 @@ +/** + * 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(2) + +/** + * Updates render position for the turn animation state. + * + * @param entity Pointer to the entity to update. + */ +void entityAnimTurnUpdate(entity_t *entity); diff --git a/src/dusk/rpg/entity/anim/entityanimwalk.c b/src/dusk/rpg/entity/anim/entityanimwalk.c new file mode 100644 index 00000000..e5af80ff --- /dev/null +++ b/src/dusk/rpg/entity/anim/entityanimwalk.c @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg/entity/entity.h" + +void entityAnimWalkUpdate(entity_t *entity) { + float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_WALK_DURATION); + 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] = (float_t)entity->lastPosition.z + t * ( + (float_t)entity->position.z - (float_t)entity->lastPosition.z + ); +} diff --git a/src/dusk/rpg/entity/anim/entityanimwalk.h b/src/dusk/rpg/entity/anim/entityanimwalk.h new file mode 100644 index 00000000..608a46ff --- /dev/null +++ b/src/dusk/rpg/entity/anim/entityanimwalk.h @@ -0,0 +1,21 @@ +/** + * 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(10) + +/** + * Updates render position for the walk animation state. + * + * @param entity Pointer to the entity to update. + */ +void entityAnimWalkUpdate(entity_t *entity); diff --git a/src/dusk/rpg/entity/entity.c b/src/dusk/rpg/entity/entity.c index 85d00fb6..af4d272a 100644 --- a/src/dusk/rpg/entity/entity.c +++ b/src/dusk/rpg/entity/entity.c @@ -12,6 +12,7 @@ #include "util/math.h" #include "rpg/cutscene/cutscenemode.h" #include "rpg/overworld/map.h" +#include "rpg/overworld/chunk.h" #include "rpg/overworld/tile.h" entity_t ENTITIES[ENTITY_COUNT]; @@ -28,6 +29,7 @@ void entityInit(entity_t *entity, const entitytype_t type) { memoryZero(entity, sizeof(entity_t)); entity->id = (uint8_t)(entity - ENTITIES); entity->type = type; + entity->chunkIndex = 0xFF; if(ENTITY_CALLBACKS[type].init != NULL) ENTITY_CALLBACKS[type].init(entity); } @@ -216,4 +218,32 @@ uint8_t entityGetAvailable() { } while(++ent, ent < &ENTITIES[ENTITY_COUNT]); return 0xFF; +} + +void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) { + assertNotNull(entity, "Entity pointer cannot be NULL"); + + if(entity->chunkIndex != 0xFF) { + chunk_t *old = mapGetChunk(entity->chunkIndex); + if(old != NULL) { + for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) { + if(old->entities[i] != entity->id) continue; + old->entities[i] = 0xFF; + break; + } + } + } + + entity->chunkIndex = chunkIndex; + + if(chunkIndex != 0xFF) { + chunk_t *next = mapGetChunk(chunkIndex); + if(next != NULL) { + for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) { + if(next->entities[i] != 0xFF) continue; + next->entities[i] = entity->id; + break; + } + } + } } \ No newline at end of file diff --git a/src/dusk/rpg/entity/entity.h b/src/dusk/rpg/entity/entity.h index 544e341c..6e8f9085 100644 --- a/src/dusk/rpg/entity/entity.h +++ b/src/dusk/rpg/entity/entity.h @@ -7,8 +7,8 @@ #pragma once #include "entitydir.h" -#include "entityanim.h" -#include "entityinteract.h" +#include "anim/entityanim.h" +#include "interact/entityinteract.h" #include "entitytype.h" #include "npc/npc.h" @@ -29,6 +29,8 @@ typedef struct entity_s { float_t animTime; entityinteract_t interact; + + uint8_t chunkIndex; } entity_t; extern entity_t ENTITIES[ENTITY_COUNT]; @@ -107,7 +109,16 @@ entity_t *entityGetAt(const worldpos_t pos); /** * Gets an available entity index. - * + * * @return The index of an available entity, or 0xFF if none are available. */ -uint8_t entityGetAvailable(); \ No newline at end of file +uint8_t entityGetAvailable(); + +/** + * Assigns an entity to a chunk, removing it from its current chunk first. + * Pass 0xFF as chunkIndex to detach the entity from any chunk. + * + * @param entity Pointer to the entity. + * @param chunkIndex Index of the chunk to assign to, or 0xFF for none. + */ +void entitySetChunk(entity_t *entity, const uint8_t chunkIndex); \ No newline at end of file diff --git a/src/dusk/rpg/entity/entityanim.c b/src/dusk/rpg/entity/entityanim.c deleted file mode 100644 index 8882a222..00000000 --- a/src/dusk/rpg/entity/entityanim.c +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "entity.h" -#include "time/time.h" - -void entityAnimUpdate(entity_t *entity) { - if(entity->animation != ENTITY_ANIM_IDLE) { - entity->animTime -= TIME.delta; - if(entity->animTime <= 0) { - entity->animation = ENTITY_ANIM_IDLE; - entity->animTime = 0; - } - } - - if( - entity->animation == ENTITY_ANIM_WALK || - entity->animation == ENTITY_ANIM_RUN - ) { - float_t duration = entity->animation == ENTITY_ANIM_WALK ? - ENTITY_ANIM_WALK_DURATION : ENTITY_ANIM_RUN_DURATION; - float_t t = 1.0f - (entity->animTime / duration); - 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] = (float_t)entity->lastPosition.z + t * ( - (float_t)entity->position.z - (float_t)entity->lastPosition.z - ); - } else { - entity->renderPosition[0] = (float_t)entity->position.x; - entity->renderPosition[1] = (float_t)entity->position.y; - entity->renderPosition[2] = (float_t)entity->position.z; - } -} \ No newline at end of file diff --git a/src/dusk/rpg/entity/entityanim.h b/src/dusk/rpg/entity/entityanim.h deleted file mode 100644 index c822d605..00000000 --- a/src/dusk/rpg/entity/entityanim.h +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "time/time.h" - -#define ENTITY_ANIM_TURN_DURATION TIME_TICKS_TO_TIME(2) -#define ENTITY_ANIM_WALK_DURATION TIME_TICKS_TO_TIME(12) -#define ENTITY_ANIM_RUN_DURATION TIME_TICKS_TO_TIME(6) - -typedef struct entity_s entity_t; - -typedef enum { - ENTITY_ANIM_IDLE, - ENTITY_ANIM_TURN, - ENTITY_ANIM_WALK, - ENTITY_ANIM_RUN, -} entityanim_t; - -/** - * Updates the entity's animation state. - * - * @param entity Pointer to the entity to update. - */ -void entityAnimUpdate(entity_t *entity); \ No newline at end of file diff --git a/src/dusk/rpg/entity/interact/CMakeLists.txt b/src/dusk/rpg/entity/interact/CMakeLists.txt new file mode 100644 index 00000000..f7e07e35 --- /dev/null +++ b/src/dusk/rpg/entity/interact/CMakeLists.txt @@ -0,0 +1,9 @@ +# 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 + entityinteract.c +) diff --git a/src/dusk/rpg/entity/entityinteract.c b/src/dusk/rpg/entity/interact/entityinteract.c similarity index 97% rename from src/dusk/rpg/entity/entityinteract.c rename to src/dusk/rpg/entity/interact/entityinteract.c index c618ab0c..83a1573d 100644 --- a/src/dusk/rpg/entity/entityinteract.c +++ b/src/dusk/rpg/entity/interact/entityinteract.c @@ -5,7 +5,7 @@ * https://opensource.org/licenses/MIT */ -#include "entity.h" +#include "rpg/entity/entity.h" #include "assert/assert.h" #include "rpg/cutscene/cutscenesystem.h" #include "ui/rpg/uitextboxmain.h" diff --git a/src/dusk/rpg/entity/entityinteract.h b/src/dusk/rpg/entity/interact/entityinteract.h similarity index 100% rename from src/dusk/rpg/entity/entityinteract.h rename to src/dusk/rpg/entity/interact/entityinteract.h diff --git a/src/dusk/rpg/overworld/CMakeLists.txt b/src/dusk/rpg/overworld/CMakeLists.txt index b697bf8a..d5d69e56 100644 --- a/src/dusk/rpg/overworld/CMakeLists.txt +++ b/src/dusk/rpg/overworld/CMakeLists.txt @@ -10,4 +10,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} map.c worldpos.c tile.c -) \ No newline at end of file +) + diff --git a/src/dusk/rpg/overworld/chunk.h b/src/dusk/rpg/overworld/chunk.h index baf82341..c19dbebd 100644 --- a/src/dusk/rpg/overworld/chunk.h +++ b/src/dusk/rpg/overworld/chunk.h @@ -22,7 +22,6 @@ typedef struct chunk_s { meshvertex_t vertices[CHUNK_VERTEX_COUNT]; uint32_t vertCount; mesh_t mesh; - color_t testColor; // uint8_t meshCount; // meshvertex_t vertices[CHUNK_VERTEX_COUNT_MAX]; diff --git a/src/dusk/rpg/overworld/map.c b/src/dusk/rpg/overworld/map.c index 78d78ca0..806342cd 100644 --- a/src/dusk/rpg/overworld/map.c +++ b/src/dusk/rpg/overworld/map.c @@ -202,84 +202,65 @@ errorret_t mapDispose() { } void mapChunkUnload(chunk_t* chunk) { + uint8_t chunkIndex = (uint8_t)(chunk - MAP.chunks); for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) { - if(chunk->entities[i] == 0xFF) break; + if(chunk->entities[i] == 0xFF) continue; entity_t *entity = &ENTITIES[chunk->entities[i]]; - entity->type = ENTITY_TYPE_NULL; + assertTrue( + entity->chunkIndex == chunkIndex, + "Entity chunk index does not match chunk" + ); + if(entity->type == ENTITY_TYPE_PLAYER) { + entitySetChunk(entity, 0xFF); + } else { + entity->type = ENTITY_TYPE_NULL; + } } chunk->vertCount = 0; } errorret_t mapChunkLoad(chunk_t* chunk) { if(!mapIsLoaded()) errorThrow("No map loaded"); - - color_t color = COLOR_WHITE; - if(chunk->position.y % 2 == 0) { - if(chunk->position.x % 2 == 0) { - color = COLOR_BLACK; - } else { - color = COLOR_WHITE; - } - } else { - if(chunk->position.x % 2 == 0) { - color = COLOR_WHITE; - } else { - color = COLOR_BLACK; - } - } - // if(chunk->position.x == 0 && chunk->position.y == 0 && chunk->position.z == 0) { - // color = COLOR_RED; - // } - chunk->testColor = color; - - memorySet(chunk->tiles, TILE_SHAPE_GROUND, sizeof(chunk->tiles)); + memorySet(chunk->entities, 0xFF, sizeof(chunk->entities)); chunk->vertCount = 0; - if(chunk->position.z != 0) { + char_t name[64]; + stringFormat( + name, sizeof(name), + "chunks/%d_%d_%d.dcf", + (int32_t)chunk->position.x, + (int32_t)chunk->position.y, + (int32_t)chunk->position.z + ); + + if(!assetFileExists(name)) { + memorySet(chunk->tiles, TILE_SHAPE_GROUND, sizeof(chunk->tiles)); errorOk(); } - // Set Chunk sprites. - vec3 spriteMin = { - chunk->position.x * CHUNK_WIDTH, - chunk->position.y * CHUNK_HEIGHT, - chunk->position.z * CHUNK_DEPTH - }; - - spritebatchsprite_t sprites[CHUNK_TILE_COUNT]; - uint32_t i = 0; - for(uint8_t x = 0; x < CHUNK_WIDTH; x++) { - for(uint8_t y = 0; y < CHUNK_HEIGHT; y++) { - glm_vec3_copy(spriteMin, sprites[i].min); - glm_vec3_add( - sprites[i].min, - (vec3){ x, y, 0 }, - sprites[i].min - ); + assetentry_t *entry = assetLock(name, ASSET_LOADER_TYPE_CHUNK, NULL); + assertNotNull(entry, "Failed to get chunk asset entry"); - glm_vec3_copy(sprites[i].min, sprites[i].max); - glm_vec3_add( - sprites[i].max, - (vec3){ 1, 1, 0 }, - sprites[i].max - ); - - glm_vec2_copy((vec2){ 0, 0 }, sprites[i].uvMin); - glm_vec2_copy((vec2){ 1, 1 }, sprites[i].uvMax); - - i++; - } + errorret_t ret = assetRequireLoaded(entry); + if(errorIsNotOk(ret)) { + assetUnlockEntry(entry); + return ret; } - chunk->vertCount = i * QUAD_VERTEX_COUNT; - spriteBatchBufferToMesh( - sprites, - i, - chunk->vertices, - chunk->vertCount - ); - errorChain(meshFlush(&chunk->mesh, 0, chunk->vertCount)); + memoryCopy( + chunk->tiles, entry->data.chunk.tiles, sizeof(chunk->tiles) + ); + chunk->vertCount = entry->data.chunk.vertCount; + memoryCopy( + chunk->vertices, + entry->data.chunk.vertices, + chunk->vertCount * sizeof(meshvertex_t) + ); + assetUnlockEntry(entry); + + if(chunk->vertCount == 0) errorOk(); + errorChain(meshFlush(&chunk->mesh, 0, chunk->vertCount)); errorOk(); } diff --git a/src/dusk/rpg/overworld/worldpos.h b/src/dusk/rpg/overworld/worldpos.h index 41c803a1..30430a80 100644 --- a/src/dusk/rpg/overworld/worldpos.h +++ b/src/dusk/rpg/overworld/worldpos.h @@ -11,14 +11,14 @@ #define TILE_SIZE_PIXELS 24 #define CHUNK_WIDTH 16 -#define CHUNK_HEIGHT 14 -#define CHUNK_DEPTH 8 +#define CHUNK_HEIGHT 16 +#define CHUNK_DEPTH 32 #define CHUNK_TILE_COUNT (CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH) #define MAP_CHUNK_WIDTH 5 #define MAP_CHUNK_HEIGHT 3 -#define MAP_CHUNK_DEPTH 2 +#define MAP_CHUNK_DEPTH 1 #define MAP_CHUNK_COUNT (MAP_CHUNK_WIDTH * MAP_CHUNK_HEIGHT * MAP_CHUNK_DEPTH) #define ENTITY_COUNT 32 diff --git a/src/dusk/rpg/rpg.c b/src/dusk/rpg/rpg.c index 128902fc..fb4d9a42 100644 --- a/src/dusk/rpg/rpg.c +++ b/src/dusk/rpg/rpg.c @@ -37,6 +37,12 @@ errorret_t rpgInit(void) { entityInit(ent, ENTITY_TYPE_PLAYER); RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY; RPG_CAMERA.followEntity.followEntityId = ent->id; + { + chunkpos_t cp; + worldPosToChunkPos(&ent->position, &cp); + chunkindex_t ci = mapGetChunkIndexAt(cp); + if(ci != -1) entitySetChunk(ent, (uint8_t)ci); + } uint8_t npcIndex = entityGetAvailable(); entity_t *npc = &ENTITIES[npcIndex]; @@ -45,6 +51,12 @@ errorret_t rpgInit(void) { npc->position = (worldpos_t){ 3, 3, 0 }; npc->interact.type = ENTITY_INTERACT_PRINT; npc->interact.data.message = "hello world"; + { + chunkpos_t cp; + worldPosToChunkPos(&npc->position, &cp); + chunkindex_t ci = mapGetChunkIndexAt(cp); + if(ci != -1) entitySetChunk(npc, (uint8_t)ci); + } npcpath_t *path = &npc->data.npc.moveData.path; path->positions[0] = (worldpos_t){ 3, 3, 0 }; diff --git a/src/dusk/scene/overworld/sceneoverworld.c b/src/dusk/scene/overworld/sceneoverworld.c index 064a4748..3c5156f1 100644 --- a/src/dusk/scene/overworld/sceneoverworld.c +++ b/src/dusk/scene/overworld/sceneoverworld.c @@ -13,15 +13,32 @@ #include "display/screen/screen.h" #include "display/shader/shaderunlit.h" #include "display/spritebatch/spritebatch.h" +#include "display/texture/texture.h" #include "rpg/overworld/map.h" #include "rpg/entity/entity.h" #include "rpg/rpgcamera.h" +#define TEXTURE_CHUNK_SIZE 16 + +static texture_t TEXTURE_CHUNK; +static color_t TEXTURE_CHUNK_PIXELS[TEXTURE_CHUNK_SIZE * TEXTURE_CHUNK_SIZE]; + errorret_t sceneOverworldInit(scenedata_t *sceneData) { assertNotNull(sceneData, "Scene data cannot be null"); - + for(uint32_t i = 0; i < TEXTURE_CHUNK_SIZE * TEXTURE_CHUNK_SIZE; i++) { + uint8_t r = (uint8_t)((i & 7) * 36); + uint8_t g = (uint8_t)(((i >> 3) & 7) * 36); + uint8_t b = (uint8_t)((i >> 6) * 85); + TEXTURE_CHUNK_PIXELS[i] = color4b(r, g, b, 255); + } + errorChain(textureInit( + &TEXTURE_CHUNK, + TEXTURE_CHUNK_SIZE, TEXTURE_CHUNK_SIZE, + TEXTURE_FORMAT_RGBA, + (texturedata_t){ .rgbaColors = TEXTURE_CHUNK_PIXELS } + )); errorOk(); } @@ -84,7 +101,7 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) { shadermaterial_t chunkMaterial = { .unlit = { .color = COLOR_WHITE, - .texture = NULL + .texture = &TEXTURE_CHUNK } }; @@ -98,7 +115,6 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) { continue; } - chunkMaterial.unlit.color = chunk->testColor; errorChain(shaderSetMaterial(&SHADER_UNLIT, &chunkMaterial)); errorChain(meshDraw(&chunk->mesh, 0, chunk->vertCount)); i++; @@ -143,7 +159,7 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) { errorret_t sceneOverworldDispose(scenedata_t *sceneData) { assertNotNull(sceneData, "Scene data cannot be null"); - + errorChain(textureDispose(&TEXTURE_CHUNK)); errorOk(); } \ No newline at end of file