diff --git a/CMakeLists.txt b/CMakeLists.txt index d6d7ae3..520bd27 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,7 @@ set(APP_SOURCES # Core - Input (nova capa) source/core/input/gamepad.cpp source/core/input/global_inputs.cpp + source/core/input/key_remap.cpp source/core/input/mouse.cpp # Core - System (nova capa) diff --git a/source/core/input/gamepad.cpp b/source/core/input/gamepad.cpp index 4c611e9..acc9995 100644 --- a/source/core/input/gamepad.cpp +++ b/source/core/input/gamepad.cpp @@ -74,10 +74,10 @@ namespace Gamepad { pad_ = nullptr; pad_id_ = 0; // Neteja qualsevol tecla virtual que poguera estar premuda - JI_SetVirtualKey(SDL_SCANCODE_UP, false); - JI_SetVirtualKey(SDL_SCANCODE_DOWN, false); - JI_SetVirtualKey(SDL_SCANCODE_LEFT, false); - JI_SetVirtualKey(SDL_SCANCODE_RIGHT, false); + JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, false); + JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, false); + JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, false); + JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, false); } } } @@ -140,16 +140,16 @@ namespace Gamepad { if (b && !prev_b_) pushKey(SDL_SCANCODE_BACKSPACE); // Assegura que el joc no rep tecles de moviment mentre el menú està obert - JI_SetVirtualKey(SDL_SCANCODE_UP, false); - JI_SetVirtualKey(SDL_SCANCODE_DOWN, false); - JI_SetVirtualKey(SDL_SCANCODE_LEFT, false); - JI_SetVirtualKey(SDL_SCANCODE_RIGHT, false); + JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, false); + JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, false); + JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, false); + JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, false); } else { // Moviment al joc — level-triggered (polling) - JI_SetVirtualKey(SDL_SCANCODE_UP, up); - JI_SetVirtualKey(SDL_SCANCODE_DOWN, dn); - JI_SetVirtualKey(SDL_SCANCODE_LEFT, lt); - JI_SetVirtualKey(SDL_SCANCODE_RIGHT, rt); + JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, up); + JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, dn); + JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, lt); + JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, rt); // Botó A al joc: emet Enter per avançar seqüències (JI_AnyKey) if (a && !prev_a_) pushKey(SDL_SCANCODE_RETURN); } diff --git a/source/core/input/key_remap.cpp b/source/core/input/key_remap.cpp new file mode 100644 index 0000000..8543f40 --- /dev/null +++ b/source/core/input/key_remap.cpp @@ -0,0 +1,27 @@ +#include "core/input/key_remap.hpp" + +#include + +#include "core/jail/jinput.hpp" +#include "game/options.hpp" + +namespace KeyRemap { + + static void mirror(SDL_Scancode custom, SDL_Scancode standard, const bool* ks) { + if (custom == standard || custom == SDL_SCANCODE_UNKNOWN) { + JI_SetVirtualKey(standard, JI_VSRC_REMAP, false); + return; + } + JI_SetVirtualKey(standard, JI_VSRC_REMAP, ks[custom]); + } + + void update() { + const bool* ks = SDL_GetKeyboardState(nullptr); + if (!ks) return; + mirror(Options::keys_game.up, SDL_SCANCODE_UP, ks); + mirror(Options::keys_game.down, SDL_SCANCODE_DOWN, ks); + mirror(Options::keys_game.left, SDL_SCANCODE_LEFT, ks); + mirror(Options::keys_game.right, SDL_SCANCODE_RIGHT, ks); + } + +} // namespace KeyRemap diff --git a/source/core/input/key_remap.hpp b/source/core/input/key_remap.hpp new file mode 100644 index 0000000..7a61ecf --- /dev/null +++ b/source/core/input/key_remap.hpp @@ -0,0 +1,8 @@ +#pragma once + +// Remapeja tecles del joc: llig les tecles personalitzades de l'usuari +// (Options::keys_game) i les reflecteix a les tecles estàndard virtuals +// que el joc polla (SDL_SCANCODE_UP/DOWN/LEFT/RIGHT). +namespace KeyRemap { + void update(); // cridat cada frame des del Director +} diff --git a/source/core/jail/jinput.cpp b/source/core/jail/jinput.cpp index 6df9076..1ffd1c5 100644 --- a/source/core/jail/jinput.cpp +++ b/source/core/jail/jinput.cpp @@ -20,12 +20,12 @@ void JI_SetInputBlocked(bool blocked) { input_blocked = blocked; } -static Uint8 virtual_keystates[SDL_SCANCODE_COUNT] = {0}; +static Uint8 virtual_keystates[JI_VSRC_COUNT][SDL_SCANCODE_COUNT] = {{0}}; -void JI_SetVirtualKey(int scancode, bool pressed) { - if (scancode >= 0 && scancode < SDL_SCANCODE_COUNT) { - virtual_keystates[scancode] = pressed ? 1 : 0; - } +void JI_SetVirtualKey(int scancode, int source, bool pressed) { + if (scancode < 0 || scancode >= SDL_SCANCODE_COUNT) return; + if (source < 0 || source >= JI_VSRC_COUNT) return; + virtual_keystates[source][scancode] = pressed ? 1 : 0; } void JI_moveCheats(Uint8 new_key) { @@ -56,7 +56,11 @@ bool JI_KeyPressed(int key) { // ESC bloquejada pel Director (primera pulsació mostra notificació) if (key == SDL_SCANCODE_ESCAPE && Director::get()->isEscBlocked()) return false; if (key < 0 || key >= SDL_SCANCODE_COUNT) return false; - return keystates[key] != 0 || virtual_keystates[key] != 0; + if (keystates[key] != 0) return true; + for (int src = 0; src < JI_VSRC_COUNT; src++) { + if (virtual_keystates[src][key] != 0) return true; + } + return false; } bool JI_CheatActivated(const char* cheat_code) { diff --git a/source/core/jail/jinput.hpp b/source/core/jail/jinput.hpp index 0d2a36f..5f64f95 100644 --- a/source/core/jail/jinput.hpp +++ b/source/core/jail/jinput.hpp @@ -6,9 +6,15 @@ void JI_DisableKeyboard(Uint32 time); // Bloqueja tot l'input cap al joc (JI_KeyPressed retorna false per a tot) void JI_SetInputBlocked(bool blocked); -// Estableix l'estat d'una tecla virtual (p.ex. des del gamepad). -// JI_KeyPressed retorna true si el teclat real O la virtual estan premudes. -void JI_SetVirtualKey(int scancode, bool pressed); +// Estableix l'estat d'una tecla virtual. Múltiples fonts (gamepad, remap) +// s'agrupen per OR. JI_KeyPressed retorna true si el teclat real O qualsevol +// font virtual està premuda. +enum JI_VirtualSource { + JI_VSRC_GAMEPAD = 0, + JI_VSRC_REMAP = 1, + JI_VSRC_COUNT +}; +void JI_SetVirtualKey(int scancode, int source, bool pressed); void JI_Update(); diff --git a/source/core/rendering/menu.cpp b/source/core/rendering/menu.cpp index 8e68bbb..f447d30 100644 --- a/source/core/rendering/menu.cpp +++ b/source/core/rendering/menu.cpp @@ -41,7 +41,8 @@ namespace Menu { enum class ItemKind { Toggle, Cycle, IntRange, - Submenu }; + Submenu, + KeyBind }; struct Item { const char* label; @@ -49,6 +50,7 @@ namespace Menu { std::function getValue; // opcional std::function change; // per Toggle/Cycle/IntRange std::function enter; // per Submenu + SDL_Scancode* scancode{nullptr}; // per KeyBind }; struct Page { @@ -62,6 +64,7 @@ namespace Menu { static std::unique_ptr font_; static float open_anim_{0.0F}; // 0 = tancat, 1 = obert static Uint32 last_ticks_{0}; + static SDL_Scancode* capturing_{nullptr}; // != null → esperant tecla per assignar // --- Helpers --- @@ -72,11 +75,13 @@ namespace Menu { static Page buildVideo(); static Page buildAudio(); + static Page buildControls(); static Page buildRoot() { Page p{"OPCIONS", {}, 0}; - p.items.push_back({"VIDEO", ItemKind::Submenu, nullptr, nullptr, [] { stack_.push_back(buildVideo()); }}); - p.items.push_back({"AUDIO", ItemKind::Submenu, nullptr, nullptr, [] { stack_.push_back(buildAudio()); }}); + p.items.push_back({"VIDEO", ItemKind::Submenu, nullptr, nullptr, [] { stack_.push_back(buildVideo()); }, nullptr}); + p.items.push_back({"AUDIO", ItemKind::Submenu, nullptr, nullptr, [] { stack_.push_back(buildAudio()); }, nullptr}); + p.items.push_back({"CONTROLS", ItemKind::Submenu, nullptr, nullptr, [] { stack_.push_back(buildControls()); }, nullptr}); return p; } @@ -137,6 +142,16 @@ namespace Menu { Options::applyAudio(); } + static Page buildControls() { + Page p{"CONTROLS", {}, 0}; + p.items.push_back({"MOU AMUNT", ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.up}); + p.items.push_back({"MOU AVALL", ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.down}); + p.items.push_back({"MOU ESQUERRA", ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.left}); + p.items.push_back({"MOU DRETA", ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.right}); + p.items.push_back({"TECLA MENU", ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_gui.menu_toggle}); + return p; + } + static Page buildAudio() { Page p{"AUDIO", {}, 0}; @@ -242,6 +257,22 @@ namespace Menu { void close() { stack_.clear(); open_anim_ = 0.0F; + capturing_ = nullptr; + } + + auto isCapturing() -> bool { + return capturing_ != nullptr; + } + + void captureKey(SDL_Scancode sc) { + if (!capturing_) return; + if (sc == SDL_SCANCODE_ESCAPE) { + // Cancel·la + capturing_ = nullptr; + return; + } + *capturing_ = sc; + capturing_ = nullptr; } void handleKey(SDL_Scancode sc) { @@ -279,6 +310,8 @@ namespace Menu { case SDL_SCANCODE_KP_ENTER: if (page.items[page.cursor].kind == ItemKind::Submenu) { if (page.items[page.cursor].enter) page.items[page.cursor].enter(); + } else if (page.items[page.cursor].kind == ItemKind::KeyBind) { + capturing_ = page.items[page.cursor].scancode; } else if (page.items[page.cursor].change) { page.items[page.cursor].change(+1); } @@ -363,6 +396,13 @@ namespace Menu { int aw = font_->width(arrow); Uint32 ac = selected ? CURSOR_COLOR : VALUE_COLOR; font_->draw(pixel_data, box_x + BOX_W - ITEM_PAD_X - aw, y, arrow, ac); + } else if (item.kind == ItemKind::KeyBind) { + bool this_capturing = (capturing_ == item.scancode); + const char* text = this_capturing ? "" : (item.scancode ? SDL_GetScancodeName(*item.scancode) : ""); + if (!text || !*text) text = "---"; + int tw = font_->width(text); + Uint32 tc = this_capturing ? 0xFF00FFFF : (selected ? CURSOR_COLOR : VALUE_COLOR); + font_->draw(pixel_data, box_x + BOX_W - ITEM_PAD_X - tw, y, text, tc); } else if (item.getValue) { std::string value = item.getValue(); int value_w = font_->width(value.c_str()); diff --git a/source/core/rendering/menu.hpp b/source/core/rendering/menu.hpp index 49829b8..3dd2bfb 100644 --- a/source/core/rendering/menu.hpp +++ b/source/core/rendering/menu.hpp @@ -15,4 +15,8 @@ namespace Menu { // Gestió d'input — cridat des del Director en KEY_DOWN void handleKey(SDL_Scancode sc); + + // Mode de captura de tecla (per al menú de remapeig) + [[nodiscard]] auto isCapturing() -> bool; + void captureKey(SDL_Scancode sc); // assigna la tecla capturada (ESC cancel·la) } // namespace Menu diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index cfaf975..07ef265 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -5,6 +5,7 @@ #include "core/input/gamepad.hpp" #include "core/input/global_inputs.hpp" +#include "core/input/key_remap.hpp" #include "core/input/mouse.hpp" #include "core/jail/jgame.hpp" #include "core/jail/jinput.hpp" @@ -54,6 +55,7 @@ void Director::run() { handleEvents(); Gamepad::update(); + KeyRemap::update(); GlobalInputs::handle(); Mouse::updateCursorVisibility(); @@ -117,11 +119,24 @@ void Director::handleEvents() { Gamepad::handleEvent(event); continue; } + // Empassar-se el KEY_UP de qualsevol tecla que el menú va consumir en KEY_DOWN + if (event.type == SDL_EVENT_KEY_UP && event.key.scancode >= 0 && + event.key.scancode < SDL_SCANCODE_COUNT && menu_keys_held_[event.key.scancode]) { + menu_keys_held_[event.key.scancode] = false; + continue; + } + // Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot + if (Menu::isCapturing() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) { + Menu::captureKey(event.key.scancode); + menu_keys_held_[event.key.scancode] = true; + continue; + } // Menú: F12 (o tecla configurada) obre/tanca el menú flotant if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && event.key.scancode == Options::keys_gui.menu_toggle) { Menu::toggle(); JI_SetInputBlocked(Menu::isOpen()); + menu_keys_held_[event.key.scancode] = true; continue; } // Si el menú està obert, consumeix tot l'input de teclat @@ -133,7 +148,12 @@ void Director::handleEvents() { esc_swallow_until_release_ = true; } else { Menu::handleKey(event.key.scancode); + // El menú pot haver-se tancat (p.ex. Backspace al nivell arrel) + if (!Menu::isOpen()) { + JI_SetInputBlocked(false); + } } + menu_keys_held_[event.key.scancode] = true; continue; } if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) { diff --git a/source/core/system/director.hpp b/source/core/system/director.hpp index 6df3fef..bc7e2bd 100644 --- a/source/core/system/director.hpp +++ b/source/core/system/director.hpp @@ -59,4 +59,7 @@ class Director { // Quan el menú tanca amb ESC, empassem-nos l'ESC fins que l'usuari la deixe anar, // per no fer eixir el joc al proper poll de JI_KeyPressed. std::atomic esc_swallow_until_release_{false}; + // Tecles consumides pel menú (KEY_DOWN): el KEY_UP associat cal empassar-lo + // per evitar que el joc (JI_AnyKey / JI_moveCheats) les veja quan el menú tanca. + bool menu_keys_held_[SDL_SCANCODE_COUNT]{}; }; diff --git a/source/game/options.cpp b/source/game/options.cpp index 0aaf8d9..0a4e71f 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -110,6 +110,25 @@ namespace Options { window.fullscreen = node["fullscreen"].get_value(); } + // Helper: carrega una SDL_Scancode des d'un string (nom SDL de la tecla). + static void loadScancodeField(const fkyaml::node& node, const std::string& key, SDL_Scancode& target) { + if (!node.contains(key)) return; + auto name = node[key].get_value(); + SDL_Scancode sc = SDL_GetScancodeFromName(name.c_str()); + if (sc != SDL_SCANCODE_UNKNOWN) target = sc; + } + + static void loadControlsFromYaml(const fkyaml::node& yaml) { + if (yaml.contains("controls")) { + const auto& node = yaml["controls"]; + loadScancodeField(node, "up", keys_game.up); + loadScancodeField(node, "down", keys_game.down); + loadScancodeField(node, "left", keys_game.left); + loadScancodeField(node, "right", keys_game.right); + loadScancodeField(node, "menu_toggle", keys_gui.menu_toggle); + } + } + static void loadGameConfigFromYaml(const fkyaml::node& yaml) { if (!yaml.contains("game")) return; const auto& node = yaml["game"]; @@ -158,6 +177,7 @@ namespace Options { loadWindowConfigFromYaml(yaml); loadAudioConfigFromYaml(yaml); loadGameConfigFromYaml(yaml); + loadControlsFromYaml(yaml); std::cout << "Config file loaded successfully\n\n"; return true; @@ -247,6 +267,16 @@ namespace Options { file << " habitacio_inicial: " << game.habitacio_inicial << "\n"; file << " piramide_inicial: " << game.piramide_inicial << "\n"; file << " vides: " << game.vides << "\n"; + file << "\n"; + + // CONTROLS + file << "# CONTROLS (noms SDL: \"Up\", \"Down\", \"W\", \"Space\", \"F12\", etc.)\n"; + file << "controls:\n"; + file << " up: \"" << SDL_GetScancodeName(keys_game.up) << "\"\n"; + file << " down: \"" << SDL_GetScancodeName(keys_game.down) << "\"\n"; + file << " left: \"" << SDL_GetScancodeName(keys_game.left) << "\"\n"; + file << " right: \"" << SDL_GetScancodeName(keys_game.right) << "\"\n"; + file << " menu_toggle: \"" << SDL_GetScancodeName(keys_gui.menu_toggle) << "\"\n"; file.close();