#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/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/defaults.hpp" // Para Defaults::Music #include "game/options.hpp" // Para Options, options, SectionState, Section #include "game/scene_manager.hpp" // Para SceneManager #include "utils/color.hpp" // Para Color #include "utils/defines.hpp" // Para GameCanvas::CENTER_X, GameCanvas::WIDTH #include "utils/utils.hpp" // Para stringToColor // Constructor Title::Title() : game_logo_surface_(Resource::Cache::get()->getSurface("title_logo.gif")), game_logo_sprite_(std::make_unique( game_logo_surface_, (GameCanvas::WIDTH - game_logo_surface_->getWidth()) / 2, // Centrado horizontal dinámico static_cast(GameCanvas::HEIGHT * 0.05F), // Posición Y proporcional (~5% desde arriba) game_logo_surface_->getWidth(), game_logo_surface_->getHeight())), title_surface_(std::make_shared(Options::game.width, Options::game.height)), delta_timer_(std::make_unique()), 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 state_ = State::MAIN_MENU; // Establece SceneManager SceneManager::current = SceneManager::Scene::TITLE; SceneManager::options = SceneManager::Options::NONE; // Acciones iniciales Screen::get()->setBorderColor(Color::index(Color::Cpc::BLACK)); // Cambia el color del borde Audio::get()->playMusic(Defaults::Music::TITLE_TRACK); // Inicia la musica } // 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(Defaults::Music::FADE_DURATION_MS); 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; 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; } GlobalInputs::handle(); } // 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::MAIN_MENU: updateMainMenu(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 MAIN_MENU void Title::updateMainMenu(float 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, vuelve al logo if (state_time_ >= MAIN_MENU_IDLE_TIMEOUT) { exit_scene_ = SceneManager::Scene::LOGO; transitionToState(State::FADE_MENU); } } } // 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 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(Color::index(Color::Cpc::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(); } } // 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 dinámica del menú (proporcional al canvas) // El logo ocupa la parte superior, el menú se centra en el espacio restante const int LOGO_BOTTOM = static_cast(GameCanvas::HEIGHT * 0.25F); // Espacio reservado para logo const int MENU_ZONE_HEIGHT = GameCanvas::HEIGHT - LOGO_BOTTOM; // Espacio disponible para menú // Menú principal normal con 3 opciones centradas verticalmente en la zona const Uint8 COLOR = stringToColor("green"); const int TEXT_SIZE = menu_text_->getCharacterSize(); const int MENU_CENTER_Y = LOGO_BOTTOM + (MENU_ZONE_HEIGHT / 2); const int SPACING = 2 * TEXT_SIZE; // Espaciado entre opciones // Calcula posiciones centradas verticalmente (3 items con espaciado) const int TOTAL_HEIGHT = 2 * SPACING; // 2 espacios entre 3 items const int START_Y = MENU_CENTER_Y - (TOTAL_HEIGHT / 2); menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, START_Y, "1. PLAY", 1, COLOR); menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, START_Y + SPACING, "2. REDEFINE KEYBOARD", 1, COLOR); menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, START_Y + (2 * SPACING), "3. REDEFINE JOYSTICK", 1, COLOR); } // 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(Color::index(Color::Cpc::BLACK)); switch (state_) { case State::MAIN_MENU: case State::FADE_MENU: renderGameLogo(); renderMainMenu(); 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_ = "INVALID KEY! TRY ANOTHER"; return; } // Verifica duplicados if (isKeyDuplicate(scancode, remap_step_)) { remap_error_message_ = "KEY ALREADY USED! TRY ANOTHER"; 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 dinámica del menú (proporcional al canvas) const int LOGO_BOTTOM = static_cast(GameCanvas::HEIGHT * 0.25F); const int MENU_ZONE_HEIGHT = GameCanvas::HEIGHT - LOGO_BOTTOM; const Uint8 COLOR = stringToColor("green"); const Uint8 ERROR_COLOR = stringToColor("red"); const int TEXT_SIZE = menu_text_->getCharacterSize(); const int MENU_CENTER_Y = LOGO_BOTTOM + (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 if (remap_step_ >= 3) { menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, START_Y, "KEYS DEFINED", 1, COLOR); } else { const std::string ACTION = getActionName(remap_step_); const std::string MESSAGE = "PRESS KEY FOR " + ACTION; menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, START_Y, MESSAGE, 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 = "LEFT: " + LEFT_KEY; menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::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 = "RIGHT: " + RIGHT_KEY; menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::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 = "JUMP: " + JUMP_KEY; menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::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, GameCanvas::CENTER_X, KEYS_START_Y + (4 * LINE_SPACING), remap_error_message_, 1, ERROR_COLOR); } } // Dibuja la pantalla de redefinir joystick void Title::renderJoystickRemap() { // Zona dinámica del menú (proporcional al canvas) const int LOGO_BOTTOM = static_cast(GameCanvas::HEIGHT * 0.25F); const int MENU_ZONE_HEIGHT = GameCanvas::HEIGHT - LOGO_BOTTOM; const Uint8 COLOR = stringToColor("green"); const Uint8 ERROR_COLOR = stringToColor("red"); const int TEXT_SIZE = menu_text_->getCharacterSize(); const int MENU_CENTER_Y = LOGO_BOTTOM + (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 if (remap_step_ >= 3) { menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, START_Y, "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_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, START_Y, MESSAGE, 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 = "LEFT: " + LEFT_BTN; menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::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 = "RIGHT: " + RIGHT_BTN; menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::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 = "JUMP: " + JUMP_BTN; menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::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, GameCanvas::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_ = "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) { 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"; }