875 lines
34 KiB
C++
875 lines
34 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() { // NOLINT(modernize-use-equals-default)
|
|
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<float>(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() const {
|
|
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<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() { // NOLINT(readability-convert-member-functions-to-static)
|
|
// 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"); // NOLINT(readability-static-accessed-through-instance)
|
|
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 { // NOLINT(readability-convert-member-functions-to-static)
|
|
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 { // NOLINT(readability-convert-member-functions-to-static)
|
|
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() { // 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 (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; // 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 {
|
|
// 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; // 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;
|
|
}
|
|
|
|
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 { // 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";
|
|
} |