#include "game/scenes/title.hpp" #include #include // Para clamp #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, REP... #include "core/locale/locale.hpp" // Para Locale #include "core/rendering/screen.hpp" // Para Screen #include "core/rendering/surface.hpp" // Para Surface #include "core/rendering/surface_sprite.hpp" // Para SSprite #include "core/rendering/text.hpp" // Para Text, Text::CENTER_FLAG, Text::COLOR_FLAG #include "core/resources/resource_cache.hpp" // Para Resource #include "core/resources/resource_list.hpp" // Para Asset #include "core/system/global_events.hpp" // Para check #include "game/gameplay/cheevos.hpp" // Para Cheevos, Achievement #include "game/options.hpp" // Para Options, options, SectionState, Section #include "game/scene_manager.hpp" // Para SceneManager #include "utils/defines.hpp" // Para PlayArea::CENTER_X, GameCanvas::WIDTH #include "utils/utils.hpp" // Para stringToColor, PaletteColor, playMusic // Constructor Title::Title() : game_logo_surface_(Resource::Cache::get()->getSurface("title_logo.gif")), game_logo_sprite_(std::make_unique(game_logo_surface_, 29, 9, game_logo_surface_->getWidth(), game_logo_surface_->getHeight())), loading_screen_surface_(Resource::Cache::get()->getSurface("loading_screen_color.gif")), loading_screen_sprite_(std::make_unique(loading_screen_surface_, 0, 0, loading_screen_surface_->getWidth(), loading_screen_surface_->getHeight())), title_surface_(std::make_shared(Options::game.width, Options::game.height)), delta_timer_(std::make_unique()), marquee_text_(Resource::Cache::get()->getText("gauntlet")), menu_text_(Resource::Cache::get()->getText("gauntlet")) { // Inicializa arrays con valores por defecto temp_keys_.fill(SDL_SCANCODE_UNKNOWN); temp_buttons_.fill(-1); // Determina el estado inicial basado en opciones state_ = SceneManager::options == SceneManager::Options::TITLE_WITH_LOADING_SCREEN ? State::SHOW_LOADING_SCREEN : State::MAIN_MENU; // Establece SceneManager SceneManager::current = SceneManager::Scene::TITLE; SceneManager::options = SceneManager::Options::NONE; // Acciones iniciales initMarquee(); // Inicializa la marquesina createCheevosTexture(); // Crea y rellena la textura para mostrar los logros Screen::get()->setBorderColor(static_cast(PaletteColor::BLACK)); // Cambia el color del borde Audio::get()->playMusic("title.ogg"); // Inicia la musica } // Destructor Title::~Title() { loading_screen_surface_->resetSubPalette(); title_surface_->resetSubPalette(); } // Inicializa la marquesina void Title::initMarquee() { letters_.clear(); long_text_ = Locale::get()->get("title.marquee"); // Pre-calcular anchos de caracteres para eficiencia (iteración por codepoints UTF-8) size_t pos = 0; while (pos < long_text_.size()) { uint32_t cp = Text::nextCodepoint(long_text_, pos); Glyph l; l.codepoint = cp; l.clip = marquee_text_->getGlyphClip(cp); // Pre-calcular clip rect (evita búsqueda por frame) l.x = MARQUEE_START_X; l.width = static_cast(marquee_text_->glyphWidth(cp, 0)); // Pre-calcular ancho visual del glifo l.enabled = false; letters_.push_back(l); } if (!letters_.empty()) { letters_[0].enabled = true; } first_active_letter_ = 0; last_active_letter_ = 0; } // Comprueba el manejador de eventos void Title::handleEvents() { SDL_Event event; while (SDL_PollEvent(&event)) { GlobalEvents::handle(event); // Manejo especial para captura de botones de gamepad if (is_remapping_joystick_ && !remap_completed_ && (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN || event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION)) { handleJoystickRemap(event); continue; // No procesar más este evento } if (event.type == SDL_EVENT_KEY_DOWN) { // Si estamos en modo remap de teclado, capturar tecla if (is_remapping_keyboard_ && !remap_completed_) { handleKeyboardRemap(event); } // Si estamos en el menú principal normal else if (state_ == State::MAIN_MENU && !is_remapping_keyboard_ && !is_remapping_joystick_) { handleMainMenuKeyPress(event.key.key); } } } } // Maneja las teclas del menu principal void Title::handleMainMenuKeyPress(SDL_Keycode key) { switch (key) { case SDLK_1: // PLAY exit_scene_ = SceneManager::Scene::GAME; transitionToState(State::FADE_MENU); Audio::get()->fadeOutMusic(1000); break; case SDLK_2: // REDEFINE KEYBOARD is_remapping_keyboard_ = true; is_remapping_joystick_ = false; remap_step_ = 0; remap_completed_ = false; remap_error_message_.clear(); state_time_ = 0.0F; break; case SDLK_3: // REDEFINE JOYSTICK (siempre visible, pero solo funciona si hay gamepad) if (Input::get()->gameControllerFound()) { is_remapping_keyboard_ = false; is_remapping_joystick_ = true; remap_step_ = 0; remap_completed_ = false; remap_error_message_.clear(); axis_cooldown_ = 0.0F; state_time_ = 0.0F; } // Si no hay gamepad, simplemente no hacer nada break; case SDLK_4: // PROJECTS transitionToState(State::CHEEVOS_MENU); break; default: break; } } // Comprueba las entradas void Title::handleInput(float delta_time) { Input::get()->update(); // Permitir cancelar remap con ESC/CANCEL if ((is_remapping_keyboard_ || is_remapping_joystick_) && !remap_completed_) { if (Input::get()->checkAction(InputAction::CANCEL, Input::DO_NOT_ALLOW_REPEAT)) { is_remapping_keyboard_ = false; is_remapping_joystick_ = false; remap_step_ = 0; remap_completed_ = false; remap_error_message_.clear(); } // Durante el remap, no procesar otras entradas GlobalInputs::handle(); return; } switch (state_) { case State::SHOW_LOADING_SCREEN: if (Input::get()->checkAction(InputAction::ACCEPT, Input::DO_NOT_ALLOW_REPEAT)) { transitionToState(State::FADE_LOADING_SCREEN); } break; case State::CHEEVOS_MENU: if (Input::get()->checkAction(InputAction::ACCEPT, Input::DO_NOT_ALLOW_REPEAT) || Input::get()->checkAction(InputAction::CANCEL, Input::DO_NOT_ALLOW_REPEAT)) { resetCheevosScroll(); transitionToState(State::MAIN_MENU); } break; default: break; } GlobalInputs::handle(); } // Actualiza la marquesina void Title::updateMarquee(float delta_time) { const float DISPLACEMENT = MARQUEE_SPEED * delta_time; // Solo procesar letras en rango activo + 1 para poder activar la siguiente for (int i = first_active_letter_; i <= last_active_letter_ + 1 && i < (int)letters_.size(); ++i) { auto& letter = letters_[i]; if (letter.enabled) { letter.x -= DISPLACEMENT; // Desactivar si sale de pantalla if (letter.x < MARQUEE_EXIT_X) { letter.enabled = false; if (i == first_active_letter_) { first_active_letter_++; // Avanzar inicio del rango } } } else if (i > 0 && letters_[i - 1].x < MARQUEE_START_X && letters_[i - 1].enabled) { // Activar siguiente letra usando ancho pre-calculado letter.enabled = true; letter.x = letters_[i - 1].x + letters_[i - 1].width + MARQUEE_LETTER_SPACING; last_active_letter_ = i; // Expandir fin del rango } } // Comprueba si ha terminado la marquesina y la reinicia if (letters_[letters_.size() - 1].x < MARQUEE_EXIT_X) { initMarquee(); } } // Dibuja la marquesina void Title::renderMarquee() { auto* sprite = marquee_text_->getSprite(); sprite->setY(MARQUEE_Y); // Solo renderizar letras activas (optimización: usa cache y rangos) for (int i = first_active_letter_; i <= last_active_letter_ + 1 && i < (int)letters_.size(); ++i) { const auto& letter = letters_[i]; if (letter.enabled && letter.clip.w > 0.0F) { sprite->setClip(letter.clip); sprite->setX(letter.x); sprite->render(1, static_cast(PaletteColor::MAGENTA)); } } } // Actualiza las variables void Title::update() { const float DELTA_TIME = delta_timer_->tick(); handleEvents(); // Comprueba los eventos handleInput(DELTA_TIME); // Comprueba las entradas updateState(DELTA_TIME); // Actualiza el estado actual Audio::update(); // Actualiza el objeto Audio Screen::get()->update(DELTA_TIME); // Actualiza el objeto Screen } // Actualiza el estado actual void Title::updateState(float delta_time) { switch (state_) { case State::SHOW_LOADING_SCREEN: updateShowLoadingScreen(delta_time); break; case State::FADE_LOADING_SCREEN: updateFadeLoadingScreen(delta_time); break; case State::MAIN_MENU: updateMainMenu(delta_time); break; case State::CHEEVOS_MENU: updateCheevosMenu(delta_time); break; case State::FADE_MENU: updateFadeMenu(delta_time); break; case State::POST_FADE_MENU: updatePostFadeMenu(delta_time); break; default: break; } } // Transiciona a un nuevo estado void Title::transitionToState(State new_state) { state_ = new_state; state_time_ = 0.0F; fade_accumulator_ = 0.0F; } // Actualiza el estado SHOW_LOADING_SCREEN void Title::updateShowLoadingScreen(float delta_time) { state_time_ += delta_time; if (state_time_ >= SHOW_LOADING_DURATION) { transitionToState(State::FADE_LOADING_SCREEN); } } // Actualiza el estado FADE_LOADING_SCREEN void Title::updateFadeLoadingScreen(float delta_time) { fade_accumulator_ += delta_time; if (fade_accumulator_ >= FADE_STEP_INTERVAL) { fade_accumulator_ = 0.0F; if (loading_screen_surface_->fadeSubPalette()) { transitionToState(State::MAIN_MENU); } } } // Actualiza el estado MAIN_MENU void Title::updateMainMenu(float delta_time) { // Actualiza la marquesina updateMarquee(delta_time); // Si estamos en modo remap, manejar la lógica específica if (is_remapping_keyboard_ || is_remapping_joystick_) { // Decrementar cooldown de ejes si estamos capturando botones de joystick if (is_remapping_joystick_ && axis_cooldown_ > 0.0F) { axis_cooldown_ -= delta_time; axis_cooldown_ = std::max(axis_cooldown_, 0.0F); } // Si el remap está completado, esperar antes de guardar if (remap_completed_) { state_time_ += delta_time; if (state_time_ >= KEYBOARD_REMAP_DISPLAY_DELAY) { if (is_remapping_keyboard_) { applyKeyboardRemap(); } else if (is_remapping_joystick_) { applyJoystickRemap(); } // Resetear estado de remap is_remapping_keyboard_ = false; is_remapping_joystick_ = false; remap_completed_ = false; state_time_ = 0.0F; } } } else { // Incrementa el temporizador solo en el menú principal normal state_time_ += delta_time; // Si el tiempo alcanza el timeout, va a créditos con fade if (state_time_ >= MAIN_MENU_IDLE_TIMEOUT) { exit_scene_ = SceneManager::Scene::CREDITS; transitionToState(State::FADE_MENU); } } } // Actualiza el estado CHEEVOS_MENU void Title::updateCheevosMenu(float delta_time) { // Actualiza la marquesina (sigue visible en fondo) updateMarquee(delta_time); // Determina la velocidad objetivo basada en el input float target_velocity = 0.0F; if (Input::get()->checkAction(InputAction::RIGHT, Input::ALLOW_REPEAT)) { target_velocity = CHEEVOS_SCROLL_MAX_SPEED; // Scroll hacia abajo } else if (Input::get()->checkAction(InputAction::LEFT, Input::ALLOW_REPEAT)) { target_velocity = -CHEEVOS_SCROLL_MAX_SPEED; // Scroll hacia arriba } // Interpola suavemente la velocidad actual hacia la velocidad objetivo if (target_velocity != 0.0F) { // Acelerando hacia la velocidad objetivo const float ACCELERATION_STEP = CHEEVOS_SCROLL_ACCELERATION * delta_time; if (cheevos_scroll_velocity_ < target_velocity) { cheevos_scroll_velocity_ = std::min(cheevos_scroll_velocity_ + ACCELERATION_STEP, target_velocity); } else if (cheevos_scroll_velocity_ > target_velocity) { cheevos_scroll_velocity_ = std::max(cheevos_scroll_velocity_ - ACCELERATION_STEP, target_velocity); } } else { // Desacelerando hacia 0 const float DECELERATION_STEP = CHEEVOS_SCROLL_DECELERATION * delta_time; if (cheevos_scroll_velocity_ > 0.0F) { cheevos_scroll_velocity_ = std::max(cheevos_scroll_velocity_ - DECELERATION_STEP, 0.0F); } else if (cheevos_scroll_velocity_ < 0.0F) { cheevos_scroll_velocity_ = std::min(cheevos_scroll_velocity_ + DECELERATION_STEP, 0.0F); } } // Aplica la velocidad actual al scroll position if (cheevos_scroll_velocity_ != 0.0F) { cheevos_surface_view_.y += cheevos_scroll_velocity_ * delta_time; // Ajusta los límites const float BOTTOM = cheevos_surface_->getHeight() - cheevos_surface_view_.h; cheevos_surface_view_.y = std::clamp(cheevos_surface_view_.y, 0.0F, BOTTOM); cheevos_sprite_->setClip(cheevos_surface_view_); } // No incrementar state_time_ (no timeout en este estado) } // Actualiza el estado FADE_MENU void Title::updateFadeMenu(float delta_time) { fade_accumulator_ += delta_time; if (fade_accumulator_ >= FADE_STEP_INTERVAL) { fade_accumulator_ = 0.0F; if (title_surface_->fadeSubPalette()) { transitionToState(State::POST_FADE_MENU); } } // Actualiza la marquesina (sigue visible en fondo) updateMarquee(delta_time); } // Actualiza el estado POST_FADE_MENU void Title::updatePostFadeMenu(float delta_time) { state_time_ += delta_time; if (state_time_ >= POST_FADE_DELAY) { SceneManager::current = exit_scene_; SceneManager::options = SceneManager::Options::NONE; } } // Dibuja en pantalla void Title::render() { // Rellena la surface fillTitleSurface(); // Prepara para empezar a dibujar en la textura de juego Screen::get()->start(); Screen::get()->clearSurface(static_cast(PaletteColor::BLACK)); // Dibuja en pantalla la surface con la composicion title_surface_->render(); // Vuelca el contenido del renderizador en pantalla Screen::get()->render(); } // Bucle para el logo del juego void Title::run() { while (SceneManager::current == SceneManager::Scene::TITLE) { update(); render(); } } // Crea y rellena la textura para mostrar los logros void Title::createCheevosTexture() { // Define la zona central del menu (entre el logo y la marquesina) constexpr int MENU_ZONE_Y = 73; // Top of menu zone constexpr int MENU_ZONE_HEIGHT = 102; // Height of menu zone // Crea la textura con el listado de logros const auto CHEEVOS_LIST = Cheevos::get()->list(); const auto TEXT = Resource::Cache::get()->getText("subatomic"); constexpr int CHEEVOS_TEXTURE_WIDTH = 200; constexpr int CHEEVOS_TEXTURE_VIEW_HEIGHT = MENU_ZONE_HEIGHT; constexpr int CHEEVOS_PADDING = 10; const int CHEEVO_HEIGHT = CHEEVOS_PADDING + (TEXT->getCharacterSize() * 2) + 1; const int CHEEVOS_TEXTURE_HEIGHT = (CHEEVO_HEIGHT * CHEEVOS_LIST.size()) + 2 + TEXT->getCharacterSize() + 8; cheevos_surface_ = std::make_shared(CHEEVOS_TEXTURE_WIDTH, CHEEVOS_TEXTURE_HEIGHT); // Prepara para dibujar sobre la textura auto previuos_renderer = Screen::get()->getRendererSurface(); Screen::get()->setRendererSurface(cheevos_surface_); // Rellena la textura con color sólido const auto CHEEVOS_BG_COLOR = static_cast(PaletteColor::BLACK); cheevos_surface_->clear(CHEEVOS_BG_COLOR); // Escribe la lista de logros en la textura const std::string CHEEVOS_OWNER = Locale::get()->get("title.projects"); const std::string CHEEVOS_LIST_CAPTION = CHEEVOS_OWNER + " (" + std::to_string(Cheevos::get()->getTotalUnlockedAchievements()) + " / " + std::to_string(Cheevos::get()->size()) + ")"; int pos = 2; TEXT->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, cheevos_surface_->getWidth() / 2, pos, CHEEVOS_LIST_CAPTION, 1, stringToColor("bright_green")); pos += TEXT->getCharacterSize(); const Uint8 CHEEVO_LOCKED_COLOR = stringToColor("white"); const Uint8 CHEEVO_UNLOCKED_COLOR = stringToColor("bright_green"); constexpr int LINE_X1 = (CHEEVOS_TEXTURE_WIDTH / 7) * 3; constexpr int LINE_X2 = LINE_X1 + ((CHEEVOS_TEXTURE_WIDTH / 7) * 1); for (const auto& cheevo : CHEEVOS_LIST) { const Uint8 CHEEVO_COLOR = cheevo.completed ? CHEEVO_UNLOCKED_COLOR : CHEEVO_LOCKED_COLOR; pos += CHEEVOS_PADDING; constexpr int HALF = CHEEVOS_PADDING / 2; cheevos_surface_->drawLine(LINE_X1, pos - HALF - 1, LINE_X2, pos - HALF - 1, CHEEVO_COLOR); TEXT->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, CHEEVOS_TEXTURE_WIDTH / 2, pos, cheevo.caption, 1, CHEEVO_COLOR); pos += TEXT->getCharacterSize() + 1; TEXT->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, CHEEVOS_TEXTURE_WIDTH / 2, pos, cheevo.description, 1, CHEEVO_COLOR); pos += TEXT->getCharacterSize(); } // Restablece el RenderSurface Screen::get()->setRendererSurface(previuos_renderer); // Crea el sprite para el listado de logros (usa la zona del menu) cheevos_sprite_ = std::make_unique(cheevos_surface_, (GameCanvas::WIDTH - cheevos_surface_->getWidth()) / 2, MENU_ZONE_Y, cheevos_surface_->getWidth(), cheevos_surface_->getHeight()); cheevos_surface_view_ = {.x = 0, .y = 0, .w = cheevos_surface_->getWidth(), .h = CHEEVOS_TEXTURE_VIEW_HEIGHT}; cheevos_sprite_->setClip(cheevos_surface_view_); } // Resetea el scroll de la lista de logros void Title::resetCheevosScroll() { cheevos_surface_view_.y = 0; cheevos_scroll_velocity_ = 0.0F; cheevos_sprite_->setClip(cheevos_surface_view_); } // Dibuja el logo con el titulo del juego void Title::renderGameLogo() { game_logo_sprite_->render(); } // Dibuja el menu principal void Title::renderMainMenu() { // Si estamos en modo remap, mostrar la pantalla correspondiente if (is_remapping_keyboard_) { renderKeyboardRemap(); return; } if (is_remapping_joystick_) { renderJoystickRemap(); return; } // Zona central del menu (debe coincidir con la textura de cheevos) constexpr int MENU_ZONE_Y = 73; constexpr int MENU_ZONE_HEIGHT = 102; // Menú principal normal con 4 opciones centradas verticalmente en la zona const Uint8 COLOR = stringToColor("green"); const int TEXT_SIZE = menu_text_->getCharacterSize(); const int MENU_CENTER_Y = MENU_ZONE_Y + (MENU_ZONE_HEIGHT / 2); const int SPACING = 2 * TEXT_SIZE; // Espaciado entre opciones // Calcula posiciones centradas verticalmente (4 items con espaciado) const int TOTAL_HEIGHT = 3 * SPACING; // 3 espacios entre 4 items const int START_Y = MENU_CENTER_Y - (TOTAL_HEIGHT / 2); auto* loc = Locale::get(); menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, START_Y, loc->get("title.menu.play"), 1, COLOR); menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, START_Y + SPACING, loc->get("title.menu.keyboard"), 1, COLOR); menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, START_Y + (2 * SPACING), loc->get("title.menu.joystick"), 1, COLOR); menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, START_Y + (3 * SPACING), loc->get("title.menu.projects"), 1, COLOR); } // Dibuja el menu de logros void Title::renderCheevosMenu() { cheevos_sprite_->render(); } // Dibuja los elementos en la surface void Title::fillTitleSurface() { // Renderiza sobre la textura auto previuos_renderer = Screen::get()->getRendererSurface(); Screen::get()->setRendererSurface(title_surface_); // Rellena la textura de color title_surface_->clear(static_cast(PaletteColor::BLACK)); switch (state_) { case State::MAIN_MENU: case State::FADE_MENU: renderGameLogo(); renderMainMenu(); renderMarquee(); break; case State::CHEEVOS_MENU: renderGameLogo(); renderCheevosMenu(); renderMarquee(); break; case State::SHOW_LOADING_SCREEN: case State::FADE_LOADING_SCREEN: loading_screen_sprite_->render(); renderGameLogo(); break; default: break; } // Deja el renderizador como estaba Screen::get()->setRendererSurface(previuos_renderer); } // Maneja la captura de teclas para redefinir el teclado void Title::handleKeyboardRemap(const SDL_Event& event) { SDL_Scancode scancode = event.key.scancode; // Valida la tecla if (!isKeyValid(scancode)) { remap_error_message_ = Locale::get()->get("title.keys.invalid"); return; } // Verifica duplicados if (isKeyDuplicate(scancode, remap_step_)) { remap_error_message_ = Locale::get()->get("title.keys.already_used"); return; } // Tecla valida, guardar temp_keys_[remap_step_] = scancode; remap_error_message_.clear(); remap_step_++; // Si completamos los 3 pasos, mostrar resultado y esperar if (remap_step_ >= 3) { remap_completed_ = true; state_time_ = 0.0F; // Resetear el timer para el delay de 1 segundo } } // Valida si una tecla es permitida auto Title::isKeyValid(SDL_Scancode scancode) -> bool { // Prohibir ESC (reservado para cancelar) if (scancode == SDL_SCANCODE_ESCAPE) { return false; } // Prohibir teclas F1-F12 (reservadas para funciones del sistema) if (scancode >= SDL_SCANCODE_F1 && scancode <= SDL_SCANCODE_F12) { return false; } // Prohibir Enter/Return (reservado para confirmaciones) if (scancode == SDL_SCANCODE_RETURN || scancode == SDL_SCANCODE_RETURN2) { return false; } return true; } // Verifica si una tecla ya fue usada en pasos anteriores auto Title::isKeyDuplicate(SDL_Scancode scancode, int current_step) -> bool { for (int i = 0; i < current_step; i++) { if (temp_keys_[i] == scancode) { return true; } } return false; } // Retorna el nombre de la accion para el paso actual auto Title::getActionName(int step) -> std::string { switch (step) { case 0: return "LEFT"; case 1: return "RIGHT"; case 2: return "JUMP"; default: return "UNKNOWN"; } } // Aplica y guarda las teclas redefinidas void Title::applyKeyboardRemap() { // Guardar las nuevas teclas en Options::controls Options::keyboard_controls.key_left = temp_keys_[0]; Options::keyboard_controls.key_right = temp_keys_[1]; Options::keyboard_controls.key_jump = temp_keys_[2]; // Aplicar los bindings al sistema de Input Input::get()->applyKeyboardBindingsFromOptions(); // Guardar a archivo de configuracion Options::saveToFile(); } // Dibuja la pantalla de redefinir teclado void Title::renderKeyboardRemap() { // Zona central del menu (debe coincidir con la textura de cheevos) constexpr int MENU_ZONE_Y = 73; constexpr int MENU_ZONE_HEIGHT = 102; const Uint8 COLOR = stringToColor("green"); const Uint8 ERROR_COLOR = stringToColor("red"); const int TEXT_SIZE = menu_text_->getCharacterSize(); const int MENU_CENTER_Y = MENU_ZONE_Y + (MENU_ZONE_HEIGHT / 2); // Calcula posiciones centradas verticalmente // Layout: Mensaje principal, espacio, 3 teclas (LEFT/RIGHT/JUMP), espacio, mensaje de error const int LINE_SPACING = TEXT_SIZE; const int START_Y = MENU_CENTER_Y - (2 * TEXT_SIZE); // Centrado aproximado // Mensaje principal: "PRESS KEY FOR [ACTION]" o "KEYS DEFINED" si completado auto* loc = Locale::get(); if (remap_step_ >= 3) { menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, START_Y, loc->get("title.keys.defined"), 1, COLOR); } else { menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, START_Y, loc->get("title.keys.prompt" + std::to_string(remap_step_)), 1, COLOR); } // Mostrar teclas ya capturadas (con espaciado de 2 líneas desde el mensaje principal) const int KEYS_START_Y = START_Y + (2 * LINE_SPACING); if (remap_step_ > 0) { const std::string LEFT_KEY = SDL_GetScancodeName(temp_keys_[0]); const std::string LEFT_MSG = loc->get("title.keys.label0") + LEFT_KEY; menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, KEYS_START_Y, LEFT_MSG, 1, COLOR); } if (remap_step_ > 1) { const std::string RIGHT_KEY = SDL_GetScancodeName(temp_keys_[1]); const std::string RIGHT_MSG = loc->get("title.keys.label1") + RIGHT_KEY; menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, KEYS_START_Y + LINE_SPACING, RIGHT_MSG, 1, COLOR); } if (remap_step_ >= 3) { const std::string JUMP_KEY = SDL_GetScancodeName(temp_keys_[2]); const std::string JUMP_MSG = loc->get("title.keys.label2") + JUMP_KEY; menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, KEYS_START_Y + (2 * LINE_SPACING), JUMP_MSG, 1, COLOR); } // Mensaje de error si existe (4 líneas después del inicio de las teclas) if (!remap_error_message_.empty()) { menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, KEYS_START_Y + (4 * LINE_SPACING), remap_error_message_, 1, ERROR_COLOR); } } // Dibuja la pantalla de redefinir joystick void Title::renderJoystickRemap() { // Zona central del menu (debe coincidir con la textura de cheevos) constexpr int MENU_ZONE_Y = 73; constexpr int MENU_ZONE_HEIGHT = 102; const Uint8 COLOR = stringToColor("green"); const Uint8 ERROR_COLOR = stringToColor("red"); const int TEXT_SIZE = menu_text_->getCharacterSize(); const int MENU_CENTER_Y = MENU_ZONE_Y + (MENU_ZONE_HEIGHT / 2); // Calcula posiciones centradas verticalmente // Layout: Mensaje principal, espacio, 3 botones (LEFT/RIGHT/JUMP), espacio, mensaje de error const int LINE_SPACING = TEXT_SIZE; const int START_Y = MENU_CENTER_Y - (2 * TEXT_SIZE); // Centrado aproximado // Mensaje principal: "PRESS BUTTON FOR [ACTION]" o "BUTTONS DEFINED" si completado auto* loc = Locale::get(); if (remap_step_ >= 3) { menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, START_Y, loc->get("title.buttons.defined"), 1, COLOR); } else { menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, START_Y, loc->get("title.buttons.prompt" + std::to_string(remap_step_)), 1, COLOR); } // Mostrar botones ya capturados (con espaciado de 2 líneas desde el mensaje principal) const int BUTTONS_START_Y = START_Y + (2 * LINE_SPACING); if (remap_step_ > 0) { const std::string LEFT_BTN = getButtonName(temp_buttons_[0]); const std::string LEFT_MSG = loc->get("title.keys.label0") + LEFT_BTN; menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, BUTTONS_START_Y, LEFT_MSG, 1, COLOR); } if (remap_step_ > 1) { const std::string RIGHT_BTN = getButtonName(temp_buttons_[1]); const std::string RIGHT_MSG = loc->get("title.keys.label1") + RIGHT_BTN; menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, BUTTONS_START_Y + LINE_SPACING, RIGHT_MSG, 1, COLOR); } if (remap_step_ >= 3) { const std::string JUMP_BTN = getButtonName(temp_buttons_[2]); const std::string JUMP_MSG = loc->get("title.keys.label2") + JUMP_BTN; menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, BUTTONS_START_Y + (2 * LINE_SPACING), JUMP_MSG, 1, COLOR); } // Mensaje de error si existe (4 líneas después del inicio de los botones) if (!remap_error_message_.empty()) { menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, BUTTONS_START_Y + (4 * LINE_SPACING), remap_error_message_, 1, ERROR_COLOR); } } // Maneja la captura de botones del gamepad para redefinir void Title::handleJoystickRemap(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 y ejes analógicos else if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) { // Si el cooldown está activo, ignorar eventos de ejes (evita múltiples capturas) if (axis_cooldown_ > 0.0F) { return; } constexpr Sint16 TRIGGER_THRESHOLD = 20000; constexpr Sint16 AXIS_THRESHOLD = 20000; // Capturar triggers como botones (usando valores especiales 100/101) if (event.gaxis.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER && event.gaxis.value > TRIGGER_THRESHOLD) { captured_button = Input::TRIGGER_L2_AS_BUTTON; // 100 axis_cooldown_ = 0.5F; // Cooldown de medio segundo } else if (event.gaxis.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER && event.gaxis.value > TRIGGER_THRESHOLD) { captured_button = Input::TRIGGER_R2_AS_BUTTON; // 101 axis_cooldown_ = 0.5F; } // Capturar ejes del stick analógico (usando valores especiales 200+) else if (event.gaxis.axis == SDL_GAMEPAD_AXIS_LEFTX) { if (event.gaxis.value < -AXIS_THRESHOLD) { captured_button = 200; // Left stick izquierda axis_cooldown_ = 0.5F; } else if (event.gaxis.value > AXIS_THRESHOLD) { captured_button = 201; // Left stick derecha axis_cooldown_ = 0.5F; } } } // 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_ = Locale::get()->get("title.buttons.already_used"); 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) { remap_completed_ = true; state_time_ = 0.0F; // Resetear el timer para el delay } } // Valida si un botón está duplicado auto Title::isButtonDuplicate(int button, int current_step) -> bool { 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(); } // Retorna el nombre amigable del botón del gamepad auto Title::getButtonName(int button) -> std::string { // 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 GAMEPAD_BUTTON = static_cast(button); const char* button_name = SDL_GetGamepadStringForButton(GAMEPAD_BUTTON); return (button_name != nullptr) ? std::string(button_name) : "UNKNOWN"; }