From fcf13591be4cdc31e21cba69a70fe1733cb28219 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 24 May 2026 20:17:30 +0200 Subject: [PATCH] feat(input): modul DefineInputs per redefinir teclat i mando MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Singleton inspirat en aee_arcade DefineButtons: pinta una caixa central modal, captura events SDL i avança per una sequencia fixa d'accions per jugador. Teclat: LEFT/RIGHT/FIRE/ACCELERATE. Mando: FIRE/ACCELERATE/ START/MENU. ESC cancel-la, duplicats dins la sessio es rebutgen. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/input/define_inputs.cpp | 401 ++++++++++++++++++++++++++++ source/core/input/define_inputs.hpp | 107 ++++++++ 2 files changed, 508 insertions(+) create mode 100644 source/core/input/define_inputs.cpp create mode 100644 source/core/input/define_inputs.hpp diff --git a/source/core/input/define_inputs.cpp b/source/core/input/define_inputs.cpp new file mode 100644 index 0000000..8f46847 --- /dev/null +++ b/source/core/input/define_inputs.cpp @@ -0,0 +1,401 @@ +// define_inputs.cpp - Implementacio de l'overlay modal de redefinicio +// © 2026 JailDesigner + +#include "core/input/define_inputs.hpp" + +#include +#include +#include +#include +#include + +#include "core/defaults/service_menu.hpp" +#include "core/input/input.hpp" +#include "core/locale/locale.hpp" +#include "core/types.hpp" +#include "game/config_yaml.hpp" + +namespace { + + constexpr float CANVAS_W = 1280.0F; + constexpr float CANVAS_H = 720.0F; + + // Llindar de trigger per a edge-detect L2/R2 com a boto virtual. + constexpr Sint16 TRIGGER_THRESHOLD = 16384; + + // Codis virtuals per als triggers (consistents amb input_types.cpp). + constexpr int TRIGGER_L2_VIRTUAL = 100; + constexpr int TRIGGER_R2_VIRTUAL = 101; + + // Durada del missatge de confirmacio abans de tancar-se. + constexpr float COMPLETE_DISPLAY_S = 1.5F; + + // Llindar dpad als axis sticks: no es captura per evitar conflicte amb el + // moviment LEFT/RIGHT/UP/DOWN (que es presuposen no redefinibles al mando). + constexpr Sint16 STICK_THRESHOLD = 16384; + + // Crida pushRect amb un SDL_Color (les components s'escalen a [0..1]). + void fillRect(Rendering::Renderer* renderer, float x, float y, float w, float h, SDL_Color color) { + renderer->pushRect(x, y, w, h, static_cast(color.r) / 255.0F, static_cast(color.g) / 255.0F, static_cast(color.b) / 255.0F, static_cast(color.a) / 255.0F); + } + + auto titleKey(System::DefineInputs::Mode mode, System::DefineInputs::Player player) -> std::string { + const bool IS_KB = (mode == System::DefineInputs::Mode::KEYBOARD); + const bool IS_P1 = (player == System::DefineInputs::Player::P1); + if (IS_KB && IS_P1) { + return "define.title_keyboard_p1"; + } + if (IS_KB) { + return "define.title_keyboard_p2"; + } + if (IS_P1) { + return "define.title_gamepad_p1"; + } + return "define.title_gamepad_p2"; + } + + // Scancodes que MAI capturem com a binding (reservats per a navegacio o + // global hotkeys). Evita que l'usuari es bloquegi ell mateix. + auto isReservedScancode(SDL_Scancode sc) -> bool { + if (sc == SDL_SCANCODE_ESCAPE) { + return true; // ESC sempre cancel·la + } + if (sc >= SDL_SCANCODE_F1 && sc <= SDL_SCANCODE_F12) { + return true; // F1-F12 globals + } + if (sc == SDL_SCANCODE_RETURN || sc == SDL_SCANCODE_KP_ENTER) { + return true; + } + if (sc == SDL_SCANCODE_BACKSPACE || sc == SDL_SCANCODE_TAB) { + return true; + } + return false; + } + + // Conversio sense pèrdua de SDL_Scancode → int per a comparacions + // homogenies dins de sequence_ (que guarda codis de tots dos modes). + auto scancodeToInt(SDL_Scancode sc) -> int { + return static_cast(sc); + } + +} // namespace + +namespace System { + + std::unique_ptr DefineInputs::instance; + + void DefineInputs::init(Rendering::Renderer* renderer) { + if (!instance) { + instance = std::unique_ptr(new DefineInputs(renderer)); + } + } + + void DefineInputs::destroy() { instance.reset(); } + + auto DefineInputs::get() -> DefineInputs* { return instance.get(); } + + DefineInputs::DefineInputs(Rendering::Renderer* renderer) + : renderer_(renderer), + text_(renderer) {} + + auto DefineInputs::isActive() const -> bool { + return phase_ != Phase::INACTIVE; + } + + auto DefineInputs::begin(Mode mode, Player player) -> bool { + if (mode == Mode::GAMEPAD) { + // Requereix un pad assignat al jugador. + const auto* input = Input::get(); + if (input == nullptr) { + return false; + } + const int IDX = (player == Player::P1) ? 0 : 1; + if (input->getPlayerGamepad(IDX) == nullptr) { + return false; + } + } + + mode_ = mode; + player_ = player; + index_ = 0; + complete_timer_s_ = 0.0F; + l2_was_pressed_ = false; + r2_was_pressed_ = false; + + buildSequence(); + phase_ = Phase::CAPTURING; + return true; + } + + void DefineInputs::cancel() { + phase_ = Phase::INACTIVE; + sequence_.clear(); + index_ = 0; + complete_timer_s_ = 0.0F; + } + + void DefineInputs::buildSequence() { + sequence_.clear(); + if (mode_ == Mode::KEYBOARD) { + // Teclat: LEFT, RIGHT, FIRE (SHOOT), ACCELERATE (THRUST) + sequence_.push_back({.action_label_key = "define.action.left", .action = InputAction::LEFT, .captured = -1}); + sequence_.push_back({.action_label_key = "define.action.right", .action = InputAction::RIGHT, .captured = -1}); + sequence_.push_back({.action_label_key = "define.action.fire", .action = InputAction::SHOOT, .captured = -1}); + sequence_.push_back({.action_label_key = "define.action.accelerate", .action = InputAction::THRUST, .captured = -1}); + } else { + // Mando: FIRE, ACCELERATE, START, MENU + sequence_.push_back({.action_label_key = "define.action.fire", .action = InputAction::SHOOT, .captured = -1}); + sequence_.push_back({.action_label_key = "define.action.accelerate", .action = InputAction::THRUST, .captured = -1}); + sequence_.push_back({.action_label_key = "define.action.start", .action = InputAction::START, .captured = -1}); + sequence_.push_back({.action_label_key = "define.action.menu", .action = InputAction::MENU, .captured = -1}); + } + } + + auto DefineInputs::isInUse(int code) const -> bool { + return std::ranges::any_of(sequence_, [code](const Step& s) { + return s.captured == code; + }); + } + + void DefineInputs::captureAndAdvance(int code) { + if (index_ >= sequence_.size()) { + return; + } + if (isInUse(code)) { + return; // Duplicat dins de la sessio: rebutgem silenciosament + } + sequence_[index_].captured = code; + ++index_; + if (index_ >= sequence_.size()) { + persistAndComplete(); + } + } + + void DefineInputs::persistAndComplete() { + auto& cfg = (player_ == Player::P1) + ? ConfigYaml::engine_config.player1 + : ConfigYaml::engine_config.player2; + + if (mode_ == Mode::KEYBOARD) { + for (const Step& s : sequence_) { + switch (s.action) { + case InputAction::LEFT: + cfg.keyboard.key_left = static_cast(s.captured); + break; + case InputAction::RIGHT: + cfg.keyboard.key_right = static_cast(s.captured); + break; + case InputAction::SHOOT: + cfg.keyboard.key_shoot = static_cast(s.captured); + break; + case InputAction::THRUST: + cfg.keyboard.key_thrust = static_cast(s.captured); + break; + default: + break; // START / MENU no es redefineixen al teclat + } + } + } else { + for (const Step& s : sequence_) { + switch (s.action) { + case InputAction::SHOOT: + cfg.gamepad.button_shoot = s.captured; + break; + case InputAction::THRUST: + cfg.gamepad.button_thrust = s.captured; + break; + case InputAction::START: + cfg.gamepad.button_start = s.captured; + break; + case InputAction::MENU: + cfg.gamepad.button_menu = s.captured; + break; + default: + break; // LEFT / RIGHT no es redefineixen al mando + } + } + } + + // Aplicar canvis al runtime de l'Input i persistir a disc. + if (auto* input = Input::get(); input != nullptr) { + if (player_ == Player::P1) { + input->applyPlayer1Bindings(ConfigYaml::engine_config.player1); + } else { + input->applyPlayer2Bindings(ConfigYaml::engine_config.player2); + } + } + ConfigYaml::saveToFile(); + + phase_ = Phase::COMPLETE; + complete_timer_s_ = COMPLETE_DISPLAY_S; + } + + void DefineInputs::update(float delta_time) { + if (phase_ != Phase::COMPLETE) { + return; + } + complete_timer_s_ -= delta_time; + if (complete_timer_s_ <= 0.0F) { + cancel(); + } + } + + void DefineInputs::processTrigger(int virtual_button, bool& was_pressed, bool now) { + if (now && !was_pressed) { + captureAndAdvance(virtual_button); + } + was_pressed = now; + } + + auto DefineInputs::handleKeyboardEvent(const SDL_Event& event) -> bool { + if (event.type != SDL_EVENT_KEY_DOWN) { + return true; // Empassem la resta sense fer res + } + const SDL_Scancode SC = event.key.scancode; + if (SC == SDL_SCANCODE_ESCAPE) { + cancel(); + return true; + } + if (isReservedScancode(SC)) { + return true; // Ignorada + } + captureAndAdvance(scancodeToInt(SC)); + return true; + } + + auto DefineInputs::handleGamepadEvent(const SDL_Event& event) -> bool { + // ESC al teclat tambe cancel·la durant rebind del mando. + if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE) { + cancel(); + return true; + } + + // Filtrar events al pad del jugador actiu. + const auto* input = Input::get(); + if (input == nullptr) { + return true; + } + const int IDX = (player_ == Player::P1) ? 0 : 1; + auto pad = input->getPlayerGamepad(IDX); + if (!pad) { + return true; + } + + if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) { + if (event.gbutton.which != pad->instance_id) { + return true; + } + captureAndAdvance(static_cast(event.gbutton.button)); + return true; + } + + if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) { + if (event.gaxis.which != pad->instance_id) { + return true; + } + const auto AXIS = static_cast(event.gaxis.axis); + const Sint16 VAL = event.gaxis.value; + if (AXIS == SDL_GAMEPAD_AXIS_LEFT_TRIGGER) { + processTrigger(TRIGGER_L2_VIRTUAL, l2_was_pressed_, VAL >= TRIGGER_THRESHOLD); + } else if (AXIS == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) { + processTrigger(TRIGGER_R2_VIRTUAL, r2_was_pressed_, VAL >= TRIGGER_THRESHOLD); + } + // Sticks LEFTX/LEFTY/RIGHTX/RIGHTY: ignorats (no son redefinibles). + (void)STICK_THRESHOLD; + return true; + } + + return true; + } + + auto DefineInputs::handleEvent(const SDL_Event& event) -> bool { + if (phase_ == Phase::INACTIVE) { + return false; + } + if (phase_ == Phase::COMPLETE) { + // Mentre mostrem el missatge OK, empassem els events sense capturar. + return true; + } + if (mode_ == Mode::KEYBOARD) { + return handleKeyboardEvent(event); + } + return handleGamepadEvent(event); + } + + void DefineInputs::draw() const { + if (phase_ == Phase::INACTIVE) { + return; + } + + using namespace Defaults::ServiceMenu; + + // Caixa centrada, dimensions fixes (no depen del contingut a redefinir). + constexpr float BOX_W = 560.0F; + constexpr float BOX_H = 280.0F; + const float BOX_X = (CANVAS_W - BOX_W) * 0.5F; + const float BOX_Y = (CANVAS_H - BOX_H) * 0.5F; + + // Fons + brackets als 4 cantons (estil HUD del menu de servei). + fillRect(renderer_, BOX_X, BOX_Y, BOX_W, BOX_H, BG_COLOR); + + const auto T = static_cast(CORNER_THICKNESS); + const auto AH = static_cast(CORNER_ARM_H); + const auto AV = static_cast(CORNER_ARM_V); + fillRect(renderer_, BOX_X, BOX_Y, AH, T, CORNER_COLOR); + fillRect(renderer_, BOX_X, BOX_Y, T, AV, CORNER_COLOR); + fillRect(renderer_, BOX_X + BOX_W - AH, BOX_Y, AH, T, CORNER_COLOR); + fillRect(renderer_, BOX_X + BOX_W - T, BOX_Y, T, AV, CORNER_COLOR); + fillRect(renderer_, BOX_X, BOX_Y + BOX_H - T, AH, T, CORNER_COLOR); + fillRect(renderer_, BOX_X, BOX_Y + BOX_H - AV, T, AV, CORNER_COLOR); + fillRect(renderer_, BOX_X + BOX_W - AH, BOX_Y + BOX_H - T, AH, T, CORNER_COLOR); + fillRect(renderer_, BOX_X + BOX_W - T, BOX_Y + BOX_H - AV, T, AV, CORNER_COLOR); + + const std::string TITLE = Locale::get().text(titleKey(mode_, player_)); + const float TITLE_W = Graphics::VectorText::getTextWidth(TITLE, TITLE_SCALE, TEXT_SPACING); + const float TITLE_X = BOX_X + ((BOX_W - TITLE_W) * 0.5F); + const float TITLE_Y = BOX_Y + 26.0F; + text_.render(TITLE, Vec2{.x = TITLE_X, .y = TITLE_Y}, TITLE_SCALE, TEXT_SPACING, 1.0F, TITLE_COLOR); + + if (phase_ == Phase::COMPLETE) { + const std::string OK = Locale::get().text("define.complete"); + constexpr float OK_SCALE = 0.7F; + const float OK_W = Graphics::VectorText::getTextWidth(OK, OK_SCALE, TEXT_SPACING); + const float OK_X = BOX_X + ((BOX_W - OK_W) * 0.5F); + const float OK_Y = BOX_Y + (BOX_H * 0.5F) - 10.0F; + constexpr SDL_Color OK_COLOR{.r = 120, .g = 255, .b = 140, .a = 255}; + text_.render(OK, Vec2{.x = OK_X, .y = OK_Y}, OK_SCALE, TEXT_SPACING, 1.0F, OK_COLOR); + return; + } + + // Instruccio (premeu tecla / boto) + accio actual + progres. + const std::string PROMPT = Locale::get().text( + mode_ == Mode::KEYBOARD ? "define.press_key" : "define.press_button"); + const float PROMPT_W = Graphics::VectorText::getTextWidth(PROMPT, ITEM_SCALE, TEXT_SPACING); + const float PROMPT_X = BOX_X + ((BOX_W - PROMPT_W) * 0.5F); + const float PROMPT_Y = BOX_Y + 86.0F; + text_.render(PROMPT, Vec2{.x = PROMPT_X, .y = PROMPT_Y}, ITEM_SCALE, TEXT_SPACING, 1.0F, SUBTITLE_COLOR); + + if (index_ < sequence_.size()) { + const std::string ACTION = Locale::get().text(sequence_[index_].action_label_key); + constexpr float ACTION_SCALE = 0.9F; + const float ACTION_W = Graphics::VectorText::getTextWidth(ACTION, ACTION_SCALE, TEXT_SPACING); + const float ACTION_X = BOX_X + ((BOX_W - ACTION_W) * 0.5F); + const float ACTION_Y = BOX_Y + 130.0F; + text_.render(ACTION, Vec2{.x = ACTION_X, .y = ACTION_Y}, ACTION_SCALE, TEXT_SPACING, 1.0F, CURSOR_COLOR); + } + + const std::string PROGRESS = std::format("{}/{}", index_ + 1, sequence_.size()); + constexpr float PROG_SCALE = 0.4F; + const float PROG_W = Graphics::VectorText::getTextWidth(PROGRESS, PROG_SCALE, TEXT_SPACING); + const float PROG_X = BOX_X + ((BOX_W - PROG_W) * 0.5F); + const float PROG_Y = BOX_Y + 200.0F; + text_.render(PROGRESS, Vec2{.x = PROG_X, .y = PROG_Y}, PROG_SCALE, TEXT_SPACING, 1.0F, LABEL_COLOR); + + const std::string CANCEL_HINT = Locale::get().text("define.cancel_hint"); + const float CH_W = Graphics::VectorText::getTextWidth(CANCEL_HINT, PROG_SCALE, TEXT_SPACING); + const float CH_X = BOX_X + ((BOX_W - CH_W) * 0.5F); + const float CH_Y = BOX_Y + BOX_H - 30.0F; + text_.render(CANCEL_HINT, Vec2{.x = CH_X, .y = CH_Y}, PROG_SCALE, TEXT_SPACING, 1.0F, SEPARATOR_COLOR); + } + +} // namespace System diff --git a/source/core/input/define_inputs.hpp b/source/core/input/define_inputs.hpp new file mode 100644 index 0000000..e117316 --- /dev/null +++ b/source/core/input/define_inputs.hpp @@ -0,0 +1,107 @@ +// define_inputs.hpp - Overlay modal de redefinici de controls (singleton) +// © 2026 JailDesigner +// +// Sub-mòdul inspirat en aee_arcade/source/core/input/define_buttons. Quan el +// menú de servei dispara una acció "Redefinir tecles/botons P1/P2", aquest +// singleton pren el control: pinta una caixa central, captura events SDL i +// avança per una seqüència fixa d'accions, persistint les noves assignacions +// a config.yaml en acabar. +// +// Cicle de vida: +// 1. begin(mode, player) → construeix la seqüència (4 passos) i activa +// l'overlay. Per a GAMEPAD, retorna false si el jugador no té pad. +// 2. handleEvent() captura el següent event vàlid; ESC cancel·la sense +// desar; duplicats dins de la sessió es rebutgen silenciosament. +// 3. Quan la seqüència es completa, persistim a engine_config + saveToFile, +// reapliquem els bindings i mostrem un missatge "OK" durant 1.5 s +// abans d'auto-tancar-se. +// +// El routing d'events es fa des de GlobalEvents::handle: mentre isActive() +// retorna true, tots els events SDL es desvien aquí i no arriben al joc ni +// al menú de servei. + +#pragma once + +#include + +#include +#include +#include +#include + +#include "core/graphics/vector_text.hpp" +#include "core/input/input_types.hpp" +#include "core/rendering/render_context.hpp" + +namespace System { + + class DefineInputs { + public: + enum class Mode : std::uint8_t { KEYBOARD, + GAMEPAD }; + enum class Player : std::uint8_t { P1, + P2 }; + + static void init(Rendering::Renderer* renderer); + static void destroy(); + [[nodiscard]] static auto get() -> DefineInputs*; + + // Comença la sessió. Retorna false per a GAMEPAD si el jugador no té + // cap pad assignat (el caller hauria de notificar a l'usuari abans). + auto begin(Mode mode, Player player) -> bool; + void cancel(); + + [[nodiscard]] auto isActive() const -> bool; + + void update(float delta_time); + void draw() const; + + // Retorna true si l'event s'ha consumit (és a dir, mentre l'overlay + // és actiu sempre consumeix tot per evitar passages al joc o menú). + auto handleEvent(const SDL_Event& event) -> bool; + + private: + explicit DefineInputs(Rendering::Renderer* renderer); + + enum class Phase : std::uint8_t { + INACTIVE, + CAPTURING, + COMPLETE, // mostra missatge OK breu abans d'auto-cancel + }; + + struct Step { + std::string action_label_key; // p.ex. "define.action.left" + InputAction action; // mapeig a la struct PlayerBindings + int captured{-1}; // scancode o button code; -1 = sense capturar + }; + + void buildSequence(); + [[nodiscard]] auto isInUse(int code) const -> bool; + void captureAndAdvance(int code); + void persistAndComplete(); + + // Handlers especialitzats segons mode_. + auto handleKeyboardEvent(const SDL_Event& event) -> bool; + auto handleGamepadEvent(const SDL_Event& event) -> bool; + + // Edge-detect per als triggers L2/R2 com a botons virtuals. + void processTrigger(int virtual_button, bool& was_pressed, bool now); + + Rendering::Renderer* renderer_; + Graphics::VectorText text_; + + Phase phase_{Phase::INACTIVE}; + Mode mode_{Mode::KEYBOARD}; + Player player_{Player::P1}; + std::vector sequence_; + std::size_t index_{0}; + float complete_timer_s_{0.0F}; + + // Estat d'edge-detect dels triggers durant la sessió GAMEPAD. + bool l2_was_pressed_{false}; + bool r2_was_pressed_{false}; + + static std::unique_ptr instance; + }; + +} // namespace System