864 lines
34 KiB
C++
864 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/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
|
|
}
|
|
|
|
// Inicializa la marquesina
|
|
void Title::initMarquee() {
|
|
letters_.clear();
|
|
long_text_ = "HEY JAILERS!! IT'S 2022 AND WE'RE STILL ROCKING LIKE IT'S 1998!!! HAVE YOU HEARD IT? JAILGAMES ARE BACK!! YEEESSS BACK!! MORE THAN 10 TITLES ON JAILDOC'S KITCHEN!! THATS A LOOOOOOT OF JAILGAMES, BUT WHICH ONE WILL STRIKE FIRST? THERE IS ALSO A NEW DEVICE TO COME THAT WILL BLOW YOUR MIND WITH JAILGAMES ON THE GO: P.A.C.O. BUT WAIT! WHAT'S THAT BEAUTY I'M SEEING RIGHT OVER THERE?? OOOH THAT TINY MINIASCII IS PURE LOVE!! I WANT TO LICK EVERY BYTE OF IT!! OH SHIT! AND DON'T FORGET TO BRING BACK THOSE OLD AND FAT MS-DOS JAILGAMES TO GITHUB TO KEEP THEM ALIVE!! WHAT WILL BE THE NEXT JAILDOC RELEASE? WHAT WILL BE THE NEXT PROJECT TO COME ALIVE?? OH BABY WE DON'T KNOW BUT HERE YOU CAN FIND THE ANSWER, YOU JUST HAVE TO COMPLETE JAILDOCTOR'S DILEMMA ... COULD YOU?";
|
|
|
|
// Pre-calcular anchos de caracteres para eficiencia
|
|
for (size_t i = 0; i < long_text_.length(); ++i) {
|
|
Glyph l;
|
|
l.letter = long_text_[i]; // char directo, no substring
|
|
l.x = MARQUEE_START_X; // Usar constante
|
|
l.width = marquee_text_->length(std::string(1, long_text_[i])); // Pre-calcular ancho
|
|
l.enabled = false;
|
|
letters_.push_back(l);
|
|
}
|
|
|
|
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
|
|
std::string(1, letter.letter), // Convertir char a string
|
|
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 = "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);
|
|
|
|
menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, START_Y, "1. PLAY", 1, COLOR);
|
|
menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, START_Y + SPACING, "2. REDEFINE KEYBOARD", 1, COLOR);
|
|
menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, START_Y + (2 * SPACING), "3. REDEFINE JOYSTICK", 1, COLOR);
|
|
menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, START_Y + (3 * SPACING), "4. 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_ = "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 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
|
|
if (remap_step_ >= 3) {
|
|
menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::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, PlayArea::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, 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 = "RIGHT: " + 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 = "JUMP: " + 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
|
|
if (remap_step_ >= 3) {
|
|
menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::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, PlayArea::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, 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 = "RIGHT: " + 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 = "JUMP: " + 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_ = "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<SDL_GamepadButton>(button);
|
|
const char* button_name = SDL_GetGamepadStringForButton(GAMEPAD_BUTTON);
|
|
return (button_name != nullptr) ? std::string(button_name) : "UNKNOWN";
|
|
} |