diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 223985a..b01cd93 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -19,6 +19,7 @@ target_include_directories(${DUSK_TARGET_NAME} target_sources(${DUSK_TARGET_NAME} PRIVATE main.c + input.c ) # Subdirs diff --git a/src/display/color.h b/src/display/color.h new file mode 100644 index 0000000..3bc7adf --- /dev/null +++ b/src/display/color.h @@ -0,0 +1,22 @@ +/** + * 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 { + COLOR_BLACK, + COLOR_RED, + COLOR_GREEN, + COLOR_YELLOW, + COLOR_BLUE, + COLOR_MAGENTA, + COLOR_CYAN, + COLOR_WHITE, +} color_t; + +#define COLOR_COUNT (COLOR_WHITE + 1) \ No newline at end of file diff --git a/src/display/render.c b/src/display/render.c index 5a1f258..7f2a2f1 100644 --- a/src/display/render.c +++ b/src/display/render.c @@ -5,20 +5,64 @@ * https://opensource.org/licenses/MIT */ +#include "assert/assert.h" #include "render.h" #include "term.h" +#include "rpg/entity/entity.h" void renderInit() { termInit(); } void renderUpdate() { + char_t c; + color_t color, colorCurrent; + entity_t *ent; + termUpdate(); termClear(); + colorCurrent = COLOR_WHITE; + termPushColor(colorCurrent); + for(int32_t y = 0; y < TERM.height; y++) { for(int32_t x = 0; x < TERM.width; x++) { - char_t c = ' '; // Replace with actual rendering logic + color = COLOR_WHITE; + c = ' '; + + ent = entityGetAt(x, y); + if(ent) { + color = COLOR_RED; + + switch(ent->dir) { + case ENTITY_DIR_UP: + c = '^'; + color = COLOR_GREEN; + break; + case ENTITY_DIR_DOWN: + c = 'v'; + color = COLOR_RED; + break; + case ENTITY_DIR_LEFT: + c = '<'; + color = COLOR_YELLOW; + break; + case ENTITY_DIR_RIGHT: + c = '>'; + color = COLOR_BLUE; + break; + default: + assertUnreachable("Invalid entity direction"); + break; + } + } + + if(c == ' ') { + termPushChar(' '); + continue; + } + + if(color != colorCurrent) termPushColor((colorCurrent = color)); termPushChar(c); } } diff --git a/src/display/render.h b/src/display/render.h index 2eea46c..fff8dad 100644 --- a/src/display/render.h +++ b/src/display/render.h @@ -8,9 +8,6 @@ #pragma once #include "dusk.h" -extern int32_t renderColumnCount; -extern int32_t renderRowCount; - /** * Init the render system. */ diff --git a/src/display/term.c b/src/display/term.c index d4a66d7..f79192d 100644 --- a/src/display/term.c +++ b/src/display/term.c @@ -5,12 +5,24 @@ * https://opensource.org/licenses/MIT */ +#include "assert/assert.h" #include "term.h" #include "util/memory.h" #include term_t TERM; +const char_t* TERM_COLORS[COLOR_COUNT] = { + [COLOR_BLACK] = "\033[30m", + [COLOR_RED] = "\033[31m", + [COLOR_GREEN] = "\033[32m", + [COLOR_YELLOW] = "\033[33m", + [COLOR_BLUE] = "\033[34m", + [COLOR_MAGENTA] = "\033[35m", + [COLOR_CYAN] = "\033[36m", + [COLOR_WHITE] = "\033[37m" +}; + void termInit() { memoryZero(&TERM, sizeof(term_t)); @@ -33,6 +45,11 @@ void termClear() { printf("\033[2J\033[H"); } +void termPushColor(const color_t color) { + assertTrue(color < COLOR_COUNT, "Invalid color index"); + printf("%s", TERM_COLORS[color]); +} + void termPushChar(const char_t c) { putchar(c); } diff --git a/src/display/term.h b/src/display/term.h index 093b535..af9f261 100644 --- a/src/display/term.h +++ b/src/display/term.h @@ -7,6 +7,7 @@ #pragma once #include "dusk.h" +#include "display/color.h" typedef struct { int32_t width; @@ -15,6 +16,8 @@ typedef struct { extern term_t TERM; +extern const char_t* TERM_COLORS[COLOR_COUNT]; + /** * Initialize the terminal system. */ @@ -30,6 +33,8 @@ void termUpdate(); */ void termClear(); +void termPushColor(const color_t color); + /** * Push a character to the terminal output buffer. * diff --git a/src/input.c b/src/input.c index 386a993..8907c3e 100644 --- a/src/input.c +++ b/src/input.c @@ -8,14 +8,96 @@ #include "input.h" #include "util/memory.h" #include "assert/assert.h" - -#define INPUT_BIND_MAP_COUNT (sizeof(INPUT_BIND_MAPS) / sizeof(inputbindmap_t)) +#include input_t INPUT; -void inputInit(void) { - memoryZero(&INPUT, sizeof(input_t)); +inputmap_t INPUT_MAP[] = { + {"w", INPUT_UP}, + {"s", INPUT_DOWN}, + {"a", INPUT_LEFT}, + {"d", INPUT_RIGHT}, + {" ", INPUT_ACTION}, + {"e", INPUT_ACTION}, + {"\n", INPUT_ACTION}, // Enter key + {"\x1B", INPUT_CANCEL}, // Escape key + {"\033[A", INPUT_UP}, // Up arrow + {"\033[B", INPUT_DOWN}, // Down arrow + {"\033[C", INPUT_RIGHT}, // Right arrow + {"\033[D", INPUT_LEFT}, // Left arrow + { 0 } +}; + +void inputEnableRawMode() { + if (INPUT.isRawMode) return; + tcgetattr(STDIN_FILENO, &INPUT.origin); + struct termios raw = INPUT.origin; + raw.c_lflag &= ~(ICANON | ECHO); + tcsetattr(STDIN_FILENO, TCSANOW, &raw); + INPUT.isRawMode = 1; } -void inputUpdate(void) { +void inputDisableRawMode() { + if(!INPUT.isRawMode) return; + tcsetattr(STDIN_FILENO, TCSANOW, &INPUT.origin); + INPUT.isRawMode = 0; +} + +void inputSetNonBlocking() { + int flags = fcntl(STDIN_FILENO, F_GETFL, 0); + fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK); +} + +void inputInit() { + memoryZero(&INPUT, sizeof(input_t)); + + inputEnableRawMode(); + inputSetNonBlocking(); +} + +void inputUpdate() { + char_t buffer[INPUT_BUFFER_SIZE]; + ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer) - 1); + + if(n < 0) { + INPUT.previous = INPUT.current; + INPUT.current = 0; + return; + } + + assertTrue(n < INPUT_BUFFER_SIZE, "Input buffer overflow"); + + uint8_t val = 0; + inputmap_t *map; + uint8_t len; + char_t *c; + for(uint8_t i = 0; i < n; i++) { + map = INPUT_MAP; + + do { + len = strlen(map->key); + + if(i + len > n) { + map++; + continue; + } + + c = buffer + i; + if(memoryCompare(c, map->key, len) != 0) { + map++; + continue; + } + + // Button was pressed + val |= map->value; + i += len - 1; + } while(map->key); + } + + INPUT.previous = INPUT.current; + INPUT.current = val; +} + +void inputDispose() { + inputDisableRawMode(); } \ No newline at end of file diff --git a/src/input.h b/src/input.h index 727e1be..d66b54a 100644 --- a/src/input.h +++ b/src/input.h @@ -7,19 +7,39 @@ #pragma once #include "dusk.h" +#include + +#define INPUT_UP (1 << 0) +#define INPUT_DOWN (1 << 1) +#define INPUT_LEFT (1 << 2) +#define INPUT_RIGHT (1 << 3) +#define INPUT_ACTION (1 << 4) +#define INPUT_CANCEL (1 << 5) + +#define INPUT_BUFFER_SIZE 128 typedef struct { - char_t input; + uint8_t current; + uint8_t previous; + bool_t isRawMode; + struct termios origin; } input_t; extern input_t INPUT; +typedef struct { + char_t *key; + uint8_t value; +} inputmap_t; + +extern inputmap_t INPUT_MAP[]; + /** * Initializes the input system. */ -void inputInit(void); +void inputInit(); /** * Updates the input system. */ -void inputUpdate(void); \ No newline at end of file +void inputUpdate(); \ No newline at end of file diff --git a/src/main.c b/src/main.c index f41a72d..f02bb6c 100644 --- a/src/main.c +++ b/src/main.c @@ -5,14 +5,34 @@ * https://opensource.org/licenses/MIT */ +#include "input.h" #include "util/random.h" #include "display/render.h" +#include "rpg/entity/entity.h" + int32_t main(const int32_t argc, const char **argv) { + inputInit(); randomInit(); renderInit(); + entity_t *ent; + + ent = &ENTITIES[0]; + entityInit(ent, ENTITY_TYPE_PLAYER); + while(1) { + inputUpdate(); + + ent = ENTITIES; + do { + if(ent->type == ENTITY_TYPE_NULL) { + ent++; + continue; + } + entityUpdate(ent++); + } while(ent < (ENTITIES + ENTITY_COUNT)); + renderUpdate(); usleep(100 * 1000); // Sleep for 16 milliseconds (60 FPS) } diff --git a/src/rpg/CMakeLists.txt b/src/rpg/CMakeLists.txt index 418fd16..5312e04 100644 --- a/src/rpg/CMakeLists.txt +++ b/src/rpg/CMakeLists.txt @@ -11,4 +11,5 @@ target_sources(${DUSK_TARGET_NAME} # Subdirs add_subdirectory(entity) add_subdirectory(item) -add_subdirectory(quest) \ No newline at end of file +add_subdirectory(quest) +add_subdirectory(world) \ No newline at end of file diff --git a/src/rpg/entity/entity.c b/src/rpg/entity/entity.c index d2e0b8c..2557782 100644 --- a/src/rpg/entity/entity.c +++ b/src/rpg/entity/entity.c @@ -12,12 +12,11 @@ entity_t ENTITIES[ENTITY_COUNT]; entitycallbacks_t ENTITY_CALLBACKS[ENTITY_TYPE_COUNT] = { - [ENTITY_TYPE_NULL] = { - .init = NULL - }, + [ENTITY_TYPE_NULL] = { 0 }, [ENTITY_TYPE_PLAYER] = { - .init = playerInit + .init = playerInit, + .update = playerUpdate }, }; @@ -31,4 +30,23 @@ void entityInit(entity_t *entity, const entitytype_t type) { memoryZero(entity, sizeof(entity_t)); entity->type = type; ENTITY_CALLBACKS[type].init(entity); +} + +void entityUpdate(entity_t *entity) { + assertNotNull(entity, "Entity cannot be NULL"); + assertNotNull( + ENTITY_CALLBACKS[entity->type].update, + "Entity type has no update" + ); + + ENTITY_CALLBACKS[entity->type].update(entity); +} + +entity_t * entityGetAt(const uint8_t x, const uint8_t y) { + entity_t *e = ENTITIES; + do { + if(e->type && e->x == x && e->y == y) return e; + e++; + } while(e < (ENTITIES + ENTITY_COUNT)); + return NULL; } \ No newline at end of file diff --git a/src/rpg/entity/entity.h b/src/rpg/entity/entity.h index 0f0286a..31b5316 100644 --- a/src/rpg/entity/entity.h +++ b/src/rpg/entity/entity.h @@ -43,6 +43,7 @@ extern entity_t ENTITIES[ENTITY_COUNT]; typedef struct { void (*init)(entity_t *entity); + void (*update)(entity_t *entity); } entitycallbacks_t; extern entitycallbacks_t ENTITY_CALLBACKS[ENTITY_TYPE_COUNT]; @@ -52,4 +53,20 @@ extern entitycallbacks_t ENTITY_CALLBACKS[ENTITY_TYPE_COUNT]; * @param entity Pointer to the entity to initialize. * @param type The type of the entity to initialize. */ -void entityInit(entity_t *entity, const entitytype_t type); \ No newline at end of file +void entityInit(entity_t *entity, const entitytype_t type); + +/** + * Updates the entity's state based on its type. + * + * @param entity Pointer to the entity to update. + */ +void entityUpdate(entity_t *entity); + +/** + * Resets the entity at a given position. + * + * @param x The x-coordinate of the entity. + * @param y The y-coordinate of the entity. + * @return Pointer to the entity at the specified position, or NULL. + */ +entity_t * entityGetAt(const uint8_t x, const uint8_t y); \ No newline at end of file diff --git a/src/rpg/entity/player.c b/src/rpg/entity/player.c index ffa37b4..d92b8e3 100644 --- a/src/rpg/entity/player.c +++ b/src/rpg/entity/player.c @@ -11,4 +11,9 @@ void playerInit(entity_t *player) { assertNotNull(player, "Player entity is NULL"); assertTrue(player->type == ENTITY_TYPE_PLAYER, "Entity is not a player"); +} + +void playerUpdate(entity_t *entity) { + assertNotNull(entity, "Entity is NULL"); + assertTrue(entity->type == ENTITY_TYPE_PLAYER, "Entity is not a player"); } \ No newline at end of file diff --git a/src/rpg/entity/player.h b/src/rpg/entity/player.h index f43c78a..14fd5dd 100644 --- a/src/rpg/entity/player.h +++ b/src/rpg/entity/player.h @@ -19,4 +19,11 @@ typedef struct { * * @param ent Pointer to the player entity to initialize. */ -void playerInit(entity_t *ent); \ No newline at end of file +void playerInit(entity_t *ent); + +/** + * Updates a player entity. + * + * @param ent Entity to update. + */ +void playerUpdate(entity_t *ent); \ No newline at end of file diff --git a/src/rpg/world/CMakeLists.txt b/src/rpg/world/CMakeLists.txt new file mode 100644 index 0000000..4fb70fa --- /dev/null +++ b/src/rpg/world/CMakeLists.txt @@ -0,0 +1,10 @@ +# 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 + map.c +) \ No newline at end of file diff --git a/src/rpg/world/map.c b/src/rpg/world/map.c new file mode 100644 index 0000000..e69de29 diff --git a/src/rpg/world/map.h b/src/rpg/world/map.h new file mode 100644 index 0000000..0f52ca8 --- /dev/null +++ b/src/rpg/world/map.h @@ -0,0 +1,16 @@ +/** + * 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 { + uint8_t width; + uint8_t height; +} map_t; + +extern map_t MAP; \ No newline at end of file