Files
projecte_2026/source/game/scenes/title.cpp
2026-04-11 16:25:56 +02:00

570 lines
22 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/sprite/sprite.hpp" // Para SSprite
#include "core/rendering/surface.hpp" // Para Surface
#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/event_buffer.hpp" // Para EventBuffer
#include "core/system/global_events.hpp" // Para check
#include "game/defaults.hpp" // Para Defaults::Music::Files
#include "game/options.hpp" // Para Options, options, SectionState, Section
#include "game/scene_manager.hpp" // Para SceneManager
#include "game/ui/console.hpp" // Para Console
#include "utils/defines.hpp" // Para PlayArea::CENTER_X, GameCanvas::WIDTH
#include "utils/utils.hpp"
// Constructor
Title::Title()
: game_logo_surface_(Resource::Cache::get()->getSurface("title_logo.gif")),
game_logo_sprite_(std::make_unique<Sprite>(game_logo_surface_, 29, 9, game_logo_surface_->getWidth(), game_logo_surface_->getHeight())),
title_surface_(std::make_shared<Surface>(Options::game.width, Options::game.height)),
delta_timer_(std::make_unique<DeltaTimer>()),
menu_text_(Resource::Cache::get()->getText("gauntlet")) {
// Inicializa arrays con valores por defecto
temp_keys_.fill(SDL_SCANCODE_UNKNOWN);
temp_buttons_.fill(-1);
// Estado inicial: menú principal
state_ = State::MAIN_MENU;
// Establece SceneManager
SceneManager::current = SceneManager::Scene::TITLE;
SceneManager::options = SceneManager::Options::NONE;
// Acciones iniciales
Screen::get()->setBorderColor(0); // Cambia el color del borde
Audio::get()->playMusic(Defaults::Music::Files::TITLE_TRACK); // Inicia la musica
}
// Destructor
Title::~Title() { // NOLINT(modernize-use-equals-default)
title_surface_->resetSubPalette();
}
// Comprueba el manejador de eventos
void Title::handleEvents() {
for (const auto& event : EventBuffer::events) {
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 && !Console::get()->isActive()) {
// 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;
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;
}
}
// 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(0);
// Dibuja en pantalla la surface con la composicion
title_surface_->render();
// Vuelca el contenido del renderizador en pantalla
Screen::get()->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;
}
// Menú principal normal con 3 opciones centradas verticalmente en la zona
const Uint8 COLOR = 8;
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 (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);
const 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);
}
// 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(0);
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_ = 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 { // NOLINT(readability-convert-member-functions-to-static)
for (int i = 0; i < current_step; i++) {
if (temp_keys_[i] == scancode) {
return true;
}
}
return false;
}
// Aplica y guarda las teclas redefinidas
void Title::applyKeyboardRemap() { // NOLINT(readability-convert-member-functions-to-static)
// 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() const {
// Zona central del menu
const Uint8 COLOR = 8;
const Uint8 ERROR_COLOR = 4;
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
const 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; // NOLINT(readability-static-accessed-through-instance)
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; // NOLINT(readability-static-accessed-through-instance)
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; // NOLINT(readability-static-accessed-through-instance)
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() const {
const Uint8 COLOR = 8;
const Uint8 ERROR_COLOR = 4;
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
const 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; // NOLINT(readability-static-accessed-through-instance)
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; // NOLINT(readability-static-accessed-through-instance)
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; // NOLINT(readability-static-accessed-through-instance)
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;
}
// Capturar triggers como botones (usando valores especiales 100/101)
if (event.gaxis.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER && event.gaxis.value > Defaults::Controls::JOYSTICK_AXIS_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 > Defaults::Controls::JOYSTICK_AXIS_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 < -Defaults::Controls::JOYSTICK_AXIS_THRESHOLD) {
captured_button = 200; // Left stick izquierda
axis_cooldown_ = 0.5F;
} else if (event.gaxis.value > Defaults::Controls::JOYSTICK_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 { // NOLINT(readability-convert-member-functions-to-static)
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() { // NOLINT(readability-convert-member-functions-to-static)
// 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 { // NOLINT(readability-convert-member-functions-to-static)
// 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";
}