diff --git a/src/input.c b/src/input.c index 90bcf30..5ac7a0f 100644 --- a/src/input.c +++ b/src/input.c @@ -31,6 +31,7 @@ input_init(Input *input) input->lastMouseY = 0; input->modKeyState = 0; input->lastModKeyState = 0; + input->textInput[0] = '\0'; } void @@ -44,6 +45,7 @@ input_reset(Input *input) input->modKeyState = 0; input->lastMouseX = input->mouseX; input->lastMouseY = input->mouseY; + input->textInput[0] = '\0'; } static Uint64 @@ -101,6 +103,9 @@ get_event_key(SDL_Event *event) case SDLK_TAB: key = KEY_TAB; break; + case SDLK_BACKSPACE: + key = KEY_BACKSPACE; + break; default: key = 0; break; @@ -195,6 +200,9 @@ get_event_modkey(SDL_Event *event) case SDLK_F: key = KEY_CTRL_F; break; + case SDLK_V: + key = KEY_CTRL_V; + break; } } else if (event->key.mod & (SDL_KMOD_LSHIFT | SDL_KMOD_RSHIFT)) { switch (event->key.key) { @@ -278,6 +286,10 @@ input_handle_event(Input *input, SDL_Event *event, InputDeviceType *device_type) } else { input->keyState &= ~get_axis_motion(event); } + } else if (event->type == SDL_EVENT_TEXT_INPUT) { + // Copy the string and ensure null-termination + strncpy(input->textInput, event->text.text, TEXT_INPUT_MAX_LEN - 1); + input->textInput[TEXT_INPUT_MAX_LEN - 1] = '\0'; } if (device_type != NULL) { diff --git a/src/input.h b/src/input.h index cb38fb3..b008780 100644 --- a/src/input.h +++ b/src/input.h @@ -40,6 +40,7 @@ #define KEY_ENTER 0x8000 #define KEY_SPACE 0x10000 #define KEY_TAB 0x20000 +#define KEY_BACKSPACE 0x40000 #define KEY_CTRL_M 0x1 #define KEY_CTRL_S 0x2 @@ -50,11 +51,14 @@ #define KEY_SHIFT_NUM4 0x40 #define KEY_SHIFT_NUM5 0x80 #define KEY_CTRL_F 0x100 +#define KEY_CTRL_V 0x200 #define MBUTTON_LEFT 0x1 #define MBUTTON_MIDDLE 0x2 #define MBUTTON_RIGHT 0x4 +#define TEXT_INPUT_MAX_LEN 16 + typedef enum InputDeviceType { DeviceType_Unknown, DeviceType_Keyboard, DeviceType_Gamepad } InputDeviceType; typedef struct Input { @@ -68,6 +72,7 @@ typedef struct Input { Uint32 lastMouseY; Uint32 mouseX; Uint32 mouseY; + char textInput[TEXT_INPUT_MAX_LEN]; } Input; void input_init(Input *); diff --git a/src/main.c b/src/main.c index 3c8e872..2df473f 100644 --- a/src/main.c +++ b/src/main.c @@ -56,6 +56,7 @@ #include "event.h" #include "config.h" #include "save.h" +#include "text_input.h" #ifdef DEBUG #include "debug/debug.h" @@ -104,6 +105,7 @@ static SDL_Rect statsGuiViewport; static SDL_Rect minimapViewport; static SDL_Rect menuViewport; static Input input; +static unsigned int gCustomSeed = 0; #ifdef DEBUG static Sprite *fpsSprite = NULL; @@ -260,10 +262,16 @@ startGame(void) else cLevel = 1; - if (weeklyGame) + if (weeklyGame) { + debug("Using weekly game seed"); set_random_seed((unsigned int)time_get_weekly_seed()); - else - set_random_seed(0); + } else if (gCustomSeed) { + debug("Using custom game seed"); + set_random_seed(gCustomSeed); + } else { + debug("Setting random seed"); + set_random_seed(0); // 0 will trigger a random seed later on. + } gGameState = PLAYING; if (gPlayer) @@ -471,6 +479,18 @@ goToGameSelectMenu(void *unused) gGameState = GAME_SELECT; } +static void +openSeedEntry(void *unused) +{ + (void)unused; + char seed_str[16] = {0}; + if (gCustomSeed > 0) { + SDL_snprintf(seed_str, sizeof(seed_str), "%u", gCustomSeed); + } + text_input_init(gWindow, gRenderer, "Enter game seed:", seed_str); + gGameState = SEED_ENTRY; +} + static void showHowToTooltip(void *unused) { @@ -479,33 +499,66 @@ showHowToTooltip(void *unused) gGui->activeTooltip = tooltip_manager_get_tooltip(TOOLTIP_TYPE_HOWTO); } +static void +copySeedToClipboard(void *unused) +{ + (void)unused; + char seed_str[16]; + SDL_snprintf(seed_str, sizeof(seed_str), "%u", get_random_seed()); + SDL_SetClipboardText(seed_str); + gui_event_message("Seed copied to clipboard"); + toggleInGameMenu(NULL); +} + +static void +buildSeedLabel(char *buf, size_t size) +{ + SDL_snprintf(buf, size, "SEED:%u", get_random_seed()); +} + static void initInGameMenu(void) { - static TEXT_MENU_ITEM menu_items[] = { - {"RESUME", "", toggleInGameMenu}, - {"HOW TO PLAY", "", showHowToTooltip}, - {"MAIN MENU", "", goToMainMenu}, - {"QUIT", "Exit game", exitGame}, - }; + static char seed_label[32]; + buildSeedLabel(seed_label, sizeof(seed_label)); + + TEXT_MENU_ITEM menu_items[5]; + int count = 0; - menu_create_text_menu(&inGameMenu, &menu_items[0], 4, gRenderer); + menu_items[count++] = (TEXT_MENU_ITEM){"RESUME", "Resume the current game", toggleInGameMenu}; + menu_items[count++] = (TEXT_MENU_ITEM){"HOW TO PLAY", "Show the in-game guide", showHowToTooltip}; + if (!weeklyGame) { + menu_items[count++] = (TEXT_MENU_ITEM){seed_label, "Copy seed to clipboard", copySeedToClipboard}; + } + menu_items[count++] = (TEXT_MENU_ITEM){"MAIN MENU", "Return to the main menu", goToMainMenu}; + menu_items[count++] = (TEXT_MENU_ITEM){"QUIT", "Exit game", exitGame}; + + menu_create_text_menu(&inGameMenu, &menu_items[0], count, gRenderer); } static void createInGameGameOverMenu(void) { - static TEXT_MENU_ITEM menu_items[] = { - {"NEW GAME", "Start a new game with the same settings", goToCharacterMenu}, - {"MAIN MENU", "", goToMainMenu}, - {"QUIT", "Exit game", exitGame}, - }; + static char seed_label[32]; + buildSeedLabel(seed_label, sizeof(seed_label)); if (inGameMenu) { menu_destroy(inGameMenu); inGameMenu = NULL; } - menu_create_text_menu(&inGameMenu, &menu_items[0], 3, gRenderer); + + TEXT_MENU_ITEM menu_items[4]; + int count = 0; + + menu_items[count++] = + (TEXT_MENU_ITEM){"NEW GAME", "Start a new game with the same settings", goToCharacterMenu}; + if (!weeklyGame) { + menu_items[count++] = (TEXT_MENU_ITEM){seed_label, "Copy seed to clipboard", copySeedToClipboard}; + } + menu_items[count++] = (TEXT_MENU_ITEM){"MAIN MENU", "Return to the main menu", goToMainMenu}; + menu_items[count++] = (TEXT_MENU_ITEM){"QUIT", "Exit game", exitGame}; + + menu_create_text_menu(&inGameMenu, &menu_items[0], count, gRenderer); } static void @@ -547,6 +600,7 @@ initMainMenu(void) { static TEXT_MENU_ITEM menu_items[] = { {"PLAY", "Start game", goToGameSelectMenu}, + {"SET SEED", "Set the game seed", openSeedEntry}, {"SCORES", "View your top 10 scores", viewScoreScreen}, {"CREDITS", "View game credits", viewCredits}, {"QUIT", "Exit game", exitGame}, @@ -557,7 +611,7 @@ initMainMenu(void) gMap = map_lua_generator_single_room__run(cLevel, gRenderer); - menu_create_text_menu(&mainMenu, &menu_items[0], 4, gRenderer); + menu_create_text_menu(&mainMenu, &menu_items[0], SDL_arraysize(menu_items), gRenderer); mixer_play_music(MENU_MUSIC); creditsScreen = screen_create_credits(gRenderer); scoreScreen = screen_create_hiscore(gRenderer); @@ -752,6 +806,10 @@ handle_main_input(void) charSelectMenu = NULL; gGameState = GAME_SELECT; break; + case SEED_ENTRY: + text_input_close(gWindow); + gGameState = MENU; + break; case MENU: gGameState = QUIT; break; @@ -769,7 +827,7 @@ handle_main_input(void) } handle_settings_input(); - if (input_key_is_pressed(&input, KEY_TAB)) { + if ((gGameState == PLAYING || gGameState == GAME_OVER) && input_key_is_pressed(&input, KEY_TAB)) { gShowMap = !gShowMap; } } @@ -1059,6 +1117,7 @@ run_game_render(void) static inline void register_scores(void) { + debug("Registering steam scores"); uint8_t details[4] = {(uint8_t)gPlayer->stats.lvl, (uint8_t)cLevel, (uint8_t)(gPlayer->class + 1), 0}; steam_register_score((int)gPlayer->gold, (int32_t *)&details, 1); steam_register_kills((int)gPlayer->stat_data.kills, (int32_t *)&details, 1); @@ -1080,6 +1139,10 @@ register_scores(void) steam_set_achievement(MAGICAL); steam_register_mage_score((int)gPlayer->gold, (int32_t *)&details, 1); } + + if (gCustomSeed != 0 && !weeklyGame) { + steam_set_achievement(SEEDLING); + } } #endif @@ -1207,6 +1270,28 @@ run_menu(void) SDL_RenderPresent(gRenderer); } +static void +run_seed_entry(void) +{ + SDL_SetRenderViewport(gRenderer, &mainViewport); + text_input_update(&input); + text_input_render(gCamera); + SDL_RenderPresent(gRenderer); + + // Di this last since it resets the text_input + if (text_input_is_confirmed()) { + const char *val = text_input_get_value(); + if (SDL_strlen(val) > 0) { + gCustomSeed = (unsigned int)SDL_atoi(val); + debug("Custom seed set: %u", gCustomSeed); + } else { + gCustomSeed = 0; + } + text_input_close(gWindow); + gGameState = MENU; + } +} + static void run(void) { @@ -1252,6 +1337,9 @@ run(void) case CHARACTER_MENU: run_menu(); break; + case SEED_ENTRY: + run_seed_entry(); + break; case QUIT: quit = true; break; diff --git a/src/steam/steamworks_api_wrapper.c b/src/steam/steamworks_api_wrapper.c index 9e8b79c..f276ce7 100644 --- a/src/steam/steamworks_api_wrapper.c +++ b/src/steam/steamworks_api_wrapper.c @@ -23,8 +23,9 @@ static Achievement g_Achievements[] = {_ACH_ID(BAD_DOG, "Bad Dog"), _ACH_ID(BACK_TO_WORK, "Back to work"), _ACH_ID(DRAGON_SLAYER, "Platinum dragon slayer"), _ACH_ID(ROGUE_LIKE, "Rogue-like"), - _ACH_ID(MAGICAL, "Magical")}; -static Uint8 numAchievements = 7; + _ACH_ID(MAGICAL, "Magical"), + _ACH_ID(SEEDLING, "Seedling")}; +static Uint8 numAchievements = SDL_arraysize(g_Achievements); static bool m_Initiated = false; static uint32_t m_AppID = 0; @@ -140,6 +141,7 @@ steam_set_achievement(EAchievement eAch) Achievement *a = &g_Achievements[i]; if (a->m_eAchievementID == eAch && !a->m_bAchieved) { c_SteamUserStats_SetAchievement(g_Achievements[i].m_pchAchievementID); + debug("Setting \"%s\" achievement", a->m_rgchName); gui_log("You just earned the \"%s\" achievement", a->m_rgchName); } } diff --git a/src/steam/steamworks_api_wrapper.h b/src/steam/steamworks_api_wrapper.h index a783505..578e3c1 100644 --- a/src/steam/steamworks_api_wrapper.h +++ b/src/steam/steamworks_api_wrapper.h @@ -13,7 +13,8 @@ typedef enum EAchievement { BUGGCREATOR = 8, ROGUE_LIKE = 9, MAGICAL = 10, - ARCADE_HACK = 11 + ARCADE_HACK = 11, + SEEDLING = 12, } EAchievement; #define _ACH_ID(id, name) {id, #id, name, "", 0, 0} diff --git a/src/text_input.c b/src/text_input.c new file mode 100644 index 0000000..669de6f --- /dev/null +++ b/src/text_input.c @@ -0,0 +1,151 @@ +/* + * BreakHack - A dungeone crawler RPG + * Copyright (C) 2025 Linus Probert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "text_input.h" +#include "SDL3/SDL_clipboard.h" +#include "sprite.h" +#include "defines.h" + +#define BUF_SIZE 16 + +static char inputBuffer[BUF_SIZE] = {0}; +static Sprite *inputSprite = NULL; +static Sprite *headingSprite = NULL; +static Sprite *hintSprite = NULL; +static SDL_Renderer *gRenderer = NULL; +static bool gInputConfirmed = false; + +void +text_input_init(SDL_Window *window, SDL_Renderer *renderer, const char *title, const char *current) +{ + gRenderer = renderer; + gInputConfirmed = false; + + // Clear and set the buffer + SDL_strlcpy(inputBuffer, current, BUF_SIZE); + + SDL_StartTextInput(window); + + if (headingSprite == NULL) { + headingSprite = sprite_create(); + sprite_load_text_texture(headingSprite, "GUI/SDS_8x8.ttf", 0, 18, 1); + texture_load_from_text(headingSprite->textures[0], title, C_BLUE, C_BLACK, renderer); + headingSprite->dim = headingSprite->textures[0]->dim; + headingSprite->pos = (Position){(SCREEN_WIDTH - headingSprite->dim.width) >> 1, SCREEN_HEIGHT / 2 - 40}; + headingSprite->fixed = true; + } + + if (inputSprite == NULL) { + inputSprite = sprite_create(); + sprite_load_text_texture(inputSprite, "GUI/SDS_8x8.ttf", 0, 18, 1); + inputSprite->fixed = true; + } + + if (hintSprite == NULL) { + hintSprite = sprite_create(); + sprite_load_text_texture(hintSprite, "GUI/SDS_8x8.ttf", 0, 10, 1); + texture_load_from_text(hintSprite->textures[0], "ENTER to confirm | ESC to cancel", C_WHITE, C_BLACK, + renderer); + hintSprite->dim = hintSprite->textures[0]->dim; + hintSprite->pos = (Position){15, SCREEN_HEIGHT - 25}; + hintSprite->fixed = true; + } +} + +static void +parseBufferToInput(const char *buffer) +{ + for (const char *c = buffer; *c; c++) { + if (*c >= '0' && *c <= '9') { + size_t len = SDL_strlen(inputBuffer); + if (len < sizeof(inputBuffer) - 1) { + inputBuffer[len] = *c; + inputBuffer[len + 1] = '\0'; + } + } + } +} + +void +text_input_update(Input *input) +{ + if (input->textInput[0] != '\0') { + parseBufferToInput(input->textInput); + } else if (input_key_is_pressed(input, KEY_BACKSPACE)) { + size_t len = SDL_strlen(inputBuffer); + if (len > 0) { + inputBuffer[len - 1] = '\0'; + } + } else if (input_key_is_pressed(input, KEY_ENTER)) { + gInputConfirmed = true; + } else if (input_modkey_is_pressed(input, KEY_CTRL_V)) { + char *cb_text = SDL_GetClipboardText(); + parseBufferToInput(cb_text); + SDL_free(cb_text); + } +} + +bool +text_input_is_confirmed(void) +{ + return gInputConfirmed; +} + +const char * +text_input_get_value(void) +{ + return inputBuffer; +} + +void +text_input_render(Camera *cam) +{ + SDL_FRect bg = {0, 0, SCREEN_WIDTH, SCREEN_HEIGHT}; + SDL_SetRenderDrawColor(cam->renderer, 0, 0, 0, 175); + SDL_RenderFillRect(cam->renderer, &bg); + + sprite_render(headingSprite, cam); + + char displayBuf[20]; + SDL_snprintf(displayBuf, sizeof(displayBuf), "%s_", inputBuffer); + texture_load_from_text(inputSprite->textures[0], displayBuf, C_YELLOW, C_BLACK, gRenderer); + inputSprite->dim = inputSprite->textures[0]->dim; + inputSprite->pos = (Position){(SCREEN_WIDTH - inputSprite->dim.width) >> 1, SCREEN_HEIGHT / 2}; + sprite_render(inputSprite, cam); + + sprite_render(hintSprite, cam); +} + +void +text_input_close(SDL_Window *window) +{ + SDL_StopTextInput(window); + + if (headingSprite) { + sprite_destroy(headingSprite); + headingSprite = NULL; + } + if (inputSprite) { + sprite_destroy(inputSprite); + inputSprite = NULL; + } + if (hintSprite) { + sprite_destroy(hintSprite); + hintSprite = NULL; + } +} diff --git a/src/text_input.h b/src/text_input.h new file mode 100644 index 0000000..1c9d231 --- /dev/null +++ b/src/text_input.h @@ -0,0 +1,35 @@ +/* + * BreakHack - A dungeone crawler RPG + * Copyright (C) 2026 Linus Probert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include "camera.h" +#include "input.h" + +void text_input_init(SDL_Window *window, SDL_Renderer *renderer, const char *title, const char *current); + +void text_input_update(Input *input); + +bool text_input_is_confirmed(void); + +const char *text_input_get_value(void); + +void text_input_render(Camera *cam); + +void text_input_close(SDL_Window *window);