Files
jaildoctors_dilemma/source/game/scenes/title.cpp

874 lines
33 KiB
C++

#include "game/scenes/title.hpp"
#include <SDL3/SDL.h>
#include <algorithm> // 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<SurfaceSprite>(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<SurfaceSprite>(loading_screen_surface_, 0, 0, loading_screen_surface_->getWidth(), loading_screen_surface_->getHeight())),
title_surface_(std::make_shared<Surface>(Options::game.width, Options::game.height)),
delta_timer_(std::make_unique<DeltaTimer>()),
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<Uint8>(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.x = MARQUEE_START_X;
l.width = static_cast<float>(marquee_text_->glyphWidth(cp, 1)); // Pre-calcular ancho con kerning
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() {
// 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) {
marquee_text_->writeColored(
static_cast<int>(letter.x), // Conversión explícita float→int
static_cast<int>(MARQUEE_Y), // Usar constante
Text::codepointToUtf8(letter.codepoint), // Convertir codepoint a string UTF-8
static_cast<Uint8>(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<Uint8>(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<Surface>(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<Uint8>(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<SurfaceSprite>(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<Uint8>(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<int>(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<SDL_GamepadButton>(button);
const char* button_name = SDL_GetGamepadStringForButton(GAMEPAD_BUTTON);
return (button_name != nullptr) ? std::string(button_name) : "UNKNOWN";
}