From 848b6586115f63734de586046a33778b86c72a6b Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 12 Apr 2026 15:21:43 +0200 Subject: [PATCH] key_config.cpp i keys.yaml per a centralitzar i no hardcodejar tecles --- CMakeLists.txt | 1 + config/assets.yaml | 1 + data/console/commands.yaml | 2 +- data/input/keys.yaml | 203 +++++++++++++++++++++++++++ source/core/input/global_inputs.cpp | 6 +- source/core/input/input.cpp | 25 +--- source/core/input/input_types.cpp | 4 + source/core/input/key_config.cpp | 126 +++++++++++++++++ source/core/input/key_config.hpp | 53 +++++++ source/core/rendering/screenshot.cpp | 3 +- source/core/system/director.cpp | 8 ++ source/game/editor/map_editor.cpp | 43 +++--- source/game/editor/mini_map.cpp | 9 +- source/game/editor/mini_map.hpp | 8 +- source/game/scenes/game.cpp | 88 +++++------- source/game/ui/console.cpp | 4 +- source/game/ui/console.hpp | 3 +- source/game/ui/console_commands.cpp | 48 ++++++- source/game/ui/console_commands.hpp | 1 + 19 files changed, 521 insertions(+), 115 deletions(-) create mode 100644 data/input/keys.yaml create mode 100644 source/core/input/key_config.cpp create mode 100644 source/core/input/key_config.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e272b2e..f4d17c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ set(APP_SOURCES source/core/input/global_inputs.cpp source/core/input/input_types.cpp source/core/input/input.cpp + source/core/input/key_config.cpp source/core/input/mouse.cpp # Core - Rendering diff --git a/config/assets.yaml b/config/assets.yaml index 7f1c697..06cf7eb 100644 --- a/config/assets.yaml +++ b/config/assets.yaml @@ -35,6 +35,7 @@ assets: input: DATA: - ${PREFIX}/gamecontrollerdb.txt + - ${PREFIX}/data/input/keys.yaml # SYSTEM system: diff --git a/data/console/commands.yaml b/data/console/commands.yaml index 890bec1..3152e39 100644 --- a/data/console/commands.yaml +++ b/data/console/commands.yaml @@ -182,7 +182,7 @@ categories: - keyword: HELP handler: cmd_help description: "Show this help" - usage: "HELP []" + usage: "HELP [KEYS [scope]|]" dynamic_completions: true - keyword: "?" diff --git a/data/input/keys.yaml b/data/input/keys.yaml new file mode 100644 index 0000000..3662051 --- /dev/null +++ b/data/input/keys.yaml @@ -0,0 +1,203 @@ +# Projecte 2026 - Keybinding Configuration +# Single source of truth for all key assignments. +# Code reads this at startup; HELP KEYS displays it in the console. +# +# Fields: +# id - Identifier used in C++ code (KeyConfig::key("SCOPE", "id")) +# key - Display text for HELP KEYS (human-readable) +# code - SDL key name for SDL_GetKeyFromName() — omit for mouse/composite entries +# desc - Short description (console is 256px wide) +# action - (Optional, GLOBAL only) InputAction name to bind via Input::bindKey() + +scopes: + - name: GLOBAL + keys: + - id: zoom_down + key: "F1" + code: "F1" + desc: "zoom down" + action: WINDOW_DEC_ZOOM + - id: zoom_up + key: "F2" + code: "F2" + desc: "zoom up" + action: WINDOW_INC_ZOOM + - id: fullscreen + key: "F3" + code: "F3" + desc: "fullscreen" + action: TOGGLE_FULLSCREEN + - id: shader + key: "F4" + code: "F4" + desc: "shader on/off" + action: TOGGLE_POSTFX + - id: shader_preset + key: "Shift+F4" + desc: "next shader preset" + - id: shader_type + key: "Ctrl+F4" + desc: "next shader type" + - id: next_palette + key: "F5" + code: "F5" + desc: "next palette" + action: NEXT_PALETTE + - id: prev_palette + key: "Ctrl+F5" + desc: "prev palette" + - id: palette_sort + key: "F6" + code: "F6" + desc: "palette sort mode" + action: NEXT_PALETTE_SORT + - id: integer_scale + key: "F7" + code: "F7" + desc: "integer scale" + action: TOGGLE_INTEGER_SCALE + - id: music + key: "F8" + code: "F8" + desc: "music on/off" + action: TOGGLE_MUSIC + - id: border + key: "F9" + code: "F9" + desc: "border" + action: TOGGLE_BORDER + - id: vsync + key: "F10" + code: "F10" + desc: "vsync" + action: TOGGLE_VSYNC + - id: pause + key: "F11" + code: "F11" + desc: "pause" + action: PAUSE + - id: info + key: "F12" + code: "F12" + desc: "show info" + action: TOGGLE_DEBUG + - id: screenshot + key: "Ctrl+S" + code: "S" + desc: "screenshot" + action: SCREENSHOT + - id: console + key: "`" + code: "`" + desc: "console" + action: TOGGLE_CONSOLE + - id: quit + key: "Esc" + code: "Escape" + desc: "quit/back" + action: EXIT + + - name: EDITOR + keys: + - id: toggle + key: "9" + code: "9" + desc: "toggle editor" + - id: grid + key: "G" + code: "G" + desc: "toggle grid" + - id: collision + key: "8" + code: "8" + desc: "draw/collision mode" + - id: tile_picker + key: "T" + code: "T" + desc: "tile picker" + - id: eraser + key: "E" + code: "E" + desc: "eraser" + - id: minimap + key: "M" + code: "M" + desc: "minimap" + - id: nav_up + key: "Up" + code: "Up" + desc: "room up" + - id: nav_down + key: "Down" + code: "Down" + desc: "room down" + - id: nav_left + key: "Left" + code: "Left" + desc: "room left" + - id: nav_right + key: "Right" + code: "Right" + desc: "room right" + - id: cancel + key: "Esc" + code: "Escape" + desc: "cancel/clear brush" + + - name: MINIMAP + keys: + - id: numbers + key: "N" + code: "N" + desc: "room numbers" + - id: capture + key: "S" + code: "S" + desc: "capture minimap" + - id: close_m + key: "M" + code: "M" + desc: "close" + - id: close_esc + key: "Esc" + code: "Escape" + desc: "close" + + - name: DEBUG + keys: + - id: debug_mode + key: "0" + code: "0" + desc: "debug mode" + - id: infinite_lives + key: "1" + code: "1" + desc: "infinite lives" + - id: invincibility + key: "2" + code: "2" + desc: "invincibility" + - id: nav_up + key: "W" + code: "W" + desc: "room up" + - id: nav_left + key: "A" + code: "A" + desc: "room left" + - id: nav_down + key: "S" + code: "S" + desc: "room down" + - id: nav_right + key: "D" + code: "D" + desc: "room right" + - id: reload + key: "R" + code: "R" + desc: "reload resources" + - id: test_cheevo + key: "3" + code: "3" + desc: "test achievement" diff --git a/source/core/input/global_inputs.cpp b/source/core/input/global_inputs.cpp index c3295a6..263e263 100644 --- a/source/core/input/global_inputs.cpp +++ b/source/core/input/global_inputs.cpp @@ -7,9 +7,9 @@ #include "core/input/input.hpp" // Para Input, InputAction, Input::DO_NOT_ALLOW_REPEAT #include "core/locale/locale.hpp" // Para Locale -#include "core/rendering/render_info.hpp" // Para RenderInfo -#include "core/rendering/screen.hpp" // Para Screen -#include "core/rendering/screenshot.hpp" // Para Screenshot +#include "core/rendering/render_info.hpp" // Para RenderInfo +#include "core/rendering/screen.hpp" // Para Screen +#include "core/rendering/screenshot.hpp" // Para Screenshot #ifdef _DEBUG #include "core/system/debug.hpp" // Para Debug (persistencia de render_info en debug.yaml) #endif diff --git a/source/core/input/input.cpp b/source/core/input/input.cpp index 9971ab8..02ff13c 100644 --- a/source/core/input/input.cpp +++ b/source/core/input/input.cpp @@ -27,34 +27,17 @@ auto Input::get() -> Input* { return Input::instance; } // Constructor Input::Input(std::string game_controller_db_path) : gamepad_mappings_file_(std::move(game_controller_db_path)) { - // Inicializar bindings del teclado + // Bindings de gameplay (rebindeables por el jugador vía Options) + // Las teclas de sistema (F1-F12, B, S, `, Esc...) se cargan desde keys.yaml + // vía KeyConfig::applyGlobalBindings() — NO duplicar aquí. keyboard_.bindings = { - // Movimiento del jugador {Action::LEFT, KeyState{.scancode = SDL_SCANCODE_LEFT}}, {Action::RIGHT, KeyState{.scancode = SDL_SCANCODE_RIGHT}}, {Action::JUMP, KeyState{.scancode = SDL_SCANCODE_UP}}, {Action::DOWN, KeyState{.scancode = SDL_SCANCODE_DOWN}}, - - // Inputs de control {Action::ACCEPT, KeyState{.scancode = SDL_SCANCODE_RETURN}}, {Action::CANCEL, KeyState{.scancode = SDL_SCANCODE_ESCAPE}}, - {Action::EXIT, KeyState{.scancode = SDL_SCANCODE_ESCAPE}}, - - // Inputs de sistema - {Action::WINDOW_DEC_ZOOM, KeyState{.scancode = SDL_SCANCODE_F1}}, - {Action::WINDOW_INC_ZOOM, KeyState{.scancode = SDL_SCANCODE_F2}}, - {Action::TOGGLE_FULLSCREEN, KeyState{.scancode = SDL_SCANCODE_F3}}, - {Action::TOGGLE_SHADER, KeyState{.scancode = SDL_SCANCODE_F4}}, - {Action::NEXT_PALETTE, KeyState{.scancode = SDL_SCANCODE_F5}}, - {Action::NEXT_PALETTE_SORT, KeyState{.scancode = SDL_SCANCODE_F6}}, - {Action::TOGGLE_INTEGER_SCALE, KeyState{.scancode = SDL_SCANCODE_F7}}, - {Action::TOGGLE_IN_GAME_MUSIC, KeyState{.scancode = SDL_SCANCODE_F8}}, - {Action::TOGGLE_BORDER, KeyState{.scancode = SDL_SCANCODE_F9}}, - {Action::TOGGLE_VSYNC, KeyState{.scancode = SDL_SCANCODE_F10}}, - {Action::PAUSE, KeyState{.scancode = SDL_SCANCODE_F11}}, - {Action::TOGGLE_INFO, KeyState{.scancode = SDL_SCANCODE_F12}}, - {Action::TOGGLE_CONSOLE, KeyState{.scancode = SDL_SCANCODE_GRAVE}}, - {Action::SCREENSHOT, KeyState{.scancode = SDL_SCANCODE_S}}}; + }; initSDLGamePad(); // Inicializa el subsistema SDL_INIT_GAMEPAD } diff --git a/source/core/input/input_types.cpp b/source/core/input/input_types.cpp index f1e614c..3888c48 100644 --- a/source/core/input/input_types.cpp +++ b/source/core/input/input_types.cpp @@ -24,7 +24,9 @@ const std::unordered_map ACTION_TO_STRING = { {InputAction::NEXT_PALETTE_SORT, "NEXT_PALETTE_SORT"}, {InputAction::TOGGLE_SHADER, "TOGGLE_POSTFX"}, {InputAction::NEXT_SHADER_PRESET, "NEXT_POSTFX_PRESET"}, + {InputAction::TOGGLE_SUPERSAMPLING, "TOGGLE_SUPERSAMPLING"}, {InputAction::TOGGLE_INFO, "TOGGLE_DEBUG"}, + {InputAction::TOGGLE_CONSOLE, "TOGGLE_CONSOLE"}, {InputAction::SCREENSHOT, "SCREENSHOT"}, {InputAction::NONE, "NONE"}}; @@ -49,7 +51,9 @@ const std::unordered_map STRING_TO_ACTION = { {"NEXT_PALETTE_SORT", InputAction::NEXT_PALETTE_SORT}, {"TOGGLE_POSTFX", InputAction::TOGGLE_SHADER}, {"NEXT_POSTFX_PRESET", InputAction::NEXT_SHADER_PRESET}, + {"TOGGLE_SUPERSAMPLING", InputAction::TOGGLE_SUPERSAMPLING}, {"TOGGLE_DEBUG", InputAction::TOGGLE_INFO}, + {"TOGGLE_CONSOLE", InputAction::TOGGLE_CONSOLE}, {"SCREENSHOT", InputAction::SCREENSHOT}, {"NONE", InputAction::NONE}}; diff --git a/source/core/input/key_config.cpp b/source/core/input/key_config.cpp new file mode 100644 index 0000000..af31097 --- /dev/null +++ b/source/core/input/key_config.cpp @@ -0,0 +1,126 @@ +#include "core/input/key_config.hpp" + +#include // Para cerr +#include // Para string +#include // Para move +#include // Para vector + +#include "core/input/input.hpp" // Para Input +#include "core/input/input_types.hpp" // Para STRING_TO_ACTION +#include "core/resources/resource_helper.hpp" // Para Resource::Helper +#include "external/fkyaml_node.hpp" // Para fkyaml::node + +// ── Singleton ──────────────────────────────────────────────────────────────── + +KeyConfig* KeyConfig::instance_ = nullptr; + +void KeyConfig::init(const std::string& yaml_path) { + instance_ = new KeyConfig(); + instance_->load(yaml_path); +} + +void KeyConfig::destroy() { + delete instance_; + instance_ = nullptr; +} + +auto KeyConfig::get() -> KeyConfig* { return instance_; } + +// ── Carga del YAML ────────────────────────────────────────────────────────── + +void KeyConfig::load(const std::string& yaml_path) { + auto file_data = Resource::Helper::loadFile(yaml_path); + if (file_data.empty()) { + std::cerr << "KeyConfig: Unable to load " << yaml_path << '\n'; + return; + } + + std::string yaml_content(file_data.begin(), file_data.end()); + fkyaml::node yaml; + try { + yaml = fkyaml::node::deserialize(yaml_content); + } catch (const fkyaml::exception& e) { + std::cerr << "KeyConfig: YAML parse error: " << e.what() << '\n'; + return; + } + + if (!yaml.contains("scopes")) { return; } + + for (const auto& scope_node : yaml["scopes"]) { + KeyConfigScope scope; + scope.name = scope_node["name"].get_value(); + + if (scope_node.contains("keys")) { + for (const auto& key_node : scope_node["keys"]) { + KeyEntry entry; + entry.id = key_node["id"].get_value(); + entry.display_key = key_node["key"].get_value(); + entry.desc = key_node["desc"].get_value(); + + // Convertir el nombre SDL a keycode + if (key_node.contains("code")) { + auto code = key_node["code"].get_value(); + entry.keycode = SDL_GetKeyFromName(code.c_str()); + if (entry.keycode == SDLK_UNKNOWN) { + std::cerr << "KeyConfig: Unknown key name '" << code + << "' for " << scope.name << "." << entry.id << '\n'; + } + } + + // InputAction opcional (para scope GLOBAL) + if (key_node.contains("action")) { + entry.action = key_node["action"].get_value(); + } + + scope.index[entry.id] = scope.entries.size(); + scope.entries.push_back(std::move(entry)); + } + } + + scope_index_[scope.name] = scopes_.size(); + scopes_.push_back(std::move(scope)); + } + + std::cout << "KeyConfig: Loaded " << scopes_.size() << " scopes from " << yaml_path << '\n'; +} + +// ── Consultas ─────────────────────────────────────────────────────────────── + +auto KeyConfig::key(const std::string& scope, const std::string& id) const -> SDL_Keycode { + const auto* s = getScope(scope); + if (s == nullptr) { return SDLK_UNKNOWN; } + auto it = s->index.find(id); + if (it == s->index.end()) { return SDLK_UNKNOWN; } + return s->entries[it->second].keycode; +} + +void KeyConfig::applyGlobalBindings() const { + const auto* global = getScope("GLOBAL"); + if (global == nullptr) { return; } + + for (const auto& entry : global->entries) { + if (entry.action.empty() || entry.keycode == SDLK_UNKNOWN) { continue; } + + auto it = STRING_TO_ACTION.find(entry.action); + if (it == STRING_TO_ACTION.end()) { + std::cerr << "KeyConfig: Unknown action '" << entry.action << "' for " << entry.id << '\n'; + continue; + } + + // Convertir keycode a scancode para el sistema de Input + SDL_Scancode scancode = SDL_GetScancodeFromKey(entry.keycode, nullptr); + if (scancode != SDL_SCANCODE_UNKNOWN) { + Input::get()->bindKey(it->second, scancode); + } + } +} + +auto KeyConfig::getScopes() const -> const std::vector& { + return scopes_; +} + +auto KeyConfig::getScope(const std::string& name) const -> const KeyConfigScope* { + auto it = scope_index_.find(name); + if (it == scope_index_.end()) { return nullptr; } + return &scopes_[it->second]; +} diff --git a/source/core/input/key_config.hpp b/source/core/input/key_config.hpp new file mode 100644 index 0000000..40f1149 --- /dev/null +++ b/source/core/input/key_config.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include // Para string +#include // Para unordered_map +#include // Para vector + +// Entrada de tecla cargada desde keys.yaml +struct KeyEntry { + std::string id; // Identificador usado en código (ej. "grid", "tile_picker") + std::string display_key; // Texto para mostrar en HELP KEYS (ej. "G", "T") + std::string desc; // Descripción corta + SDL_Keycode keycode{SDLK_UNKNOWN}; // Tecla SDL asignada + std::string action; // (Opcional) Nombre de InputAction para scope GLOBAL +}; + +// Grupo de teclas por ámbito +struct KeyConfigScope { + std::string name; // "GLOBAL", "EDITOR", "MINIMAP", "DEBUG" + std::vector entries; + std::unordered_map index; // id → posición en entries +}; + +// Registro centralizado de teclas — fuente única de verdad (keys.yaml) +class KeyConfig { + public: + // Singleton + static void init(const std::string& yaml_path); + static void destroy(); + static auto get() -> KeyConfig*; + + // Consulta la SDL_Keycode asignada a un id dentro de un scope + [[nodiscard]] auto key(const std::string& scope, const std::string& id) const -> SDL_Keycode; + + // Aplica las teclas del scope GLOBAL al sistema de Input (Input::bindKey) + void applyGlobalBindings() const; + + // Acceso a scopes para HELP KEYS y para aplicar bindings globales + [[nodiscard]] auto getScopes() const -> const std::vector&; + [[nodiscard]] auto getScope(const std::string& name) const -> const KeyConfigScope*; + + private: + static KeyConfig* instance_; + + KeyConfig() = default; + ~KeyConfig() = default; + + void load(const std::string& yaml_path); + + std::vector scopes_; + std::unordered_map scope_index_; // nombre → posición en scopes_ +}; diff --git a/source/core/rendering/screenshot.cpp b/source/core/rendering/screenshot.cpp index 367bfa1..eeba243 100644 --- a/source/core/rendering/screenshot.cpp +++ b/source/core/rendering/screenshot.cpp @@ -9,9 +9,8 @@ #include // Para vector #define STB_IMAGE_WRITE_IMPLEMENTATION -#include "external/stb_image_write.h" // Para stbi_write_png - #include "core/rendering/surface.hpp" // Para Surface +#include "external/stb_image_write.h" // Para stbi_write_png namespace Screenshot { diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 6c42009..129e27b 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -13,6 +13,7 @@ #include "core/audio/audio.hpp" // Para Audio #include "core/input/input.hpp" // Para Input, InputAction +#include "core/input/key_config.hpp" // Para KeyConfig #include "core/locale/locale.hpp" // Para Locale #include "core/rendering/render_info.hpp" // Para RenderInfo #include "core/rendering/screen.hpp" // Para Screen @@ -186,6 +187,9 @@ Director::Director() { Console::init("8bithud"); Screen::get()->setNotificationsEnabled(true); + // Cargar configuración de teclas desde YAML (fuente única de verdad) + KeyConfig::init("data/input/keys.yaml"); + // Special handling for gamecontrollerdb.txt - SDL needs filesystem path #ifdef RELEASE_BUILD // In release, construct the path manually (not from Asset which has empty executable_path) @@ -196,6 +200,9 @@ Director::Director() { Input::init(Resource::List::get()->get("gamecontrollerdb.txt")); // Carga configuración de controles #endif + // Aplicar teclas globales desde KeyConfig al sistema de Input + KeyConfig::get()->applyGlobalBindings(); + // Aplica las teclas y botones del gamepad configurados desde Options Input::get()->applyKeyboardBindingsFromOptions(); Input::get()->applyGamepadBindingsFromOptions(); @@ -246,6 +253,7 @@ Director::~Director() { Debug::destroy(); #endif Input::destroy(); + KeyConfig::destroy(); Console::destroy(); RenderInfo::destroy(); Notifier::destroy(); diff --git a/source/game/editor/map_editor.cpp b/source/game/editor/map_editor.cpp index 134c655..2672e41 100644 --- a/source/game/editor/map_editor.cpp +++ b/source/game/editor/map_editor.cpp @@ -13,6 +13,7 @@ #include // Para set #include // Para std::error_code +#include "core/input/key_config.hpp" // Para KeyConfig #include "core/input/mouse.hpp" // Para Mouse #include "core/rendering/render_info.hpp" // Para RenderInfo #include "core/rendering/screen.hpp" // Para Screen @@ -434,8 +435,10 @@ void MapEditor::render() { void MapEditor::handleEvent(const SDL_Event& event) { // NOLINT(readability-function-cognitive-complexity) // Si el tile picker está abierto, los eventos van a él. // Excepción: la T lo cierra como toggle (sin tocar el brush). + const auto* kc = KeyConfig::get(); + if (tile_picker_.isOpen()) { - if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_T && static_cast(event.key.repeat) == 0) { + if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "tile_picker") && static_cast(event.key.repeat) == 0) { tile_picker_.close(); return; } @@ -446,7 +449,7 @@ void MapEditor::handleEvent(const SDL_Event& event) { // NOLINT(readability-fun // Si el mini mapa está visible, delegar eventos (ESC o M para cerrar) if (mini_map_visible_ && mini_map_) { if (event.type == SDL_EVENT_KEY_DOWN && - (event.key.key == SDLK_ESCAPE || event.key.key == SDLK_M) && + (event.key.key == kc->key("EDITOR", "cancel") || event.key.key == kc->key("EDITOR", "minimap")) && static_cast(event.key.repeat) == 0) { mini_map_visible_ = false; return; @@ -456,19 +459,19 @@ void MapEditor::handleEvent(const SDL_Event& event) { // NOLINT(readability-fun } // ESC: cancelar eyedropper en progreso (sin tocar el brush previo) - if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE && eyedropper_.active) { + if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "cancel") && eyedropper_.active) { eyedropper_.active = false; return; } // ESC: desactivar brush - if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE && !brush_.isEmpty()) { + if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "cancel") && !brush_.isEmpty()) { brush_.clear(); return; } // E: toggle borrador (alterna entre brush vacío y brush 1x1 ERASE) - if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_E && static_cast(event.key.repeat) == 0) { + if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "eraser") && static_cast(event.key.repeat) == 0) { if (brush_.width == 1 && brush_.height == 1 && !brush_.tiles.empty() && brush_.tiles[0] == BrushPattern::ERASE) { brush_.clear(); } else { @@ -478,13 +481,13 @@ void MapEditor::handleEvent(const SDL_Event& event) { // NOLINT(readability-fun } // M: toggle mini mapa - if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_M && static_cast(event.key.repeat) == 0) { + if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "minimap") && static_cast(event.key.repeat) == 0) { toggleMiniMap(); return; } // 8: alternar entre draw y collision - if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_8 && static_cast(event.key.repeat) == 0) { + if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "collision") && static_cast(event.key.repeat) == 0) { setEditingCollision(!editing_collision_); return; } @@ -492,21 +495,15 @@ void MapEditor::handleEvent(const SDL_Event& event) { // NOLINT(readability-fun // Cursores: navegar a habitación adyacente if (event.type == SDL_EVENT_KEY_DOWN && static_cast(event.key.repeat) == 0) { std::string direction; - switch (event.key.key) { - case SDLK_UP: - direction = "UP"; - break; - case SDLK_DOWN: - direction = "DOWN"; - break; - case SDLK_LEFT: - direction = "LEFT"; - break; - case SDLK_RIGHT: - direction = "RIGHT"; - break; - default: - break; + const auto NAV_KEY = event.key.key; + if (NAV_KEY == kc->key("EDITOR", "nav_up")) { + direction = "UP"; + } else if (NAV_KEY == kc->key("EDITOR", "nav_down")) { + direction = "DOWN"; + } else if (NAV_KEY == kc->key("EDITOR", "nav_left")) { + direction = "LEFT"; + } else if (NAV_KEY == kc->key("EDITOR", "nav_right")) { + direction = "RIGHT"; } if (!direction.empty() && GameControl::get_adjacent_room) { std::string adjacent = GameControl::get_adjacent_room(direction); @@ -523,7 +520,7 @@ void MapEditor::handleEvent(const SDL_Event& event) { // NOLINT(readability-fun } // T: abrir TilePicker (el cierre con T también se gestiona arriba, antes de delegar al picker) - if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_T && static_cast(event.key.repeat) == 0) { + if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "tile_picker") && static_cast(event.key.repeat) == 0) { // Deseleccionar entidades selection_.clear(); diff --git a/source/game/editor/mini_map.cpp b/source/game/editor/mini_map.cpp index 5e3ce97..8df4290 100644 --- a/source/game/editor/mini_map.cpp +++ b/source/game/editor/mini_map.cpp @@ -10,9 +10,10 @@ #include // Para queue (BFS) #include // Para set +#include "core/input/key_config.hpp" // Para KeyConfig #include "core/rendering/screen.hpp" // Para Screen -#include "core/rendering/surface.hpp" // Para Surface #include "core/rendering/screenshot.hpp" // Para Screenshot::save +#include "core/rendering/surface.hpp" // Para Surface #include "core/rendering/text.hpp" // Para Text (números de room) #include "core/resources/resource_cache.hpp" // Para Resource::Cache #include "game/gameplay/room.hpp" // Para Room::Data @@ -365,13 +366,15 @@ void MiniMap::render(const std::string& current_room) { // Maneja eventos del minimapa (drag para explorar, click para navegar) void MiniMap::handleEvent(const SDL_Event& event, const std::string& current_room) { // Toggle de números de room - if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_N && static_cast(event.key.repeat) == 0) { + const auto* kc = KeyConfig::get(); + + if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("MINIMAP", "numbers") && static_cast(event.key.repeat) == 0) { show_numbers_ = !show_numbers_; return; } // Captura del minimapa - if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_S && static_cast(event.key.repeat) == 0) { + if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("MINIMAP", "capture") && static_cast(event.key.repeat) == 0) { if (map_surface_) { // Renderizar números sobre map_surface_ si están activos if (show_numbers_) { diff --git a/source/game/editor/mini_map.hpp b/source/game/editor/mini_map.hpp index c51d618..7c32c8a 100644 --- a/source/game/editor/mini_map.hpp +++ b/source/game/editor/mini_map.hpp @@ -103,10 +103,10 @@ class MiniMap { static constexpr int PADDING = 4; // Padding alrededor del minimapa // Colores del minimapa (índices de paleta) - Uint8 bg_color_{2}; // Fondo general (configurable) - Uint8 conn_color_{14}; // Líneas de conexión (configurable) - static constexpr Uint8 COLOR_ROOM_BORDER = 0; // Borde de cada miniroom - static constexpr Uint8 COLOR_SHADOW = 1; // Sombra de cada miniroom + Uint8 bg_color_{2}; // Fondo general (configurable) + Uint8 conn_color_{14}; // Líneas de conexión (configurable) + static constexpr Uint8 COLOR_ROOM_BORDER = 0; // Borde de cada miniroom + static constexpr Uint8 COLOR_SHADOW = 1; // Sombra de cada miniroom static constexpr Uint8 COLOR_NUMBER_TEXT = 15; // Texto de números (blanco) static constexpr Uint8 COLOR_NUMBER_SHADOW = 1; // Sombra de números (oscuro) }; diff --git a/source/game/scenes/game.cpp b/source/game/scenes/game.cpp index 9b7be7f..a6e3423 100644 --- a/source/game/scenes/game.cpp +++ b/source/game/scenes/game.cpp @@ -9,6 +9,7 @@ #include "core/audio/audio.hpp" // Para Audio #include "core/input/global_inputs.hpp" // Para check #include "core/input/input.hpp" // Para Input, InputAction, Input::DO_NOT_ALLOW_REPEAT +#include "core/input/key_config.hpp" // Para KeyConfig #include "core/locale/locale.hpp" // Para Locale #include "core/rendering/screen.hpp" // Para Screen #include "core/rendering/surface.hpp" // Para Surface @@ -207,7 +208,7 @@ void Game::handleEvents() { if (!Console::get()->isActive()) { // Tecla 9: toggle editor (funciona tanto dentro como fuera del editor) - if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_9 && static_cast(event.key.repeat) == 0) { + if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == KeyConfig::get()->key("EDITOR", "toggle") && static_cast(event.key.repeat) == 0) { if (MapEditor::get()->isActive()) { GameControl::exit_editor(); Notifier::get()->show({Locale::get()->get("game.editor_disabled")}); @@ -215,7 +216,7 @@ void Game::handleEvents() { GameControl::enter_editor(); Notifier::get()->show({Locale::get()->get("game.editor_enabled")}); } - } else if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_G && static_cast(event.key.repeat) == 0 && MapEditor::get()->isActive()) { + } else if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == KeyConfig::get()->key("EDITOR", "grid") && static_cast(event.key.repeat) == 0 && MapEditor::get()->isActive()) { MapEditor::get()->showGrid(!MapEditor::get()->isGridEnabled()); } else if (MapEditor::get()->isActive()) { MapEditor::get()->handleEvent(event); @@ -640,59 +641,40 @@ void Game::renderDebugInfo() { // Comprueba los eventos void Game::handleDebugEvents(const SDL_Event& event) { if (event.type == SDL_EVENT_KEY_DOWN && static_cast(event.key.repeat) == 0) { - switch (event.key.key) { - case SDLK_R: - Resource::Cache::get()->reload(); - break; + const auto KEY = event.key.key; + const auto* kc = KeyConfig::get(); - case SDLK_W: - changeRoom(room_->getRoom(Room::Border::TOP)); - break; - - case SDLK_A: - changeRoom(room_->getRoom(Room::Border::LEFT)); - break; - - case SDLK_S: - changeRoom(room_->getRoom(Room::Border::BOTTOM)); - break; - - case SDLK_D: - changeRoom(room_->getRoom(Room::Border::RIGHT)); - break; - - case SDLK_1: - toggleCheat(Options::cheats.infinite_lives, Locale::get()->get("game.cheat_infinite_lives")); - break; - - case SDLK_2: - toggleCheat(Options::cheats.invincible, Locale::get()->get("game.cheat_invincible")); - break; - - case SDLK_7: - Notifier::get()->show({Locale::get()->get("achievements.header"), Locale::get()->get("achievements.c11")}, Notifier::Style::CHEEVO, -1, false, "F7"); - break; - - case SDLK_0: { - const bool ENTERING_DEBUG = !Debug::get()->isEnabled(); - if (ENTERING_DEBUG) { - invincible_before_debug_ = (Options::cheats.invincible == Options::Cheat::State::ENABLED); - } - Debug::get()->toggleEnabled(); - Notifier::get()->show({Debug::get()->isEnabled() ? Locale::get()->get("game.debug_enabled") : Locale::get()->get("game.debug_disabled")}); - room_->redrawMap(); - if (Debug::get()->isEnabled()) { - Options::cheats.invincible = Options::Cheat::State::ENABLED; - } else { - Options::cheats.invincible = invincible_before_debug_ ? Options::Cheat::State::ENABLED : Options::Cheat::State::DISABLED; - } - scoreboard_data_->music = !Debug::get()->isEnabled(); - scoreboard_data_->music ? Audio::get()->resumeMusic() : Audio::get()->pauseMusic(); - break; + if (KEY == kc->key("DEBUG", "reload")) { + Resource::Cache::get()->reload(); + } else if (KEY == kc->key("DEBUG", "nav_up")) { + changeRoom(room_->getRoom(Room::Border::TOP)); + } else if (KEY == kc->key("DEBUG", "nav_left")) { + changeRoom(room_->getRoom(Room::Border::LEFT)); + } else if (KEY == kc->key("DEBUG", "nav_down")) { + changeRoom(room_->getRoom(Room::Border::BOTTOM)); + } else if (KEY == kc->key("DEBUG", "nav_right")) { + changeRoom(room_->getRoom(Room::Border::RIGHT)); + } else if (KEY == kc->key("DEBUG", "infinite_lives")) { + toggleCheat(Options::cheats.infinite_lives, Locale::get()->get("game.cheat_infinite_lives")); + } else if (KEY == kc->key("DEBUG", "invincibility")) { + toggleCheat(Options::cheats.invincible, Locale::get()->get("game.cheat_invincible")); + } else if (KEY == kc->key("DEBUG", "test_cheevo")) { + Notifier::get()->show({Locale::get()->get("achievements.header"), Locale::get()->get("achievements.c11")}, Notifier::Style::CHEEVO, -1, false, "F7"); + } else if (KEY == kc->key("DEBUG", "debug_mode")) { + const bool ENTERING_DEBUG = !Debug::get()->isEnabled(); + if (ENTERING_DEBUG) { + invincible_before_debug_ = (Options::cheats.invincible == Options::Cheat::State::ENABLED); } - - default: - break; + Debug::get()->toggleEnabled(); + Notifier::get()->show({Debug::get()->isEnabled() ? Locale::get()->get("game.debug_enabled") : Locale::get()->get("game.debug_disabled")}); + room_->redrawMap(); + if (Debug::get()->isEnabled()) { + Options::cheats.invincible = Options::Cheat::State::ENABLED; + } else { + Options::cheats.invincible = invincible_before_debug_ ? Options::Cheat::State::ENABLED : Options::Cheat::State::DISABLED; + } + scoreboard_data_->music = !Debug::get()->isEnabled(); + scoreboard_data_->music ? Audio::get()->resumeMusic() : Audio::get()->pauseMusic(); } } } diff --git a/source/game/ui/console.cpp b/source/game/ui/console.cpp index 166857f..eab308f 100644 --- a/source/game/ui/console.cpp +++ b/source/game/ui/console.cpp @@ -43,7 +43,7 @@ static auto parseTokens(const std::string& input) -> std::vector { // Calcula la altura total de la consola para N líneas de mensaje (+ 1 línea de input) static auto calcTargetHeight(int num_msg_lines) -> float { constexpr int PADDING_IN_V = Console::TEXT_SIZE / 2; - return static_cast((Console::TEXT_SIZE * (num_msg_lines + 1)) + (PADDING_IN_V * 2)); + return static_cast((Console::LINE_HEIGHT * (num_msg_lines + 1)) + (PADDING_IN_V * 2)); } // Divide text en líneas respetando los \n existentes y haciendo word-wrap por ancho en píxeles @@ -173,7 +173,7 @@ void Console::redrawText() { const int VISIBLE = std::min(remaining, static_cast(line.size())); text_->writeColored(PADDING_IN_H, y_pos, line.substr(0, VISIBLE), MSG_COLOR); remaining -= VISIBLE; - y_pos += TEXT_SIZE; + y_pos += LINE_HEIGHT; } // Línea de input (siempre la última): prompt en PROMPT_COLOR, comando + cursor en COMMAND_COLOR diff --git a/source/game/ui/console.hpp b/source/game/ui/console.hpp index c6a30eb..3bef90b 100644 --- a/source/game/ui/console.hpp +++ b/source/game/ui/console.hpp @@ -29,7 +29,8 @@ class Console { void handleEvent(const SDL_Event& event); // Constantes públicas - static constexpr int TEXT_SIZE = 6; // Tamaño de carácter del font de la consola + static constexpr int TEXT_SIZE = 6; // Tamaño de carácter del font de la consola + static constexpr int LINE_HEIGHT = 7; // TEXT_SIZE + 1px de interlineado // Consultas auto isActive() -> bool; // true si RISING, ACTIVE o VANISHING diff --git a/source/game/ui/console_commands.cpp b/source/game/ui/console_commands.cpp index a4d2cad..8c176e4 100644 --- a/source/game/ui/console_commands.cpp +++ b/source/game/ui/console_commands.cpp @@ -10,6 +10,7 @@ #include // Para vector #include "core/audio/audio.hpp" // Para Audio +#include "core/input/key_config.hpp" // Para KeyConfig #include "core/locale/locale.hpp" // Para Locale #include "core/rendering/render_info.hpp" // Para RenderInfo #include "core/rendering/screen.hpp" // Para Screen @@ -1092,7 +1093,9 @@ void CommandRegistry::registerHandlers() { // NOLINT(readability-function-cogni // HELP: lista de comandos visibles en el scope activo dynamic_providers_["HELP"] = [this]() -> std::vector { - return getVisibleKeywords(); + auto kws = getVisibleKeywords(); + kws.insert(kws.begin(), "KEYS"); + return kws; }; dynamic_providers_["SET ITEMCOLOR1"] = color_provider; dynamic_providers_["SET ITEMCOLOR2"] = color_provider; @@ -1237,6 +1240,10 @@ void CommandRegistry::load(const std::string& yaml_path) { // NOLINT(readabilit // Registrar el handler de HELP (captura this) handlers_["cmd_help"] = [this](const std::vector& args) -> std::string { if (!args.empty()) { + // HELP KEYS [scope]: referencia de atajos de teclado + if (args[0] == "KEYS") { + return generateKeysHelp(args.size() > 1 ? args[1] : ""); + } // HELP : mostrar ayuda detallada de un comando const auto* cmd = findCommand(args[0]); if (cmd != nullptr) { @@ -1268,6 +1275,44 @@ void CommandRegistry::load(const std::string& yaml_path) { // NOLINT(readabilit completions_map_[path] = opts; } } + + // Proveedor dinámico para HELP KEYS (usa KeyConfig) + dynamic_providers_["HELP KEYS"] = []() -> std::vector { + std::vector names; + if (KeyConfig::get() != nullptr) { + for (const auto& scope : KeyConfig::get()->getScopes()) { + names.push_back(scope.name); + } + } + return names; + }; +} + +auto CommandRegistry::generateKeysHelp(const std::string& scope_filter) -> std::string { + if (KeyConfig::get() == nullptr) { return "KeyConfig not loaded"; } + + // Sin argumento: mostrar solo GLOBAL + const std::string FILTER = scope_filter.empty() ? "GLOBAL" : scope_filter; + const auto* scope = KeyConfig::get()->getScope(FILTER); + + if (scope == nullptr) { + std::string filter_lower = FILTER; + std::ranges::transform(filter_lower, filter_lower.begin(), ::tolower); + return "Unknown scope: " + filter_lower; + } + + // Cabecera del scope + std::string name_lower = scope->name; + std::ranges::transform(name_lower, name_lower.begin(), ::tolower); + std::string result = '[' + name_lower + "]\n"; + + // Una tecla por línea: key = desc + for (size_t i = 0; i < scope->entries.size(); ++i) { + const auto& entry = scope->entries[i]; + result += entry.display_key + " = " + entry.desc; + if (i + 1 < scope->entries.size()) { result += '\n'; } + } + return result; } auto CommandRegistry::findCommand(const std::string& keyword) const -> const CommandDef* { @@ -1346,7 +1391,6 @@ auto CommandRegistry::generateConsoleHelp() const -> std::string { // NOLINT(re if (active_scope_ == "editor" && !editor_cmds.empty()) { result += "Editor:\n" + editor_cmds + "\n"; - result += "keys: 9=editor g=grid 8=collision e=eraser m=map\n"; } if (!debug_cmds.empty()) { diff --git a/source/game/ui/console_commands.hpp b/source/game/ui/console_commands.hpp index b775402..b425439 100644 --- a/source/game/ui/console_commands.hpp +++ b/source/game/ui/console_commands.hpp @@ -58,4 +58,5 @@ class CommandRegistry { void registerHandlers(); [[nodiscard]] auto isCommandVisible(const CommandDef& cmd) const -> bool; + [[nodiscard]] static auto generateKeysHelp(const std::string& scope_filter) -> std::string; };