// 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). Tornen true → handleEvent les deixa passar al pipeline // global perque facin la seua feina (ESC obre el prompt d'eixida, F1-F12 // son hotkeys de sistema, RETURN/BACKSPACE/TAB son navegacio). auto isReservedScancode(SDL_Scancode sc) -> bool { if (sc == SDL_SCANCODE_ESCAPE) { return true; } if (sc >= SDL_SCANCODE_F1 && sc <= SDL_SCANCODE_F12) { return true; } 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 (isReservedScancode(SC)) { // ESC, F1-F12, RETURN, BACKSPACE, TAB es deixen passar al pipeline // global (ESC obre el prompt d'eixida; F1-F12 hotkeys, etc.). return false; } captureAndAdvance(scancodeToInt(SC)); return true; } auto DefineInputs::handleGamepadEvent(const SDL_Event& event) -> bool { // KEY_DOWN no es per al rebind de mando: deixem que el global el // gestioni (ex. ESC → prompt d'eixida, F12 → tanca menu, etc.). if (event.type == SDL_EVENT_KEY_DOWN) { return false; } // 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; } // SDL_EVENT_QUIT i WINDOW_CLOSE_REQUESTED han de poder tancar la // finestra encara que el modal estiga obert; els passem al pipeline. if (event.type == SDL_EVENT_QUIT || event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) { return false; } if (phase_ == Phase::COMPLETE) { // Mentre mostrem el missatge OK, empassem la resta d'events sense // capturar perque l'usuari no puga avançar accions sense voler. 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); } } // namespace System