diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0d76f8c..1dcfc54 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -36,7 +36,7 @@ add_subdirectory(error) add_subdirectory(input) # add_subdirectory(locale) add_subdirectory(physics) -# add_subdirectory(rpg) +add_subdirectory(rpg) add_subdirectory(scene) add_subdirectory(thread) add_subdirectory(time) diff --git a/src/asset/assetmanager.h b/src/asset/assetmanager.h index 14984fb..d38fbd0 100644 --- a/src/asset/assetmanager.h +++ b/src/asset/assetmanager.h @@ -29,7 +29,6 @@ static const char_t ASSET_MANAGER_SEARCH_PATHS[][FILENAME_MAX] = { ) typedef struct { - int32_t nothing; zip_t *zip; asset_t assets[ASSET_MANAGER_ASSET_COUNT_MAX]; uint8_t assetCount; diff --git a/src/console/cmd/cmdscene.h b/src/console/cmd/cmdscene.h index db8c81d..c729e43 100644 --- a/src/console/cmd/cmdscene.h +++ b/src/console/cmd/cmdscene.h @@ -15,6 +15,11 @@ void cmdScene(const consolecmdexec_t *exec) { consolePrint("Usage: scene "); return; } + + if(strcmp(exec->argv[0], "null") == 0) { + sceneManagerSetScene(NULL); + return; + } scene_t *scene = sceneManagerGetSceneByName(exec->argv[0]); if(scene == NULL) { @@ -22,17 +27,16 @@ void cmdScene(const consolecmdexec_t *exec) { return; } - if((scene->flags & SCENE_FLAG_INITIALIZED) == 0) { - if(scene->init) { - errorret_t ret = errorPrint(scene->init(&SCENE_MANAGER.sceneData)); - if(ret.code != ERROR_OK) { - errorCatch(ret); - consolePrint("Error: Failed to initialize scene '%s'.", exec->argv[0]); - return; - } - } - scene->flags |= SCENE_FLAG_INITIALIZED; - } - sceneManagerSetScene(scene); + + if(scene->init) { + errorret_t ret = errorPrint(scene->init(&SCENE_MANAGER.sceneData)); + if(ret.code != ERROR_OK) { + errorCatch(ret); + sceneManagerSetScene(NULL); + consolePrint("Error: Failed to initialize scene '%s'.", exec->argv[0]); + return; + } + } + scene->flags |= SCENE_FLAG_INITIALIZED; } \ No newline at end of file diff --git a/src/engine/engine.c b/src/engine/engine.c index d83c7bb..9a9fe8b 100644 --- a/src/engine/engine.c +++ b/src/engine/engine.c @@ -13,6 +13,7 @@ #include "display/display.h" #include "scene/scenemanager.h" #include "asset/assetmanager.h" +#include "rpg/rpg.h" engine_t ENGINE; @@ -29,6 +30,7 @@ errorret_t engineInit(void) { inputInit(); errorChain(assetManagerInit()); errorChain(displayInit()); + errorChain(rpgInit()); errorChain(sceneManagerInit()); // Init scripts @@ -47,6 +49,7 @@ errorret_t engineUpdate(void) { consoleUpdate(); assetManagerUpdate(); + rpgUpdate(); sceneManagerUpdate(); errorChain(displayUpdate()); @@ -59,6 +62,7 @@ void engineExit(void) { errorret_t engineDispose(void) { sceneManagerDispose(); + rpgDispose(); errorChain(displayDispose()); assetManagerDispose(); consoleDispose(); diff --git a/src/rpg/CMakeLists.txt b/src/rpg/CMakeLists.txt new file mode 100644 index 0000000..b3123df --- /dev/null +++ b/src/rpg/CMakeLists.txt @@ -0,0 +1,13 @@ +# Copyright (c) 2025 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_TARGET_NAME} + PRIVATE + rpg.c +) + +# Subdirs +add_subdirectory(entity) \ No newline at end of file diff --git a/src/rpg/entity/CMakeLists.txt b/src/rpg/entity/CMakeLists.txt new file mode 100644 index 0000000..f1edcfd --- /dev/null +++ b/src/rpg/entity/CMakeLists.txt @@ -0,0 +1,13 @@ +# Copyright (c) 2025 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_TARGET_NAME} + PRIVATE + entity.c + npc.c + player.c + direction.c +) \ No newline at end of file diff --git a/src/rpg/entity/direction.c b/src/rpg/entity/direction.c new file mode 100644 index 0000000..aa04586 --- /dev/null +++ b/src/rpg/entity/direction.c @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "direction.h" +#include "assert/assert.h" + +float_t directionToAngle(const direction_t dir) { + switch(dir) { + case DIRECTION_NORTH: return (M_PI_2); + case DIRECTION_SOUTH: return -(M_PI_2); + case DIRECTION_EAST: return 0; + case DIRECTION_WEST: return (M_PI); + default: return 0; // Should never happen + } +} + +void directionGetVec2(const direction_t dir, vec2 out) { + assertNotNull(out, "Output vector cannot be NULL"); + + switch(dir) { + case DIRECTION_NORTH: + out[0] = 0.0f; + out[1] = 1.0f; + break; + + case DIRECTION_SOUTH: + out[0] = 0.0f; + out[1] = -1.0f; + break; + + case DIRECTION_EAST: + out[0] = 1.0f; + out[1] = 0.0f; + break; + + case DIRECTION_WEST: + out[0] = -1.0f; + out[1] = 0.0f; + break; + + default: + assertUnreachable("Invalid direction"); + } +} \ No newline at end of file diff --git a/src/rpg/entity/direction.h b/src/rpg/entity/direction.h new file mode 100644 index 0000000..54b9c11 --- /dev/null +++ b/src/rpg/entity/direction.h @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef enum { + DIRECTION_SOUTH = 0, + DIRECTION_EAST = 1, + DIRECTION_WEST = 2, + DIRECTION_NORTH = 3, + + DIRECTION_UP = DIRECTION_NORTH, + DIRECTION_DOWN = DIRECTION_SOUTH, + DIRECTION_LEFT = DIRECTION_WEST, + DIRECTION_RIGHT = DIRECTION_EAST, +} direction_t; + +/** + * Converts a direction to an angle in float_t format. + * + * @param dir The direction to convert. + * @return The angle corresponding to the direction. + */ +float_t directionToAngle(const direction_t dir); + +/** + * Converts a direction to a vec2 unit vector. + * + * @param dir The direction to convert. + * @param out Pointer to the vec2 array to populate. + */ +void directionGetVec2(const direction_t dir, vec2 out); \ No newline at end of file diff --git a/src/rpg/entity/entity.c b/src/rpg/entity/entity.c new file mode 100644 index 0000000..c9ab810 --- /dev/null +++ b/src/rpg/entity/entity.c @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "entity.h" +#include "rpg/world/map.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "display/tileset/tileset_entities.h" +#include "time/time.h" +#include "util/math.h" + +void entityInit(entity_t *entity, const entitytype_t type, map_t *map) { + assertNotNull(entity, "Entity pointer cannot be NULL"); + assertNotNull(map, "Map pointer cannot be NULL"); + assertTrue(type < ENTITY_TYPE_COUNT, "Invalid entity type"); + assertTrue(type != ENTITY_TYPE_NULL, "Cannot have NULL entity type"); + + memoryZero(entity, sizeof(entity_t)); + entity->type = type; + entity->map = map; + + // Init. I did use a callback struct but it was not flexible enough. + switch(type) { + case ENTITY_TYPE_PLAYER: + playerInit(entity); + break; + + case ENTITY_TYPE_NPC: + npcInit(entity); + break; + + default: + break; + } +} + +void entityUpdate(entity_t *entity) { + assertNotNull(entity, "Entity pointer cannot be NULL"); + assertTrue(entity->type < ENTITY_TYPE_COUNT, "Invalid entity type"); + assertTrue(entity->type != ENTITY_TYPE_NULL, "Cannot have NULL entity type"); + + // Handle movement logic + switch(entity->type) { + case ENTITY_TYPE_PLAYER: + playerMovement(entity); + break; + + case ENTITY_TYPE_NPC: + npcUpdate(entity); + break; + + default: + break; + } + + // Apply velocity + if(entity->velocity[0] != 0.0f || entity->velocity[1] != 0.0f) { + entity->position[0] += entity->velocity[0] * TIME.delta; + entity->position[1] += entity->velocity[1] * TIME.delta; + + // Hit test on other entities. + entity_t *start = entity->map->entities; + entity_t *end = &entity->map->entities[entity->map->entityCount]; + + // Our hitbox + physicscircle_t self; + glm_vec2_copy(entity->position, self.position); + self.radius = TILESET_ENTITIES.tileWidth / 2.0f; + + physicscircle_t other; + other.radius = self.radius; + + // TODO: what if multiple collisions? + do { + if(start == entity) continue; + if(start->type == ENTITY_TYPE_NULL) continue; + glm_vec2_copy(start->position, other.position); + + physicscirclecircleresult_t result; + physicsCircleCheckCircle(self, other, &result); + + if(result.hit) { + entity->position[0] -= result.normal[0] * result.depth; + entity->position[1] -= result.normal[1] * result.depth; + break; + } + } while((start++) != end); + + // Friction (and dampening) + entity->velocity[0] *= ENTITY_FRICTION * TIME.delta; + entity->velocity[1] *= ENTITY_FRICTION * TIME.delta; + if(mathAbs(entity->velocity[0]) < ENTITY_MIN_VELOCITY) { + entity->velocity[0] = 0.0f; + } + if(mathAbs(entity->velocity[1]) < ENTITY_MIN_VELOCITY) { + entity->velocity[1] = 0.0f; + } + } + + if(entity->type == ENTITY_TYPE_PLAYER) { + playerInteraction(entity); + } +} \ No newline at end of file diff --git a/src/rpg/entity/entity.h b/src/rpg/entity/entity.h new file mode 100644 index 0000000..0015c48 --- /dev/null +++ b/src/rpg/entity/entity.h @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "direction.h" +#include "rpg/entity/player.h" +#include "npc.h" +#include "physics/physics.h" + +#define ENTITY_FRICTION 0.9f +#define ENTITY_MIN_VELOCITY 0.05f + +typedef struct map_s map_t; + +typedef enum { + ENTITY_TYPE_NULL, + ENTITY_TYPE_PLAYER, + ENTITY_TYPE_NPC, + + ENTITY_TYPE_COUNT +} entitytype_t; + +typedef struct entity_s { + map_t *map; + entitytype_t type; + direction_t direction; + + vec2 position; + vec2 velocity; + + union { + player_t player; + npc_t npc; + }; +} entity_t; + +/** + * Initializes an entity structure. + * + * @param entity Pointer to the entity structure to initialize. + * @param type The type of the entity. + * @param map Pointer to the map the entity belongs to. + */ +void entityInit(entity_t *entity, const entitytype_t type, map_t *map); + +/** + * Updates an entity. + * + * @param entity Pointer to the entity structure to update. + */ +void entityUpdate(entity_t *entity); \ No newline at end of file diff --git a/src/rpg/entity/npc.c b/src/rpg/entity/npc.c new file mode 100644 index 0000000..79d21d6 --- /dev/null +++ b/src/rpg/entity/npc.c @@ -0,0 +1,17 @@ +/** + * 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" + +void npcInit(entity_t *entity) { + assertNotNull(entity, "Entity pointer cannot be NULL"); +} + +void npcUpdate(entity_t *entity) { + assertNotNull(entity, "Entity pointer cannot be NULL"); +} \ No newline at end of file diff --git a/src/rpg/entity/npc.h b/src/rpg/entity/npc.h new file mode 100644 index 0000000..b22754f --- /dev/null +++ b/src/rpg/entity/npc.h @@ -0,0 +1,29 @@ +/** + * 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 npcUpdate(entity_t *entity); \ No newline at end of file diff --git a/src/rpg/entity/player.c b/src/rpg/entity/player.c new file mode 100644 index 0000000..9c546c7 --- /dev/null +++ b/src/rpg/entity/player.c @@ -0,0 +1,114 @@ +/** + * 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 "input/input.h" +#include "display/tileset/tileset_entities.h" + +void playerInit(entity_t *entity) { + assertNotNull(entity, "Entity pointer cannot be NULL"); +} + +void playerMovement(entity_t *entity) { + assertNotNull(entity, "Entity pointer cannot be NULL"); + + // Update velocity. + vec2 dir = { + inputAxis(INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT), + inputAxis(INPUT_ACTION_DOWN, INPUT_ACTION_UP) + }; + if(dir[0] == 0 && dir[1] == 0) return; + + glm_vec2_normalize(dir); + entity->velocity[0] += PLAYER_SPEED * dir[0]; + entity->velocity[1] += PLAYER_SPEED * dir[1]; + + // Update direction. + if(dir[0] > 0) { + if(entity->direction == DIRECTION_RIGHT) { + entity->direction = DIRECTION_RIGHT; + } else { + if(dir[1] < 0) { + entity->direction = DIRECTION_UP; + } else if(dir[1] > 0) { + entity->direction = DIRECTION_DOWN; + } else { + entity->direction = DIRECTION_RIGHT; + } + } + } else if(dir[0] < 0) { + if(entity->direction == DIRECTION_LEFT) { + entity->direction = DIRECTION_LEFT; + } else { + if(dir[1] < 0) { + entity->direction = DIRECTION_UP; + } else if(dir[1] > 0) { + entity->direction = DIRECTION_DOWN; + } else { + entity->direction = DIRECTION_LEFT; + } + } + } else if(dir[1] < 0) { + entity->direction = DIRECTION_UP; + } else if(dir[1] > 0) { + entity->direction = DIRECTION_DOWN; + } +} + +void playerInteraction(entity_t *entity) { + assertNotNull(entity, "Entity pointer cannot be NULL"); + + if(!inputPressed(INPUT_ACTION_ACCEPT)) return; + + physicsbox_t interactBox; + + // Get direction vector + directionGetVec2(entity->direction, interactBox.min); + + // Scale by interact range + glm_vec2_scale(interactBox.min, PLAYER_INTERACTION_RANGE, interactBox.min); + + // Add entity position, this makes the center of the box. + glm_vec2_add(interactBox.min, entity->position, interactBox.min); + + // Copy to max + glm_vec2_copy(interactBox.min, interactBox.max); + + // Size of the hitbox + vec2 halfSize = { + TILESET_ENTITIES.tileWidth * PLAYER_INTERACTION_SIZE * 0.5f, + TILESET_ENTITIES.tileHeight * PLAYER_INTERACTION_SIZE * 0.5f + }; + + // Subtract from min, add to max. + glm_vec2_sub(interactBox.min, halfSize, interactBox.min); + glm_vec2_add(interactBox.max, halfSize, interactBox.max); + + // For each entity + entity_t *start = entity->map->entities; + entity_t *end = &entity->map->entities[entity->map->entityCount]; + vec2 otherSize = { TILESET_ENTITIES.tileWidth, TILESET_ENTITIES.tileHeight }; + physicsbox_t otherBox; + physicsboxboxresult_t result; + + do { + if(start->type != ENTITY_TYPE_NPC) continue; + + // Setup other box. + glm_vec2_copy(start->position, otherBox.min); + glm_vec2_copy(start->position, otherBox.max); + glm_vec2_sub(otherBox.min, otherSize, otherBox.min); + glm_vec2_add(otherBox.min, otherSize, otherBox.max); + + physicsBoxCheckBox(interactBox, otherBox, &result); + if(!result.hit) continue; + + printf("Interacted with entity at (%.2f, %.2f)\n", start->position[0], start->position[1]); + break; + } while(++start != end); +} \ No newline at end of file diff --git a/src/rpg/entity/player.h b/src/rpg/entity/player.h new file mode 100644 index 0000000..efb3326 --- /dev/null +++ b/src/rpg/entity/player.h @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +#define PLAYER_SPEED 64.0f +#define PLAYER_INTERACTION_RANGE 1.0f +#define PLAYER_INTERACTION_SIZE 0.5f + +typedef struct entity_s entity_t; + +typedef struct { + void *nothing; +} player_t; + +/** + * Initializes a player entity. + * + * @param entity Pointer to the entity structure to initialize. + */ +void playerInit(entity_t *entity); + +/** + * Handles movement logic for the player entity. + * + * @param entity Pointer to the player entity structure. + */ +void playerMovement(entity_t *entity); + +/** + * Handles interaction logic for the player entity. + * + * @param entity Pointer to the player entity structure. + */ +void playerInteraction(entity_t *entity); \ No newline at end of file diff --git a/src/rpg/rpg.c b/src/rpg/rpg.c new file mode 100644 index 0000000..6464364 --- /dev/null +++ b/src/rpg/rpg.c @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "rpg.h" + +errorret_t rpgInit(void) { + errorOk(); +} + +void rpgUpdate(void) { + +} + +void rpgDispose(void) { + +} \ No newline at end of file diff --git a/src/rpg/rpg.h b/src/rpg/rpg.h index 5d7ba8f..d9ee07c 100644 --- a/src/rpg/rpg.h +++ b/src/rpg/rpg.h @@ -6,8 +6,25 @@ */ #pragma once -#include "dusk.h" +#include "error/error.h" typedef struct { - map_t map; -} rpg_t; \ No newline at end of file + int32_t nothing; +} rpg_t; + +/** + * Initialize the RPG subsystem. + * + * @return An error code and state. + */ +errorret_t rpgInit(void); + +/** + * Update the RPG subsystem. + */ +void rpgUpdate(void); + +/** + * Dispose of the RPG subsystem. + */ +void rpgDispose(void); \ No newline at end of file diff --git a/src/rpg/world/worldunit.h b/src/rpg/world/worldunit.h new file mode 100644 index 0000000..89b5ff0 --- /dev/null +++ b/src/rpg/world/worldunit.h @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +/** + * Position in SUBTILE space in a world, each unit represents a single subtile. + * This is divided by the size of the tile, e.g. if a tile is 16x16 then there + * are 256 / tile size = units per pixel of a tile. This means there are always + * uint8_t max subtiles in a tile. + */ +typedef uint8_t worldsubtile_t; + +/** + * Position in TILE space in a world, each unit represents a single tile. This + * is within CHUNK space. This is not different depending on chunk size, if the + * chunks are 32 tiles wide then the max tile value is 31. + */ +typedef uint8_t worldtile_t; + +/** + * Represents a position in a world in SUBTILE and TILE space. This is basically + * just a convenience struct so you don't have to pass two variables around. + * + * For example, an entity may be at tile (2, 3) and subtile (8, 12). + * meaning that the entity is at pixel (2 * TILE_SIZE + 8, 3 * TILE_SIZE + 12) + * in world space. + * + * This is still within CHUNK space. + */ +typedef struct worldchunkpos_s { + worldsubtile_t subtile; + worldtile_t tile; +} worldchunkpos_t; + +/** + * Position in CHUNK space in a world, each unit represents a single chunk. + */ +typedef uint8_t worldchunk_t; + +/** + * Represents a position in a world in SUBTILE, TILE and CHUNK space. This is in + * WORLD space, so this is the full position of an entity in the world. + */ +typedef struct worldpos_s { + worldsubtile_t subtile; + worldtile_t tile; + worldchunk_t chunk; +} worldpos_t; \ No newline at end of file diff --git a/src/scene/scene/scenetest.h b/src/scene/scene/scenetest.h index 839873e..de8051d 100644 --- a/src/scene/scene/scenetest.h +++ b/src/scene/scene/scenetest.h @@ -22,5 +22,6 @@ static scene_t SCENE_TEST = { .init = sceneTestInit, .update = sceneTestUpdate, .render = sceneTestRender, - .dispose = sceneTestDispose + .dispose = sceneTestDispose, + .flags = 0 }; \ No newline at end of file diff --git a/src/scene/scenemanager.c b/src/scene/scenemanager.c index 253614f..4abad30 100644 --- a/src/scene/scenemanager.c +++ b/src/scene/scenemanager.c @@ -65,8 +65,8 @@ void sceneManagerSetScene(scene_t *scene) { if(scene) { assertTrue( - scene->flags & SCENE_FLAG_INITIALIZED, - "Scene not initialized" + (scene->flags & SCENE_FLAG_INITIALIZED) == 0, + "Scene should not yet be initialized" ); } }