segon commit

This commit is contained in:
2026-04-05 21:34:38 +02:00
parent d168ed59f9
commit 20ad7d778f
502 changed files with 178145 additions and 0 deletions

View File

@@ -0,0 +1,247 @@
#include "game/scenes/credits.hpp"
#include <SDL3/SDL.h>
#include "core/audio/audio.hpp" // Para Audio
#include "core/input/global_inputs.hpp" // Para check
#include "core/input/input.hpp" // Para Input
#include "core/locale/locale.hpp" // Para Locale
#include "core/rendering/pixel_reveal.hpp" // Para PixelReveal
#include "core/rendering/screen.hpp" // Para Screen
#include "core/rendering/sprite/animated_sprite.hpp" // Para SAnimatedSprite
#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/system/global_events.hpp" // Para check
#include "game/options.hpp" // Para Options, options, OptionsGame, Sectio...
#include "game/scene_manager.hpp" // Para SceneManager
#include "utils/defines.hpp" // Para GAME_SPEED, PlayArea::CENTER_X, PLAY_...
#include "utils/delta_timer.hpp" // Para DeltaTimer
#include "utils/utils.hpp" // Para PaletteColor
// Destructor
Credits::~Credits() = default;
// Constructor
Credits::Credits()
: text_surface_(std::make_shared<Surface>(Options::game.width, Options::game.height)),
shining_sprite_(std::make_shared<AnimatedSprite>(Resource::Cache::get()->getAnimationData("shine.yaml"))),
delta_timer_(std::make_unique<DeltaTimer>()) {
// Configura la escena
SceneManager::current = SceneManager::Scene::CREDITS;
SceneManager::options = SceneManager::Options::NONE;
shining_sprite_->setPos({.x = 194, .y = 174, .w = 8, .h = 8});
Screen::get()->setBorderColor(static_cast<Uint8>(PaletteColor::BLACK)); // Cambia el color del borde
fillTexture(); // Escribe el texto en la textura
Audio::get()->playMusic("574071_EA_DTV.ogg"); // Inicia la musica
}
// Comprueba el manejador de eventos
void Credits::handleEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event);
}
}
// Comprueba las entradas
void Credits::handleInput() {
Input::get()->update();
GlobalInputs::handle();
}
// Inicializa los textos
void Credits::iniTexts() { // NOLINT(readability-convert-member-functions-to-static)
auto* loc = Locale::get();
texts_.clear();
texts_.push_back({.label = "", .color = static_cast<Uint8>(PaletteColor::WHITE)});
texts_.push_back({.label = loc->get("credits.instructions"), .color = static_cast<Uint8>(PaletteColor::YELLOW)});
texts_.push_back({.label = "", .color = static_cast<Uint8>(PaletteColor::WHITE)});
texts_.push_back({.label = loc->get("credits.l0"), .color = static_cast<Uint8>(PaletteColor::WHITE)});
texts_.push_back({.label = loc->get("credits.l1"), .color = static_cast<Uint8>(PaletteColor::WHITE)});
texts_.push_back({.label = loc->get("credits.l2"), .color = static_cast<Uint8>(PaletteColor::WHITE)});
texts_.push_back({.label = "", .color = static_cast<Uint8>(PaletteColor::WHITE)});
texts_.push_back({.label = "", .color = static_cast<Uint8>(PaletteColor::WHITE)});
texts_.push_back({.label = loc->get("credits.keys"), .color = static_cast<Uint8>(PaletteColor::YELLOW)});
texts_.push_back({.label = "", .color = static_cast<Uint8>(PaletteColor::WHITE)});
texts_.push_back({.label = loc->get("credits.keys_move"), .color = static_cast<Uint8>(PaletteColor::WHITE)});
texts_.push_back({.label = loc->get("credits.f8"), .color = static_cast<Uint8>(PaletteColor::WHITE)});
texts_.push_back({.label = loc->get("credits.f11"), .color = static_cast<Uint8>(PaletteColor::WHITE)});
texts_.push_back({.label = loc->get("credits.f1f2"), .color = static_cast<Uint8>(PaletteColor::WHITE)});
texts_.push_back({.label = loc->get("credits.f3"), .color = static_cast<Uint8>(PaletteColor::WHITE)});
texts_.push_back({.label = loc->get("credits.f9"), .color = static_cast<Uint8>(PaletteColor::WHITE)});
texts_.push_back({.label = "", .color = static_cast<Uint8>(PaletteColor::WHITE)});
texts_.push_back({.label = "", .color = static_cast<Uint8>(PaletteColor::WHITE)});
texts_.push_back({.label = loc->get("credits.author"), .color = static_cast<Uint8>(PaletteColor::YELLOW)});
texts_.push_back({.label = loc->get("credits.date"), .color = static_cast<Uint8>(PaletteColor::YELLOW)});
texts_.push_back({.label = "", .color = static_cast<Uint8>(PaletteColor::WHITE)});
texts_.push_back({.label = "", .color = static_cast<Uint8>(PaletteColor::WHITE)});
texts_.push_back({.label = loc->get("credits.love"), .color = static_cast<Uint8>(PaletteColor::WHITE)});
texts_.push_back({.label = "", .color = static_cast<Uint8>(PaletteColor::WHITE)});
}
// Escribe el texto en la textura
void Credits::fillTexture() {
// Inicializa los textos
iniTexts();
// Rellena la textura de texto
auto previuos_renderer = Screen::get()->getRendererSurface();
Screen::get()->setRendererSurface(text_surface_);
text_surface_->clear(static_cast<Uint8>(PaletteColor::BLACK));
auto text = Resource::Cache::get()->getText("smb2");
// Escribe el texto en la textura
const int SIZE = text->getCharacterSize();
int pos_y = 0;
for (const auto& t : texts_) {
text->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, pos_y * SIZE, t.label, 1, t.color);
pos_y++;
}
// Escribe el corazón
const int TEXT_LENGHT = text->length(texts_[22].label, 1) - text->length(" ", 1); // Se resta el ultimo caracter que es un espacio
const int POS_X = ((PlayArea::WIDTH - TEXT_LENGHT) / 2) + TEXT_LENGHT;
text->writeColored(POS_X, 176, "ä", static_cast<Uint8>(PaletteColor::BRIGHT_RED));
Screen::get()->setRendererSurface(previuos_renderer);
// Recoloca el sprite del brillo
shining_sprite_->setPosX(POS_X + 2);
// Crea el efecto de revelado pixel a pixel
pixel_reveal_ = std::make_unique<PixelReveal>(Options::game.width, Options::game.height, PIXELS_PER_SECOND, STEP_DURATION, REVEAL_STEPS);
}
// Actualiza las variables
void Credits::update() {
const float DELTA_TIME = delta_timer_->tick();
total_time_ += DELTA_TIME; // Actualiza el tiempo total
handleEvents(); // Comprueba los eventos
handleInput(); // Comprueba las entradas
updateState(DELTA_TIME); // Actualiza la máquina de estados
pixel_reveal_->update(reveal_time_); // Actualiza el efecto de revelado
// Actualiza el sprite con el brillo si está después del tiempo de inicio
if (reveal_time_ > SHINE_START_TIME) {
shining_sprite_->update(DELTA_TIME);
}
Audio::update(); // Actualiza el objeto Audio
Screen::get()->update(DELTA_TIME); // Actualiza el objeto Screen
}
// Transición entre estados
void Credits::transitionToState(State new_state) {
state_ = new_state;
state_time_ = 0.0F;
}
// Actualiza la máquina de estados
void Credits::updateState(float delta_time) {
state_time_ += delta_time;
switch (state_) {
case State::REVEALING_TEXT:
reveal_time_ += delta_time; // Incrementa reveal_time durante revelación
if (state_time_ >= REVEAL_PHASE_1_DURATION) {
transitionToState(State::PAUSE_1);
}
break;
case State::PAUSE_1:
// reveal_time_ NO incrementa durante pausa (se congela)
if (state_time_ >= PAUSE_DURATION) {
transitionToState(State::REVEALING_TEXT_2);
}
break;
case State::REVEALING_TEXT_2:
reveal_time_ += delta_time; // Incrementa reveal_time durante revelación
if (state_time_ >= REVEAL_PHASE_2_DURATION) {
transitionToState(State::PAUSE_2);
}
break;
case State::PAUSE_2:
// reveal_time_ NO incrementa durante pausa (se congela)
if (state_time_ >= PAUSE_DURATION) {
transitionToState(State::REVEALING_TEXT_3);
}
break;
case State::REVEALING_TEXT_3:
reveal_time_ += delta_time; // Incrementa reveal_time durante revelación
if (state_time_ >= REVEAL_PHASE_3_DURATION) {
transitionToState(State::PAUSE_3);
}
break;
case State::PAUSE_3:
// reveal_time_ NO incrementa durante pausa (se congela)
if (state_time_ >= PAUSE_DURATION) {
transitionToState(State::DISPLAYING_WITH_SHINE);
}
break;
case State::DISPLAYING_WITH_SHINE:
reveal_time_ += delta_time; // Incrementa reveal_time durante revelación
if (state_time_ >= DISPLAY_WITH_SHINE_DURATION) {
transitionToState(State::FADING_OUT);
}
break;
case State::FADING_OUT:
reveal_time_ += delta_time; // Incrementa reveal_time durante fade
if (state_time_ >= FADE_OUT_DURATION) {
transitionToState(State::EXITING);
}
break;
case State::EXITING:
SceneManager::current = SceneManager::Scene::DEMO;
break;
}
}
// Dibuja en pantalla
void Credits::render() {
// Prepara para empezar a dibujar en la textura de juego
Screen::get()->start();
// Limpia la pantalla
Screen::get()->clearSurface(static_cast<Uint8>(PaletteColor::BLACK));
if (state_ != State::EXITING) {
// Dibuja la textura con el texto en pantalla
text_surface_->render(0, 0);
// Dibuja la máscara de revelado pixel a pixel
pixel_reveal_->render(0, 0);
// Dibuja el sprite con el brillo
if (reveal_time_ > SHINE_START_TIME) {
shining_sprite_->render(1, static_cast<Uint8>(PaletteColor::BRIGHT_WHITE));
}
}
// Vuelca el contenido del renderizador en pantalla
Screen::get()->render();
}
// Bucle para el logo del juego
void Credits::run() {
while (SceneManager::current == SceneManager::Scene::CREDITS) {
update();
render();
}
}

View File

@@ -0,0 +1,80 @@
#pragma once
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
class AnimatedSprite; // lines 11-11
class Surface;
class PixelReveal;
class DeltaTimer;
class Credits {
public:
// --- Constructor y Destructor ---
Credits();
~Credits(); // NOLINT(modernize-use-equals-default, performance-trivially-destructible) -- defined in .cpp for unique_ptr with forward declarations
// --- Bucle principal ---
void run();
private:
// --- Tipos anidados ---
enum class State {
REVEALING_TEXT,
PAUSE_1,
REVEALING_TEXT_2,
PAUSE_2,
REVEALING_TEXT_3,
PAUSE_3,
DISPLAYING_WITH_SHINE,
FADING_OUT,
EXITING
};
struct Captions {
std::string label; // Texto a escribir
Uint8 color{0}; // Color del texto
};
// --- Constantes de tiempo (basado en 60 FPS) ---
static constexpr float REVEAL_PHASE_1_DURATION = 3.733F; // 224 frames @ 60fps
static constexpr float PAUSE_DURATION = 1.667F; // 100 frames @ 60fps
static constexpr float REVEAL_PHASE_2_DURATION = 5.333F; // 320 frames (544-224) @ 60fps
static constexpr float REVEAL_PHASE_3_DURATION = 2.133F; // 128 frames (672-544) @ 60fps
static constexpr float DISPLAY_WITH_SHINE_DURATION = 7.967F; // 478 frames (1150-672) @ 60fps
static constexpr float FADE_OUT_DURATION = 0.833F; // 50 frames (1200-1150) @ 60fps
static constexpr float TOTAL_DURATION = 20.0F; // 1200 frames @ 60fps
static constexpr float SHINE_START_TIME = 12.833F; // 770 frames @ 60fps
static constexpr float FADE_OUT_START = 19.167F; // 1150 frames @ 60fps
static constexpr float PIXELS_PER_SECOND = 15.0F; // Filas reveladas por segundo (REVEAL_SPEED/8*2 = 60/8*2 = 15)
static constexpr float STEP_DURATION = 2.0F / 60.0F; // Segundos por paso de revelado (2 frames @ 60fps)
static constexpr int REVEAL_STEPS = 16; // Pasos de revelado por fila (más pasos = efecto más visible)
// --- Métodos privados ---
void update(); // Actualiza las variables
void render(); // Dibuja en pantalla
static void handleEvents(); // Comprueba el manejador de eventos
static void handleInput(); // Comprueba las entradas
void updateState(float delta_time); // Actualiza la máquina de estados
void transitionToState(State new_state); // Transición entre estados
void iniTexts(); // Inicializa los textos
void fillTexture(); // Escribe el texto en la textura
// --- Variables miembro ---
// Recursos gráficos
std::shared_ptr<Surface> text_surface_; // Textura para dibujar el texto
std::unique_ptr<PixelReveal> pixel_reveal_; // Efecto de revelado pixel a pixel
std::shared_ptr<AnimatedSprite> shining_sprite_; // Sprite para el brillo del corazón
// Temporizadores y estado
std::unique_ptr<DeltaTimer> delta_timer_; // Temporizador delta para time-based update
State state_{State::REVEALING_TEXT}; // Estado actual
float state_time_{0.0F}; // Tiempo acumulado en el estado actual
float total_time_{0.0F}; // Tiempo total acumulado
float reveal_time_{0.0F}; // Tiempo acumulado solo durante revelación (se congela en pausas)
// Textos
std::vector<Captions> texts_; // Vector con los textos
};

View File

@@ -0,0 +1,434 @@
#include "game/scenes/ending.hpp"
#include <SDL3/SDL.h>
#include "core/audio/audio.hpp" // Para Audio
#include "core/input/global_inputs.hpp" // Para check
#include "core/input/input.hpp" // Para Input
#include "core/locale/locale.hpp" // Para Locale
#include "core/rendering/pixel_reveal.hpp" // Para PixelReveal
#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_STROKE
#include "core/resources/resource_cache.hpp" // Para Resource
#include "core/system/global_events.hpp" // Para check
#include "game/options.hpp" // Para Options, options, OptionsGame, SectionS...
#include "game/scene_manager.hpp" // Para SceneManager
#include "utils/delta_timer.hpp" // Para DeltaTimer
#include "utils/utils.hpp" // Para PaletteColor
// Destructor
Ending::~Ending() = default;
// Constructor
Ending::Ending()
: delta_timer_(std::make_unique<DeltaTimer>()) {
SceneManager::current = SceneManager::Scene::ENDING;
SceneManager::options = SceneManager::Options::NONE;
iniTexts(); // Inicializa los textos
iniPics(); // Inicializa las imagenes
iniScenes(); // Inicializa las escenas
Screen::get()->setBorderColor(static_cast<Uint8>(PaletteColor::BLACK)); // Cambia el color del borde
}
// Actualiza el objeto
void Ending::update() {
const float DELTA_TIME = delta_timer_->tick();
total_time_ += DELTA_TIME; // Actualiza el tiempo total
handleEvents(); // Comprueba los eventos
handleInput(); // Comprueba las entradas
updateState(DELTA_TIME); // Actualiza la máquina de estados
updateSpriteCovers(); // Actualiza las cortinillas de los elementos
Audio::update(); // Actualiza el objeto Audio
Screen::get()->update(DELTA_TIME); // Actualiza el objeto Screen
}
// Dibuja el final en pantalla
void Ending::render() {
// Prepara para empezar a dibujar en la textura de juego
Screen::get()->start();
// Limpia la pantalla
Screen::get()->clearSurface(static_cast<Uint8>(PaletteColor::BLACK));
// Skip rendering durante WARMING_UP
if (state_ != State::WARMING_UP) {
// Dibuja las imagenes de la escena
const auto& pic = sprite_pics_.at(current_scene_);
pic.image_sprite->render();
pic.pixel_reveal->render(pic.pos_x, pic.pos_y);
// Dibuja los textos de la escena
for (const auto& ti : scenes_.at(current_scene_).text_index) {
// Convertir trigger de frames a segundos @ 60fps
const float TRIGGER_TIME = static_cast<float>(ti.trigger) / 60.0F;
if (state_time_ > TRIGGER_TIME) {
const auto& txt = sprite_texts_.at(ti.index);
txt.image_sprite->render();
txt.pixel_reveal->render(txt.pos_x, txt.pos_y);
}
}
// Dibuja la cortinilla de cambio de escena
if (scene_cover_) {
scene_cover_->render(0, 0);
}
}
// Vuelca el contenido del renderizador en pantalla
Screen::get()->render();
}
// Comprueba el manejador de eventos
void Ending::handleEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event);
}
}
// Comprueba las entradas
void Ending::handleInput() {
Input::get()->update();
GlobalInputs::handle();
}
// Transición entre estados
void Ending::transitionToState(State new_state) {
state_ = new_state;
state_time_ = 0.0F;
// Al cambiar a una escena, resetear la cortinilla de salida y el contador de fade
if (new_state != State::WARMING_UP && new_state != State::ENDING) {
fadeout_time_ = 0.0F;
scene_cover_.reset();
}
}
// Lógica de fade común a los estados SCENE_N
void Ending::handleSceneFadeout(float scene_duration, float delta_time) {
if (state_time_ >= scene_duration - FADEOUT_START_OFFSET) {
fadeout_time_ += delta_time;
if (!scene_cover_) {
scene_cover_ = std::make_unique<PixelReveal>(Options::game.width, Options::game.height, COVER_PIXELS_PER_SECOND, STEP_DURATION, COVER_STEPS, true);
}
scene_cover_->update(fadeout_time_);
}
}
// Actualiza la máquina de estados
void Ending::updateState(float delta_time) {
state_time_ += delta_time;
switch (state_) {
case State::WARMING_UP:
if (state_time_ >= WARMUP_DURATION) {
transitionToState(State::SCENE_0);
current_scene_ = 0;
}
break;
case State::SCENE_0:
checkChangeScene();
handleSceneFadeout(SCENE_0_DURATION, delta_time);
break;
case State::SCENE_1:
checkChangeScene();
handleSceneFadeout(SCENE_1_DURATION, delta_time);
break;
case State::SCENE_2:
checkChangeScene();
handleSceneFadeout(SCENE_2_DURATION, delta_time);
break;
case State::SCENE_3:
checkChangeScene();
handleSceneFadeout(SCENE_3_DURATION, delta_time);
break;
case State::SCENE_4:
checkChangeScene();
handleSceneFadeout(SCENE_4_DURATION, delta_time);
break;
case State::ENDING:
// Esperar ENDING_DURATION y luego transicionar a ENDING2
if (state_time_ >= ENDING_DURATION) {
SceneManager::current = SceneManager::Scene::ENDING2;
}
break;
}
}
// Inicializa los textos
void Ending::iniTexts() { // NOLINT(readability-convert-member-functions-to-static)
// Vector con los textos (traducidos según el idioma activo)
std::vector<TextAndPosition> texts;
auto* loc = Locale::get();
// Escena #0
texts.push_back({.caption = loc->get("ending.t0"), .pos = 32});
texts.push_back({.caption = loc->get("ending.t1"), .pos = 42});
texts.push_back({.caption = loc->get("ending.t2"), .pos = 142});
texts.push_back({.caption = loc->get("ending.t3"), .pos = 152});
// Escena #1
texts.push_back({.caption = loc->get("ending.t4"), .pos = 1});
texts.push_back({.caption = loc->get("ending.t5"), .pos = 11});
texts.push_back({.caption = loc->get("ending.t6"), .pos = 21});
texts.push_back({.caption = loc->get("ending.t7"), .pos = 161});
texts.push_back({.caption = loc->get("ending.t8"), .pos = 171});
texts.push_back({.caption = loc->get("ending.t9"), .pos = 181});
// Escena #2
texts.push_back({.caption = loc->get("ending.t10"), .pos = 19});
texts.push_back({.caption = loc->get("ending.t11"), .pos = 29});
// Escena #3
texts.push_back({.caption = loc->get("ending.t12"), .pos = 36});
texts.push_back({.caption = loc->get("ending.t13"), .pos = 46});
// Escena #4
texts.push_back({.caption = loc->get("ending.t14"), .pos = 36});
texts.push_back({.caption = loc->get("ending.t15"), .pos = 46});
texts.push_back({.caption = loc->get("ending.t16"), .pos = 158});
// Crea los sprites
sprite_texts_.clear();
for (const auto& txt : texts) {
auto text = Resource::Cache::get()->getText("smb2");
const float WIDTH = text->length(txt.caption, 1) + 2 + 2;
const float HEIGHT = text->getCharacterSize() + 2 + 2;
auto text_color = static_cast<Uint8>(PaletteColor::WHITE);
auto shadow_color = static_cast<Uint8>(PaletteColor::BLACK);
EndingSurface st;
// Crea la textura
st.image_surface = std::make_shared<Surface>(WIDTH, HEIGHT);
auto previuos_renderer = Screen::get()->getRendererSurface();
Screen::get()->setRendererSurface(st.image_surface);
text->writeDX(Text::STROKE_FLAG, 2, 2, txt.caption, 1, text_color, 2, shadow_color);
// Crea el sprite
st.image_sprite = std::make_shared<Sprite>(st.image_surface, 0, 0, st.image_surface->getWidth(), st.image_surface->getHeight());
st.pos_x = static_cast<int>((Options::game.width - st.image_surface->getWidth()) / 2);
st.pos_y = txt.pos;
st.image_sprite->setPosition(st.pos_x, st.pos_y);
// Crea el efecto de revelado pixel a pixel
st.pixel_reveal = std::make_unique<PixelReveal>(static_cast<int>(WIDTH), static_cast<int>(HEIGHT), TEXT_PIXELS_PER_SECOND, STEP_DURATION, REVEAL_STEPS);
sprite_texts_.push_back(std::move(st));
Screen::get()->setRendererSurface(previuos_renderer);
}
}
// Inicializa las imagenes
void Ending::iniPics() {
// Vector con las rutas y la posición
std::vector<TextAndPosition> pics;
pics.push_back({.caption = "ending1.gif", .pos = 48});
pics.push_back({.caption = "ending2.gif", .pos = 26});
pics.push_back({.caption = "ending3.gif", .pos = 29});
pics.push_back({.caption = "ending4.gif", .pos = 63});
pics.push_back({.caption = "ending5.gif", .pos = 53});
// Crea los sprites
sprite_pics_.clear();
for (const auto& pic : pics) {
EndingSurface sp;
// Crea la texture
sp.image_surface = Resource::Cache::get()->getSurface(pic.caption);
sp.image_surface->setTransparentColor();
const float WIDTH = sp.image_surface->getWidth();
const float HEIGHT = sp.image_surface->getHeight();
// Crea el sprite
sp.pos_x = static_cast<int>((Options::game.width - WIDTH) / 2);
sp.pos_y = pic.pos;
sp.image_sprite = std::make_shared<Sprite>(sp.image_surface, 0, 0, WIDTH, HEIGHT);
sp.image_sprite->setPosition(sp.pos_x, sp.pos_y);
// Crea el efecto de revelado pixel a pixel
sp.pixel_reveal = std::make_unique<PixelReveal>(static_cast<int>(WIDTH), static_cast<int>(HEIGHT), IMAGE_PIXELS_PER_SECOND, STEP_DURATION, REVEAL_STEPS);
sprite_pics_.push_back(std::move(sp));
}
}
// Inicializa las escenas
void Ending::iniScenes() { // NOLINT(readability-convert-member-functions-to-static)
// Variable para los tiempos
int trigger;
constexpr int LAPSE = 80;
// Crea el contenedor
SceneData sc;
// Inicializa el vector
scenes_.clear();
// Crea la escena #0
sc.counter_end = 1000;
sc.picture_index = 0;
sc.text_index.clear();
trigger = 85 * 2;
trigger += LAPSE;
sc.text_index.push_back({.index = 0, .trigger = trigger});
trigger += LAPSE;
sc.text_index.push_back({.index = 1, .trigger = trigger});
trigger += LAPSE * 3;
sc.text_index.push_back({.index = 2, .trigger = trigger});
trigger += LAPSE;
sc.text_index.push_back({.index = 3, .trigger = trigger});
scenes_.push_back(sc);
// Crea la escena #1
sc.counter_end = 1400;
sc.picture_index = 1;
sc.text_index.clear();
trigger = 140 * 2;
trigger += LAPSE;
sc.text_index.push_back({.index = 4, .trigger = trigger});
trigger += LAPSE;
sc.text_index.push_back({.index = 5, .trigger = trigger});
trigger += LAPSE;
sc.text_index.push_back({.index = 6, .trigger = trigger});
trigger += LAPSE * 3;
sc.text_index.push_back({.index = 7, .trigger = trigger});
trigger += LAPSE;
sc.text_index.push_back({.index = 8, .trigger = trigger});
trigger += LAPSE * 3;
sc.text_index.push_back({.index = 9, .trigger = trigger});
scenes_.push_back(sc);
// Crea la escena #2
sc.counter_end = 1000;
sc.picture_index = 2;
sc.text_index.clear();
trigger = 148 / 2;
trigger += LAPSE;
sc.text_index.push_back({.index = 10, .trigger = trigger});
trigger += LAPSE;
sc.text_index.push_back({.index = 11, .trigger = trigger});
scenes_.push_back(sc);
// Crea la escena #3
sc.counter_end = 800;
sc.picture_index = 3;
sc.text_index.clear();
trigger = 87 / 2;
trigger += LAPSE;
sc.text_index.push_back({.index = 12, .trigger = trigger});
trigger += LAPSE / 2;
sc.text_index.push_back({.index = 13, .trigger = trigger});
scenes_.push_back(sc);
// Crea la escena #4
sc.counter_end = 1000;
sc.picture_index = 4;
sc.text_index.clear();
trigger = 91 * 2;
trigger += LAPSE;
sc.text_index.push_back({.index = 14, .trigger = trigger});
trigger += LAPSE * 2;
sc.text_index.push_back({.index = 15, .trigger = trigger});
trigger += LAPSE * 3;
sc.text_index.push_back({.index = 16, .trigger = trigger});
scenes_.push_back(sc);
}
// Bucle principal
void Ending::run() {
Audio::get()->playMusic("574070_KUVO_Farewell_to_school.ogg");
while (SceneManager::current == SceneManager::Scene::ENDING) {
update();
render();
}
Audio::get()->stopMusic();
}
// Actualiza las cortinillas de los elementos
void Ending::updateSpriteCovers() {
// Skip durante WARMING_UP
if (state_ == State::WARMING_UP) {
return;
}
// Actualiza el revelado de los textos
for (const auto& ti : scenes_.at(current_scene_).text_index) {
const float TRIGGER_TIME = static_cast<float>(ti.trigger) / 60.0F;
if (state_time_ > TRIGGER_TIME) {
const float TIME_SINCE_TRIGGER = state_time_ - TRIGGER_TIME;
sprite_texts_.at(ti.index).pixel_reveal->update(TIME_SINCE_TRIGGER);
}
}
// Actualiza el revelado de la imagen (desde el inicio de la escena)
sprite_pics_.at(current_scene_).pixel_reveal->update(state_time_);
}
// Comprueba si se ha de cambiar de escena
void Ending::checkChangeScene() {
// Obtener duración de la escena actual
float current_duration = 0.0F;
State next_state = State::ENDING;
switch (state_) {
case State::SCENE_0:
current_duration = SCENE_0_DURATION;
next_state = State::SCENE_1;
break;
case State::SCENE_1:
current_duration = SCENE_1_DURATION;
next_state = State::SCENE_2;
break;
case State::SCENE_2:
current_duration = SCENE_2_DURATION;
next_state = State::SCENE_3;
break;
case State::SCENE_3:
current_duration = SCENE_3_DURATION;
next_state = State::SCENE_4;
break;
case State::SCENE_4:
current_duration = SCENE_4_DURATION;
next_state = State::ENDING;
break;
default:
return;
}
// Comprobar si ha pasado la duración de la escena
if (state_time_ >= current_duration) {
if (next_state == State::ENDING) {
// Transición al estado ENDING con fade de audio
transitionToState(State::ENDING);
Audio::get()->fadeOutMusic(MUSIC_FADE_DURATION);
} else {
// Transición a la siguiente escena
current_scene_++;
transitionToState(next_state);
}
}
}

View File

@@ -0,0 +1,106 @@
#pragma once
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
class Sprite; // lines 8-8
class Surface; // lines 9-9
class PixelReveal;
class DeltaTimer;
class Ending {
public:
// --- Constructor y Destructor ---
Ending();
~Ending(); // NOLINT(modernize-use-equals-default, performance-trivially-destructible) -- defined in .cpp for unique_ptr with forward declarations
// --- Bucle principal ---
void run();
private:
// --- Enumeraciones ---
enum class State {
WARMING_UP,
SCENE_0,
SCENE_1,
SCENE_2,
SCENE_3,
SCENE_4,
ENDING
};
// --- Estructuras ---
struct EndingSurface {
std::shared_ptr<Surface> image_surface; // Surface a mostrar
std::shared_ptr<Sprite> image_sprite; // SSprite para mostrar la textura
std::unique_ptr<PixelReveal> pixel_reveal; // Efecto de revelado pixel a pixel
int pos_x{0}; // Posición X de renderizado
int pos_y{0}; // Posición Y de renderizado
};
struct TextAndPosition {
std::string caption; // Texto
int pos{0}; // Posición
};
struct TextIndex {
int index{0}; // Índice del texto
int trigger{0}; // Disparador temporal
};
struct SceneData {
std::vector<TextIndex> text_index; // Índices del vector de textos a mostrar y su disparador
int picture_index{0}; // Índice del vector de imágenes a mostrar
int counter_end{0}; // Valor del contador en el que finaliza la escena
};
// --- Constantes de tiempo (basado en 60 FPS) ---
static constexpr float WARMUP_DURATION = 3.333F; // 200 frames @ 60fps
static constexpr float SCENE_0_DURATION = 16.667F; // 1000 frames @ 60fps
static constexpr float SCENE_1_DURATION = 23.333F; // 1400 frames @ 60fps
static constexpr float SCENE_2_DURATION = 16.667F; // 1000 frames @ 60fps
static constexpr float SCENE_3_DURATION = 13.333F; // 800 frames @ 60fps
static constexpr float SCENE_4_DURATION = 16.667F; // 1000 frames @ 60fps
static constexpr float TEXT_PIXELS_PER_SECOND = 30.0F; // Filas de texto reveladas por segundo
static constexpr float IMAGE_PIXELS_PER_SECOND = 60.0F; // Filas de imagen reveladas por segundo
static constexpr float STEP_DURATION = 2.0F / 60.0F; // Segundos por paso de revelado (2 frames @ 60fps)
static constexpr int REVEAL_STEPS = 4; // Pasos de revelado por fila
static constexpr float TEXT_LAPSE = 1.333F; // 80 frames @ 60fps
static constexpr float FADEOUT_START_OFFSET = 1.667F; // Inicio cortinilla 100 frames antes del fin
static constexpr float COVER_PIXELS_PER_SECOND = 120.0F; // Filas cubiertas por segundo
static constexpr int COVER_STEPS = 4; // Pasos por fila
static constexpr float ENDING_DURATION = 2.0F; // Duración del estado ENDING (2 segundos)
static constexpr int MUSIC_FADE_DURATION = 1800; // Fade de audio en milisegundos (1.8 segundos)
// --- Métodos ---
void update(); // Actualiza el objeto
void render(); // Dibuja el final en pantalla
static void handleEvents(); // Comprueba el manejador de eventos
static void handleInput(); // Comprueba las entradas
void iniTexts(); // Inicializa los textos
void iniPics(); // Inicializa las imágenes
void iniScenes(); // Inicializa las escenas
void updateState(float delta_time); // Actualiza la máquina de estados
void handleSceneFadeout(float scene_duration, float delta_time); // Lógica de fade común a los estados SCENE_N
void transitionToState(State new_state); // Transición entre estados
void updateSpriteCovers(); // Actualiza las cortinillas de los elementos
void checkChangeScene(); // Comprueba si se ha de cambiar de escena
void updateMusicVolume() const; // Actualiza el volumen de la música
// --- Variables miembro ---
// Objetos y punteros a recursos
std::unique_ptr<PixelReveal> scene_cover_; // Cortinilla de salida (negro sobre la escena)
std::unique_ptr<DeltaTimer> delta_timer_; // Timer para time-based update
std::vector<EndingSurface> sprite_texts_; // Vector con los sprites de texto con su cortinilla
std::vector<EndingSurface> sprite_pics_; // Vector con los sprites de imágenes con su cortinilla
std::vector<SceneData> scenes_; // Vector con los textos e imágenes de cada escena
// Variables de estado
State state_{State::WARMING_UP}; // Estado actual
float state_time_{0.0F}; // Tiempo acumulado en el estado actual
float total_time_{0.0F}; // Tiempo total acumulado desde el inicio
float fadeout_time_{0.0F}; // Tiempo acumulado para la cortinilla de salida
int current_scene_{0}; // Escena actual (0-4)
};

View File

@@ -0,0 +1,505 @@
#include "game/scenes/ending2.hpp"
#include <SDL3/SDL.h>
#include <algorithm> // Para max, replace
#include "core/audio/audio.hpp" // Para Audio
#include "core/input/global_inputs.hpp" // Para check
#include "core/input/input.hpp" // Para Input
#include "core/locale/locale.hpp" // Para Locale
#include "core/rendering/screen.hpp" // Para Screen
#include "core/rendering/sprite/dissolve_sprite.hpp" // Para SurfaceDissolveSprite
#include "core/rendering/sprite/moving_sprite.hpp" // Para SMovingSprite
#include "core/rendering/surface.hpp" // Para Surface
#include "core/rendering/text.hpp" // Para Text
#include "core/resources/resource_cache.hpp" // Para Resource
#include "core/system/global_events.hpp" // Para check
#include "game/options.hpp" // Para Options, options, OptionsGame, Sectio...
#include "game/scene_manager.hpp" // Para SceneManager
#include "utils/defines.hpp" // Para GameCanvas::CENTER_X, GameCanvas::CENTER_Y
#include "utils/delta_timer.hpp" // Para DeltaTimer
#include "utils/utils.hpp" // Para PaletteColor, stringToColor
// Constructor
Ending2::Ending2()
: delta_timer_(std::make_unique<DeltaTimer>()),
state_{.state = EndingState::PRE_CREDITS, .duration = STATE_PRE_CREDITS_DURATION} {
// Establece la escena
SceneManager::current = SceneManager::Scene::ENDING2;
SceneManager::options = SceneManager::Options::NONE;
// Inicializa el vector de colores
const std::vector<std::string> COLORS = {"white", "yellow", "cyan", "green", "magenta", "red", "blue", "black"};
for (const auto& color : COLORS) {
colors_.push_back(stringToColor(color));
}
Screen::get()->setBorderColor(static_cast<Uint8>(PaletteColor::BLACK)); // Cambia el color del borde
iniSpriteList(); // Inicializa la lista de sprites
loadSprites(); // Carga todos los sprites desde una lista
placeSprites(); // Coloca los sprites en su sito
createSpriteTexts(); // Crea los sprites con las texturas con los textos
createTexts(); // Crea los sprites con las texturas con los textos del final
}
// Actualiza el objeto
void Ending2::update() {
const float DELTA_TIME = delta_timer_->tick();
handleEvents(); // Comprueba los eventos
handleInput(); // Comprueba las entradas
updateState(DELTA_TIME); // Actualiza el estado
switch (state_.state) {
case EndingState::CREDITS:
// Actualiza los sprites, los textos y los textos del final
updateSprites(DELTA_TIME);
updateTextSprites(DELTA_TIME);
updateTexts(DELTA_TIME);
break;
case EndingState::FADING:
// Actualiza el fade final
updateFinalFade();
break;
default:
// No hacer nada si el estado no corresponde a un caso manejado
break;
}
Audio::update(); // Actualiza el objeto Audio
Screen::get()->update(DELTA_TIME); // Actualiza el objeto Screen
}
// Dibuja el final en pantalla
void Ending2::render() {
// Prepara para empezar a dibujar en la surface de juego
Screen::get()->start();
// Limpia la pantalla
Screen::get()->clearSurface(static_cast<Uint8>(PaletteColor::BLACK));
// Dibuja los sprites
renderSprites();
// Dibuja los sprites con el texto
renderSpriteTexts();
// Dibuja los sprites con el texto del final
renderTexts();
// Vuelca el contenido del renderizador en pantalla
Screen::get()->render();
}
// Comprueba el manejador de eventos
void Ending2::handleEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event);
}
}
// Comprueba las entradas
void Ending2::handleInput() {
Input::get()->update();
GlobalInputs::handle();
}
// Bucle principal
void Ending2::run() {
Audio::get()->playMusic("574071_EA_DTV.ogg");
while (SceneManager::current == SceneManager::Scene::ENDING2) {
update();
render();
}
Audio::get()->stopMusic();
}
// Actualiza el estado
void Ending2::updateState(float delta_time) {
state_time_ += delta_time;
switch (state_.state) {
case EndingState::PRE_CREDITS:
if (state_time_ >= STATE_PRE_CREDITS_DURATION) {
transitionToState(EndingState::CREDITS);
}
break;
case EndingState::CREDITS:
if (texts_.back()->getPosY() <= GameCanvas::CENTER_Y) {
transitionToState(EndingState::POST_CREDITS);
}
break;
case EndingState::POST_CREDITS:
if (state_time_ >= STATE_POST_CREDITS_DURATION) {
transitionToState(EndingState::FADING);
}
break;
case EndingState::FADING:
if (state_time_ >= STATE_FADE_DURATION) {
SceneManager::current = SceneManager::Scene::LOGO;
SceneManager::options = SceneManager::Options::LOGO_TO_TITLE;
}
break;
default:
break;
}
}
// Transición entre estados
void Ending2::transitionToState(EndingState new_state) {
state_.state = new_state;
state_time_ = 0.0F;
// Actualizar duración según el nuevo estado
switch (new_state) {
case EndingState::PRE_CREDITS:
state_.duration = STATE_PRE_CREDITS_DURATION;
break;
case EndingState::POST_CREDITS:
state_.duration = STATE_POST_CREDITS_DURATION;
break;
case EndingState::FADING:
state_.duration = STATE_FADE_DURATION;
// Al entrar en FADING, iniciar fade de audio
Audio::get()->fadeOutMusic(MUSIC_FADE_DURATION);
break;
case EndingState::CREDITS:
state_.duration = 0.0F; // CREDITS no tiene duración fija, termina cuando el último texto llega al centro
break;
default:
break;
}
}
// Inicializa la lista de sprites
void Ending2::iniSpriteList() {
// Reinicia el vector
sprite_list_.clear();
// Añade los valores
sprite_list_.emplace_back("bin");
sprite_list_.emplace_back("floppy");
sprite_list_.emplace_back("bird");
sprite_list_.emplace_back("chip");
sprite_list_.emplace_back("jeannine");
sprite_list_.emplace_back("spark");
sprite_list_.emplace_back("code");
sprite_list_.emplace_back("paco");
sprite_list_.emplace_back("elsa");
sprite_list_.emplace_back("z80");
sprite_list_.emplace_back("bell");
sprite_list_.emplace_back("dong");
sprite_list_.emplace_back("amstrad_cs");
sprite_list_.emplace_back("breakout");
sprite_list_.emplace_back("flying_arounder");
sprite_list_.emplace_back("stopped_arounder");
sprite_list_.emplace_back("walking_arounder");
sprite_list_.emplace_back("arounders_door");
sprite_list_.emplace_back("arounders_machine");
sprite_list_.emplace_back("abad");
sprite_list_.emplace_back("abad_bell");
sprite_list_.emplace_back("lord_abad");
sprite_list_.emplace_back("bat");
sprite_list_.emplace_back("batman_bell");
sprite_list_.emplace_back("batman_fire");
sprite_list_.emplace_back("batman");
sprite_list_.emplace_back("demon");
sprite_list_.emplace_back("heavy");
sprite_list_.emplace_back("dimallas");
sprite_list_.emplace_back("guitar");
sprite_list_.emplace_back("jailbattle_alien");
sprite_list_.emplace_back("jailbattle_human");
sprite_list_.emplace_back("jailer1");
sprite_list_.emplace_back("jailer2");
sprite_list_.emplace_back("jailer3");
sprite_list_.emplace_back("bry");
sprite_list_.emplace_back("upv_student");
sprite_list_.emplace_back("lamp");
sprite_list_.emplace_back("robot");
sprite_list_.emplace_back("congo");
sprite_list_.emplace_back("crosshair");
sprite_list_.emplace_back("tree_thing");
sprite_list_.emplace_back("matatunos");
sprite_list_.emplace_back("tuno");
sprite_list_.emplace_back("mummy");
sprite_list_.emplace_back("sam");
sprite_list_.emplace_back("qvoid");
sprite_list_.emplace_back("sigmasua");
sprite_list_.emplace_back("tv_panel");
sprite_list_.emplace_back("tv");
sprite_list_.emplace_back("spider");
sprite_list_.emplace_back("shock");
sprite_list_.emplace_back("wave");
sprite_list_.emplace_back("player");
}
// Carga todos los sprites desde una lista
void Ending2::loadSprites() {
// Inicializa variables
sprite_max_width_ = 0;
sprite_max_height_ = 0;
// Carga los sprites
for (const auto& file : sprite_list_) {
const auto& animation_data = Resource::Cache::get()->getAnimationData(file + ".yaml");
sprites_.emplace_back(std::make_shared<DissolveSprite>(animation_data));
sprites_.back()->setColorReplace(1, static_cast<Uint8>(PaletteColor::RED));
sprites_.back()->setProgress(1.0F); // comença invisible
sprite_max_width_ = std::max(sprites_.back()->getWidth(), sprite_max_width_);
sprite_max_height_ = std::max(sprites_.back()->getHeight(), sprite_max_height_);
}
// El último sprite (player) va en blanco, no en rojo
sprites_.back()->setColorReplace(1, static_cast<Uint8>(PaletteColor::WHITE));
}
// Actualiza los sprites
void Ending2::updateSprites(float delta) {
for (const auto& sprite : sprites_) {
sprite->update(delta);
const float Y = sprite->getPosY();
const float H = sprite->getHeight();
const auto CANVAS_H = Options::game.height;
// Checkpoint inferior: sprite entra per baix → generar de dalt a baix
if (Y > static_cast<float>(ENTRY_EXIT_PADDING) && Y <= CANVAS_H - H - ENTRY_EXIT_PADDING && sprite->getProgress() >= 1.0F && sprite->isTransitionDone()) {
sprite->startGenerate(TRANSITION_DURATION_MS, DissolveDirection::UP);
}
// Checkpoint superior: sprite surt per dalt → dissoldre de dalt a baix
if (Y <= static_cast<float>(ENTRY_EXIT_PADDING) && sprite->getProgress() <= 0.0F && sprite->isTransitionDone()) {
sprite->startDissolve(TRANSITION_DURATION_MS, DissolveDirection::DOWN);
}
}
}
// Actualiza los sprites de texto
void Ending2::updateTextSprites(float delta) {
for (const auto& sprite : sprite_texts_) {
sprite->update(delta);
const float Y = sprite->getPosY();
const float H = sprite->getHeight();
const auto CANVAS_H = Options::game.height;
if (Y > static_cast<float>(ENTRY_EXIT_PADDING) && Y <= CANVAS_H - H - ENTRY_EXIT_PADDING && sprite->getProgress() >= 1.0F && sprite->isTransitionDone()) {
sprite->startGenerate(TRANSITION_DURATION_MS, DissolveDirection::UP);
}
if (Y <= static_cast<float>(ENTRY_EXIT_PADDING) && sprite->getProgress() <= 0.0F && sprite->isTransitionDone()) {
sprite->startDissolve(TRANSITION_DURATION_MS, DissolveDirection::DOWN);
}
}
}
// Actualiza los sprites de texto del final
void Ending2::updateTexts(float delta) {
for (const auto& sprite : texts_) {
sprite->update(delta);
const float Y = sprite->getPosY();
const float H = sprite->getHeight();
const auto CANVAS_H = Options::game.height;
// Checkpoint inferior: text entra per baix → generar de dalt a baix
if (Y > static_cast<float>(ENTRY_EXIT_PADDING) && Y <= CANVAS_H - H - ENTRY_EXIT_PADDING && sprite->getProgress() >= 1.0F && sprite->isTransitionDone()) {
sprite->startGenerate(TRANSITION_DURATION_MS, DissolveDirection::UP);
}
// Checkpoint superior: text surt per dalt → dissoldre de dalt a baix
if (Y <= static_cast<float>(ENTRY_EXIT_PADDING) && sprite->getProgress() <= 0.0F && sprite->isTransitionDone()) {
sprite->startDissolve(TRANSITION_DURATION_MS, DissolveDirection::DOWN);
}
}
}
// Dibuja los sprites
void Ending2::renderSprites() {
for (const auto& sprite : sprites_) {
const bool A = sprite->getRect().y + sprite->getRect().h > 0;
const bool B = sprite->getRect().y < Options::game.height;
if (A && B) {
sprite->render();
}
}
}
// Dibuja los sprites con el texto
void Ending2::renderSpriteTexts() {
for (const auto& sprite : sprite_texts_) {
const bool A = sprite->getRect().y + sprite->getRect().h > 0;
const bool B = sprite->getRect().y < Options::game.height;
if (A && B) {
sprite->render();
}
}
}
// Dibuja los sprites con el texto del final
void Ending2::renderTexts() {
for (const auto& sprite : texts_) {
const bool A = sprite->getRect().y + sprite->getRect().h > 0;
const bool B = sprite->getRect().y < Options::game.height;
if (A && B) {
sprite->render();
}
}
}
// Coloca los sprites en su sito
void Ending2::placeSprites() const {
for (int i = 0; i < static_cast<int>(sprites_.size()); ++i) {
const float X = i % 2 == 0 ? FIRST_COL : SECOND_COL;
const float Y = ((i / 1) * (sprite_max_height_ + DIST_SPRITE_TEXT + Resource::Cache::get()->getText("smb2")->getCharacterSize() + DIST_SPRITE_SPRITE)) + Options::game.height + INITIAL_Y_OFFSET;
const float W = sprites_.at(i)->getWidth();
const float H = sprites_.at(i)->getHeight();
const float DX = -(W / 2);
const float DY = sprite_max_height_ - H;
sprites_.at(i)->setPos({.x = X + DX, .y = Y + DY, .w = W, .h = H});
sprites_.at(i)->setVelY(SPRITE_DESP_SPEED);
}
// Recoloca el sprite del jugador, que es el último de la lista
const float X = (Options::game.width - sprites_.back()->getWidth()) / 2;
const float Y = sprites_.back()->getPosY() + (sprite_max_height_ * 2);
sprites_.back()->setPos(X, Y);
sprites_.back()->setCurrentAnimation("default");
}
// Crea los sprites con las texturas con los textos
void Ending2::createSpriteTexts() { // NOLINT(readability-convert-member-functions-to-static)
// Crea los sprites de texto a partir de la lista
for (size_t i = 0; i < sprite_list_.size(); ++i) {
auto text = Resource::Cache::get()->getText("smb2");
// Procesa y ajusta el texto del sprite actual
std::string txt = sprite_list_[i];
std::ranges::replace(txt, '_', ' '); // Reemplaza '_' por ' '
if (txt == "player") {
txt = Locale::get()->get("ending2.jaildoctor"); // NOLINT(readability-static-accessed-through-instance) Reemplaza "player" por nombre localizado
}
// Calcula las dimensiones del texto
const float W = text->length(txt, 1);
const float H = text->getCharacterSize();
// Determina la columna y la posición X del texto
const float X = (i == sprite_list_.size() - 1)
? (GameCanvas::CENTER_X - (W / 2))
: ((i % 2 == 0 ? FIRST_COL : SECOND_COL) - (W / 2));
// Calcula la posición Y del texto en base a la posición y altura del sprite
const float Y = sprites_.at(i)->getPosY() + sprites_.at(i)->getHeight() + DIST_SPRITE_TEXT;
// Crea la surface
auto surface = std::make_shared<Surface>(W, H);
auto previuos_renderer = Screen::get()->getRendererSurface();
Screen::get()->setRendererSurface(surface);
text->write(0, 0, txt);
// Crea el sprite
SDL_FRect pos = {.x = X, .y = Y, .w = W, .h = H};
sprite_texts_.emplace_back(std::make_shared<DissolveSprite>(surface, pos));
sprite_texts_.back()->setColorReplace(1, static_cast<Uint8>(PaletteColor::WHITE));
sprite_texts_.back()->setProgress(1.0F); // comença invisible
sprite_texts_.back()->setVelY(SPRITE_DESP_SPEED);
Screen::get()->setRendererSurface(previuos_renderer);
}
}
// Crea los sprites con las texturas con los textos del final
void Ending2::createTexts() { // NOLINT(readability-convert-member-functions-to-static)
// Crea los primeros textos
std::vector<std::string> list;
list.emplace_back(Locale::get()->get("ending2.starring"));
auto text = Resource::Cache::get()->getText("smb2");
// Crea los sprites de texto a partir de la lista
for (size_t i = 0; i < list.size(); ++i) {
// Calcula constantes
const float W = text->length(list[i], 1);
const float H = text->getCharacterSize();
const float X = GameCanvas::CENTER_X;
const float DX = -(W / 2);
const float Y = Options::game.height + (text->getCharacterSize() * (i * TEXT_SPACING_MULTIPLIER));
// Crea la surface
auto surface = std::make_shared<Surface>(W, H);
auto previuos_renderer = Screen::get()->getRendererSurface();
Screen::get()->setRendererSurface(surface);
text->write(0, 0, list[i]);
// Crea el sprite
SDL_FRect pos = {.x = X + DX, .y = Y, .w = W, .h = H};
texts_.emplace_back(std::make_shared<DissolveSprite>(surface, pos));
texts_.back()->setProgress(1.0F); // comença invisible
texts_.back()->setVelY(SPRITE_DESP_SPEED);
Screen::get()->setRendererSurface(previuos_renderer);
}
// Crea los últimos textos
// El primer texto va a continuación del ultimo spriteText
const int START = sprite_texts_.back()->getPosY() + (text->getCharacterSize() * 15);
list.clear();
list.emplace_back(Locale::get()->get("ending2.thank_you"));
list.emplace_back(Locale::get()->get("ending2.for_playing"));
// Crea los sprites de texto a partir de la lista
for (size_t i = 0; i < list.size(); ++i) {
// Calcula constantes
const float W = text->length(list[i], 1);
const float H = text->getCharacterSize();
const float X = GameCanvas::CENTER_X;
const float DX = -(W / 2);
const float Y = START + (text->getCharacterSize() * (i * TEXT_SPACING_MULTIPLIER));
// Crea la surface
auto surface = std::make_shared<Surface>(W, H);
auto previuos_renderer = Screen::get()->getRendererSurface();
Screen::get()->setRendererSurface(surface);
text->write(0, 0, list[i]);
// Crea el sprite
SDL_FRect pos = {.x = X + DX, .y = Y, .w = W, .h = H};
texts_.emplace_back(std::make_shared<DissolveSprite>(surface, pos));
texts_.back()->setProgress(1.0F); // comença invisible
texts_.back()->setVelY(SPRITE_DESP_SPEED);
Screen::get()->setRendererSurface(previuos_renderer);
}
}
// Actualiza el fade final
void Ending2::updateFinalFade() {
for (const auto& sprite : texts_) {
sprite->getSurface()->fadeSubPalette(0);
}
}

View File

@@ -0,0 +1,94 @@
#pragma once
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include "core/rendering/sprite/dissolve_sprite.hpp" // Para SurfaceDissolveSprite
#include "utils/defines.hpp" // Para GameCanvas::WIDTH, GameCanvas::FIRST_QUAR...
class MovingSprite;
class DeltaTimer;
class Ending2 {
public:
// --- Constructor y Destructor ---
Ending2();
~Ending2() = default;
// --- Bucle principal ---
void run();
private:
// --- Enumeraciones ---
enum class EndingState : int {
PRE_CREDITS, // Estado previo a los créditos
CREDITS, // Estado de los créditos
POST_CREDITS, // Estado posterior a los créditos
FADING, // Estado de fundido de los textos a negro
};
// --- Estructuras ---
struct State {
EndingState state{EndingState::PRE_CREDITS}; // Estado actual
float duration{0.0F}; // Duración en segundos para el estado actual
};
// --- Constantes ---
static constexpr int FIRST_COL = GameCanvas::FIRST_QUARTER_X + (GameCanvas::WIDTH / 16); // Primera columna por donde desfilan los sprites
static constexpr int SECOND_COL = GameCanvas::THIRD_QUARTER_X - (GameCanvas::WIDTH / 16); // Segunda columna por donde desfilan los sprites
static constexpr int DIST_SPRITE_TEXT = 8; // Distancia entre el sprite y el texto que lo acompaña
static constexpr int DIST_SPRITE_SPRITE = 0; // Distancia entre dos sprites de la misma columna
static constexpr int INITIAL_Y_OFFSET = 40; // Offset inicial en Y para posicionar sprites
static constexpr int SCREEN_MESH_HEIGHT = 8; // Altura de la malla superior/inferior de la pantalla
static constexpr int FADE_H = 24; // Alçada de la zona de dissolució als cantons (files)
static constexpr float TRANSITION_DURATION_MS = 500.0F; // ms per canviar d'estat (generar o dissoldre)
static constexpr int ENTRY_EXIT_PADDING = 2; // px de padding als bordes per activar dissolució/generació
static constexpr int TEXT_SPACING_MULTIPLIER = 2; // Multiplicador para espaciado entre líneas de texto
// Constantes de tiempo (basadas en tiempo real, no en frames)
static constexpr float SPRITE_DESP_SPEED = -12.0F; // Velocidad de desplazamiento en pixels/segundo (era -0.2 px/frame @ 60fps)
static constexpr float STATE_PRE_CREDITS_DURATION = 3.0F; // Duración del estado previo a créditos en segundos
static constexpr float STATE_POST_CREDITS_DURATION = 5.0F; // Duración del estado posterior a créditos en segundos
static constexpr float STATE_FADE_DURATION = 5.0F; // Duración del fade final en segundos
static constexpr int MUSIC_FADE_DURATION = 3000; // Duración del fade de música en milisegundos (para Audio API)
// --- Métodos ---
void update(); // Actualiza el objeto
void render(); // Dibuja el final en pantalla
static void handleEvents(); // Comprueba el manejador de eventos
static void handleInput(); // Comprueba las entradas
void updateState(float delta_time); // Actualiza el estado
void transitionToState(EndingState new_state); // Transición entre estados
void iniSpriteList(); // Inicializa la lista de sprites
void loadSprites(); // Carga todos los sprites desde una lista
void updateSprites(float delta); // Actualiza los sprites
void updateTextSprites(float delta); // Actualiza los sprites de texto
void updateTexts(float delta); // Actualiza los sprites de texto del final
void renderSprites(); // Dibuja los sprites
void renderSpriteTexts(); // Dibuja los sprites con el texto
void renderTexts(); // Dibuja los sprites con el texto del final
void placeSprites() const; // Coloca los sprites en su sitio
void createSpriteTexts(); // Crea los sprites con las texturas con los textos
void createTexts(); // Crea los sprites con las texturas con los textos del final
void updateFinalFade(); // Actualiza el fade final
// --- Variables miembro ---
// Objetos y punteros a recursos
std::vector<std::shared_ptr<DissolveSprite>> sprites_; // Vector con todos los sprites a dibujar
std::vector<std::shared_ptr<DissolveSprite>> sprite_texts_; // Vector con los sprites de texto de los sprites
std::vector<std::shared_ptr<DissolveSprite>> texts_; // Vector con los sprites de texto
std::unique_ptr<DeltaTimer> delta_timer_; // Timer para time-based update
// Variables de estado
State state_; // Controla el estado de la clase
float state_time_{0.0F}; // Tiempo acumulado en el estado actual
// Variables auxiliares
std::vector<std::string> sprite_list_; // Lista con todos los sprites a dibujar
std::vector<Uint8> colors_; // Vector con los colores para el fade
float sprite_max_width_{0.0F}; // El valor de ancho del sprite más ancho
float sprite_max_height_{0.0F}; // El valor de alto del sprite más alto
};

1034
source/game/scenes/game.cpp Normal file

File diff suppressed because it is too large Load Diff

133
source/game/scenes/game.hpp Normal file
View File

@@ -0,0 +1,133 @@
#pragma once
#include <SDL3/SDL.h>
#include <initializer_list> // Para initializer_list
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include "game/entities/player.hpp" // Para PlayerSpawn
#include "utils/delta_timer.hpp" // Para DeltaTimer
class Room; // lines 12-12
class RoomTracker; // lines 13-13
class Scoreboard; // lines 14-14
class Surface;
class Game {
public:
// --- Estructuras ---
enum class Mode {
DEMO,
GAME
};
enum class State {
PLAYING, // Normal gameplay
BLACK_SCREEN, // Black screen after death (0.30s)
GAME_OVER, // Intermediate state before changing scene
FADE_TO_ENDING, // Fade out transition
POST_FADE_ENDING, // Black screen delay before ending
};
// --- Constructor y Destructor ---
explicit Game(Mode mode);
~Game();
// --- Bucle para el juego ---
void run();
private:
// --- Constantes de tiempo ---
static constexpr float BLACK_SCREEN_DURATION = 0.30F; // Duración de la pantalla negra en segundos (20 frames a 66.67fps)
static constexpr float GAME_OVER_THRESHOLD = 0.255F; // Tiempo antes del game over en segundos (17 frames a 66.67fps)
static constexpr float DEMO_ROOM_DURATION = 6.0F; // Duración de cada habitación en modo demo en segundos (400 frames)
static constexpr float JAIL_RESTORE_INTERVAL = 1.5F; // Intervalo de restauración de vidas en la Jail en segundos (100 frames)
static constexpr float FADE_STEP_INTERVAL = 0.05F; // Intervalo entre pasos de fade en segundos
static constexpr float POST_FADE_DELAY = 2.0F; // Duración de la pantalla negra después del fade
// --- Estructuras ---
struct DemoData {
float time_accumulator{0.0F}; // Acumulador de tiempo para el modo demo
int room_index{0}; // Índice para el vector de habitaciones
std::vector<std::string> rooms; // Listado con los mapas de la demo
};
// --- Métodos ---
void update(); // Actualiza el juego, las variables, comprueba la entrada, etc.
void render(); // Pinta los objetos en pantalla
void handleEvents(); // Comprueba los eventos de la cola
void renderRoomName(); // Escribe el nombre de la pantalla
void transitionToState(State new_state); // Cambia al estado especificado y resetea los timers
void updatePlaying(float delta_time); // Actualiza el juego en estado PLAYING
void updateBlackScreen(float delta_time); // Actualiza el juego en estado BLACK_SCREEN
void updateGameOver(float delta_time); // Actualiza el juego en estado GAME_OVER
void updateFadeToEnding(float delta_time); // Actualiza el juego en estado FADE_TO_ENDING
void updatePostFadeEnding(float delta_time); // Actualiza el juego en estado POST_FADE_ENDING
void renderPlaying(); // Renderiza el juego en estado PLAYING (directo a pantalla)
static void renderBlackScreen(); // Renderiza el juego en estado BLACK_SCREEN (pantalla negra)
static void renderGameOver(); // Renderiza el juego en estado GAME_OVER (pantalla negra)
void renderFadeToEnding(); // Renderiza el juego en estado FADE_TO_ENDING (via backbuffer)
static void renderPostFadeEnding(); // Renderiza el juego en estado POST_FADE_ENDING (pantalla negra)
auto changeRoom(const std::string& room_path) -> bool; // Cambia de habitación
void handleInput(); // Comprueba el teclado
void checkPlayerIsOnBorder(); // Comprueba si el jugador esta en el borde de la pantalla y actua
auto checkPlayerAndEnemies() -> bool; // Comprueba las colisiones del jugador con los enemigos
void checkPlayerAndItems(); // Comprueba las colisiones del jugador con los objetos
void checkIfPlayerIsAlive(); // Comprueba si el jugador esta vivo
void killPlayer(); // Mata al jugador
void setScoreBoardColor(); // Pone el color del marcador en función del color del borde de la habitación
auto checkEndGame() -> bool; // Comprueba si ha finalizado el juego
static auto getTotalItems() -> int; // Obtiene la cantidad total de items que hay en el mapeado del juego
void togglePause(); // Pone el juego en pausa
void checkRestoringJail(float delta_time); // Da vidas al jugador cuando está en la Jail
void fillRoomNameTexture(); // Pone el nombre de la habitación en la textura
void checkSomeCheevos(); // Comprueba algunos logros
void checkEndGameCheevos(); // Comprueba los logros de completar el juego
void initPlayer(const Player::SpawnData& spawn_point, std::shared_ptr<Room> room); // Inicializa al jugador
void createRoomNameTexture(); // Crea la textura para poner el nombre de la habitación
void keepMusicPlaying(); // Hace sonar la música
void demoInit(); // DEMO MODE: Inicializa las variables para el modo demo
void demoCheckRoomChange(float delta_time); // DEMO MODE: Comprueba si se ha de cambiar de habitación
#ifdef _DEBUG
static void renderDebugInfo(); // Pone la información de debug en pantalla
void handleDebugEvents(const SDL_Event& event); // Comprueba los eventos
void handleDebugMouseDrag(float delta_time); // Maneja el arrastre del jugador con el ratón (debug)
#endif
// --- Variables miembro ---
// Objetos y punteros a recursos
std::shared_ptr<Scoreboard::Data> scoreboard_data_; // Estructura con los datos del marcador
std::shared_ptr<Scoreboard> scoreboard_; // Objeto encargado de gestionar el marcador
std::shared_ptr<RoomTracker> room_tracker_; // Lleva el control de las habitaciones visitadas
std::shared_ptr<Room> room_; // Objeto encargado de gestionar cada habitación del juego
std::shared_ptr<Player> player_; // Objeto con el jugador
std::shared_ptr<Surface> room_name_surface_; // Textura para escribir el nombre de la habitación
std::shared_ptr<Surface> game_backbuffer_surface_; // Backbuffer para efectos de fade
// Variables de estado del juego
Mode mode_; // Modo del juego
State state_{State::PLAYING}; // Estado actual de la escena
DeltaTimer delta_timer_; // Timer para calcular delta time
std::string current_room_; // Fichero de la habitación actual
Player::SpawnData spawn_data_; // Lugar de la habitación donde aparece el jugador
int total_items_; // Cantidad total de items que hay en el mapeado del juego
bool paused_{false}; // Indica si el juego se encuentra en pausa
float state_time_{0.0F}; // Tiempo acumulado en el estado actual
float fade_accumulator_{0.0F}; // Acumulador de tiempo para el fade
// Variables de demo mode
DemoData demo_; // Variables para el modo demo
// Variables de efectos visuales
SDL_FRect room_name_rect_; // Rectangulo donde pintar la textura con el nombre de la habitación
float jail_restore_time_{0.0F}; // Tiempo acumulado para restauración de vidas en la Jail
#ifdef _DEBUG
// Variables de debug para arrastre con ratón
bool debug_dragging_player_{false}; // Indica si estamos arrastrando al jugador con el ratón
float debug_drag_speed_{0.0F}; // Velocidad actual del arrastre (ease-in)
// Estado previo de invencibilidad antes de entrar en modo debug
bool invincible_before_debug_{false};
#endif
};

View File

@@ -0,0 +1,210 @@
#include "game/scenes/game_over.hpp"
#include <SDL3/SDL.h>
#include <algorithm> // Para min, max
#include <string> // Para basic_string, operator+, to_string
#include "core/audio/audio.hpp" // Para Audio
#include "core/input/global_inputs.hpp" // Para check
#include "core/input/input.hpp" // Para Input
#include "core/locale/locale.hpp" // Para Locale
#include "core/rendering/screen.hpp" // Para Screen
#include "core/rendering/sprite/animated_sprite.hpp" // Para SAnimatedSprite
#include "core/rendering/text.hpp" // Para Text::CENTER_FLAG, Text::COLOR_FLAG, Text
#include "core/resources/resource_cache.hpp" // Para Resource
#include "core/system/global_events.hpp" // Para check
#include "game/options.hpp" // Para Options, options, OptionsStats, Secti...
#include "game/scene_manager.hpp" // Para SceneManager
#include "utils/defines.hpp" // Para GameCanvas::CENTER_X
#include "utils/delta_timer.hpp" // Para DeltaTimer
#include "utils/utils.hpp" // Para PaletteColor, stringToColor
// Constructor
GameOver::GameOver()
: player_sprite_(std::make_shared<AnimatedSprite>(Resource::Cache::get()->getAnimationData("player_game_over.yaml"))),
tv_sprite_(std::make_shared<AnimatedSprite>(Resource::Cache::get()->getAnimationData("tv.yaml"))),
delta_timer_(std::make_shared<DeltaTimer>()) {
SceneManager::current = SceneManager::Scene::GAME_OVER;
SceneManager::options = SceneManager::Options::NONE;
// Inicializa las posiciones de los sprites usando las constantes
player_sprite_->setPosX(GameCanvas::CENTER_X + PLAYER_X_OFFSET);
player_sprite_->setPosY(TEXT_Y + SPRITE_Y_OFFSET);
tv_sprite_->setPosX(GameCanvas::CENTER_X - tv_sprite_->getWidth() - TV_X_OFFSET);
tv_sprite_->setPosY(TEXT_Y + SPRITE_Y_OFFSET);
Screen::get()->setBorderColor(static_cast<Uint8>(PaletteColor::BLACK));
// Inicializa el vector de colores (de brillante a oscuro para fade)
const std::vector<std::string> COLORS = {"white", "yellow", "cyan", "green", "magenta", "red", "blue", "black"};
for (const auto& color : COLORS) {
colors_.push_back(stringToColor(color));
}
color_ = colors_.back(); // Empieza en black
}
// Actualiza el objeto
void GameOver::update() {
const float DELTA_TIME = delta_timer_->tick();
elapsed_time_ += DELTA_TIME;
handleEvents(); // Comprueba los eventos
handleInput(); // Comprueba las entradas
updateState(); // Actualiza el estado de la escena
updateColor(); // Actualiza el color usado para renderizar los textos e imagenes
player_sprite_->update(DELTA_TIME); // Actualiza el sprite
tv_sprite_->update(DELTA_TIME); // Actualiza el sprite
Audio::update(); // Actualiza el objeto Audio
Screen::get()->update(DELTA_TIME); // Actualiza el objeto Screen
}
// Dibuja el final en pantalla
void GameOver::render() {
Screen::get()->start();
Screen::get()->clearSurface(static_cast<Uint8>(PaletteColor::BLACK));
auto text = Resource::Cache::get()->getText("smb2");
// Escribe el texto de GAME OVER
auto* loc = Locale::get();
text->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, TEXT_Y, loc->get("game_over.title"), 1, color_); // NOLINT(readability-static-accessed-through-instance)
// Dibuja los sprites (ya posicionados en el constructor, solo ajustamos Y)
player_sprite_->setPosY(TEXT_Y + SPRITE_Y_OFFSET);
tv_sprite_->setPosY(TEXT_Y + SPRITE_Y_OFFSET);
renderSprites();
// Escribe el texto con las habitaciones y los items
const std::string ITEMS_TEXT = std::to_string(Options::stats.items / 100) + std::to_string((Options::stats.items % 100) / 10) + std::to_string(Options::stats.items % 10);
const std::string ROOMS_TEXT = std::to_string(Options::stats.rooms / 100) + std::to_string((Options::stats.rooms % 100) / 10) + std::to_string(Options::stats.rooms % 10);
text->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, TEXT_Y + ITEMS_Y_OFFSET, loc->get("game_over.items") + ITEMS_TEXT, 1, color_); // NOLINT(readability-static-accessed-through-instance)
text->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, TEXT_Y + ROOMS_Y_OFFSET, loc->get("game_over.rooms") + ROOMS_TEXT, 1, color_); // NOLINT(readability-static-accessed-through-instance)
// Vuelca el contenido del renderizador en pantalla
Screen::get()->render();
}
// Comprueba el manejador de eventos
void GameOver::handleEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event);
}
}
// Comprueba las entradas
void GameOver::handleInput() {
Input::get()->update();
GlobalInputs::handle();
}
// Bucle principal
void GameOver::run() {
while (SceneManager::current == SceneManager::Scene::GAME_OVER) {
update();
render();
}
}
// Actualiza el color usado para renderizar los textos e imagenes
void GameOver::updateColor() {
// Calcula el color basado en el estado actual
switch (state_) {
case State::WAITING:
// Durante la espera, mantener en black
color_ = colors_.back(); // black
break;
case State::FADE_IN: {
// Fade in: de black (último color) a white (primer color)
// Progreso: 0.0 (black) -> 1.0 (white)
const float PROGRESS = std::min(elapsed_time_ / FADE_IN_DURATION, 1.0F);
const int INDEX = (colors_.size() - 1) - static_cast<int>((colors_.size() - 1) * PROGRESS);
color_ = colors_[std::clamp(INDEX, 0, static_cast<int>(colors_.size() - 1))];
break;
}
case State::DISPLAY:
// Durante display, mantener el color más brillante
color_ = colors_[0]; // white
break;
case State::FADE_OUT: {
// Fade out: de white (primer color) a black (último color)
// Progreso: 0.0 (white) -> 1.0 (black)
const float PROGRESS = std::min(elapsed_time_ / FADE_OUT_DURATION, 1.0F);
const int INDEX = static_cast<int>((colors_.size() - 1) * PROGRESS);
color_ = colors_[std::clamp(INDEX, 0, static_cast<int>(colors_.size() - 1))];
break;
}
case State::ENDING:
case State::TRANSITION:
// Al final, mantener en black
color_ = colors_.back(); // black
break;
}
}
// Dibuja los sprites
void GameOver::renderSprites() {
player_sprite_->render(1, color_);
tv_sprite_->render(1, color_);
}
// Actualiza el estado de la escena y gestiona transiciones
void GameOver::updateState() {
// Máquina de estados basada en tiempo transcurrido
switch (state_) {
case State::WAITING:
// Espera inicial antes de empezar
if (elapsed_time_ >= WAITING_DURATION) {
state_ = State::FADE_IN;
elapsed_time_ = 0.0F;
// Hace sonar la música cuando termina la espera
Audio::get()->playMusic("574070_KUVO_Farewell_to_school.ogg", 0);
}
break;
case State::FADE_IN:
// Fade in de colores desde black
if (elapsed_time_ >= FADE_IN_DURATION) {
state_ = State::DISPLAY;
elapsed_time_ = 0.0F;
}
break;
case State::DISPLAY:
// Mostrando contenido con color brillante
if (elapsed_time_ >= DISPLAY_DURATION) {
state_ = State::FADE_OUT;
elapsed_time_ = 0.0F;
}
break;
case State::FADE_OUT:
// Fade out hacia black
if (elapsed_time_ >= FADE_OUT_DURATION) {
state_ = State::ENDING;
elapsed_time_ = 0.0F;
}
break;
case State::ENDING:
// Pantalla en negro antes de salir
if (elapsed_time_ >= ENDING_DURATION) {
state_ = State::TRANSITION;
elapsed_time_ = 0.0F;
}
break;
case State::TRANSITION:
// Transición a la escena de logo
SceneManager::current = SceneManager::Scene::LOGO;
SceneManager::options = SceneManager::Options::LOGO_TO_TITLE;
break;
}
}

View File

@@ -0,0 +1,67 @@
#pragma once
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr
#include <vector> // Para vector
class AnimatedSprite; // lines 7-7
class DeltaTimer; // Forward declaration
class GameOver {
public:
// Constructor y Destructor
GameOver();
~GameOver() = default;
// Bucle principal
void run();
private:
// --- Enumeraciones ---
enum class State {
WAITING, // Espera inicial antes de empezar
FADE_IN, // Fade in de colores desde black
DISPLAY, // Mostrando contenido con color brillante
FADE_OUT, // Fade out hacia black
ENDING, // Pantalla en negro antes de salir
TRANSITION // Cambio a logo
};
// --- Constantes de duración (segundos) ---
static constexpr float WAITING_DURATION = 0.8F; // Espera inicial
static constexpr float FADE_IN_DURATION = 0.32F; // Duración del fade in
static constexpr float DISPLAY_DURATION = 4.64F; // Duración mostrando contenido
static constexpr float FADE_OUT_DURATION = 0.32F; // Duración del fade out
static constexpr float ENDING_DURATION = 1.12F; // Espera en negro antes de salir
// --- Constantes de posición ---
static constexpr int TEXT_Y = 32; // Posición Y del texto principal
static constexpr int SPRITE_Y_OFFSET = 30; // Offset Y para sprites desde TEXT_Y
static constexpr int PLAYER_X_OFFSET = 10; // Offset X del jugador desde el centro
static constexpr int TV_X_OFFSET = 10; // Offset X del TV desde el centro
static constexpr int ITEMS_Y_OFFSET = 80; // Offset Y del texto de items desde TEXT_Y
static constexpr int ROOMS_Y_OFFSET = 90; // Offset Y del texto de rooms desde TEXT_Y
// --- Métodos ---
void update(); // Actualiza el objeto
void render(); // Dibuja el final en pantalla
static void handleEvents(); // Comprueba el manejador de eventos
static void handleInput(); // Comprueba las entradas
void updateState(); // Actualiza el estado y transiciones
void updateColor(); // Actualiza el color usado para renderizar
void renderSprites(); // Dibuja los sprites
// --- Variables miembro ---
// Objetos y punteros a recursos
std::shared_ptr<AnimatedSprite> player_sprite_; // Sprite con el jugador
std::shared_ptr<AnimatedSprite> tv_sprite_; // Sprite con el televisor
std::shared_ptr<DeltaTimer> delta_timer_; // Timer para time-based logic
// Variables de estado de la escena
State state_{State::WAITING}; // Estado actual de la escena
float elapsed_time_{0.0F}; // Tiempo transcurrido en el estado actual
// Variables de efectos visuales
std::vector<Uint8> colors_; // Vector con los colores para el fade
Uint8 color_{0}; // Color actual para texto y sprites
};

View File

@@ -0,0 +1,498 @@
#include "game/scenes/loading_screen.hpp"
#include <SDL3/SDL.h>
#include <cmath> // Para std::sin
#include <cstdlib> // Para rand
#include "core/audio/audio.hpp" // Para Audio
#include "core/input/global_inputs.hpp" // Para check
#include "core/input/input.hpp" // Para Input
#include "core/rendering/screen.hpp" // Para Screen
#include "core/rendering/sprite/sprite.hpp" // Para SSprite
#include "core/rendering/surface.hpp" // Para Surface
#include "core/resources/resource_cache.hpp" // Para Resource
#include "core/system/global_events.hpp" // Para check
#include "game/options.hpp" // Para Options, options, SectionState, Options...
#include "game/scene_manager.hpp" // Para SceneManager
#include "utils/defines.hpp" // Para GAME_SPEED
#include "utils/utils.hpp" // Para stringToColor, PaletteColor
// Constructor
LoadingScreen::LoadingScreen()
: mono_loading_screen_surface_(Resource::Cache::get()->getSurface("loading_screen_bn.gif")),
color_loading_screen_surface_(Resource::Cache::get()->getSurface("loading_screen_color.gif")),
mono_loading_screen_sprite_(std::make_unique<Sprite>(mono_loading_screen_surface_, 0, 0, mono_loading_screen_surface_->getWidth(), mono_loading_screen_surface_->getHeight())),
color_loading_screen_sprite_(std::make_unique<Sprite>(color_loading_screen_surface_, 0, 0, color_loading_screen_surface_->getWidth(), color_loading_screen_surface_->getHeight())),
program_sprite_(std::make_unique<Sprite>(Resource::Cache::get()->getSurface("program_jaildoc.gif"))),
screen_surface_(std::make_shared<Surface>(Options::game.width, Options::game.height)),
delta_timer_(std::make_unique<DeltaTimer>()) {
// Configura la superficie donde se van a pintar los sprites
screen_surface_->clear(static_cast<Uint8>(PaletteColor::WHITE));
// Inicializa variables
SceneManager::current = SceneManager::Scene::LOADING_SCREEN;
SceneManager::options = SceneManager::Options::NONE;
program_sprite_->setPosition(0.0F, 8.0F);
// Inicializa el array de índices de líneas
initLineIndexArray();
// Cambia el color del borde
Screen::get()->setBorderColor(stringToColor("white"));
transitionToState(State::SILENT1);
}
// Destructor
LoadingScreen::~LoadingScreen() {
Audio::get()->stopMusic();
}
// Comprueba el manejador de eventos
void LoadingScreen::handleEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event);
}
}
// Comprueba las entradas
void LoadingScreen::handleInput() {
Input::get()->update();
GlobalInputs::handle();
}
// Inicializa el array de índices de líneas (imita el direccionamiento de memoria del Spectrum)
void LoadingScreen::initLineIndexArray() { // NOLINT(readability-convert-member-functions-to-static)
for (int i = 0; i < MONO_TOTAL_LINES; ++i) {
if (i < 64) { // Primer bloque de 2K
line_index_[i] = ((i % 8) * 8) + (i / 8);
} else if (i < 128) { // Segundo bloque de 2K
line_index_[i] = 64 + ((i % 8) * 8) + ((i - 64) / 8);
} else { // Tercer bloque de 2K
line_index_[i] = 128 + ((i % 8) * 8) + ((i - 128) / 8);
}
}
}
// Transiciona a un nuevo estado
void LoadingScreen::transitionToState(State new_state) {
state_ = new_state;
state_time_ = 0.0F;
// Acciones específicas al entrar en cada estado
switch (new_state) {
case State::SILENT1:
case State::SILENT2:
current_border_type_ = Border::RED;
Audio::get()->stopMusic();
break;
case State::HEADER1:
case State::HEADER2:
current_border_type_ = Border::RED_AND_CYAN;
Audio::get()->playMusic("574071_EA_DTV.ogg", 0);
break;
case State::DATA1:
printProgramName();
current_border_type_ = Border::YELLOW_AND_BLUE;
Audio::get()->playMusic("574071_EA_DTV.ogg", 0);
break;
case State::DATA2:
current_border_type_ = Border::YELLOW_AND_BLUE;
Audio::get()->playMusic("574070_KUVO_Farewell_to_school.ogg", 0);
break;
case State::LOADING_MONO:
current_border_type_ = Border::YELLOW_AND_BLUE;
Audio::get()->playMusic("574071_EA_DTV.ogg", 0);
last_mono_step_ = -1; // Resetear contador de pasos mono
break;
case State::LOADING_COLOR:
current_border_type_ = Border::YELLOW_AND_BLUE;
Audio::get()->playMusic("574070_KUVO_Farewell_to_school.ogg", 0);
last_color_block_ = -1; // Resetear contador de bloques color
break;
case State::COMPLETE:
current_border_type_ = Border::BLACK;
// Transicionar a la pantalla de título
SceneManager::current = SceneManager::Scene::TITLE;
SceneManager::options = SceneManager::Options::TITLE_WITH_LOADING_SCREEN;
Audio::get()->stopMusic();
break;
}
}
// Actualiza el estado actual
void LoadingScreen::updateState(float delta_time) {
state_time_ += delta_time;
// Transiciones automáticas por tiempo para los estados iniciales
// LOADING_MONO y LOADING_COLOR transicionan en sus propias funciones
switch (state_) {
case State::SILENT1:
if (state_time_ >= SILENT1_DURATION) {
transitionToState(State::HEADER1);
}
break;
case State::HEADER1:
if (state_time_ >= HEADER1_DURATION) {
transitionToState(State::DATA1);
}
break;
case State::DATA1:
if (state_time_ >= DATA1_DURATION) {
transitionToState(State::SILENT2);
}
break;
case State::SILENT2:
if (state_time_ >= SILENT2_DURATION) {
transitionToState(State::HEADER2);
}
break;
case State::HEADER2:
if (state_time_ >= HEADER2_DURATION) {
transitionToState(State::LOADING_MONO);
}
break;
case State::DATA2:
if (state_time_ >= DATA2_DURATION) {
transitionToState(State::COMPLETE);
}
break;
case State::LOADING_MONO:
case State::LOADING_COLOR:
case State::COMPLETE:
// Estos estados se gestionan en updateMonoLoad/updateColorLoad
break;
}
}
// Gestiona la carga monocromática (time-based simplificado)
void LoadingScreen::updateMonoLoad(float delta_time) {
// Calcular progreso lineal (0.0 - 1.0)
float progress = state_time_ / LOADING_MONO_DURATION;
progress = std::min(progress, 1.0F);
// Calcular paso total actual (0-959)
const int TOTAL_STEPS = MONO_TOTAL_LINES * MONO_STEPS_PER_LINE; // 192 * 5 = 960
const int CURRENT_STEP = static_cast<int>(progress * TOTAL_STEPS);
// Verificar si ha completado todas las líneas
if (CURRENT_STEP >= TOTAL_STEPS) {
transitionToState(State::LOADING_COLOR);
return;
}
// Dibujar todos los pasos intermedios desde el último dibujado
const float TEXTURE_WIDTH = mono_loading_screen_surface_->getWidth();
const float CLIP_WIDTH = TEXTURE_WIDTH / MONO_STEPS_PER_LINE;
auto previous_renderer = Screen::get()->getRendererSurface();
Screen::get()->setRendererSurface(screen_surface_);
for (int step = last_mono_step_ + 1; step <= CURRENT_STEP; ++step) {
// Calcular línea y sub-paso para este paso
const int CURRENT_LINE = step / MONO_STEPS_PER_LINE; // 0-191
const int CURRENT_SUBSTEP = step % MONO_STEPS_PER_LINE; // 0-4
// Saltar si excede el total de líneas
if (CURRENT_LINE >= MONO_TOTAL_LINES) {
break;
}
// Calcular rectángulo de clip para este paso
const float CLIP_X = CURRENT_SUBSTEP * CLIP_WIDTH;
load_rect_.x = CLIP_X;
load_rect_.y = static_cast<float>(line_index_[CURRENT_LINE]);
load_rect_.w = CLIP_WIDTH;
load_rect_.h = 1.0F;
// Configurar y dibujar sobre screen_surface_
mono_loading_screen_sprite_->setClip(load_rect_);
mono_loading_screen_sprite_->setPosition(load_rect_);
mono_loading_screen_sprite_->render();
}
Screen::get()->setRendererSurface(previous_renderer);
// Actualizar el último paso dibujado
last_mono_step_ = CURRENT_STEP;
}
// Gestiona la carga en color
void LoadingScreen::updateColorLoad(float delta_time) {
// Calcular progreso lineal (0.0 - 1.0)
float progress = state_time_ / LOADING_COLOR_DURATION;
progress = std::min(progress, 1.0F);
// Calcular bloque actual (0-767) - ahora pinta de 1 en 1 en lugar de 2 en 2
const int CURRENT_BLOCK = static_cast<int>(progress * COLOR_TOTAL_BLOCKS);
// Verificar si ha completado todos los bloques
if (CURRENT_BLOCK >= COLOR_TOTAL_BLOCKS) {
transitionToState(State::DATA2);
return;
}
// Dibujar todos los bloques intermedios desde el último dibujado
auto previous_renderer = Screen::get()->getRendererSurface();
Screen::get()->setRendererSurface(screen_surface_);
// Iterar desde el último bloque + 1 hasta el bloque actual (de 1 en 1)
for (int block = last_color_block_ + 1; block <= CURRENT_BLOCK; ++block) {
// Saltar si excede el total de bloques
if (block >= COLOR_TOTAL_BLOCKS) {
break;
}
// Calcular posición del bloque
load_rect_.x = static_cast<float>((block * COLOR_BLOCK_SPACING) % 256);
load_rect_.y = static_cast<float>((block / COLOR_BLOCKS_PER_ROW) * COLOR_BLOCK_SPACING);
load_rect_.w = static_cast<float>(COLOR_BLOCK_WIDTH);
load_rect_.h = static_cast<float>(COLOR_BLOCK_HEIGHT);
// Configurar y dibujar sobre screen_surface_
color_loading_screen_sprite_->setClip(load_rect_);
color_loading_screen_sprite_->setPosition(load_rect_);
color_loading_screen_sprite_->render();
}
Screen::get()->setRendererSurface(previous_renderer);
// Actualizar el último bloque dibujado
last_color_block_ = CURRENT_BLOCK;
}
// Dibuja el efecto de carga amarillo y azul en el borde
void LoadingScreen::renderDataBorder() {
// Obtiene la Surface del borde
auto border = Screen::get()->getBorderSurface();
// Pinta el borde de color azul
border->clear(static_cast<Uint8>(PaletteColor::BLUE));
// Añade lineas amarillas
const auto COLOR = static_cast<Uint8>(PaletteColor::YELLOW);
const int WIDTH = Options::game.width + (Options::video.border.width * 2);
const int HEIGHT = Options::game.height + (Options::video.border.height * 2);
bool draw_enabled = rand() % 2 == 0;
int row = 0;
while (row < HEIGHT) {
const int ROW_HEIGHT = (rand() % 4) + 3;
if (draw_enabled) {
for (int i = row; i < row + ROW_HEIGHT; ++i) {
border->drawLine(0, i, WIDTH, i, COLOR);
}
}
row += ROW_HEIGHT;
draw_enabled = !draw_enabled;
}
}
// Dibuja el efecto de carga rojo y azul en el borde
void LoadingScreen::renderHeaderBorder() const {
// Obtiene la Surface del borde
auto border = Screen::get()->getBorderSurface();
// Pinta el borde de color azul o rojo
border->clear(carrier_.toggle ? static_cast<Uint8>(PaletteColor::CYAN) : static_cast<Uint8>(PaletteColor::RED));
// Añade lineas rojas o azules
const auto COLOR = carrier_.toggle ? static_cast<Uint8>(PaletteColor::RED) : static_cast<Uint8>(PaletteColor::CYAN);
const int WIDTH = Options::game.width + (Options::video.border.width * 2);
const int HEIGHT = Options::game.height + (Options::video.border.height * 2);
// Primera linea (con el color y tamaño de la portadora)
int row = 0;
const int FIRST_ROW_HEIGHT = static_cast<int>(carrier_.offset);
for (int i = row; i < row + FIRST_ROW_HEIGHT; ++i) {
border->drawLine(0, i, WIDTH, i, COLOR);
}
row += FIRST_ROW_HEIGHT;
// Resto de lineas (siguen a la portadora)
bool draw_enabled = false;
while (row < HEIGHT) {
if (draw_enabled) {
for (int i = row; i < row + HEADER_DATAROW_HEIGHT; ++i) {
border->drawLine(0, i, WIDTH, i, COLOR);
}
}
row += HEADER_DATAROW_HEIGHT;
draw_enabled = !draw_enabled;
}
}
// Dibuja el borde de color
void LoadingScreen::renderColoredBorder(PaletteColor color) {
// Obtiene la Surface del borde
auto border = Screen::get()->getBorderSurface();
// Pinta el borde de color azul
border->clear(static_cast<Uint8>(color));
}
// Actualiza las variables
void LoadingScreen::update() {
const float DELTA_TIME = delta_timer_->tick();
handleEvents(); // Comprueba los eventos
handleInput(); // Comprueba las entradas
updateState(DELTA_TIME); // Actualiza el estado y gestiona transiciones
// Actualizar la carga según el estado actual
switch (state_) {
case State::DATA1:
case State::DATA2:
// Por ahora no hacen nada específico
break;
case State::SILENT1:
case State::SILENT2:
updateSilent(DELTA_TIME);
break;
case State::HEADER1:
case State::HEADER2:
updateCarrier(DELTA_TIME);
break;
case State::LOADING_MONO:
updateMonoLoad(DELTA_TIME);
break;
case State::LOADING_COLOR:
updateColorLoad(DELTA_TIME);
break;
case State::COMPLETE:
// No hay más actualizaciones
break;
}
Audio::update(); // Actualiza el objeto Audio
Screen::get()->update(DELTA_TIME); // Actualiza el objeto Screen
}
// Dibuja en pantalla
void LoadingScreen::render() {
// Pinta el borde
renderBorder();
// Prepara para empezar a dibujar en la textura de juego
Screen::get()->start();
Screen::get()->clearSurface(stringToColor("white"));
// Copia la surface a la surface de Screen
screen_surface_->render(0, 0);
// Vuelca el contenido del renderizador en pantalla
Screen::get()->render();
}
// Bucle para el logo del juego
void LoadingScreen::run() {
// Ajusta el volumen
Audio::get()->setMusicVolume(50);
// Limpia la pantalla
Screen::get()->start();
Screen::get()->clearRenderer();
Screen::get()->render();
while (SceneManager::current == SceneManager::Scene::LOADING_SCREEN) {
update();
render();
}
Audio::get()->setMusicVolume(100);
}
// Pinta el borde
void LoadingScreen::renderBorder() {
if (Options::video.border.enabled) {
// Dibuja el efecto de carga en el borde según el tipo actual
switch (current_border_type_) {
case Border::YELLOW_AND_BLUE:
renderDataBorder();
break;
case Border::RED_AND_CYAN:
renderHeaderBorder();
break;
case Border::WHITE:
renderColoredBorder(PaletteColor::WHITE);
break;
case Border::BLACK:
renderColoredBorder(PaletteColor::BLACK);
break;
case Border::RED:
renderColoredBorder(PaletteColor::RED);
break;
case Border::CYAN:
renderColoredBorder(PaletteColor::CYAN);
break;
case Border::NONE:
// No renderizar borde
break;
}
}
}
// Escribe el nombre del programa
void LoadingScreen::printProgramName() { // NOLINT(readability-convert-member-functions-to-static)
auto previous_renderer = Screen::get()->getRendererSurface();
Screen::get()->setRendererSurface(screen_surface_);
program_sprite_->render();
Screen::get()->setRendererSurface(previous_renderer);
}
// Actualiza la portadora
void LoadingScreen::updateCarrier(float delta_time) {
constexpr float CARRIER_BASE_SPEED = -250.0F;
constexpr float CARRIER_HEIGHT = HEADER_DATAROW_HEIGHT;
// Oscilación compuesta: mezcla de dos frecuencias para evitar patrón predecible
const float MODULATION = std::sin(carrier_.total_time * 1.2F) * std::sin((carrier_.total_time * 0.35F) + 1.0F);
const float SPEED = CARRIER_BASE_SPEED * (0.5F + (0.5F * MODULATION)); // rango [-200, 0]
carrier_.offset += SPEED * delta_time;
if (carrier_.offset < 0.0F) {
carrier_.offset += CARRIER_HEIGHT; // reinicia al rango [0,HEADER_DATAROW_HEIGHT]
carrier_.toggle = !carrier_.toggle;
}
carrier_.total_time += delta_time;
}
// Actualiza el ruido durante el tiempo de silencio
void LoadingScreen::updateSilent(float delta_time) {
constexpr float NOISE_THRESHOLD = 0.35F;
// Oscilación compuesta para simular picos de ruido
const float MODULATION = std::sin(noise_.total_time * 4.2F) * std::sin((noise_.total_time * 1.7F) + 0.5F);
noise_.value = std::fabs(MODULATION); // rango [0.0, 1.0]
// Detecta cruce de umbral solo si venía de abajo
if (noise_.value > NOISE_THRESHOLD && !noise_.crossed) {
noise_.crossed = true;
current_border_type_ = (current_border_type_ == Border::RED) ? Border::CYAN : Border::RED;
}
// Restablece el flag cuando baja del umbral
if (noise_.value < NOISE_THRESHOLD) {
noise_.crossed = false;
}
noise_.total_time += delta_time;
}

View File

@@ -0,0 +1,123 @@
#pragma once
#include <SDL3/SDL.h>
#include <array> // Para std::array
#include <memory> // Para shared_ptr
#include "utils/delta_timer.hpp" // Para DeltaTimer
#include "utils/utils.hpp" // Para PaletteColor
class Sprite; // Forward declaration
class Surface; // Forward declaration
class LoadingScreen {
public:
// --- Constructor y Destructor ---
LoadingScreen();
~LoadingScreen();
// --- Bucle principal ---
void run();
private:
// --- Enumeraciones ---
// Estados de la secuencia de carga
enum class State {
SILENT1, // Pausa inicial antes de empezar
HEADER1, // Cabecera
DATA1, // Datos
SILENT2, // Segunda pausa
HEADER2, // Cabecera pantalla
LOADING_MONO, // Carga de pantalla monocromática (escaneo de líneas)
LOADING_COLOR, // Carga de pantalla en color (bloques)
DATA2, // Datos
COMPLETE // Carga completa
};
// Tipos de borde para la pantalla de carga
enum class Border {
NONE,
YELLOW_AND_BLUE,
RED_AND_CYAN,
WHITE,
BLACK,
RED,
CYAN
};
// --- Estructuras ---
struct Carrier {
float offset{0.0F}; // Offset para la carga de cabeceras
bool toggle{false}; // Para cambiar el color inicial
float total_time{0.0F}; // Tiempo acumulado para modulación de velocidad
};
struct Noise {
float value{0.0F}; // Nivel actual de ruido (0.0 a 1.0)
float total_time{0.0F}; // Tiempo acumulado para modulación
bool crossed{false}; // Flag para detectar cruce de umbral
};
// --- Constantes de tiempo (en segundos) ---
static constexpr float SILENT1_DURATION = 2.0F; // Pausa inicial
static constexpr float HEADER1_DURATION = 4.0F; // Cabecera
static constexpr float DATA1_DURATION = 0.18F; // Datos
static constexpr float SILENT2_DURATION = 1.6F; // Segunda pausa
static constexpr float HEADER2_DURATION = 2.0F; // Cabecera pantalla
static constexpr float LOADING_MONO_DURATION = 16.0F; // Duración total de la carga monocromática
static constexpr float LOADING_COLOR_DURATION = 4.0F; // Duración total de la carga en color
static constexpr float DATA2_DURATION = 5.0F; // Datos
// --- Constantes de geometría ---
static constexpr int MONO_TOTAL_LINES = 192; // Total de líneas en carga monocromática
static constexpr int MONO_STEPS_PER_LINE = 5; // Pasos de animación por línea
static constexpr int COLOR_TOTAL_BLOCKS = 768; // Total de bloques en carga color
static constexpr int COLOR_BLOCK_WIDTH = 16; // Ancho del bloque de color
static constexpr int COLOR_BLOCK_HEIGHT = 8; // Alto del bloque de color
static constexpr int COLOR_BLOCKS_PER_ROW = 32; // Bloques por fila (256 / 8)
static constexpr int COLOR_BLOCK_SPACING = 8; // Espaciado entre bloques
static constexpr int HEADER_DATAROW_HEIGHT = 9.0F; // Alto de las barras del borde de la carga de las cabeceras
// --- Métodos ---
void update(); // Actualiza las variables
void render(); // Dibuja en pantalla
static void handleEvents(); // Comprueba el manejador de eventos
static void handleInput(); // Comprueba las entradas
void updateState(float delta_time); // Actualiza el estado actual
void transitionToState(State new_state); // Transiciona a un nuevo estado
void updateMonoLoad(float delta_time); // Gestiona la carga monocromática (time-based)
void updateColorLoad(float delta_time); // Gestiona la carga en color (time-based)
void renderBorder(); // Pinta el borde
static void renderDataBorder(); // Dibuja el efecto de carga amarillo y azul en el borde
void renderHeaderBorder() const; // Dibuja el efecto de carga rojo y azul en el borde
static void renderColoredBorder(PaletteColor color); // Dibuja el borde de color
void initLineIndexArray(); // Inicializa el array de índices de líneas
void printProgramName(); // Escribe el nombre del programa
void updateCarrier(float delta_time); // Actualiza la portadora
void updateSilent(float delta_time); // Actualiza el ruido durante el tiempo de silencio
// --- Variables miembro ---
// Objetos y punteros a recursos
std::shared_ptr<Surface> mono_loading_screen_surface_; // Surface con la pantalla de carga en blanco y negro
std::shared_ptr<Surface> color_loading_screen_surface_; // Surface con la pantalla de carga en color
std::unique_ptr<Sprite> mono_loading_screen_sprite_; // SurfaceSprite para manejar la textura mono_loading_screen_surface_
std::unique_ptr<Sprite> color_loading_screen_sprite_; // SurfaceSprite para manejar la textura color_loading_screen_surface_
std::unique_ptr<Sprite> program_sprite_; // SurfaceSprite para manejar la textura con el nombre del programa
std::shared_ptr<Surface> screen_surface_; // Surface para dibujar la pantalla de carga
std::unique_ptr<DeltaTimer> delta_timer_; // Timer para delta time
// Variables de estado de la secuencia
State state_{State::SILENT1}; // Estado actual de la secuencia
float state_time_{0.0F}; // Tiempo acumulado en el estado actual
Border current_border_type_{Border::NONE}; // Tipo de borde actual
// Arrays y estructuras auxiliares
std::array<int, MONO_TOTAL_LINES> line_index_; // El orden en el que se procesan las 192 líneas de la pantalla de carga
SDL_FRect load_rect_{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 1.0F}; // Rectángulo para dibujar la pantalla de carga
Carrier carrier_; // Estructura para los efectos de la carga de cabeceras
Noise noise_; // Variaciones de ruido durante los silencios
// Variables de seguimiento para evitar saltos de pasos/bloques
int last_mono_step_{-1}; // Último paso mono dibujado
int last_color_block_{-1}; // Último bloque color dibujado
};

290
source/game/scenes/logo.cpp Normal file
View File

@@ -0,0 +1,290 @@
#include "game/scenes/logo.hpp"
#include <SDL3/SDL.h>
#include <algorithm> // Para std::clamp
#include <array> // Para std::array
#include <random> // Para generador aleatorio
#include "core/audio/audio.hpp" // Para Audio
#include "core/input/global_inputs.hpp" // Para check
#include "core/input/input.hpp" // Para Input
#include "core/rendering/screen.hpp" // Para Screen
#include "core/rendering/sprite/sprite.hpp" // Para SSprite
#include "core/rendering/surface.hpp" // Para Surface
#include "core/resources/resource_cache.hpp" // Para Resource
#include "core/system/global_events.hpp" // Para check
#include "game/options.hpp" // Para Options, SectionState, options, Section
#include "game/scene_manager.hpp" // Para SceneManager
#include "utils/delta_timer.hpp" // Para DeltaTimer
#include "utils/easing_functions.hpp" // Para funciones de suavizado
#include "utils/utils.hpp" // Para PaletteColor
// Constructor
Logo::Logo()
: jailgames_surface_(Resource::Cache::get()->getSurface("jailgames.gif")),
since_1998_surface_(Resource::Cache::get()->getSurface("since_1998.gif")),
since_1998_sprite_(std::make_shared<Sprite>(since_1998_surface_, (256 - since_1998_surface_->getWidth()) / 2, 83 + jailgames_surface_->getHeight() + 5, since_1998_surface_->getWidth(), since_1998_surface_->getHeight())),
delta_timer_(std::make_unique<DeltaTimer>()) {
// Configura variables
since_1998_sprite_->setClip(0, 0, since_1998_surface_->getWidth(), since_1998_surface_->getHeight());
since_1998_color_ = static_cast<Uint8>(PaletteColor::BLACK);
jailgames_color_ = static_cast<Uint8>(PaletteColor::BRIGHT_WHITE);
// Inicializa variables
SceneManager::current = SceneManager::Scene::LOGO;
initSprites(); // Crea los sprites de cada linea
initColors(); // Inicializa el vector de colores
// Seleccionar función de easing aleatoria para la animación del logo
// Usamos lambdas para funciones con parámetros opcionales
static const std::array<EasingFunction, 4> EASING_OPTIONS = {
[](float t) -> float { return Easing::backOut(t); }, // Overshoot retro
[](float t) -> float { return Easing::elasticOut(t); }, // Rebote múltiple con oscilación
Easing::bounceOut, // Rebote físico decreciente
Easing::cubicOut // Suavizado sin overshoot (para variedad)
};
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<size_t> dist(0, EASING_OPTIONS.size() - 1);
easing_function_ = EASING_OPTIONS[dist(gen)];
// Cambia el color del borde
Screen::get()->setBorderColor(static_cast<Uint8>(PaletteColor::BLACK));
}
// Comprueba el manejador de eventos
void Logo::handleEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event);
}
}
// Comprueba las entradas
void Logo::handleInput() {
Input::get()->update();
GlobalInputs::handle();
}
// Gestiona el logo de JAILGAME
void Logo::updateJAILGAMES(float delta_time) {
// Solo actualizar durante el estado JAILGAMES_SLIDE_IN
if (state_ != State::JAILGAMES_SLIDE_IN) {
return;
}
// Calcular el progreso de la animación (0.0 a 1.0)
const float PROGRESS = std::clamp(state_time_ / JAILGAMES_SLIDE_DURATION, 0.0F, 1.0F);
// Aplicar función de suavizado seleccionada aleatoriamente (permite overshoot para efecto de rebote)
// La posición final exacta se garantiza en updateState() antes de transicionar
const float EASED_PROGRESS = easing_function_(PROGRESS);
// Actualizar cada línea del sprite JAILGAMES interpolando con easing
for (size_t i = 0; i < jailgames_sprite_.size(); ++i) {
// Interpolar entre posición inicial y destino usando el progreso suavizado
const auto INITIAL_X = static_cast<float>(jailgames_initial_x_[i]);
const auto DEST_X = static_cast<float>(JAILGAMES_DEST_X);
const float NEW_X = INITIAL_X + ((DEST_X - INITIAL_X) * EASED_PROGRESS);
jailgames_sprite_[i]->setX(NEW_X);
}
}
// Calcula el índice de color según el progreso (0.0-1.0)
auto Logo::getColorIndex(float progress) const -> int { // NOLINT(readability-convert-member-functions-to-static)
// Asegurar que progress esté en el rango [0.0, 1.0]
progress = std::clamp(progress, 0.0F, 1.0F);
// Mapear el progreso al índice de color (0-7)
const int MAX_INDEX = static_cast<int>(color_.size()) - 1;
const int INDEX = static_cast<int>(progress * MAX_INDEX);
return INDEX;
}
// Gestiona el color de las texturas
void Logo::updateTextureColors() {
switch (state_) {
case State::SINCE_1998_FADE_IN: {
// Fade-in de "Since 1998" de negro a blanco
const float PROGRESS = state_time_ / SINCE_1998_FADE_DURATION;
since_1998_color_ = color_[getColorIndex(PROGRESS)];
break;
}
case State::DISPLAY: {
// Asegurar que ambos logos estén en blanco durante el display
jailgames_color_ = color_.back(); // BRIGHT_WHITE
since_1998_color_ = color_.back(); // BRIGHT_WHITE
break;
}
case State::FADE_OUT: {
// Fade-out de ambos logos de blanco a negro
const float PROGRESS = 1.0F - (state_time_ / FADE_OUT_DURATION);
const int COLOR_INDEX = getColorIndex(PROGRESS);
jailgames_color_ = color_[COLOR_INDEX];
since_1998_color_ = color_[COLOR_INDEX];
break;
}
default:
// En otros estados, mantener los colores actuales
break;
}
}
// Transiciona a un nuevo estado
void Logo::transitionToState(State new_state) {
state_ = new_state;
state_time_ = 0.0F;
}
// Actualiza el estado actual
void Logo::updateState(float delta_time) {
state_time_ += delta_time;
// Gestionar transiciones entre estados basándose en el tiempo
switch (state_) {
case State::INITIAL:
if (state_time_ >= INITIAL_DELAY) {
transitionToState(State::JAILGAMES_SLIDE_IN);
}
break;
case State::JAILGAMES_SLIDE_IN:
if (state_time_ >= JAILGAMES_SLIDE_DURATION) {
// Garantizar que todas las líneas estén exactamente en la posición final
// antes de transicionar (previene race condition con updateJAILGAMES)
for (auto& sprite : jailgames_sprite_) {
sprite->setX(JAILGAMES_DEST_X);
}
transitionToState(State::SINCE_1998_FADE_IN);
}
break;
case State::SINCE_1998_FADE_IN:
if (state_time_ >= SINCE_1998_FADE_DURATION) {
transitionToState(State::DISPLAY);
}
break;
case State::DISPLAY:
if (state_time_ >= DISPLAY_DURATION) {
transitionToState(State::FADE_OUT);
}
break;
case State::FADE_OUT:
if (state_time_ >= FADE_OUT_DURATION) {
transitionToState(State::BLACK_SCREEN);
}
break;
case State::BLACK_SCREEN:
if (state_time_ >= BLACK_SCREEN_DURATION) {
transitionToState(State::END);
endSection();
}
break;
case State::END:
// Estado final, no hacer nada
break;
}
}
// Actualiza las variables
void Logo::update() {
const float DELTA_TIME = delta_timer_->tick();
handleEvents(); // Comprueba los eventos
handleInput(); // Comprueba las entradas
updateState(DELTA_TIME); // Actualiza el estado y gestiona transiciones
updateJAILGAMES(DELTA_TIME); // Gestiona el logo de JAILGAME
updateTextureColors(); // Gestiona el color de las texturas
Audio::update(); // Actualiza el objeto Audio
Screen::get()->update(DELTA_TIME); // Actualiza el objeto Screen
}
// Dibuja en pantalla
void Logo::render() { // NOLINT(readability-convert-member-functions-to-static)
// Prepara para empezar a dibujar en la textura de juego
Screen::get()->start();
Screen::get()->clearSurface(static_cast<Uint8>(PaletteColor::BLACK));
// Dibuja los objetos
for (const auto& sprite : jailgames_sprite_) {
sprite->render(1, jailgames_color_);
}
since_1998_sprite_->render(1, since_1998_color_);
// Vuelca el contenido del renderizador en pantalla
Screen::get()->render();
}
// Bucle para el logo del juego
void Logo::run() {
while (SceneManager::current == SceneManager::Scene::LOGO) {
update();
render();
}
}
// Termina la sección
void Logo::endSection() {
switch (SceneManager::options) {
case SceneManager::Options::LOGO_TO_TITLE:
SceneManager::current = SceneManager::Scene::TITLE;
break;
case SceneManager::Options::LOGO_TO_LOADING_SCREEN:
SceneManager::current = SceneManager::Scene::LOADING_SCREEN;
break;
default:
SceneManager::current = SceneManager::Scene::LOADING_SCREEN;
break;
}
}
// Inicializa el vector de colores
void Logo::initColors() { // NOLINT(readability-convert-member-functions-to-static)
// Inicializa el vector de colores
const std::vector<Uint8> COLORS = {
static_cast<Uint8>(PaletteColor::BLACK),
static_cast<Uint8>(PaletteColor::BLUE),
static_cast<Uint8>(PaletteColor::RED),
static_cast<Uint8>(PaletteColor::MAGENTA),
static_cast<Uint8>(PaletteColor::GREEN),
static_cast<Uint8>(PaletteColor::CYAN),
static_cast<Uint8>(PaletteColor::YELLOW),
static_cast<Uint8>(PaletteColor::BRIGHT_WHITE)};
for (const auto& color : COLORS) {
color_.push_back(color);
}
}
// Crea los sprites de cada linea
void Logo::initSprites() { // NOLINT(readability-convert-member-functions-to-static)
const float WIDTH = jailgames_surface_->getWidth();
jailgames_initial_x_.reserve(jailgames_surface_->getHeight());
for (int i = 0; i < jailgames_surface_->getHeight(); ++i) {
jailgames_sprite_.push_back(std::make_shared<Sprite>(jailgames_surface_, 0, i, jailgames_surface_->getWidth(), 1));
jailgames_sprite_.back()->setClip(0, i, jailgames_surface_->getWidth(), 1);
// Calcular posición inicial (alternando entre derecha e izquierda)
constexpr int LINE_OFFSET = 6;
const int INITIAL_X = (i % 2 == 0) ? (256 + (i * LINE_OFFSET)) : (static_cast<int>(-WIDTH) - (i * LINE_OFFSET));
jailgames_initial_x_.push_back(INITIAL_X);
jailgames_sprite_.at(i)->setX(INITIAL_X);
jailgames_sprite_.at(i)->setY(83 + i);
}
}

View File

@@ -0,0 +1,80 @@
#pragma once
#include <SDL3/SDL.h>
#include <functional> // Para std::function
#include <memory> // Para shared_ptr
#include <vector> // Para vector
#include "utils/delta_timer.hpp" // Para DeltaTimer
class Sprite; // Forward declaration
class Surface; // Forward declaration
class Logo {
public:
// --- Tipos ---
using EasingFunction = std::function<float(float)>; // Función de easing (permite lambdas)
// --- Enumeraciones ---
enum class State {
INITIAL, // Espera inicial
JAILGAMES_SLIDE_IN, // Las líneas de JAILGAMES se deslizan hacia el centro
SINCE_1998_FADE_IN, // Aparición gradual del texto "Since 1998"
DISPLAY, // Logo completo visible
FADE_OUT, // Desaparición gradual
BLACK_SCREEN, // Pantalla en negro antes de terminar
END // Fin de la secuencia
};
// --- Constructor y Destructor ---
Logo();
~Logo() = default;
// --- Bucle principal ---
void run();
private:
// --- Constantes de tiempo (en segundos) ---
static constexpr float INITIAL_DELAY = 0.5F; // Tiempo antes de que empiece la animación
static constexpr float SINCE_1998_FADE_DURATION = 0.5F; // Duración del fade-in de "Since 1998"
static constexpr float DISPLAY_DURATION = 3.5F; // Tiempo que el logo permanece visible
static constexpr float FADE_OUT_DURATION = 0.5F; // Duración del fade-out final
static constexpr float BLACK_SCREEN_DURATION = 1.0F; // Duración de la pantalla negra final
// --- Constantes de animación ---
static constexpr float JAILGAMES_SLIDE_DURATION = 0.8F; // Duración de la animación de slide-in (segundos)
static constexpr int JAILGAMES_DEST_X = 37; // Posición X de destino para JAILGAMES
// --- Métodos ---
void update(); // Actualiza las variables
void render(); // Dibuja en pantalla
static void handleEvents(); // Comprueba el manejador de eventos
static void handleInput(); // Comprueba las entradas
void updateJAILGAMES(float delta_time); // Gestiona el logo de JAILGAME (time-based)
void updateTextureColors(); // Gestiona el color de las texturas
void updateState(float delta_time); // Actualiza el estado actual
void transitionToState(State new_state); // Transiciona a un nuevo estado
[[nodiscard]] auto getColorIndex(float progress) const -> int; // Calcula el índice de color según el progreso (0.0-1.0)
static void endSection(); // Termina la sección
void initColors(); // Inicializa el vector de colores
void initSprites(); // Crea los sprites de cada linea
// --- Variables miembro ---
// Objetos y punteros a recursos
std::shared_ptr<Surface> jailgames_surface_; // Textura con los graficos "JAILGAMES"
std::shared_ptr<Surface> since_1998_surface_; // Textura con los graficos "Since 1998"
std::vector<std::shared_ptr<Sprite>> jailgames_sprite_; // Vector con los sprites de cada linea que forman el bitmap JAILGAMES
std::vector<int> jailgames_initial_x_; // Posiciones X iniciales de cada línea (para interpolación con easing)
std::shared_ptr<Sprite> since_1998_sprite_; // SSprite para manejar la textura2
std::unique_ptr<DeltaTimer> delta_timer_; // Timer para delta time
// Variables de estado de colores
std::vector<Uint8> color_; // Vector con los colores para el fade
Uint8 jailgames_color_{0}; // Color para el sprite de "JAILGAMES"
Uint8 since_1998_color_{0}; // Color para el sprite de "Since 1998"
// Variables de estado de la secuencia
State state_{State::INITIAL}; // Estado actual de la secuencia
float state_time_{0.0F}; // Tiempo acumulado en el estado actual
EasingFunction easing_function_; // Función de easing para la animación del logo
};

View File

@@ -0,0 +1,876 @@
#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/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 "game/ui/console.hpp" // Para Console
#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<Sprite>(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<Sprite>(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("574071_EA_DTV.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 && !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;
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<Sprite>(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";
}

View File

@@ -0,0 +1,134 @@
#pragma once
#include <SDL3/SDL.h>
#include <array> // Para std::array
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include "game/scene_manager.hpp" // Para SceneManager::Scene
#include "utils/delta_timer.hpp" // Para DeltaTimer
class Sprite; // Forward declaration
class Surface; // Forward declaration
class Text; // Forward declaration
class Title {
public:
// --- Constructor y Destructor ---
Title();
~Title();
// --- Bucle principal ---
void run();
private:
// --- Estructuras y enumeraciones ---
struct Glyph {
uint32_t codepoint{0}; // Codepoint Unicode del carácter
SDL_FRect clip{}; // Clip rect pre-calculado en el bitmap de fuente
float x{0.0F}; // Posición en el eje x (float para precisión con delta time)
float width{0.0F}; // Ancho pre-calculado del carácter
bool enabled{false}; // Solo se escriben y mueven si estan habilitadas
};
enum class State {
SHOW_LOADING_SCREEN,
FADE_LOADING_SCREEN,
MAIN_MENU,
CHEEVOS_MENU,
FADE_MENU,
POST_FADE_MENU,
};
// --- Constantes de tiempo (en segundos) ---
static constexpr float SHOW_LOADING_DURATION = 5.0F; // Tiempo mostrando loading screen (antes 500 frames)
static constexpr float FADE_STEP_INTERVAL = 0.05F; // Intervalo entre pasos de fade (antes cada 4 frames)
static constexpr float POST_FADE_DELAY = 1.0F; // Delay después del fade (pantalla en negro)
static constexpr float MAIN_MENU_IDLE_TIMEOUT = 20.0F; // Timeout para ir a créditos (antes 2200 frames)
static constexpr float KEYBOARD_REMAP_DISPLAY_DELAY = 2.0F; // Tiempo mostrando teclas definidas antes de guardar
static constexpr float MARQUEE_SPEED = 100.0F; // Velocidad de marquesina (pixels/segundo)
static constexpr float CHEEVOS_SCROLL_MAX_SPEED = 180.0F; // Velocidad máxima de scroll de logros (pixels/segundo)
static constexpr float CHEEVOS_SCROLL_ACCELERATION = 600.0F; // Aceleración del scroll (pixels/segundo²)
static constexpr float CHEEVOS_SCROLL_DECELERATION = 800.0F; // Desaceleración del scroll (pixels/segundo²)
// --- Constantes de marquesina ---
static constexpr float MARQUEE_START_X = 256.0F; // Posición inicial (ancho pantalla)
static constexpr float MARQUEE_EXIT_X = -10.0F; // Cuando desaparece de pantalla
static constexpr float MARQUEE_Y = 184.0F; // Posición Y
static constexpr float MARQUEE_LETTER_SPACING = 1.0F; // Espaciado entre letras
// --- Métodos ---
void update(); // Actualiza las variables
void render(); // Dibuja en pantalla
void handleEvents(); // Comprueba el manejador de eventos
void handleMainMenuKeyPress(SDL_Keycode key); // Maneja las teclas del menu principal
void handleInput(float delta_time); // Comprueba las entradas
void updateState(float delta_time); // Actualiza el estado actual
void transitionToState(State new_state); // Transiciona a un nuevo estado
void updateShowLoadingScreen(float delta_time); // Actualiza SHOW_LOADING_SCREEN
void updateFadeLoadingScreen(float delta_time); // Actualiza FADE_LOADING_SCREEN
void updateMainMenu(float delta_time); // Actualiza MAIN_MENU
void updateCheevosMenu(float delta_time); // Actualiza CHEEVOS_MENU
void updateFadeMenu(float delta_time); // Actualiza FADE_MENU
void updatePostFadeMenu(float delta_time); // Actualiza POST_FADE_MENU
void initMarquee(); // Inicializa la marquesina
void updateMarquee(float delta_time); // Actualiza la marquesina (time-based)
void renderMarquee() const; // Dibuja la marquesina
void renderGameLogo(); // Dibuja el logo con el titulo del juego
void renderMainMenu(); // Dibuja el menu principal
void renderCheevosMenu(); // Dibuja el menu de logros
void renderKeyboardRemap() const; // Dibuja la pantalla de redefinir teclado
void renderJoystickRemap() const; // Dibuja la pantalla de redefinir joystick
void handleKeyboardRemap(const SDL_Event& event); // Maneja la captura de teclas
void handleJoystickRemap(const SDL_Event& event); // Maneja la captura de botones del gamepad
static auto isKeyValid(SDL_Scancode scancode) -> bool; // Valida si una tecla es permitida
auto isKeyDuplicate(SDL_Scancode scancode, int current_step) -> bool; // Valida si una tecla esta duplicada
auto isButtonDuplicate(int button, int current_step) -> bool; // Valida si un boton esta duplicado
void applyKeyboardRemap(); // Aplica y guarda las teclas redefinidas
void applyJoystickRemap(); // Aplica y guarda los botones del gamepad redefinidos
static auto getActionName(int step) -> std::string; // Retorna el nombre de la accion (LEFT/RIGHT/JUMP)
static auto getButtonName(int button) -> std::string; // Retorna el nombre amigable del boton del gamepad
void createCheevosTexture(); // Crea y rellena la surface para mostrar los logros
void resetCheevosScroll(); // Resetea el scroll de la lista de logros
void fillTitleSurface(); // Dibuja los elementos en la surface
// --- Variables miembro ---
// Objetos y punteros
std::shared_ptr<Surface> game_logo_surface_; // Textura con los graficos
std::unique_ptr<Sprite> game_logo_sprite_; // SSprite para manejar la surface
std::shared_ptr<Surface> loading_screen_surface_; // Surface con los gráficos de la pantalla de carga
std::unique_ptr<Sprite> loading_screen_sprite_; // SSprite con los gráficos de la pantalla de carga
std::shared_ptr<Surface> cheevos_surface_; // Textura con la lista de logros
std::unique_ptr<Sprite> cheevos_sprite_; // SSprite para manejar la surface con la lista de logros
std::shared_ptr<Surface> title_surface_; // Surface donde se dibuja toda la clase
std::unique_ptr<DeltaTimer> delta_timer_; // Timer para delta time
std::shared_ptr<Text> marquee_text_; // Texto para marquesina
std::shared_ptr<Text> menu_text_; // Texto para los menus
// Variables de estado de marquesina
std::string long_text_; // Texto que aparece en la parte inferior del titulo
std::vector<Glyph> letters_; // Vector con las letras de la marquesina
int first_active_letter_{0}; // Primera letra activa (optimización)
int last_active_letter_{0}; // Última letra activa (optimización)
// Variables de estado del menú de logros
SDL_FRect cheevos_surface_view_; // Zona visible de la surface con el listado de logros
float cheevos_scroll_velocity_{0.0F}; // Velocidad actual del scroll de logros (pixels/segundo)
// Variables de estado general
State state_; // Estado en el que se encuentra el bucle principal
float state_time_{0.0F}; // Tiempo acumulado en el estado actual
float fade_accumulator_{0.0F}; // Acumulador para controlar el fade por tiempo
SceneManager::Scene exit_scene_{SceneManager::Scene::GAME}; // Escena de destino al salir del título
// Variables para redefinir controles
bool is_remapping_keyboard_{false}; // True si estamos redefiniendo teclado
bool is_remapping_joystick_{false}; // True si estamos redefiniendo joystick
int remap_step_{0}; // Paso actual en la redefinicion (0=LEFT, 1=RIGHT, 2=JUMP)
std::array<SDL_Scancode, 3> temp_keys_; // Almacenamiento temporal de teclas capturadas
std::array<int, 3> temp_buttons_; // Almacenamiento temporal de botones de gamepad capturados
std::string remap_error_message_; // Mensaje de error si la tecla/boton es invalido
float axis_cooldown_{0.0F}; // Cooldown para evitar múltiples capturas de ejes
bool remap_completed_{false}; // True cuando se completa el remap (mostrar antes de guardar)
};