diff --git a/source/core/input/input.cpp b/source/core/input/input.cpp index 69340fc5..38556f1c 100644 --- a/source/core/input/input.cpp +++ b/source/core/input/input.cpp @@ -43,6 +43,27 @@ void Input::applyKeyboardBindingsFromOptions() { bindKey(Action::JUMP, Options::controls.key_jump); } +// Aplica configuración de botones del gamepad desde Options al primer gamepad conectado +void Input::applyGamepadBindingsFromOptions() { + // Si no hay gamepads conectados, no hay nada que hacer + if (gamepads_.empty()) { + return; + } + + // Obtener el primer gamepad conectado + auto& gamepad = gamepads_[0]; + + // Aplicar bindings desde Options + // Los valores pueden ser: + // - 0-20+: Botones SDL_GamepadButton (DPAD, face buttons, shoulders) + // - 100: L2 trigger + // - 101: R2 trigger + // - 200+: Ejes del stick analógico + gamepad->bindings[Action::LEFT].button = Options::gamepad_controls.button_left; + gamepad->bindings[Action::RIGHT].button = Options::gamepad_controls.button_right; + gamepad->bindings[Action::JUMP].button = Options::gamepad_controls.button_jump; +} + // Asigna inputs a botones del mando void Input::bindGameControllerButton(const std::shared_ptr& gamepad, Action action, SDL_GamepadButton button) { if (gamepad != nullptr) { diff --git a/source/core/input/input.hpp b/source/core/input/input.hpp index 0a36db22..0cd916c6 100644 --- a/source/core/input/input.hpp +++ b/source/core/input/input.hpp @@ -122,6 +122,7 @@ class Input { // --- Métodos de configuración de controles --- void bindKey(Action action, SDL_Scancode code); void applyKeyboardBindingsFromOptions(); // Aplica las teclas configuradas desde Options + void applyGamepadBindingsFromOptions(); // Aplica los botones del gamepad configurados desde Options static void bindGameControllerButton(const std::shared_ptr& gamepad, Action action, SDL_GamepadButton button); static void bindGameControllerButton(const std::shared_ptr& gamepad, Action action_target, Action action_source); diff --git a/source/game/options.cpp b/source/game/options.cpp index 98a9cf9d..4794a2ba 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -179,6 +179,15 @@ auto saveToFile(const std::string& file_path) -> bool { file << "# Tecla para saltar (SDL_Scancode)\n"; file << "controls.jump " << static_cast(controls.key_jump) << "\n"; + file << "\n## GAMEPAD CONTROLS\n"; + file << "# Botón del gamepad para mover a la izquierda\n"; + file << "# Valores: 0-20+ = Botones SDL_GamepadButton, 100 = L2, 101 = R2, 200+ = Ejes\n"; + file << "gamepad_controls.left " << gamepad_controls.button_left << "\n\n"; + file << "# Botón del gamepad para mover a la derecha\n"; + file << "gamepad_controls.right " << gamepad_controls.button_right << "\n\n"; + file << "# Botón del gamepad para saltar\n"; + file << "gamepad_controls.jump " << gamepad_controls.button_jump << "\n"; + // Cierra el fichero file.close(); @@ -244,6 +253,18 @@ auto setOptions(const std::string& var, const std::string& value) -> bool { {"controls.jump", [](const std::string& v) { int val = safeStoi(v, SDL_SCANCODE_UP); controls.key_jump = static_cast(val); + }}, + {"gamepad_controls.left", [](const std::string& v) { + int val = safeStoi(v, static_cast(SDL_GAMEPAD_BUTTON_DPAD_LEFT)); + gamepad_controls.button_left = val; + }}, + {"gamepad_controls.right", [](const std::string& v) { + int val = safeStoi(v, static_cast(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)); + gamepad_controls.button_right = val; + }}, + {"gamepad_controls.jump", [](const std::string& v) { + int val = safeStoi(v, static_cast(SDL_GAMEPAD_BUTTON_WEST)); + gamepad_controls.button_jump = val; }}}; auto it = OPTION_HANDLERS.find(var); diff --git a/source/game/options.hpp b/source/game/options.hpp index 0ce1bd52..57295a18 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -58,6 +58,28 @@ struct ControlScheme { key_jump(jump) {} }; +// Estructura para las opciones de control del gamepad/joystick +// Los valores pueden ser: +// - 0-20+: Botones SDL_GamepadButton (DPAD, face buttons, shoulders, etc.) +// - 100: L2 trigger +// - 101: R2 trigger +// - 200: Left stick X axis (negativo = izquierda) +// - 201: Left stick X axis (positivo = derecha) +struct GamepadControlScheme { + int button_left{static_cast(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}; // Botón para mover a la izquierda (por defecto: DPAD_LEFT) + int button_right{static_cast(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}; // Botón para mover a la derecha (por defecto: DPAD_RIGHT) + int button_jump{static_cast(SDL_GAMEPAD_BUTTON_WEST)}; // Botón para saltar (por defecto: WEST/X button) + + // Constructor por defecto + GamepadControlScheme() = default; + + // Constructor + GamepadControlScheme(int left, int right, int jump) + : button_left(left), + button_right(right), + button_jump(jump) {} +}; + // Estructura para albergar trucos struct Cheat { enum class State : bool { @@ -234,6 +256,7 @@ inline Notification notifications{}; // Opciones relativas a las n inline Window window{}; // Opciones relativas a la ventana inline Audio audio{}; // Opciones relativas al audio inline ControlScheme controls{}; // Teclas usadas para jugar +inline GamepadControlScheme gamepad_controls{}; // Botones del gamepad usados para jugar // --- Funciones --- void init(); // Crea e inicializa las opciones del programa diff --git a/source/game/scenes/title.cpp b/source/game/scenes/title.cpp index 61ac2136..1e2c1293 100644 --- a/source/game/scenes/title.cpp +++ b/source/game/scenes/title.cpp @@ -115,8 +115,12 @@ void Title::handleEvents() { break; case SDLK_2: - // Redefinir joystick (futuro) - // controls_menu_state_ = ControlsMenuState::JOYSTICK_REMAP; + // Redefinir joystick - solo si hay gamepads conectados + if (Input::get()->gameControllerFound()) { + controls_menu_state_ = ControlsMenuState::JOYSTICK_REMAP; + remap_step_ = 0; + remap_error_message_.clear(); + } break; default: @@ -125,6 +129,9 @@ void Title::handleEvents() { } else if (controls_menu_state_ == ControlsMenuState::KEYBOARD_REMAP) { // Captura de teclas para redefinir handleControlsMenuKeyboardRemap(event); + } else if (controls_menu_state_ == ControlsMenuState::JOYSTICK_REMAP) { + // Captura de botones del gamepad para redefinir + handleControlsMenuJoystickRemap(event); } break; @@ -336,6 +343,16 @@ void Title::updateControlsMenu(float delta_time) { transitionToState(State::MAIN_MENU); } } + // Si estamos mostrando los botones definidos, esperar antes de guardar + else if (controls_menu_state_ == ControlsMenuState::JOYSTICK_REMAP_COMPLETE) { + state_time_ += delta_time; + if (state_time_ >= KEYBOARD_REMAP_DISPLAY_DELAY) { + // Aplicar y guardar los botones + applyJoystickRemap(); + // Volver al menu principal + transitionToState(State::MAIN_MENU); + } + } } // Actualiza el estado FADE_MENU @@ -484,12 +501,21 @@ void Title::renderControlsMenu() { if (controls_menu_state_ == ControlsMenuState::KEYBOARD_REMAP || controls_menu_state_ == ControlsMenuState::KEYBOARD_REMAP_COMPLETE) { renderKeyboardRemap(); + } else if (controls_menu_state_ == ControlsMenuState::JOYSTICK_REMAP || + controls_menu_state_ == ControlsMenuState::JOYSTICK_REMAP_COMPLETE) { + renderJoystickRemap(); } else { // Menu principal de controles const Uint8 COLOR = stringToColor("green"); + const Uint8 DISABLED_COLOR = stringToColor("dark grey"); const int TEXT_SIZE = menu_text_->getCharacterSize(); menu_text_->writeDX(TEXT_CENTER | TEXT_COLOR, PLAY_AREA_CENTER_X, 11 * TEXT_SIZE, "1. REDEFINE KEYBOARD", 1, COLOR); - menu_text_->writeDX(TEXT_CENTER | TEXT_COLOR, PLAY_AREA_CENTER_X, 13 * TEXT_SIZE, "2. REDEFINE JOYSTICK", 1, COLOR); + + // Deshabilitar opcion de joystick si no hay gamepads conectados + const bool gamepad_available = Input::get()->gameControllerFound(); + menu_text_->writeDX(TEXT_CENTER | TEXT_COLOR, PLAY_AREA_CENTER_X, 13 * TEXT_SIZE, + "2. REDEFINE JOYSTICK", 1, gamepad_available ? COLOR : DISABLED_COLOR); + menu_text_->writeDX(TEXT_CENTER | TEXT_COLOR, PLAY_AREA_CENTER_X, 17 * TEXT_SIZE, "ENTER TO GO BACK", 1, COLOR); } } @@ -662,4 +688,140 @@ void Title::renderKeyboardRemap() { if (!remap_error_message_.empty()) { menu_text_->writeDX(TEXT_CENTER | TEXT_COLOR, PLAY_AREA_CENTER_X, 18 * TEXT_SIZE, remap_error_message_, 1, ERROR_COLOR); } +} + +// Dibuja la pantalla de redefinir joystick +void Title::renderJoystickRemap() { + const Uint8 COLOR = stringToColor("green"); + const Uint8 ERROR_COLOR = stringToColor("red"); + const int TEXT_SIZE = menu_text_->getCharacterSize(); + + // Mensaje principal: "PRESS BUTTON FOR [ACTION]" o "BUTTONS DEFINED" si completado + if (remap_step_ >= 3) { + menu_text_->writeDX(TEXT_CENTER | TEXT_COLOR, PLAY_AREA_CENTER_X, 12 * TEXT_SIZE, "BUTTONS DEFINED", 1, COLOR); + } else { + const std::string ACTION = getActionName(remap_step_); + const std::string MESSAGE = "PRESS BUTTON FOR " + ACTION; + menu_text_->writeDX(TEXT_CENTER | TEXT_COLOR, PLAY_AREA_CENTER_X, 12 * TEXT_SIZE, MESSAGE, 1, COLOR); + } + + // Mostrar botones ya capturados + if (remap_step_ > 0) { + const std::string LEFT_BTN = getButtonName(temp_buttons_[0]); + const std::string LEFT_MSG = "LEFT: " + LEFT_BTN; + menu_text_->writeDX(TEXT_CENTER | TEXT_COLOR, PLAY_AREA_CENTER_X, 14 * TEXT_SIZE, LEFT_MSG, 1, COLOR); + } + if (remap_step_ > 1) { + const std::string RIGHT_BTN = getButtonName(temp_buttons_[1]); + const std::string RIGHT_MSG = "RIGHT: " + RIGHT_BTN; + menu_text_->writeDX(TEXT_CENTER | TEXT_COLOR, PLAY_AREA_CENTER_X, 15 * TEXT_SIZE, RIGHT_MSG, 1, COLOR); + } + if (remap_step_ >= 3) { + const std::string JUMP_BTN = getButtonName(temp_buttons_[2]); + const std::string JUMP_MSG = "JUMP: " + JUMP_BTN; + menu_text_->writeDX(TEXT_CENTER | TEXT_COLOR, PLAY_AREA_CENTER_X, 16 * TEXT_SIZE, JUMP_MSG, 1, COLOR); + } + + // Mensaje de error si existe + if (!remap_error_message_.empty()) { + menu_text_->writeDX(TEXT_CENTER | TEXT_COLOR, PLAY_AREA_CENTER_X, 18 * TEXT_SIZE, remap_error_message_, 1, ERROR_COLOR); + } +} + +// Maneja la captura de botones del gamepad para redefinir +void Title::handleControlsMenuJoystickRemap(const SDL_Event& event) { + int captured_button = -1; + + // Capturar botones del gamepad + if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) { + captured_button = static_cast(event.gbutton.button); + } + // Capturar triggers como botones (usando valores especiales 100/101) + else if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) { + constexpr Sint16 TRIGGER_THRESHOLD = 20000; + if (event.gaxis.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER && event.gaxis.value > TRIGGER_THRESHOLD) { + captured_button = Input::TRIGGER_L2_AS_BUTTON; // 100 + } else if (event.gaxis.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER && event.gaxis.value > TRIGGER_THRESHOLD) { + captured_button = Input::TRIGGER_R2_AS_BUTTON; // 101 + } + // Capturar ejes del stick analógico (usando valores especiales 200+) + else if (event.gaxis.axis == SDL_GAMEPAD_AXIS_LEFTX) { + constexpr Sint16 AXIS_THRESHOLD = 20000; + if (event.gaxis.value < -AXIS_THRESHOLD) { + captured_button = 200; // Left stick izquierda + } else if (event.gaxis.value > AXIS_THRESHOLD) { + captured_button = 201; // Left stick derecha + } + } + } + + // Si no se capturó ningún input válido, salir + if (captured_button == -1) { + return; + } + + // Verifica duplicados + if (isButtonDuplicate(captured_button, remap_step_)) { + remap_error_message_ = "BUTTON ALREADY USED! TRY ANOTHER"; + return; + } + + // Botón válido, guardar + temp_buttons_[remap_step_] = captured_button; + remap_error_message_.clear(); + remap_step_++; + + // Si completamos los 3 pasos, mostrar resultado y esperar + if (remap_step_ >= 3) { + controls_menu_state_ = ControlsMenuState::JOYSTICK_REMAP_COMPLETE; + state_time_ = 0.0F; // Resetear el timer para el delay + } +} + +// Valida si un botón está duplicado +bool Title::isButtonDuplicate(int button, int current_step) { + for (int i = 0; i < current_step; ++i) { + if (temp_buttons_[i] == button) { + return true; + } + } + return false; +} + +// Aplica y guarda los botones del gamepad redefinidos +void Title::applyJoystickRemap() { + // Guardar los nuevos botones en Options::gamepad_controls + Options::gamepad_controls.button_left = temp_buttons_[0]; + Options::gamepad_controls.button_right = temp_buttons_[1]; + Options::gamepad_controls.button_jump = temp_buttons_[2]; + + // Aplicar los bindings al sistema de Input + Input::get()->applyGamepadBindingsFromOptions(); + + // Guardar a archivo de configuracion + Options::saveToFile(Asset::get()->get("config.txt")); +} + +// Retorna el nombre amigable del botón del gamepad +std::string Title::getButtonName(int button) { + // Triggers especiales + if (button == Input::TRIGGER_L2_AS_BUTTON) { + return "L2"; + } + if (button == Input::TRIGGER_R2_AS_BUTTON) { + return "R2"; + } + + // Ejes del stick analógico + if (button == 200) { + return "LEFT STICK LEFT"; + } + if (button == 201) { + return "LEFT STICK RIGHT"; + } + + // Botones estándar SDL + const auto sdl_button = static_cast(button); + const char* button_name = SDL_GetGamepadStringForButton(sdl_button); + return button_name ? std::string(button_name) : "UNKNOWN"; } \ No newline at end of file diff --git a/source/game/scenes/title.hpp b/source/game/scenes/title.hpp index 1a49eee2..ae7b7bae 100644 --- a/source/game/scenes/title.hpp +++ b/source/game/scenes/title.hpp @@ -41,10 +41,11 @@ class Title { }; enum class ControlsMenuState { - MAIN, // Mostrar menu principal de controles - KEYBOARD_REMAP, // Redefinir teclas del teclado - KEYBOARD_REMAP_COMPLETE,// Mostrar teclas definidas antes de guardar - JOYSTICK_REMAP, // Redefinir botones del joystick (futuro) + MAIN, // Mostrar menu principal de controles + KEYBOARD_REMAP, // Redefinir teclas del teclado + KEYBOARD_REMAP_COMPLETE, // Mostrar teclas definidas antes de guardar + JOYSTICK_REMAP, // Redefinir botones del joystick + JOYSTICK_REMAP_COMPLETE, // Mostrar botones definidos antes de guardar }; // --- Constantes de tiempo (en segundos) --- @@ -89,7 +90,8 @@ class Title { ControlsMenuState controls_menu_state_; // Subestado del menu de controles int remap_step_; // Paso actual en la redefinicion (0=LEFT, 1=RIGHT, 2=JUMP) SDL_Scancode temp_keys_[3]; // Almacenamiento temporal de teclas capturadas - std::string remap_error_message_; // Mensaje de error si la tecla es invalida + int temp_buttons_[3]; // Almacenamiento temporal de botones de gamepad capturados + std::string remap_error_message_; // Mensaje de error si la tecla/boton es invalido // --- Funciones --- void update(); // Actualiza las variables @@ -113,12 +115,17 @@ class Title { void renderCheevosMenu(); // Dibuja el menu de logros void renderControlsMenu(); // Dibuja el menu de controles void renderKeyboardRemap(); // Dibuja la pantalla de redefinir teclado + void renderJoystickRemap(); // Dibuja la pantalla de redefinir joystick void moveCheevosList(int direction, float delta_time); // Desplaza la lista de logros (time-based) void handleControlsMenuKeyboardRemap(const SDL_Event& event); // Maneja la captura de teclas + void handleControlsMenuJoystickRemap(const SDL_Event& event); // Maneja la captura de botones del gamepad bool isKeyValid(SDL_Scancode scancode); // Valida si una tecla es permitida bool isKeyDuplicate(SDL_Scancode scancode, int current_step); // Valida si una tecla esta duplicada + bool isButtonDuplicate(int button, int current_step); // Valida si un boton esta duplicado void applyKeyboardRemap(); // Aplica y guarda las teclas redefinidas + void applyJoystickRemap(); // Aplica y guarda los botones del gamepad redefinidos std::string getActionName(int step); // Retorna el nombre de la accion (LEFT/RIGHT/JUMP) + std::string getButtonName(int button); // Retorna el nombre amigable del boton del gamepad void createCheevosTexture(); // Crea y rellena la surface para mostrar los logros void resetCheevosScroll(); // Resetea el scroll de la lista de logros void fillTitleSurface(); // Dibuja los elementos en la surface