key_config.cpp i keys.yaml per a centralitzar i no hardcodejar tecles

This commit is contained in:
2026-04-12 15:21:43 +02:00
parent 999cf53821
commit 848b658611
19 changed files with 521 additions and 115 deletions

View File

@@ -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

View File

@@ -35,6 +35,7 @@ assets:
input:
DATA:
- ${PREFIX}/gamecontrollerdb.txt
- ${PREFIX}/data/input/keys.yaml
# SYSTEM
system:

View File

@@ -182,7 +182,7 @@ categories:
- keyword: HELP
handler: cmd_help
description: "Show this help"
usage: "HELP [<command>]"
usage: "HELP [KEYS [scope]|<command>]"
dynamic_completions: true
- keyword: "?"

203
data/input/keys.yaml Normal file
View File

@@ -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"

View File

@@ -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
}

View File

@@ -24,7 +24,9 @@ const std::unordered_map<InputAction, std::string> 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<std::string, InputAction> 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}};

View File

@@ -0,0 +1,126 @@
#include "core/input/key_config.hpp"
#include <iostream> // Para cerr
#include <string> // Para string
#include <utility> // Para move
#include <vector> // 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<std::string>();
if (scope_node.contains("keys")) {
for (const auto& key_node : scope_node["keys"]) {
KeyEntry entry;
entry.id = key_node["id"].get_value<std::string>();
entry.display_key = key_node["key"].get_value<std::string>();
entry.desc = key_node["desc"].get_value<std::string>();
// Convertir el nombre SDL a keycode
if (key_node.contains("code")) {
auto code = key_node["code"].get_value<std::string>();
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<std::string>();
}
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<KeyConfigScope>& {
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];
}

View File

@@ -0,0 +1,53 @@
#pragma once
#include <SDL3/SDL.h>
#include <string> // Para string
#include <unordered_map> // Para unordered_map
#include <vector> // 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<KeyEntry> entries;
std::unordered_map<std::string, size_t> 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<KeyConfigScope>&;
[[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<KeyConfigScope> scopes_;
std::unordered_map<std::string, size_t> scope_index_; // nombre → posición en scopes_
};

View File

@@ -9,9 +9,8 @@
#include <vector> // 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 {

View File

@@ -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();

View File

@@ -13,6 +13,7 @@
#include <set> // Para set
#include <system_error> // 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<int>(event.key.repeat) == 0) {
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "tile_picker") && static_cast<int>(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<int>(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<int>(event.key.repeat) == 0) {
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "eraser") && static_cast<int>(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<int>(event.key.repeat) == 0) {
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "minimap") && static_cast<int>(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<int>(event.key.repeat) == 0) {
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "collision") && static_cast<int>(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<int>(event.key.repeat) == 0) {
std::string direction;
switch (event.key.key) {
case SDLK_UP:
const auto NAV_KEY = event.key.key;
if (NAV_KEY == kc->key("EDITOR", "nav_up")) {
direction = "UP";
break;
case SDLK_DOWN:
} else if (NAV_KEY == kc->key("EDITOR", "nav_down")) {
direction = "DOWN";
break;
case SDLK_LEFT:
} else if (NAV_KEY == kc->key("EDITOR", "nav_left")) {
direction = "LEFT";
break;
case SDLK_RIGHT:
} else if (NAV_KEY == kc->key("EDITOR", "nav_right")) {
direction = "RIGHT";
break;
default:
break;
}
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<int>(event.key.repeat) == 0) {
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "tile_picker") && static_cast<int>(event.key.repeat) == 0) {
// Deseleccionar entidades
selection_.clear();

View File

@@ -10,9 +10,10 @@
#include <queue> // Para queue (BFS)
#include <set> // 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<int>(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<int>(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<int>(event.key.repeat) == 0) {
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("MINIMAP", "capture") && static_cast<int>(event.key.repeat) == 0) {
if (map_surface_) {
// Renderizar números sobre map_surface_ si están activos
if (show_numbers_) {

View File

@@ -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<int>(event.key.repeat) == 0) {
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == KeyConfig::get()->key("EDITOR", "toggle") && static_cast<int>(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<int>(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<int>(event.key.repeat) == 0 && MapEditor::get()->isActive()) {
MapEditor::get()->showGrid(!MapEditor::get()->isGridEnabled());
} else if (MapEditor::get()->isActive()) {
MapEditor::get()->handleEvent(event);
@@ -640,40 +641,26 @@ void Game::renderDebugInfo() {
// Comprueba los eventos
void Game::handleDebugEvents(const SDL_Event& event) {
if (event.type == SDL_EVENT_KEY_DOWN && static_cast<int>(event.key.repeat) == 0) {
switch (event.key.key) {
case SDLK_R:
const auto KEY = event.key.key;
const auto* kc = KeyConfig::get();
if (KEY == kc->key("DEBUG", "reload")) {
Resource::Cache::get()->reload();
break;
case SDLK_W:
} else if (KEY == kc->key("DEBUG", "nav_up")) {
changeRoom(room_->getRoom(Room::Border::TOP));
break;
case SDLK_A:
} else if (KEY == kc->key("DEBUG", "nav_left")) {
changeRoom(room_->getRoom(Room::Border::LEFT));
break;
case SDLK_S:
} else if (KEY == kc->key("DEBUG", "nav_down")) {
changeRoom(room_->getRoom(Room::Border::BOTTOM));
break;
case SDLK_D:
} else if (KEY == kc->key("DEBUG", "nav_right")) {
changeRoom(room_->getRoom(Room::Border::RIGHT));
break;
case SDLK_1:
} else if (KEY == kc->key("DEBUG", "infinite_lives")) {
toggleCheat(Options::cheats.infinite_lives, Locale::get()->get("game.cheat_infinite_lives"));
break;
case SDLK_2:
} else if (KEY == kc->key("DEBUG", "invincibility")) {
toggleCheat(Options::cheats.invincible, Locale::get()->get("game.cheat_invincible"));
break;
case SDLK_7:
} 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");
break;
case SDLK_0: {
} 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);
@@ -688,11 +675,6 @@ void Game::handleDebugEvents(const SDL_Event& event) {
}
scoreboard_data_->music = !Debug::get()->isEnabled();
scoreboard_data_->music ? Audio::get()->resumeMusic() : Audio::get()->pauseMusic();
break;
}
default:
break;
}
}
}

View File

@@ -43,7 +43,7 @@ static auto parseTokens(const std::string& input) -> std::vector<std::string> {
// 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<float>((Console::TEXT_SIZE * (num_msg_lines + 1)) + (PADDING_IN_V * 2));
return static_cast<float>((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<int>(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

View File

@@ -30,6 +30,7 @@ class Console {
// Constantes públicas
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

View File

@@ -10,6 +10,7 @@
#include <vector> // 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<std::string> {
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<std::string>& 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 <command>: 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 <scope> (usa KeyConfig)
dynamic_providers_["HELP KEYS"] = []() -> std::vector<std::string> {
std::vector<std::string> 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()) {

View File

@@ -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;
};