reestructuració

This commit is contained in:
2026-04-14 13:26:22 +02:00
parent 4ac34b8583
commit 4429cd92c1
143 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,386 @@
#include "balloon.hpp"
#include <algorithm> // Para clamp
#include <array> // Para array
#include <cmath> // Para fabs
#include "animated_sprite.hpp" // Para AnimatedSprite
#include "audio.hpp" // Para Audio
#include "param.hpp" // Para Param, ParamBalloon, param
#include "sprite.hpp" // Para Sprite
#include "texture.hpp" // Para Texture
// Constructor
Balloon::Balloon(const Config& config)
: sprite_(std::make_unique<AnimatedSprite>(config.texture, config.animation)),
x_(config.x),
y_(config.y),
vx_(config.vel_x),
being_created_(config.creation_counter > 0),
invulnerable_(config.creation_counter > 0),
stopped_(config.creation_counter > 0),
creation_counter_(config.creation_counter),
creation_counter_ini_(config.creation_counter),
type_(config.type),
size_(config.size),
game_tempo_(config.game_tempo),
play_area_(config.play_area),
sound_(config.sound) {
switch (type_) {
case Type::BALLOON: {
vy_ = 0;
max_vy_ = 3.0F * 60.0F; // Convert from frames to seconds (180 pixels/s)
const int INDEX = static_cast<int>(size_);
gravity_ = param.balloon.settings.at(INDEX).grav;
default_vy_ = param.balloon.settings.at(INDEX).vel;
h_ = w_ = WIDTH.at(INDEX);
power_ = POWER.at(INDEX);
menace_ = MENACE.at(INDEX);
score_ = SCORE.at(INDEX);
sound_.bouncing_file = BOUNCING_SOUND.at(INDEX);
sound_.popping_file = POPPING_SOUND.at(INDEX);
break;
}
case Type::FLOATER: {
default_vy_ = max_vy_ = vy_ = std::fabs(vx_ * 2.0F);
gravity_ = 0.00F;
const int INDEX = static_cast<int>(size_);
h_ = w_ = WIDTH.at(INDEX);
power_ = POWER.at(INDEX);
menace_ = MENACE.at(INDEX);
score_ = SCORE.at(INDEX);
sound_.bouncing_file = BOUNCING_SOUND.at(INDEX);
sound_.popping_file = POPPING_SOUND.at(INDEX);
break;
}
case Type::POWERBALL: {
constexpr int INDEX = 3;
h_ = w_ = WIDTH.at(4);
sound_.bouncing_file = BOUNCING_SOUND.at(3);
sound_.popping_file = "power_ball_explosion.wav";
power_ = score_ = menace_ = 0;
vy_ = 0;
max_vy_ = 3.0F * 60.0F; // Convert from frames to seconds (180 pixels/s)
gravity_ = param.balloon.settings.at(INDEX).grav;
default_vy_ = param.balloon.settings.at(INDEX).vel;
sprite_->setRotate(config.creation_counter <= 0);
sprite_->setRotateAmount(vx_ > 0.0F ? 120.0 : -120.0); // Convert from 2 degrees/frame to 120 degrees/second
break;
}
default:
break;
}
// Configura el sprite
sprite_->setWidth(w_);
sprite_->setHeight(h_);
shiftSprite();
// Alinea el circulo de colisión con el objeto
collider_.r = w_ / 2;
shiftColliders();
// Establece la animación a usar
setAnimation();
// Si no se está creando (creation_counter = 0), asegurar estado activo
if (!being_created_) {
start();
setInvulnerable(false);
}
}
// Centra el globo en la posición X
void Balloon::alignTo(int x) {
const float MIN_X = play_area_.x;
const float MAX_X = play_area_.w - w_;
x_ = std::clamp(x - (w_ / 2), MIN_X, MAX_X);
}
// Pinta el globo en la pantalla
void Balloon::render() {
if (type_ == Type::POWERBALL) {
// Renderiza el fondo azul
{
auto sp = std::make_unique<Sprite>(sprite_->getTexture(), sprite_->getPosition());
sp->setSpriteClip(0, 0, WIDTH.at(4), WIDTH.at(4));
sp->render();
}
// Renderiza la estrella
if (!invulnerable_) {
SDL_FPoint p = {.x = 24.0F, .y = 24.0F};
sprite_->setRotatingCenter(p);
sprite_->render();
}
// Añade la máscara del borde y los reflejos
{
auto sp = std::make_unique<Sprite>(sprite_->getTexture(), sprite_->getPosition());
sp->setSpriteClip(WIDTH.at(4) * 2, 0, WIDTH.at(4), WIDTH.at(4));
sp->render();
}
} else {
// Renderizado para el resto de globos
if (isBeingCreated()) {
// Renderizado con transparencia
sprite_->getTexture()->setAlpha(255 - static_cast<int>(creation_counter_ * (255.0F / creation_counter_ini_)));
sprite_->render();
sprite_->getTexture()->setAlpha(255);
} else {
// Renderizado normal
sprite_->render();
}
}
}
// Actualiza la posición y estados del globo (time-based)
void Balloon::move(float delta_time) {
if (isStopped()) {
return;
}
handleHorizontalMovement(delta_time);
handleVerticalMovement(delta_time);
applyGravity(delta_time);
}
void Balloon::handleHorizontalMovement(float delta_time) {
// DeltaTime en segundos: velocidad (pixels/s) * tempo * tiempo (s)
x_ += vx_ * game_tempo_ * delta_time;
const int CLIP = 2;
const float MIN_X = play_area_.x - CLIP;
const float MAX_X = play_area_.x + play_area_.w - w_ + CLIP;
if (isOutOfHorizontalBounds(MIN_X, MAX_X)) {
handleHorizontalBounce(MIN_X, MAX_X);
}
}
void Balloon::handleVerticalMovement(float delta_time) {
// DeltaTime en segundos: velocidad (pixels/s) * tempo * tiempo (s)
y_ += vy_ * game_tempo_ * delta_time;
if (shouldCheckTopCollision()) {
handleTopCollision();
}
handleBottomCollision();
}
auto Balloon::isOutOfHorizontalBounds(float min_x, float max_x) const -> bool {
return x_ < min_x || x_ > max_x;
}
void Balloon::handleHorizontalBounce(float min_x, float max_x) {
playBouncingSound();
x_ = std::clamp(x_, min_x, max_x);
vx_ = -vx_;
if (type_ == Type::POWERBALL) {
sprite_->switchRotate();
} else {
enableBounceEffect();
}
}
auto Balloon::shouldCheckTopCollision() const -> bool {
// Colisión en la parte superior solo si el globo va de subida
return vy_ < 0;
}
void Balloon::handleTopCollision() {
const int MIN_Y = play_area_.y;
if (y_ < MIN_Y) {
playBouncingSound();
y_ = MIN_Y;
vy_ = -vy_;
enableBounceEffect();
}
}
void Balloon::handleBottomCollision() {
const int MAX_Y = play_area_.y + play_area_.h - h_;
if (y_ > MAX_Y) {
playBouncingSound();
y_ = MAX_Y;
vy_ = -default_vy_;
if (type_ != Type::POWERBALL) {
enableBounceEffect();
} else {
setInvulnerable(false);
}
}
}
void Balloon::applyGravity(float delta_time) {
// DeltaTime en segundos: aceleración (pixels/s²) * tempo * tiempo (s)
vy_ += gravity_ * game_tempo_ * delta_time;
}
void Balloon::playBouncingSound() const {
if (sound_.enabled && sound_.bouncing_enabled) {
Audio::get()->playSound(sound_.bouncing_file);
}
}
void Balloon::playPoppingSound() const {
if (sound_.enabled && sound_.poping_enabled) {
Audio::get()->playSound(sound_.popping_file);
}
}
// Actualiza al globo a su posicion, animación y controla los contadores (time-based)
void Balloon::update(float delta_time) {
move(delta_time);
updateState(delta_time);
updateBounceEffect();
shiftSprite();
shiftColliders();
sprite_->update(delta_time);
// Contador interno con deltaTime en segundos
counter_ += delta_time;
}
// Actualiza los estados del globo (time-based)
void Balloon::updateState(float delta_time) {
// Si se está creando
if (isBeingCreated()) {
// Actualiza el valor de las variables
stop();
setInvulnerable(true);
if (creation_counter_ > 0) {
// Desplaza lentamente el globo hacia abajo y hacia un lado
// Cada 10/60 segundos (equivalente a 10 frames a 60fps)
movement_accumulator_ += delta_time;
constexpr float MOVEMENT_INTERVAL_S = 10.0F / 60.0F; // 10 frames = ~0.167s
if (movement_accumulator_ >= MOVEMENT_INTERVAL_S) {
movement_accumulator_ -= MOVEMENT_INTERVAL_S;
y_++;
x_ += vx_ / 60.0F; // Convierte de pixels/segundo a pixels/frame para movimiento discreto
// Comprueba no se salga por los laterales
const int MIN_X = play_area_.x;
const int MAX_X = play_area_.w - w_;
if (x_ < MIN_X || x_ > MAX_X) {
// Corrige y cambia el sentido de la velocidad
x_ -= vx_ / 60.0F;
vx_ = -vx_;
}
}
creation_counter_ -= delta_time;
creation_counter_ = std::max<float>(creation_counter_, 0);
}
else {
// El contador ha llegado a cero
being_created_ = false;
start();
setInvulnerable(false);
setAnimation();
}
}
}
// Establece la animación correspondiente al estado
void Balloon::setAnimation() {
std::string creating_animation;
std::string normal_animation;
switch (type_) {
case Type::POWERBALL:
creating_animation = "powerball";
normal_animation = "powerball";
break;
case Type::FLOATER:
creating_animation = param.balloon.color.at(2);
normal_animation = param.balloon.color.at(3);
break;
default:
creating_animation = param.balloon.color.at(0);
normal_animation = param.balloon.color.at(1);
break;
}
// Establece el frame de animación
std::string chosen_animation;
if (use_reversed_colors_) {
chosen_animation = creating_animation;
} else {
chosen_animation = isBeingCreated() ? creating_animation : normal_animation;
}
sprite_->setCurrentAnimation(chosen_animation);
}
// Detiene el globo
void Balloon::stop() {
stopped_ = true;
if (isPowerBall()) {
sprite_->setRotate(!stopped_);
}
}
// Pone el globo en movimiento
void Balloon::start() {
stopped_ = false;
if (isPowerBall()) {
sprite_->setRotate(!stopped_);
}
}
// Alinea el circulo de colisión con la posición del objeto globo
void Balloon::shiftColliders() {
collider_.x = static_cast<int>(x_) + collider_.r;
collider_.y = static_cast<int>(y_) + collider_.r;
}
// Alinea el sprite con la posición del objeto globo
void Balloon::shiftSprite() {
sprite_->setPosX(x_);
sprite_->setPosY(y_);
}
void Balloon::enableBounceEffect() {
bounce_effect_.enable(sprite_.get(), size_);
}
void Balloon::disableBounceEffect() {
bounce_effect_.disable(sprite_.get());
}
void Balloon::updateBounceEffect() {
bounce_effect_.update(sprite_.get());
}
// Pone el color alternativo en el globo
void Balloon::useReverseColor() {
if (!isBeingCreated()) {
use_reversed_colors_ = true;
setAnimation();
}
}
// Pone el color normal en el globo
void Balloon::useNormalColor() {
use_reversed_colors_ = false;
setAnimation();
}
// Explota el globo
void Balloon::pop(bool should_sound) {
if (should_sound) { playPoppingSound(); }
enabled_ = false;
}

View File

@@ -0,0 +1,305 @@
#pragma once
#include <SDL3/SDL.h> // Para Uint8, Uint16, SDL_FRect, Uint32
#include <array> // Para array
#include <memory> // Para allocator, shared_ptr, unique_ptr
#include <string> // Para basic_string, string
#include <string_view> // Para string_view
#include <vector> // Para vector
#include "animated_sprite.hpp" // Para AnimatedSprite
#include "utils.hpp" // Para Circle
class Texture;
// --- Clase Balloon ---
class Balloon {
public:
// --- Constantes relacionadas con globos ---
static constexpr int MAX_BOUNCE = 10; // Cantidad de elementos del vector de deformación
static constexpr std::array<int, 4> SCORE = {50, 100, 200, 400};
static constexpr std::array<int, 4> POWER = {1, 3, 7, 15};
static constexpr std::array<int, 4> MENACE = {1, 2, 4, 8};
static constexpr std::array<int, 5> WIDTH = {10, 16, 26, 48, 49};
static constexpr std::array<std::string_view, 4> BOUNCING_SOUND = {
"balloon_bounce0.wav",
"balloon_bounce1.wav",
"balloon_bounce2.wav",
"balloon_bounce3.wav"};
static constexpr std::array<std::string_view, 4> POPPING_SOUND = {
"balloon_pop0.wav",
"balloon_pop1.wav",
"balloon_pop2.wav",
"balloon_pop3.wav"};
// Velocidades horizontales en pixels/segundo (convertidas desde 0.7 pixels/frame a 60fps)
static constexpr float VELX_POSITIVE = 0.7F * 60.0F; // 42 pixels/segundo
static constexpr float VELX_NEGATIVE = -0.7F * 60.0F; // -42 pixels/segundo
// Multiplicadores de tempo del juego (sin cambios, son puros multiplicadores)
static constexpr std::array<float, 5> GAME_TEMPO = {0.60F, 0.70F, 0.80F, 0.90F, 1.00F};
static constexpr int POWERBALL_SCREENPOWER_MINIMUM = 10;
static constexpr int POWERBALL_COUNTER = 8;
// --- Enums ---
enum class Size : Uint8 {
SMALL = 0, // Tamaño pequeño
MEDIUM = 1, // Tamaño mediano
LARGE = 2, // Tamaño grande
EXTRALARGE = 3, // Tamaño extra grande
};
enum class Type : Uint8 {
BALLOON = 0, // Globo normal
FLOATER = 1, // Globo flotante
POWERBALL = 2, // Globo de poder
};
// --- Estructura para manejo de sonido ---
struct Sound {
std::string bouncing_file; // Archivo de sonido al rebotar
std::string popping_file; // Archivo de sonido al explotar
bool bouncing_enabled = false; // Si debe sonar el globo al rebotar
bool poping_enabled = true; // Si debe sonar el globo al explotar
bool enabled = true; // Indica si los globos deben hacer algun sonido
};
// --- Estructura de configuración para inicialización ---
struct Config {
float x = 0.0F;
float y = 0.0F;
Type type = Type::BALLOON;
Size size = Size::EXTRALARGE;
float vel_x = VELX_POSITIVE;
float game_tempo = GAME_TEMPO.at(0);
float creation_counter = 0.0F;
SDL_FRect play_area = {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
std::shared_ptr<Texture> texture = nullptr;
std::vector<std::string> animation;
Sound sound;
};
// --- Constructores y destructor ---
Balloon(const Config& config);
~Balloon() = default;
// --- Métodos principales ---
void alignTo(int x); // Centra el globo en la posición X
void render(); // Pinta el globo en la pantalla
void move(float delta_time); // Actualiza la posición y estados del globo (time-based)
void update(float delta_time); // Actualiza el globo (posición, animación, contadores) (time-based)
void stop(); // Detiene el globo
void start(); // Pone el globo en movimiento
void pop(bool should_sound = false); // Explota el globo
// --- Métodos de color ---
void useReverseColor(); // Pone el color alternativo en el globo
void useNormalColor(); // Pone el color normal en el globo
// --- Getters ---
[[nodiscard]] auto getPosX() const -> float { return x_; }
[[nodiscard]] auto getPosY() const -> float { return y_; }
[[nodiscard]] auto getWidth() const -> int { return w_; }
[[nodiscard]] auto getHeight() const -> int { return h_; }
[[nodiscard]] auto getSize() const -> Size { return size_; }
[[nodiscard]] auto getType() const -> Type { return type_; }
[[nodiscard]] auto getScore() const -> Uint16 { return score_; }
auto getCollider() -> Circle& { return collider_; }
[[nodiscard]] auto getMenace() const -> Uint8 { return isEnabled() ? menace_ : 0; }
[[nodiscard]] auto getPower() const -> Uint8 { return power_; }
[[nodiscard]] auto isStopped() const -> bool { return stopped_; }
[[nodiscard]] auto isPowerBall() const -> bool { return type_ == Type::POWERBALL; }
[[nodiscard]] auto isInvulnerable() const -> bool { return invulnerable_; }
[[nodiscard]] auto isBeingCreated() const -> bool { return being_created_; }
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
[[nodiscard]] auto isUsingReversedColor() const -> bool { return use_reversed_colors_; }
[[nodiscard]] auto canBePopped() const -> bool { return !isBeingCreated(); }
// --- Setters ---
void setVelY(float vel_y) { vy_ = vel_y; }
void setVelX(float vel_x) { vx_ = vel_x; }
void alterVelX(float percent) { vx_ *= percent; }
void setGameTempo(float tempo) { game_tempo_ = tempo; }
void setInvulnerable(bool value) { invulnerable_ = value; }
void setBouncingSound(bool value) { sound_.bouncing_enabled = value; }
void setPoppingSound(bool value) { sound_.poping_enabled = value; }
void setSound(bool value) { sound_.enabled = value; }
private:
// --- Estructura para el efecto de rebote ---
struct BounceEffect {
private:
static constexpr int BOUNCE_FRAMES = 10; // Cantidad de elementos del vector de deformación
// Tablas de valores predefinidos para el efecto de rebote
static constexpr std::array<float, BOUNCE_FRAMES> HORIZONTAL_ZOOM_VALUES = {
1.10F,
1.05F,
1.00F,
0.95F,
0.90F,
0.95F,
1.00F,
1.02F,
1.05F,
1.02F};
static constexpr std::array<float, BOUNCE_FRAMES> VERTICAL_ZOOM_VALUES = {
0.90F,
0.95F,
1.00F,
1.05F,
1.10F,
1.05F,
1.00F,
0.98F,
0.95F,
0.98F};
// Estado del efecto
bool enabled_ = false; // Si el efecto está activo
Uint8 counter_ = 0; // Contador para el efecto
Uint8 speed_ = 2; // Velocidad del efecto
// Valores actuales de transformación
float horizontal_zoom_ = 1.0F; // Zoom en anchura
float verical_zoom_ = 1.0F; // Zoom en altura
float x_offset_ = 0.0F; // Desplazamiento X antes de pintar
float y_offset_ = 0.0F; // Desplazamiento Y antes de pintar
public:
// Constructor por defecto
BounceEffect() = default;
// Reinicia el efecto a sus valores iniciales
void reset() {
counter_ = 0;
horizontal_zoom_ = 1.0F;
verical_zoom_ = 1.0F;
x_offset_ = 0.0F;
y_offset_ = 0.0F;
}
// Aplica la deformación visual al sprite
void apply(AnimatedSprite* sprite) const {
if (sprite != nullptr) {
sprite->setHorizontalZoom(horizontal_zoom_);
sprite->setVerticalZoom(verical_zoom_);
}
}
// Activa el efecto de rebote
void enable(AnimatedSprite* sprite, Size balloon_size) {
// Los globos pequeños no tienen efecto de rebote
if (balloon_size == Size::SMALL) {
return;
}
enabled_ = true;
reset();
apply(sprite);
}
// Detiene el efecto de rebote
void disable(AnimatedSprite* sprite) {
enabled_ = false;
reset();
apply(sprite);
}
// Actualiza el efecto en cada frame
void update(AnimatedSprite* sprite) {
if (!enabled_) {
return;
}
// Calcula el índice basado en el contador y velocidad
const int INDEX = counter_ / speed_;
// Actualiza los valores de zoom desde las tablas predefinidas
horizontal_zoom_ = HORIZONTAL_ZOOM_VALUES.at(INDEX);
verical_zoom_ = VERTICAL_ZOOM_VALUES.at(INDEX);
// Aplica la deformación al sprite
apply(sprite);
// Incrementa el contador y verifica si el efecto debe terminar
if (++counter_ / speed_ >= BOUNCE_FRAMES) {
disable(sprite);
}
}
// Getters para acceso a los valores actuales
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
[[nodiscard]] auto getHorizontalZoom() const -> float { return horizontal_zoom_; }
[[nodiscard]] auto getVerticalZoom() const -> float { return verical_zoom_; }
[[nodiscard]] auto getXOffset() const -> float { return x_offset_; }
[[nodiscard]] auto getYOffset() const -> float { return y_offset_; }
};
// --- Objetos y punteros ---
std::unique_ptr<AnimatedSprite> sprite_; // Sprite del objeto globo
// --- Variables de estado y físicas ---
float x_; // Posición X
float y_; // Posición Y
float w_; // Ancho
float h_; // Alto
float vx_; // Velocidad X
float vy_; // Velocidad Y
float gravity_; // Aceleración en Y
float default_vy_; // Velocidad inicial al rebotar
float max_vy_; // Máxima velocidad en Y
bool being_created_; // Si el globo se está creando
bool enabled_ = true; // Si el globo está activo
bool invulnerable_; // Si el globo es invulnerable
bool stopped_; // Si el globo está parado
bool use_reversed_colors_ = false; // Si se usa el color alternativo
Circle collider_; // Círculo de colisión
float creation_counter_; // Temporizador de creación
float creation_counter_ini_; // Valor inicial del temporizador de creación
Uint16 score_; // Puntos al destruir el globo
Type type_; // Tipo de globo
Size size_; // Tamaño de globo
Uint8 menace_; // Amenaza que genera el globo
Uint32 counter_ = 0; // Contador interno
float game_tempo_; // Multiplicador de tempo del juego
float movement_accumulator_ = 0.0F; // Acumulador para movimiento durante creación (deltaTime)
Uint8 power_; // Poder que alberga el globo
SDL_FRect play_area_; // Zona de movimiento del globo
Sound sound_; // Configuración de sonido del globo
BounceEffect bounce_effect_; // Efecto de rebote
// --- Posicionamiento y transformación ---
void shiftColliders(); // Alinea el círculo de colisión con el sprite
void shiftSprite(); // Alinea el sprite en pantalla
// --- Animación y sonido ---
void setAnimation(); // Establece la animación correspondiente
void playBouncingSound() const; // Reproduce el sonido de rebote
void playPoppingSound() const; // Reproduce el sonido de reventar
// --- Movimiento y física ---
void handleHorizontalMovement(float delta_time); // Maneja el movimiento horizontal (time-based)
void handleVerticalMovement(float delta_time); // Maneja el movimiento vertical (time-based)
void applyGravity(float delta_time); // Aplica la gravedad al objeto (time-based)
// --- Rebote ---
void enableBounceEffect(); // Activa el efecto de rebote
void disableBounceEffect(); // Detiene el efecto de rebote
void updateBounceEffect(); // Actualiza el estado del rebote
void handleHorizontalBounce(float min_x, float max_x); // Maneja el rebote horizontal dentro de límites
// --- Colisiones ---
[[nodiscard]] auto isOutOfHorizontalBounds(float min_x, float max_x) const -> bool; // Verifica si está fuera de los límites horizontales
[[nodiscard]] auto shouldCheckTopCollision() const -> bool; // Determina si debe comprobarse la colisión superior
void handleTopCollision(); // Maneja la colisión superior
void handleBottomCollision(); // Maneja la colisión inferior
// --- Lógica de estado ---
void updateState(float delta_time); // Actualiza los estados del globo (time-based)
};

View File

@@ -0,0 +1,130 @@
#include "bullet.hpp"
#include <memory> // Para unique_ptr, make_unique
#include <string> // Para basic_string, string
#include "param.hpp" // Para Param, ParamGame, param
#include "resource.hpp" // Para Resource
// Constructor
Bullet::Bullet(float x, float y, Type type, Color color, int owner)
: sprite_(std::make_unique<AnimatedSprite>(Resource::get()->getTexture("bullet.png"), Resource::get()->getAnimation("bullet.ani"))),
type_(type),
owner_(owner),
pos_x_(x),
pos_y_(y) {
vel_x_ = calculateVelocity(type_);
sprite_->setCurrentAnimation(buildAnimationString(type_, color));
collider_.r = WIDTH / 2;
shiftColliders();
}
// Calcula la velocidad horizontal de la bala basada en su tipo
auto Bullet::calculateVelocity(Type type) -> float {
switch (type) {
case Type::LEFT:
return VEL_X_LEFT;
case Type::RIGHT:
return VEL_X_RIGHT;
default:
return VEL_X_CENTER;
}
}
// Construye el string de animación basado en el tipo de bala y color específico
auto Bullet::buildAnimationString(Type type, Color color) -> std::string {
std::string animation_string;
// Mapear color a string específico
switch (color) {
case Color::YELLOW:
animation_string = "yellow_";
break;
case Color::GREEN:
animation_string = "green_";
break;
case Color::RED:
animation_string = "red_";
break;
case Color::PURPLE:
animation_string = "purple_";
break;
}
// Añadir dirección
switch (type) {
case Type::UP:
animation_string += "up";
break;
case Type::LEFT:
animation_string += "left";
break;
case Type::RIGHT:
animation_string += "right";
break;
default:
break;
}
return animation_string;
}
// Implementación de render
void Bullet::render() {
if (type_ != Type::NONE) {
sprite_->render();
}
}
// Actualiza el estado del objeto
auto Bullet::update(float delta_time) -> MoveStatus {
sprite_->update(delta_time);
return move(delta_time);
}
// Implementación del movimiento usando MoveStatus
auto Bullet::move(float delta_time) -> MoveStatus {
pos_x_ += vel_x_ * delta_time;
if (pos_x_ < param.game.play_area.rect.x - WIDTH || pos_x_ > param.game.play_area.rect.w) {
disable();
return MoveStatus::OUT;
}
pos_y_ += VEL_Y * delta_time;
if (pos_y_ < param.game.play_area.rect.y - HEIGHT) {
disable();
return MoveStatus::OUT;
}
shiftSprite();
shiftColliders();
return MoveStatus::OK;
}
auto Bullet::isEnabled() const -> bool {
return type_ != Type::NONE;
}
void Bullet::disable() {
type_ = Type::NONE;
}
auto Bullet::getOwner() const -> int {
return owner_;
}
auto Bullet::getCollider() -> Circle& {
return collider_;
}
void Bullet::shiftColliders() {
collider_.x = pos_x_ + collider_.r;
collider_.y = pos_y_ + collider_.r;
}
void Bullet::shiftSprite() {
sprite_->setX(pos_x_);
sprite_->setY(pos_y_);
}

View File

@@ -0,0 +1,76 @@
#pragma once
#include <SDL3/SDL.h> // Para Uint8
#include <memory> // Para unique_ptr
#include <string> // Para string
#include "animated_sprite.hpp" // Para AnimatedSprite
#include "utils.hpp" // Para Circle
// --- Clase Bullet: representa una bala del jugador ---
class Bullet {
public:
// --- Constantes ---
static constexpr float WIDTH = 12.0F; // Anchura de la bala
static constexpr float HEIGHT = 12.0F; // Altura de la bala
// --- Enums ---
enum class Type : Uint8 {
UP, // Bala hacia arriba
LEFT, // Bala hacia la izquierda
RIGHT, // Bala hacia la derecha
NONE // Sin bala
};
enum class MoveStatus : Uint8 {
OK = 0, // Movimiento normal
OUT = 1 // Fuera de los límites
};
enum class Color : Uint8 {
YELLOW,
GREEN,
RED,
PURPLE
};
// --- Constructor y destructor ---
Bullet(float x, float y, Type type, Color color, int owner); // Constructor principal
~Bullet() = default; // Destructor
// --- Métodos principales ---
void render(); // Dibuja la bala en pantalla
auto update(float delta_time) -> MoveStatus; // Actualiza el estado del objeto (time-based)
void disable(); // Desactiva la bala
// --- Getters ---
[[nodiscard]] auto isEnabled() const -> bool; // Comprueba si está activa
[[nodiscard]] auto getOwner() const -> int; // Devuelve el identificador del dueño
auto getCollider() -> Circle&; // Devuelve el círculo de colisión
private:
// --- Constantes ---
static constexpr float VEL_Y = -180.0F; // Velocidad vertical (pixels/segundo) - era -0.18F pixels/ms
static constexpr float VEL_X_LEFT = -120.0F; // Velocidad izquierda (pixels/segundo) - era -0.12F pixels/ms
static constexpr float VEL_X_RIGHT = 120.0F; // Velocidad derecha (pixels/segundo) - era 0.12F pixels/ms
static constexpr float VEL_X_CENTER = 0.0F; // Velocidad central
// --- Objetos y punteros ---
std::unique_ptr<AnimatedSprite> sprite_; // Sprite con los gráficos
// --- Variables de estado ---
Circle collider_; // Círculo de colisión
Type type_; // Tipo de bala
int owner_; // Identificador del jugador
float pos_x_; // Posición en el eje X
float pos_y_; // Posición en el eje Y
float vel_x_; // Velocidad en el eje X
// --- Métodos internos ---
void shiftColliders(); // Ajusta el círculo de colisión
void shiftSprite(); // Ajusta el sprite
auto move(float delta_time) -> MoveStatus; // Mueve la bala y devuelve su estado (time-based)
static auto calculateVelocity(Type type) -> float; // Calcula la velocidad horizontal de la bala
static auto buildAnimationString(Type type, Color color) -> std::string; // Construye el string de animación
};

View File

@@ -0,0 +1,58 @@
#include "explosions.hpp"
#include <utility> // Para std::cmp_less
#include "animated_sprite.hpp" // Para AnimatedSprite
class Texture; // lines 4-4
// Actualiza la lógica de la clase (time-based)
void Explosions::update(float delta_time) {
for (auto& explosion : explosions_) {
explosion->update(delta_time);
}
// Vacia el vector de elementos finalizados
freeExplosions();
}
// Dibuja el objeto en pantalla
void Explosions::render() {
for (auto& explosion : explosions_) {
explosion->render();
}
}
// Añade texturas al objeto
void Explosions::addTexture(int size, const std::shared_ptr<Texture>& texture, const std::vector<std::string>& animation) {
textures_.emplace_back(size, texture, animation);
}
// Añade una explosión
void Explosions::add(int x, int y, int size) {
const auto INDEX = getIndexBySize(size);
explosions_.emplace_back(std::make_unique<AnimatedSprite>(textures_[INDEX].texture, textures_[INDEX].animation));
explosions_.back()->setPos(x, y);
}
// Vacia el vector de elementos finalizados
void Explosions::freeExplosions() {
if (!explosions_.empty()) {
for (int i = explosions_.size() - 1; i >= 0; --i) {
if (explosions_[i]->animationIsCompleted()) {
explosions_.erase(explosions_.begin() + i);
}
}
}
}
// Busca una textura a partir del tamaño
auto Explosions::getIndexBySize(int size) -> int {
for (int i = 0; std::cmp_less(i, textures_.size()); ++i) {
if (size == textures_[i].size) {
return i;
}
}
return 0;
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include <memory> // Para unique_ptr, shared_ptr
#include <string> // Para string
#include <utility> // Para move
#include <vector> // Para vector
#include "animated_sprite.hpp" // Para AnimatedSprite
class Texture;
// --- Estructura ExplosionTexture: almacena información de una textura de explosión ---
struct ExplosionTexture {
int size; // Tamaño de la explosión
std::shared_ptr<Texture> texture; // Textura para la explosión
std::vector<std::string> animation; // Animación para la textura
ExplosionTexture(int sz, std::shared_ptr<Texture> tex, const std::vector<std::string>& anim)
: size(sz),
texture(std::move(tex)),
animation(anim) {}
};
// --- Clase Explosions: gestor de explosiones ---
class Explosions {
public:
// --- Constructor y destructor ---
Explosions() = default; // Constructor por defecto
~Explosions() = default; // Destructor por defecto
// --- Métodos principales ---
void update(float delta_time); // Actualiza la lógica de la clase (time-based)
void render(); // Dibuja el objeto en pantalla
// --- Configuración ---
void addTexture(int size, const std::shared_ptr<Texture>& texture, const std::vector<std::string>& animation); // Añade texturas al objeto
void add(int x, int y, int size); // Añade una explosión
private:
// --- Variables de estado ---
std::vector<ExplosionTexture> textures_; // Vector con las texturas a utilizar
std::vector<std::unique_ptr<AnimatedSprite>> explosions_; // Lista con todas las explosiones
// --- Métodos internos ---
void freeExplosions(); // Vacía el vector de elementos finalizados
auto getIndexBySize(int size) -> int; // Busca una textura a partir del tamaño
};

View File

@@ -0,0 +1,228 @@
#include "item.hpp"
#include <algorithm> // Para clamp
#include <cmath> // Para fmod
#include <cstdlib> // Para rand
#include "animated_sprite.hpp" // Para AnimatedSprite
#include "param.hpp" // Para Param, ParamGame, param
class Texture; // lines 6-6
Item::Item(ItemType type, float x, float y, SDL_FRect& play_area, const std::shared_ptr<Texture>& texture, const std::vector<std::string>& animation)
: sprite_(std::make_unique<AnimatedSprite>(texture, animation)),
play_area_(play_area),
type_(type) {
switch (type) {
case ItemType::COFFEE_MACHINE: {
width_ = COFFEE_MACHINE_WIDTH;
height_ = COFFEE_MACHINE_HEIGHT;
pos_x_ = getCoffeeMachineSpawn(x, width_, play_area_.w);
pos_y_ = y;
vel_x_ = ((rand() % 3) - 1) * COFFEE_MACHINE_VEL_X_FACTOR;
vel_y_ = COFFEE_MACHINE_VEL_Y;
accel_y_ = COFFEE_MACHINE_ACCEL_Y;
collider_.r = 10;
break;
}
default: {
pos_x_ = x;
pos_y_ = y;
// 6 velocidades: 3 negativas (-1.0, -0.66, -0.33) y 3 positivas (0.33, 0.66, 1.0)
const int DIRECTION = rand() % 6;
if (DIRECTION < 3) {
// Velocidades negativas: -1.0, -0.66, -0.33
vel_x_ = -ITEM_VEL_X_BASE + (DIRECTION * ITEM_VEL_X_STEP);
rotate_speed_ = -720.0F;
} else {
// Velocidades positivas: 0.33, 0.66, 1.0
vel_x_ = ITEM_VEL_X_STEP + ((DIRECTION - 3) * ITEM_VEL_X_STEP);
rotate_speed_ = 720.0F;
}
vel_y_ = ITEM_VEL_Y;
accel_y_ = ITEM_ACCEL_Y;
collider_.r = width_ / 2;
sprite_->startRotate();
sprite_->setRotateAmount(rotate_speed_);
sprite_->setCurrentAnimation("no-blink");
break;
}
}
// Actualiza el sprite
shiftSprite();
shiftColliders();
}
void Item::alignTo(int x) {
const float MIN_X = param.game.play_area.rect.x + 1;
const float MAX_X = play_area_.w - width_ - 1;
pos_x_ = x - (width_ / 2);
// Ajusta para que no quede fuera de la zona de juego
pos_x_ = std::clamp(pos_x_, MIN_X, MAX_X);
// Actualiza el sprite
shiftSprite();
shiftColliders();
}
void Item::render() {
if (enabled_) {
// Muestra normalmente hasta los últimos ~3.3 segundos
constexpr float BLINK_START_S = LIFETIME_DURATION_S - 3.33F;
if (lifetime_timer_ < BLINK_START_S) {
sprite_->render();
} else {
// Efecto de parpadeo en los últimos segundos (cada ~0.33 segundos)
constexpr float BLINK_INTERVAL_S = 0.33F;
const float PHASE = std::fmod(lifetime_timer_, BLINK_INTERVAL_S);
const float HALF_INTERVAL = BLINK_INTERVAL_S / 2.0F;
if (PHASE < HALF_INTERVAL) {
sprite_->render();
}
}
}
}
void Item::move(float delta_time) {
floor_collision_ = false;
// Calcula la nueva posición usando deltaTime (velocidad en pixels/segundo)
pos_x_ += vel_x_ * delta_time;
pos_y_ += vel_y_ * delta_time;
// Aplica las aceleraciones a la velocidad usando deltaTime (aceleración en pixels/segundo²)
vel_x_ += accel_x_ * delta_time;
vel_y_ += accel_y_ * delta_time;
// Comprueba los laterales de la zona de juego
const float MIN_X = param.game.play_area.rect.x;
const float MAX_X = play_area_.w - width_;
pos_x_ = std::clamp(pos_x_, MIN_X, MAX_X);
// Si toca el borde lateral
if (pos_x_ == MIN_X || pos_x_ == MAX_X) {
vel_x_ = -vel_x_; // Invierte la velocidad horizontal
sprite_->scaleRotateAmount(-1.0F); // Invierte la rotación
}
// Si colisiona por arriba, rebota (excepto la máquina de café)
if ((pos_y_ < param.game.play_area.rect.y) && !(type_ == ItemType::COFFEE_MACHINE)) {
// Corrige
pos_y_ = param.game.play_area.rect.y;
// Fuerza la velocidad hacia abajo para evitar oscilaciones
vel_y_ = std::abs(vel_y_);
}
// Si colisiona con la parte inferior
if (pos_y_ > play_area_.h - height_) {
pos_y_ = play_area_.h - height_; // Corrige la posición
sprite_->scaleRotateAmount(0.5F); // Reduce la rotación
sprite_->stopRotate(300.0F); // Detiene la rotacion
switch (type_) {
case ItemType::COFFEE_MACHINE:
// La máquina de café es mas pesada y tiene una fisica diferente, ademas hace ruido
floor_collision_ = true;
if (std::abs(vel_y_) < BOUNCE_VEL_THRESHOLD) {
// Si la velocidad vertical es baja, detiene el objeto
vel_y_ = vel_x_ = accel_x_ = accel_y_ = 0;
} else {
// Si la velocidad vertical es alta, el objeto rebota y pierde velocidad
vel_y_ *= COFFEE_BOUNCE_DAMPING;
vel_x_ *= HORIZONTAL_DAMPING;
}
break;
default:
// Si no es una máquina de café
if (std::abs(vel_y_) < BOUNCE_VEL_THRESHOLD) {
// Si la velocidad vertical es baja, detiene el objeto
vel_y_ = vel_x_ = accel_x_ = accel_y_ = 0;
sprite_->setCurrentAnimation("blink");
} else {
// Si la velocidad vertical es alta, el objeto rebota y pierde velocidad
vel_y_ *= ITEM_BOUNCE_DAMPING;
vel_x_ *= HORIZONTAL_DAMPING;
}
break;
}
}
// Actualiza la posición del sprite
shiftSprite();
shiftColliders();
}
void Item::disable() { enabled_ = false; }
void Item::update(float delta_time) {
move(delta_time);
sprite_->update(delta_time);
updateTimeToLive(delta_time);
}
void Item::updateTimeToLive(float delta_time) {
lifetime_timer_ += delta_time;
if (lifetime_timer_ >= LIFETIME_DURATION_S) {
disable();
}
}
void Item::shiftColliders() {
collider_.x = static_cast<int>(pos_x_ + (width_ / 2));
collider_.y = static_cast<int>(pos_y_ + (height_ / 2));
}
void Item::shiftSprite() {
sprite_->setPosX(pos_x_);
sprite_->setPosY(pos_y_);
}
// Calcula la zona de aparición de la máquina de café
auto Item::getCoffeeMachineSpawn(int player_x, int item_width, int area_width, int margin) -> int {
// Distancia mínima del jugador (ajusta según necesites)
const int MIN_DISTANCE_FROM_PLAYER = area_width / 2;
const int LEFT_BOUND = margin;
const int RIGHT_BOUND = area_width - item_width - margin;
// Calcular zona de exclusión alrededor del jugador
int exclude_left = player_x - MIN_DISTANCE_FROM_PLAYER;
int exclude_right = player_x + MIN_DISTANCE_FROM_PLAYER;
// Verificar si hay espacio suficiente a la izquierda
bool can_spawn_left = (exclude_left > LEFT_BOUND) && (exclude_left - LEFT_BOUND > item_width);
// Verificar si hay espacio suficiente a la derecha
bool can_spawn_right = (exclude_right < RIGHT_BOUND) && (RIGHT_BOUND - exclude_right > item_width);
if (can_spawn_left && can_spawn_right) {
// Ambos lados disponibles, elegir aleatoriamente
if (rand() % 2 == 0) {
// Lado izquierdo
return (rand() % (exclude_left - LEFT_BOUND)) + LEFT_BOUND;
} // Lado derecho
return (rand() % (RIGHT_BOUND - exclude_right)) + exclude_right;
}
if (can_spawn_left) {
// Solo lado izquierdo disponible
return (rand() % (exclude_left - LEFT_BOUND)) + LEFT_BOUND;
}
if (can_spawn_right) {
// Solo lado derecho disponible
return (rand() % (RIGHT_BOUND - exclude_right)) + exclude_right;
} // No hay espacio suficiente lejos del jugador
// Por ahora, intentar spawn en el extremo más lejano posible
int distance_to_left = abs(player_x - LEFT_BOUND);
int distance_to_right = abs(RIGHT_BOUND - player_x);
if (distance_to_left > distance_to_right) {
return LEFT_BOUND;
}
return RIGHT_BOUND - item_width;
}

View File

@@ -0,0 +1,100 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FRect, Uint16
#include <memory> // Para shared_ptr, unique_ptr
#include <string> // Para string
#include <vector> // Para vector
#include "animated_sprite.hpp" // Para AnimatedSprite
#include "utils.hpp" // Para Circle
class Texture;
// --- Enums ---
enum class ItemType : int {
DISK = 1, // Disco
GAVINA = 2, // Gavina
PACMAR = 3, // Pacman
CLOCK = 4, // Reloj
COFFEE = 5, // Café
DEBIAN = 6, // Debian
COFFEE_MACHINE = 7, // Máquina de café
NONE = 8, // Ninguno
};
// --- Clase Item: representa un objeto en el juego ---
class Item {
public:
// --- Constantes ---
static constexpr float WIDTH = 20.0F; // Anchura del item
static constexpr float HEIGHT = 20.0F; // ALtura del item
static constexpr int COFFEE_MACHINE_WIDTH = 30; // Anchura de la máquina de café
static constexpr int COFFEE_MACHINE_HEIGHT = 39; // Altura de la máquina de café
static constexpr float LIFETIME_DURATION_S = 10.0F; // Duración de vida del ítem en segundos
// Velocidades base (pixels/segundo) - Coffee Machine
static constexpr float COFFEE_MACHINE_VEL_X_FACTOR = 30.0F; // Factor para velocidad X de máquina de café (0.5*60fps)
static constexpr float COFFEE_MACHINE_VEL_Y = -6.0F; // Velocidad Y inicial de máquina de café (-0.1*60fps)
static constexpr float COFFEE_MACHINE_ACCEL_Y = 360.0F; // Aceleración Y de máquina de café (0.1*60²fps = 360 pixels/segundo²)
// Velocidades base (pixels/segundo) - Items normales
static constexpr float ITEM_VEL_X_BASE = 60.0F; // Velocidad X base para items (1.0F*60fps)
static constexpr float ITEM_VEL_X_STEP = 20.0F; // Incremento de velocidad X (0.33F*60fps)
static constexpr float ITEM_VEL_Y = -240.0F; // Velocidad Y inicial de items (-4.0F*60fps)
static constexpr float ITEM_ACCEL_Y = 720.0F; // Aceleración Y de items (0.2*60²fps = 720 pixels/segundo²)
// Constantes de física de rebote
static constexpr float BOUNCE_VEL_THRESHOLD = 60.0F; // Umbral de velocidad para parar (1.0F*60fps)
static constexpr float COFFEE_BOUNCE_DAMPING = -0.20F; // Factor de rebote Y para máquina de café
static constexpr float ITEM_BOUNCE_DAMPING = -0.5F; // Factor de rebote Y para items normales
static constexpr float HORIZONTAL_DAMPING = 0.75F; // Factor de amortiguación horizontal
// --- Constructor y destructor ---
Item(ItemType type, float x, float y, SDL_FRect& play_area, const std::shared_ptr<Texture>& texture, const std::vector<std::string>& animation); // Constructor principal
~Item() = default; // Destructor
// --- Métodos principales ---
void alignTo(int x); // Centra el objeto en la posición X indicada
void render(); // Renderiza el objeto en pantalla
void disable(); // Desactiva el objeto
void update(float delta_time); // Actualiza la posición, animación y contadores (time-based)
// --- Getters ---
[[nodiscard]] auto getPosX() const -> float { return pos_x_; } // Obtiene la posición X
[[nodiscard]] auto getPosY() const -> float { return pos_y_; } // Obtiene la posición Y
[[nodiscard]] auto getWidth() const -> int { return width_; } // Obtiene la anchura
[[nodiscard]] auto getHeight() const -> int { return height_; } // Obtiene la altura
[[nodiscard]] auto getType() const -> ItemType { return type_; } // Obtiene el tipo
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; } // Verifica si está habilitado
[[nodiscard]] auto isOnFloor() const -> bool { return floor_collision_; } // Verifica si está en el suelo
auto getCollider() -> Circle& { return collider_; } // Obtiene el colisionador
private:
// --- Objetos y punteros ---
std::unique_ptr<AnimatedSprite> sprite_; // Sprite con los gráficos del objeto
// --- Variables de estado ---
SDL_FRect play_area_; // Rectángulo con la zona de juego
Circle collider_; // Círculo de colisión del objeto
ItemType type_; // Tipo de objeto
float pos_x_ = 0.0F; // Posición X del objeto
float pos_y_ = 0.0F; // Posición Y del objeto
float vel_x_ = 0.0F; // Velocidad en el eje X
float vel_y_ = 0.0F; // Velocidad en el eje Y
float accel_x_ = 0.0F; // Aceleración en el eje X
float accel_y_ = 0.0F; // Aceleración en el eje Y
float width_ = WIDTH; // Ancho del objeto
float height_ = HEIGHT; // Alto del objeto
float rotate_speed_ = 0.0F; // Velocidad de rotacion
float lifetime_timer_ = 0.0F; // Acumulador de tiempo de vida del ítem (segundos)
bool floor_collision_ = false; // Indica si el objeto colisiona con el suelo
bool enabled_ = true; // Indica si el objeto está habilitado
// --- Métodos internos ---
void shiftColliders(); // Alinea el círculo de colisión con la posición del objeto
void shiftSprite(); // Coloca el sprite en la posición del objeto
void move(float delta_time); // Actualiza la posición y estados del objeto (time-based)
void updateTimeToLive(float delta_time); // Actualiza el contador de tiempo de vida (time-based)
static auto getCoffeeMachineSpawn(int player_x, int item_width, int area_width, int margin = 2) -> int; // Calcula la zona de aparición de la máquina de café
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,405 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FRect, SDL_FlipMode
#include <cstddef> // Para size_t
#include <iterator> // Para pair
#include <memory> // Para shared_ptr, unique_ptr
#include <string> // Para basic_string, string
#include <utility> // Para move, pair
#include <vector> // Para vector
#include "animated_sprite.hpp" // for AnimatedSprite
#include "bullet.hpp" // for Bullet
#include "cooldown.hpp"
#include "enter_name.hpp" // for EnterName
#include "input.hpp" // for Input
#include "manage_hiscore_table.hpp" // for Table
#include "scoreboard.hpp" // for Scoreboard
#include "utils.hpp" // for Circle
class IStageInfo;
class Texture;
// --- Clase Player: jugador principal del juego ---
//
// Esta clase gestiona todos los aspectos de un jugador durante el juego,
// incluyendo movimiento, disparos, animaciones y estados especiales.
//
// Funcionalidades principales:
// • Sistema de disparo de dos líneas: funcional (cooldown) + visual (animaciones)
// • Estados de animación: normal → aiming → recoiling → threat_pose → normal
// • Movimiento time-based: compatibilidad con deltaTime para fluidez variable
// • Power-ups e invulnerabilidad: coffee machine, extra hits, parpadeos
// • Sistema de puntuación: multipliers, high scores, entrada de nombres
// • Estados de juego: playing, rolling, continue, entering_name, etc.
//
// El sistema de disparo utiliza duraciones configurables mediante constantes
// para facilitar el ajuste del gameplay y la sensación de disparo.
class Player {
public:
// --- Constantes ---
static constexpr int WIDTH = 32; // Anchura
static constexpr int HEIGHT = 32; // Altura
// --- Estructuras ---
struct BulletColorPair {
Bullet::Color normal_color; // Color de bala sin power-up
Bullet::Color powered_color; // Color de bala con power-up
};
// --- Enums ---
enum class Id : int {
NO_PLAYER = -1, // Sin jugador
BOTH_PLAYERS = 0, // Ambos jugadores
PLAYER1 = 1, // Jugador 1
PLAYER2 = 2 // Jugador 2
};
enum class State {
// Estados de movimiento
WALKING_LEFT, // Caminando hacia la izquierda
WALKING_RIGHT, // Caminando hacia la derecha
WALKING_STOP, // Parado, sin moverse
// Estados de disparo
FIRING_UP, // Disparando hacia arriba
FIRING_LEFT, // Disparando hacia la izquierda
FIRING_RIGHT, // Disparando hacia la derecha
FIRING_NONE, // No está disparando
// Estados de retroceso tras disparar
RECOILING_UP, // Retroceso tras disparar hacia arriba
RECOILING_LEFT, // Retroceso tras disparar hacia la izquierda
RECOILING_RIGHT, // Retroceso tras disparar hacia la derecha
// Estados de enfriamiento tras disparar
COOLING_UP, // Enfriando tras disparar hacia arriba
COOLING_LEFT, // Enfriando tras disparar hacia la izquierda
COOLING_RIGHT, // Enfriando tras disparar hacia la derecha
// Estados generales del jugador
PLAYING, // Está jugando activamente
CONTINUE, // Cuenta atrás para continuar tras perder
CONTINUE_TIME_OUT, // Se ha terminado la cuenta atras para continuar y se retira al jugador de la zona de juego
WAITING, // Esperando para entrar a jugar
ENTERING_NAME, // Introduciendo nombre para la tabla de puntuaciones
SHOWING_NAME, // Mostrando el nombre introducido
ROLLING, // El jugador está dando vueltas y rebotando
LYING_ON_THE_FLOOR_FOREVER, // El jugador está inconsciente para siempre en el suelo (demo)
GAME_OVER, // Fin de la partida, no puede jugar
CELEBRATING, // Celebrando victoria (pose de victoria)
ENTERING_NAME_GAME_COMPLETED, // Introduciendo nombre tras completar el juego
LEAVING_SCREEN, // Saliendo de la pantalla (animación)
ENTERING_SCREEN, // Entrando a la pantalla (animación)
CREDITS, // Estado para mostrar los créditos del juego
TITLE_ANIMATION, // Animacion para el titulo
TITLE_HIDDEN, // Animacion para el titulo
RECOVER, // Al aceptar continuar
RESPAWNING, // Tras continuar y dar las gracias, otorga inmunidad y vuelve al juego
};
// --- Estructuras ---
struct Config {
Id id; // Identificador del jugador
float x; // Posición X inicial
int y; // Posición Y inicial
bool demo; // Modo demo
SDL_FRect* play_area; // Área de juego (puntero para mantener referencia)
std::vector<std::shared_ptr<Texture>> texture; // Texturas del jugador
std::vector<std::vector<std::string>> animations; // Animaciones del jugador
Table* hi_score_table; // Tabla de puntuaciones (puntero para referencia)
int* glowing_entry; // Entrada brillante (puntero para mantener referencia)
IStageInfo* stage_info; // Gestor de pantallas (puntero)
};
// --- Constructor y destructor ---
Player(const Config& config);
~Player() = default;
// --- Inicialización y ciclo de vida ---
void init(); // Inicializa el jugador
void update(float delta_time); // Actualiza estado, animación y contadores (time-based)
void render(); // Dibuja el jugador en pantalla
// --- Entrada y control ---
void setInput(Input::Action action); // Procesa entrada general
void setInputPlaying(Input::Action action); // Procesa entrada en modo jugando
void setInputEnteringName(Input::Action action); // Procesa entrada al introducir nombre
// --- Movimiento y animación ---
void move(float delta_time); // Mueve el jugador (time-based)
void setAnimation(float delta_time); // Establece la animación según el estado (time-based)
// --- Texturas y animaciones ---
void setPlayerTextures(const std::vector<std::shared_ptr<Texture>>& texture); // NOLINT(readability-avoid-const-params-in-decls) Cambia las texturas del jugador
// --- Gameplay: Puntuación y power-ups ---
void addScore(int score, int lowest_hi_score_entry); // Añade puntos
void incScoreMultiplier(); // Incrementa el multiplicador
void decScoreMultiplier(); // Decrementa el multiplicador
// --- Estados de juego ---
void setPlayingState(State state); // Cambia el estado de juego
void setInvulnerable(bool value); // Establece el valor del estado de invulnerabilidad
void setPowerUp(); // Activa el modo PowerUp
void updatePowerUp(float delta_time); // Actualiza el valor de PowerUp
void giveExtraHit(); // Concede un toque extra al jugador
void removeExtraHit(); // Quita el toque extra al jugador
void decContinueCounter(); // Decrementa el contador de continuar
void setWalkingState(State state) { walking_state_ = state; } // Establece el estado de caminar
void startFiringSystem(int cooldown_frames); // Inicia el sistema de disparo
void setScoreBoardPanel(Scoreboard::Id panel) { scoreboard_panel_ = panel; } // Establece el panel del marcador
void addCredit();
void passShowingName();
// --- Estado del juego: Consultas (is* methods) ---
[[nodiscard]] auto isLyingOnTheFloorForever() const -> bool { return playing_state_ == State::LYING_ON_THE_FLOOR_FOREVER; }
[[nodiscard]] auto isCelebrating() const -> bool { return playing_state_ == State::CELEBRATING; }
[[nodiscard]] auto isContinue() const -> bool { return playing_state_ == State::CONTINUE; }
[[nodiscard]] auto isDying() const -> bool { return playing_state_ == State::ROLLING; }
[[nodiscard]] auto isEnteringName() const -> bool { return playing_state_ == State::ENTERING_NAME; }
[[nodiscard]] auto isShowingName() const -> bool { return playing_state_ == State::SHOWING_NAME; }
[[nodiscard]] auto isEnteringNameGameCompleted() const -> bool { return playing_state_ == State::ENTERING_NAME_GAME_COMPLETED; }
[[nodiscard]] auto isLeavingScreen() const -> bool { return playing_state_ == State::LEAVING_SCREEN; }
[[nodiscard]] auto isGameOver() const -> bool { return playing_state_ == State::GAME_OVER; }
[[nodiscard]] auto isPlaying() const -> bool { return playing_state_ == State::PLAYING; }
[[nodiscard]] auto isWaiting() const -> bool { return playing_state_ == State::WAITING; }
[[nodiscard]] auto isTitleHidden() const -> bool { return playing_state_ == State::TITLE_HIDDEN; }
// --- Estados específicos: Consultas adicionales ---
[[nodiscard]] auto canFire() const -> bool { return can_fire_new_system_; } // Usa nuevo sistema
[[nodiscard]] auto hasExtraHit() const -> bool { return extra_hit_; }
[[nodiscard]] auto isCooling() const -> bool { return firing_state_ == State::COOLING_LEFT || firing_state_ == State::COOLING_UP || firing_state_ == State::COOLING_RIGHT; }
[[nodiscard]] auto isRecoiling() const -> bool { return firing_state_ == State::RECOILING_LEFT || firing_state_ == State::RECOILING_UP || firing_state_ == State::RECOILING_RIGHT; }
[[nodiscard]] auto qualifiesForHighScore() const -> bool { return qualifies_for_high_score_; }
[[nodiscard]] auto isInvulnerable() const -> bool { return invulnerable_; }
[[nodiscard]] auto isPowerUp() const -> bool { return power_up_; }
[[nodiscard]] auto isInBulletColorToggleMode() const -> bool { return in_power_up_ending_phase_; }
// --- Getters: Propiedades y valores ---
// Posición y dimensiones
[[nodiscard]] auto getPosX() const -> int { return static_cast<int>(pos_x_); }
[[nodiscard]] auto getPosY() const -> int { return pos_y_; }
[[nodiscard]] static auto getWidth() -> int { return WIDTH; }
[[nodiscard]] static auto getHeight() -> int { return HEIGHT; }
// Jugador y identificación
[[nodiscard]] auto getId() const -> Player::Id { return id_; }
[[nodiscard]] auto getName() const -> const std::string& { return name_; }
[[nodiscard]] auto getPlayingState() const -> State { return playing_state_; }
auto getCollider() -> Circle& { return collider_; }
[[nodiscard]] auto getZOrder() const -> size_t { return z_order_; }
void setZOrder(size_t z_order) { z_order_ = z_order; }
// Puntuación y juego
[[nodiscard]] auto getScore() const -> int { return score_; }
[[nodiscard]] auto getScoreMultiplier() const -> float { return score_multiplier_; }
[[nodiscard]] auto get1CC() const -> bool { return game_completed_ && credits_used_ <= 1; }
[[nodiscard]] auto getScoreBoardPanel() const -> Scoreboard::Id { return scoreboard_panel_; }
// Power-ups y estado especial
[[nodiscard]] auto getCoffees() const -> int { return coffees_; }
[[nodiscard]] auto getPowerUpCounter() const -> int { return power_up_counter_; }
[[nodiscard]] auto getInvulnerableCounter() const -> int { return invulnerable_counter_; }
[[nodiscard]] auto getBulletColor() const -> Bullet::Color; // Devuelve el color actual de bala según el estado
auto getNextBulletColor() -> Bullet::Color; // Devuelve el color para la próxima bala (alterna si está en modo toggle)
void setBulletColors(Bullet::Color normal, Bullet::Color powered); // Establece los colores de bala para este jugador
[[nodiscard]] auto getBulletSoundFile() const -> std::string { return bullet_sound_file_; } // Devuelve el archivo de sonido de bala
void setBulletSoundFile(const std::string& filename); // Establece el archivo de sonido de bala para este jugador
// Contadores y timers
[[nodiscard]] auto getContinueCounter() const -> int { return continue_counter_; }
[[nodiscard]] auto getRecordName() const -> std::string { return enter_name_ ? enter_name_->getFinalName() : "xxx"; }
[[nodiscard]] auto getLastEnterName() const -> std::string { return last_enter_name_; }
// --- Configuración e interfaz externa ---
void setName(const std::string& name) { name_ = name; }
void setGamepad(std::shared_ptr<Input::Gamepad> gamepad) { gamepad_ = std::move(gamepad); }
[[nodiscard]] auto getGamepad() const -> std::shared_ptr<Input::Gamepad> { return gamepad_; }
void setUsesKeyboard(bool value) { uses_keyboard_ = value; }
[[nodiscard]] auto getUsesKeyboard() const -> bool { return uses_keyboard_; }
[[nodiscard]] auto getController() const -> int { return controller_index_; }
// Demo file management
[[nodiscard]] auto getDemoFile() const -> size_t { return demo_file_; }
void setDemoFile(size_t demo_file) { demo_file_ = demo_file; }
private:
// --- Constantes de física y movimiento ---
static constexpr float BASE_SPEED = 90.0F; // Velocidad base del jugador (pixels/segundo)
// --- Constantes de power-ups y estados especiales ---
static constexpr int POWERUP_COUNTER = 1500; // Duración del estado PowerUp (frames)
static constexpr int INVULNERABLE_COUNTER = 200; // Duración del estado invulnerable (frames)
static constexpr size_t INVULNERABLE_TEXTURE = 3; // Textura usada durante invulnerabilidad
// --- Constantes del sistema de disparo (obsoletas - usar nuevo sistema) ---
static constexpr int COOLING_DURATION = 50; // Duración del enfriamiento tras disparar
static constexpr int COOLING_COMPLETE = 0; // Valor que indica enfriamiento completado
// --- Constantes de estados de espera ---
static constexpr int WAITING_COUNTER = 1000; // Tiempo de espera en estado de espera
// --- Constantes del nuevo sistema de disparo de dos líneas ---
static constexpr float AIMING_DURATION_FACTOR = 0.5F; // 50% del cooldown funcional
static constexpr float RECOILING_DURATION_MULTIPLIER = 4.0F; // 4 veces la duración de aiming
static constexpr float THREAT_POSE_DURATION = 50.0F / 60.0F; // 50 frames = ~0.833s (duración base)
static constexpr float MIN_THREAT_POSE_DURATION = 6.0F / 60.0F; // 6 frames = ~0.1s (duración mínima)
// --- Objetos y punteros ---
std::unique_ptr<AnimatedSprite> player_sprite_; // Sprite para dibujar el jugador
std::unique_ptr<AnimatedSprite> power_sprite_; // Sprite para dibujar el aura del jugador con el poder a tope
std::unique_ptr<EnterName> enter_name_; // Clase utilizada para introducir el nombre
std::unique_ptr<Cooldown> cooldown_ = nullptr; // Objeto para gestionar cooldowns de teclado
std::shared_ptr<Input::Gamepad> gamepad_ = nullptr; // Dispositivo asociado
Table* hi_score_table_ = nullptr; // Tabla de máximas puntuaciones
int* glowing_entry_ = nullptr; // Entrada de la tabla de puntuaciones para hacerla brillar
IStageInfo* stage_info_; // Informacion de la pantalla actual
// --- Variables de estado ---
SDL_FRect play_area_; // Rectángulo con la zona de juego
Circle collider_ = Circle(0, 0, 9); // Círculo de colisión del jugador
std::string name_; // Nombre del jugador
std::string last_enter_name_; // Último nombre introducido en la tabla de puntuaciones
Scoreboard::Id scoreboard_panel_ = Scoreboard::Id::LEFT; // Panel del marcador asociado al jugador
Id id_; // Identificador para el jugador
State walking_state_ = State::WALKING_STOP; // Estado del jugador al moverse
State firing_state_ = State::FIRING_NONE; // Estado del jugador al disparar
State playing_state_ = State::WAITING; // Estado del jugador en el juego
BulletColorPair bullet_colors_ = {.normal_color = Bullet::Color::YELLOW, .powered_color = Bullet::Color::GREEN}; // Par de colores de balas para este jugador
std::string bullet_sound_file_ = "bullet1p.wav"; // Archivo de sonido de bala para este jugador
float pos_x_ = 0.0F; // Posición en el eje X
float default_pos_x_; // Posición inicial para el jugador
float vel_x_ = 0.0F; // Cantidad de píxeles a desplazarse en el eje X
float score_multiplier_ = 1.0F; // Multiplicador de puntos
int pos_y_ = 0; // Posición en el eje Y
int default_pos_y_; // Posición inicial para el jugador
int vel_y_ = 0; // Cantidad de píxeles a desplazarse en el eje Y
float invulnerable_time_accumulator_ = 0.0F; // Acumulador de tiempo para invulnerabilidad (time-based)
float power_up_time_accumulator_ = 0.0F; // Acumulador de tiempo para power-up (time-based)
float continue_time_accumulator_ = 0.0F; // Acumulador de tiempo para continue counter (time-based)
float name_entry_time_accumulator_ = 0.0F; // Acumulador de tiempo para name entry counter (time-based)
float showing_name_time_accumulator_ = 0.0F; // Acumulador de tiempo para showing name (time-based)
float waiting_time_accumulator_ = 0.0F; // Acumulador de tiempo para waiting movement (time-based)
float step_time_accumulator_ = 0.0F; // Acumulador de tiempo para step counter (time-based)
// ========================================
// NUEVO SISTEMA DE DISPARO DE DOS LÍNEAS
// ========================================
// LÍNEA 1: SISTEMA FUNCIONAL (CanFire)
float fire_cooldown_timer_ = 0.0F; // Tiempo restante hasta poder disparar otra vez
bool can_fire_new_system_ = true; // true si puede disparar ahora mismo
// LÍNEA 2: SISTEMA VISUAL (Animaciones)
enum class VisualFireState {
NORMAL, // Brazo en posición neutral
AIMING, // Brazo alzado (disparando)
RECOILING, // Brazo en retroceso
THREAT_POSE // Posición amenazante
};
VisualFireState visual_fire_state_ = VisualFireState::NORMAL;
float visual_state_timer_ = 0.0F; // Tiempo en el estado visual actual
float aiming_duration_ = 0.0F; // Duración del estado AIMING
float recoiling_duration_ = 0.0F; // Duración del estado RECOILING
int invulnerable_counter_ = INVULNERABLE_COUNTER; // Contador para la invulnerabilidad
int score_ = 0; // Puntos del jugador
int coffees_ = 0; // Indica cuántos cafés lleva acumulados
int power_up_counter_ = POWERUP_COUNTER; // Temporizador para el modo PowerUp
int power_up_x_offset_ = 0; // Desplazamiento del sprite de PowerUp respecto al sprite del jugador
int continue_counter_ = 10; // Contador para poder continuar
int controller_index_ = 0; // Índice del array de mandos que utilizará para moverse
size_t demo_file_ = 0; // Indice del fichero de datos para el modo demo
size_t z_order_ = 0; // Orden de dibujado en la pantalla
float name_entry_idle_time_accumulator_ = 0.0F; // Tiempo idle acumulado para poner nombre (milisegundos)
float name_entry_total_time_accumulator_ = 0.0F; // Tiempo total acumulado poniendo nombre (milisegundos)
int step_counter_ = 0; // Cuenta los pasos para los estados en los que camina automáticamente
int credits_used_ = 0; // Indica el número de veces que ha continuado
int waiting_counter_ = 0; // Contador para el estado de espera
bool qualifies_for_high_score_ = false; // Indica si tiene una puntuación que le permite entrar en la tabla de records
bool invulnerable_ = true; // Indica si el jugador es invulnerable
bool extra_hit_ = false; // Indica si el jugador tiene un toque extra
bool power_up_ = false; // Indica si el jugador tiene activo el modo PowerUp
bool power_sprite_visible_ = false; // Indica si el sprite de power-up debe ser visible
bool in_power_up_ending_phase_ = false; // Indica si está en la fase final del power-up (alternando colores)
bool bullet_color_toggle_ = false; // Para alternar entre verde y amarillo en fase final
bool demo_ = false; // Para que el jugador sepa si está en el modo demostración
bool game_completed_ = false; // Indica si ha completado el juego
bool uses_keyboard_ = false; // Indica si usa el teclado como dispositivo de control
bool recover_sound_triggered_ = false; // Indica si ya ha sonado el sonido en el estado RECOVER
// --- Métodos internos ---
void shiftColliders(); // Actualiza el círculo de colisión a la posición del jugador
void shiftSprite(); // Recoloca el sprite
// --- Setters internos ---
void setController(int index) { controller_index_ = index; }
void setFiringState(State state) { firing_state_ = state; }
void setInvulnerableCounter(int value) { invulnerable_counter_ = value; }
void setPowerUpCounter(int value) { power_up_counter_ = value; }
void setScore(int score) { score_ = score; }
void setScoreMultiplier(float value) { score_multiplier_ = value; }
// --- Actualizadores de estado (time-based) ---
void updateInvulnerable(float delta_time); // Monitoriza el estado de invulnerabilidad
void updateContinueCounter(float delta_time); // Actualiza el contador de continue
void updateEnterNameCounter(float delta_time); // Actualiza el contador de entrar nombre
void updateShowingName(float delta_time); // Actualiza el estado SHOWING_NAME
void decNameEntryCounter(); // Decrementa el contador de entrar nombre
// --- Utilidades generales ---
void updateScoreboard(); // Actualiza el panel del marcador
void setScoreboardMode(Scoreboard::Mode mode) const; // Cambia el modo del marcador
void playSound(const std::string& name) const; // Hace sonar un sonido
[[nodiscard]] auto isRenderable() const -> bool; // Indica si se puede dibujar el objeto
void addScoreToScoreBoard() const; // Añade una puntuación a la tabla de records
// --- Sistema de disparo (nuevo - dos líneas) ---
void updateFireSystem(float delta_time); // Método principal del nuevo sistema de disparo
void updateFunctionalLine(float delta_time); // Actualiza la línea funcional (CanFire)
void updateVisualLine(float delta_time); // Actualiza la línea visual (Animaciones)
void updateFiringStateFromVisual(); // Sincroniza firing_state_ con visual_fire_state_
void transitionToRecoilingNew(); // Transición AIMING → RECOILING
void transitionToThreatPose(); // Transición RECOILING → THREAT_POSE
void transitionToNormalNew(); // Transición THREAT_POSE → NORMAL
// --- Manejadores de movimiento ---
void handlePlayingMovement(float delta_time); // Gestiona el movimiento durante el juego
void handleRecoverMovement(); // Comprueba si ha acabado la animación de recuperación
void updateStepCounter(float delta_time); // Incrementa o ajusta el contador de pasos
void setInputBasedOnPlayerId(); // Asocia las entradas de control según el jugador
// --- Manejadores de estados especiales ---
void handleRollingMovement(); // Actualiza la lógica de movimiento de "rodar"
void handleRollingBoundaryCollision(); // Detecta colisiones con límites durante rodamiento
void handleRollingGroundCollision(); // Gestiona interacción con el suelo durante rodamiento
void handleRollingStop(); // Detiene el movimiento del objeto rodante
void handleRollingBounce(); // Aplica lógica de rebote durante rodamiento
void handleContinueTimeOut(); // Gestiona tiempo de espera en pantalla "Continuar"
// --- Manejadores de transiciones de pantalla ---
void handleTitleAnimation(float delta_time); // Ejecuta animación del título
void handleLeavingScreen(float delta_time); // Lógica para salir de pantalla
void handleEnteringScreen(float delta_time); // Lógica para entrar en pantalla
void handlePlayer1Entering(float delta_time); // Entrada del Jugador 1
void handlePlayer2Entering(float delta_time); // Entrada del Jugador 2
// --- Manejadores de pantallas especiales ---
void handleCreditsMovement(float delta_time); // Movimiento en pantalla de créditos
void handleCreditsRightMovement(); // Movimiento hacia la derecha en créditos
void handleCreditsLeftMovement(); // Movimiento hacia la izquierda en créditos
void handleWaitingMovement(float delta_time); // Animación del jugador saludando
void updateWalkingStateForCredits(); // Actualiza estado de caminata en créditos
// --- Introducción de nombre ---
void handleNameCharacterAddition();
void handleNameCharacterRemoval();
void handleNameSelectionMove(Input::Action action);
void confirmNameEntry();
// --- Utilidades de animación ---
[[nodiscard]] auto computeAnimation() const -> std::pair<std::string, SDL_FlipMode>; // Calcula animación de movimiento y disparo
};

View File

@@ -0,0 +1,225 @@
// IWYU pragma: no_include <bits/std_abs.h>
#include "tabe.hpp"
#include <SDL3/SDL.h> // Para SDL_FlipMode, SDL_GetTicks
#include <algorithm> // Para max
#include <array> // Para array
#include <cstdlib> // Para rand, abs
#include <string> // Para basic_string
#include "audio.hpp" // Para Audio
#include "param.hpp" // Para Param, param, ParamGame, ParamTabe
#include "resource.hpp" // Para Resource
#include "utils.hpp" // Para Zone
// Constructor
Tabe::Tabe()
: sprite_(std::make_unique<AnimatedSprite>(Resource::get()->getTexture("tabe.png"), Resource::get()->getAnimation("tabe.ani"))),
timer_(Timer(param.tabe.min_spawn_time, param.tabe.max_spawn_time)) {}
// Actualiza la lógica (time-based)
void Tabe::update(float delta_time) {
if (enabled_ && !timer_.is_paused) {
sprite_->update(delta_time);
move(delta_time);
updateState(delta_time);
}
timer_.update();
if (timer_.shouldSpawn()) {
enable();
}
}
// Dibuja el objeto
void Tabe::render() {
if (enabled_) {
sprite_->render();
}
}
// Mueve el objeto (time-based)
void Tabe::move(float delta_time) {
const int X = static_cast<int>(x_);
speed_ += accel_ * delta_time;
x_ += speed_ * delta_time;
fly_distance_ -= std::abs(X - static_cast<int>(x_));
// Comprueba si sale por los bordes
const float MIN_X = param.game.game_area.rect.x - WIDTH;
const float MAX_X = param.game.game_area.rect.x + param.game.game_area.rect.w;
switch (destiny_) {
case Direction::TO_THE_LEFT: {
if (x_ < MIN_X) {
disable();
}
if (x_ > param.game.game_area.rect.x + param.game.game_area.rect.w - WIDTH && direction_ == Direction::TO_THE_RIGHT) {
setRandomFlyPath(Direction::TO_THE_LEFT, 80);
x_ = param.game.game_area.rect.x + param.game.game_area.rect.w - WIDTH;
}
break;
}
case Direction::TO_THE_RIGHT: {
if (x_ > MAX_X) {
disable();
}
if (x_ < param.game.game_area.rect.x && direction_ == Direction::TO_THE_LEFT) {
setRandomFlyPath(Direction::TO_THE_RIGHT, 80);
x_ = param.game.game_area.rect.x;
}
break;
}
default:
break;
}
if (fly_distance_ <= 0) {
if (waiting_counter_ > 0) {
accel_ = speed_ = 0.0F;
waiting_counter_ -= delta_time;
waiting_counter_ = std::max<float>(waiting_counter_, 0);
} else {
constexpr int CHOICES = 4;
const std::array<Direction, CHOICES> LEFT = {
Direction::TO_THE_LEFT,
Direction::TO_THE_LEFT,
Direction::TO_THE_LEFT,
Direction::TO_THE_RIGHT};
const std::array<Direction, CHOICES> RIGHT = {
Direction::TO_THE_LEFT,
Direction::TO_THE_RIGHT,
Direction::TO_THE_RIGHT,
Direction::TO_THE_RIGHT};
const Direction DIRECTION = destiny_ == Direction::TO_THE_LEFT
? LEFT[rand() % CHOICES]
: RIGHT[rand() % CHOICES];
setRandomFlyPath(DIRECTION, 20 + (rand() % 40));
}
}
shiftSprite();
}
// Habilita el objeto
void Tabe::enable() {
if (!enabled_) {
enabled_ = true;
has_bonus_ = true;
hit_counter_ = 0;
number_of_hits_ = 0;
y_ = param.game.game_area.rect.y + 20.0F;
// Establece una dirección aleatoria
destiny_ = direction_ = rand() % 2 == 0 ? Direction::TO_THE_LEFT : Direction::TO_THE_RIGHT;
// Establece la posición inicial
x_ = (direction_ == Direction::TO_THE_LEFT) ? param.game.game_area.rect.x + param.game.game_area.rect.w : param.game.game_area.rect.x - WIDTH;
// Crea una ruta de vuelo
setRandomFlyPath(direction_, 60);
shiftSprite();
}
}
// Establece un vuelo aleatorio
void Tabe::setRandomFlyPath(Direction direction, int length) {
direction_ = direction;
fly_distance_ = length;
waiting_counter_ = 0.083F + ((rand() % 15) * 0.0167F); // 5-20 frames converted to seconds (5/60 to 20/60)
Audio::get()->playSound("tabe.wav");
constexpr float SPEED = 120.0F; // 2 pixels/frame * 60fps = 120 pixels/second
switch (direction) {
case Direction::TO_THE_LEFT: {
speed_ = -1.0F * SPEED;
accel_ = -1.0F * (1 + (rand() % 10)) * 2.0F; // Converted from frame-based to seconds
sprite_->setFlip(SDL_FLIP_NONE);
break;
}
case Direction::TO_THE_RIGHT: {
speed_ = SPEED;
accel_ = (1 + (rand() % 10)) * 2.0F; // Converted from frame-based to seconds
sprite_->setFlip(SDL_FLIP_HORIZONTAL);
break;
}
default:
break;
}
}
// Establece el estado
void Tabe::setState(State state) {
if (enabled_) {
state_ = state;
switch (state) {
case State::FLY:
sprite_->setCurrentAnimation("fly");
break;
case State::HIT:
sprite_->setCurrentAnimation("hit");
hit_counter_ = 0.083F; // 5 frames converted to seconds (5/60)
++number_of_hits_;
break;
default:
break;
}
}
}
// Actualiza el estado (time-based)
void Tabe::updateState(float delta_time) {
if (state_ == State::HIT) {
hit_counter_ -= delta_time;
if (hit_counter_ <= 0) {
setState(State::FLY);
}
}
}
// Intenta obtener el bonus
auto Tabe::tryToGetBonus() -> bool {
if (has_bonus_ && rand() % std::max(1, 15 - number_of_hits_) == 0) {
has_bonus_ = false;
return true;
}
return false;
}
// Actualiza el temporizador
void Tabe::updateTimer() {
timer_.current_time = SDL_GetTicks();
timer_.delta_time = timer_.current_time - timer_.last_time;
timer_.last_time = timer_.current_time;
}
// Deshabilita el objeto
void Tabe::disable() {
enabled_ = false;
timer_.reset();
}
// Detiene/activa el timer
void Tabe::pauseTimer(bool value) {
timer_.setPaused(value);
}
// Deshabilita el spawning permanentemente
void Tabe::disableSpawning() {
timer_.setSpawnDisabled(true);
}
// Habilita el spawning nuevamente
void Tabe::enableSpawning() {
timer_.setSpawnDisabled(false);
}

View File

@@ -0,0 +1,150 @@
#pragma once
#include <SDL3/SDL.h> // Para Uint32, SDL_GetTicks, SDL_FRect
#include <cstdlib> // Para rand
#include <memory> // Para unique_ptr
#include "animated_sprite.hpp" // Para AnimatedSprite
// --- Clase Tabe ---
class Tabe {
public:
// --- Enumeraciones para dirección y estado ---
enum class Direction : int {
TO_THE_LEFT = 0,
TO_THE_RIGHT = 1,
};
enum class State : int {
FLY = 0,
HIT = 1,
};
// --- Constructores y destructor ---
Tabe();
~Tabe() = default;
// --- Métodos principales ---
void update(float delta_time); // Actualiza la lógica (time-based)
void render(); // Dibuja el objeto
void enable(); // Habilita el objeto
void setState(State state); // Establece el estado
auto tryToGetBonus() -> bool; // Intenta obtener el bonus
void pauseTimer(bool value); // Detiene/activa el timer
void disableSpawning(); // Deshabilita el spawning permanentemente
void enableSpawning(); // Habilita el spawning nuevamente
// --- Getters ---
auto getCollider() -> SDL_FRect& { return sprite_->getRect(); } // Obtiene el área de colisión
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; } // Indica si el objeto está activo
private:
// --- Constantes ---
static constexpr int WIDTH = 32;
static constexpr int HEIGHT = 32;
// --- Estructura para el temporizador del Tabe ---
struct Timer {
private:
static constexpr Uint32 MINUTES_TO_MILLISECONDS = 60000; // Factor de conversión de minutos a milisegundos
public:
Uint32 time_until_next_spawn; // Tiempo restante para la próxima aparición
Uint32 min_spawn_time; // Tiempo mínimo entre apariciones (en milisegundos)
Uint32 max_spawn_time; // Tiempo máximo entre apariciones (en milisegundos)
Uint32 current_time; // Tiempo actual
Uint32 delta_time; // Diferencia de tiempo desde la última actualización
Uint32 last_time; // Tiempo de la última actualización
bool is_paused{false}; // Indica si el temporizador está pausado (por pausa de juego)
bool spawn_disabled{false}; // Indica si el spawning está deshabilitado permanentemente
// Constructor - los parámetros min_time y max_time están en mintos
Timer(float min_time, float max_time)
: min_spawn_time(static_cast<Uint32>(min_time * MINUTES_TO_MILLISECONDS)),
max_spawn_time(static_cast<Uint32>(max_time * MINUTES_TO_MILLISECONDS)),
current_time(SDL_GetTicks()) {
reset();
}
// Restablece el temporizador con un nuevo tiempo hasta la próxima aparición
void reset() {
Uint32 range = max_spawn_time - min_spawn_time;
time_until_next_spawn = min_spawn_time + (rand() % (range + 1));
last_time = SDL_GetTicks();
}
// Actualiza el temporizador, decrementando el tiempo hasta la próxima aparición
void update() {
current_time = SDL_GetTicks();
// Solo actualizar si no está pausado (ni por juego ni por spawn deshabilitado)
if (!is_paused && !spawn_disabled) {
delta_time = current_time - last_time;
if (time_until_next_spawn > delta_time) {
time_until_next_spawn -= delta_time;
} else {
time_until_next_spawn = 0;
}
}
// Siempre actualizar last_time para evitar saltos de tiempo al despausar
last_time = current_time;
}
// Pausa o reanuda el temporizador
void setPaused(bool paused) {
if (is_paused != paused) {
is_paused = paused;
// Al despausar, actualizar last_time para evitar saltos
if (!paused) {
last_time = SDL_GetTicks();
}
}
}
// Pausa o reanuda el spawning
void setSpawnDisabled(bool disabled) {
if (spawn_disabled != disabled) {
spawn_disabled = disabled;
// Al reactivar, actualizar last_time para evitar saltos
if (!disabled) {
last_time = SDL_GetTicks();
}
}
}
// Indica si el temporizador ha finalizado
[[nodiscard]] auto shouldSpawn() const -> bool {
return time_until_next_spawn == 0 && !is_paused && !spawn_disabled;
}
};
// --- Objetos y punteros ---
std::unique_ptr<AnimatedSprite> sprite_; // Sprite con los gráficos y animaciones
// --- Variables de estado ---
float x_ = 0; // Posición X
float y_ = 0; // Posición Y
float speed_ = 0.0F; // Velocidad de movimiento
float accel_ = 0.0F; // Aceleración
int fly_distance_ = 0; // Distancia de vuelo
float waiting_counter_ = 0; // Tiempo que pasa quieto
bool enabled_ = false; // Indica si el objeto está activo
Direction direction_ = Direction::TO_THE_LEFT; // Dirección actual
Direction destiny_ = Direction::TO_THE_LEFT; // Destino
State state_ = State::FLY; // Estado actual
float hit_counter_ = 0; // Contador para el estado HIT
int number_of_hits_ = 0; // Cantidad de disparos recibidos
bool has_bonus_ = true; // Indica si aún tiene el bonus para soltar
Timer timer_; // Temporizador para gestionar la aparición
// --- Métodos internos ---
void move(float delta_time); // Mueve el objeto (time-based)
void shiftSprite() { sprite_->setPos(x_, y_); } // Actualiza la posición del sprite
void setRandomFlyPath(Direction direction, int length); // Establece un vuelo aleatorio
void updateState(float delta_time); // Actualiza el estado (time-based)
void updateTimer(); // Actualiza el temporizador
void disable(); // Deshabilita el objeto
};

View File

@@ -0,0 +1,424 @@
#include "balloon_formations.hpp"
#include <algorithm> // Para max, min, copy
#include <array> // Para array
#include <cctype> // Para isdigit
#include <cstddef> // Para size_t
#include <exception> // Para exception
#include <fstream> // Para basic_istream, basic_ifstream, ifstream, istringstream
#include <iterator> // Para reverse_iterator
#include <map> // Para map, operator==, _Rb_tree_iterator
#include <sstream> // Para basic_istringstream
#include <string> // Para string, char_traits, allocator, operator==, stoi, getline, operator<=>, basic_string
#include <utility> // Para std::cmp_less
#include "asset.hpp" // Para Asset
#include "balloon.hpp" // Para Balloon
#include "param.hpp" // Para Param, ParamGame, param
#include "utils.hpp" // Para Zone, BLOCK
void BalloonFormations::initFormations() {
// Calcular posiciones base
const int DEFAULT_POS_Y = param.game.play_area.rect.h - BALLOON_SPAWN_HEIGHT;
const int X3_0 = param.game.play_area.rect.x;
const int X3_25 = param.game.play_area.first_quarter_x - (Balloon::WIDTH.at(3) / 2);
const int X3_75 = param.game.play_area.third_quarter_x - (Balloon::WIDTH.at(3) / 2);
const int X3_100 = param.game.play_area.rect.w - Balloon::WIDTH.at(3);
const int X2_0 = param.game.play_area.rect.x;
const int X2_100 = param.game.play_area.rect.w - Balloon::WIDTH.at(2);
const int X1_0 = param.game.play_area.rect.x;
const int X1_100 = param.game.play_area.rect.w - Balloon::WIDTH.at(1);
const int X0_0 = param.game.play_area.rect.x;
const int X0_50 = param.game.play_area.center_x - (Balloon::WIDTH.at(0) / 2);
const int X0_100 = param.game.play_area.rect.w - Balloon::WIDTH.at(0);
// Mapa de variables para reemplazar en el archivo
std::map<std::string, float> variables = {
{"X0_0", X0_0},
{"X0_50", X0_50},
{"X0_100", X0_100},
{"X1_0", X1_0},
{"X1_100", X1_100},
{"X2_0", X2_0},
{"X2_100", X2_100},
{"X3_0", X3_0},
{"X3_100", X3_100},
{"X3_25", X3_25},
{"X3_75", X3_75},
{"DEFAULT_POS_Y", DEFAULT_POS_Y},
{"RIGHT", Balloon::VELX_POSITIVE},
{"LEFT", Balloon::VELX_NEGATIVE}};
if (!loadFormationsFromFile(Asset::get()->getPath("formations.txt"), variables)) {
// Fallback: cargar formaciones por defecto si falla la carga del archivo
loadDefaultFormations();
}
}
auto BalloonFormations::loadFormationsFromFile(const std::string& filename, const std::map<std::string, float>& variables) -> bool {
std::ifstream file(filename);
if (!file.is_open()) {
return false;
}
std::string line;
int current_formation = -1;
std::vector<SpawnParams> current_params;
while (std::getline(file, line)) {
// Eliminar espacios en blanco al inicio y final
line = trim(line);
// Saltar líneas vacías y comentarios
if (line.empty() || line.at(0) == '#') {
continue;
}
// Verificar si es una nueva formación
if (line.starts_with("formation:")) {
// Guardar formación anterior si existe
if (current_formation >= 0 && !current_params.empty()) {
formations_.emplace_back(current_params);
}
// Iniciar nueva formación
current_formation = std::stoi(line.substr(10));
current_params.clear();
continue;
}
// Procesar línea de parámetros de balloon
if (current_formation >= 0) {
auto params = parseBalloonLine(line, variables);
if (params.has_value()) {
current_params.push_back(params.value());
}
}
}
// Guardar última formación
if (current_formation >= 0 && !current_params.empty()) {
formations_.emplace_back(current_params);
}
// Crear variantes flotantes (formaciones 50-99)
createFloaterVariants();
#ifdef _DEBUG
// Añadir formación de prueba
addTestFormation();
#endif
file.close();
return true;
}
auto BalloonFormations::parseBalloonLine(const std::string& line, const std::map<std::string, float>& variables) -> std::optional<SpawnParams> {
std::istringstream iss(line);
std::string token;
std::vector<std::string> tokens;
// Dividir por comas
while (std::getline(iss, token, ',')) {
tokens.push_back(trim(token));
}
if (tokens.size() != 7) {
return std::nullopt;
}
try {
int x = evaluateExpression(tokens.at(0), variables);
int offset = evaluateExpression(tokens.at(1), variables);
int y = evaluateExpression(tokens.at(2), variables);
float vel_x = evaluateExpression(tokens.at(3), variables);
Balloon::Type type = (tokens.at(4) == "BALLOON") ? Balloon::Type::BALLOON : Balloon::Type::FLOATER;
Balloon::Size size;
if (tokens.at(5) == "SMALL") {
size = Balloon::Size::SMALL;
offset = offset * (Balloon::WIDTH.at(0) + 1);
} else if (tokens.at(5) == "MEDIUM") {
size = Balloon::Size::MEDIUM;
offset = offset * (Balloon::WIDTH.at(1) + 1);
} else if (tokens.at(5) == "LARGE") {
size = Balloon::Size::LARGE;
offset = offset * (Balloon::WIDTH.at(2) + 1);
} else if (tokens.at(5) == "EXTRALARGE") {
size = Balloon::Size::EXTRALARGE;
offset = offset * (Balloon::WIDTH.at(3) + 1);
} else {
return std::nullopt;
}
float creation_time = CREATION_TIME + evaluateExpression(tokens.at(6), variables); // Base time + offset from formations.txt
return SpawnParams(x + offset, y, vel_x, type, size, creation_time);
} catch (const std::exception&) {
return std::nullopt;
}
}
auto BalloonFormations::evaluateExpression(const std::string& expr, const std::map<std::string, float>& variables) -> float {
std::string trimmed_expr = trim(expr);
// Si es un número directo
if ((std::isdigit(trimmed_expr.at(0)) != 0) || (trimmed_expr.at(0) == '-' && trimmed_expr.length() > 1)) {
return std::stof(trimmed_expr);
}
// Si es una variable simple
if (variables.contains(trimmed_expr)) {
return variables.at(trimmed_expr);
}
// Evaluación de expresiones simples (suma, resta, multiplicación)
return evaluateSimpleExpression(trimmed_expr, variables);
}
auto BalloonFormations::evaluateSimpleExpression(const std::string& expr, const std::map<std::string, float>& variables) -> float {
// Buscar operadores (+, -, *, /)
for (size_t i = 1; i < expr.length(); ++i) {
char op = expr.at(i);
if (op == '+' || op == '-' || op == '*' || op == '/') {
std::string left = trim(expr.substr(0, i));
std::string right = trim(expr.substr(i + 1));
int left_val = evaluateExpression(left, variables);
int right_val = evaluateExpression(right, variables);
switch (op) {
case '+':
return left_val + right_val;
case '-':
return left_val - right_val;
case '*':
return left_val * right_val;
case '/':
return right_val != 0 ? left_val / right_val : 0;
}
}
}
// Si no se encuentra operador, intentar como variable o número
return variables.contains(expr) ? variables.at(expr) : std::stof(expr);
}
auto BalloonFormations::trim(const std::string& str) -> std::string {
size_t start = str.find_first_not_of(" \t\r\n");
if (start == std::string::npos) {
return "";
}
size_t end = str.find_last_not_of(" \t\r\n");
return str.substr(start, end - start + 1);
}
void BalloonFormations::createFloaterVariants() {
formations_.resize(100);
// Crear variantes flotantes de las primeras 50 formaciones
for (size_t k = 0; k < 50 && k < formations_.size(); k++) {
std::vector<SpawnParams> floater_params;
floater_params.reserve(formations_.at(k).balloons.size());
for (const auto& original : formations_.at(k).balloons) {
floater_params.emplace_back(original.x, original.y, original.vel_x, Balloon::Type::FLOATER, original.size, original.creation_counter);
}
formations_.at(k + 50) = Formation(floater_params);
}
}
#ifdef _DEBUG
void BalloonFormations::addTestFormation() {
std::vector<SpawnParams> test_params = {
{10, -BLOCK, 0, Balloon::Type::FLOATER, Balloon::Size::SMALL, 3.334F}, // 200 frames ÷ 60fps = 3.334s
{50, -BLOCK, 0, Balloon::Type::FLOATER, Balloon::Size::MEDIUM, 3.334F}, // 200 frames ÷ 60fps = 3.334s
{90, -BLOCK, 0, Balloon::Type::FLOATER, Balloon::Size::LARGE, 3.334F}, // 200 frames ÷ 60fps = 3.334s
{140, -BLOCK, 0, Balloon::Type::FLOATER, Balloon::Size::EXTRALARGE, 3.334F}}; // 200 frames ÷ 60fps = 3.334s
formations_.at(99) = Formation(test_params);
}
#endif
void BalloonFormations::loadDefaultFormations() {
// Código de fallback con algunas formaciones básicas hardcodeadas
// para que el juego funcione aunque falle la carga del archivo
const int DEFAULT_POS_Y = param.game.play_area.rect.h - BALLOON_SPAWN_HEIGHT;
const int X4_0 = param.game.play_area.rect.x;
const int X4_100 = param.game.play_area.rect.w - Balloon::WIDTH.at(3);
// Formación básica #00
std::vector<SpawnParams> basic_formation = {
SpawnParams(X4_0, DEFAULT_POS_Y, Balloon::VELX_POSITIVE, Balloon::Type::BALLOON, Balloon::Size::EXTRALARGE, DEFAULT_CREATION_TIME),
SpawnParams(X4_100, DEFAULT_POS_Y, Balloon::VELX_NEGATIVE, Balloon::Type::BALLOON, Balloon::Size::EXTRALARGE, DEFAULT_CREATION_TIME)};
formations_.emplace_back(basic_formation);
}
// Nuevas implementaciones para el sistema de pools flexible
void BalloonFormations::initFormationPools() {
// Intentar cargar pools desde archivo
if (!loadPoolsFromFile(Asset::get()->getPath("pools.txt"))) {
// Fallback: cargar pools por defecto si falla la carga del archivo
loadDefaultPools();
}
}
auto BalloonFormations::loadPoolsFromFile(const std::string& filename) -> bool {
std::ifstream file(filename);
if (!file.is_open()) {
return false;
}
std::string line;
pools_.clear(); // Limpiar pools existentes
// Map temporal para ordenar los pools por ID
std::map<int, std::vector<int>> temp_pools;
while (std::getline(file, line)) {
// Eliminar espacios en blanco al inicio y final
line = trim(line);
// Saltar líneas vacías y comentarios
if (line.empty() || line.at(0) == '#') {
continue;
}
// Procesar línea de pool
auto pool_data = parsePoolLine(line);
if (pool_data.has_value()) {
temp_pools[pool_data->first] = pool_data->second;
}
}
file.close();
// Convertir el map ordenado a vector
// Redimensionar el vector para el pool con ID más alto
if (!temp_pools.empty()) {
int max_pool_id = temp_pools.rbegin()->first;
pools_.resize(max_pool_id + 1);
for (const auto& [pool_id, formations] : temp_pools) {
pools_[pool_id] = formations;
}
}
return !pools_.empty();
}
auto BalloonFormations::parsePoolLine(const std::string& line) -> std::optional<std::pair<int, std::vector<int>>> {
// Formato esperado: "POOL: 0 FORMATIONS: 1, 2, 14, 3, 5, 5"
// Buscar "POOL:"
size_t pool_pos = line.find("POOL:");
if (pool_pos == std::string::npos) {
return std::nullopt;
}
// Buscar "FORMATIONS:"
size_t formations_pos = line.find("FORMATIONS:");
if (formations_pos == std::string::npos) {
return std::nullopt;
}
try {
// Extraer el ID del pool
std::string pool_id_str = trim(line.substr(pool_pos + 5, formations_pos - pool_pos - 5));
int pool_id = std::stoi(pool_id_str);
// Extraer la lista de formaciones
std::string formations_str = trim(line.substr(formations_pos + 11));
std::vector<int> formation_ids;
// Parsear la lista de formaciones separadas por comas
std::istringstream iss(formations_str);
std::string token;
while (std::getline(iss, token, ',')) {
token = trim(token);
if (!token.empty()) {
int formation_id = std::stoi(token);
// Validar que el ID de formación existe
if (formation_id >= 0 && std::cmp_less(formation_id, formations_.size())) {
formation_ids.push_back(formation_id);
}
}
}
if (!formation_ids.empty()) {
return std::make_pair(pool_id, formation_ids);
}
} catch (const std::exception&) {
// Error de conversión o parsing
return std::nullopt;
}
return std::nullopt;
}
void BalloonFormations::loadDefaultPools() {
// Pools por defecto como fallback
pools_.clear();
// Crear algunos pools básicos si tenemos formaciones disponibles
if (formations_.empty()) {
return;
}
size_t total_formations = formations_.size();
// Pool 0: Primeras 10 formaciones (o las que haya disponibles)
Pool pool0;
for (size_t i = 0; i < std::min(static_cast<size_t>(10), total_formations); ++i) {
pool0.push_back(static_cast<int>(i));
}
if (!pool0.empty()) {
pools_.push_back(pool0);
}
// Pool 1: Formaciones 10-19 (si existen)
if (total_formations > 10) {
Pool pool1;
for (size_t i = 10; i < std::min(static_cast<size_t>(20), total_formations); ++i) {
pool1.push_back(static_cast<int>(i));
}
if (!pool1.empty()) {
pools_.push_back(pool1);
}
}
// Pool 2: Mix de formaciones normales y floaters (50+)
if (total_formations > 50) {
Pool pool2;
// Agregar algunas formaciones básicas
for (size_t i = 0; i < std::min(static_cast<size_t>(5), total_formations); ++i) {
pool2.push_back(static_cast<int>(i));
}
// Agregar algunas floaters si existen
for (size_t i = 50; i < std::min(static_cast<size_t>(55), total_formations); ++i) {
pool2.push_back(static_cast<int>(i));
}
if (!pool2.empty()) {
pools_.push_back(pool2);
}
}
// Pool 3: Solo floaters (si existen formaciones 50+)
if (total_formations > 50) {
Pool pool3;
for (size_t i = 50; i < std::min(static_cast<size_t>(70), total_formations); ++i) {
pool3.push_back(static_cast<int>(i));
}
if (!pool3.empty()) {
pools_.push_back(pool3);
}
}
}

View File

@@ -0,0 +1,110 @@
#pragma once
#include <cstddef> // Para size_t
#include <iterator> // Para pair
#include <map> // Para map
#include <optional> // Para optional
#include <string> // Para string
#include <utility> // Para pair
#include <vector> // Para vector
#include "balloon.hpp" // for Balloon
// --- Clase BalloonFormations ---
class BalloonFormations {
public:
// --- Estructuras ---
struct SpawnParams {
float x = 0; // Posición en el eje X donde crear el globo
float y = 0; // Posición en el eje Y donde crear el globo
float vel_x = 0.0F; // Velocidad inicial en el eje X
Balloon::Type type = Balloon::Type::BALLOON; // Tipo de globo
Balloon::Size size = Balloon::Size::SMALL; // Tamaño de globo
float creation_counter = 0.0F; // Temporizador para la creación del globo
// Constructor por defecto
SpawnParams() = default;
// Constructor con parámetros
SpawnParams(float x, float y, float vel_x, Balloon::Type type, Balloon::Size size, float creation_counter)
: x(x),
y(y),
vel_x(vel_x),
type(type),
size(size),
creation_counter(creation_counter) {}
};
struct Formation {
std::vector<SpawnParams> balloons; // Vector con todas las inicializaciones de los globos de la formación
// Constructor con parámetros
Formation(const std::vector<SpawnParams>& spawn_params)
: balloons(spawn_params) {}
// Constructor por defecto
Formation() = default;
};
// --- Types ---
using Pool = std::vector<int>; // Vector de índices a formaciones
// --- Constructor y destructor ---
BalloonFormations() {
initFormations();
initFormationPools();
}
~BalloonFormations() = default;
// --- Getters ---
auto getPool(int pool_id) -> const Pool& {
return pools_.at(pool_id);
}
auto getFormationFromPool(int pool_id, int formation_index) -> const Formation& {
int formation_id = pools_.at(pool_id).at(formation_index);
return formations_.at(formation_id);
}
[[nodiscard]] auto getFormation(int formation_id) const -> const Formation& {
return formations_.at(formation_id);
}
// --- Nuevos getters para información de pools ---
[[nodiscard]] auto getPoolCount() const -> size_t {
return pools_.size();
}
[[nodiscard]] auto getPoolSize(int pool_id) const -> size_t {
return pools_.at(pool_id).size();
}
private:
// --- Constantes ---
static constexpr int BALLOON_SPAWN_HEIGHT = 208; // Altura desde el suelo en la que aparecen los globos
static constexpr float CREATION_TIME = 5.0F; // Tiempo base de creación de los globos en segundos (300 frames ÷ 60fps = 5.0s)
static constexpr float DEFAULT_CREATION_TIME = 3.334F; // Tiempo base de creación de los globos en segundos (200 frames ÷ 60fps = 3.334s)
// --- Variables ---
std::vector<Formation> formations_; // Vector con todas las formaciones disponibles
std::vector<Pool> pools_; // Vector de pools, cada pool contiene índices a formaciones
// --- Métodos internos ---
void initFormations(); // Inicializa la lista principal de formaciones de globos disponibles
void initFormationPools(); // Carga los pools desde archivo de configuración
auto loadFormationsFromFile(const std::string& filename, const std::map<std::string, float>& variables) -> bool;
auto parseBalloonLine(const std::string& line, const std::map<std::string, float>& variables) -> std::optional<SpawnParams>;
auto loadPoolsFromFile(const std::string& filename) -> bool; // Nueva función para cargar pools
auto parsePoolLine(const std::string& line) -> std::optional<std::pair<int, std::vector<int>>>; // Nueva función para parsear líneas de pools
auto evaluateExpression(const std::string& expr, const std::map<std::string, float>& variables) -> float;
auto evaluateSimpleExpression(const std::string& expr, const std::map<std::string, float>& variables) -> float;
static auto trim(const std::string& str) -> std::string;
void createFloaterVariants();
void loadDefaultFormations();
void loadDefaultPools(); // Nueva función para pools por defecto
// --- Depuración (solo en modo DEBUG) ---
#ifdef _DEBUG
void addTestFormation();
#endif
};

View File

@@ -0,0 +1,427 @@
#include "balloon_manager.hpp"
#include <algorithm> // Para remove_if
#include <array>
#include <cstdlib> // Para rand
#include <numeric> // Para accumulate
#include "balloon.hpp" // Para Balloon, Balloon::SCORE.at( )ALLOON_VELX...
#include "balloon_formations.hpp" // Para BalloonFormationParams, BalloonForma...
#include "color.hpp" // Para Zone, Color, flash_color
#include "explosions.hpp" // Para Explosions
#include "param.hpp" // Para Param, ParamGame, param
#include "resource.hpp" // Para Resource
#include "screen.hpp" // Para Screen
#include "stage_interface.hpp" // Para IStageInfo
#include "utils.hpp"
// Constructor
BalloonManager::BalloonManager(IStageInfo* stage_info)
: explosions_(std::make_unique<Explosions>()),
balloon_formations_(std::make_unique<BalloonFormations>()),
stage_info_(stage_info) { init(); }
// Inicializa
void BalloonManager::init() {
// Limpia
balloon_textures_.clear();
balloon_animations_.clear();
explosions_textures_.clear();
explosions_animations_.clear();
// Texturas - Globos
balloon_textures_.emplace_back(Resource::get()->getTexture("balloon0.png"));
balloon_textures_.emplace_back(Resource::get()->getTexture("balloon1.png"));
balloon_textures_.emplace_back(Resource::get()->getTexture("balloon2.png"));
balloon_textures_.emplace_back(Resource::get()->getTexture("balloon3.png"));
balloon_textures_.emplace_back(Resource::get()->getTexture("powerball.png"));
// Animaciones -- Globos
balloon_animations_.emplace_back(Resource::get()->getAnimation("balloon0.ani"));
balloon_animations_.emplace_back(Resource::get()->getAnimation("balloon1.ani"));
balloon_animations_.emplace_back(Resource::get()->getAnimation("balloon2.ani"));
balloon_animations_.emplace_back(Resource::get()->getAnimation("balloon3.ani"));
balloon_animations_.emplace_back(Resource::get()->getAnimation("powerball.ani"));
// Texturas - Explosiones
explosions_textures_.emplace_back(Resource::get()->getTexture("explosion0.png"));
explosions_textures_.emplace_back(Resource::get()->getTexture("explosion1.png"));
explosions_textures_.emplace_back(Resource::get()->getTexture("explosion2.png"));
explosions_textures_.emplace_back(Resource::get()->getTexture("explosion3.png"));
// Animaciones -- Explosiones
explosions_animations_.emplace_back(Resource::get()->getAnimation("explosion0.ani"));
explosions_animations_.emplace_back(Resource::get()->getAnimation("explosion1.ani"));
explosions_animations_.emplace_back(Resource::get()->getAnimation("explosion2.ani"));
explosions_animations_.emplace_back(Resource::get()->getAnimation("explosion3.ani"));
// Añade texturas
explosions_->addTexture(0, explosions_textures_.at(0), explosions_animations_.at(0));
explosions_->addTexture(1, explosions_textures_.at(1), explosions_animations_.at(1));
explosions_->addTexture(2, explosions_textures_.at(2), explosions_animations_.at(2));
explosions_->addTexture(3, explosions_textures_.at(3), explosions_animations_.at(3));
}
// Actualiza (time-based)
void BalloonManager::update(float delta_time) {
for (const auto& balloon : balloons_) {
balloon->update(delta_time);
}
updateBalloonDeployCounter(delta_time);
explosions_->update(delta_time);
}
// Renderiza los objetos
void BalloonManager::render() {
for (auto& balloon : balloons_) {
balloon->render();
}
explosions_->render();
}
// Crea una formación de globos
void BalloonManager::deployRandomFormation(int stage) {
// Solo despliega una formación enemiga si el timer ha llegado a cero
if (balloon_deploy_counter_ <= 0.0F) {
// En este punto se decide entre crear una powerball o una formación enemiga
if ((rand() % 100 < 15) && (canPowerBallBeCreated())) {
createPowerBall(); // Crea una powerball
balloon_deploy_counter_ = POWERBALL_DEPLOY_DELAY; // Resetea con pequeño retraso
} else {
// Decrementa el contador de despliegues de globos necesarios para la siguiente PowerBall
if (power_ball_counter_ > 0) {
--power_ball_counter_;
}
// Elige una formación enemiga la azar
const auto NUM_FORMATIONS = balloon_formations_->getPoolSize(stage);
int formation_id = rand() % NUM_FORMATIONS;
// Evita repetir la ultima formación enemiga desplegada
if (formation_id == last_balloon_deploy_) {
++formation_id %= NUM_FORMATIONS;
}
last_balloon_deploy_ = formation_id;
// Crea los globos de la formación
const auto BALLOONS = balloon_formations_->getFormationFromPool(stage, formation_id).balloons;
for (auto balloon : BALLOONS) {
Balloon::Config config = {
.x = balloon.x,
.y = balloon.y,
.type = balloon.type,
.size = balloon.size,
.vel_x = balloon.vel_x,
.game_tempo = balloon_speed_,
.creation_counter = creation_time_enabled_ ? balloon.creation_counter : 0.0F};
createBalloon(config);
}
// Reinicia el timer para el próximo despliegue
balloon_deploy_counter_ = DEFAULT_BALLOON_DEPLOY_DELAY;
}
}
}
// Crea una formación de globos específica
void BalloonManager::deployFormation(int formation_id) {
const auto BALLOONS = balloon_formations_->getFormation(formation_id).balloons;
for (auto balloon : BALLOONS) {
Balloon::Config config = {
.x = balloon.x,
.y = balloon.y,
.type = balloon.type,
.size = balloon.size,
.vel_x = balloon.vel_x,
.game_tempo = balloon_speed_,
.creation_counter = balloon.creation_counter};
createBalloon(config);
}
}
// Crea una formación de globos específica a una altura determinada
void BalloonManager::deployFormation(int formation_id, float y) {
const auto BALLOONS = balloon_formations_->getFormation(formation_id).balloons;
for (auto balloon : BALLOONS) {
Balloon::Config config = {
.x = balloon.x,
.y = y,
.type = balloon.type,
.size = balloon.size,
.vel_x = balloon.vel_x,
.game_tempo = balloon_speed_,
.creation_counter = balloon.creation_counter};
createBalloon(config);
}
}
// Vacia del vector de globos los globos que ya no sirven
void BalloonManager::freeBalloons() {
std::erase_if(balloons_, [](const auto& balloon) -> auto {
return !balloon->isEnabled();
});
}
// Actualiza el timer de despliegue de globos (time-based)
void BalloonManager::updateBalloonDeployCounter(float delta_time) {
// DeltaTime en segundos - timer decrementa hasta llegar a cero
balloon_deploy_counter_ -= delta_time;
}
// Indica si se puede crear una powerball
auto BalloonManager::canPowerBallBeCreated() -> bool { return (!power_ball_enabled_) && (calculateScreenPower() > Balloon::POWERBALL_SCREENPOWER_MINIMUM) && (power_ball_counter_ == 0); }
// Calcula el poder actual de los globos en pantalla
auto BalloonManager::calculateScreenPower() -> int {
return std::accumulate(balloons_.begin(), balloons_.end(), 0, [](int sum, const auto& balloon) -> auto { return sum + (balloon->isEnabled() ? balloon->getPower() : 0); });
}
// Crea un globo nuevo en el vector de globos
auto BalloonManager::createBalloon(Balloon::Config config) -> std::shared_ptr<Balloon> {
if (can_deploy_balloons_) {
const int INDEX = static_cast<int>(config.size);
config.play_area = play_area_;
config.texture = balloon_textures_.at(INDEX);
config.animation = balloon_animations_.at(INDEX);
config.sound.enabled = sound_enabled_;
config.sound.bouncing_enabled = bouncing_sound_enabled_;
config.sound.poping_enabled = poping_sound_enabled_;
balloons_.emplace_back(std::make_shared<Balloon>(config));
return balloons_.back();
}
return nullptr;
}
// Crea un globo a partir de otro globo
void BalloonManager::createChildBalloon(const std::shared_ptr<Balloon>& parent_balloon, const std::string& direction) {
if (can_deploy_balloons_) {
// Calcula parametros
const int PARENT_HEIGHT = parent_balloon->getHeight();
const int CHILD_HEIGHT = Balloon::WIDTH.at(static_cast<size_t>(parent_balloon->getSize()) - 1);
const int CHILD_WIDTH = CHILD_HEIGHT;
const float X = direction == "LEFT" ? parent_balloon->getPosX() + (parent_balloon->getWidth() / 3) : parent_balloon->getPosX() + (2 * (parent_balloon->getWidth() / 3));
const float MIN_X = play_area_.x;
const float MAX_X = play_area_.w - CHILD_WIDTH;
Balloon::Config config = {
.x = std::clamp(X - (CHILD_WIDTH / 2), MIN_X, MAX_X),
.y = parent_balloon->getPosY() + ((PARENT_HEIGHT - CHILD_HEIGHT) / 2),
.type = parent_balloon->getType(),
.size = static_cast<Balloon::Size>(static_cast<int>(parent_balloon->getSize()) - 1),
.vel_x = direction == "LEFT" ? Balloon::VELX_NEGATIVE : Balloon::VELX_POSITIVE,
.game_tempo = balloon_speed_,
.creation_counter = 0};
// Crea el globo hijo
auto child_balloon = createBalloon(config);
// Configura el globo hijo
if (child_balloon != nullptr) {
// Establece parametros
constexpr float VEL_Y_BALLOON_PER_S = -150.0F;
switch (child_balloon->getType()) {
case Balloon::Type::BALLOON: {
child_balloon->setVelY(VEL_Y_BALLOON_PER_S);
break;
}
case Balloon::Type::FLOATER: {
child_balloon->setVelY(Balloon::VELX_NEGATIVE * 2.0F);
break;
}
default:
break;
}
// Herencia de estados
if (parent_balloon->isStopped()) { child_balloon->stop(); }
if (parent_balloon->isUsingReversedColor()) { child_balloon->useReverseColor(); }
}
}
}
// Crea una PowerBall
void BalloonManager::createPowerBall() {
if (can_deploy_balloons_) {
constexpr int VALUES = 6;
const int LUCK = rand() % VALUES;
const float LEFT = param.game.play_area.rect.x;
const float CENTER = param.game.play_area.center_x - (Balloon::WIDTH.at(4) / 2);
const float RIGHT = param.game.play_area.rect.w - Balloon::WIDTH.at(4);
const std::array<float, VALUES> POS_X = {LEFT, LEFT, CENTER, CENTER, RIGHT, RIGHT};
const std::array<float, VALUES> VEL_X = {Balloon::VELX_POSITIVE, Balloon::VELX_POSITIVE, Balloon::VELX_POSITIVE, Balloon::VELX_NEGATIVE, Balloon::VELX_NEGATIVE, Balloon::VELX_NEGATIVE};
Balloon::Config config = {
.x = POS_X.at(LUCK),
.y = -Balloon::WIDTH.at(4),
.type = Balloon::Type::POWERBALL,
.size = Balloon::Size::EXTRALARGE,
.vel_x = VEL_X.at(LUCK),
.game_tempo = balloon_speed_,
.creation_counter = 0,
.play_area = play_area_,
.texture = balloon_textures_.at(4),
.animation = balloon_animations_.at(4),
.sound = {
.bouncing_enabled = bouncing_sound_enabled_,
.poping_enabled = poping_sound_enabled_,
.enabled = sound_enabled_}};
balloons_.emplace_back(std::make_unique<Balloon>(config));
balloons_.back()->setInvulnerable(true);
power_ball_enabled_ = true;
power_ball_counter_ = Balloon::POWERBALL_COUNTER;
}
}
// Establece la velocidad de los globos
void BalloonManager::setBalloonSpeed(float speed) {
balloon_speed_ = speed;
for (auto& balloon : balloons_) {
balloon->setGameTempo(speed);
}
}
// Explosiona un globo. Lo destruye y crea otros dos si es el caso
auto BalloonManager::popBalloon(const std::shared_ptr<Balloon>& balloon) -> int {
stage_info_->addPower(1);
int score = 0;
if (balloon->getType() == Balloon::Type::POWERBALL) {
balloon->pop(true);
score = destroyAllBalloons();
power_ball_enabled_ = false;
balloon_deploy_counter_ = BALLOON_POP_DELAY; // Resetea con retraso
} else {
score = balloon->getScore();
if (balloon->getSize() != Balloon::Size::SMALL) {
createChildBalloon(balloon, "LEFT");
createChildBalloon(balloon, "RIGHT");
}
// Agrega la explosión y elimina el globo
explosions_->add(balloon->getPosX(), balloon->getPosY(), static_cast<int>(balloon->getSize()));
balloon->pop(true);
}
return score;
}
// Explosiona un globo. Lo destruye = no crea otros globos
auto BalloonManager::destroyBalloon(std::shared_ptr<Balloon>& balloon) -> int {
int score = 0;
// Calcula la puntuación y el poder que generaria el globo en caso de romperlo a él y a sus hijos
switch (balloon->getSize()) {
case Balloon::Size::EXTRALARGE:
score = Balloon::SCORE.at(3) + (2 * Balloon::SCORE.at(2)) + (4 * Balloon::SCORE.at(1)) + (8 * Balloon::SCORE.at(0));
break;
case Balloon::Size::LARGE:
score = Balloon::SCORE.at(2) + (2 * Balloon::SCORE.at(1)) + (4 * Balloon::SCORE.at(0));
break;
case Balloon::Size::MEDIUM:
score = Balloon::SCORE.at(1) + (2 * Balloon::SCORE.at(0));
break;
case Balloon::Size::SMALL:
score = Balloon::SCORE.at(0);
break;
default:
score = 0;
break;
}
// Aumenta el poder de la fase
stage_info_->addPower(balloon->getPower());
// Destruye el globo
explosions_->add(balloon->getPosX(), balloon->getPosY(), static_cast<int>(balloon->getSize()));
balloon->pop();
return score;
}
// Destruye todos los globos
auto BalloonManager::destroyAllBalloons() -> int {
int score = 0;
for (auto& balloon : balloons_) {
score += destroyBalloon(balloon);
}
balloon_deploy_counter_ = DEFAULT_BALLOON_DEPLOY_DELAY;
Screen::get()->flash(Colors::FLASH, 0.05F);
Screen::get()->shake();
return score;
}
// Detiene todos los globos
void BalloonManager::stopAllBalloons() {
for (auto& balloon : balloons_) {
if (!balloon->isBeingCreated()) {
balloon->stop();
}
}
}
// Pone en marcha todos los globos
void BalloonManager::startAllBalloons() {
for (auto& balloon : balloons_) {
if (!balloon->isBeingCreated()) {
balloon->start();
}
}
}
// Cambia el color de todos los globos
void BalloonManager::reverseColorsToAllBalloons() {
for (auto& balloon : balloons_) {
if (balloon->isStopped()) {
balloon->useReverseColor();
}
}
}
// Cambia el color de todos los globos
void BalloonManager::normalColorsToAllBalloons() {
for (auto& balloon : balloons_) {
balloon->useNormalColor();
}
}
// Crea dos globos gordos
void BalloonManager::createTwoBigBalloons() {
deployFormation(1);
}
// Obtiene el nivel de ameza actual generado por los globos
auto BalloonManager::getMenace() -> int {
return std::accumulate(balloons_.begin(), balloons_.end(), 0, [](int sum, const auto& balloon) -> auto { return sum + (balloon->isEnabled() ? balloon->getMenace() : 0); });
}
// Establece el sonido de los globos
void BalloonManager::setSounds(bool value) {
sound_enabled_ = value;
for (auto& balloon : balloons_) {
balloon->setSound(value);
}
}
// Activa o desactiva los sonidos de rebote los globos
void BalloonManager::setBouncingSounds(bool value) {
bouncing_sound_enabled_ = value;
for (auto& balloon : balloons_) {
balloon->setBouncingSound(value);
}
}
// Activa o desactiva los sonidos de los globos al explotar
void BalloonManager::setPoppingSounds(bool value) {
poping_sound_enabled_ = value;
for (auto& balloon : balloons_) {
balloon->setPoppingSound(value);
}
}

View File

@@ -0,0 +1,115 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FRect
#include <array> // Para array
#include <list> // Para list
#include <memory> // Para shared_ptr, unique_ptr
#include <string> // Para basic_string, string
#include <vector> // Para vector
#include "balloon.hpp" // for Balloon
#include "balloon_formations.hpp" // for BalloonFormations
#include "explosions.hpp" // for Explosions
#include "param.hpp" // for Param, ParamGame, param
#include "utils.hpp" // for Zone
class IStageInfo;
class Texture;
// --- Types ---
using Balloons = std::list<std::shared_ptr<Balloon>>;
// --- Clase BalloonManager: gestiona todos los globos del juego ---
class BalloonManager {
public:
// --- Constructor y destructor ---
BalloonManager(IStageInfo* stage_info);
~BalloonManager() = default;
// --- Métodos principales ---
void update(float delta_time); // Actualiza el estado de los globos (time-based)
void render(); // Renderiza los globos en pantalla
// --- Gestión de globos ---
void freeBalloons(); // Libera globos que ya no sirven
// --- Creación de formaciones enemigas ---
void deployRandomFormation(int stage); // Crea una formación de globos aleatoria
void deployFormation(int formation_id); // Crea una formación específica
void deployFormation(int formation_id, float y); // Crea una formación específica con coordenadas
// --- Creación de globos ---
auto createBalloon(Balloon::Config config) -> std::shared_ptr<Balloon>; // Crea un nuevo globo
void createChildBalloon(const std::shared_ptr<Balloon>& balloon, const std::string& direction); // Crea un globo a partir de otro
void createPowerBall(); // Crea una PowerBall
void createTwoBigBalloons(); // Crea dos globos grandes
// --- Control de velocidad y despliegue ---
void setBalloonSpeed(float speed); // Ajusta la velocidad de los globos
void setDefaultBalloonSpeed(float speed) { default_balloon_speed_ = speed; }; // Establece la velocidad base
void resetBalloonSpeed() { setBalloonSpeed(default_balloon_speed_); }; // Restablece la velocidad de los globos
void updateBalloonDeployCounter(float delta_time); // Actualiza el contador de despliegue (time-based)
auto canPowerBallBeCreated() -> bool; // Indica si se puede crear una PowerBall
auto calculateScreenPower() -> int; // Calcula el poder de los globos en pantalla
// --- Manipulación de globos existentes ---
auto popBalloon(const std::shared_ptr<Balloon>& balloon) -> int; // Explosiona un globo, creando otros si aplica
auto destroyBalloon(std::shared_ptr<Balloon>& balloon) -> int; // Explosiona un globo sin crear otros
auto destroyAllBalloons() -> int; // Destruye todos los globos
void stopAllBalloons(); // Detiene el movimiento de los globos
void startAllBalloons(); // Reactiva el movimiento de los globos
// --- Cambios de apariencia ---
void reverseColorsToAllBalloons(); // Invierte los colores de los globos
void normalColorsToAllBalloons(); // Restaura los colores originales
// --- Configuración de sonido ---
void setSounds(bool value); // Activa o desactiva los sonidos de los globos
void setBouncingSounds(bool value); // Activa o desactiva los sonidos de rebote los globos
void setPoppingSounds(bool value); // Activa o desactiva los sonidos de los globos al explotar
// --- Configuración de juego ---
void setPlayArea(SDL_FRect play_area) { play_area_ = play_area; }; // Define el área de juego
void setCreationTimeEnabled(bool value) { creation_time_enabled_ = value; }; // Activa o desactiva el tiempo de creación de globos
void enableBalloonDeployment(bool value) { can_deploy_balloons_ = value; }; // Activa o desactiva la generación de globos
// --- Getters ---
auto getMenace() -> int; // Obtiene el nivel de amenaza generado por los globos
[[nodiscard]] auto getBalloonSpeed() const -> float { return balloon_speed_; }
auto getBalloons() -> Balloons& { return balloons_; }
[[nodiscard]] auto getNumBalloons() const -> int { return balloons_.size(); }
private:
// --- Constantes ---
static constexpr float DEFAULT_BALLOON_DEPLOY_DELAY = 5.0F; // 300 frames = 5 segundos
static constexpr float POWERBALL_DEPLOY_DELAY = 0.167F; // 10 frames = 0.167 segundos
static constexpr float BALLOON_POP_DELAY = 0.333F; // 20 frames = 0.333 segundos
// --- Objetos y punteros ---
Balloons balloons_; // Vector con los globos activos
std::unique_ptr<Explosions> explosions_; // Objeto para gestionar explosiones
std::unique_ptr<BalloonFormations> balloon_formations_; // Objeto para manejar formaciones enemigas
std::vector<std::shared_ptr<Texture>> balloon_textures_; // Texturas de los globos
std::vector<std::shared_ptr<Texture>> explosions_textures_; // Texturas de explosiones
std::vector<std::vector<std::string>> balloon_animations_; // Animaciones de los globos
std::vector<std::vector<std::string>> explosions_animations_; // Animaciones de las explosiones
IStageInfo* stage_info_; // Informacion de la pantalla actual
// --- Variables de estado ---
SDL_FRect play_area_ = param.game.play_area.rect;
float balloon_speed_ = Balloon::GAME_TEMPO.at(0);
float default_balloon_speed_ = Balloon::GAME_TEMPO.at(0);
float balloon_deploy_counter_ = 0;
int power_ball_counter_ = 0;
int last_balloon_deploy_ = 0;
bool power_ball_enabled_ = false;
bool creation_time_enabled_ = true;
bool can_deploy_balloons_ = true;
bool bouncing_sound_enabled_ = false; // Si debe sonar el globo al rebotar
bool poping_sound_enabled_ = true; // Si debe sonar el globo al explotar
bool sound_enabled_ = true; // Indica si los globos deben hacer algun sonido
// --- Métodos internos ---
void init();
};

View File

@@ -0,0 +1,104 @@
#include "bullet_manager.hpp"
#include <algorithm> // Para remove_if
#include <utility>
#include "bullet.hpp" // Para Bullet
#include "param.hpp" // Para Param, ParamGame, param
#include "utils.hpp" // Para Circle, Zone
// Constructor
BulletManager::BulletManager()
: play_area_(param.game.play_area.rect) {
}
// Actualiza el estado de todas las balas
void BulletManager::update(float delta_time) {
for (auto& bullet : bullets_) {
if (bullet->isEnabled()) {
processBulletUpdate(bullet, delta_time);
}
}
}
// Renderiza todas las balas activas
void BulletManager::render() {
for (auto& bullet : bullets_) {
if (bullet->isEnabled()) {
bullet->render();
}
}
}
// Crea una nueva bala
void BulletManager::createBullet(int x, int y, Bullet::Type type, Bullet::Color color, int owner) {
bullets_.emplace_back(std::make_shared<Bullet>(x, y, type, color, owner));
}
// Libera balas que ya no están habilitadas
void BulletManager::freeBullets() {
std::erase_if(bullets_, [](const std::shared_ptr<Bullet>& bullet) -> bool {
return !bullet->isEnabled();
});
}
// Elimina todas las balas
void BulletManager::clearAllBullets() {
bullets_.clear();
}
// Verifica colisiones de todas las balas
void BulletManager::checkCollisions() {
for (auto& bullet : bullets_) {
if (!bullet->isEnabled()) {
continue;
}
// Verifica colisión con Tabe
if (tabe_collision_callback_ && tabe_collision_callback_(bullet)) {
break; // Sale del bucle si hubo colisión
}
// Verifica colisión con globos
if (balloon_collision_callback_ && balloon_collision_callback_(bullet)) {
break; // Sale del bucle si hubo colisión
}
}
}
// Establece el callback para colisión con Tabe
void BulletManager::setTabeCollisionCallback(CollisionCallback callback) {
tabe_collision_callback_ = std::move(callback);
}
// Establece el callback para colisión con globos
void BulletManager::setBalloonCollisionCallback(CollisionCallback callback) {
balloon_collision_callback_ = std::move(callback);
}
// Establece el callback para balas fuera de límites
void BulletManager::setOutOfBoundsCallback(OutOfBoundsCallback callback) {
out_of_bounds_callback_ = std::move(callback);
}
// --- Métodos privados ---
// Procesa la actualización individual de una bala
void BulletManager::processBulletUpdate(const std::shared_ptr<Bullet>& bullet, float delta_time) {
auto status = bullet->update(delta_time);
// Si la bala salió de los límites, llama al callback
if (status == Bullet::MoveStatus::OUT && out_of_bounds_callback_) {
out_of_bounds_callback_(bullet);
}
}
// Verifica si la bala está fuera de los límites del área de juego
auto BulletManager::isBulletOutOfBounds(const std::shared_ptr<Bullet>& bullet) const -> bool {
auto collider = bullet->getCollider();
return (collider.x < play_area_.x ||
collider.x > play_area_.x + play_area_.w ||
collider.y < play_area_.y ||
collider.y > play_area_.y + play_area_.h);
}

View File

@@ -0,0 +1,76 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FRect
#include <functional> // Para function
#include <list> // Para list
#include <memory> // Para shared_ptr
#include <vector> // Para vector
#include "bullet.hpp" // for Bullet
// --- Types ---
using Bullets = std::list<std::shared_ptr<Bullet>>;
// --- Clase BulletManager: gestiona todas las balas del juego ---
//
// Esta clase se encarga de la gestión completa de las balas del juego,
// incluyendo su creación, actualización, renderizado y colisiones.
//
// Funcionalidades principales:
// • Gestión del ciclo de vida: creación, actualización y destrucción de balas
// • Renderizado: dibuja todas las balas activas en pantalla
// • Detección de colisiones: mediante sistema de callbacks
// • Limpieza automática: elimina balas deshabilitadas del contenedor
// • Configuración flexible: permite ajustar parámetros de las balas
//
// La clase utiliza un sistema de callbacks para manejar las colisiones,
// permitiendo que la lógica específica del juego permanezca en Game.
class BulletManager {
public:
// --- Types para callbacks ---
using CollisionCallback = std::function<bool(const std::shared_ptr<Bullet>&)>;
using OutOfBoundsCallback = std::function<void(const std::shared_ptr<Bullet>&)>;
// --- Constructor y destructor ---
BulletManager();
~BulletManager() = default;
// --- Métodos principales ---
void update(float delta_time); // Actualiza el estado de las balas (time-based)
void render(); // Renderiza las balas en pantalla
// --- Gestión de balas ---
void createBullet(int x, int y, Bullet::Type type, Bullet::Color color, int owner); // Crea una nueva bala
void freeBullets(); // Libera balas que ya no sirven
void clearAllBullets(); // Elimina todas las balas
// --- Detección de colisiones ---
void checkCollisions(); // Verifica colisiones de todas las balas
void setTabeCollisionCallback(CollisionCallback callback); // Establece callback para colisión con Tabe
void setBalloonCollisionCallback(CollisionCallback callback); // Establece callback para colisión con globos
void setOutOfBoundsCallback(OutOfBoundsCallback callback); // Establece callback para balas fuera de límites
// --- Configuración ---
void setPlayArea(SDL_FRect play_area) { play_area_ = play_area; }; // Define el área de juego
// --- Getters ---
auto getBullets() -> Bullets& { return bullets_; } // Obtiene referencia al vector de balas
[[nodiscard]] auto getNumBullets() const -> int { return bullets_.size(); } // Obtiene el número de balas activas
private:
// --- Objetos y punteros ---
Bullets bullets_; // Vector con las balas activas
// --- Variables de configuración ---
SDL_FRect play_area_; // Área de juego para límites
// --- Callbacks para colisiones ---
CollisionCallback tabe_collision_callback_; // Callback para colisión con Tabe
CollisionCallback balloon_collision_callback_; // Callback para colisión con globos
OutOfBoundsCallback out_of_bounds_callback_; // Callback para balas fuera de límites
// --- Métodos internos ---
void processBulletUpdate(const std::shared_ptr<Bullet>& bullet, float delta_time); // Procesa actualización individual
[[nodiscard]] auto isBulletOutOfBounds(const std::shared_ptr<Bullet>& bullet) const -> bool; // Verifica si la bala está fuera de límites
};

View File

@@ -0,0 +1,49 @@
#pragma once
#include <algorithm> // Para std::max
class Cooldown {
public:
Cooldown(float first_delay_s = 0.0F, float repeat_delay_s = 0.0F)
: first_delay_s_(first_delay_s),
repeat_delay_s_(repeat_delay_s) {}
// Llamar cada frame con delta en segundos (float)
void update(float delta_s) {
if (remaining_s_ <= 0.0F) {
remaining_s_ = 0.0F;
return;
}
remaining_s_ -= delta_s;
remaining_s_ = std::max(remaining_s_, 0.0F);
}
// Llamar cuando el input está activo. Devuelve true si debe ejecutarse la acción ahora.
auto tryConsumeOnHeld() -> bool {
if (remaining_s_ > 0.0F) {
return false;
}
float delay = held_before_ ? repeat_delay_s_ : first_delay_s_;
remaining_s_ = delay;
held_before_ = true;
return true;
}
// Llamar cuando el input se suelta
void onReleased() {
held_before_ = false;
remaining_s_ = 0.0F;
}
[[nodiscard]] auto empty() const -> bool { return remaining_s_ == 0.0F; }
// Fuerza un valor en segundos (útil para tests o resets)
void forceSet(float seconds) { remaining_s_ = seconds > 0.0F ? seconds : 0.0F; }
private:
float first_delay_s_;
float repeat_delay_s_;
float remaining_s_{0.0F};
bool held_before_{false};
};

View File

@@ -0,0 +1,38 @@
#include "difficulty.hpp"
#include <vector> // Para vector
namespace Difficulty {
static std::vector<Info> difficulties_list;
void init() {
difficulties_list = {
{.code = Code::EASY, .name = "Easy"},
{.code = Code::NORMAL, .name = "Normal"},
{.code = Code::HARD, .name = "Hard"}};
}
auto getDifficulties() -> std::vector<Info>& {
return difficulties_list;
}
auto getNameFromCode(Code code) -> std::string {
for (const auto& difficulty : difficulties_list) {
if (difficulty.code == code) {
return difficulty.name;
}
}
return !difficulties_list.empty() ? difficulties_list.front().name : "Unknown";
}
auto getCodeFromName(const std::string& name) -> Code {
for (const auto& difficulty : difficulties_list) {
if (difficulty.name == name) {
return difficulty.code;
}
}
return !difficulties_list.empty() ? difficulties_list.front().code : Code::NORMAL;
}
} // namespace Difficulty

View File

@@ -0,0 +1,28 @@
#pragma once
#include <string> // Para string
#include <vector> // Para vector
namespace Difficulty {
// --- Enums ---
enum class Code {
EASY = 0, // Dificultad fácil
NORMAL = 1, // Dificultad normal
HARD = 2, // Dificultad difícil
};
// --- Estructuras ---
struct Info {
Code code; // Código de dificultad
std::string name; // Nombre traducible
};
// --- Funciones ---
void init(); // Inicializa la lista de dificultades con sus valores por defecto
auto getDifficulties() -> std::vector<Info>&; // Devuelve una referencia al vector de todas las dificultades
auto getNameFromCode(Code code) -> std::string; // Obtiene el nombre de una dificultad a partir de su código
auto getCodeFromName(const std::string& name) -> Code; // Obtiene el código de una dificultad a partir de su nombre
} // namespace Difficulty

View File

@@ -0,0 +1,126 @@
#include "enter_name.hpp"
#include <array> // Para array
#include <cstdlib> // Para rand
#include <string_view> // Para basic_string_view, string_view
// Constructor
EnterName::EnterName() = default;
// Inicializa el objeto
void EnterName::init(const std::string& name) {
name_ = sanitizeName(name);
selected_index_ = 0;
// Si el nombre está completo, cambia el caracter seleccionado a el caracter de finalizar
if (!nameIsEmpty()) {
forceEndCharSelected();
}
}
// Incrementa el índice del carácter seleccionado
void EnterName::incIndex() {
++selected_index_;
if (selected_index_ >= character_list_.size()) {
selected_index_ = 0;
}
}
// Decrementa el índice del carácter seleccionado
void EnterName::decIndex() {
if (selected_index_ == 0) {
selected_index_ = character_list_.size() - 1;
} else {
--selected_index_;
}
}
// Añade el carácter seleccionado al nombre
void EnterName::addCharacter() {
// Si no es el ultimo caracter, lo añade
if (name_.length() < MAX_NAME_SIZE) {
name_.push_back(character_list_[selected_index_]);
}
// Si el nombre está completo, cambia el caracter seleccionado a el caracter de finalizar
if (nameIsFull()) {
forceEndCharSelected();
}
}
// Elimina el último carácter del nombre
void EnterName::removeLastCharacter() {
if (!name_.empty()) {
name_.pop_back();
}
}
// Devuelve el carácter seleccionado con offset relativo como string
auto EnterName::getSelectedCharacter(int offset) const -> std::string {
// Calcular el índice con offset, con wrap-around circular
int size = static_cast<int>(character_list_.size());
int index = (selected_index_ + offset) % size;
// Manejar índices negativos (hacer wrap-around hacia atrás)
if (index < 0) {
index += size;
}
return {1, character_list_[index]};
}
// Devuelve el carrusel completo de caracteres centrado en el seleccionado
auto EnterName::getCarousel(int size) const -> std::string {
// Asegurar que el tamaño sea impar para tener un centro claro
if (size % 2 == 0) {
++size;
}
std::string carousel;
carousel.reserve(size); // Optimización: reservar memoria de antemano
int half = size / 2;
// Construir desde -half hasta +half (inclusive)
for (int offset = -half; offset <= half; ++offset) {
carousel += getSelectedCharacter(offset);
}
return carousel;
}
// Valida y limpia el nombre: solo caracteres legales y longitud máxima
auto EnterName::sanitizeName(const std::string& name) const -> std::string {
std::string sanitized;
for (size_t i = 0; i < name.length() && sanitized.length() < MAX_NAME_SIZE; ++i) {
// Verifica si el carácter está en la lista permitida
if (character_list_.find(name[i]) != std::string::npos) {
sanitized.push_back(name[i]);
}
}
return sanitized;
}
// Devuelve un nombre al azar
auto EnterName::getRandomName() -> std::string {
static constexpr std::array<std::string_view, 8> NAMES = {
"BAL1",
"TABE",
"DOC",
"MON",
"SAM1",
"JORDI",
"JDES",
"PEPE"};
return std::string(NAMES[rand() % NAMES.size()]);
}
// Obtiene el nombre final introducido
auto EnterName::getFinalName() -> std::string {
if (name_.empty()) {
name_ = getRandomName();
}
return name_;
}

View File

@@ -0,0 +1,42 @@
#pragma once
#include <cstddef> // Para size_t
#include <string> // Para allocator, string
// --- Clase EnterName: gestor de entrada de nombre del jugador ---
class EnterName {
public:
// --- Constantes ---
static constexpr size_t MAX_NAME_SIZE = 6; // Tamaño máximo del nombre
EnterName();
~EnterName() = default;
void init(const std::string& name = ""); // Inicializa con nombre opcional (vacío por defecto)
void incIndex(); // Incrementa el índice del carácter seleccionado en la lista
void decIndex(); // Decrementa el índice del carácter seleccionado en la lista
void addCharacter(); // Añade el carácter seleccionado al nombre
void removeLastCharacter(); // Elimina el último carácter del nombre
auto getFinalName() -> std::string; // Obtiene el nombre final (o aleatorio si vacío)
[[nodiscard]] auto getCurrentName() const -> std::string { return name_; } // Obtiene el nombre actual en proceso
[[nodiscard]] auto getSelectedCharacter(int offset = 0) const -> std::string; // Devuelve el carácter seleccionado con offset relativo
[[nodiscard]] auto getCarousel(int size) const -> std::string; // Devuelve el carrusel de caracteres (size debe ser impar)
[[nodiscard]] auto getSelectedIndex() const -> int { return selected_index_; } // Obtiene el índice del carácter seleccionado
[[nodiscard]] auto getCharacterList() const -> const std::string& { return character_list_; } // Obtiene la lista completa de caracteres
[[nodiscard]] auto nameIsFull() const -> bool { return name_.size() == MAX_NAME_SIZE; } // Informa de si el nombre ha alcanzado su limite
[[nodiscard]] auto nameIsEmpty() const -> bool { return name_.empty(); } // Informa de si el nombre está vacío
[[nodiscard]] auto endCharSelected() const -> bool { return selected_index_ == character_list_.size() - 1; } // Informa de si está seleccionado el caracter de terminar
private:
// --- Variables de estado ---
std::string character_list_{"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{"}; // Lista de caracteres permitidos
std::string name_; // Nombre en proceso
size_t selected_index_ = 0; // Índice del carácter seleccionado en "character_list_"
[[nodiscard]] auto sanitizeName(const std::string& name) const -> std::string; // Valida y limpia el nombre
static auto getRandomName() -> std::string; // Devuelve un nombre al azar
void forceEndCharSelected() { selected_index_ = character_list_.size() - 1; } // Establece como seleccionado el caracter de terminar
};

View File

@@ -0,0 +1,279 @@
#include "game_logo.hpp"
#include <SDL3/SDL.h> // Para SDL_SetTextureScaleMode, SDL_FlipMode, SDL_ScaleMode
#include <algorithm> // Para max
#include <string> // Para basic_string
#include "animated_sprite.hpp" // Para AnimatedSprite
#include "audio.hpp" // Para Audio
#include "color.hpp" // Para Color
#include "param.hpp" // Para Param, param, ParamGame, ParamTitle
#include "resource.hpp" // Para Resource
#include "screen.hpp" // Para Screen
#include "smart_sprite.hpp" // Para SmartSprite
#include "sprite.hpp" // Para Sprite
#include "texture.hpp" // Para Texture
constexpr int ZOOM_FACTOR = 5;
constexpr float FLASH_DELAY_S = 0.05F; // 3 frames → 0.05s
constexpr float FLASH_DURATION_S = 0.1F; // 6 frames → 0.1s (3 + 3)
constexpr Color FLASH_COLOR = Color(0xFF, 0xFF, 0xFF); // Color blanco para el flash
// Constructor
GameLogo::GameLogo(int x, int y)
: dust_texture_(Resource::get()->getTexture("title_dust.png")),
coffee_texture_(Resource::get()->getTexture("title_coffee.png")),
crisis_texture_(Resource::get()->getTexture("title_crisis.png")),
arcade_edition_texture_(Resource::get()->getTexture("title_arcade_edition.png")),
dust_left_sprite_(std::make_unique<AnimatedSprite>(dust_texture_, Resource::get()->getAnimation("title_dust.ani"))),
dust_right_sprite_(std::make_unique<AnimatedSprite>(dust_texture_, Resource::get()->getAnimation("title_dust.ani"))),
coffee_sprite_(std::make_unique<SmartSprite>(coffee_texture_)),
crisis_sprite_(std::make_unique<SmartSprite>(crisis_texture_)),
arcade_edition_sprite_(std::make_unique<Sprite>(arcade_edition_texture_, (param.game.width - arcade_edition_texture_->getWidth()) / 2, param.title.arcade_edition_position, arcade_edition_texture_->getWidth(), arcade_edition_texture_->getHeight())),
x_(x),
y_(y) {}
// Inicializa las variables
void GameLogo::init() {
const auto XP = x_ - (coffee_texture_->getWidth() / 2);
const auto DESP = getInitialVerticalDesp();
// Configura texturas
SDL_SetTextureScaleMode(Resource::get()->getTexture("title_arcade_edition.png")->getSDLTexture(), SDL_SCALEMODE_NEAREST);
// Variables
coffee_crisis_status_ = Status::DISABLED;
arcade_edition_status_ = Status::DISABLED;
shake_.init(1, 2, 8, XP);
zoom_ = 3.0F * ZOOM_FACTOR;
post_finished_timer_ = 0.0F;
// Inicializa el bitmap de 'Coffee'
coffee_sprite_->setPosX(XP);
coffee_sprite_->setPosY(y_ - coffee_texture_->getHeight() - DESP);
coffee_sprite_->setWidth(coffee_texture_->getWidth());
coffee_sprite_->setHeight(coffee_texture_->getHeight());
coffee_sprite_->setVelX(0.0F);
coffee_sprite_->setVelY(COFFEE_VEL_Y);
coffee_sprite_->setAccelX(0.0F);
coffee_sprite_->setAccelY(COFFEE_ACCEL_Y);
coffee_sprite_->setSpriteClip(0, 0, coffee_texture_->getWidth(), coffee_texture_->getHeight());
coffee_sprite_->setEnabled(true);
coffee_sprite_->setFinishedDelay(0.0F);
coffee_sprite_->setDestX(XP);
coffee_sprite_->setDestY(y_ - coffee_texture_->getHeight());
// Inicializa el bitmap de 'Crisis'
crisis_sprite_->setPosX(XP + CRISIS_OFFSET_X);
crisis_sprite_->setPosY(y_ + DESP);
crisis_sprite_->setWidth(crisis_texture_->getWidth());
crisis_sprite_->setHeight(crisis_texture_->getHeight());
crisis_sprite_->setVelX(0.0F);
crisis_sprite_->setVelY(CRISIS_VEL_Y);
crisis_sprite_->setAccelX(0.0F);
crisis_sprite_->setAccelY(CRISIS_ACCEL_Y);
crisis_sprite_->setSpriteClip(0, 0, crisis_texture_->getWidth(), crisis_texture_->getHeight());
crisis_sprite_->setEnabled(true);
crisis_sprite_->setFinishedDelay(0.0F);
crisis_sprite_->setDestX(XP + CRISIS_OFFSET_X);
crisis_sprite_->setDestY(y_);
// Inicializa el bitmap de 'DustRight'
dust_right_sprite_->resetAnimation();
dust_right_sprite_->setPosX(coffee_sprite_->getPosX() + coffee_sprite_->getWidth());
dust_right_sprite_->setPosY(y_);
dust_right_sprite_->setWidth(DUST_SIZE);
dust_right_sprite_->setHeight(DUST_SIZE);
dust_right_sprite_->setFlip(SDL_FLIP_HORIZONTAL);
// Inicializa el bitmap de 'DustLeft'
dust_left_sprite_->resetAnimation();
dust_left_sprite_->setPosX(coffee_sprite_->getPosX() - DUST_SIZE);
dust_left_sprite_->setPosY(y_);
dust_left_sprite_->setWidth(DUST_SIZE);
dust_left_sprite_->setHeight(DUST_SIZE);
// Inicializa el bitmap de 'Arcade Edition'
arcade_edition_sprite_->setZoom(zoom_);
}
// Pinta la clase en pantalla
void GameLogo::render() {
// Dibuja el logo
coffee_sprite_->render();
crisis_sprite_->render();
if (arcade_edition_status_ != Status::DISABLED) {
arcade_edition_sprite_->render();
}
// Dibuja el polvillo del logo
if (coffee_crisis_status_ != Status::MOVING) {
dust_right_sprite_->render();
dust_left_sprite_->render();
}
}
// Actualiza la lógica de la clase (time-based)
void GameLogo::update(float delta_time) {
updateCoffeeCrisis(delta_time);
updateArcadeEdition(delta_time);
updatePostFinishedCounter(delta_time);
}
void GameLogo::updateCoffeeCrisis(float delta_time) {
switch (coffee_crisis_status_) {
case Status::MOVING:
handleCoffeeCrisisMoving(delta_time);
break;
case Status::SHAKING:
handleCoffeeCrisisShaking(delta_time);
break;
case Status::FINISHED:
handleCoffeeCrisisFinished(delta_time);
break;
default:
break;
}
}
void GameLogo::updateArcadeEdition(float delta_time) {
switch (arcade_edition_status_) {
case Status::MOVING:
handleArcadeEditionMoving(delta_time);
break;
case Status::SHAKING:
handleArcadeEditionShaking(delta_time);
break;
default:
break;
}
}
void GameLogo::handleCoffeeCrisisMoving(float delta_time) {
coffee_sprite_->update(delta_time);
crisis_sprite_->update(delta_time);
if (coffee_sprite_->hasFinished() && crisis_sprite_->hasFinished()) {
coffee_crisis_status_ = Status::SHAKING;
playTitleEffects();
}
}
void GameLogo::handleCoffeeCrisisShaking(float delta_time) {
if (shake_.remaining > 0) {
processShakeEffect(coffee_sprite_.get(), crisis_sprite_.get(), delta_time);
} else {
finishCoffeeCrisisShaking();
}
updateDustSprites(delta_time);
}
void GameLogo::handleCoffeeCrisisFinished(float delta_time) {
updateDustSprites(delta_time);
}
void GameLogo::handleArcadeEditionMoving(float delta_time) {
// DeltaTime en segundos: decremento por segundo
zoom_ -= (ZOOM_DECREMENT_PER_S * ZOOM_FACTOR) * delta_time;
arcade_edition_sprite_->setZoom(zoom_);
if (zoom_ <= 1.0F) {
finishArcadeEditionMoving();
}
}
void GameLogo::handleArcadeEditionShaking(float delta_time) {
if (shake_.remaining > 0) {
processArcadeEditionShake(delta_time);
} else {
arcade_edition_sprite_->setX(shake_.origin);
arcade_edition_status_ = Status::FINISHED;
}
}
void GameLogo::processShakeEffect(SmartSprite* primary_sprite, SmartSprite* secondary_sprite, float delta_time) {
shake_.time_accumulator += delta_time;
if (shake_.time_accumulator >= SHAKE_DELAY_S) {
shake_.time_accumulator -= SHAKE_DELAY_S;
const auto DISPLACEMENT = calculateShakeDisplacement();
primary_sprite->setPosX(shake_.origin + DISPLACEMENT);
if (secondary_sprite != nullptr) {
secondary_sprite->setPosX(shake_.origin + DISPLACEMENT + CRISIS_OFFSET_X);
}
shake_.remaining--;
}
}
void GameLogo::processArcadeEditionShake(float delta_time) {
// Delay fijo en segundos (shake_.delay era frames, ahora usamos constante)
float delay_time = SHAKE_DELAY_S;
shake_.time_accumulator += delta_time;
if (shake_.time_accumulator >= delay_time) {
shake_.time_accumulator -= delay_time;
const auto DISPLACEMENT = calculateShakeDisplacement();
arcade_edition_sprite_->setX(shake_.origin + DISPLACEMENT);
shake_.remaining--;
}
}
auto GameLogo::calculateShakeDisplacement() const -> int {
return shake_.remaining % 2 == 0 ? shake_.desp * (-1) : shake_.desp;
}
void GameLogo::finishCoffeeCrisisShaking() {
coffee_sprite_->setPosX(shake_.origin);
crisis_sprite_->setPosX(shake_.origin + CRISIS_OFFSET_X);
coffee_crisis_status_ = Status::FINISHED;
arcade_edition_status_ = Status::MOVING;
}
void GameLogo::finishArcadeEditionMoving() {
arcade_edition_status_ = Status::SHAKING;
zoom_ = 1.0F;
arcade_edition_sprite_->setZoom(zoom_);
shake_.init(1, 2, 8, arcade_edition_sprite_->getX());
playTitleEffects();
}
void GameLogo::playTitleEffects() {
Audio::get()->playSound("title.wav");
Screen::get()->flash(FLASH_COLOR, FLASH_DURATION_S, FLASH_DELAY_S);
Screen::get()->shake();
}
void GameLogo::updateDustSprites(float delta_time) {
dust_right_sprite_->update(delta_time);
dust_left_sprite_->update(delta_time);
}
void GameLogo::updatePostFinishedCounter(float delta_time) {
if (coffee_crisis_status_ == Status::FINISHED &&
arcade_edition_status_ == Status::FINISHED) {
post_finished_timer_ += delta_time;
}
}
// Activa la clase
void GameLogo::enable() {
init();
coffee_crisis_status_ = Status::MOVING;
}
// Indica si ha terminado la animación
auto GameLogo::hasFinished() const -> bool {
return post_finished_timer_ >= post_finished_delay_s_;
}
// Calcula el desplazamiento vertical inicial
auto GameLogo::getInitialVerticalDesp() const -> int {
const float OFFSET_UP = y_;
const float OFFSET_DOWN = param.game.height - y_;
return std::max(OFFSET_UP, OFFSET_DOWN);
}

View File

@@ -0,0 +1,125 @@
#pragma once
#include <memory> // Para unique_ptr, shared_ptr
#include "animated_sprite.hpp" // Para AnimatedSprite
#include "smart_sprite.hpp" // Para SmartSprite
#include "sprite.hpp" // Para Sprite
class Texture;
// --- Clase GameLogo: gestor del logo del juego ---
class GameLogo {
public:
// --- Constantes ---
static constexpr float COFFEE_VEL_Y = 0.15F * 1000.0F; // Velocidad Y de coffee sprite (pixels/s) - 0.15F * 1000 = 150 pixels/s
static constexpr float COFFEE_ACCEL_Y = 0.00036F * 1000000.0F; // Aceleración Y de coffee sprite (pixels/s²) - 0.00036F * 1000000 = 360 pixels/s²
static constexpr float CRISIS_VEL_Y = -0.15F * 1000.0F; // Velocidad Y de crisis sprite (pixels/s) - -0.15F * 1000 = -150 pixels/s
static constexpr float CRISIS_ACCEL_Y = -0.00036F * 1000000.0F; // Aceleración Y de crisis sprite (pixels/s²) - -0.00036F * 1000000 = -360 pixels/s²
static constexpr int CRISIS_OFFSET_X = 15; // Desplazamiento X de crisis sprite
static constexpr int DUST_SIZE = 16; // Tamaño de dust sprites
static constexpr float ZOOM_DECREMENT_PER_S = 0.006F * 1000.0F; // Decremento de zoom por segundo (0.006F * 1000 = 6.0F per second)
static constexpr float SHAKE_DELAY_S = 33.34F / 1000.0F; // Delay de shake en segundos (33.34ms / 1000 = 0.03334s)
static constexpr float POST_FINISHED_FRAME_TIME_S = 16.67F / 1000.0F; // Tiempo entre decrementos del counter (16.67ms / 1000 = 0.01667s)
// --- Constructores y destructor ---
GameLogo(int x, int y);
~GameLogo() = default;
// --- Métodos principales ---
void render(); // Pinta la clase en pantalla
void update(float delta_time); // Actualiza la lógica de la clase (time-based)
void enable(); // Activa la clase
// --- Getters ---
[[nodiscard]] auto hasFinished() const -> bool; // Indica si ha terminado la animación
private:
// --- Enums ---
enum class Status {
DISABLED, // Deshabilitado
MOVING, // En movimiento
SHAKING, // Temblando
FINISHED, // Terminado
};
// --- Estructuras privadas ---
struct Shake {
int desp = 1; // Pixels de desplazamiento para agitar la pantalla en el eje x
int delay = 2; // Retraso entre cada desplazamiento de la pantalla al agitarse (frame-based)
int length = 8; // Cantidad de desplazamientos a realizar
int remaining = length; // Cantidad de desplazamientos pendientes a realizar
int counter = delay; // Contador para el retraso (frame-based)
float time_accumulator = 0.0F; // Acumulador de tiempo para deltaTime
int origin = 0; // Valor inicial de la pantalla para dejarla igual tras el desplazamiento
Shake() = default;
Shake(int d, int de, int l, int o)
: desp(d),
delay(de),
length(l),
remaining(l),
counter(de),
origin(o) {}
void init(int d, int de, int l, int o) {
desp = d;
delay = de;
length = l;
remaining = l;
counter = de;
time_accumulator = 0.0F;
origin = o;
}
};
// --- Objetos y punteros ---
std::shared_ptr<Texture> dust_texture_; // Textura con los graficos del polvo
std::shared_ptr<Texture> coffee_texture_; // Textura con los graficos de la palabra "COFFEE"
std::shared_ptr<Texture> crisis_texture_; // Textura con los graficos de la palabra "CRISIS"
std::shared_ptr<Texture> arcade_edition_texture_; // Textura con los graficos de "Arcade Edition"
std::unique_ptr<AnimatedSprite> dust_left_sprite_; // Sprite del polvo (izquierda)
std::unique_ptr<AnimatedSprite> dust_right_sprite_; // Sprite del polvo (derecha)
std::unique_ptr<SmartSprite> coffee_sprite_; // Sprite de "COFFEE"
std::unique_ptr<SmartSprite> crisis_sprite_; // Sprite de "CRISIS"
std::unique_ptr<Sprite> arcade_edition_sprite_; // Sprite de "Arcade Edition"
// --- Variables de estado ---
Shake shake_; // Efecto de agitación
Status coffee_crisis_status_ = Status::DISABLED; // Estado de "COFFEE CRISIS"
Status arcade_edition_status_ = Status::DISABLED; // Estado de "ARCADE EDITION"
float x_; // Posición X del logo
float y_; // Posición Y del logo
float zoom_ = 1.0F; // Zoom aplicado al texto "ARCADE EDITION"
float post_finished_delay_s_ = POST_FINISHED_FRAME_TIME_S; // Retraso final tras animaciones (s)
float post_finished_timer_ = 0.0F; // Timer acumulado para retraso final (s)
// --- Inicialización ---
void init(); // Inicializa las variables
[[nodiscard]] auto getInitialVerticalDesp() const -> int; // Calcula el desplazamiento vertical inicial
// --- Actualización de estados específicos ---
void updateCoffeeCrisis(float delta_time); // Actualiza el estado de "Coffee Crisis" (time-based)
void updateArcadeEdition(float delta_time); // Actualiza el estado de "Arcade Edition" (time-based)
void updatePostFinishedCounter(float delta_time); // Actualiza el contador tras finalizar una animación (time-based)
// --- Efectos visuales: movimiento y sacudidas ---
void handleCoffeeCrisisMoving(float delta_time); // Maneja el movimiento de "Coffee Crisis" (time-based)
void handleCoffeeCrisisShaking(float delta_time); // Maneja la sacudida de "Coffee Crisis" (time-based)
void handleArcadeEditionMoving(float delta_time); // Maneja el movimiento de "Arcade Edition" (time-based)
void handleArcadeEditionShaking(float delta_time); // Maneja la sacudida de "Arcade Edition" (time-based)
void processShakeEffect(SmartSprite* primary_sprite, SmartSprite* secondary_sprite = nullptr); // Procesa el efecto de sacudida en sprites (frame-based)
void processShakeEffect(SmartSprite* primary_sprite, SmartSprite* secondary_sprite, float delta_time); // Procesa el efecto de sacudida en sprites (time-based)
void processArcadeEditionShake(float delta_time); // Procesa la sacudida específica de "Arcade Edition" (time-based)
[[nodiscard]] auto calculateShakeDisplacement() const -> int; // Calcula el desplazamiento de la sacudida
// --- Gestión de finalización de efectos ---
void handleCoffeeCrisisFinished(float delta_time); // Maneja el final de la animación "Coffee Crisis" (time-based)
void finishCoffeeCrisisShaking(); // Finaliza la sacudida de "Coffee Crisis"
void finishArcadeEditionMoving(); // Finaliza el movimiento de "Arcade Edition"
// --- Utilidades ---
static void playTitleEffects(); // Reproduce efectos visuales/sonoros del título
void updateDustSprites(float delta_time); // Actualiza los sprites de polvo (time-based)
};

View File

@@ -0,0 +1,326 @@
#include "manage_hiscore_table.hpp"
#include <SDL3/SDL.h> // Para SDL_ReadIO, SDL_WriteIO, SDL_CloseIO, SDL_GetError, SDL_IOFromFile, SDL_LogError, SDL_LogCategory, SDL_LogInfo
#include <algorithm> // Para __sort_fn, sort
#include <array> // Para array
#include <functional> // Para identity
#include <iomanip> // Para std::setw, std::setfill
#include <iostream> // Para std::cout
#include <iterator> // Para distance
#include <ranges> // Para __find_if_fn, find_if
#include <utility> // Para move
#include "utils.hpp" // Para getFileName
// Resetea la tabla a los valores por defecto
void ManageHiScoreTable::clear() {
// Limpia la tabla
table_.clear();
// Añade 10 entradas predefinidas
table_.emplace_back("BRY", 1000000);
table_.emplace_back("USUFO", 500000);
table_.emplace_back("GLUCA", 100000);
table_.emplace_back("PARRA", 50000);
table_.emplace_back("CAGAM", 10000);
table_.emplace_back("PEPE", 5000);
table_.emplace_back("ROSIT", 1000);
table_.emplace_back("SAM", 500);
table_.emplace_back("PACMQ", 200);
table_.emplace_back("PELEC", 100);
/*
table_.emplace_back("BRY", 1000);
table_.emplace_back("USUFO", 500);
table_.emplace_back("GLUCA", 100);
table_.emplace_back("PARRA", 50);
table_.emplace_back("CAGAM", 10);
table_.emplace_back("PEPE", 5);
table_.emplace_back("ROSIT", 4);
table_.emplace_back("SAM", 3);
table_.emplace_back("PACMQ", 2);
table_.emplace_back("PELEC", 1);
*/
/*
table_.emplace_back("BRY", 5000000);
table_.emplace_back("USUFO", 5000000);
table_.emplace_back("GLUCA", 5000000);
table_.emplace_back("PARRA", 5000000);
table_.emplace_back("CAGAM", 5000000);
table_.emplace_back("PEPE", 5000000);
table_.emplace_back("ROSIT", 5000000);
table_.emplace_back("SAM", 5000000);
table_.emplace_back("PACMQ", 5000000);
table_.emplace_back("PELEC", 5000000);
*/
sort();
}
// Añade un elemento a la tabla
auto ManageHiScoreTable::add(const HiScoreEntry& entry) -> int {
// Añade la entrada a la tabla
table_.push_back(entry);
// Ordena la tabla
sort();
// Encontrar la posición del nuevo elemento
auto it = std::ranges::find_if(table_, [&](const HiScoreEntry& e) -> bool {
return e.name == entry.name && e.score == entry.score && e.one_credit_complete == entry.one_credit_complete;
});
int position = -1;
if (it != table_.end()) {
position = std::distance(table_.begin(), it);
}
// Deja solo las 10 primeras entradas
if (table_.size() > 10) {
table_.resize(10);
// Si el nuevo elemento quedó fuera del top 10
if (position >= 10) {
position = NO_ENTRY; // No entró en el top 10
}
}
// Devuelve la posición
return position;
}
// Ordena la tabla
void ManageHiScoreTable::sort() {
struct
{
auto operator()(const HiScoreEntry& a, const HiScoreEntry& b) const -> bool { return a.score > b.score; }
} score_descending_comparator;
std::ranges::sort(table_, score_descending_comparator);
}
// Carga la tabla desde un fichero
auto ManageHiScoreTable::loadFromFile(const std::string& file_path) -> bool {
auto* file = SDL_IOFromFile(file_path.c_str(), "rb");
if (file == nullptr) {
std::cout << "Error: Unable to load " << getFileName(file_path) << " file! " << SDL_GetError() << '\n';
clear();
return false;
}
// Validar header (magic number + version + table size)
if (!validateMagicNumber(file, file_path)) {
SDL_CloseIO(file);
clear();
return false;
}
if (!validateVersion(file, file_path)) {
SDL_CloseIO(file);
clear();
return false;
}
int table_size = 0;
if (!readTableSize(file, file_path, table_size)) {
SDL_CloseIO(file);
clear();
return false;
}
// Leer todas las entradas
Table temp_table;
bool success = true;
for (int i = 0; i < table_size; ++i) {
HiScoreEntry entry;
if (!readEntry(file, file_path, i, entry)) {
success = false;
break;
}
temp_table.push_back(entry);
}
// Verificar checksum
if (success) {
success = verifyChecksum(file, file_path, temp_table);
}
SDL_CloseIO(file);
// Si todo fue bien, actualizar la tabla; si no, usar valores por defecto
if (success) {
table_ = std::move(temp_table);
} else {
std::cout << "File " << getFileName(file_path) << " is corrupted - loading default values" << '\n';
clear();
}
return success;
}
// Métodos auxiliares privados para loadFromFile
auto ManageHiScoreTable::validateMagicNumber(SDL_IOStream* file, const std::string& file_path) -> bool {
std::array<char, 4> magic;
if (SDL_ReadIO(file, magic.data(), 4) != 4 || magic[0] != 'C' || magic[1] != 'C' || magic[2] != 'A' || magic[3] != 'E') {
std::cout << "Error: Invalid magic number in " << getFileName(file_path) << " - file may be corrupted or old format" << '\n';
return false;
}
return true;
}
auto ManageHiScoreTable::validateVersion(SDL_IOStream* file, const std::string& file_path) -> bool {
int version = 0;
if (SDL_ReadIO(file, &version, sizeof(int)) != sizeof(int)) {
std::cout << "Error: Cannot read version in " << getFileName(file_path) << '\n';
return false;
}
if (version != FILE_VERSION) {
std::cout << "Error: Unsupported file version " << version << " in " << getFileName(file_path) << " (expected " << FILE_VERSION << ")" << '\n';
return false;
}
return true;
}
auto ManageHiScoreTable::readTableSize(SDL_IOStream* file, const std::string& file_path, int& table_size) -> bool {
if (SDL_ReadIO(file, &table_size, sizeof(int)) != sizeof(int)) {
std::cout << "Error: Cannot read table size in " << getFileName(file_path) << '\n';
return false;
}
if (table_size < 0 || table_size > MAX_TABLE_SIZE) {
std::cout << "Error: Invalid table size " << table_size << " in " << getFileName(file_path) << " (expected 0-" << MAX_TABLE_SIZE << ")" << '\n';
return false;
}
return true;
}
auto ManageHiScoreTable::readEntry(SDL_IOStream* file, const std::string& file_path, int index, HiScoreEntry& entry) -> bool {
// Leer y validar puntuación
if (SDL_ReadIO(file, &entry.score, sizeof(int)) != sizeof(int)) {
std::cout << "Error: Cannot read score for entry " << index << " in " << getFileName(file_path) << '\n';
return false;
}
if (entry.score < 0 || entry.score > MAX_SCORE) {
std::cout << "Error: Invalid score " << entry.score << " for entry " << index << " in " << getFileName(file_path) << '\n';
return false;
}
// Leer y validar tamaño del nombre
int name_size = 0;
if (SDL_ReadIO(file, &name_size, sizeof(int)) != sizeof(int)) {
std::cout << "Error: Cannot read name size for entry " << index << " in " << getFileName(file_path) << '\n';
return false;
}
if (name_size < 0 || name_size > MAX_NAME_SIZE) {
std::cout << "Error: Invalid name size " << name_size << " for entry " << index << " in " << getFileName(file_path) << '\n';
return false;
}
// Leer el nombre
std::vector<char> name_buffer(name_size + 1);
if (SDL_ReadIO(file, name_buffer.data(), name_size) != static_cast<size_t>(name_size)) {
std::cout << "Error: Cannot read name for entry " << index << " in " << getFileName(file_path) << '\n';
return false;
}
name_buffer[name_size] = '\0';
entry.name = std::string(name_buffer.data());
// Leer one_credit_complete
int occ_value = 0;
if (SDL_ReadIO(file, &occ_value, sizeof(int)) != sizeof(int)) {
std::cout << "Error: Cannot read one_credit_complete for entry " << index << " in " << getFileName(file_path) << '\n';
return false;
}
entry.one_credit_complete = (occ_value != 0);
return true;
}
auto ManageHiScoreTable::verifyChecksum(SDL_IOStream* file, const std::string& file_path, const Table& temp_table) -> bool {
unsigned int stored_checksum = 0;
if (SDL_ReadIO(file, &stored_checksum, sizeof(unsigned int)) != sizeof(unsigned int)) {
std::cout << "Error: Cannot read checksum in " << getFileName(file_path) << '\n';
return false;
}
unsigned int calculated_checksum = calculateChecksum(temp_table);
if (stored_checksum != calculated_checksum) {
std::cout << "Error: Checksum mismatch in " << getFileName(file_path) << " (stored: 0x" << std::hex << std::setw(8) << std::setfill('0') << stored_checksum << ", calculated: 0x" << std::setw(8) << std::setfill('0') << calculated_checksum << std::dec << ") - file is corrupted" << '\n';
return false;
}
return true;
}
// Calcula checksum de la tabla
auto ManageHiScoreTable::calculateChecksum(const Table& table) -> unsigned int {
unsigned int checksum = 0x12345678; // Magic seed
for (const auto& entry : table) {
// Checksum del score
checksum = ((checksum << 5) + checksum) + static_cast<unsigned int>(entry.score);
// Checksum del nombre
for (char c : entry.name) {
checksum = ((checksum << 5) + checksum) + static_cast<unsigned int>(c);
}
// Checksum de one_credit_complete
checksum = ((checksum << 5) + checksum) + (entry.one_credit_complete ? 1U : 0U);
}
return checksum;
}
// Guarda la tabla en un fichero
auto ManageHiScoreTable::saveToFile(const std::string& file_path) -> bool {
auto success = true;
auto* file = SDL_IOFromFile(file_path.c_str(), "w+b");
if (file != nullptr) {
// Escribe magic number "CCAE"
constexpr std::array<char, 4> MAGIC = {'C', 'C', 'A', 'E'};
SDL_WriteIO(file, MAGIC.data(), 4);
// Escribe versión del formato
int version = FILE_VERSION;
SDL_WriteIO(file, &version, sizeof(int));
// Guarda el número de entradas en la tabla
int table_size = static_cast<int>(table_.size());
SDL_WriteIO(file, &table_size, sizeof(int));
// Guarda los datos de cada entrada
for (int i = 0; i < table_size; ++i) {
const HiScoreEntry& entry = table_.at(i);
// Guarda la puntuación
SDL_WriteIO(file, &entry.score, sizeof(int));
// Guarda el tamaño del nombre y luego el nombre
int name_size = static_cast<int>(entry.name.size());
SDL_WriteIO(file, &name_size, sizeof(int));
SDL_WriteIO(file, entry.name.c_str(), name_size);
// Guarda el valor de one_credit_complete como un entero (0 o 1)
int occ_value = entry.one_credit_complete ? 1 : 0;
SDL_WriteIO(file, &occ_value, sizeof(int));
}
// Calcula y escribe el checksum
unsigned int checksum = calculateChecksum(table_);
SDL_WriteIO(file, &checksum, sizeof(unsigned int));
SDL_CloseIO(file);
} else {
std::cout << "Error: Unable to save " << getFileName(file_path) << " file! " << SDL_GetError() << '\n';
success = false;
}
return success;
}

View File

@@ -0,0 +1,59 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_IOStream
#include <string> // Para std::string
#include <vector> // Para std::vector
// --- Estructuras ---
struct HiScoreEntry {
std::string name; // Nombre
int score; // Puntuación
bool one_credit_complete; // Indica si se ha conseguido 1CC
// Constructor
explicit HiScoreEntry(const std::string& name = "", int score = 0, bool one_credit_complete = false)
: name(name.substr(0, 6)),
score(score),
one_credit_complete(one_credit_complete) {}
};
// --- Tipos ---
using Table = std::vector<HiScoreEntry>; // Tabla de puntuaciones
// --- Clase ManageHiScoreTable ---
class ManageHiScoreTable {
public:
// --- Constantes ---
static constexpr int NO_ENTRY = -1;
static constexpr int FILE_VERSION = 1;
static constexpr int MAX_TABLE_SIZE = 100;
static constexpr int MAX_NAME_SIZE = 50;
static constexpr int MAX_SCORE = 999999999;
// --- Constructor y destructor ---
explicit ManageHiScoreTable(Table& table) // Constructor con referencia a tabla
: table_(table) {}
~ManageHiScoreTable() = default; // Destructor
// --- Métodos públicos ---
void clear(); // Resetea la tabla a los valores por defecto
auto add(const HiScoreEntry& entry) -> int; // Añade un elemento a la tabla (devuelve la posición en la que se inserta)
auto loadFromFile(const std::string& file_path) -> bool; // Carga la tabla con los datos de un fichero
auto saveToFile(const std::string& file_path) -> bool; // Guarda la tabla en un fichero
private:
// --- Variables privadas ---
Table& table_; // Referencia a la tabla con los records
// --- Métodos privados ---
void sort(); // Ordena la tabla
static auto calculateChecksum(const Table& table) -> unsigned int; // Calcula checksum de la tabla
// Métodos auxiliares para loadFromFile
static auto validateMagicNumber(SDL_IOStream* file, const std::string& file_path) -> bool;
static auto validateVersion(SDL_IOStream* file, const std::string& file_path) -> bool;
static auto readTableSize(SDL_IOStream* file, const std::string& file_path, int& table_size) -> bool;
static auto readEntry(SDL_IOStream* file, const std::string& file_path, int index, HiScoreEntry& entry) -> bool;
static auto verifyChecksum(SDL_IOStream* file, const std::string& file_path, const Table& temp_table) -> bool;
};

View File

@@ -0,0 +1,809 @@
#include "scoreboard.hpp"
#include <SDL3/SDL.h> // Para SDL_DestroyTexture, SDL_SetRenderDrawColor, SDL_SetRenderTarget, SDL_CreateTexture, SDL_GetRenderTarget, SDL_GetTicks, SDL_RenderClear, SDL_RenderLine, SDL_RenderTexture, SDL_SetTextureBlendMode, SDL_FRect, SDL_BLENDMODE_BLEND, SDL_PixelFormat, SDL_Texture, SDL_TextureAccess
#include <algorithm> // Para max
#include <cmath> // Para roundf
#include <iomanip> // Para operator<<, setfill, setw
#include <iostream>
#include <sstream> // Para basic_ostream, basic_ostringstream, basic_ostream::operator<<, ostringstream
#include "color.hpp"
#include "enter_name.hpp" // Para NAME_SIZE
#include "lang.hpp" // Para getText
#include "param.hpp" // Para Param, ParamScoreboard, param
#include "resource.hpp" // Para Resource
#include "screen.hpp" // Para Screen
#include "sprite.hpp" // Para Sprite
#include "text.hpp" // Para Text, Text::CENTER, Text::COLOR
#include "texture.hpp" // Para Texture
#include "utils.hpp" // Para easeOutCubic
// .at(SINGLETON) Hay que definir las variables estáticas, desde el .h sólo la hemos declarado
Scoreboard* Scoreboard::instance = nullptr;
// .at(SINGLETON) Crearemos el objeto score_board con esta función estática
void Scoreboard::init() {
Scoreboard::instance = new Scoreboard();
}
// .at(SINGLETON) Destruiremos el objeto score_board con esta función estática
void Scoreboard::destroy() {
delete Scoreboard::instance;
}
// .at(SINGLETON) Con este método obtenemos el objeto score_board y podemos trabajar con él
auto Scoreboard::get() -> Scoreboard* {
return Scoreboard::instance;
}
// Constructor
Scoreboard::Scoreboard()
: renderer_(Screen::get()->getRenderer()),
game_power_meter_texture_(Resource::get()->getTexture("game_power_meter.png")),
power_meter_sprite_(std::make_unique<Sprite>(game_power_meter_texture_)),
text_(Resource::get()->getText("8bithud")) {
// Inicializa variables
for (size_t i = 0; i < static_cast<size_t>(Id::SIZE); ++i) {
name_.at(i).clear();
enter_name_.at(i).clear();
selector_pos_.at(i) = 0;
score_.at(i) = 0;
mult_.at(i) = 0;
continue_counter_.at(i) = 0;
carousel_prev_index_.at(i) = -1; // Inicializar a -1 para detectar primera inicialización
enter_name_ref_.at(i) = nullptr;
text_slide_offset_.at(i) = 0.0F;
}
panel_.at(static_cast<size_t>(Id::LEFT)).mode = Mode::SCORE;
panel_.at(static_cast<size_t>(Id::RIGHT)).mode = Mode::SCORE;
panel_.at(static_cast<size_t>(Id::CENTER)).mode = Mode::STAGE_INFO;
// Recalcula las anclas de los elementos
recalculateAnchors();
power_meter_sprite_->setPosition(SDL_FRect{
.x = static_cast<float>(slot4_2_.x - 20),
.y = slot4_2_.y,
.w = 40,
.h = 7});
// Crea la textura de fondo
background_ = nullptr;
createBackgroundTexture();
// Crea las texturas de los paneles
createPanelTextures();
// Rellena la textura de fondo
fillBackgroundTexture();
// Inicializa el ciclo de colores para el nombre
name_color_cycle_ = Colors::generateMirroredCycle(color_.INVERSE(), ColorCycleStyle::VIBRANT);
animated_color_ = name_color_cycle_.at(0);
}
Scoreboard::~Scoreboard() {
if (background_ != nullptr) {
SDL_DestroyTexture(background_);
}
for (auto* texture : panel_texture_) {
if (texture != nullptr) {
SDL_DestroyTexture(texture);
}
}
}
// Configura la animación del carrusel
void Scoreboard::setCarouselAnimation(Id id, int selected_index, EnterName* enter_name_ptr) {
auto idx = static_cast<size_t>(id);
// Guardar referencia
enter_name_ref_.at(idx) = enter_name_ptr;
if ((enter_name_ptr == nullptr) || selected_index < 0) {
return;
}
// ===== Inicialización (primera vez) =====
if (carousel_prev_index_.at(idx) == -1) {
carousel_position_.at(idx) = static_cast<float>(selected_index);
carousel_target_.at(idx) = static_cast<float>(selected_index);
carousel_prev_index_.at(idx) = selected_index;
return;
}
int prev_index = carousel_prev_index_.at(idx);
if (selected_index == prev_index) {
return; // nada que hacer
}
// ===== Bloquear si aún animando =====
if (std::abs(carousel_position_.at(idx) - carousel_target_.at(idx)) > 0.01F) {
return;
}
// ===== Calcular salto circular =====
int delta = selected_index - prev_index;
const int LIST_SIZE = static_cast<int>(enter_name_ptr->getCharacterList().size());
if (delta > LIST_SIZE / 2) {
delta -= LIST_SIZE;
} else if (delta < -LIST_SIZE / 2) {
delta += LIST_SIZE;
}
// ===== Alinear posición actual antes de moverse =====
carousel_position_.at(idx) = std::round(carousel_position_.at(idx));
// ===== Control del salto =====
const int ABS_DELTA = std::abs(delta);
if (ABS_DELTA <= 2) {
// Movimiento corto → animación normal
carousel_target_.at(idx) = carousel_position_.at(idx) + static_cast<float>(delta);
} else {
// Movimiento largo → animado pero limitado en tiempo
// Normalizamos el salto para que visualmente tarde como mucho el doble
const float MAX_DURATION_FACTOR = 2.0F; // máximo 2x la duración de una letra
const float SPEED_SCALE = std::min(1.0F, MAX_DURATION_FACTOR / static_cast<float>(ABS_DELTA));
// Guardamos el destino real
float target = std::round(carousel_position_.at(idx)) + static_cast<float>(delta);
// Interpolaremos más rápido en updateCarouselAnimation usando un factor auxiliar
// guardado en un nuevo vector (si no existe aún, puedes declararlo en la clase):
carousel_speed_scale_.at(idx) = SPEED_SCALE;
// Asignamos el target real
carousel_target_.at(idx) = target;
}
carousel_prev_index_.at(idx) = selected_index;
}
// Establece el modo del panel y gestiona transiciones
void Scoreboard::setMode(Id id, Mode mode) {
auto idx = static_cast<size_t>(id);
// Cambiar el modo
panel_.at(idx).mode = mode;
// Gestionar inicialización/transiciones según el nuevo modo
switch (mode) {
case Mode::SCORE_TO_ENTER_NAME:
// Iniciar animación de transición SCORE → ENTER_NAME
text_slide_offset_.at(idx) = 0.0F;
// Resetear carrusel para que se inicialice correctamente en ENTER_NAME
if (carousel_prev_index_.at(idx) != -1) {
carousel_prev_index_.at(idx) = -1;
}
break;
case Mode::ENTER_NAME:
// Resetear carrusel al entrar en modo de entrada de nombre
// Esto fuerza una reinicialización en la próxima llamada a setCarouselAnimation()
if (carousel_prev_index_.at(idx) != -1) {
carousel_prev_index_.at(idx) = -1;
}
text_slide_offset_.at(idx) = 0.0F;
break;
case Mode::ENTER_TO_SHOW_NAME:
// Iniciar animación de transición ENTER_NAME → SHOW_NAME
text_slide_offset_.at(idx) = 0.0F;
break;
case Mode::SHOW_NAME:
// Asegurar que la animación está completa
text_slide_offset_.at(idx) = 1.0F;
break;
// Otros modos no requieren inicialización especial
default:
break;
}
}
// Transforma un valor numérico en una cadena de 7 cifras
auto Scoreboard::updateScoreText(int num) -> std::string {
std::ostringstream oss;
oss << std::setw(7) << std::setfill('0') << num;
return oss.str();
}
// Actualiza el contador
void Scoreboard::updateTimeCounter() {
constexpr int TICKS_SPEED = 100;
if (SDL_GetTicks() - ticks_ > TICKS_SPEED) {
ticks_ = SDL_GetTicks();
++time_counter_;
}
}
// Actualiza el índice del color animado del nombre
void Scoreboard::updateNameColorIndex() {
constexpr Uint64 COLOR_UPDATE_INTERVAL = 100; // 100ms entre cambios de color
if (SDL_GetTicks() - name_color_last_update_ >= COLOR_UPDATE_INTERVAL) {
++name_color_index_;
name_color_last_update_ = SDL_GetTicks();
}
// Precalcular el color actual del ciclo
animated_color_ = name_color_cycle_.at(name_color_index_ % name_color_cycle_.size());
}
// Actualiza la animación del carrusel
void Scoreboard::updateCarouselAnimation(float delta_time) {
const float BASE_SPEED = 8.0F; // Posiciones por segundo
for (size_t i = 0; i < carousel_position_.size(); ++i) {
// Solo animar si no hemos llegado al target
if (std::abs(carousel_position_.at(i) - carousel_target_.at(i)) > 0.01F) {
// Determinar dirección
float direction = (carousel_target_.at(i) > carousel_position_.at(i)) ? 1.0F : -1.0F;
// Calcular movimiento
float speed = BASE_SPEED / carousel_speed_scale_.at(i); // ajusta según salto
float movement = speed * delta_time * direction;
// Mover, pero no sobrepasar el target
float new_position = carousel_position_.at(i) + movement;
// Clamp para no sobrepasar
if (direction > 0) {
carousel_position_.at(i) = std::min(new_position, carousel_target_.at(i));
} else {
carousel_position_.at(i) = std::max(new_position, carousel_target_.at(i));
}
} else {
// Forzar al target exacto cuando estamos muy cerca
carousel_position_.at(i) = carousel_target_.at(i);
carousel_speed_scale_.at(i) = 1.0F; // restaurar velocidad normal
}
}
}
// Actualiza las animaciones de deslizamiento de texto
void Scoreboard::updateTextSlideAnimation(float delta_time) {
for (size_t i = 0; i < static_cast<size_t>(Id::SIZE); ++i) {
Mode current_mode = panel_.at(i).mode;
if (current_mode == Mode::SCORE_TO_ENTER_NAME) {
// Incrementar progreso de animación SCORE → ENTER_NAME (0.0 a 1.0)
text_slide_offset_.at(i) += delta_time / TEXT_SLIDE_DURATION;
// Terminar animación y cambiar a ENTER_NAME cuando se complete
if (text_slide_offset_.at(i) >= 1.0F) {
setMode(static_cast<Id>(i), Mode::ENTER_NAME);
}
} else if (current_mode == Mode::ENTER_TO_SHOW_NAME) {
// Incrementar progreso de animación ENTER_NAME → SHOW_NAME (0.0 a 1.0)
text_slide_offset_.at(i) += delta_time / TEXT_SLIDE_DURATION;
// Terminar animación y cambiar a SHOW_NAME cuando se complete
if (text_slide_offset_.at(i) >= 1.0F) {
setMode(static_cast<Id>(i), Mode::SHOW_NAME);
}
}
}
}
// Actualiza las animaciones de pulso de los paneles
void Scoreboard::updatePanelPulses(float delta_time) {
for (size_t i = 0; i < static_cast<size_t>(Id::SIZE); ++i) {
auto& pulse = panel_pulse_.at(i);
if (!pulse.active) {
continue;
}
// Avanzar el tiempo transcurrido
pulse.elapsed_s += delta_time;
// Desactivar el pulso si ha terminado
if (pulse.elapsed_s >= pulse.duration_s) {
pulse.active = false;
pulse.elapsed_s = 0.0F;
}
}
}
// Activa un pulso en el panel especificado
void Scoreboard::triggerPanelPulse(Id id, float duration_s) {
auto idx = static_cast<size_t>(id);
panel_pulse_.at(idx).active = true;
panel_pulse_.at(idx).elapsed_s = 0.0F;
panel_pulse_.at(idx).duration_s = duration_s;
}
// Actualiza la lógica del marcador
void Scoreboard::update(float delta_time) {
updateTimeCounter();
updateNameColorIndex();
updateCarouselAnimation(delta_time);
updateTextSlideAnimation(delta_time);
updatePanelPulses(delta_time);
fillBackgroundTexture(); // Renderizar DESPUÉS de actualizar
}
// Pinta el marcador
void Scoreboard::render() {
SDL_RenderTexture(renderer_, background_, nullptr, &rect_);
}
// Establece el valor de la variable
void Scoreboard::setColor(Color color) {
// Actualiza las variables de colores
color_ = color;
text_color1_ = param.scoreboard.text_autocolor ? color_.LIGHTEN(100) : param.scoreboard.text_color1;
text_color2_ = param.scoreboard.text_autocolor ? color_.LIGHTEN(150) : param.scoreboard.text_color2;
// Aplica los colores
power_meter_sprite_->getTexture()->setColor(text_color2_);
fillBackgroundTexture();
name_color_cycle_ = Colors::generateMirroredCycle(color_.INVERSE(), ColorCycleStyle::VIBRANT);
}
// Establece el valor de la variable
void Scoreboard::setPos(SDL_FRect rect) {
rect_ = rect;
recalculateAnchors(); // Recalcula las anclas de los elementos
createBackgroundTexture(); // Crea la textura de fondo
createPanelTextures(); // Crea las texturas de los paneles
fillBackgroundTexture(); // Rellena la textura de fondo
}
// Rellena los diferentes paneles del marcador
void Scoreboard::fillPanelTextures() {
// Guarda a donde apunta actualmente el renderizador
auto* temp = SDL_GetRenderTarget(renderer_);
// Genera el contenido de cada panel_
for (size_t i = 0; i < static_cast<int>(Id::SIZE); ++i) {
// Cambia el destino del renderizador
SDL_SetRenderTarget(renderer_, panel_texture_.at(i));
// Calcula el color de fondo del panel (puede tener pulso activo)
Color background_color = Color(0, 0, 0, 0); // Transparente por defecto
const auto& pulse = panel_pulse_.at(i);
if (pulse.active) {
// Calcular el progreso del pulso (0.0 a 1.0 y de vuelta a 0.0)
float progress = pulse.elapsed_s / pulse.duration_s;
// Crear curva de ida y vuelta (0 → 1 → 0)
float pulse_intensity;
if (progress < 0.5F) {
pulse_intensity = progress * 2.0F; // 0.0 a 1.0
} else {
pulse_intensity = (1.0F - progress) * 2.0F; // 1.0 a 0.0
}
// Interpolar entre color base y color aclarado
Color target_color = color_.LIGHTEN(PANEL_PULSE_LIGHTEN_AMOUNT);
// Color target_color = color_.INVERSE();
background_color = color_.LERP(target_color, pulse_intensity);
background_color.a = 255; // Opaco durante el pulso
}
// Dibuja el fondo de la textura
SDL_SetRenderDrawColor(renderer_, background_color.r, background_color.g, background_color.b, background_color.a);
SDL_RenderClear(renderer_);
renderPanelContent(i);
}
// Deja el renderizador apuntando donde estaba
SDL_SetRenderTarget(renderer_, temp);
}
void Scoreboard::renderPanelContent(size_t panel_index) {
switch (panel_.at(panel_index).mode) {
case Mode::SCORE:
renderScoreMode(panel_index);
break;
case Mode::DEMO:
renderDemoMode();
break;
case Mode::WAITING:
renderWaitingMode();
break;
case Mode::GAME_OVER:
renderGameOverMode();
break;
case Mode::STAGE_INFO:
renderStageInfoMode();
break;
case Mode::CONTINUE:
renderContinueMode(panel_index);
break;
case Mode::SCORE_TO_ENTER_NAME:
renderScoreToEnterNameMode(panel_index);
break;
case Mode::ENTER_NAME:
renderEnterNameMode(panel_index);
break;
case Mode::ENTER_TO_SHOW_NAME:
renderEnterToShowNameMode(panel_index);
break;
case Mode::SHOW_NAME:
renderShowNameMode(panel_index);
break;
case Mode::GAME_COMPLETED:
renderGameCompletedMode(panel_index);
break;
default:
break;
}
}
void Scoreboard::renderScoreMode(size_t panel_index) {
// SCORE
text_->writeDX(Text::COLOR | Text::CENTER, slot4_1_.x, slot4_1_.y, name_.at(panel_index), 1, text_color1_);
text_->writeDX(Text::COLOR | Text::CENTER, slot4_2_.x, slot4_2_.y, updateScoreText(score_.at(panel_index)), 1, text_color2_);
// MULT
text_->writeDX(Text::COLOR | Text::CENTER, slot4_3_.x, slot4_3_.y, Lang::getText("[SCOREBOARD] 3"), 1, text_color1_);
text_->writeDX(Text::COLOR | Text::CENTER, slot4_4_.x, slot4_4_.y, "x" + std::to_string(mult_.at(panel_index)).substr(0, 3), 1, text_color2_);
}
void Scoreboard::renderDemoMode() {
// DEMO MODE
text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y + 4, Lang::getText("[SCOREBOARD] 6"), 1, text_color1_);
// PRESS START TO PLAY
if (time_counter_ % 10 < 8) {
text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y - 2, Lang::getText("[SCOREBOARD] 8"), 1, text_color1_);
text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y - 2, Lang::getText("[SCOREBOARD] 9"), 1, text_color1_);
}
}
void Scoreboard::renderWaitingMode() {
// GAME OVER
text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y + 4, Lang::getText("[SCOREBOARD] 7"), 1, text_color1_);
// PRESS START TO PLAY
if (time_counter_ % 10 < 8) {
text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y - 2, Lang::getText("[SCOREBOARD] 8"), 1, text_color1_);
text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y - 2, Lang::getText("[SCOREBOARD] 9"), 1, text_color1_);
}
}
void Scoreboard::renderGameOverMode() {
// GAME OVER
text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y + 4, Lang::getText("[SCOREBOARD] 7"), 1, text_color1_);
// PLEASE WAIT
if (time_counter_ % 10 < 8) {
text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y - 2, Lang::getText("[SCOREBOARD] 12"), 1, text_color1_);
text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y - 2, Lang::getText("[SCOREBOARD] 13"), 1, text_color1_);
}
}
void Scoreboard::renderStageInfoMode() {
// STAGE
text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y, Lang::getText("[SCOREBOARD] 5") + " " + std::to_string(stage_), 1, text_color1_);
// POWERMETER
power_meter_sprite_->setSpriteClip(0, 0, 40, 7);
power_meter_sprite_->render();
power_meter_sprite_->setSpriteClip(40, 0, static_cast<int>(power_ * 40.0F), 7);
power_meter_sprite_->render();
// HI-SCORE
text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y, Lang::getText("[SCOREBOARD] 4"), 1, text_color1_);
const std::string NAME = hi_score_name_.empty() ? "" : hi_score_name_ + " - ";
text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y, NAME + updateScoreText(hi_score_), 1, text_color2_);
}
void Scoreboard::renderContinueMode(size_t panel_index) {
// SCORE
text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y, name_.at(panel_index), 1, text_color1_);
text_->writeDX(Text::CENTER | Text::COLOR, slot4_2_.x, slot4_2_.y, updateScoreText(score_.at(panel_index)), 1, text_color2_);
// CONTINUE
text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y, Lang::getText("[SCOREBOARD] 10"), 1, text_color1_);
text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y, std::to_string(continue_counter_.at(panel_index)), 1, text_color2_);
}
void Scoreboard::renderScoreToEnterNameMode(size_t panel_index) {
// Calcular progreso suavizado de la animación (0.0 a 1.0)
const auto T = static_cast<float>(easeInOutSine(text_slide_offset_.at(panel_index)));
// Calcular desplazamientos reales entre slots (no son exactamente ROW_SIZE)
const float DELTA_1_TO_2 = slot4_2_.y - slot4_1_.y; // Diferencia real entre ROW1 y ROW2
const float DELTA_2_TO_3 = slot4_3_.y - slot4_2_.y; // Diferencia real entre ROW2 y ROW3
const float DELTA_3_TO_4 = slot4_4_.y - slot4_3_.y; // Diferencia real entre ROW3 y ROW4
// ========== Texto que SALE hacia arriba ==========
// name_ (sale desde ROW1 hacia arriba)
text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y - (T * DELTA_1_TO_2), name_.at(panel_index), 1, text_color1_);
// ========== Textos que SE MUEVEN hacia arriba ==========
// score_ (se mueve de ROW2 a ROW1)
text_->writeDX(Text::CENTER | Text::COLOR, slot4_2_.x, slot4_2_.y - (T * DELTA_1_TO_2), updateScoreText(score_.at(panel_index)), 1, text_color2_);
// "ENTER NAME" (se mueve de ROW3 a ROW2)
text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y - (T * DELTA_2_TO_3), Lang::getText("[SCOREBOARD] 11"), 1, text_color1_);
// enter_name_ (se mueve de ROW4 a ROW3)
text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y - (T * DELTA_3_TO_4), enter_name_.at(panel_index), 1, text_color2_);
// ========== Elemento que ENTRA desde abajo ==========
// CARRUSEL (entra desde debajo de ROW4 hacia ROW4)
renderCarousel(panel_index, slot4_4_.x, static_cast<int>(slot4_4_.y + DELTA_3_TO_4 - (T * DELTA_3_TO_4)));
}
void Scoreboard::renderEnterNameMode(size_t panel_index) {
/*
// SCORE
text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y, name_.at(panel_index), 1, text_color1_);
text_->writeDX(Text::CENTER | Text::COLOR, slot4_2_.x, slot4_2_.y, updateScoreText(score_.at(panel_index)), 1, text_color2_);
// ENTER NAME
text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y, Lang::getText("[SCOREBOARD] 11"), 1, text_color1_);
renderNameInputField(panel_index);
*/
// SCORE
text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y, updateScoreText(score_.at(panel_index)), 1, text_color2_);
// ENTER NAME
text_->writeDX(Text::CENTER | Text::COLOR, slot4_2_.x, slot4_2_.y, Lang::getText("[SCOREBOARD] 11"), 1, text_color1_);
// NAME
text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y, enter_name_.at(panel_index), 1, text_color2_);
// CARRUSEL
renderCarousel(panel_index, slot4_4_.x, slot4_4_.y);
}
void Scoreboard::renderEnterToShowNameMode(size_t panel_index) {
// Calcular progreso suavizado de la animación (0.0 a 1.0)
const auto T = static_cast<float>(easeInOutSine(text_slide_offset_.at(panel_index)));
// Calcular desplazamientos reales entre slots (no son exactamente ROW_SIZE)
const float DELTA_1_TO_2 = slot4_2_.y - slot4_1_.y; // Diferencia real entre ROW1 y ROW2
const float DELTA_2_TO_3 = slot4_3_.y - slot4_2_.y; // Diferencia real entre ROW2 y ROW3
const float DELTA_3_TO_4 = slot4_4_.y - slot4_3_.y; // Diferencia real entre ROW3 y ROW4
// ========== Texto que ENTRA desde arriba ==========
// name_ (entra desde arriba hacia ROW1)
// Debe venir desde donde estaría ROW0, que está a delta_1_to_2 píxeles arriba de ROW1
text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y + (T * DELTA_1_TO_2) - DELTA_1_TO_2, name_.at(panel_index), 1, text_color1_);
// ========== Textos que SE MUEVEN (renderizar UNA sola vez) ==========
// SCORE (se mueve de ROW1 a ROW2)
text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y + (T * DELTA_1_TO_2), updateScoreText(score_.at(panel_index)), 1, text_color2_);
// "ENTER NAME" (se mueve de ROW2 a ROW3)
text_->writeDX(Text::CENTER | Text::COLOR, slot4_2_.x, slot4_2_.y + (T * DELTA_2_TO_3), Lang::getText("[SCOREBOARD] 11"), 1, text_color1_);
// enter_name_ (se mueve de ROW3 a ROW4)
text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y + (T * DELTA_3_TO_4), enter_name_.at(panel_index), 1, text_color2_);
// ========== Elemento que SALE hacia abajo ==========
// CARRUSEL (sale desde ROW4 hacia abajo, fuera de pantalla)
renderCarousel(panel_index, slot4_4_.x, static_cast<int>(slot4_4_.y + (T * DELTA_3_TO_4)));
}
void Scoreboard::renderShowNameMode(size_t panel_index) {
// NOMBRE DEL JUGADOR
text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y, name_.at(panel_index), 1, text_color1_);
// SCORE
text_->writeDX(Text::CENTER | Text::COLOR, slot4_2_.x, slot4_2_.y, updateScoreText(score_.at(panel_index)), 1, text_color2_);
// "ENTER NAME"
text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y, Lang::getText("[SCOREBOARD] 11"), 1, text_color1_);
// NOMBRE INTRODUCIDO (con color animado)
text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y, enter_name_.at(panel_index), 1, animated_color_);
}
void Scoreboard::renderGameCompletedMode(size_t panel_index) {
// GAME OVER
text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y + 4, Lang::getText("[SCOREBOARD] 7"), 1, text_color1_);
// SCORE
if (time_counter_ % 10 < 8) {
text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y - 2, Lang::getText("[SCOREBOARD] 14"), 1, text_color1_);
text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y - 2, updateScoreText(score_.at(panel_index)), 1, text_color2_);
}
}
// Rellena la textura de fondo
void Scoreboard::fillBackgroundTexture() {
// Rellena los diferentes paneles del marcador
fillPanelTextures();
// Cambia el destino del renderizador
SDL_Texture* temp = SDL_GetRenderTarget(renderer_);
SDL_SetRenderTarget(renderer_, background_);
// Dibuja el fondo del marcador
SDL_SetRenderDrawColor(renderer_, color_.r, color_.g, color_.b, 255);
SDL_RenderClear(renderer_);
// Copia las texturas de los paneles
for (int i = 0; i < static_cast<int>(Id::SIZE); ++i) {
SDL_RenderTexture(renderer_, panel_texture_.at(i), nullptr, &panel_.at(i).pos);
}
// Dibuja la linea que separa la zona de juego del marcador
renderSeparator();
// Deja el renderizador apuntando donde estaba
SDL_SetRenderTarget(renderer_, temp);
}
// Recalcula las anclas de los elementos
void Scoreboard::recalculateAnchors() {
// Recalcula la posición y el tamaño de los paneles
const float PANEL_WIDTH = rect_.w / static_cast<float>(static_cast<int>(Id::SIZE));
for (int i = 0; i < static_cast<int>(Id::SIZE); ++i) {
panel_.at(i).pos.x = roundf(PANEL_WIDTH * i);
panel_.at(i).pos.y = 0;
panel_.at(i).pos.w = roundf(PANEL_WIDTH * (i + 1)) - panel_.at(i).pos.x;
panel_.at(i).pos.h = rect_.h;
}
// Constantes para definir las zonas del panel_: 4 filas y 1 columna
const int ROW_SIZE = rect_.h / 4;
const int TEXT_HEIGHT = 7;
// Filas
const float ROW1 = 1 + (ROW_SIZE * 0) + (TEXT_HEIGHT / 2);
const float ROW2 = 1 + (ROW_SIZE * 1) + (TEXT_HEIGHT / 2) - 1;
const float ROW3 = 1 + (ROW_SIZE * 2) + (TEXT_HEIGHT / 2) - 2;
const float ROW4 = 1 + (ROW_SIZE * 3) + (TEXT_HEIGHT / 2) - 3;
// Columna
const float COL = PANEL_WIDTH / 2;
// Slots de 4
slot4_1_ = {.x = COL, .y = ROW1};
slot4_2_ = {.x = COL, .y = ROW2};
slot4_3_ = {.x = COL, .y = ROW3};
slot4_4_ = {.x = COL, .y = ROW4};
// Primer cuadrado para poner el nombre de record
const int ENTER_NAME_LENGTH = text_->length(std::string(EnterName::MAX_NAME_SIZE, 'A'));
enter_name_pos_.x = COL - (ENTER_NAME_LENGTH / 2);
enter_name_pos_.y = ROW4;
// Recoloca los sprites
if (power_meter_sprite_) {
power_meter_sprite_->setX(slot4_2_.x - 20);
power_meter_sprite_->setY(slot4_2_.y);
}
}
// Crea la textura de fondo
void Scoreboard::createBackgroundTexture() {
// Elimina la textura en caso de existir
if (background_ != nullptr) {
SDL_DestroyTexture(background_);
}
// Recrea la textura de fondo
background_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, rect_.w, rect_.h);
SDL_SetTextureBlendMode(background_, SDL_BLENDMODE_BLEND);
}
// Crea las texturas de los paneles
void Scoreboard::createPanelTextures() {
// Elimina las texturas en caso de existir
for (auto* texture : panel_texture_) {
if (texture != nullptr) {
SDL_DestroyTexture(texture);
}
}
panel_texture_.clear();
// Crea las texturas para cada panel_
for (auto& i : panel_) {
SDL_Texture* tex = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, i.pos.w, i.pos.h);
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
panel_texture_.push_back(tex);
}
}
// Dibuja la linea que separa la zona de juego del marcador
void Scoreboard::renderSeparator() {
// Dibuja la linea que separa el marcador de la zona de juego
auto color = param.scoreboard.separator_autocolor ? color_.DARKEN() : param.scoreboard.separator_color;
SDL_SetRenderDrawColor(renderer_, color.r, color.g, color.b, 255);
SDL_RenderLine(renderer_, 0, 0, rect_.w, 0);
}
// Pinta el carrusel de caracteres con efecto de color LERP y animación suave
void Scoreboard::renderCarousel(size_t panel_index, int center_x, int y) {
// Obtener referencia a EnterName
EnterName* enter_name = enter_name_ref_.at(panel_index);
if (enter_name == nullptr) {
return;
}
// Obtener la lista completa de caracteres
const std::string& char_list = enter_name->getCharacterList();
if (char_list.empty()) {
return;
}
// --- Parámetros del carrusel ---
constexpr int EXTRA_SPACING = 2;
constexpr int HALF_VISIBLE = CAROUSEL_VISIBLE_LETTERS / 2; // 4 letras a cada lado
// Posición flotante actual del carrusel (índice en la lista de caracteres)
float carousel_pos = carousel_position_.at(panel_index);
const int CHAR_LIST_SIZE = static_cast<int>(char_list.size());
// Calcular ancho promedio de una letra (asumimos ancho uniforme)
const int AVG_CHAR_WIDTH = text_->getCharacterSize();
const int CHAR_STEP = AVG_CHAR_WIDTH + EXTRA_SPACING;
// --- Corrección visual de residuales flotantes (evita “baile”) ---
float frac = carousel_pos - std::floor(carousel_pos);
if (frac > 0.999F || frac < 0.001F) {
carousel_pos = std::round(carousel_pos);
frac = 0.0F;
}
const float FRACTIONAL_OFFSET = frac;
const int PIXEL_OFFSET = static_cast<int>((FRACTIONAL_OFFSET * CHAR_STEP) + 0.5F);
// Índice base en la lista de caracteres (posición central)
const int BASE_INDEX = static_cast<int>(std::floor(carousel_pos));
// Calcular posición X inicial (centrar las 9 letras visibles)
int start_x = center_x - (HALF_VISIBLE * CHAR_STEP) - (AVG_CHAR_WIDTH / 2) - PIXEL_OFFSET;
// === Renderizar las letras visibles del carrusel ===
for (int i = -HALF_VISIBLE; i <= HALF_VISIBLE; ++i) {
// Índice real en character_list_ (con wrap-around circular)
int char_index = BASE_INDEX + i;
char_index = char_index % CHAR_LIST_SIZE;
if (char_index < 0) {
char_index += CHAR_LIST_SIZE;
}
// --- Calcular distancia circular correcta (corregido el bug de wrap) ---
float normalized_pos = std::fmod(carousel_pos, static_cast<float>(CHAR_LIST_SIZE));
if (normalized_pos < 0.0F) {
normalized_pos += static_cast<float>(CHAR_LIST_SIZE);
}
float diff = std::abs(static_cast<float>(char_index) - normalized_pos);
if (diff > static_cast<float>(CHAR_LIST_SIZE) / 2.0F) {
diff = static_cast<float>(CHAR_LIST_SIZE) - diff;
}
const float DISTANCE_FROM_CENTER = diff;
// --- Seleccionar color con LERP según la distancia ---
Color letter_color;
if (DISTANCE_FROM_CENTER < 0.5F) {
// Letra central → transiciona hacia animated_color_
float lerp_to_animated = DISTANCE_FROM_CENTER / 0.5F; // 0.0 a 1.0
letter_color = animated_color_.LERP(text_color1_, lerp_to_animated);
} else {
// Letras alejadas → degradan hacia color_ base
float base_lerp = (DISTANCE_FROM_CENTER - 0.5F) / (HALF_VISIBLE - 0.5F);
base_lerp = std::min(base_lerp, 1.0F);
const float LERP_FACTOR = base_lerp * 0.85F;
letter_color = text_color1_.LERP(color_, LERP_FACTOR);
}
// Calcular posición X de la letra
const int LETTER_X = start_x + ((i + HALF_VISIBLE) * CHAR_STEP);
// Renderizar la letra
std::string single_char(1, char_list[char_index]);
text_->writeDX(Text::COLOR, LETTER_X, y, single_char, 1, letter_color);
}
}

View File

@@ -0,0 +1,169 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FPoint, SDL_GetTicks, SDL_FRect, SDL_Texture, SDL_Renderer, Uint64
#include <array> // Para array
#include <cstddef> // Para size_t
#include <memory> // Para shared_ptr, unique_ptr
#include <string> // Para string, basic_string
#include <vector> // Para vector
// Forward declarations
class EnterName;
#include "color.hpp" // Para Color
class Sprite;
class Text;
class Texture;
// --- Clase Scoreboard ---
class Scoreboard {
public:
// --- Enums ---
enum class Id : size_t {
LEFT = 0,
CENTER = 1,
RIGHT = 2,
SIZE = 3
};
enum class Mode : int {
SCORE,
STAGE_INFO,
CONTINUE,
WAITING,
GAME_OVER,
DEMO,
SCORE_TO_ENTER_NAME, // Transición animada: SCORE → ENTER_NAME
ENTER_NAME,
ENTER_TO_SHOW_NAME, // Transición animada: ENTER_NAME → SHOW_NAME
SHOW_NAME,
GAME_COMPLETED,
NUM_MODES,
};
// --- Estructuras ---
struct Panel {
Mode mode; // Modo en el que se encuentra el panel
SDL_FRect pos; // Posición donde dibujar el panel dentro del marcador
};
struct PanelPulse {
bool active = false; // Si el pulso está activo
float elapsed_s = 0.0F; // Tiempo transcurrido desde el inicio
float duration_s = 0.5F; // Duración total del pulso
};
// --- Métodos de singleton ---
static void init(); // Crea el objeto Scoreboard
static void destroy(); // Libera el objeto Scoreboard
static auto get() -> Scoreboard*; // Obtiene el puntero al objeto Scoreboard
// --- Métodos principales ---
void update(float delta_time); // Actualiza la lógica del marcador
void render(); // Pinta el marcador
// --- Setters ---
void setColor(Color color); // Establece el color del marcador
void setPos(SDL_FRect rect); // Establece la posición y tamaño del marcador
void setContinue(Id id, int continue_counter) { continue_counter_.at(static_cast<size_t>(id)) = continue_counter; }
void setHiScore(int hi_score) { hi_score_ = hi_score; }
void setHiScoreName(const std::string& name) { hi_score_name_ = name; }
void setMode(Id id, Mode mode); // Establece el modo del panel y gestiona transiciones
void setMult(Id id, float mult) { mult_.at(static_cast<size_t>(id)) = mult; }
void setName(Id id, const std::string& name) { name_.at(static_cast<size_t>(id)) = name; }
void setPower(float power) { power_ = power; }
void setEnterName(Id id, const std::string& enter_name) { enter_name_.at(static_cast<size_t>(id)) = enter_name; }
void setCharacterSelected(Id id, const std::string& character_selected) { character_selected_.at(static_cast<size_t>(id)) = character_selected; }
void setCarouselAnimation(Id id, int selected_index, EnterName* enter_name_ptr); // Configura la animación del carrusel
void setScore(Id id, int score) { score_.at(static_cast<size_t>(id)) = score; }
void setSelectorPos(Id id, int pos) { selector_pos_.at(static_cast<size_t>(id)) = pos; }
void setStage(int stage) { stage_ = stage; }
void triggerPanelPulse(Id id, float duration_s = 0.5F); // Activa un pulso en el panel especificado
private:
// --- Objetos y punteros ---
SDL_Renderer* renderer_; // El renderizador de la ventana
std::shared_ptr<Texture> game_power_meter_texture_; // Textura con el marcador de poder de la fase
std::unique_ptr<Sprite> power_meter_sprite_; // Sprite para el medidor de poder de la fase
std::shared_ptr<Text> text_; // Fuente para el marcador del juego
SDL_Texture* background_ = nullptr; // Textura para dibujar el marcador
std::vector<SDL_Texture*> panel_texture_; // Texturas para dibujar cada panel
// --- Variables de estado ---
std::array<std::string, static_cast<int>(Id::SIZE)> name_ = {}; // Nombre de cada jugador
std::array<std::string, static_cast<int>(Id::SIZE)> enter_name_ = {}; // Nombre introducido para la tabla de records
std::array<std::string, static_cast<int>(Id::SIZE)> character_selected_ = {}; // Caracter seleccionado
std::array<EnterName*, static_cast<int>(Id::SIZE)> enter_name_ref_ = {}; // Referencias a EnterName para obtener character_list_
std::array<float, static_cast<int>(Id::SIZE)> carousel_position_ = {}; // Posición actual del carrusel (índice en character_list_)
std::array<float, static_cast<int>(Id::SIZE)> carousel_target_ = {}; // Posición objetivo del carrusel
std::array<int, static_cast<int>(Id::SIZE)> carousel_prev_index_ = {}; // Índice previo para detectar cambios
std::array<float, static_cast<int>(Id::SIZE)> text_slide_offset_ = {}; // Progreso de animación de deslizamiento (0.0 a 1.0)
std::array<Panel, static_cast<int>(Id::SIZE)> panel_ = {}; // Lista con todos los paneles del marcador
std::array<PanelPulse, static_cast<int>(Id::SIZE)> panel_pulse_ = {}; // Estado de pulso para cada panel
Colors::Cycle name_color_cycle_; // Ciclo de colores para destacar el nombre una vez introducido
Color animated_color_; // Color actual animado (ciclo automático cada 100ms)
std::string hi_score_name_; // Nombre del jugador con la máxima puntuación
SDL_FRect rect_ = {.x = 0, .y = 0, .w = 320, .h = 40}; // Posición y dimensiones del marcador
Color color_; // Color del marcador
std::array<size_t, static_cast<int>(Id::SIZE)> selector_pos_ = {}; // Posición del selector de letra para introducir el nombre
std::array<int, static_cast<int>(Id::SIZE)> score_ = {}; // Puntuación de los jugadores
std::array<int, static_cast<int>(Id::SIZE)> continue_counter_ = {}; // Tiempo para continuar de los jugadores
std::array<float, static_cast<int>(Id::SIZE)> mult_ = {}; // Multiplicador de los jugadores
Uint64 ticks_ = SDL_GetTicks(); // Variable donde almacenar el valor de SDL_GetTicks()
int stage_ = 1; // Número de fase actual
int hi_score_ = 0; // Máxima puntuación
int time_counter_ = 0; // Contador de segundos
Uint32 name_color_index_ = 0; // Índice actual del color en el ciclo de animación del nombre
Uint64 name_color_last_update_ = 0; // Último tick de actualización del color del nombre
float power_ = 0.0F; // Poder actual de la fase
std::array<float, static_cast<size_t>(Id::SIZE)> carousel_speed_scale_ = {1.0F, 1.0F, 1.0F};
// --- Constantes ---
static constexpr int CAROUSEL_VISIBLE_LETTERS = 9;
static constexpr float TEXT_SLIDE_DURATION = 0.3F; // Duración de la animación de deslizamiento en segundos
static constexpr int PANEL_PULSE_LIGHTEN_AMOUNT = 40; // Cantidad de aclarado para el pulso del panel
// --- Variables de aspecto ---
Color text_color1_, text_color2_; // Colores para los marcadores del texto;
// --- Puntos predefinidos para colocar elementos en los paneles ---
SDL_FPoint slot4_1_, slot4_2_, slot4_3_, slot4_4_;
SDL_FPoint enter_name_pos_;
// --- Métodos internos ---
void recalculateAnchors(); // Recalcula las anclas de los elementos
static auto updateScoreText(int num) -> std::string; // Transforma un valor numérico en una cadena de 7 cifras
void createBackgroundTexture(); // Crea la textura de fondo
void createPanelTextures(); // Crea las texturas de los paneles
void fillPanelTextures(); // Rellena los diferentes paneles del marcador
void fillBackgroundTexture(); // Rellena la textura de fondo
void updateTimeCounter(); // Actualiza el contador
void updateNameColorIndex(); // Actualiza el índice del color animado del nombre
void updateCarouselAnimation(float delta_time); // Actualiza la animación del carrusel
void updateTextSlideAnimation(float delta_time); // Actualiza la animación de deslizamiento de texto
void updatePanelPulses(float delta_time); // Actualiza las animaciones de pulso de los paneles
void renderSeparator(); // Dibuja la línea que separa la zona de juego del marcador
void renderPanelContent(size_t panel_index);
void renderScoreMode(size_t panel_index);
void renderDemoMode();
void renderWaitingMode();
void renderGameOverMode();
void renderStageInfoMode();
void renderContinueMode(size_t panel_index);
void renderScoreToEnterNameMode(size_t panel_index); // Renderiza la transición SCORE → ENTER_NAME
void renderEnterNameMode(size_t panel_index);
void renderNameInputField(size_t panel_index);
void renderEnterToShowNameMode(size_t panel_index); // Renderiza la transición ENTER_NAME → SHOW_NAME
void renderShowNameMode(size_t panel_index);
void renderGameCompletedMode(size_t panel_index);
void renderCarousel(size_t panel_index, int center_x, int y); // Pinta el carrusel de caracteres con colores LERP
// --- Constructores y destructor privados (singleton) ---
Scoreboard(); // Constructor privado
~Scoreboard(); // Destructor privado
// --- Instancia singleton ---
static Scoreboard* instance; // Instancia única de Scoreboard
};

View File

@@ -0,0 +1,367 @@
#include "stage.hpp"
#include <algorithm> // Para max, min
#include <exception> // Para exception
#include <fstream> // Para basic_istream, basic_ifstream, ifstream, stringstream
#include <sstream> // Para basic_stringstream
#include <utility> // Para move
// Implementación de StageData
StageData::StageData(int power_to_complete, int min_menace, int max_menace, std::string name)
: status_(StageStatus::LOCKED),
name_(std::move(name)),
power_to_complete_(power_to_complete),
min_menace_(min_menace),
max_menace_(max_menace) {}
// Implementación de StageManager
StageManager::StageManager()
: power_change_callback_(nullptr),
power_collection_state_(PowerCollectionState::ENABLED),
current_stage_index_(0),
current_power_(0),
total_power_(0) { initialize(); }
void StageManager::initialize() {
stages_.clear();
createDefaultStages();
reset();
}
void StageManager::initialize(const std::string& stages_file) {
stages_.clear();
// Intentar cargar desde archivo, si falla usar valores predeterminados
if (!loadStagesFromFile(stages_file)) {
createDefaultStages();
}
reset();
}
void StageManager::reset() {
current_power_ = 0;
total_power_ = 0;
current_stage_index_ = 0;
power_collection_state_ = PowerCollectionState::ENABLED;
updateStageStatuses();
}
void StageManager::createDefaultStages() {
// Crear las 10 fases predeterminadas con dificultad progresiva
stages_.emplace_back(200, 7 + (4 * 1), 7 + (4 * 3), "Tutorial");
stages_.emplace_back(300, 7 + (4 * 2), 7 + (4 * 4), "Primeros pasos");
stages_.emplace_back(600, 7 + (4 * 3), 7 + (4 * 5), "Intensificación");
stages_.emplace_back(600, 7 + (4 * 3), 7 + (4 * 5), "Persistencia");
stages_.emplace_back(600, 7 + (4 * 4), 7 + (4 * 6), "Desafío medio");
stages_.emplace_back(600, 7 + (4 * 4), 7 + (4 * 6), "Resistencia");
stages_.emplace_back(650, 7 + (4 * 5), 7 + (4 * 7), "Aproximación final");
stages_.emplace_back(750, 7 + (4 * 5), 7 + (4 * 7), "Penúltimo obstáculo");
stages_.emplace_back(850, 7 + (4 * 6), 7 + (4 * 8), "Clímax");
stages_.emplace_back(950, 7 + (4 * 7), 7 + (4 * 10), "Maestría");
}
auto StageManager::loadStagesFromFile(const std::string& filename) -> bool {
std::ifstream file(filename);
if (!file.is_open()) {
return false; // No se pudo abrir el archivo
}
std::string line;
while (std::getline(file, line)) {
// Ignorar líneas vacías y comentarios (líneas que empiezan con #)
if (line.empty() || line[0] == '#') {
continue;
}
// Parsear línea: power_to_complete,min_menace,max_menace,name
std::stringstream ss(line);
std::string token;
std::vector<std::string> tokens;
// Dividir por comas
while (std::getline(ss, token, ',')) {
// Eliminar espacios en blanco al inicio y final
token.erase(0, token.find_first_not_of(" \t"));
token.erase(token.find_last_not_of(" \t") + 1);
tokens.push_back(token);
}
// Verificar que tenemos exactamente 4 campos
if (tokens.size() != 4) {
// Error de formato, continuar con la siguiente línea
continue;
}
try {
// Convertir a enteros los primeros tres campos
int power_to_complete = std::stoi(tokens[0]);
int min_menace = std::stoi(tokens[1]);
int max_menace = std::stoi(tokens[2]);
std::string name = tokens[3];
// Validar valores
if (power_to_complete <= 0 || min_menace < 0 || max_menace < min_menace) {
continue; // Valores inválidos, saltar línea
}
// Crear y añadir la fase
stages_.emplace_back(power_to_complete, min_menace, max_menace, name);
} catch (const std::exception&) {
// Error de conversión, continuar con la siguiente línea
continue;
}
}
file.close();
// Verificar que se cargó al menos una fase
return !stages_.empty();
}
auto StageManager::advanceToNextStage() -> bool {
if (!isCurrentStageCompleted() || current_stage_index_ >= stages_.size() - 1) {
return false;
}
current_stage_index_++;
current_power_ = 0; // Reiniciar poder para la nueva fase
updateStageStatuses();
return true;
}
auto StageManager::jumpToStage(size_t target_stage_index) -> bool {
if (!validateStageIndex(target_stage_index)) {
return false;
}
// Calcular el poder acumulado hasta la fase objetivo
int accumulated_power = 0;
for (size_t i = 0; i < target_stage_index; ++i) {
accumulated_power += stages_[i].getPowerToComplete();
}
// Actualizar estado
current_stage_index_ = target_stage_index;
current_power_ = 0; // Comenzar la fase objetivo sin poder
total_power_ = accumulated_power; // Poder total como si se hubieran completado las anteriores
updateStageStatuses();
return true;
}
auto StageManager::setTotalPower(int target_total_power) -> bool {
if (target_total_power < 0) {
return false;
}
int total_power_needed = getTotalPowerNeededToCompleteGame();
if (target_total_power > total_power_needed) {
return false;
}
// Calcular en qué fase debería estar y cuánto poder de esa fase
int accumulated_power = 0;
size_t target_stage_index = 0;
int target_current_power = 0;
for (size_t i = 0; i < stages_.size(); ++i) {
int stage_power = stages_[i].getPowerToComplete();
if (accumulated_power + stage_power > target_total_power) {
// El objetivo está dentro de esta fase
target_stage_index = i;
target_current_power = target_total_power - accumulated_power;
break;
}
accumulated_power += stage_power;
if (accumulated_power == target_total_power) {
// El objetivo coincide exactamente con el final de esta fase
// Mover a la siguiente fase (si existe) con power 0
target_stage_index = (i + 1 < stages_.size()) ? i + 1 : i;
target_current_power = (i + 1 < stages_.size()) ? 0 : stage_power;
break;
}
}
// Actualizar estado
current_stage_index_ = target_stage_index;
current_power_ = target_current_power;
total_power_ = target_total_power;
updateStageStatuses();
return true;
}
auto StageManager::subtractPower(int amount) -> bool {
if (amount <= 0 || current_power_ < amount) {
return false;
}
current_power_ -= amount;
updateStageStatuses();
return true;
}
void StageManager::enablePowerCollection() {
power_collection_state_ = PowerCollectionState::ENABLED;
}
void StageManager::disablePowerCollection() {
power_collection_state_ = PowerCollectionState::DISABLED;
}
auto StageManager::getCurrentStage() const -> std::optional<StageData> {
return getStage(current_stage_index_);
}
auto StageManager::getStage(size_t index) const -> std::optional<StageData> {
if (!validateStageIndex(index)) {
return std::nullopt;
}
return stages_[index];
}
auto StageManager::isCurrentStageCompleted() const -> bool {
auto current_stage = getCurrentStage();
if (!current_stage.has_value()) {
return false;
}
return current_power_ >= current_stage->getPowerToComplete();
}
auto StageManager::isGameCompleted() const -> bool {
return current_stage_index_ >= stages_.size() - 1 && isCurrentStageCompleted();
}
auto StageManager::getProgressPercentage() const -> double {
if (stages_.empty()) {
return 0.0;
}
int total_power_needed = getTotalPowerNeededToCompleteGame();
if (total_power_needed == 0) {
return 100.0;
}
return (static_cast<double>(total_power_) / total_power_needed) * 100.0;
}
auto StageManager::getCurrentStageProgressPercentage() const -> double {
return getCurrentStageProgressFraction() * 100.0;
}
auto StageManager::getCurrentStageProgressFraction() const -> double {
auto current_stage = getCurrentStage();
if (!current_stage.has_value()) {
return 0.0;
}
int power_needed = current_stage->getPowerToComplete();
if (power_needed == 0) {
return 1.0;
}
// Devuelve una fracción entre 0.0 y 1.0
double fraction = static_cast<double>(current_power_) / power_needed;
return std::min(fraction, 1.0);
}
auto StageManager::getPowerNeededForCurrentStage() const -> int {
auto current_stage = getCurrentStage();
if (!current_stage.has_value()) {
return 0;
}
return std::max(0, current_stage->getPowerToComplete() - current_power_);
}
auto StageManager::getTotalPowerNeededToCompleteGame() const -> int {
int total_power_needed = 0;
for (const auto& stage : stages_) {
total_power_needed += stage.getPowerToComplete();
}
return total_power_needed;
}
auto StageManager::getPowerNeededToReachStage(size_t target_stage_index) const -> int {
if (!validateStageIndex(target_stage_index)) {
return 0;
}
int power_needed = 0;
for (size_t i = 0; i < target_stage_index; ++i) {
power_needed += stages_[i].getPowerToComplete();
}
return power_needed;
}
// Implementación de la interfaz IStageInfo
auto StageManager::canCollectPower() const -> bool {
return power_collection_state_ == PowerCollectionState::ENABLED;
}
void StageManager::addPower(int amount) {
if (amount <= 0 || !canCollectPower()) {
return;
}
current_power_ += amount;
total_power_ += amount;
// Ejecutar callback si está registrado
if (power_change_callback_) {
power_change_callback_(amount);
}
// Verificar si se completó la fase actual
if (isCurrentStageCompleted()) {
auto current_stage = getCurrentStage();
if (current_stage.has_value()) {
stages_[current_stage_index_].setStatus(StageStatus::COMPLETED);
}
}
updateStageStatuses();
}
auto StageManager::getCurrentMenaceLevel() const -> int {
auto current_stage = getCurrentStage();
if (!current_stage.has_value()) {
return 0;
}
return current_stage->getMinMenace();
}
// Gestión de callbacks
void StageManager::setPowerChangeCallback(PowerChangeCallback callback) {
power_change_callback_ = std::move(callback);
}
void StageManager::removePowerChangeCallback() {
power_change_callback_ = nullptr;
}
// Métodos privados
auto StageManager::validateStageIndex(size_t index) const -> bool {
return index < stages_.size();
}
void StageManager::updateStageStatuses() {
// Actualizar el estado de cada fase según su posición relativa a la actual
for (size_t i = 0; i < stages_.size(); ++i) {
if (i < current_stage_index_) {
stages_[i].setStatus(StageStatus::COMPLETED);
} else if (i == current_stage_index_) {
stages_[i].setStatus(StageStatus::IN_PROGRESS);
} else {
stages_[i].setStatus(StageStatus::LOCKED);
}
}
}

View File

@@ -0,0 +1,114 @@
#pragma once
#include <cstddef> // Para size_t
#include <functional> // Para function
#include <optional> // Para optional
#include <string> // Para basic_string, string
#include <vector> // Para vector
#include "stage_interface.hpp" // for IStageInfo
// --- Enums ---
enum class PowerCollectionState {
ENABLED, // Recolección habilitada
DISABLED // Recolección deshabilitada
};
enum class StageStatus {
LOCKED, // Fase bloqueada
IN_PROGRESS, // Fase en progreso
COMPLETED // Fase completada
};
// --- Clase StageData: representa los datos de una fase del juego ---
class StageData {
public:
// --- Constructor ---
StageData(int power_to_complete, int min_menace, int max_menace, std::string name = ""); // Constructor de una fase
// --- Getters ---
[[nodiscard]] auto getPowerToComplete() const -> int { return power_to_complete_; } // Obtiene el poder necesario para completar
[[nodiscard]] auto getMinMenace() const -> int { return min_menace_; } // Obtiene el nivel mínimo de amenaza
[[nodiscard]] auto getMaxMenace() const -> int { return max_menace_; } // Obtiene el nivel máximo de amenaza
[[nodiscard]] auto getName() const -> const std::string& { return name_; } // Obtiene el nombre de la fase
[[nodiscard]] auto getStatus() const -> StageStatus { return status_; } // Obtiene el estado actual
[[nodiscard]] auto isCompleted() const -> bool { return status_ == StageStatus::COMPLETED; } // Verifica si está completada
// --- Setters ---
void setStatus(StageStatus status) { status_ = status; } // Establece el estado de la fase
private:
// --- Variables de estado ---
StageStatus status_; // Estado actual de la fase
std::string name_; // Nombre de la fase
int power_to_complete_; // Poder necesario para completar la fase
int min_menace_; // Nivel mínimo de amenaza
int max_menace_; // Nivel máximo de amenaza
};
// --- Clase StageManager: gestor principal del sistema de fases del juego ---
class StageManager : public IStageInfo {
public:
// --- Tipos ---
using PowerChangeCallback = std::function<void(int)>; // Callback para cambios de poder
// --- Constructor ---
StageManager(); // Constructor principal
// --- Métodos principales del juego ---
void initialize(); // Inicializa el gestor de fases
void initialize(const std::string& stages_file); // Inicializa con archivo personalizado
void reset(); // Reinicia el progreso del juego
auto advanceToNextStage() -> bool; // Avanza a la siguiente fase
// --- Gestión de poder ---
auto subtractPower(int amount) -> bool; // Resta poder de la fase actual
void enablePowerCollection() override; // Habilita la recolección de poder
void disablePowerCollection(); // Deshabilita la recolección de poder
// --- Navegación ---
auto jumpToStage(size_t target_stage_index) -> bool; // Salta a una fase específica
auto setTotalPower(int target_total_power) -> bool; // Establece el poder total y ajusta fase/progreso
// --- Consultas de estado ---
[[nodiscard]] auto getCurrentStage() const -> std::optional<StageData>; // Obtiene la fase actual
[[nodiscard]] auto getStage(size_t index) const -> std::optional<StageData>; // Obtiene una fase específica
[[nodiscard]] auto getCurrentStageIndex() const -> size_t { return current_stage_index_; } // Obtiene el índice de la fase actual
[[nodiscard]] auto getCurrentPower() const -> int { return current_power_; } // Obtiene el poder actual
[[nodiscard]] auto getTotalPower() const -> int { return total_power_; } // Obtiene el poder total acumulado
[[nodiscard]] auto getTotalPowerNeededToCompleteGame() const -> int; // Poder total necesario para completar el juego
[[nodiscard]] auto getPowerNeededToReachStage(size_t target_stage_index) const -> int; // Poder necesario para llegar a la fase X
[[nodiscard]] auto getTotalStages() const -> size_t { return stages_.size(); } // Obtiene el número total de fases
// --- Seguimiento de progreso ---
[[nodiscard]] auto isCurrentStageCompleted() const -> bool; // Verifica si la fase actual está completada
[[nodiscard]] auto isGameCompleted() const -> bool; // Verifica si el juego está completado
[[nodiscard]] auto getProgressPercentage() const -> double; // Progreso total del juego (0-100%)
[[nodiscard]] auto getCurrentStageProgressPercentage() const -> double; // Progreso de la fase actual (0-100%)
[[nodiscard]] auto getCurrentStageProgressFraction() const -> double; // Progreso de la fase actual (0.0-1.0)
[[nodiscard]] auto getPowerNeededForCurrentStage() const -> int; // Poder restante para completar la fase actual
// --- Gestión de callbacks ---
void setPowerChangeCallback(PowerChangeCallback callback); // Establece callback para cambios de poder
void removePowerChangeCallback(); // Elimina callback de cambios de poder
// --- Implementación de la interfaz IStageInfo ---
[[nodiscard]] auto canCollectPower() const -> bool override; // Verifica si se puede recolectar poder
void addPower(int amount) override; // Añade poder a la fase actual
[[nodiscard]] auto getCurrentMenaceLevel() const -> int override; // Obtiene el nivel de amenaza actual
private:
// --- Variables de estado ---
std::vector<StageData> stages_; // Lista de todas las fases
PowerChangeCallback power_change_callback_; // Callback para notificar cambios de poder
PowerCollectionState power_collection_state_; // Estado de recolección de poder
size_t current_stage_index_; // Índice de la fase actual
int current_power_; // Poder actual en la fase activa
int total_power_; // Poder total acumulado en todo el juego
// --- Métodos internos ---
void createDefaultStages(); // Crea las fases predeterminadas del juego
auto loadStagesFromFile(const std::string& filename) -> bool; // Carga fases desde archivo
[[nodiscard]] auto validateStageIndex(size_t index) const -> bool; // Valida que un índice de fase sea válido
void updateStageStatuses(); // Actualiza los estados de todas las fases
};

868
source/game/options.cpp Normal file
View File

@@ -0,0 +1,868 @@
#include "options.hpp"
#include <SDL3/SDL.h> // Para SDL_ScaleMode, SDL_LogCategory, SDL_LogError, SDL_LogInfo, SDL_LogWarn
#include <algorithm> // Para clamp
#include <cstddef> // Para size_t
#include <fstream> // Para ifstream, ofstream
#include <iostream> // Para std::cout
#include <string> // Para string
#include <vector> // Para vector
#include "difficulty.hpp" // Para Code, init
#include "external/fkyaml_node.hpp" // Para fkyaml::node
#include "input.hpp" // Para Input
#include "lang.hpp" // Para getText, Code
#include "utils.hpp" // Para boolToString, getFileName
namespace Options {
// --- Variables globales ---
Window window; // Opciones de la ventana
Settings settings; // Opciones del juego
Video video; // Opciones de vídeo
Audio audio; // Opciones de audio
GamepadManager gamepad_manager; // Opciones de mando para cada jugador
Keyboard keyboard; // Opciones para el teclado
PendingChanges pending_changes; // Opciones que se aplican al cerrar
std::vector<PostFXPreset> postfx_presets = {
{"CRT", 0.15F, 0.7F, 0.2F, 0.5F, 0.1F, 0.0F, 0.0F, 0.0F},
{"NTSC", 0.4F, 0.5F, 0.2F, 0.3F, 0.3F, 0.0F, 0.6F, 0.0F},
{"Curved", 0.5F, 0.6F, 0.1F, 0.4F, 0.4F, 0.8F, 0.0F, 0.0F},
{"Scanlines", 0.0F, 0.8F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F},
{"Subtle", 0.3F, 0.4F, 0.05F, 0.0F, 0.2F, 0.0F, 0.0F, 0.0F},
{"CRT Live", 0.15F, 0.6F, 0.3F, 0.3F, 0.1F, 0.0F, 0.4F, 0.8F},
};
std::string postfx_file_path;
std::vector<CrtPiPreset> crtpi_presets;
std::string crtpi_file_path;
// Establece el fichero de configuración
void setConfigFile(const std::string& file_path) { settings.config_file = file_path; }
// Establece el fichero de configuración de mandos
void setControllersFile(const std::string& file_path) { settings.controllers_file = file_path; }
// Establece la ruta del fichero de PostFX
void setPostFXFile(const std::string& path) { postfx_file_path = path; }
// Establece la ruta del fichero de CrtPi
void setCrtPiFile(const std::string& path) { crtpi_file_path = path; }
// Helper: extrae un campo float de un nodo YAML si existe, ignorando errores de conversión
static void parseFloatField(const fkyaml::node& node, const std::string& key, float& target) {
if (node.contains(key)) {
try {
target = node[key].get_value<float>();
} catch (...) {}
}
}
// Carga los presets de PostFX desde el fichero
auto loadPostFXFromFile() -> bool {
postfx_presets.clear();
std::ifstream file(postfx_file_path);
if (!file.good()) {
return savePostFXToFile();
}
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
file.close();
try {
auto yaml = fkyaml::node::deserialize(content);
if (yaml.contains("presets")) {
const auto& presets = yaml["presets"];
for (const auto& p : presets) {
PostFXPreset preset;
if (p.contains("name")) {
preset.name = p["name"].get_value<std::string>();
}
parseFloatField(p, "vignette", preset.vignette);
parseFloatField(p, "scanlines", preset.scanlines);
parseFloatField(p, "chroma", preset.chroma);
parseFloatField(p, "mask", preset.mask);
parseFloatField(p, "gamma", preset.gamma);
parseFloatField(p, "curvature", preset.curvature);
parseFloatField(p, "bleeding", preset.bleeding);
parseFloatField(p, "flicker", preset.flicker);
postfx_presets.push_back(preset);
}
}
if (!postfx_presets.empty()) {
// Resolver nombre → índice
if (!video.shader.current_postfx_preset_name.empty()) {
for (int i = 0; i < static_cast<int>(postfx_presets.size()); ++i) {
if (postfx_presets[static_cast<size_t>(i)].name == video.shader.current_postfx_preset_name) {
video.shader.current_postfx_preset = i;
break;
}
}
}
video.shader.current_postfx_preset = std::clamp(
video.shader.current_postfx_preset,
0,
static_cast<int>(postfx_presets.size()) - 1);
} else {
video.shader.current_postfx_preset = 0;
}
return true;
} catch (const fkyaml::exception& e) {
std::cout << "Error parsing PostFX YAML: " << e.what() << ". Recreating defaults." << '\n';
return savePostFXToFile();
}
}
// Guarda los presets de PostFX por defecto al fichero
auto savePostFXToFile() -> bool {
if (postfx_file_path.empty()) {
return false;
}
std::ofstream file(postfx_file_path);
if (!file.is_open()) {
std::cout << "Error: " << postfx_file_path << " can't be opened for writing" << '\n';
return false;
}
file << "# Coffee Crisis Arcade Edition - PostFX Presets\n";
file << "# Each preset defines the intensity of post-processing effects (0.0 to 1.0).\n";
file << "# vignette: screen darkening at the edges\n";
file << "# scanlines: horizontal scanline effect\n";
file << "# chroma: chromatic aberration (RGB color fringing)\n";
file << "# mask: phosphor dot mask (RGB subpixel pattern)\n";
file << "# gamma: gamma correction input 2.4 / output 2.2\n";
file << "# curvature: CRT barrel distortion\n";
file << "# bleeding: NTSC horizontal colour bleeding\n";
file << "# flicker: phosphor CRT flicker ~50 Hz (0.0 = off, 1.0 = max)\n";
file << "\n";
file << "presets:\n";
file << " - name: \"CRT\"\n";
file << " vignette: 0.15\n";
file << " scanlines: 0.7\n";
file << " chroma: 0.2\n";
file << " mask: 0.5\n";
file << " gamma: 0.1\n";
file << " curvature: 0.0\n";
file << " bleeding: 0.0\n";
file << " flicker: 0.0\n";
file << " - name: \"NTSC\"\n";
file << " vignette: 0.4\n";
file << " scanlines: 0.5\n";
file << " chroma: 0.2\n";
file << " mask: 0.3\n";
file << " gamma: 0.3\n";
file << " curvature: 0.0\n";
file << " bleeding: 0.6\n";
file << " flicker: 0.0\n";
file << " - name: \"Curved\"\n";
file << " vignette: 0.5\n";
file << " scanlines: 0.6\n";
file << " chroma: 0.1\n";
file << " mask: 0.4\n";
file << " gamma: 0.4\n";
file << " curvature: 0.8\n";
file << " bleeding: 0.0\n";
file << " flicker: 0.0\n";
file << " - name: \"Scanlines\"\n";
file << " vignette: 0.0\n";
file << " scanlines: 0.8\n";
file << " chroma: 0.0\n";
file << " mask: 0.0\n";
file << " gamma: 0.0\n";
file << " curvature: 0.0\n";
file << " bleeding: 0.0\n";
file << " flicker: 0.0\n";
file << " - name: \"Subtle\"\n";
file << " vignette: 0.3\n";
file << " scanlines: 0.4\n";
file << " chroma: 0.05\n";
file << " mask: 0.0\n";
file << " gamma: 0.2\n";
file << " curvature: 0.0\n";
file << " bleeding: 0.0\n";
file << " flicker: 0.0\n";
file << " - name: \"CRT Live\"\n";
file << " vignette: 0.15\n";
file << " scanlines: 0.6\n";
file << " chroma: 0.3\n";
file << " mask: 0.3\n";
file << " gamma: 0.1\n";
file << " curvature: 0.0\n";
file << " bleeding: 0.4\n";
file << " flicker: 0.8\n";
file.close();
// Cargar los presets recién escritos
postfx_presets.clear();
postfx_presets.push_back({"CRT", 0.15F, 0.7F, 0.2F, 0.5F, 0.1F, 0.0F, 0.0F, 0.0F});
postfx_presets.push_back({"NTSC", 0.4F, 0.5F, 0.2F, 0.3F, 0.3F, 0.0F, 0.6F, 0.0F});
postfx_presets.push_back({"Curved", 0.5F, 0.6F, 0.1F, 0.4F, 0.4F, 0.8F, 0.0F, 0.0F});
postfx_presets.push_back({"Scanlines", 0.0F, 0.8F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F});
postfx_presets.push_back({"Subtle", 0.3F, 0.4F, 0.05F, 0.0F, 0.2F, 0.0F, 0.0F, 0.0F});
postfx_presets.push_back({"CRT Live", 0.15F, 0.6F, 0.3F, 0.3F, 0.1F, 0.0F, 0.4F, 0.8F});
video.shader.current_postfx_preset = 0;
return true;
}
// Helper: extrae un campo bool de un nodo YAML si existe, ignorando errores
static void parseBoolField(const fkyaml::node& node, const std::string& key, bool& target) {
if (node.contains(key)) {
try {
target = node[key].get_value<bool>();
} catch (...) {}
}
}
// Helper: extrae un campo int de un nodo YAML si existe, ignorando errores
static void parseIntField(const fkyaml::node& node, const std::string& key, int& target) {
if (node.contains(key)) {
try {
target = node[key].get_value<int>();
} catch (...) {}
}
}
// Rellena los presets CrtPi por defecto
static void populateDefaultCrtPiPresets() {
crtpi_presets.clear();
crtpi_presets.push_back({"Default", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, false, false});
crtpi_presets.push_back({"Curved", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, true, false});
crtpi_presets.push_back({"Sharp", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, false, true, false, true});
crtpi_presets.push_back({"Minimal", 8.0F, 0.05F, 2.0F, 2.4F, 2.2F, 1.00F, 0.0F, 0.0F, 0, true, false, false, false, false});
}
// Escribe los presets CrtPi por defecto al fichero
static auto saveCrtPiDefaults() -> bool {
if (crtpi_file_path.empty()) { return false; }
std::ofstream file(crtpi_file_path);
if (!file.is_open()) {
std::cout << "Error: " << crtpi_file_path << " can't be opened for writing" << '\n';
return false;
}
file << "# Coffee Crisis Arcade Edition - CrtPi Shader Presets\n";
file << "# scanline_weight: gaussian adjustment (higher = narrower scanlines, default 6.0)\n";
file << "# scanline_gap_brightness: min brightness between scanlines (0.0-1.0, default 0.12)\n";
file << "# bloom_factor: brightness for bright areas (default 3.5)\n";
file << "# input_gamma: input gamma - linearization (default 2.4)\n";
file << "# output_gamma: output gamma - encoding (default 2.2)\n";
file << "# mask_brightness: sub-pixel brightness (default 0.80)\n";
file << "# curvature_x/y: barrel CRT distortion (0.0 = flat)\n";
file << "# mask_type: 0=none, 1=green/magenta, 2=RGB phosphor\n";
file << "# enable_scanlines/multisample/gamma/curvature/sharper: true/false\n";
file << "\npresets:\n";
file << " - name: \"Default\"\n scanline_weight: 6.0\n scanline_gap_brightness: 0.12\n bloom_factor: 3.5\n input_gamma: 2.4\n output_gamma: 2.2\n mask_brightness: 0.80\n curvature_x: 0.05\n curvature_y: 0.10\n mask_type: 2\n enable_scanlines: true\n enable_multisample: true\n enable_gamma: true\n enable_curvature: false\n enable_sharper: false\n";
file << " - name: \"Curved\"\n scanline_weight: 6.0\n scanline_gap_brightness: 0.12\n bloom_factor: 3.5\n input_gamma: 2.4\n output_gamma: 2.2\n mask_brightness: 0.80\n curvature_x: 0.05\n curvature_y: 0.10\n mask_type: 2\n enable_scanlines: true\n enable_multisample: true\n enable_gamma: true\n enable_curvature: true\n enable_sharper: false\n";
file << " - name: \"Sharp\"\n scanline_weight: 6.0\n scanline_gap_brightness: 0.12\n bloom_factor: 3.5\n input_gamma: 2.4\n output_gamma: 2.2\n mask_brightness: 0.80\n curvature_x: 0.05\n curvature_y: 0.10\n mask_type: 2\n enable_scanlines: true\n enable_multisample: false\n enable_gamma: true\n enable_curvature: false\n enable_sharper: true\n";
file << " - name: \"Minimal\"\n scanline_weight: 8.0\n scanline_gap_brightness: 0.05\n bloom_factor: 2.0\n input_gamma: 2.4\n output_gamma: 2.2\n mask_brightness: 1.00\n curvature_x: 0.0\n curvature_y: 0.0\n mask_type: 0\n enable_scanlines: true\n enable_multisample: false\n enable_gamma: false\n enable_curvature: false\n enable_sharper: false\n";
file.close();
populateDefaultCrtPiPresets();
return true;
}
// Carga los presets de CrtPi desde el fichero
auto loadCrtPiFromFile() -> bool {
crtpi_presets.clear();
std::ifstream file(crtpi_file_path);
if (!file.good()) {
return saveCrtPiDefaults();
}
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
file.close();
try {
auto yaml = fkyaml::node::deserialize(content);
if (yaml.contains("presets")) {
const auto& presets = yaml["presets"];
for (const auto& p : presets) {
CrtPiPreset preset;
if (p.contains("name")) {
preset.name = p["name"].get_value<std::string>();
}
parseFloatField(p, "scanline_weight", preset.scanline_weight);
parseFloatField(p, "scanline_gap_brightness", preset.scanline_gap_brightness);
parseFloatField(p, "bloom_factor", preset.bloom_factor);
parseFloatField(p, "input_gamma", preset.input_gamma);
parseFloatField(p, "output_gamma", preset.output_gamma);
parseFloatField(p, "mask_brightness", preset.mask_brightness);
parseFloatField(p, "curvature_x", preset.curvature_x);
parseFloatField(p, "curvature_y", preset.curvature_y);
parseIntField(p, "mask_type", preset.mask_type);
parseBoolField(p, "enable_scanlines", preset.enable_scanlines);
parseBoolField(p, "enable_multisample", preset.enable_multisample);
parseBoolField(p, "enable_gamma", preset.enable_gamma);
parseBoolField(p, "enable_curvature", preset.enable_curvature);
parseBoolField(p, "enable_sharper", preset.enable_sharper);
crtpi_presets.push_back(preset);
}
}
if (!crtpi_presets.empty()) {
// Resolver nombre → índice
if (!video.shader.current_crtpi_preset_name.empty()) {
for (int i = 0; i < static_cast<int>(crtpi_presets.size()); ++i) {
if (crtpi_presets[static_cast<size_t>(i)].name == video.shader.current_crtpi_preset_name) {
video.shader.current_crtpi_preset = i;
break;
}
}
}
video.shader.current_crtpi_preset = std::clamp(
video.shader.current_crtpi_preset,
0,
static_cast<int>(crtpi_presets.size()) - 1);
} else {
video.shader.current_crtpi_preset = 0;
}
return true;
} catch (const fkyaml::exception& e) {
std::cout << "Error parsing CrtPi YAML: " << e.what() << ". Recreating defaults." << '\n';
return saveCrtPiDefaults();
}
}
// Inicializa las opciones del programa
void init() {
// Dificultades
Difficulty::init();
// Opciones de control
gamepad_manager.init();
setKeyboardToPlayer(Player::Id::PLAYER1);
// Opciones de cambios pendientes
pending_changes.new_language = settings.language;
pending_changes.new_difficulty = settings.difficulty;
pending_changes.has_pending_changes = false;
}
// --- Funciones helper de carga desde YAML ---
void loadWindowFromYaml(const fkyaml::node& yaml) {
if (!yaml.contains("window")) { return; }
const auto& win = yaml["window"];
if (win.contains("zoom")) {
try {
int val = win["zoom"].get_value<int>();
window.zoom = (val > 0) ? val : window.zoom;
} catch (...) {}
}
}
void loadVideoFromYaml(const fkyaml::node& yaml) {
if (!yaml.contains("video")) { return; }
const auto& vid = yaml["video"];
if (vid.contains("fullscreen")) {
try {
video.fullscreen = vid["fullscreen"].get_value<bool>();
} catch (...) {}
}
if (vid.contains("scale_mode")) {
try {
video.scale_mode = static_cast<SDL_ScaleMode>(vid["scale_mode"].get_value<int>());
} catch (...) {}
}
if (vid.contains("vsync")) {
try {
video.vsync = vid["vsync"].get_value<bool>();
} catch (...) {}
}
if (vid.contains("integer_scale")) {
try {
video.integer_scale = vid["integer_scale"].get_value<bool>();
} catch (...) {}
}
// --- GPU ---
if (vid.contains("gpu")) {
const auto& gpu_node = vid["gpu"];
if (gpu_node.contains("acceleration")) {
try {
video.gpu.acceleration = gpu_node["acceleration"].get_value<bool>();
} catch (...) {}
}
if (gpu_node.contains("preferred_driver")) {
try {
video.gpu.preferred_driver = gpu_node["preferred_driver"].get_value<std::string>();
} catch (...) {}
}
}
// --- Shader config ---
if (vid.contains("shader")) {
const auto& sh = vid["shader"];
if (sh.contains("enabled")) {
try {
video.shader.enabled = sh["enabled"].get_value<bool>();
} catch (...) {}
}
if (sh.contains("current_shader")) {
try {
auto s = sh["current_shader"].get_value<std::string>();
video.shader.current_shader = (s == "crtpi") ? Rendering::ShaderType::CRTPI : Rendering::ShaderType::POSTFX;
} catch (...) {}
}
if (sh.contains("postfx_preset")) {
try {
video.shader.current_postfx_preset_name = sh["postfx_preset"].get_value<std::string>();
} catch (...) {}
}
if (sh.contains("crtpi_preset")) {
try {
video.shader.current_crtpi_preset_name = sh["crtpi_preset"].get_value<std::string>();
} catch (...) {}
}
}
// --- Supersampling ---
if (vid.contains("supersampling")) {
const auto& ss_node = vid["supersampling"];
if (ss_node.contains("enabled")) {
try {
video.supersampling.enabled = ss_node["enabled"].get_value<bool>();
} catch (...) {}
}
if (ss_node.contains("linear_upscale")) {
try {
video.supersampling.linear_upscale = ss_node["linear_upscale"].get_value<bool>();
} catch (...) {}
}
if (ss_node.contains("downscale_algo")) {
try {
video.supersampling.downscale_algo = ss_node["downscale_algo"].get_value<int>();
} catch (...) {}
}
}
}
void loadAudioFromYaml(const fkyaml::node& yaml) {
if (!yaml.contains("audio")) { return; }
const auto& aud = yaml["audio"];
if (aud.contains("enabled")) {
try {
audio.enabled = aud["enabled"].get_value<bool>();
} catch (...) {}
}
if (aud.contains("volume")) {
try {
audio.volume = std::clamp(aud["volume"].get_value<int>(), 0, 100);
} catch (...) {}
}
if (aud.contains("music")) {
const auto& mus = aud["music"];
if (mus.contains("enabled")) {
try {
audio.music.enabled = mus["enabled"].get_value<bool>();
} catch (...) {}
}
if (mus.contains("volume")) {
try {
audio.music.volume = std::clamp(mus["volume"].get_value<int>(), 0, 100);
} catch (...) {}
}
}
if (aud.contains("sound")) {
const auto& snd = aud["sound"];
if (snd.contains("enabled")) {
try {
audio.sound.enabled = snd["enabled"].get_value<bool>();
} catch (...) {}
}
if (snd.contains("volume")) {
try {
audio.sound.volume = std::clamp(snd["volume"].get_value<int>(), 0, 100);
} catch (...) {}
}
}
}
void loadGameFromYaml(const fkyaml::node& yaml) {
if (!yaml.contains("game")) { return; }
const auto& game = yaml["game"];
if (game.contains("language")) {
try {
auto lang = static_cast<Lang::Code>(game["language"].get_value<int>());
if (lang == Lang::Code::ENGLISH || lang == Lang::Code::VALENCIAN || lang == Lang::Code::SPANISH) {
settings.language = lang;
} else {
settings.language = Lang::Code::ENGLISH;
}
pending_changes.new_language = settings.language;
} catch (...) {}
}
if (game.contains("difficulty")) {
try {
settings.difficulty = static_cast<Difficulty::Code>(game["difficulty"].get_value<int>());
pending_changes.new_difficulty = settings.difficulty;
} catch (...) {}
}
if (game.contains("autofire")) {
try {
settings.autofire = game["autofire"].get_value<bool>();
} catch (...) {}
}
if (game.contains("shutdown_enabled")) {
try {
settings.shutdown_enabled = game["shutdown_enabled"].get_value<bool>();
} catch (...) {}
}
if (game.contains("params_file")) {
try {
settings.params_file = game["params_file"].get_value<std::string>();
} catch (...) {}
}
}
void loadControllersFromYaml(const fkyaml::node& yaml) {
if (!yaml.contains("controllers")) { return; }
const auto& controllers = yaml["controllers"];
size_t i = 0;
for (const auto& ctrl : controllers) {
if (i >= GamepadManager::size()) { break; }
if (ctrl.contains("name")) {
try {
gamepad_manager[i].name = ctrl["name"].get_value<std::string>();
} catch (...) {}
}
if (ctrl.contains("path")) {
try {
gamepad_manager[i].path = ctrl["path"].get_value<std::string>();
} catch (...) {}
}
if (ctrl.contains("player")) {
try {
int player_int = ctrl["player"].get_value<int>();
if (player_int == 1) {
gamepad_manager[i].player_id = Player::Id::PLAYER1;
} else if (player_int == 2) {
gamepad_manager[i].player_id = Player::Id::PLAYER2;
}
} catch (...) {}
}
++i;
}
}
void loadKeyboardFromYaml(const fkyaml::node& yaml) {
if (!yaml.contains("keyboard")) { return; }
const auto& kb = yaml["keyboard"];
if (kb.contains("player")) {
try {
keyboard.player_id = static_cast<Player::Id>(kb["player"].get_value<int>());
} catch (...) {}
}
}
// Carga el fichero de configuración
auto loadFromFile() -> bool {
init();
std::ifstream file(settings.config_file);
if (!file.is_open()) {
saveToFile();
return true;
}
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
file.close();
try {
auto yaml = fkyaml::node::deserialize(content);
// Comprobar versión: si no coincide, regenerar config por defecto
int file_version = 0;
if (yaml.contains("version")) {
try {
file_version = yaml["version"].get_value<int>();
} catch (...) {}
}
if (file_version != Settings::CURRENT_CONFIG_VERSION) {
std::cout << "Config version " << file_version << " != expected " << Settings::CURRENT_CONFIG_VERSION << ". Recreating defaults." << '\n';
init();
saveToFile();
return true;
}
loadWindowFromYaml(yaml);
loadVideoFromYaml(yaml);
loadAudioFromYaml(yaml);
loadGameFromYaml(yaml);
loadControllersFromYaml(yaml);
loadKeyboardFromYaml(yaml);
} catch (const fkyaml::exception& e) {
std::cout << "Error parsing YAML config: " << e.what() << ". Using defaults." << '\n';
init();
saveToFile();
return true;
}
gamepad_manager.assignAndLinkGamepads();
return true;
}
// Guarda el fichero de configuración
auto saveToFile() -> bool {
std::ofstream file(settings.config_file);
if (!file.good()) {
std::cout << "Error: " << getFileName(settings.config_file) << " can't be opened" << '\n';
return false;
}
applyPendingChanges();
file << "# Coffee Crisis Arcade Edition - Configuration File\n";
file << "# This file is automatically generated and managed by the game.\n";
file << "\n";
file << "version: " << settings.config_version << "\n";
file << "\n";
// WINDOW
file << "# WINDOW\n";
file << "window:\n";
file << " zoom: " << window.zoom << "\n";
file << "\n";
// VIDEO
file << "# VIDEO\n";
file << "video:\n";
file << " fullscreen: " << boolToString(video.fullscreen) << "\n";
file << " scale_mode: " << static_cast<int>(video.scale_mode) << " # " << static_cast<int>(SDL_ScaleMode::SDL_SCALEMODE_NEAREST) << ": nearest, " << static_cast<int>(SDL_ScaleMode::SDL_SCALEMODE_LINEAR) << ": linear\n";
file << " vsync: " << boolToString(video.vsync) << "\n";
file << " integer_scale: " << boolToString(video.integer_scale) << "\n";
file << " gpu:\n";
file << " acceleration: " << boolToString(video.gpu.acceleration) << "\n";
file << " preferred_driver: \"" << video.gpu.preferred_driver << "\"\n";
file << " shader:\n";
file << " enabled: " << boolToString(video.shader.enabled) << "\n";
file << " current_shader: " << (video.shader.current_shader == Rendering::ShaderType::CRTPI ? "crtpi" : "postfx") << "\n";
{
std::string postfx_name = (!postfx_presets.empty() && video.shader.current_postfx_preset < static_cast<int>(postfx_presets.size()))
? postfx_presets[static_cast<size_t>(video.shader.current_postfx_preset)].name
: "";
std::string crtpi_name = (!crtpi_presets.empty() && video.shader.current_crtpi_preset < static_cast<int>(crtpi_presets.size()))
? crtpi_presets[static_cast<size_t>(video.shader.current_crtpi_preset)].name
: "";
file << " postfx_preset: \"" << postfx_name << "\"\n";
file << " crtpi_preset: \"" << crtpi_name << "\"\n";
}
file << " supersampling:\n";
file << " enabled: " << boolToString(video.supersampling.enabled) << "\n";
file << " linear_upscale: " << boolToString(video.supersampling.linear_upscale) << "\n";
file << " downscale_algo: " << video.supersampling.downscale_algo << "\n";
file << "\n";
// AUDIO
file << "# AUDIO (volume range: 0..100)\n";
file << "audio:\n";
file << " enabled: " << boolToString(audio.enabled) << "\n";
file << " volume: " << audio.volume << "\n";
file << " music:\n";
file << " enabled: " << boolToString(audio.music.enabled) << "\n";
file << " volume: " << audio.music.volume << "\n";
file << " sound:\n";
file << " enabled: " << boolToString(audio.sound.enabled) << "\n";
file << " volume: " << audio.sound.volume << "\n";
file << "\n";
// GAME
file << "# GAME\n";
file << "game:\n";
file << " language: " << static_cast<int>(settings.language) << " # 0: spanish, 1: valencian, 2: english\n";
file << " difficulty: " << static_cast<int>(settings.difficulty) << " # " << static_cast<int>(Difficulty::Code::EASY) << ": easy, " << static_cast<int>(Difficulty::Code::NORMAL) << ": normal, " << static_cast<int>(Difficulty::Code::HARD) << ": hard\n";
file << " autofire: " << boolToString(settings.autofire) << "\n";
file << " shutdown_enabled: " << boolToString(settings.shutdown_enabled) << "\n";
file << " params_file: " << settings.params_file << "\n";
file << "\n";
// CONTROLLERS
file << "# CONTROLLERS\n";
file << "controllers:\n";
gamepad_manager.saveToFile(file);
file << "\n";
// KEYBOARD
file << "# KEYBOARD\n";
file << "keyboard:\n";
file << " player: " << static_cast<int>(keyboard.player_id) << "\n";
file.close();
return true;
}
// Asigna el teclado al jugador
void setKeyboardToPlayer(Player::Id player_id) {
keyboard.player_id = player_id;
}
// Intercambia el teclado de jugador
void swapKeyboard() {
keyboard.player_id = keyboard.player_id == Player::Id::PLAYER1 ? Player::Id::PLAYER2 : Player::Id::PLAYER1;
}
// Intercambia los jugadores asignados a los dos primeros mandos
void swapControllers() {
gamepad_manager.swapPlayers();
}
// Averigua quien está usando el teclado
auto getPlayerWhoUsesKeyboard() -> Player::Id {
return keyboard.player_id;
}
// Aplica los cambios pendientes copiando los valores a sus variables
void applyPendingChanges() {
if (pending_changes.has_pending_changes) {
settings.language = pending_changes.new_language;
settings.difficulty = pending_changes.new_difficulty;
pending_changes.has_pending_changes = false;
}
}
void checkPendingChanges() {
pending_changes.has_pending_changes = settings.language != pending_changes.new_language ||
settings.difficulty != pending_changes.new_difficulty;
}
// Buscar y asignar un mando disponible por nombre
auto assignGamepadByName(const std::string& gamepad_name_to_find, Player::Id player_id) -> bool {
auto found_gamepad = Input::get()->findAvailableGamepadByName(gamepad_name_to_find);
if (found_gamepad) {
return gamepad_manager.assignGamepadToPlayer(player_id, found_gamepad, found_gamepad->name);
}
return false;
}
// Obtener información de un gamepad específico
auto getGamepadInfo(Player::Id player_id) -> std::string {
try {
const auto& gamepad = gamepad_manager.getGamepad(player_id);
return "Player " + std::to_string(static_cast<int>(player_id)) +
": " + (gamepad.name.empty() ? "No gamepad" : gamepad.name);
} catch (const std::exception&) {
return "Invalid player";
}
}
// Asigna los mandos físicos basándose en la configuración actual.
void GamepadManager::assignAndLinkGamepads() {
auto physical_gamepads = Input::get()->getGamepads();
std::array<std::string, MAX_PLAYERS> desired_paths;
for (size_t i = 0; i < MAX_PLAYERS; ++i) {
desired_paths[i] = gamepads_[i].path;
gamepads_[i].instance = nullptr;
}
std::vector<std::shared_ptr<Input::Gamepad>> assigned_instances;
assignGamepadsByPath(desired_paths, physical_gamepads, assigned_instances);
assignRemainingGamepads(physical_gamepads, assigned_instances);
clearUnassignedGamepadSlots();
}
// --- PRIMERA PASADA: Intenta asignar mandos basándose en la ruta guardada ---
void GamepadManager::assignGamepadsByPath(
const std::array<std::string, MAX_PLAYERS>& desired_paths,
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-named-parameter)
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) {
for (size_t i = 0; i < MAX_PLAYERS; ++i) {
const std::string& desired_path = desired_paths[i];
if (desired_path.empty()) {
continue;
}
for (const auto& physical_gamepad : physical_gamepads) {
if (physical_gamepad->path == desired_path && !isGamepadAssigned(physical_gamepad, assigned_instances)) {
gamepads_[i].instance = physical_gamepad;
gamepads_[i].name = physical_gamepad->name;
assigned_instances.push_back(physical_gamepad);
break;
}
}
}
}
// --- SEGUNDA PASADA: Asigna los mandos físicos restantes a los jugadores libres ---
void GamepadManager::assignRemainingGamepads(
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-named-parameter)
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) {
for (size_t i = 0; i < MAX_PLAYERS; ++i) {
if (gamepads_[i].instance != nullptr) {
continue;
}
for (const auto& physical_gamepad : physical_gamepads) {
if (!isGamepadAssigned(physical_gamepad, assigned_instances)) {
gamepads_[i].instance = physical_gamepad;
gamepads_[i].name = physical_gamepad->name;
gamepads_[i].path = physical_gamepad->path;
assigned_instances.push_back(physical_gamepad);
break;
}
}
}
}
// --- TERCERA PASADA: Limpia la información "fantasma" de los slots no asignados ---
void GamepadManager::clearUnassignedGamepadSlots() {
for (auto& gamepad_config : gamepads_) {
if (gamepad_config.instance == nullptr) {
gamepad_config.name = Lang::getText("[SERVICE_MENU] NO_CONTROLLER");
gamepad_config.path = "";
}
}
}
auto GamepadManager::isGamepadAssigned(
const std::shared_ptr<Input::Gamepad>& physical_gamepad,
const std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) -> bool { // NOLINT(readability-named-parameter)
return std::ranges::any_of(assigned_instances,
[&physical_gamepad](const auto& assigned) -> auto {
return assigned == physical_gamepad;
});
}
// Convierte un player id a texto segun Lang
auto playerIdToString(Player::Id player_id) -> std::string {
switch (player_id) {
case Player::Id::PLAYER1:
return Lang::getText("[SERVICE_MENU] PLAYER1");
case Player::Id::PLAYER2:
return Lang::getText("[SERVICE_MENU] PLAYER2");
default:
return "";
}
}
// Convierte un texto a player id segun Lang
auto stringToPlayerId(const std::string& name) -> Player::Id {
if (name == Lang::getText("[SERVICE_MENU] PLAYER1")) {
return Player::Id::PLAYER1;
}
if (name == Lang::getText("[SERVICE_MENU] PLAYER2")) {
return Player::Id::PLAYER2;
}
return Player::Id::NO_PLAYER;
}
} // namespace Options

360
source/game/options.hpp Normal file
View File

@@ -0,0 +1,360 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_ScaleMode
#include <array> // Para array
#include <cstddef> // Para size_t
#include <exception> // Para exception
#include <fstream> // Para basic_ostream, operator<<, basic_ofstream, basic_ostream::operator<<, ofstream
#include <memory> // Para shared_ptr, __shared_ptr_access, swap
#include <stdexcept> // Para out_of_range, invalid_argument
#include <string> // Para char_traits, basic_string, string, operator==, operator<<, swap, stoi
#include <string_view> // Para string_view
#include <utility> // Para move
#include <vector> // Para vector
#include "defaults.hpp"
#include "difficulty.hpp" // for Code
#include "input.hpp" // for Input
#include "lang.hpp" // for Code
#include "manage_hiscore_table.hpp" // for ManageHiScoreTable, Table
#include "player.hpp" // for Player
#include "rendering/shader_backend.hpp" // for Rendering::ShaderType
// --- Namespace Options: gestión de configuración y opciones del juego ---
namespace Options {
// --- Estructuras ---
struct PostFXPreset {
std::string name;
float vignette{0.6F};
float scanlines{0.7F};
float chroma{0.15F};
float mask{0.0F};
float gamma{0.0F};
float curvature{0.0F};
float bleeding{0.0F};
float flicker{0.0F};
};
struct CrtPiPreset {
std::string name;
float scanline_weight{6.0F};
float scanline_gap_brightness{0.12F};
float bloom_factor{3.5F};
float input_gamma{2.4F};
float output_gamma{2.2F};
float mask_brightness{0.80F};
float curvature_x{0.05F};
float curvature_y{0.10F};
int mask_type{2};
bool enable_scanlines{true};
bool enable_multisample{true};
bool enable_gamma{true};
bool enable_curvature{false};
bool enable_sharper{false};
};
struct Window {
std::string caption = Defaults::Window::CAPTION;
int zoom = Defaults::Window::ZOOM;
int max_zoom = Defaults::Window::MAX_ZOOM;
};
struct GPU {
bool acceleration{Defaults::Video::GPU_ACCELERATION};
std::string preferred_driver;
};
struct Supersampling {
bool enabled{Defaults::Video::SUPERSAMPLING};
bool linear_upscale{Defaults::Video::LINEAR_UPSCALE};
int downscale_algo{Defaults::Video::DOWNSCALE_ALGO};
};
struct ShaderConfig {
bool enabled{Defaults::Video::SHADER_ENABLED};
Rendering::ShaderType current_shader{Rendering::ShaderType::POSTFX};
std::string current_postfx_preset_name;
std::string current_crtpi_preset_name;
int current_postfx_preset{0};
int current_crtpi_preset{0};
};
struct Video {
SDL_ScaleMode scale_mode = Defaults::Video::SCALE_MODE;
bool fullscreen = Defaults::Video::FULLSCREEN;
bool vsync = Defaults::Video::VSYNC;
bool integer_scale = Defaults::Video::INTEGER_SCALE;
std::string info;
GPU gpu{};
Supersampling supersampling{};
ShaderConfig shader{};
};
struct Music {
bool enabled = Defaults::Music::ENABLED; // Indica si la música suena o no
int volume = Defaults::Music::VOLUME; // Volumen de la música
};
struct Sound {
bool enabled = Defaults::Sound::ENABLED; // Indica si los sonidos suenan o no
int volume = Defaults::Sound::VOLUME; // Volumen de los sonidos
};
struct Audio {
Music music; // Opciones para la música
Sound sound; // Opciones para los efectos de sonido
bool enabled = Defaults::Audio::ENABLED; // Indica si el audio está activo o no
int volume = Defaults::Audio::VOLUME; // Volumen general del audio
};
struct Settings {
static constexpr int CURRENT_CONFIG_VERSION = 3; // Versión esperada del fichero
int config_version = CURRENT_CONFIG_VERSION; // Versión del archivo de configuración
Difficulty::Code difficulty = Difficulty::Code::NORMAL; // Dificultad del juego
Lang::Code language = Lang::Code::VALENCIAN; // Idioma usado en el juego
bool autofire = Defaults::Settings::AUTOFIRE; // Indicador de autofire
bool shutdown_enabled = Defaults::Settings::SHUTDOWN_ENABLED; // Especifica si se puede apagar el sistema
Table hi_score_table; // Tabla de mejores puntuaciones
std::vector<int> glowing_entries = {ManageHiScoreTable::NO_ENTRY, ManageHiScoreTable::NO_ENTRY}; // Últimas posiciones de entrada en la tabla
std::string config_file; // Ruta al fichero donde guardar la configuración y las opciones del juego
std::string controllers_file; // Ruta al fichero con las configuraciones de los mandos
std::string params_file = Defaults::Settings::PARAMS_FILE; // Ruta al fichero de parámetros del juego
// Reinicia las últimas entradas de puntuación
void clearLastHiScoreEntries() {
glowing_entries.at(0) = ManageHiScoreTable::NO_ENTRY;
glowing_entries.at(1) = ManageHiScoreTable::NO_ENTRY;
}
};
struct Gamepad {
std::shared_ptr<Input::Gamepad> instance = nullptr; // Referencia al mando
std::string name; // Nombre del mando
std::string path; // Ruta física del dispositivo
Player::Id player_id; // Jugador asociado al mando
Gamepad(Player::Id custom_player_id = Player::Id::NO_PLAYER)
: player_id(custom_player_id) {}
};
// --- Clases ---
class GamepadManager {
public:
void init() {
gamepads_[0] = Gamepad(Player::Id::PLAYER1);
gamepads_[1] = Gamepad(Player::Id::PLAYER2);
}
// Acceso directo por player_id (más intuitivo)
auto getGamepad(Player::Id player_id) -> Gamepad& {
return gamepads_[playerIdToIndex(player_id)];
}
[[nodiscard]] auto getGamepad(Player::Id player_id) const -> const Gamepad& {
return gamepads_[playerIdToIndex(player_id)];
}
// Acceso por índice (más eficiente si ya tienes el índice)
auto operator[](size_t index) -> Gamepad& {
if (index >= MAX_PLAYERS) {
throw std::out_of_range("Invalid gamepad index");
}
return gamepads_[index];
}
auto operator[](size_t index) const -> const Gamepad& {
if (index >= MAX_PLAYERS) {
throw std::out_of_range("Invalid gamepad index");
}
return gamepads_[index];
}
auto assignGamepadToPlayer(Player::Id player_id,
std::shared_ptr<Input::Gamepad> instance,
const std::string& name) -> bool {
try {
auto& gamepad = getGamepad(player_id);
gamepad.instance = std::move(instance);
gamepad.name = name;
return true;
} catch (const std::exception&) {
return false;
}
}
void swapPlayers() {
std::swap(gamepads_[0].instance, gamepads_[1].instance);
std::swap(gamepads_[0].name, gamepads_[1].name);
std::swap(gamepads_[0].path, gamepads_[1].path);
resyncGamepadsWithPlayers();
}
void resyncGamepadsWithPlayers() {
for (const auto& player : players_) {
switch (player->getId()) {
case Player::Id::PLAYER1:
player->setGamepad(gamepads_[0].instance);
break;
case Player::Id::PLAYER2:
player->setGamepad(gamepads_[1].instance);
break;
default:
break;
}
}
}
void saveToFile(std::ofstream& file) const {
for (size_t i = 0; i < MAX_PLAYERS; ++i) {
const auto& gamepad = gamepads_[i];
file << " - name: \"" << (gamepad.path.empty() ? "" : gamepad.name) << "\"\n";
file << " path: \"" << gamepad.path << "\"\n";
file << " player: " << static_cast<int>(gamepad.player_id) << "\n";
}
}
// Método helper para parseAndSetController
auto setControllerProperty(size_t controller_index,
const std::string& property,
const std::string& value) -> bool {
if (controller_index >= MAX_PLAYERS) {
return false;
}
auto& gamepad = gamepads_[controller_index];
if (property == "name") {
gamepad.name = value;
return true;
}
if (property == "path") {
gamepad.path = value;
return true;
}
if (property == "player") {
try {
int player_int = std::stoi(value);
if (player_int == 1) {
gamepad.player_id = Player::Id::PLAYER1;
} else if (player_int == 2) {
gamepad.player_id = Player::Id::PLAYER2;
} else {
return false;
}
return true;
} catch (const std::exception&) {
return false;
}
}
return false;
}
void addPlayer(const std::shared_ptr<Player>& player) { players_.push_back(player); } // Añade un jugador a la lista
void clearPlayers() { players_.clear(); } // Limpia la lista de jugadores
// Asigna el mando a un jugador
void assignTo(const Input::Gamepad& gamepad, Player::Id player_id) {
}
// Asigna los mandos físicos basándose en la configuración actual de nombres.
void assignAndLinkGamepads();
// Iteradores
auto begin() { return gamepads_.begin(); }
auto end() { return gamepads_.end(); }
[[nodiscard]] auto begin() const { return gamepads_.begin(); }
[[nodiscard]] auto end() const { return gamepads_.end(); }
[[nodiscard]] static auto size() -> size_t { return MAX_PLAYERS; }
private:
static constexpr std::string_view UNASSIGNED_TEXT = "---";
static constexpr size_t MAX_PLAYERS = 2;
std::array<Gamepad, MAX_PLAYERS> gamepads_; // Punteros a las estructuras de mandos de Options
std::vector<std::shared_ptr<Player>> players_; // Punteros a los jugadores
// Convierte Player::Id a índice del array
[[nodiscard]] static auto playerIdToIndex(Player::Id player_id) -> size_t {
switch (player_id) {
case Player::Id::PLAYER1:
return 0;
case Player::Id::PLAYER2:
return 1;
default:
throw std::invalid_argument("Invalid player ID");
}
}
void assignGamepadsByPath(
const std::array<std::string, MAX_PLAYERS>& desired_paths,
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-avoid-const-params-in-decls)
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances);
void assignRemainingGamepads(
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-avoid-const-params-in-decls)
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances);
void clearUnassignedGamepadSlots();
[[nodiscard]] static auto isGamepadAssigned(
const std::shared_ptr<Input::Gamepad>& physical_gamepad,
const std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) -> bool; // NOLINT(readability-avoid-const-params-in-decls)
};
struct Keyboard {
Player::Id player_id = Player::Id::PLAYER1; // Jugador asociado al teclado
std::vector<std::shared_ptr<Player>> players; // Punteros a los jugadores
void addPlayer(const std::shared_ptr<Player>& player) { players.push_back(player); } // Añade un jugador a la lista
void clearPlayers() { players.clear(); } // Limpia la lista de jugadores
// Asigna el teclado a un jugador
void assignTo(Player::Id player_id) {
this->player_id = player_id;
for (auto& player : players) {
player->setUsesKeyboard(player->getId() == player_id);
}
}
};
struct PendingChanges {
Lang::Code new_language = Lang::Code::VALENCIAN; // Idioma en espera de aplicar
Difficulty::Code new_difficulty = Difficulty::Code::NORMAL; // Dificultad en espera de aplicar
bool has_pending_changes = false; // Indica si hay cambios pendientes
};
// --- Variables ---
extern Window window; // Opciones de la ventana
extern Settings settings; // Opciones del juego
extern Video video; // Opciones de vídeo
extern Audio audio; // Opciones de audio
extern GamepadManager gamepad_manager; // Manager de mandos para cada jugador
extern Keyboard keyboard; // Opciones para el teclado
extern PendingChanges pending_changes; // Opciones que se aplican al cerrar
extern std::vector<PostFXPreset> postfx_presets; // Lista de presets de PostFX
extern std::string postfx_file_path; // Ruta al fichero de presets PostFX
extern std::vector<CrtPiPreset> crtpi_presets; // Lista de presets de CrtPi
extern std::string crtpi_file_path; // Ruta al fichero de presets CrtPi
// --- Funciones ---
void init(); // Inicializa las opciones del programa
void setConfigFile(const std::string& file_path); // Establece el fichero de configuración
void setControllersFile(const std::string& file_path); // Establece el fichero de configuración de mandos
void setPostFXFile(const std::string& path); // Establece el fichero de presets PostFX
void setCrtPiFile(const std::string& path); // Establece el fichero de presets CrtPi
auto loadPostFXFromFile() -> bool; // Carga los presets PostFX desde fichero
auto savePostFXToFile() -> bool; // Guarda los presets PostFX por defecto al fichero
auto loadCrtPiFromFile() -> bool; // Carga los presets CrtPi desde fichero
auto loadFromFile() -> bool; // Carga el fichero de configuración
auto saveToFile() -> bool; // Guarda el fichero de configuración
void setKeyboardToPlayer(Player::Id player_id); // Asigna el teclado al jugador
void swapKeyboard(); // Intercambia el teclado de jugador
void swapControllers(); // Intercambia los jugadores asignados a los dos primeros mandos
auto getPlayerWhoUsesKeyboard() -> Player::Id; // Averigua quién está usando el teclado
auto playerIdToString(Player::Id player_id) -> std::string; // Convierte un player id a texto segun Lang
auto stringToPlayerId(const std::string& name) -> Player::Id; // Convierte un texto a player id segun Lang
void applyPendingChanges(); // Aplica los cambios pendientes copiando los valores a sus variables
void checkPendingChanges(); // Verifica si hay cambios pendientes
auto assignGamepadByName(const std::string& gamepad_name, Player::Id player_id) -> bool; // Buscar y asignar un mando disponible por nombre
} // namespace Options

View File

@@ -0,0 +1,742 @@
// IWYU pragma: no_include <bits/std_abs.h>
#include "credits.hpp"
#include <SDL3/SDL.h> // Para SDL_RenderFillRect, SDL_RenderTexture, SDL_SetRenderTarget, SDL_SetRenderDrawColor, SDL_CreateTexture, SDL_DestroyTexture, SDL_GetTicks, SDL_GetRenderTarget, SDL_PixelFormat, SDL_PollEvent, SDL_RenderClear, SDL_RenderRect, SDL_SetTextureBlendMode, SDL_TextureAccess, SDL_BLENDMODE_BLEND, SDL_Event, Uint64
#include <algorithm> // Para max, min, clamp
#include <array> // Para array
#include <cmath> // Para abs
#include <stdexcept> // Para runtime_error
#include <string> // Para basic_string, string
#include <string_view> // Para string_view
#include <vector> // Para vector
#include "audio.hpp" // Para Audio
#include "balloon_manager.hpp" // Para BalloonManager
#include "color.hpp" // Para Color, SHADOW_TEXT, NO_COLOR_MOD
#include "fade.hpp" // Para Fade
#include "global_events.hpp" // Para handle
#include "global_inputs.hpp" // Para check
#include "input.hpp" // Para Input
#include "lang.hpp" // Para getText
#include "param.hpp" // Para Param, param, ParamGame, ParamFade
#include "player.hpp" // Para Player
#include "resource.hpp" // Para Resource
#include "screen.hpp" // Para Screen
#include "section.hpp" // Para Name, name
#include "sprite.hpp" // Para Sprite
#include "text.hpp" // Para Text
#include "texture.hpp" // Para Texture
#include "tiled_bg.hpp" // Para TiledBG, TiledBGMode
#include "ui/service_menu.hpp" // Para ServiceMenu
#include "utils.hpp" // Para Zone
// Textos
constexpr std::string_view TEXT_COPYRIGHT = "@2020,2025 JailDesigner";
// Constructor
Credits::Credits()
: balloon_manager_(std::make_unique<BalloonManager>(nullptr)),
tiled_bg_(std::make_unique<TiledBG>(param.game.game_area.rect, TiledBGMode::DIAGONAL)),
fade_in_(std::make_unique<Fade>()),
fade_out_(std::make_unique<Fade>()),
text_texture_(SDL_CreateTexture(Screen::get()->getRenderer(), SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, static_cast<int>(param.game.width), static_cast<int>(param.game.height))),
canvas_(SDL_CreateTexture(Screen::get()->getRenderer(), SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, static_cast<int>(param.game.width), static_cast<int>(param.game.height))) {
if (text_texture_ == nullptr) {
throw std::runtime_error("Failed to create SDL texture for text.");
}
initVars();
startCredits();
// Inicializa el timer de delta time para el primer frame del callback
last_time_ = SDL_GetTicks();
}
// Destructor
Credits::~Credits() {
SDL_DestroyTexture(text_texture_);
SDL_DestroyTexture(canvas_);
resetVolume();
Audio::get()->stopMusic();
// Desregistra los jugadores de Options
Options::keyboard.clearPlayers();
Options::gamepad_manager.clearPlayers();
}
// Calcula el deltatime
auto Credits::calculateDeltaTime() -> float {
const Uint64 CURRENT_TIME = SDL_GetTicks();
const float DELTA_TIME = static_cast<float>(CURRENT_TIME - last_time_) / 1000.0F; // Convertir ms a segundos
last_time_ = CURRENT_TIME;
return DELTA_TIME;
}
// Avanza un frame (llamado desde Director::iterate)
void Credits::iterate() {
checkInput();
const float DELTA_TIME = calculateDeltaTime();
update(DELTA_TIME);
render();
}
// Procesa un evento (llamado desde Director::handleEvent)
void Credits::handleEvent(const SDL_Event& /*event*/) {
// Eventos globales ya gestionados por Director::handleEvent
}
// Bucle principal legacy (fallback)
void Credits::run() {
last_time_ = SDL_GetTicks();
while (Section::name == Section::Name::CREDITS) {
checkInput();
const float DELTA_TIME = calculateDeltaTime();
update(DELTA_TIME);
checkEvents(); // Tiene que ir antes del render
render();
}
}
// Actualiza las variables (time-based puro - sin conversión frame-based)
void Credits::update(float delta_time) {
const float MULTIPLIER = want_to_pass_ ? FAST_FORWARD_MULTIPLIER : 1.0F;
const float ADJUSTED_DELTA_TIME = delta_time * MULTIPLIER;
static auto* const SCREEN = Screen::get();
SCREEN->update(delta_time); // Actualiza el objeto screen
Audio::update(); // Actualiza el objeto audio
tiled_bg_->update(ADJUSTED_DELTA_TIME);
cycleColors(ADJUSTED_DELTA_TIME);
balloon_manager_->update(ADJUSTED_DELTA_TIME);
updateTextureDstRects(ADJUSTED_DELTA_TIME);
throwBalloons(ADJUSTED_DELTA_TIME);
updatePlayers(ADJUSTED_DELTA_TIME);
updateAllFades(ADJUSTED_DELTA_TIME);
fillCanvas();
}
// Dibuja Credits::en patalla
void Credits::render() {
static auto* const SCREEN = Screen::get();
SCREEN->start(); // Prepara para empezar a dibujar en la textura de juego
SDL_RenderTexture(SCREEN->getRenderer(), canvas_, nullptr, nullptr); // Copia la textura con la zona de juego a la pantalla
SCREEN->render(); // Vuelca el contenido del renderizador en pantalla
}
// Comprueba el manejador de eventos
void Credits::checkEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event);
}
}
// Comprueba las entradas
void Credits::checkInput() {
Input::get()->update();
if (!ServiceMenu::get()->isEnabled()) {
// Comprueba si se ha pulsado cualquier botón (de los usados para jugar)
if (Input::get()->checkAnyButton(Input::ALLOW_REPEAT)) {
want_to_pass_ = true;
fading_ = mini_logo_on_position_;
} else {
want_to_pass_ = false;
}
}
// Comprueba los inputs que se pueden introducir en cualquier sección del juego
GlobalInputs::check();
}
// Crea la textura con el texto
void Credits::fillTextTexture() {
auto text = Resource::get()->getText("smb2");
auto text_grad = Resource::get()->getText("smb2_grad");
SDL_SetRenderTarget(Screen::get()->getRenderer(), text_texture_);
SDL_SetRenderDrawColor(Screen::get()->getRenderer(), 0, 0, 0, 0);
SDL_RenderClear(Screen::get()->getRenderer());
const std::array<std::string, 11> TEXTS = {
Lang::getText("[CREDITS] PROGRAMMED_AND_DESIGNED_BY"),
Lang::getText("[CREDITS] PIXELART_DRAWN_BY"),
Lang::getText("[CREDITS] MUSIC_COMPOSED_BY"),
Lang::getText("[CREDITS] SOUND_EFFECTS"),
"JAILDESIGNER",
"JAILDOCTOR",
"ERIC MATYAS (SOUNDIMAGE.ORG)",
"WWW.THEMOTIONMONKEY.CO.UK",
"WWW.KENNEY.NL",
"JAILDOCTOR",
"JAILDESIGNER"};
const int SPACE_POST_TITLE = 3 + text->getCharacterSize();
const int SPACE_PRE_TITLE = text->getCharacterSize() * 4;
const int TEXTS_HEIGHT = (1 * text->getCharacterSize()) + (8 * SPACE_POST_TITLE) + (3 * SPACE_PRE_TITLE);
const int POS_X = static_cast<int>(param.game.game_area.center_x);
credits_rect_dst_.h = credits_rect_src_.h = static_cast<float>(TEXTS_HEIGHT);
auto text_style = Text::Style(Text::CENTER | Text::SHADOW, Colors::NO_COLOR_MOD, Colors::SHADOW_TEXT);
// PROGRAMMED_AND_DESIGNED_BY
int y = 0;
text_grad->writeStyle(POS_X, y, TEXTS.at(0), text_style);
y += SPACE_POST_TITLE;
text->writeStyle(POS_X, y, TEXTS.at(4), text_style);
// PIXELART_DRAWN_BY
y += SPACE_PRE_TITLE;
text_grad->writeStyle(POS_X, y, TEXTS.at(1), text_style);
y += SPACE_POST_TITLE;
text->writeStyle(POS_X, y, TEXTS.at(4), text_style);
// MUSIC_COMPOSED_BY
y += SPACE_PRE_TITLE;
text_grad->writeStyle(POS_X, y, TEXTS.at(2), text_style);
y += SPACE_POST_TITLE;
text->writeStyle(POS_X, y, TEXTS.at(5), text_style);
y += SPACE_POST_TITLE;
text->writeStyle(POS_X, y, TEXTS.at(6), text_style);
// SOUND_EFFECTS
y += SPACE_PRE_TITLE;
text_grad->writeStyle(POS_X, y, TEXTS.at(3), text_style);
y += SPACE_POST_TITLE;
text->writeStyle(POS_X, y, TEXTS.at(7), text_style);
y += SPACE_POST_TITLE;
text->writeStyle(POS_X, y, TEXTS.at(8), text_style);
y += SPACE_POST_TITLE;
text->writeStyle(POS_X, y, TEXTS.at(9), text_style);
y += SPACE_POST_TITLE;
text->writeStyle(POS_X, y, TEXTS.at(10), text_style);
// Mini logo
y += SPACE_PRE_TITLE;
mini_logo_rect_src_.y = static_cast<float>(y);
auto mini_logo_sprite = std::make_unique<Sprite>(Resource::get()->getTexture("logo_jailgames_mini.png"));
mini_logo_sprite->setPosition(1 + POS_X - (mini_logo_sprite->getWidth() / 2), 1 + y);
Resource::get()->getTexture("logo_jailgames_mini.png")->setColor(Colors::SHADOW_TEXT.r, Colors::SHADOW_TEXT.g, Colors::SHADOW_TEXT.b);
mini_logo_sprite->render();
mini_logo_sprite->setPosition(POS_X - (mini_logo_sprite->getWidth() / 2), y);
Resource::get()->getTexture("logo_jailgames_mini.png")->setColor(255, 255, 255);
mini_logo_sprite->render();
// Texto con el copyright
y += mini_logo_sprite->getHeight() + 3;
text->writeDX(Text::CENTER | Text::SHADOW, POS_X, y, std::string(TEXT_COPYRIGHT), 1, Colors::NO_COLOR_MOD, 1, Colors::SHADOW_TEXT);
// Resetea el renderizador
SDL_SetRenderTarget(Screen::get()->getRenderer(), nullptr);
// Actualiza las variables
mini_logo_rect_dst_.h = mini_logo_rect_src_.h = mini_logo_sprite->getHeight() + 3 + text->getCharacterSize();
credits_rect_dst_.y = param.game.game_area.rect.h;
mini_logo_rect_dst_.y = credits_rect_dst_.y + credits_rect_dst_.h + 30;
mini_logo_final_pos_ = param.game.game_area.center_y - (mini_logo_rect_src_.h / 2);
}
// Dibuja todos los sprites en la textura
void Credits::fillCanvas() {
// Cambia el destino del renderizador
auto* temp = SDL_GetRenderTarget(Screen::get()->getRenderer());
SDL_SetRenderTarget(Screen::get()->getRenderer(), canvas_);
// Dibuja el fondo, los globos y los jugadores
tiled_bg_->render();
balloon_manager_->render();
renderPlayers();
// Dibuja los titulos de credito
SDL_RenderTexture(Screen::get()->getRenderer(), text_texture_, &credits_rect_src_, &credits_rect_dst_);
// Dibuja el mini_logo
SDL_RenderTexture(Screen::get()->getRenderer(), text_texture_, &mini_logo_rect_src_, &mini_logo_rect_dst_);
// Dibuja los rectangulos negros
SDL_SetRenderDrawColor(Screen::get()->getRenderer(), 0, 0, 0, 0xFF);
SDL_RenderFillRect(Screen::get()->getRenderer(), &top_black_rect_);
SDL_RenderFillRect(Screen::get()->getRenderer(), &bottom_black_rect_);
SDL_RenderFillRect(Screen::get()->getRenderer(), &left_black_rect_);
SDL_RenderFillRect(Screen::get()->getRenderer(), &right_black_rect_);
// Dibuja el rectangulo rojo
drawBorderRect();
// Si el mini_logo está en su destino, lo dibuja encima de lo anterior
if (mini_logo_on_position_) {
SDL_RenderTexture(Screen::get()->getRenderer(), text_texture_, &mini_logo_rect_src_, &mini_logo_rect_dst_);
}
// Dibuja el fade sobre el resto de elementos
fade_in_->render();
fade_out_->render();
// Deja el renderizador apuntando donde estaba
SDL_SetRenderTarget(Screen::get()->getRenderer(), temp);
}
// Actualiza el destino de los rectangulos de las texturas (time-based puro)
void Credits::updateTextureDstRects(float delta_time) {
constexpr float TEXTURE_UPDATE_INTERVAL_S = 10.0F / 60.0F; // ~0.167s (cada 10 frames)
credits_state_.texture_accumulator += delta_time;
if (credits_state_.texture_accumulator >= TEXTURE_UPDATE_INTERVAL_S) {
credits_state_.texture_accumulator -= TEXTURE_UPDATE_INTERVAL_S;
// Comprueba la posición de la textura con los titulos de credito
if (credits_rect_dst_.y + credits_rect_dst_.h > play_area_.y) {
--credits_rect_dst_.y;
}
// Comprueba la posición de la textura con el mini_logo
if (mini_logo_rect_dst_.y <= static_cast<float>(mini_logo_final_pos_)) {
// Forzar posición exacta para evitar problemas de comparación float
mini_logo_rect_dst_.y = static_cast<float>(mini_logo_final_pos_);
mini_logo_on_position_ = true;
} else {
--mini_logo_rect_dst_.y;
}
}
// Acumular tiempo desde que el logo llegó a su posición (fuera del if para que se ejecute cada frame)
if (mini_logo_on_position_) {
time_since_logo_positioned_ += delta_time;
// Timeout para evitar que la sección sea infinita
if (time_since_logo_positioned_ >= MAX_TIME_AFTER_LOGO_S) {
fading_ = true;
}
// Si el jugador quiere pasar los titulos de credito, el fade se inicia solo
if (want_to_pass_) {
fading_ = true;
}
}
}
// Tira globos al escenario (time-based puro)
void Credits::throwBalloons(float delta_time) {
constexpr int SPEED = 200;
constexpr size_t NUM_SETS = 8; // Tamaño del vector SETS
const std::vector<int> SETS = {0, 63, 25, 67, 17, 75, 13, 50};
constexpr float BALLOON_INTERVAL_S = SPEED / 60.0F; // ~3.33s (cada 200 frames)
constexpr float POWERBALL_INTERVAL_S = (SPEED * 4) / 60.0F; // ~13.33s (cada 800 frames)
constexpr float MAX_BALLOON_TIME_S = ((NUM_SETS - 1) * SPEED * 3) / 60.0F; // Tiempo máximo para lanzar globos
// Acumular tiempo total de globos
elapsed_time_balloons_ += delta_time;
// Detener lanzamiento después del tiempo límite
if (elapsed_time_balloons_ > MAX_BALLOON_TIME_S) {
return;
}
credits_state_.balloon_accumulator += delta_time;
credits_state_.powerball_accumulator += delta_time;
if (credits_state_.balloon_accumulator >= BALLOON_INTERVAL_S) {
credits_state_.balloon_accumulator -= BALLOON_INTERVAL_S;
const int INDEX = (static_cast<int>(elapsed_time_balloons_ * 60.0F / SPEED)) % SETS.size();
balloon_manager_->deployFormation(SETS.at(INDEX), -60);
}
if (credits_state_.powerball_accumulator >= POWERBALL_INTERVAL_S && elapsed_time_balloons_ > 0.0F) {
credits_state_.powerball_accumulator -= POWERBALL_INTERVAL_S;
balloon_manager_->createPowerBall();
}
}
// Inicializa los jugadores
void Credits::initPlayers() {
std::vector<std::vector<std::shared_ptr<Texture>>> player_textures; // Vector con todas las texturas de los jugadores;
std::vector<std::vector<std::string>> player1_animations; // Vector con las animaciones del jugador 1
std::vector<std::vector<std::string>> player2_animations; // Vector con las animaciones del jugador 2
// Texturas - Player1
std::vector<std::shared_ptr<Texture>> player1_textures;
player1_textures.emplace_back(Resource::get()->getTexture("player1_pal0"));
player1_textures.emplace_back(Resource::get()->getTexture("player1_pal1"));
player1_textures.emplace_back(Resource::get()->getTexture("player1_pal2"));
player1_textures.emplace_back(Resource::get()->getTexture("player1_pal3"));
player1_textures.emplace_back(Resource::get()->getTexture("player1_power.png"));
player_textures.push_back(player1_textures);
// Texturas - Player2
std::vector<std::shared_ptr<Texture>> player2_textures;
player2_textures.emplace_back(Resource::get()->getTexture("player2_pal0"));
player2_textures.emplace_back(Resource::get()->getTexture("player2_pal1"));
player2_textures.emplace_back(Resource::get()->getTexture("player2_pal2"));
player2_textures.emplace_back(Resource::get()->getTexture("player2_pal3"));
player2_textures.emplace_back(Resource::get()->getTexture("player2_power.png"));
player_textures.push_back(player2_textures);
// Animaciones -- Jugador
player1_animations.emplace_back(Resource::get()->getAnimation("player1.ani"));
player1_animations.emplace_back(Resource::get()->getAnimation("player_power.ani"));
player2_animations.emplace_back(Resource::get()->getAnimation("player2.ani"));
player2_animations.emplace_back(Resource::get()->getAnimation("player_power.ani"));
// Crea los dos jugadores
const int Y = play_area_.y + play_area_.h - Player::WIDTH;
constexpr bool DEMO = false;
constexpr int AWAY_DISTANCE = 700;
Player::Config config_player1;
config_player1.id = Player::Id::PLAYER1;
config_player1.x = play_area_.x - AWAY_DISTANCE - Player::WIDTH;
config_player1.y = Y;
config_player1.demo = DEMO;
config_player1.play_area = &play_area_;
config_player1.texture = player_textures.at(0);
config_player1.animations = player1_animations;
config_player1.hi_score_table = &Options::settings.hi_score_table;
config_player1.glowing_entry = &Options::settings.glowing_entries.at(static_cast<int>(Player::Id::PLAYER1) - 1);
players_.emplace_back(std::make_unique<Player>(config_player1));
players_.back()->setWalkingState(Player::State::WALKING_RIGHT);
players_.back()->setPlayingState(Player::State::CREDITS);
Player::Config config_player2;
config_player2.id = Player::Id::PLAYER2;
config_player2.x = play_area_.x + play_area_.w + AWAY_DISTANCE;
config_player2.y = Y;
config_player2.demo = DEMO;
config_player2.play_area = &play_area_;
config_player2.texture = player_textures.at(1);
config_player2.animations = player2_animations;
config_player2.hi_score_table = &Options::settings.hi_score_table;
config_player2.glowing_entry = &Options::settings.glowing_entries.at(static_cast<int>(Player::Id::PLAYER2) - 1);
players_.emplace_back(std::make_unique<Player>(config_player2));
players_.back()->setWalkingState(Player::State::WALKING_LEFT);
players_.back()->setPlayingState(Player::State::CREDITS);
// Registra los jugadores en Options
for (const auto& player : players_) {
Options::keyboard.addPlayer(player);
Options::gamepad_manager.addPlayer(player);
}
}
// Actualiza los rectangulos negros (time-based)
void Credits::updateBlackRects(float delta_time) {
if (!initialized_) { return; }
delta_time = std::max(delta_time, 0.0F);
// Fase vertical: hasta que ambos rects verticales estén exactos en su target
if (!vertical_done_) {
credits_state_.black_rect_accumulator += delta_time;
if (credits_state_.black_rect_accumulator >= BLACK_RECT_INTERVAL_S) {
credits_state_.black_rect_accumulator -= BLACK_RECT_INTERVAL_S;
// top
int prev_top_h = static_cast<int>(top_black_rect_.h);
top_black_rect_.h = std::min(top_black_rect_.h + 1.0F,
static_cast<float>(param.game.game_area.center_y - 1));
int top_delta = static_cast<int>(top_black_rect_.h) - prev_top_h;
// bottom
int prev_bottom_h = static_cast<int>(bottom_black_rect_.h);
int prev_bottom_y = static_cast<int>(bottom_black_rect_.y);
bottom_black_rect_.h = bottom_black_rect_.h + 1.0F;
bottom_black_rect_.y = std::max(bottom_black_rect_.y - 1.0F,
static_cast<float>(param.game.game_area.center_y + 1));
int bottom_steps_by_h = static_cast<int>(bottom_black_rect_.h) - prev_bottom_h;
int bottom_steps_by_y = prev_bottom_y - static_cast<int>(bottom_black_rect_.y);
int bottom_steps = std::max({0, bottom_steps_by_h, bottom_steps_by_y});
int steps_done = top_delta + bottom_steps;
if (steps_done > 0) {
current_step_ = std::max(0.0F, current_step_ - static_cast<float>(steps_done));
float vol_f = initial_volume_ * (current_step_ / static_cast<float>(total_steps_));
int vol_i = static_cast<int>(std::clamp(vol_f, 0.0F, static_cast<float>(initial_volume_)));
Audio::get()->setMusicVolume(vol_i); // usa tu API de audio aquí
}
// Si han alcanzado los objetivos, fijarlos exactamente y marcar done
bool top_at_target = static_cast<int>(top_black_rect_.h) == param.game.game_area.center_y - 1.0F;
bool bottom_at_target = static_cast<int>(bottom_black_rect_.y) == param.game.game_area.center_y + 1.0F;
if (top_at_target && bottom_at_target) {
top_black_rect_.h = param.game.game_area.center_y - 1.0F;
bottom_black_rect_.y = param.game.game_area.center_y + 1.0F;
vertical_done_ = true;
}
}
// actualizar border_rect cada frame aunque todavía en fase vertical
updateBorderRect();
return;
}
// Fase horizontal
if (!horizontal_done_) {
int prev_left_w = static_cast<int>(left_black_rect_.w);
left_black_rect_.w = std::min(left_black_rect_.w + static_cast<float>(HORIZONTAL_SPEED),
param.game.game_area.center_x);
int left_gain = static_cast<int>(left_black_rect_.w) - prev_left_w;
int prev_right_x = static_cast<int>(right_black_rect_.x);
right_black_rect_.w = right_black_rect_.w + static_cast<float>(HORIZONTAL_SPEED);
right_black_rect_.x = std::max(right_black_rect_.x - static_cast<float>(HORIZONTAL_SPEED),
param.game.game_area.center_x);
int right_move = prev_right_x - static_cast<int>(right_black_rect_.x);
int steps_done = left_gain + right_move;
if (steps_done > 0) {
current_step_ = std::max(0.0F, current_step_ - static_cast<float>(steps_done));
float vol_f = initial_volume_ * (current_step_ / static_cast<float>(total_steps_));
int vol_i = static_cast<int>(std::clamp(vol_f, 0.0F, static_cast<float>(initial_volume_)));
Audio::get()->setMusicVolume(vol_i); // usa tu API de audio aquí
}
bool left_at_target = static_cast<int>(left_black_rect_.w) == param.game.game_area.center_x;
bool right_at_target = static_cast<int>(right_black_rect_.x) == param.game.game_area.center_x;
if (left_at_target && right_at_target) {
left_black_rect_.w = param.game.game_area.center_x;
right_black_rect_.x = param.game.game_area.center_x;
horizontal_done_ = true;
}
updateBorderRect();
return;
}
// Fase final: ya completado el movimiento de rects
Audio::get()->setMusicVolume(0);
// Audio::get()->stopMusic(); // opcional, si quieres parar la reproducción
// Usar segundos puros en lugar de frames equivalentes
if (counter_pre_fade_ >= PRE_FADE_DELAY_S) {
if (fade_out_) {
fade_out_->activate();
}
} else {
counter_pre_fade_ += delta_time;
}
}
// Actualiza el rectangulo del borde
void Credits::updateBorderRect() {
border_rect_.x = left_black_rect_.x + left_black_rect_.w;
border_rect_.y = top_black_rect_.y + top_black_rect_.h - 1.0F;
float raw_w = right_black_rect_.x - border_rect_.x;
float raw_h = bottom_black_rect_.y - border_rect_.y + 1.0F;
border_rect_.w = std::max(0.0F, raw_w);
border_rect_.h = std::max(0.0F, raw_h);
}
// Actualiza el estado de fade (time-based)
void Credits::updateAllFades(float delta_time) {
if (fading_) {
updateBlackRects(delta_time);
updateBorderRect();
}
fade_in_->update();
if (fade_in_->hasEnded() && Audio::get()->getMusicState() != Audio::MusicState::PLAYING) {
Audio::get()->playMusic("credits.ogg");
}
fade_out_->update();
if (fade_out_->hasEnded()) {
Section::name = Section::Name::HI_SCORE_TABLE;
}
}
// Establece el nivel de volumen
void Credits::setVolume(int amount) {
Options::audio.music.volume = std::clamp(amount, 0, 100);
Audio::get()->setMusicVolume(Options::audio.music.volume);
}
// Reestablece el nivel de volumen
void Credits::resetVolume() const {
Options::audio.music.volume = initial_volume_;
Audio::get()->setMusicVolume(Options::audio.music.volume);
}
// Cambia el color del fondo (time-based)
void Credits::cycleColors(float delta_time) {
constexpr int UPPER_LIMIT = 140; // Límite superior
constexpr int LOWER_LIMIT = 30; // Límite inferior
// Factor para escalar los valores de incremento.
// Asumimos que los valores originales estaban balanceados para 60 FPS.
const float FRAME_ADJUSTMENT = delta_time * 60.0F;
// Inicializar valores RGB si es la primera vez
if (credits_state_.r == 255.0F && credits_state_.g == 0.0F && credits_state_.b == 0.0F && credits_state_.step_r == -0.5F) {
credits_state_.r = static_cast<float>(UPPER_LIMIT);
credits_state_.g = static_cast<float>(LOWER_LIMIT);
credits_state_.b = static_cast<float>(LOWER_LIMIT);
}
// Ajustar valores de R
credits_state_.r += credits_state_.step_r * FRAME_ADJUSTMENT;
if (credits_state_.r >= UPPER_LIMIT) {
credits_state_.r = UPPER_LIMIT; // Clamp para evitar que se pase
credits_state_.step_r = -credits_state_.step_r; // Cambia de dirección al alcanzar los límites
} else if (credits_state_.r <= LOWER_LIMIT) {
credits_state_.r = LOWER_LIMIT; // Clamp para evitar que se pase
credits_state_.step_r = -credits_state_.step_r;
}
// Ajustar valores de G
credits_state_.g += credits_state_.step_g * FRAME_ADJUSTMENT;
if (credits_state_.g >= UPPER_LIMIT) {
credits_state_.g = UPPER_LIMIT;
credits_state_.step_g = -credits_state_.step_g; // Cambia de dirección al alcanzar los límites
} else if (credits_state_.g <= LOWER_LIMIT) {
credits_state_.g = LOWER_LIMIT;
credits_state_.step_g = -credits_state_.step_g;
}
// Ajustar valores de B
credits_state_.b += credits_state_.step_b * FRAME_ADJUSTMENT;
if (credits_state_.b >= UPPER_LIMIT) {
credits_state_.b = UPPER_LIMIT;
credits_state_.step_b = -credits_state_.step_b; // Cambia de dirección al alcanzar los límites
} else if (credits_state_.b <= LOWER_LIMIT) {
credits_state_.b = LOWER_LIMIT;
credits_state_.step_b = -credits_state_.step_b;
}
// Aplicar el color, redondeando a enteros antes de usar
color_ = Color(static_cast<int>(credits_state_.r), static_cast<int>(credits_state_.g), static_cast<int>(credits_state_.b));
tiled_bg_->setColor(color_);
}
// Actualza los jugadores (time-based)
void Credits::updatePlayers(float delta_time) {
for (auto& player : players_) {
player->update(delta_time);
}
}
// Renderiza los jugadores
void Credits::renderPlayers() {
for (auto const& player : players_) {
player->render();
}
}
// Inicializa variables
void Credits::initVars() {
// Inicialización segura de rects tal y como los mostraste
top_black_rect_ = {
.x = play_area_.x,
.y = param.game.game_area.rect.y,
.w = play_area_.w,
.h = black_bars_size_};
bottom_black_rect_ = {
.x = play_area_.x,
.y = param.game.game_area.rect.h - black_bars_size_,
.w = play_area_.w,
.h = black_bars_size_};
left_black_rect_ = {
.x = play_area_.x,
.y = param.game.game_area.center_y - 1.0F,
.w = 0.0F,
.h = 2.0F};
right_black_rect_ = {
.x = play_area_.x + play_area_.w,
.y = param.game.game_area.center_y - 1.0F,
.w = 0.0F,
.h = 2.0F};
initialized_ = false;
Section::name = Section::Name::CREDITS;
balloon_manager_->setPlayArea(play_area_);
fade_in_->setColor(param.fade.color);
fade_in_->setType(Fade::Type::FULLSCREEN);
fade_in_->setPostDuration(800);
fade_in_->setMode(Fade::Mode::IN);
fade_in_->activate();
fade_out_->setColor(0, 0, 0);
fade_out_->setType(Fade::Type::FULLSCREEN);
fade_out_->setPostDuration(7000);
updateBorderRect();
tiled_bg_->setColor(Color(255, 96, 96));
tiled_bg_->setSpeed(60.0F);
initPlayers();
SDL_SetTextureBlendMode(text_texture_, SDL_BLENDMODE_BLEND);
fillTextTexture();
steps_ = static_cast<int>(std::abs((top_black_rect_.h - param.game.game_area.center_y - 1) + ((left_black_rect_.w - param.game.game_area.center_x) / 4)));
}
void Credits::startCredits() {
// Guardar iniciales (enteros para contar "pasos" por píxel)
init_top_h_ = static_cast<int>(top_black_rect_.h);
init_bottom_y_ = static_cast<int>(bottom_black_rect_.y);
init_left_w_ = static_cast<int>(left_black_rect_.w);
init_right_x_ = static_cast<int>(right_black_rect_.x);
// Objetivos
int top_target_h = param.game.game_area.center_y - 1;
int bottom_target_y = param.game.game_area.center_y + 1;
int left_target_w = param.game.game_area.center_x;
int right_target_x = param.game.game_area.center_x;
// Pasos verticales
int pasos_top = std::max(0, top_target_h - init_top_h_);
int pasos_bottom = std::max(0, init_bottom_y_ - bottom_target_y);
// Pasos horizontales. right se mueve a velocidad HORIZONTAL_SPEED, contamos pasos como unidades de movimiento equivalentes
int pasos_left = std::max(0, left_target_w - init_left_w_);
int dx_right = std::max(0, init_right_x_ - right_target_x);
int pasos_right = (dx_right + (HORIZONTAL_SPEED - 1)) / HORIZONTAL_SPEED; // ceil
total_steps_ = pasos_top + pasos_bottom + pasos_left + pasos_right;
if (total_steps_ <= 0) {
total_steps_ = 1;
}
current_step_ = static_cast<float>(total_steps_);
// Reiniciar contadores y estado
credits_state_.black_rect_accumulator = 0.0F;
counter_pre_fade_ = 0.0F;
initialized_ = true;
// Asegurar volumen inicial consistente
if (steps_ <= 0) {
steps_ = 1;
}
float vol_f = initial_volume_ * (current_step_ / static_cast<float>(total_steps_));
setVolume(static_cast<int>(std::clamp(vol_f, 0.0F, static_cast<float>(initial_volume_))));
}
// Dibuja el rectángulo del borde si es visible
void Credits::drawBorderRect() {
// Umbral: cualquier valor menor que 1 píxel no se considera visible
constexpr float VISIBLE_THRESHOLD = 1.0F;
if (border_rect_.w < VISIBLE_THRESHOLD || border_rect_.h < VISIBLE_THRESHOLD) {
return; // no dibujar
}
const Color COLOR = color_.LIGHTEN();
SDL_Renderer* rdr = Screen::get()->getRenderer();
SDL_SetRenderDrawColor(rdr, COLOR.r, COLOR.g, COLOR.b, 0xFF);
// Convertir a enteros de forma conservadora para evitar líneas de 1px por redondeo extraño
SDL_Rect r;
r.x = static_cast<int>(std::floor(border_rect_.x + 0.5F));
r.y = static_cast<int>(std::floor(border_rect_.y + 0.5F));
r.w = static_cast<int>(std::max(0.0F, std::floor(border_rect_.w + 0.5F)));
r.h = static_cast<int>(std::max(0.0F, std::floor(border_rect_.h + 0.5F)));
if (r.w > 0 && r.h > 0) {
SDL_RenderRect(Screen::get()->getRenderer(), &border_rect_);
}
}

View File

@@ -0,0 +1,171 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FRect, Uint32, SDL_Texture, Uint64
#include <memory> // Para unique_ptr, shared_ptr
#include <vector> // Para vector
#include "color.hpp" // Para Zone, Color
#include "options.hpp" // Para AudioOptions, MusicOptions, audio
#include "param.hpp" // Para Param, ParamGame, param
#include "utils.hpp"
// Declaraciones adelantadas
class BalloonManager;
class Fade;
class Player;
class TiledBG;
class Credits {
public:
// --- Constructor y destructor ---
Credits();
~Credits();
// --- Callbacks para el bucle SDL_MAIN_USE_CALLBACKS ---
void iterate(); // Ejecuta un frame
void handleEvent(const SDL_Event& event); // Procesa un evento
// --- Bucle principal legacy (fallback) ---
void run();
private:
// --- Métodos del bucle principal ---
void update(float delta_time); // Actualización principal de la lógica (time-based)
auto calculateDeltaTime() -> float; // Calcula el deltatime
void initVars(); // Inicializa variables
void startCredits(); // Inicializa mas variables
// --- Constantes de clase (time-based) ---
static constexpr int PLAY_AREA_HEIGHT = 200;
static constexpr float FAST_FORWARD_MULTIPLIER = 6.0F;
static constexpr float BLACK_RECT_INTERVAL_S = 4.0F / 60.0F; // ~0.0667s (cada 4 frames a 60fps)
static constexpr int HORIZONTAL_SPEED = 2;
static constexpr float MAX_TIME_AFTER_LOGO_S = 20.0F;
static constexpr float PRE_FADE_DELAY_S = 8.0F;
// --- Objetos principales ---
std::unique_ptr<BalloonManager> balloon_manager_; // Gestión de globos
std::unique_ptr<TiledBG> tiled_bg_; // Mosaico animado de fondo
std::unique_ptr<Fade> fade_in_; // Fundido de entrada
std::unique_ptr<Fade> fade_out_; // Fundido de salida
std::vector<std::shared_ptr<Player>> players_; // Vector de jugadores
// --- Gestión de texturas ---
SDL_Texture* text_texture_; // Textura con el texto de créditos
SDL_Texture* canvas_; // Textura donde se dibuja todo
// --- Temporización (time-based puro) ---
Uint64 last_time_ = 0; // Último tiempo registrado para deltaTime
float elapsed_time_balloons_ = 0.0F; // Tiempo acumulado para lanzamiento de globos (segundos)
float counter_pre_fade_ = 0.0F; // Tiempo antes de activar fundido final (segundos)
float time_since_logo_positioned_ = 0.0F; // Tiempo desde que el logo llegó a su posición (segundos)
float current_step_ = 0.0F;
int total_steps_ = 1;
bool initialized_ = false;
// --- Guardar estados iniciales para cálculo de pasos ---
int init_top_h_ = 0;
int init_bottom_y_ = 0;
int init_left_w_ = 0;
int init_right_x_ = 0;
// --- Variables de estado ---
bool fading_ = false; // Estado del fade final
bool want_to_pass_ = false; // Jugador quiere saltarse créditos
bool mini_logo_on_position_ = false; // Minilogo en posición final
bool vertical_done_ = false;
bool horizontal_done_ = false;
// --- Diseño y posicionamiento ---
float black_bars_size_ = (param.game.game_area.rect.h - PLAY_AREA_HEIGHT) / 2; // Tamaño de las barras negras
int mini_logo_final_pos_ = 0; // Posición final del minilogo
Color color_; // Color usado para los efectos
// --- Control de audio ---
int initial_volume_ = Options::audio.music.volume; // Volumen inicial
int steps_ = 0; // Pasos para reducir audio
// --- Estado de acumuladores para animaciones ---
struct CreditsState {
float texture_accumulator = 0.0F;
float balloon_accumulator = 0.0F;
float powerball_accumulator = 0.0F;
float black_rect_accumulator = 0.0F;
float r = 255.0F; // UPPER_LIMIT
float g = 0.0F; // LOWER_LIMIT
float b = 0.0F; // LOWER_LIMIT
float step_r = -0.5F;
float step_g = 0.3F;
float step_b = 0.1F;
} credits_state_;
// --- Rectángulos de renderizado ---
// Texto de créditos
SDL_FRect credits_rect_src_ = param.game.game_area.rect;
SDL_FRect credits_rect_dst_ = param.game.game_area.rect;
// Mini logo
SDL_FRect mini_logo_rect_src_ = param.game.game_area.rect;
SDL_FRect mini_logo_rect_dst_ = param.game.game_area.rect;
// Definición del área de juego
SDL_FRect play_area_ = {
.x = param.game.game_area.rect.x,
.y = param.game.game_area.rect.y + black_bars_size_,
.w = param.game.game_area.rect.w,
.h = PLAY_AREA_HEIGHT};
// Barras negras para efecto letterbox
SDL_FRect top_black_rect_ = {
.x = play_area_.x,
.y = param.game.game_area.rect.y,
.w = play_area_.w,
.h = black_bars_size_};
SDL_FRect bottom_black_rect_ = {
.x = play_area_.x,
.y = param.game.game_area.rect.h - black_bars_size_,
.w = play_area_.w,
.h = black_bars_size_};
SDL_FRect left_black_rect_ = {
.x = play_area_.x,
.y = param.game.game_area.center_y - 1,
.w = 0,
.h = 2};
SDL_FRect right_black_rect_ = {
.x = play_area_.x + play_area_.w,
.y = param.game.game_area.center_y - 1,
.w = 0,
.h = 2};
// Borde para la ventana
SDL_FRect border_rect_ = play_area_; // Delimitador de ventana
void render(); // Renderizado de la escena
static void checkEvents(); // Manejo de eventos
void checkInput(); // Procesamiento de entrada
// --- Métodos de renderizado ---
void fillTextTexture(); // Crear textura de texto de créditos
void fillCanvas(); // Renderizar todos los sprites y fondos
void renderPlayers(); // Renderiza los jugadores
void drawBorderRect(); // Renderiza el rectangulo del borde
// --- Métodos de lógica del juego ---
void throwBalloons(float delta_time); // Lanzar globos al escenario (time-based)
void initPlayers(); // Inicializar jugadores
void updateAllFades(float delta_time); // Actualizar estados de fade (time-based)
void cycleColors(float delta_time); // Cambiar colores de fondo
void updatePlayers(float delta_time); // Actualza los jugadores (time-based)
// --- Métodos de interfaz ---
void updateBlackRects(); // Actualizar rectángulos negros (letterbox) (frame-based)
void updateBlackRects(float delta_time); // Actualizar rectángulos negros (letterbox) (time-based)
void updateBorderRect(); // Actualizar rectángulo rojo (borde)
void updateTextureDstRects(); // Actualizar destinos de texturas (frame-based)
void updateTextureDstRects(float delta_time); // Actualizar destinos de texturas (time-based)
// --- Métodos de audio ---
static void setVolume(int amount); // Establecer volumen
void resetVolume() const; // Restablecer volumen
};

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

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,353 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_Event, SDL_Renderer, SDL_Texture, Uint64
#include <list> // Para list
#include <memory> // Para shared_ptr, unique_ptr
#include <string> // Para string
#include <vector> // Para vector
#include "bullet.hpp" // for Bullet
#include "demo.hpp" // for Demo
#include "item.hpp" // for Item (ptr only), ItemType
#include "manage_hiscore_table.hpp" // for HiScoreEntry
#include "options.hpp" // for Settings, settings
#include "player.hpp" // for Player
class Background;
class Balloon;
class BalloonManager;
class BulletManager;
class Fade;
class Input;
class PathSprite;
class PauseManager;
class Scoreboard;
class Screen;
class SmartSprite;
class StageManager;
class Tabe;
class Texture;
struct Path;
namespace Difficulty {
enum class Code;
} // namespace Difficulty
// --- Clase Game: núcleo principal del gameplay ---
//
// Esta clase gestiona toda la lógica del juego durante las partidas activas,
// incluyendo mecánicas de juego, estados, objetos y sistemas de puntuación.
//
// Funcionalidades principales:
// • Gestión de jugadores: soporte para 1 o 2 jugadores simultáneos
// • Sistema de estados: fade-in, entrada, jugando, completado, game-over
// • Mecánicas de juego: globos, balas, ítems, power-ups y efectos especiales
// • Sistema de puntuación: scoreboard y tabla de récords
// • Efectos temporales: tiempo detenido, ayudas automáticas
// • Modo demo: reproducción automática para attract mode
// • Gestión de fases: progresión entre niveles y dificultad
//
// Utiliza un sistema de tiempo basado en milisegundos para garantizar
// comportamiento consistente independientemente del framerate.
class Game {
public:
// --- Constantes ---
static constexpr bool DEMO_OFF = false; // Modo demo desactivado
static constexpr bool DEMO_ON = true; // Modo demo activado
// --- Constructor y destructor ---
Game(Player::Id player_id, int current_stage, bool demo_enabled); // Constructor principal
~Game(); // Destructor
// --- Callbacks para el bucle SDL_MAIN_USE_CALLBACKS ---
void iterate(); // Ejecuta un frame
void handleEvent(const SDL_Event& event); // Procesa un evento
// --- Bucle principal legacy (fallback) ---
void run(); // Ejecuta el bucle principal del juego
private:
using Players = std::vector<std::shared_ptr<Player>>;
// --- Enums ---
enum class State {
FADE_IN, // Transición de entrada
ENTERING_PLAYER, // Jugador entrando
SHOWING_GET_READY_MESSAGE, // Mostrando mensaje de preparado
PLAYING, // Jugando
COMPLETED, // Juego completado
GAME_OVER, // Fin del juego
};
// --- Constantes de tiempo (en segundos) ---
static constexpr float HELP_COUNTER_S = 16.667F; // Contador de ayuda (1000 frames a 60fps → segundos)
static constexpr float GAME_COMPLETED_START_FADE_S = 8.333F; // Inicio del fade al completar (500 frames → segundos)
static constexpr float GAME_COMPLETED_END_S = 11.667F; // Fin del juego completado (700 frames → segundos)
static constexpr float GAME_OVER_DURATION_S = 8.5F;
static constexpr float TIME_STOPPED_DURATION_S = 6.0F;
static constexpr float DEMO_FADE_PRE_DURATION_S = 0.5F;
static constexpr int ITEM_POINTS_1_DISK_ODDS = 10;
static constexpr int ITEM_POINTS_2_GAVINA_ODDS = 6;
static constexpr int ITEM_POINTS_3_PACMAR_ODDS = 3;
static constexpr int ITEM_CLOCK_ODDS = 5;
static constexpr int ITEM_COFFEE_ODDS = 5;
static constexpr int ITEM_POWER_BALL_ODDS = 0;
static constexpr int ITEM_COFFEE_MACHINE_ODDS = 4;
// --- Estructuras ---
struct Helper {
bool need_coffee{false}; // Indica si se necesitan cafes
bool need_coffee_machine{false}; // Indica si se necesita PowerUp
bool need_power_ball{false}; // Indica si se necesita una PowerBall
float counter{HELP_COUNTER_S * 1000}; // Contador para no dar ayudas consecutivas
int item_disk_odds{ITEM_POINTS_1_DISK_ODDS}; // Probabilidad de aparición del objeto
int item_gavina_odds{ITEM_POINTS_2_GAVINA_ODDS}; // Probabilidad de aparición del objeto
int item_pacmar_odds{ITEM_POINTS_3_PACMAR_ODDS}; // Probabilidad de aparición del objeto
int item_clock_odds{ITEM_CLOCK_ODDS}; // Probabilidad de aparición del objeto
int item_coffee_odds{ITEM_COFFEE_ODDS}; // Probabilidad de aparición del objeto
int item_coffee_machine_odds{ITEM_COFFEE_MACHINE_ODDS}; // Probabilidad de aparición del objeto
};
// --- Objetos y punteros ---
SDL_Renderer* renderer_; // El renderizador de la ventana
Screen* screen_; // Objeto encargado de dibujar en pantalla
Input* input_; // Manejador de entrada
Scoreboard* scoreboard_; // Objeto para dibujar el marcador
SDL_Texture* canvas_; // Textura para dibujar la zona de juego
Players players_; // Vector con los jugadores
Players players_draw_list_; // Vector con los jugadores ordenados para ser renderizados
std::list<std::unique_ptr<Item>> items_; // Vector con los items
std::list<std::unique_ptr<SmartSprite>> smart_sprites_; // Vector con los smartsprites
std::list<std::unique_ptr<PathSprite>> path_sprites_; // Vector con los pathsprites
std::vector<std::shared_ptr<Texture>> item_textures_; // Vector con las texturas de los items
std::vector<std::vector<std::shared_ptr<Texture>>> player_textures_; // Vector con todas las texturas de los jugadores
std::vector<std::shared_ptr<Texture>> game_text_textures_; // Vector con las texturas para los sprites con textos
std::vector<std::vector<std::string>> item_animations_; // Vector con las animaciones de los items
std::vector<std::vector<std::string>> player1_animations_; // Vector con las animaciones del jugador 1
std::vector<std::vector<std::string>> player2_animations_; // Vector con las animaciones del jugador 2
std::unique_ptr<PauseManager> pause_manager_; // Objeto para gestionar la pausa
std::unique_ptr<StageManager> stage_manager_; // Objeto para gestionar las fases
std::unique_ptr<BalloonManager> balloon_manager_; // Objeto para gestionar los globos
std::unique_ptr<BulletManager> bullet_manager_; // Objeto para gestionar las balas
std::unique_ptr<Background> background_; // Objeto para dibujar el fondo del juego
std::unique_ptr<Fade> fade_in_; // Objeto para renderizar fades
std::unique_ptr<Fade> fade_out_; // Objeto para renderizar fades
std::unique_ptr<Tabe> tabe_; // Objeto para gestionar el Tabe Volaor
std::vector<Path> paths_; // Vector con los recorridos precalculados almacenados
// --- Variables de estado ---
HiScoreEntry hi_score_ = HiScoreEntry(
Options::settings.hi_score_table[0].name,
Options::settings.hi_score_table[0].score); // Máxima puntuación y nombre de quien la ostenta
Demo demo_; // Variable con todas las variables relacionadas con el modo demo
Difficulty::Code difficulty_ = Options::settings.difficulty; // Dificultad del juego
Helper helper_; // Variable para gestionar las ayudas
Uint64 last_time_ = 0; // Último tiempo registrado para deltaTime
bool coffee_machine_enabled_ = false; // Indica si hay una máquina de café en el terreno de juego
bool hi_score_achieved_ = false; // Indica si se ha superado la puntuación máxima
float difficulty_score_multiplier_ = 1.0F; // Multiplicador de puntos en función de la dificultad
float counter_ = 0.0F; // Contador para el juego
float game_completed_timer_ = 0.0F; // Acumulador de tiempo para el tramo final (milisegundos)
float game_over_timer_ = 0.0F; // Timer para el estado de fin de partida (milisegundos)
float time_stopped_timer_ = 0.0F; // Temporizador para llevar la cuenta del tiempo detenido
float time_stopped_sound_timer_ = 0.0F; // Temporizador para controlar el sonido del tiempo detenido
int menace_ = 0; // Nivel de amenaza actual
int menace_threshold_ = 0; // Umbral del nivel de amenaza. Si el nivel de amenaza cae por debajo del umbral, se generan más globos. Si el umbral aumenta, aumenta el número de globos
State state_ = State::FADE_IN; // Estado
// Estructuras para gestionar flags de eventos basados en tiempo
struct GameOverFlags {
bool music_fade_triggered = false;
bool message_triggered = false;
bool fade_out_triggered = false;
void reset() {
music_fade_triggered = false;
message_triggered = false;
fade_out_triggered = false;
}
} game_over_flags_;
struct GameCompletedFlags {
bool start_celebrations_triggered = false;
bool end_celebrations_triggered = false;
void reset() {
start_celebrations_triggered = false;
end_celebrations_triggered = false;
}
} game_completed_flags_;
struct TimeStoppedFlags {
bool color_flash_sound_played = false;
bool warning_phase_started = false;
void reset() {
color_flash_sound_played = false;
warning_phase_started = false;
}
} time_stopped_flags_;
#ifdef _DEBUG
bool auto_pop_balloons_ = false; // Si es true, incrementa automaticamente los globos explotados
#endif
// --- Ciclo principal del juego ---
void update(float delta_time); // Actualiza la lógica principal del juego
auto calculateDeltaTime() -> float; // Calcula el deltatime
void render(); // Renderiza todos los elementos del juego
void handleEvents(); // Procesa los eventos del sistema en cola
void checkState(); // Verifica y actualiza el estado actual del juego
void setState(State state); // Cambia el estado del juego
void cleanLists(); // Limpia vectores de elementos deshabilitados
// --- Gestión de estados del juego ---
void updateGameStates(float delta_time); // Actualiza todos los estados del juego
void updateGameStateFadeIn(float delta_time); // Gestiona el estado de transición de entrada (time-based)
void updateGameStateEnteringPlayer(float delta_time); // Gestiona el estado de entrada de jugador
void updateGameStateShowingGetReadyMessage(float delta_time); // Gestiona el estado de mensaje "preparado"
void updateGameStatePlaying(float delta_time); // Gestiona el estado de juego activo
void updateGameStateCompleted(float delta_time); // Gestiona el estado de juego completado
void updateGameStateGameOver(float delta_time); // Gestiona las actualizaciones continuas del estado de fin de partida
// --- Gestión de jugadores ---
void initPlayers(Player::Id player_id); // Inicializa los datos de los jugadores
void updatePlayers(float delta_time); // Actualiza las variables y estados de los jugadores
void renderPlayers(); // Renderiza todos los jugadores en pantalla
auto getPlayer(Player::Id id) -> std::shared_ptr<Player>; // Obtiene un jugador por su identificador
static auto getController(Player::Id player_id) -> int; // Obtiene el controlador asignado a un jugador
// --- Estado de jugadores ---
void checkAndUpdatePlayerStatus(int active_player_index, int inactive_player_index); // Actualiza estado entre jugadores
void checkPlayersStatusPlaying(); // Verifica el estado de juego de todos los jugadores
auto allPlayersAreWaitingOrGameOver() -> bool; // Verifica si todos esperan o han perdido
auto allPlayersAreGameOver() -> bool; // Verifica si todos los jugadores han perdido
auto allPlayersAreNotPlaying() -> bool; // Verifica si ningún jugador está activo
// --- Colisiones de jugadores ---
void handlePlayerCollision(std::shared_ptr<Player>& player, std::shared_ptr<Balloon>& balloon); // Procesa colisión de jugador con globo
auto checkPlayerBalloonCollision(std::shared_ptr<Player>& player) -> std::shared_ptr<Balloon>; // Detecta colisión jugador-globo
void checkPlayerItemCollision(std::shared_ptr<Player>& player); // Detecta colisión jugador-ítem
// --- Sistema de entrada (input) ---
void checkInput(); // Gestiona toda la entrada durante el juego
void checkPauseInput(); // Verifica solicitudes de pausa de controladores
// --- Entrada de jugadores normales ---
void handlePlayersInput(); // Gestiona entrada de todos los jugadores
void handleNormalPlayerInput(const std::shared_ptr<Player>& player); // Procesa entrada de un jugador específico
void handleFireInput(const std::shared_ptr<Player>& player, Bullet::Type type); // Gestiona disparo de jugador
void handleFireInputs(const std::shared_ptr<Player>& player, bool autofire); // Procesa disparos automáticos
void handlePlayerContinueInput(const std::shared_ptr<Player>& player); // Permite continuar al jugador
void handlePlayerWaitingInput(const std::shared_ptr<Player>& player); // Permite (re)entrar al jugador
void handleNameInput(const std::shared_ptr<Player>& player); // Gestiona entrada de nombre del jugador
// --- Entrada en modo demo ---
void demoHandleInput(); // Gestiona entrada durante el modo demostración
void demoHandlePassInput(); // Permite saltar la demostración
void demoHandlePlayerInput(const std::shared_ptr<Player>& player, int index); // Procesa entrada de jugador en demo
// --- Colisiones específicas de balas ---
auto checkBulletTabeCollision(const std::shared_ptr<Bullet>& bullet) -> bool; // Detecta colisión bala-Tabe
auto checkBulletBalloonCollision(const std::shared_ptr<Bullet>& bullet) -> bool; // Detecta colisión bala-globo
void processBalloonHit(const std::shared_ptr<Bullet>& bullet, const std::shared_ptr<Balloon>& balloon); // Procesa impacto en globo
// --- Sistema de ítems y power-ups ---
void updateItems(float delta_time); // Actualiza posición y estado de todos los ítems
void renderItems(); // Renderiza todos los ítems activos
auto dropItem() -> ItemType; // Determina aleatoriamente qué ítem soltar
void createItem(ItemType type, float x, float y); // Crea un nuevo ítem en posición específica
void freeItems(); // Libera memoria del vector de ítems
void destroyAllItems(); // Elimina todos los ítems activos de la pantalla
// --- ítems especiales ---
void enableTimeStopItem(); // Activa el efecto de detener el tiempo
void disableTimeStopItem(); // Desactiva el efecto de detener el tiempo
void updateTimeStopped(float delta_time); // Actualiza el estado del tiempo detenido
void handleGameCompletedEvents(); // Maneja eventos del juego completado
void handleGameOverEvents(); // Maneja eventos discretos basados en tiempo durante game over
void throwCoffee(int x, int y); // Crea efecto de café arrojado al ser golpeado
// --- Gestión de caída de ítems ---
void handleItemDrop(const std::shared_ptr<Balloon>& balloon, const std::shared_ptr<Player>& player); // Gestiona caída de ítem desde globo
// --- Sprites inteligentes (smartsprites) ---
void updateSmartSprites(float delta_time); // Actualiza todos los sprites con lógica propia (time-based)
void renderSmartSprites(); // Renderiza todos los sprites inteligentes
void freeSmartSprites(); // Libera memoria de sprites inteligentes
// --- Sprites por ruta (pathsprites) ---
void updatePathSprites(float delta_time); // Actualiza sprites que siguen rutas predefinidas
void renderPathSprites(); // Renderiza sprites animados por ruta
void freePathSprites(); // Libera memoria de sprites por ruta
void initPaths(); // Inicializa rutas predefinidas para animaciones
// --- Creación de sprites especiales ---
void createItemText(int x, const std::shared_ptr<Texture>& texture); // Crea texto animado para ítems
void createMessage(const std::vector<Path>& paths, const std::shared_ptr<Texture>& texture); // Crea mensaje con animación por ruta
// --- Sistema de globos y enemigos ---
void handleBalloonDestruction(const std::shared_ptr<Balloon>& balloon, const std::shared_ptr<Player>& player); // Procesa destrucción de globo
void handleTabeHitEffects(); // Gestiona efectos al golpear a Tabe
void checkAndUpdateBalloonSpeed(); // Ajusta velocidad de globos según progreso
// --- Gestión de fases y progresión ---
void updateStage(); // Verifica y actualiza cambio de fase
void initDifficultyVars(); // Inicializa variables de dificultad
// --- Sistema de amenaza ---
void updateMenace(); // Gestiona el nivel de amenaza del juego
void setMenace(); // Calcula y establece amenaza según globos activos
// --- Puntuación y marcador ---
void updateHiScore(); // Actualiza el récord máximo si es necesario
void updateScoreboard(float delta_time); // Actualiza la visualización del marcador
void updateHiScoreName(); // Pone en el marcador el nombre del primer jugador de la tabla
void initScoreboard(); // Inicializa el sistema de puntuación
// --- Modo demostración ---
void initDemo(Player::Id player_id); // Inicializa variables para el modo demostración
void updateDemo(float delta_time); // Actualiza lógica específica del modo demo
// --- Recursos y renderizado ---
void setResources(); // Asigna texturas y animaciones a los objetos
void updateBackground(float delta_time); // Actualiza elementos del fondo (time-based)
void fillCanvas(); // Renderiza elementos del área de juego en su textura
void updateHelper(); // Actualiza variables auxiliares de renderizado
// --- Sistema de audio ---
static void playMusic(const std::string& music_file, int loop = -1); // Reproduce la música de fondo
void stopMusic() const; // Detiene la reproducción de música
static void pauseMusic(); // Pausa la música
static void resumeMusic(); // Retoma la música que eestaba pausada
void playSound(const std::string& name) const; // Reproduce un efecto de sonido específico
// --- Gestion y dibujado de jugadores en z-order ---
static void buildPlayerDrawList(const Players& elements, Players& draw_list); // Construye el draw_list a partir del vector principal
static void updatePlayerDrawList(const Players& elements, Players& draw_list); // Actualiza draw_list tras cambios en los z_order
static void renderPlayerDrawList(const Players& draw_list); // Dibuja en el orden definido
static auto findPlayerIndex(const Players& elems, const std::shared_ptr<Player>& who) -> size_t;
static void sendPlayerToBack(Players& elements, const std::shared_ptr<Player>& who, Players& draw_list); // Envia al jugador al fondo de la pantalla
static void bringPlayerToFront(Players& elements, const std::shared_ptr<Player>& who, Players& draw_list); // Envia al jugador al frente de la pantalla
// --- Varios ---
void onPauseStateChanged(bool is_paused);
// SISTEMA DE GRABACIÓN (CONDICIONAL)
#ifdef RECORDING
void updateRecording(float deltaTime); // Actualiza variables durante modo de grabación
#endif
// --- Depuración (solo en modo DEBUG) ---
#ifdef _DEBUG
void handleDebugEvents(const SDL_Event& event); // Comprueba los eventos en el modo DEBUG
#endif
};

View File

@@ -0,0 +1,399 @@
#include "hiscore_table.hpp"
#include <SDL3/SDL.h> // Para SDL_GetTicks, SDL_SetRenderTarget
#include <algorithm> // Para max
#include <cstdlib> // Para rand, size_t
#include <functional> // Para function
#include <utility> // Para std::cmp_less
#include <vector> // Para vector
#include "audio.hpp" // Para Audio
#include "background.hpp" // Para Background
#include "color.hpp" // Para Color, easeOutQuint, Colors::NO_COLOR_MOD
#include "fade.hpp" // Para Fade, FadeMode, FadeType
#include "global_events.hpp" // Para check
#include "global_inputs.hpp" // Para check
#include "input.hpp" // Para Input
#include "lang.hpp" // Para getText
#include "manage_hiscore_table.hpp" // Para HiScoreEntry
#include "options.hpp" // Para SettingsOptions, settings
#include "param.hpp" // Para Param, param, ParamGame, ParamFade
#include "path_sprite.hpp" // Para PathSprite, Path, PathType
#include "resource.hpp" // Para Resource
#include "screen.hpp" // Para Screen
#include "section.hpp" // Para Name, name, Options, options
#include "sprite.hpp" // Para Sprite
#include "text.hpp" // Para Text, Text::SHADOW, Text::COLOR
#include "texture.hpp" // Para Texture
#include "utils.hpp"
// Constructor
HiScoreTable::HiScoreTable()
: renderer_(Screen::get()->getRenderer()),
backbuffer_(SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height)),
fade_(std::make_unique<Fade>()),
background_(std::make_unique<Background>()),
view_area_(SDL_FRect{.x = 0, .y = 0, .w = param.game.width, .h = param.game.height}),
fade_mode_(Fade::Mode::IN),
background_fade_color_(Color(0, 0, 0)) {
// Inicializa el resto
Section::name = Section::Name::HI_SCORE_TABLE;
SDL_SetTextureBlendMode(backbuffer_, SDL_BLENDMODE_BLEND);
initFade();
initBackground();
iniEntryColors();
createSprites();
// Inicializa el timer de delta time y arranca la música
last_time_ = SDL_GetTicks();
Audio::get()->playMusic("title.ogg");
}
// Destructor
HiScoreTable::~HiScoreTable() {
SDL_DestroyTexture(backbuffer_);
Options::settings.clearLastHiScoreEntries();
}
// Actualiza las variables
void HiScoreTable::update(float delta_time) {
elapsed_time_ += delta_time; // Incrementa el tiempo transcurrido
static auto* const SCREEN = Screen::get();
SCREEN->update(delta_time); // Actualiza el objeto screen
Audio::update(); // Actualiza el objeto audio
updateSprites(delta_time); // Actualiza las posiciones de los sprites de texto
background_->update(delta_time); // Actualiza el fondo
updateFade(delta_time); // Gestiona el fade
updateCounter(); // Gestiona el contador y sus eventos
fillTexture(); // Dibuja los sprites en la textura
}
// Pinta en pantalla
void HiScoreTable::render() {
static auto* const SCREEN = Screen::get();
SCREEN->start(); // Prepara para empezar a dibujar en la textura de juego
SCREEN->clean(); // Limpia la pantalla
background_->render(); // Pinta el fondo
float scroll_offset = elapsed_time_ * SCROLL_SPEED_PPS; // Calcula el desplazamiento del scroll usando velocidad en pixels/segundo
view_area_.y = std::round(std::max(0.0F, (param.game.height + 100.0F) - scroll_offset)); // Establece la ventana del backbuffer (redondeado para evitar deformaciones)
SDL_RenderTexture(renderer_, backbuffer_, nullptr, &view_area_); // Copia el backbuffer al renderizador
fade_->render(); // Renderiza el fade
SCREEN->render(); // Vuelca el contenido del renderizador en pantalla
}
// Dibuja los sprites en la textura
void HiScoreTable::fillTexture() {
// Pinta en el backbuffer el texto y los sprites
auto* temp = SDL_GetRenderTarget(renderer_);
SDL_SetRenderTarget(renderer_, backbuffer_);
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0);
SDL_RenderClear(renderer_);
// Escribe el texto: Mejores puntuaciones
header_->render();
// Escribe los nombres de la tabla de puntuaciones
for (auto const& entry : entry_names_) {
entry->render();
}
// Cambia el destino de renderizado
SDL_SetRenderTarget(renderer_, temp);
}
// Comprueba los eventos
void HiScoreTable::checkEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event);
}
}
// Comprueba las entradas
void HiScoreTable::checkInput() {
Input::get()->update();
GlobalInputs::check();
}
// Calcula el tiempo transcurrido desde el último frame
auto HiScoreTable::calculateDeltaTime() -> float {
const Uint64 CURRENT_TIME = SDL_GetTicks();
const float DELTA_TIME = static_cast<float>(CURRENT_TIME - last_time_) / 1000.0F; // Convertir ms a segundos
last_time_ = CURRENT_TIME;
return DELTA_TIME;
}
// Avanza un frame (llamado desde Director::iterate)
void HiScoreTable::iterate() {
const float DELTA_TIME = calculateDeltaTime();
checkInput();
update(DELTA_TIME);
render();
}
// Procesa un evento (llamado desde Director::handleEvent)
void HiScoreTable::handleEvent(const SDL_Event& /*event*/) {
// Eventos globales ya gestionados por Director::handleEvent
}
// Bucle para la pantalla de puntuaciones (fallback legacy)
void HiScoreTable::run() {
last_time_ = SDL_GetTicks();
Audio::get()->playMusic("title.ogg");
while (Section::name == Section::Name::HI_SCORE_TABLE) {
const float DELTA_TIME = calculateDeltaTime();
checkInput();
update(DELTA_TIME);
checkEvents(); // Tiene que ir antes del render
render();
}
}
// Gestiona el fade
void HiScoreTable::updateFade(float delta_time) {
fade_->update(delta_time);
if (fade_->hasEnded() && fade_mode_ == Fade::Mode::IN) {
(*fade_).reset();
fade_mode_ = Fade::Mode::OUT;
fade_->setMode(fade_mode_);
}
if (fade_->hasEnded() && fade_mode_ == Fade::Mode::OUT) {
Section::name = (Section::options == Section::Options::HI_SCORE_AFTER_PLAYING)
? Section::Name::TITLE
: Section::Name::INSTRUCTIONS;
Section::options = Section::Options::NONE;
}
}
// Convierte un entero a un string con separadores de miles
auto HiScoreTable::format(int number) -> std::string {
const std::string SEPARATOR = ".";
const std::string SCORE = std::to_string(number);
auto index = static_cast<int>(SCORE.size()) - 1;
std::string result;
auto i = 0;
while (index >= 0) {
result = SCORE.at(index) + result;
index--;
i++;
if (i == 3) {
i = 0;
result = SEPARATOR + result;
}
}
return result;
}
// Crea los sprites con los textos
void HiScoreTable::createSprites() {
auto header_text = Resource::get()->getText("04b_25_grey");
auto entry_text = Resource::get()->getText("smb2");
// Obtiene el tamaño de la textura
float backbuffer_width;
float backbuffer_height;
SDL_GetTextureSize(backbuffer_, &backbuffer_width, &backbuffer_height);
constexpr int ENTRY_LENGTH = 22;
constexpr int MAX_NAMES = 10;
const int SPACE_BETWEEN_HEADER = entry_text->getCharacterSize() * 4;
const int SPACE_BETWEEN_LINES = entry_text->getCharacterSize() * 2;
const int SIZE = SPACE_BETWEEN_HEADER + (SPACE_BETWEEN_LINES * (MAX_NAMES - 1)) + entry_text->getCharacterSize();
const int FIRST_LINE = (param.game.height - SIZE) / 2;
// Crea el sprite para el texto de cabecera
header_ = std::make_unique<Sprite>(header_text->writeDXToTexture(Text::COLOR, Lang::getText("[HIGHSCORE_TABLE] CAPTION"), -2, background_fade_color_.INVERSE().LIGHTEN(25)));
header_->setPosition(param.game.game_area.center_x - (header_->getWidth() / 2), FIRST_LINE);
// Crea los sprites para las entradas en la tabla de puntuaciones
const int ANIMATION = rand() % 4;
const std::string SAMPLE_LINE(ENTRY_LENGTH + 3, ' ');
auto sample_entry = std::make_unique<Sprite>(entry_text->writeDXToTexture(Text::SHADOW, SAMPLE_LINE, 1, Colors::NO_COLOR_MOD, 1, Colors::SHADOW_TEXT));
const auto ENTRY_WIDTH = sample_entry->getWidth();
for (int i = 0; i < MAX_NAMES; ++i) {
const auto TABLE_POSITION = format(i + 1) + ". ";
const auto SCORE = format(Options::settings.hi_score_table.at(i).score);
const auto NUM_DOTS = ENTRY_LENGTH - Options::settings.hi_score_table.at(i).name.size() - SCORE.size();
const auto* const ONE_CC = Options::settings.hi_score_table.at(i).one_credit_complete ? " }" : "";
std::string dots;
for (int j = 0; std::cmp_less(j, NUM_DOTS); ++j) {
dots = dots + ".";
}
const auto LINE = TABLE_POSITION + Options::settings.hi_score_table.at(i).name + dots + SCORE + ONE_CC;
entry_names_.emplace_back(std::make_shared<PathSprite>(entry_text->writeDXToTexture(Text::SHADOW, LINE, 1, Colors::NO_COLOR_MOD, 1, Colors::SHADOW_TEXT)));
const int DEFAULT_POS_X = (backbuffer_width - ENTRY_WIDTH) / 2;
const int POS_X = (i < 9) ? DEFAULT_POS_X : DEFAULT_POS_X - entry_text->getCharacterSize();
const int POS_Y = (i * SPACE_BETWEEN_LINES) + FIRST_LINE + SPACE_BETWEEN_HEADER;
switch (ANIMATION) {
case 0: // Ambos lados alternativamente
{
if (i % 2 == 0) {
entry_names_.back()->addPath(-entry_names_.back()->getWidth(), POS_X, PathType::HORIZONTAL, POS_Y, ANIM_DURATION_S, easeOutQuint);
entry_names_.back()->setPosition(-entry_names_.back()->getWidth(), 0);
} else {
entry_names_.back()->addPath(backbuffer_width, POS_X, PathType::HORIZONTAL, POS_Y, ANIM_DURATION_S, easeOutQuint);
entry_names_.back()->setPosition(backbuffer_width, 0);
}
break;
}
case 1: // Entran por la izquierda
{
entry_names_.back()->addPath(-entry_names_.back()->getWidth(), POS_X, PathType::HORIZONTAL, POS_Y, ANIM_DURATION_S, easeOutQuint);
entry_names_.back()->setPosition(-entry_names_.back()->getWidth(), 0);
break;
}
case 2: // Entran por la derecha
{
entry_names_.back()->addPath(backbuffer_width, POS_X, PathType::HORIZONTAL, POS_Y, ANIM_DURATION_S, easeOutQuint);
entry_names_.back()->setPosition(backbuffer_width, 0);
break;
}
case 3: // Entran desde la parte inferior
{
entry_names_.back()->addPath(backbuffer_height, POS_Y, PathType::VERTICAL, POS_X, ANIM_DURATION_S, easeOutQuint);
entry_names_.back()->setPosition(0, backbuffer_height);
}
default:
break;
}
}
}
// Actualiza las posiciones de los sprites de texto
void HiScoreTable::updateSprites(float delta_time) {
if (elapsed_time_ >= INIT_DELAY_S) {
const float ELAPSED_SINCE_INIT = elapsed_time_ - INIT_DELAY_S;
int index = static_cast<int>(ELAPSED_SINCE_INIT / ENTRY_DELAY_S);
if (std::cmp_less(index, entry_names_.size()) && index >= 0) {
// Verificar si este índice debe activarse ahora
float expected_time = index * ENTRY_DELAY_S;
if (ELAPSED_SINCE_INIT >= expected_time && ELAPSED_SINCE_INIT < expected_time + delta_time) {
entry_names_.at(index)->enable();
}
}
}
for (auto const& entry : entry_names_) {
entry->update(delta_time);
}
glowEntryNames();
}
// Inicializa el fade
void HiScoreTable::initFade() {
fade_->setColor(param.fade.color);
fade_->setType(Fade::Type::RANDOM_SQUARE2);
fade_->setPostDuration(param.fade.post_duration_ms);
fade_->setMode(fade_mode_);
fade_->activate();
}
// Inicializa el fondo
void HiScoreTable::initBackground() {
background_->setManualMode(true);
background_->setPos(param.game.game_area.rect);
background_->setCloudsSpeed(CLOUDS_SPEED);
const int LUCKY = rand() % 3;
switch (LUCKY) {
case 0: // Fondo verde
{
background_->setGradientNumber(2);
background_->setTransition(0.0F);
background_->setSunProgression(1.0F);
background_->setMoonProgression(0.0F);
background_fade_color_ = Colors::GREEN_SKY;
break;
}
case 1: // Fondo naranja
{
background_->setGradientNumber(1);
background_->setTransition(0.0F);
background_->setSunProgression(0.65F);
background_->setMoonProgression(0.0F);
background_fade_color_ = Colors::PINK_SKY;
break;
}
case 2: // Fondo azul
{
background_->setGradientNumber(0);
background_->setTransition(0.0F);
background_->setSunProgression(0.0F);
background_->setMoonProgression(0.0F);
background_fade_color_ = Colors::BLUE_SKY;
break;
}
default:
break;
}
}
// Obtiene un color del vector de colores de entradas
auto HiScoreTable::getEntryColor(int counter) -> Color {
int cycle_length = (entry_colors_.size() * 2) - 2;
size_t n = counter % cycle_length;
size_t index;
if (n < entry_colors_.size()) {
index = n; // Avanza: 0,1,2,3
} else {
index = (2 * (entry_colors_.size() - 1)) - n; // Retrocede: 2,1
}
return entry_colors_[index];
}
// Inicializa los colores de las entradas
void HiScoreTable::iniEntryColors() {
entry_colors_.clear();
entry_colors_.emplace_back(background_fade_color_.INVERSE().LIGHTEN(75));
entry_colors_.emplace_back(background_fade_color_.INVERSE().LIGHTEN(50));
entry_colors_.emplace_back(background_fade_color_.INVERSE().LIGHTEN(25));
entry_colors_.emplace_back(background_fade_color_.INVERSE());
}
// Hace brillar los nombres de la tabla de records
void HiScoreTable::glowEntryNames() {
int color_counter = static_cast<int>(elapsed_time_ * 60.0F / 5.0F); // Convertir tiempo a equivalente frame
const Color ENTRY_COLOR = getEntryColor(color_counter);
for (const auto& entry_index : Options::settings.glowing_entries) {
if (entry_index != -1) {
entry_names_.at(entry_index)->getTexture()->setColor(ENTRY_COLOR);
}
}
}
// Gestiona el contador
void HiScoreTable::updateCounter() {
if (elapsed_time_ >= BACKGROUND_CHANGE_S && !hiscore_flags_.background_changed) {
background_->setColor(background_fade_color_.DARKEN());
background_->setAlpha(96);
hiscore_flags_.background_changed = true;
}
if (elapsed_time_ >= COUNTER_END_S && !hiscore_flags_.fade_activated) {
fade_->activate();
hiscore_flags_.fade_activated = true;
}
}

View File

@@ -0,0 +1,93 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FRect, SDL_Renderer, SDL_Texture, Uint64
#include <memory> // Para unique_ptr, shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include "color.hpp" // for Color
#include "fade.hpp" // for Fade
class Background;
class PathSprite;
class Sprite;
struct Path;
// --- Clase HiScoreTable: muestra la tabla de puntuaciones más altas ---
// Esta clase gestiona un estado del programa. Se encarga de mostrar la tabla con las puntuaciones
// más altas. Para ello utiliza un objeto que se encarga de pintar el fondo y una textura
// sobre la que escribe las puntuaciones. Esta textura se recorre modificando la ventana de vista
// para dar el efecto de que la textura se mueve sobre la pantalla.
// Para mejorar la legibilidad de los textos, el objeto que dibuja el fondo es capaz de modificar
// su atenuación.
class HiScoreTable {
public:
// --- Constructor y destructor ---
HiScoreTable();
~HiScoreTable();
// --- Callbacks para el bucle SDL_MAIN_USE_CALLBACKS ---
void iterate(); // Ejecuta un frame
void handleEvent(const SDL_Event& event); // Procesa un evento
// --- Bucle principal legacy (fallback) ---
void run();
private:
// --- Constantes (en segundos) ---
static constexpr float COUNTER_END_S = 800.0F / 60.0F; // Tiempo final (≈13.33s)
static constexpr float INIT_DELAY_S = 190.0F / 60.0F; // Retraso inicial (≈3.17s)
static constexpr float ENTRY_DELAY_S = 16.0F / 60.0F; // Retraso entre entradas (≈0.27s)
static constexpr float BACKGROUND_CHANGE_S = 150.0F / 60.0F; // Tiempo cambio fondo (≈2.5s)
static constexpr float ANIM_DURATION_S = 80.0F / 60.0F; // Duración animación (≈1.33s)
static constexpr float CLOUDS_SPEED = -6.0F; // Velocidad nubes (pixels/s)
static constexpr float SCROLL_SPEED_PPS = 60.0F; // Velocidad de scroll (60 pixels por segundo)
// --- Objetos y punteros ---
SDL_Renderer* renderer_; // El renderizador de la ventana
SDL_Texture* backbuffer_; // Textura para usar como backbuffer
std::unique_ptr<Fade> fade_; // Objeto para renderizar fades
std::unique_ptr<Background> background_; // Objeto para dibujar el fondo del juego
std::unique_ptr<Sprite> header_; // Sprite con la cabecera del texto
std::vector<std::shared_ptr<PathSprite>> entry_names_; // Lista con los sprites de cada uno de los nombres de la tabla de records
std::vector<Path> paths_; // Vector con los recorridos precalculados
// --- Variables ---
float elapsed_time_ = 0.0F; // Tiempo transcurrido (segundos)
Uint64 last_time_ = 0; // Último timestamp para calcular delta-time
SDL_FRect view_area_; // Parte de la textura que se muestra en pantalla
Fade::Mode fade_mode_; // Modo de fade a utilizar
Color background_fade_color_; // Color de atenuación del fondo
std::vector<Color> entry_colors_; // Colores para destacar las entradas en la tabla
// --- Flags para eventos basados en tiempo ---
struct HiScoreFlags {
bool background_changed = false;
bool fade_activated = false;
void reset() {
background_changed = false;
fade_activated = false;
}
} hiscore_flags_;
// --- Métodos internos ---
void update(float delta_time); // Actualiza las variables
void render(); // Pinta en pantalla
static void checkEvents(); // Comprueba los eventos
static void checkInput(); // Comprueba las entradas
static auto format(int number) -> std::string; // Convierte un entero a un string con separadores de miles
void fillTexture(); // Dibuja los sprites en la textura
void updateFade(float delta_time); // Gestiona el fade
void createSprites(); // Crea los sprites con los textos
void updateSprites(float delta_time); // Actualiza las posiciones de los sprites de texto
void initFade(); // Inicializa el fade
void initBackground(); // Inicializa el fondo
auto getEntryColor(int counter) -> Color; // Obtiene un color del vector de colores de entradas
void iniEntryColors(); // Inicializa los colores de las entradas
void glowEntryNames(); // Hace brillar los nombres de la tabla de records
void updateCounter(); // Gestiona el contador
auto calculateDeltaTime() -> float; // Calcula el tiempo transcurrido desde el último frame
};

View File

@@ -0,0 +1,387 @@
#include "instructions.hpp"
#include <SDL3/SDL.h> // Para SDL_GetTicks, SDL_SetRenderTarget, SDL_Re...
#include <algorithm> // Para max
#include <array> // Para array
#include <string> // Para basic_string, string
#include <utility> // Para move
#include <vector> // Para vector
#include "audio.hpp" // Para Audio
#include "color.hpp" // Para Color, Colors::SHADOW_TEXT, Zone, NO_TEXT_C...
#include "fade.hpp" // Para Fade, FadeMode, FadeType
#include "global_events.hpp" // Para check
#include "global_inputs.hpp" // Para check
#include "input.hpp" // Para Input
#include "item.hpp" // Para Item
#include "lang.hpp" // Para getText
#include "param.hpp" // Para Param, param, ParamGame, ParamFade, Param...
#include "resource.hpp" // Para Resource
#include "screen.hpp" // Para Screen
#include "section.hpp" // Para Name, name, Options, options
#include "sprite.hpp" // Para Sprite
#include "text.hpp" // Para Text, Text::CENTER, Text::COLOR, Text::SHADOW
#include "tiled_bg.hpp" // Para TiledBG, TiledBGMode
#include "utils.hpp"
// Constructor
Instructions::Instructions()
: renderer_(Screen::get()->getRenderer()),
texture_(SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height)),
backbuffer_(SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height)),
text_(Resource::get()->getText("smb2")),
tiled_bg_(std::make_unique<TiledBG>(param.game.game_area.rect, TiledBGMode::STATIC)),
fade_(std::make_unique<Fade>()) {
// Configura las texturas
SDL_SetTextureBlendMode(backbuffer_, SDL_BLENDMODE_BLEND);
SDL_SetTextureBlendMode(texture_, SDL_BLENDMODE_BLEND);
// Inicializa variables
Section::name = Section::Name::INSTRUCTIONS;
view_ = param.game.game_area.rect;
// Inicializa objetos
tiled_bg_->setColor(param.title.bg_color);
fade_->setColor(param.fade.color);
fade_->setType(Fade::Type::FULLSCREEN);
fade_->setPostDuration(param.fade.post_duration_ms);
fade_->setMode(Fade::Mode::IN);
fade_->activate();
// Inicializa las líneas con un retraso progresivo
lines_ = initializeLines(256, LINE_START_DELAY_S);
// Rellena la textura de texto
fillTexture();
// Inicializa los sprites de los items
iniSprites();
// Inicializa el timer de delta time y arranca la música
last_time_ = SDL_GetTicks();
Audio::get()->playMusic("title.ogg");
}
// Destructor
Instructions::~Instructions() {
item_textures_.clear();
sprites_.clear();
SDL_DestroyTexture(backbuffer_);
SDL_DestroyTexture(texture_);
}
// Inicializa los sprites de los items
void Instructions::iniSprites() {
// Inicializa las texturas
item_textures_.emplace_back(Resource::get()->getTexture("item_points1_disk.png"));
item_textures_.emplace_back(Resource::get()->getTexture("item_points2_gavina.png"));
item_textures_.emplace_back(Resource::get()->getTexture("item_points3_pacmar.png"));
item_textures_.emplace_back(Resource::get()->getTexture("item_clock.png"));
item_textures_.emplace_back(Resource::get()->getTexture("item_coffee.png"));
// Inicializa los sprites
for (int i = 0; std::cmp_less(i, item_textures_.size()); ++i) {
auto sprite = std::make_unique<Sprite>(item_textures_[i], 0, 0, Item::WIDTH, Item::HEIGHT);
sprite->setPosition((SDL_FPoint){.x = sprite_pos_.x, .y = sprite_pos_.y + ((Item::HEIGHT + item_space_) * i)});
sprites_.push_back(std::move(sprite));
}
}
// Actualiza los sprites
void Instructions::updateSprites() {
SDL_FRect src_rect = {.x = 0, .y = 0, .w = Item::WIDTH, .h = Item::HEIGHT};
// Disquito (desplazamiento 12/60 = 0.2s)
src_rect.y = Item::HEIGHT * (static_cast<int>((elapsed_time_ + 0.2F) / SPRITE_ANIMATION_CYCLE_S) % 2);
sprites_[0]->setSpriteClip(src_rect);
// Gavina (desplazamiento 9/60 = 0.15s)
src_rect.y = Item::HEIGHT * (static_cast<int>((elapsed_time_ + 0.15F) / SPRITE_ANIMATION_CYCLE_S) % 2);
sprites_[1]->setSpriteClip(src_rect);
// Pacmar (desplazamiento 6/60 = 0.1s)
src_rect.y = Item::HEIGHT * (static_cast<int>((elapsed_time_ + 0.1F) / SPRITE_ANIMATION_CYCLE_S) % 2);
sprites_[2]->setSpriteClip(src_rect);
// Time Stopper (desplazamiento 3/60 = 0.05s)
src_rect.y = Item::HEIGHT * (static_cast<int>((elapsed_time_ + 0.05F) / SPRITE_ANIMATION_CYCLE_S) % 2);
sprites_[3]->setSpriteClip(src_rect);
// Coffee (sin desplazamiento)
src_rect.y = Item::HEIGHT * (static_cast<int>(elapsed_time_ / SPRITE_ANIMATION_CYCLE_S) % 2);
sprites_[4]->setSpriteClip(src_rect);
}
// Rellena la textura de texto
void Instructions::fillTexture() {
const int X_OFFSET = Item::WIDTH + 8;
// Modifica el renderizador para pintar en la textura
auto* temp = SDL_GetRenderTarget(renderer_);
SDL_SetRenderTarget(renderer_, texture_);
// Limpia la textura
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0);
SDL_RenderClear(renderer_);
// Constantes
constexpr int NUM_LINES = 4;
constexpr int NUM_ITEM_LINES = 4;
constexpr int NUM_POST_HEADERS = 2;
constexpr int NUM_PRE_HEADERS = 1;
constexpr int SPACE_POST_HEADER = 20;
constexpr int SPACE_PRE_HEADER = 28;
const int SPACE_BETWEEN_LINES = text_->getCharacterSize() * 1.5F;
const int SPACE_BETWEEN_ITEM_LINES = Item::HEIGHT + item_space_;
const int SPACE_NEW_PARAGRAPH = SPACE_BETWEEN_LINES * 0.5F;
const int SIZE = (NUM_LINES * SPACE_BETWEEN_LINES) + (NUM_ITEM_LINES * SPACE_BETWEEN_ITEM_LINES) + (NUM_POST_HEADERS * SPACE_POST_HEADER) + (NUM_PRE_HEADERS * SPACE_PRE_HEADER) + SPACE_NEW_PARAGRAPH;
const int FIRST_LINE = (param.game.height - SIZE) / 2;
// Calcula cual es el texto más largo de las descripciones de los items
int length = 0;
const std::array<std::string, 5> ITEM_DESCRIPTIONS = {
Lang::getText("[INSTRUCTIONS] 07"),
Lang::getText("[INSTRUCTIONS] 08"),
Lang::getText("[INSTRUCTIONS] 09"),
Lang::getText("[INSTRUCTIONS] 10"),
Lang::getText("[INSTRUCTIONS] 11")};
for (const auto& desc : ITEM_DESCRIPTIONS) {
const int L = text_->length(desc);
length = L > length ? L : length;
}
const int ANCHOR_ITEM = (param.game.width - (length + X_OFFSET)) / 2;
auto caption_style = Text::Style(Text::CENTER | Text::COLOR | Text::SHADOW, Colors::ORANGE_TEXT, Colors::SHADOW_TEXT);
auto text_style = Text::Style(Text::CENTER | Text::COLOR | Text::SHADOW, Colors::NO_COLOR_MOD, Colors::SHADOW_TEXT);
// Escribe el texto de las instrucciones
text_->writeStyle(param.game.game_area.center_x, FIRST_LINE, Lang::getText("[INSTRUCTIONS] 01"), caption_style);
const int ANCHOR1 = FIRST_LINE + SPACE_POST_HEADER;
text_->writeStyle(param.game.game_area.center_x, ANCHOR1 + (SPACE_BETWEEN_LINES * 0), Lang::getText("[INSTRUCTIONS] 02"), text_style);
text_->writeStyle(param.game.game_area.center_x, ANCHOR1 + (SPACE_BETWEEN_LINES * 1), Lang::getText("[INSTRUCTIONS] 03"), text_style);
text_->writeStyle(param.game.game_area.center_x, ANCHOR1 + SPACE_NEW_PARAGRAPH + (SPACE_BETWEEN_LINES * 2), Lang::getText("[INSTRUCTIONS] 04"), text_style);
text_->writeStyle(param.game.game_area.center_x, ANCHOR1 + SPACE_NEW_PARAGRAPH + (SPACE_BETWEEN_LINES * 3), Lang::getText("[INSTRUCTIONS] 05"), text_style);
// Escribe el texto de los objetos y sus puntos
const int ANCHOR2 = ANCHOR1 + SPACE_PRE_HEADER + SPACE_NEW_PARAGRAPH + (SPACE_BETWEEN_LINES * 3);
text_->writeStyle(param.game.game_area.center_x, ANCHOR2, Lang::getText("[INSTRUCTIONS] 06"), caption_style);
const int ANCHOR3 = ANCHOR2 + SPACE_POST_HEADER;
text_->writeShadowed(ANCHOR_ITEM + X_OFFSET, ANCHOR3 + (SPACE_BETWEEN_ITEM_LINES * 0), Lang::getText("[INSTRUCTIONS] 07"), Colors::SHADOW_TEXT);
text_->writeShadowed(ANCHOR_ITEM + X_OFFSET, ANCHOR3 + (SPACE_BETWEEN_ITEM_LINES * 1), Lang::getText("[INSTRUCTIONS] 08"), Colors::SHADOW_TEXT);
text_->writeShadowed(ANCHOR_ITEM + X_OFFSET, ANCHOR3 + (SPACE_BETWEEN_ITEM_LINES * 2), Lang::getText("[INSTRUCTIONS] 09"), Colors::SHADOW_TEXT);
text_->writeShadowed(ANCHOR_ITEM + X_OFFSET, ANCHOR3 + (SPACE_BETWEEN_ITEM_LINES * 3), Lang::getText("[INSTRUCTIONS] 10"), Colors::SHADOW_TEXT);
text_->writeShadowed(ANCHOR_ITEM + X_OFFSET, ANCHOR3 + (SPACE_BETWEEN_ITEM_LINES * 4), Lang::getText("[INSTRUCTIONS] 11"), Colors::SHADOW_TEXT);
// Deja el renderizador como estaba
SDL_SetRenderTarget(renderer_, temp);
// Da valor a la variable
sprite_pos_.x = ANCHOR_ITEM;
sprite_pos_.y = ANCHOR3 - ((Item::HEIGHT - text_->getCharacterSize()) / 2);
}
// Rellena el backbuffer
void Instructions::fillBackbuffer() {
// Modifica el renderizador para pintar en la textura
auto* temp = SDL_GetRenderTarget(renderer_);
SDL_SetRenderTarget(renderer_, backbuffer_);
// Limpia la textura
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0);
SDL_RenderClear(renderer_);
// Coloca el texto de fondo
SDL_RenderTexture(renderer_, texture_, nullptr, nullptr);
// Dibuja los sprites
for (auto& sprite : sprites_) {
sprite->render();
}
// Deja el renderizador como estaba
SDL_SetRenderTarget(renderer_, temp);
}
// Actualiza las variables
void Instructions::update(float delta_time) {
elapsed_time_ += delta_time; // Incrementa el tiempo transcurrido
static auto* const SCREEN = Screen::get();
SCREEN->update(delta_time); // Actualiza el objeto screen
Audio::update(); // Actualiza el objeto audio
updateSprites(); // Actualiza los sprites
updateBackbuffer(delta_time); // Gestiona la textura con los graficos
tiled_bg_->update(delta_time); // Actualiza el mosaico de fondo
fade_->update(delta_time); // Actualiza el objeto "fade"
fillBackbuffer(); // Rellena el backbuffer
}
// Pinta en pantalla
void Instructions::render() {
static auto* const SCREEN = Screen::get();
SCREEN->start(); // Prepara para empezar a dibujar en la textura de juego
SCREEN->clean(); // Limpia la pantalla
tiled_bg_->render(); // Dibuja el mosacico de fondo
// Copia la textura y el backbuffer al renderizador
if (view_.y == 0) {
renderLines(renderer_, backbuffer_, lines_);
} else {
SDL_RenderTexture(renderer_, backbuffer_, nullptr, &view_);
}
fade_->render(); // Renderiza el fundido
SCREEN->render(); // Vuelca el contenido del renderizador en pantalla
}
// Comprueba los eventos
void Instructions::checkEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event);
}
}
// Comprueba las entradas
void Instructions::checkInput() {
Input::get()->update();
GlobalInputs::check();
}
// Calcula el tiempo transcurrido desde el último frame
auto Instructions::calculateDeltaTime() -> float {
const Uint64 CURRENT_TIME = SDL_GetTicks();
const float DELTA_TIME = static_cast<float>(CURRENT_TIME - last_time_) / 1000.0F; // Convertir ms a segundos
last_time_ = CURRENT_TIME;
return DELTA_TIME;
}
// Avanza un frame (llamado desde Director::iterate)
void Instructions::iterate() {
const float DELTA_TIME = calculateDeltaTime();
checkInput();
update(DELTA_TIME);
render();
}
// Procesa un evento (llamado desde Director::handleEvent)
void Instructions::handleEvent(const SDL_Event& /*event*/) {
// Eventos globales ya gestionados por Director::handleEvent
}
// Bucle para la pantalla de instrucciones (fallback legacy)
void Instructions::run() {
last_time_ = SDL_GetTicks();
Audio::get()->playMusic("title.ogg");
while (Section::name == Section::Name::INSTRUCTIONS) {
const float DELTA_TIME = calculateDeltaTime();
checkInput();
update(DELTA_TIME);
checkEvents(); // Tiene que ir antes del render
render();
}
}
// Método para inicializar las líneas
auto Instructions::initializeLines(int height, float line_delay) -> std::vector<Line> {
std::vector<Line> lines;
for (int y = 0; y < height; y++) {
int direction = (y % 2 == 0) ? -1 : 1; // Pares a la izquierda, impares a la derecha
float delay = y * line_delay; // Retraso progresivo basado en la línea
lines.emplace_back(y, 0.0F, direction, delay);
}
return lines;
}
// Método para mover las líneas con suavizado (usando delta_time puro)
auto Instructions::moveLines(std::vector<Line>& lines, int width, float duration, float delta_time) -> bool {
bool all_lines_off_screen = true;
for (auto& line : lines) {
// Verificar si la línea ha superado su tiempo de retraso
if (!line.started) {
line.delay_time -= delta_time;
if (line.delay_time <= 0.0F) {
line.started = true;
line.accumulated_time = 0.0F;
} else {
all_lines_off_screen = false; // Aún hay líneas esperando para empezar
continue;
}
}
// Si la línea ya ha completado su movimiento, saltarla
if (line.accumulated_time >= duration) {
continue; // Esta línea ya terminó
}
// Acumular tiempo y calcular posición
line.accumulated_time += delta_time;
if (line.accumulated_time >= duration) {
// La línea ha completado su movimiento
line.accumulated_time = duration;
} else {
// La línea aún se está moviendo
all_lines_off_screen = false;
}
// Calcular posición con suavizado
float t = line.accumulated_time / duration;
float smooth_factor = easeInOutQuint(t);
line.x = line.direction * smooth_factor * width;
}
return all_lines_off_screen;
}
// Método para renderizar las líneas
void Instructions::renderLines(SDL_Renderer* renderer, SDL_Texture* texture, const std::vector<Line>& lines) {
for (const auto& line : lines) {
SDL_FRect src_rect = {.x = 0, .y = static_cast<float>(line.y), .w = 320, .h = 1};
SDL_FRect dst_rect = {.x = static_cast<float>(line.x), .y = static_cast<float>(line.y), .w = 320, .h = 1};
SDL_RenderTexture(renderer, texture, &src_rect, &dst_rect);
}
}
// Gestiona la textura con los graficos
void Instructions::updateBackbuffer(float delta_time) {
// Establece la ventana del backbuffer usando velocidad en pixels por segundo
// El scroll comienza desde (param.game.height + 100) y desciende a 0
// IMPORTANTE: Se redondea a entero para evitar deformaciones de textura causadas por sub-pixel rendering
float scroll_offset = elapsed_time_ * SCROLL_SPEED_PPS;
view_.y = std::round(std::max(0.0F, (param.game.height + 100.0F) - scroll_offset));
// Verifica si view_.y == 0 y gestiona el temporizador
if (view_.y == 0.0F) {
if (!start_delay_triggered_) {
// Activa el temporizador si no ha sido activado
start_delay_triggered_ = true;
start_delay_timer_ = 0.0F;
} else {
start_delay_timer_ += delta_time;
if (start_delay_timer_ >= START_DELAY_S) {
// Han pasado los segundos de retraso, mover líneas
all_lines_off_screen_ = moveLines(lines_, 320, LINE_MOVE_DURATION_S, delta_time);
}
}
}
// Comprueba si todas las líneas han terminado
if (all_lines_off_screen_) {
Section::name = Section::Name::TITLE;
Section::options = Section::Options::TITLE_1;
}
}

View File

@@ -0,0 +1,102 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_Texture, SDL_Renderer, Uint32, SDL_FPoint, SDL_FRect, Uint64
#include <memory> // Para unique_ptr, shared_ptr
#include <vector> // Para vector
class Fade;
class Sprite;
class Text;
class Texture;
class TiledBG;
/*
Esta clase gestiona un estado del programa. Se encarga de poner en pantalla
un texto explicativo para entender cómo se juega.
Además muestra algunos items y explica para qué sirven.
Utiliza dos texturas de apoyo, una con el texto ya escrito y otra donde se combina
tanto el texto de la primera textura como los sprites de los items.
Finalmente, una ventana recorre la textura para dar el efecto de que todo se desplaza
por la pantalla sobre el mosaico de fondo (gestionado por el correspondiente objeto).
*/
// --- Estructuras ---
struct Line { // Almacena información de línea animada
int y; // Coordenada Y de la línea
float x; // Coordenada X inicial (usamos float para mayor precisión en el suavizado)
int direction; // Dirección de movimiento: -1 para izquierda, 1 para derecha
float accumulated_time{0}; // Tiempo acumulado desde que empezó la animación (segundos)
float delay_time{0}; // Tiempo de retraso antes de comenzar la animación (segundos)
bool started{false}; // Indica si la línea ha comenzado a moverse
// Constructor de Line
Line(int y, float x, int direction, float delay)
: y(y),
x(x),
direction(direction),
delay_time(delay) {}
};
// Clase Instructions
class Instructions {
public:
// --- Constructor y destructor ---
Instructions();
~Instructions();
// --- Callbacks para el bucle SDL_MAIN_USE_CALLBACKS ---
void iterate(); // Ejecuta un frame
void handleEvent(const SDL_Event& event); // Procesa un evento
// --- Bucle principal legacy (fallback) ---
void run();
private:
// --- Constantes de tiempo (en segundos) ---
static constexpr float SPRITE_ANIMATION_CYCLE_S = 36.0F / 60.0F; // Ciclo de animación sprites (≈0.6s)
static constexpr float START_DELAY_S = 4.0F; // Retraso antes de mover líneas (4s)
static constexpr float LINE_MOVE_DURATION_S = 1.0F; // Duración movimiento líneas (1s)
static constexpr float LINE_START_DELAY_S = 0.005F; // Retraso entre líneas (5ms = 0.005s)
static constexpr float SCROLL_SPEED_PPS = 60.0F; // Velocidad de scroll (60 pixels por segundo)
// --- Objetos y punteros ---
SDL_Renderer* renderer_; // El renderizador de la ventana
SDL_Texture* texture_; // Textura fija con el texto
SDL_Texture* backbuffer_; // Textura para usar como backbuffer
std::vector<std::shared_ptr<Texture>> item_textures_; // Vector con las texturas de los items
std::vector<std::unique_ptr<Sprite>> sprites_; // Vector con los sprites de los items
std::shared_ptr<Text> text_; // Objeto para escribir texto
std::unique_ptr<TiledBG> tiled_bg_; // Objeto para dibujar el mosaico animado de fondo
std::unique_ptr<Fade> fade_; // Objeto para renderizar fades
// --- Variables ---
float elapsed_time_ = 0.0F; // Tiempo transcurrido (segundos)
Uint64 last_time_ = 0; // Último timestamp para calcular delta-time
SDL_FRect view_; // Vista del backbuffer que se va a mostrar por pantalla
SDL_FPoint sprite_pos_ = {.x = 0, .y = 0}; // Posición del primer sprite en la lista
float item_space_ = 2.0; // Espacio entre los items en pantalla
std::vector<Line> lines_; // Vector que contiene las líneas animadas en la pantalla
bool all_lines_off_screen_ = false; // Indica si todas las líneas han salido de la pantalla
float start_delay_timer_ = 0.0F; // Timer para retraso antes de mover líneas (segundos)
bool start_delay_triggered_ = false; // Bandera para determinar si el retraso ha comenzado
// --- Métodos internos ---
void update(float delta_time); // Actualiza las variables
void render(); // Pinta en pantalla
static void checkEvents(); // Comprueba los eventos
static void checkInput(); // Comprueba las entradas
void fillTexture(); // Rellena la textura de texto
void fillBackbuffer(); // Rellena el backbuffer
void iniSprites(); // Inicializa los sprites de los items
void updateSprites(); // Actualiza los sprites
static auto initializeLines(int height, float line_delay) -> std::vector<Line>; // Inicializa las líneas animadas
static auto moveLines(std::vector<Line>& lines, int width, float duration, float delta_time) -> bool; // Mueve las líneas usando delta_time puro
static void renderLines(SDL_Renderer* renderer, SDL_Texture* texture, const std::vector<Line>& lines); // Renderiza las líneas
void updateBackbuffer(float delta_time); // Gestiona la textura con los gráficos
auto calculateDeltaTime() -> float; // Calcula el tiempo transcurrido desde el último frame
};

View File

@@ -0,0 +1,570 @@
#include "intro.hpp"
#include <SDL3/SDL.h> // Para SDL_GetTicks, SDL_SetRenderDrawColor, SDL_FRect, SDL_RenderFillRect, SDL_GetRenderTarget, SDL_RenderClear, SDL_RenderRect, SDL_SetRenderTarget, SDL_BLENDMODE_BLEND, SDL_PixelFormat, SDL_PollEvent, SDL_RenderTexture, SDL_TextureAccess, SDL_Event, Uint64
#include <array> // Para array
#include <string> // Para basic_string, string
#include <utility> // Para move
#include "audio.hpp" // Para Audio
#include "card_sprite.hpp" // Para CardSprite
#include "color.hpp" // Para Color
#include "global_events.hpp" // Para handle
#include "global_inputs.hpp" // Para check
#include "input.hpp" // Para Input
#include "lang.hpp" // Para getText
#include "param.hpp" // Para Param, param, ParamGame, ParamIntro, ParamTitle
#include "resource.hpp" // Para Resource
#include "screen.hpp" // Para Screen
#include "section.hpp" // Para Name, name, Options, options
#include "text.hpp" // Para Text
#include "texture.hpp" // Para Texture
#include "tiled_bg.hpp" // Para TiledBG, TiledBGMode
#include "utils.hpp" // Para easeOutBounce
#include "writer.hpp" // Para Writer
// Constructor
Intro::Intro()
: tiled_bg_(std::make_unique<TiledBG>(param.game.game_area.rect, TiledBGMode::DIAGONAL)) {
// Inicializa variables
Section::name = Section::Name::INTRO;
Section::options = Section::Options::NONE;
// Inicializa las tarjetas
initSprites();
// Inicializa los textos
initTexts();
// Configura el fondo
tiled_bg_->setSpeed(TILED_BG_SPEED);
tiled_bg_->setColor(bg_color_);
// Inicializa el timer de delta time y arranca la música
last_time_ = SDL_GetTicks();
Audio::get()->playMusic("intro.ogg", 0);
}
// Comprueba los eventos
void Intro::checkEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event);
}
}
// Comprueba las entradas
void Intro::checkInput() {
Input::get()->update();
GlobalInputs::check();
}
// Actualiza las escenas de la intro
void Intro::updateScenes() {
// Sonido al lanzar la tarjeta (enable() devuelve true solo la primera vez)
if (card_sprites_.at(scene_)->enable()) {
Audio::get()->playSound(SFX_CARD_THROW);
}
// Cuando la tarjeta actual toca la mesa por primera vez: shake + sonido + la anterior sale despedida
if (!shake_done_ && card_sprites_.at(scene_)->hasFirstTouch()) {
Screen::get()->shake();
Audio::get()->playSound(SFX_CARD_IMPACT);
shake_done_ = true;
if (scene_ > 0) {
card_sprites_.at(scene_ - 1)->startExit();
}
}
switch (scene_) {
case 0:
updateScene0();
break;
case 1:
updateScene1();
break;
case 2:
updateScene2();
break;
case 3:
updateScene3();
break;
case 4:
updateScene4();
break;
case 5:
updateScene5();
break;
default:
break;
}
}
void Intro::updateScene0() {
// Primer texto cuando aterriza
if (card_sprites_.at(0)->hasLanded() && !texts_.at(0)->hasFinished()) {
texts_.at(0)->setEnabled(true);
}
// Segundo texto
if (texts_.at(0)->hasFinished() && !texts_.at(1)->hasFinished()) {
switchText(0, 1);
}
// Tercer texto
if (texts_.at(1)->hasFinished() && !texts_.at(2)->hasFinished()) {
switchText(1, 2);
}
// Fin de la primera escena: la tarjeta sale despedida
if (texts_.at(2)->hasFinished()) {
texts_.at(2)->setEnabled(false);
scene_++;
shake_done_ = false;
}
}
void Intro::updateScene1() {
// Texto cuando aterriza
if (card_sprites_.at(1)->hasLanded() && !texts_.at(3)->hasFinished()) {
texts_.at(3)->setEnabled(true);
}
// Fin de la segunda escena
if (texts_.at(3)->hasFinished()) {
texts_.at(3)->setEnabled(false);
scene_++;
shake_done_ = false;
}
}
void Intro::updateScene2() {
// Tercera imagen - GRITO: tarjeta y texto a la vez
if (!texts_.at(4)->hasFinished()) {
texts_.at(4)->setEnabled(true);
}
// Fin de la tercera escena
if (card_sprites_.at(2)->hasLanded() && texts_.at(4)->hasFinished()) {
texts_.at(4)->setEnabled(false);
scene_++;
shake_done_ = false;
}
}
void Intro::updateScene3() {
// Cuarta imagen - Reflexión
if (!texts_.at(5)->hasFinished()) {
texts_.at(5)->setEnabled(true);
}
// Segundo texto
if (texts_.at(5)->hasFinished() && !texts_.at(6)->hasFinished()) {
switchText(5, 6);
}
// Fin de la cuarta escena
if (card_sprites_.at(3)->hasLanded() && texts_.at(6)->hasFinished()) {
texts_.at(6)->setEnabled(false);
scene_++;
shake_done_ = false;
}
}
void Intro::updateScene4() {
// Quinta imagen - Patada
if (!texts_.at(7)->hasFinished()) {
texts_.at(7)->setEnabled(true);
}
// Fin de la quinta escena
if (card_sprites_.at(4)->hasLanded() && texts_.at(7)->hasFinished()) {
texts_.at(7)->setEnabled(false);
scene_++;
shake_done_ = false;
}
}
void Intro::updateScene5() {
// Sexta imagen - Globos de café
if (!texts_.at(8)->hasFinished()) {
texts_.at(8)->setEnabled(true);
}
// Acaba el último texto
if (texts_.at(8)->hasFinished()) {
texts_.at(8)->setEnabled(false);
}
// Última tarjeta: sale "como si se la llevara el viento" y transición a POST
if (card_sprites_.at(5)->hasLanded() && texts_.at(8)->hasFinished()) {
card_sprites_.at(5)->startExit();
state_ = State::POST;
state_start_time_ = SDL_GetTicks() / 1000.0F;
}
}
void Intro::switchText(int from_index, int to_index) {
texts_.at(from_index)->setEnabled(false);
texts_.at(to_index)->setEnabled(true);
}
// Actualiza las variables del objeto
void Intro::update(float delta_time) {
static auto* const SCREEN = Screen::get();
SCREEN->update(delta_time); // Actualiza el objeto screen
Audio::update(); // Actualiza el objeto Audio
tiled_bg_->update(delta_time); // Actualiza el fondo
switch (state_) {
case State::SCENES:
// Pausa inicial antes de empezar
if (initial_elapsed_ < INITIAL_DELAY_S) {
initial_elapsed_ += delta_time;
break;
}
updateSprites(delta_time);
updateTexts(delta_time);
updateScenes();
break;
case State::POST:
updateSprites(delta_time); // La última tarjeta puede estar saliendo durante POST
updatePostState();
break;
}
}
// Dibuja el objeto en pantalla
void Intro::render() {
static auto* const SCREEN = Screen::get();
SCREEN->start(); // Prepara para empezar a dibujar en la textura de juego
SCREEN->clean(); // Limpia la pantalla
tiled_bg_->render(); // Dibuja el fondo
switch (state_) {
case State::SCENES: {
renderTextRect();
renderSprites();
renderTexts();
break;
}
case State::POST:
renderSprites(); // La última tarjeta puede estar saliendo
break;
}
SCREEN->render(); // Vuelca el contenido del renderizador en pantalla
}
// Calcula el tiempo transcurrido desde el último frame
auto Intro::calculateDeltaTime() -> float {
const Uint64 CURRENT_TIME = SDL_GetTicks();
const float DELTA_TIME = static_cast<float>(CURRENT_TIME - last_time_) / 1000.0F; // Convertir ms a segundos
last_time_ = CURRENT_TIME;
return DELTA_TIME;
}
// Avanza un frame (llamado desde Director::iterate)
void Intro::iterate() {
const float DELTA_TIME = calculateDeltaTime();
checkInput();
update(DELTA_TIME);
render();
}
// Procesa un evento (llamado desde Director::handleEvent)
void Intro::handleEvent(const SDL_Event& /*event*/) {
// Eventos globales ya gestionados por Director::handleEvent
}
// Bucle principal legacy (fallback)
void Intro::run() {
last_time_ = SDL_GetTicks();
Audio::get()->playMusic("intro.ogg", 0);
while (Section::name == Section::Name::INTRO) {
const float DELTA_TIME = calculateDeltaTime();
checkInput();
update(DELTA_TIME);
checkEvents(); // Tiene que ir antes del render
render();
}
}
// Inicializa las tarjetas
void Intro::initSprites() {
// Listado de imagenes a usar
const std::array<std::string, 6> TEXTURE_LIST = {
"intro1.png",
"intro2.png",
"intro3.png",
"intro4.png",
"intro5.png",
"intro6.png"};
// Constantes
constexpr int TOTAL_SPRITES = TEXTURE_LIST.size();
const float BORDER = CARD_BORDER_SIZE;
auto texture = Resource::get()->getTexture(TEXTURE_LIST.front());
const float CARD_WIDTH = texture->getWidth() + (BORDER * 2);
const float CARD_HEIGHT = texture->getHeight() + (BORDER * 2);
// Crea las texturas para las tarjetas (imagen con marco)
std::vector<std::shared_ptr<Texture>> card_textures;
for (int i = 0; i < TOTAL_SPRITES; ++i) {
auto card_texture = std::make_unique<Texture>(Screen::get()->getRenderer());
card_texture->createBlank(CARD_WIDTH, CARD_HEIGHT, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET);
card_texture->setBlendMode(SDL_BLENDMODE_BLEND);
auto* temp = SDL_GetRenderTarget(Screen::get()->getRenderer());
card_texture->setAsRenderTarget(Screen::get()->getRenderer());
SDL_SetRenderDrawColor(Screen::get()->getRenderer(), 0, 0, 0, 0);
SDL_RenderClear(Screen::get()->getRenderer());
// Marco de la tarjeta
auto color = param.intro.card_color;
SDL_SetRenderDrawColor(Screen::get()->getRenderer(), color.r, color.g, color.b, color.a);
SDL_FRect rect1 = {.x = 1, .y = 0, .w = CARD_WIDTH - 2, .h = CARD_HEIGHT};
SDL_FRect rect2 = {.x = 0, .y = 1, .w = CARD_WIDTH, .h = CARD_HEIGHT - 2};
SDL_RenderRect(Screen::get()->getRenderer(), &rect1);
SDL_RenderRect(Screen::get()->getRenderer(), &rect2);
// Imagen dentro del marco
SDL_FRect dest = {.x = BORDER, .y = BORDER, .w = CARD_WIDTH - (BORDER * 2), .h = CARD_HEIGHT - (BORDER * 2)};
SDL_RenderTexture(Screen::get()->getRenderer(), Resource::get()->getTexture(TEXTURE_LIST.at(i))->getSDLTexture(), nullptr, &dest);
SDL_SetRenderTarget(Screen::get()->getRenderer(), temp);
card_textures.push_back(std::move(card_texture));
}
// Crea la textura de sombra (compartida entre todas las tarjetas)
auto shadow_texture = std::make_shared<Texture>(Screen::get()->getRenderer());
shadow_texture->createBlank(CARD_WIDTH, CARD_HEIGHT, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET);
shadow_texture->setBlendMode(SDL_BLENDMODE_BLEND);
auto* temp = SDL_GetRenderTarget(Screen::get()->getRenderer());
shadow_texture->setAsRenderTarget(Screen::get()->getRenderer());
SDL_SetRenderDrawColor(Screen::get()->getRenderer(), 0, 0, 0, 0);
SDL_RenderClear(Screen::get()->getRenderer());
auto shadow_color = param.intro.shadow_color;
SDL_SetRenderDrawColor(Screen::get()->getRenderer(), shadow_color.r, shadow_color.g, shadow_color.b, Color::MAX_ALPHA_VALUE);
SDL_FRect shadow_rect1 = {.x = 1, .y = 0, .w = CARD_WIDTH - 2, .h = CARD_HEIGHT};
SDL_FRect shadow_rect2 = {.x = 0, .y = 1, .w = CARD_WIDTH, .h = CARD_HEIGHT - 2};
SDL_RenderFillRect(Screen::get()->getRenderer(), &shadow_rect1);
SDL_RenderFillRect(Screen::get()->getRenderer(), &shadow_rect2);
SDL_SetRenderTarget(Screen::get()->getRenderer(), temp);
shadow_texture->setAlpha(shadow_color.a);
// Posición de aterrizaje (centro de la zona de juego)
const float X_DEST = param.game.game_area.center_x - (CARD_WIDTH / 2);
const float Y_DEST = param.game.game_area.first_quarter_y - (CARD_HEIGHT / 4);
// Configuración por tarjeta: posición de entrada, ángulo, salida
// Cada tarjeta viene de un borde diferente (gente alrededor de una mesa lanzando cartas al centro)
struct CardConfig {
float entry_x; // Posición inicial X
float entry_y; // Posición inicial Y
double entry_angle; // Ángulo de entrada
float exit_vx; // Velocidad de salida X
float exit_vy; // Velocidad de salida Y
float exit_ax; // Aceleración de salida X
float exit_ay; // Aceleración de salida Y
double exit_rotation; // Velocidad de rotación de salida
};
const float W = param.game.width;
const float H = param.game.height;
const float S = CARD_EXIT_SPEED;
const float A = CARD_EXIT_ACCEL;
const double R = CARD_EXIT_ROTATION;
const CardConfig CARD_CONFIGS[] = {
// 0: Entra desde la izquierda. La 1 entra desde la derecha → sale empujada hacia la izquierda
{-CARD_WIDTH, Y_DEST - 20.0F, CARD_ANGLE_0, -S, S * 0.1F, -A, 0.0F, -R},
// 1: Entra desde la derecha. La 2 entra desde arriba → sale empujada hacia abajo
{W + CARD_WIDTH, Y_DEST + 15.0F, CARD_ANGLE_1, S * 0.15F, S, 0.0F, A, R * 1.1},
// 2: Entra desde arriba. La 3 entra desde abajo → sale empujada hacia arriba
{X_DEST + 30.0F, -CARD_HEIGHT, CARD_ANGLE_2, -S * 0.15F, -S, 0.0F, -A, -R * 0.9},
// 3: Entra desde abajo. La 4 entra desde arriba-izquierda → sale empujada hacia abajo-derecha
{X_DEST - 25.0F, H + CARD_HEIGHT, CARD_ANGLE_3, S * 0.8F, S * 0.6F, A * 0.5F, A * 0.4F, R},
// 4: Entra desde arriba-izquierda. La 5 entra desde derecha-abajo → sale empujada hacia arriba-izquierda
{-CARD_WIDTH * 0.5F, -CARD_HEIGHT, CARD_ANGLE_4, -S * 0.7F, -S * 0.5F, -A * 0.5F, -A * 0.3F, -R * 1.2},
// 5: Entra desde la derecha-abajo. Última: sale hacia la izquierda suave (viento)
{W + CARD_WIDTH, H * 0.6F, CARD_ANGLE_5, -S * 0.6F, -S * 0.1F, -A * 0.5F, 0.0F, -R * 0.7},
};
// Inicializa los CardSprites
for (int i = 0; i < TOTAL_SPRITES; ++i) {
auto card = std::make_unique<CardSprite>(card_textures.at(i));
card->setWidth(CARD_WIDTH);
card->setHeight(CARD_HEIGHT);
card->setSpriteClip(0, 0, CARD_WIDTH, CARD_HEIGHT);
const auto& cfg = CARD_CONFIGS[i];
// Posición de aterrizaje (centro)
card->setLandingPosition(X_DEST, Y_DEST);
// Posición de entrada (borde de pantalla)
card->setEntryPosition(cfg.entry_x, cfg.entry_y);
// Parámetros de entrada: zoom, ángulo, duración, easing
card->setEntryParams(CARD_START_ZOOM, cfg.entry_angle, CARD_ENTRY_DURATION_S, easeOutBounce);
// Parámetros de salida
card->setExitParams(cfg.exit_vx, cfg.exit_vy, cfg.exit_ax, cfg.exit_ay, cfg.exit_rotation);
// Sombra
card->setShadowTexture(shadow_texture);
card->setShadowOffset(SHADOW_OFFSET, SHADOW_OFFSET);
// Límites de pantalla
card->setScreenBounds(param.game.width, param.game.height);
// Última tarjeta: gana algo de altura al salir (se la lleva el viento)
if (i == TOTAL_SPRITES - 1) {
card->setExitLift(1.2F, 0.15F); // Hasta zoom 1.2, a 0.15/s
}
card_sprites_.push_back(std::move(card));
}
}
// Inicializa los textos
void Intro::initTexts() {
constexpr int TOTAL_TEXTS = 9;
for (int i = 0; i < TOTAL_TEXTS; ++i) {
auto writer = std::make_unique<Writer>(Resource::get()->getText("04b_25_metal"));
writer->setPosX(0);
writer->setPosY(param.game.height - param.intro.text_distance_from_bottom);
writer->setKerning(TEXT_KERNING);
writer->setEnabled(false);
writer->setFinishedTimerS(TEXT_DISPLAY_DURATION_S);
texts_.push_back(std::move(writer));
}
// Un dia qualsevol de l'any 2000
texts_.at(0)->setCaption(Lang::getText("[INTRO] 1"));
texts_.at(0)->setSpeedS(TEXT_SPEED_NORMAL);
// Tot esta tranquil a la UPV
texts_.at(1)->setCaption(Lang::getText("[INTRO] 2"));
texts_.at(1)->setSpeedS(TEXT_SPEED_NORMAL);
// Fins que un desaprensiu...
texts_.at(2)->setCaption(Lang::getText("[INTRO] 3"));
texts_.at(2)->setSpeedS(TEXT_SPEED_SLOW);
// HEY! ME ANE A FERME UN CORTAET...
texts_.at(3)->setCaption(Lang::getText("[INTRO] 4"));
texts_.at(3)->setSpeedS(TEXT_SPEED_NORMAL);
// UAAAAAAAAAAAAA!!!
texts_.at(4)->setCaption(Lang::getText("[INTRO] 5"));
texts_.at(4)->setSpeedS(TEXT_SPEED_ULTRA_FAST);
// Espera un moment...
texts_.at(5)->setCaption(Lang::getText("[INTRO] 6"));
texts_.at(5)->setSpeedS(TEXT_SPEED_VERY_SLOW);
// Si resulta que no tinc solt!
texts_.at(6)->setCaption(Lang::getText("[INTRO] 7"));
texts_.at(6)->setSpeedS(TEXT_SPEED_VERY_FAST);
// MERDA DE MAQUINA!
texts_.at(7)->setCaption(Lang::getText("[INTRO] 8"));
texts_.at(7)->setSpeedS(TEXT_SPEED_FAST);
// Blop... blop... blop...
texts_.at(8)->setCaption(Lang::getText("[INTRO] 9"));
texts_.at(8)->setSpeedS(TEXT_SPEED_ULTRA_SLOW);
for (auto& text : texts_) {
text->center(param.game.game_area.center_x);
}
}
// Actualiza los sprites
void Intro::updateSprites(float delta_time) {
for (auto& sprite : card_sprites_) {
sprite->update(delta_time);
}
}
// Actualiza los textos
void Intro::updateTexts(float delta_time) {
for (auto& text : texts_) {
text->updateS(delta_time); // Usar updateS para delta_time en segundos
}
}
// Dibuja los sprites (todas las tarjetas activas, para que convivan la saliente y la entrante)
void Intro::renderSprites() {
for (auto& card : card_sprites_) {
card->render();
}
}
// Dibuja los textos
void Intro::renderTexts() {
for (const auto& text : texts_) {
text->render();
}
}
// Actualiza el estado POST
void Intro::updatePostState() {
const float ELAPSED_TIME = (SDL_GetTicks() / 1000.0F) - state_start_time_;
switch (post_state_) {
case PostState::STOP_BG:
// EVENTO: Detiene el fondo después del tiempo especificado
if (ELAPSED_TIME >= POST_BG_STOP_DELAY_S) {
tiled_bg_->stopGracefully();
if (!bg_color_.IS_EQUAL_TO(param.title.bg_color)) {
bg_color_ = bg_color_.APPROACH_TO(param.title.bg_color, 1);
}
tiled_bg_->setColor(bg_color_);
}
// Cambia de estado si el fondo se ha detenido y recuperado el color
if (tiled_bg_->isStopped() && bg_color_.IS_EQUAL_TO(param.title.bg_color)) {
post_state_ = PostState::END;
state_start_time_ = SDL_GetTicks() / 1000.0F;
}
break;
case PostState::END:
// Finaliza la intro después del tiempo especificado
if (ELAPSED_TIME >= POST_END_DELAY_S) {
Audio::get()->stopMusic();
Section::name = Section::Name::TITLE;
Section::options = Section::Options::TITLE_1;
}
break;
default:
break;
}
}
void Intro::renderTextRect() {
static const float HEIGHT = Resource::get()->getText("04b_25_metal")->getCharacterSize();
static SDL_FRect rect_ = {.x = 0.0F, .y = param.game.height - param.intro.text_distance_from_bottom - HEIGHT, .w = param.game.width, .h = HEIGHT * 3};
SDL_SetRenderDrawColor(Screen::get()->getRenderer(), param.intro.shadow_color.r, param.intro.shadow_color.g, param.intro.shadow_color.b, param.intro.shadow_color.a);
SDL_RenderFillRect(Screen::get()->getRenderer(), &rect_);
}

View File

@@ -0,0 +1,133 @@
#pragma once
#include <SDL3/SDL.h> // Para Uint32, Uint64
#include <memory> // Para unique_ptr
#include <vector> // Para vector
#include "card_sprite.hpp" // Para CardSprite
#include "color.hpp" // Para Color
#include "param.hpp" // Para Param, ParamIntro, param
#include "tiled_bg.hpp" // Para TiledBG
#include "writer.hpp" // Para Writer
// --- Clase Intro: secuencia cinemática de introducción del juego ---
//
// Esta clase gestiona la secuencia de introducción narrativa del juego, mostrando
// una serie de escenas con imágenes, texto y efectos visuales sincronizados.
//
// Funcionalidades principales:
// • Sistema de escenas secuencial: 6 escenas con transiciones automáticas
// • Animaciones de tarjetas: efecto de lanzamiento sobre mesa con zoom, rotación y rebote
// • Texto narrativo: velocidades de escritura configurables por escena
// • Efectos visuales: sombras, bordes y transiciones de color
// • Audio sincronizado: música de fondo durante toda la secuencia
// • Estado POST: transición suave hacia el menú principal
class Intro {
public:
// --- Constructor y destructor ---
Intro();
~Intro() = default;
// --- Callbacks para el bucle SDL_MAIN_USE_CALLBACKS ---
void iterate(); // Ejecuta un frame
void handleEvent(const SDL_Event& event); // Procesa un evento
// --- Bucle principal legacy (fallback) ---
void run();
private:
// --- Constantes de tiempo (en segundos) ---
static constexpr float TEXT_DISPLAY_DURATION_S = 3.0F; // Duración de visualización de texto
static constexpr float POST_BG_STOP_DELAY_S = 1.0F; // Retraso antes de detener el fondo
static constexpr float POST_END_DELAY_S = 1.0F; // Retraso antes de finalizar intro
static constexpr float INITIAL_DELAY_S = 2.0F; // Pausa antes de empezar las escenas
// --- Constantes de sonido ---
static constexpr const char* SFX_CARD_THROW = "service_menu_select.wav"; // Sonido al lanzar una tarjeta
static constexpr const char* SFX_CARD_IMPACT = "player_collision.wav"; // Sonido al impactar en la mesa
// --- Constantes de layout ---
static constexpr float CARD_BORDER_SIZE = 2.0F; // Tamaño del borde de tarjetas
static constexpr float SHADOW_OFFSET = 8.0F; // Desplazamiento de sombra
static constexpr float TILED_BG_SPEED = 18.0F; // Velocidad del fondo mosaico (pixels/segundo)
static constexpr int TEXT_KERNING = -2; // Espaciado entre caracteres
// --- Constantes de velocidades de texto (segundos entre caracteres, menor = más rápido) ---
static constexpr float TEXT_SPEED_ULTRA_FAST = 0.0167F; // Ultra rápida (1 frame a 60fps)
static constexpr float TEXT_SPEED_VERY_FAST = 0.033F; // Muy rápida (2 frames a 60fps)
static constexpr float TEXT_SPEED_FAST = 0.05F; // Rápida (3 frames a 60fps)
static constexpr float TEXT_SPEED_NORMAL = 0.133F; // Normal (8 frames a 60fps)
static constexpr float TEXT_SPEED_SLOW = 0.2F; // Lenta (12 frames a 60fps)
static constexpr float TEXT_SPEED_VERY_SLOW = 0.267F; // Muy lenta (16 frames a 60fps)
static constexpr float TEXT_SPEED_ULTRA_SLOW = 0.333F; // Ultra lenta (20 frames a 60fps)
// --- Constantes de animaciones de tarjetas ---
static constexpr float CARD_ENTRY_DURATION_S = 1.5F; // Duración de la animación de entrada
static constexpr float CARD_START_ZOOM = 1.8F; // Zoom inicial (como si estuviera cerca)
static constexpr float CARD_EXIT_SPEED = 400.0F; // Velocidad base de salida (pixels/s)
static constexpr float CARD_EXIT_ACCEL = 200.0F; // Aceleración de salida (pixels/s²)
static constexpr double CARD_EXIT_ROTATION = 450.0; // Velocidad de rotación en salida (grados/s)
// --- Ángulos iniciales de entrada por tarjeta (grados) ---
static constexpr double CARD_ANGLE_0 = 12.0;
static constexpr double CARD_ANGLE_1 = -15.0;
static constexpr double CARD_ANGLE_2 = 8.0;
static constexpr double CARD_ANGLE_3 = -10.0;
static constexpr double CARD_ANGLE_4 = 18.0;
static constexpr double CARD_ANGLE_5 = -7.0;
// --- Estados internos ---
enum class State {
SCENES,
POST,
};
enum class PostState {
STOP_BG,
END,
};
// --- Objetos ---
std::vector<std::unique_ptr<CardSprite>> card_sprites_; // Tarjetas animadas con sombra integrada
std::vector<std::unique_ptr<Writer>> texts_; // Textos de la intro
std::unique_ptr<TiledBG> tiled_bg_; // Fondo en mosaico
// --- Variables ---
Uint64 last_time_ = 0; // Último timestamp para calcular delta-time
int scene_ = 0; // Indica qué escena está activa
State state_ = State::SCENES; // Estado principal de la intro
PostState post_state_ = PostState::STOP_BG; // Estado POST
float state_start_time_ = 0.0F; // Tiempo de inicio del estado actual (segundos)
Color bg_color_ = param.intro.bg_color; // Color de fondo
bool shake_done_ = false; // Evita shake repetido en la misma escena
float initial_elapsed_ = 0.0F; // Tiempo acumulado antes de empezar
// --- Métodos internos ---
void update(float delta_time); // Actualiza las variables del objeto
void render(); // Dibuja el objeto en pantalla
static void checkInput(); // Comprueba las entradas
static void checkEvents(); // Comprueba los eventos
void updateScenes(); // Actualiza las escenas de la intro
void initSprites(); // Inicializa las tarjetas
void initTexts(); // Inicializa los textos
void updateSprites(float delta_time); // Actualiza los sprites
void updateTexts(float delta_time); // Actualiza los textos
void renderSprites(); // Dibuja los sprites
void renderTexts(); // Dibuja los textos
static void renderTextRect(); // Dibuja el rectángulo de fondo del texto
void updatePostState(); // Actualiza el estado POST
auto calculateDeltaTime() -> float; // Calcula el tiempo transcurrido desde el último frame
// --- Métodos para manejar cada escena individualmente ---
void updateScene0();
void updateScene1();
void updateScene2();
void updateScene3();
void updateScene4();
void updateScene5();
// --- Métodos auxiliares ---
void switchText(int from_index, int to_index);
};

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

@@ -0,0 +1,212 @@
#include "logo.hpp"
#include <SDL3/SDL.h> // Para SDL_GetTicks, SDL_PollEvent, SDL_Event, SDL_FRect, Uint64
#include <cstddef> // Para size_t
#include <string> // Para basic_string
#include <utility> // Para move
#include "audio.hpp" // Para Audio
#include "color.hpp" // Para Color
#include "global_events.hpp" // Para handle
#include "global_inputs.hpp" // Para check
#include "input.hpp" // Para Input
#include "param.hpp" // Para Param, ParamGame, param
#include "resource.hpp" // Para Resource
#include "screen.hpp" // Para Screen
#include "section.hpp" // Para Name, name
#include "sprite.hpp" // Para Sprite
#include "texture.hpp" // Para Texture
#include "utils.hpp" // Para Zone
// Constructor
Logo::Logo()
: since_texture_(Resource::get()->getTexture("logo_since_1998.png")),
since_sprite_(std::make_unique<Sprite>(since_texture_)),
jail_texture_(Resource::get()->getTexture("logo_jailgames.png")) {
// Inicializa variables
Section::name = Section::Name::LOGO;
dest_.x = param.game.game_area.center_x - (jail_texture_->getWidth() / 2);
dest_.y = param.game.game_area.center_y - (jail_texture_->getHeight() / 2);
since_sprite_->setPosition(SDL_FRect{
.x = static_cast<float>((param.game.width - since_texture_->getWidth()) / 2),
.y = static_cast<float>(SINCE_SPRITE_Y_OFFSET + jail_texture_->getHeight() + LOGO_SPACING),
.w = static_cast<float>(since_texture_->getWidth()),
.h = static_cast<float>(since_texture_->getHeight())});
since_sprite_->setY(dest_.y + jail_texture_->getHeight() + LOGO_SPACING);
since_sprite_->setSpriteClip(0, 0, since_texture_->getWidth(), since_texture_->getHeight());
since_texture_->setColor(SPECTRUM_BLACK.r, SPECTRUM_BLACK.g, SPECTRUM_BLACK.b);
// Crea los sprites de cada linea
for (int i = 0; i < jail_texture_->getHeight(); ++i) {
auto temp = std::make_unique<Sprite>(jail_texture_, 0, i, jail_texture_->getWidth(), SPRITE_LINE_HEIGHT);
temp->setSpriteClip(0, i, jail_texture_->getWidth(), SPRITE_LINE_HEIGHT);
const int POS_X = (i % 2 == 0) ? param.game.width + (i * LINE_OFFSET_FACTOR) : -jail_texture_->getWidth() - (i * LINE_OFFSET_FACTOR);
temp->setX(POS_X);
temp->setY(dest_.y + i);
jail_sprite_.push_back(std::move(temp));
}
// Inicializa el timer de delta time para el primer frame del callback
last_time_ = SDL_GetTicks();
// Inicializa el vector de colores con la paleta ZX Spectrum
color_.emplace_back(SPECTRUM_BLACK);
color_.emplace_back(SPECTRUM_BLUE);
color_.emplace_back(SPECTRUM_RED);
color_.emplace_back(SPECTRUM_MAGENTA);
color_.emplace_back(SPECTRUM_GREEN);
color_.emplace_back(SPECTRUM_CYAN);
color_.emplace_back(SPECTRUM_YELLOW);
color_.emplace_back(SPECTRUM_WHITE);
}
// Destructor
Logo::~Logo() {
jail_texture_->setColor(RESET_COLOR.r, RESET_COLOR.g, RESET_COLOR.b);
since_texture_->setColor(RESET_COLOR.r, RESET_COLOR.g, RESET_COLOR.b);
Audio::get()->stopAllSounds();
Audio::get()->stopMusic();
}
// Comprueba el manejador de eventos
void Logo::checkEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event);
}
}
// Comprueba las entradas
void Logo::checkInput() {
Input::get()->update();
GlobalInputs::check();
}
// Maneja la reproducción del sonido del logo
void Logo::handleSound() {
if (!sound_triggered_ && elapsed_time_s_ >= SOUND_TRIGGER_TIME_S) {
Audio::get()->playSound("logo.wav");
sound_triggered_ = true;
}
}
// Gestiona el logo de JAILGAMES
void Logo::updateJAILGAMES(float delta_time) {
if (elapsed_time_s_ > SOUND_TRIGGER_TIME_S) {
const float PIXELS_TO_MOVE = LOGO_SPEED_PX_PER_S * delta_time;
for (size_t i = 0; i < jail_sprite_.size(); ++i) {
if (jail_sprite_[i]->getX() != dest_.x) {
if (i % 2 == 0) {
jail_sprite_[i]->incX(-PIXELS_TO_MOVE);
if (jail_sprite_[i]->getX() < dest_.x) {
jail_sprite_[i]->setX(dest_.x);
}
} else {
jail_sprite_[i]->incX(PIXELS_TO_MOVE);
if (jail_sprite_[i]->getX() > dest_.x) {
jail_sprite_[i]->setX(dest_.x);
}
}
}
}
}
// Comprueba si ha terminado el logo
if (elapsed_time_s_ >= END_LOGO_TIME_S + POST_LOGO_DURATION_S) {
Section::name = Section::Name::INTRO;
}
}
// Gestiona el color de las texturas
void Logo::updateTextureColors(float delta_time) {
// Manejo de 'sinceTexture'
for (int i = 0; i <= MAX_SINCE_COLOR_INDEX; ++i) {
const float TARGET_TIME = SHOW_SINCE_SPRITE_TIME_S + (COLOR_CHANGE_INTERVAL_S * i);
if (elapsed_time_s_ >= TARGET_TIME && elapsed_time_s_ - delta_time < TARGET_TIME) {
since_texture_->setColor(color_[i].r, color_[i].g, color_[i].b);
}
}
// Manejo de 'jailTexture' y 'sinceTexture' en el fade
for (int i = 0; i <= MAX_FADE_COLOR_INDEX; ++i) {
const float TARGET_TIME = INIT_FADE_TIME_S + (COLOR_CHANGE_INTERVAL_S * i);
if (elapsed_time_s_ >= TARGET_TIME && elapsed_time_s_ - delta_time < TARGET_TIME) {
jail_texture_->setColor(color_[MAX_FADE_COLOR_INDEX - i].r, color_[MAX_FADE_COLOR_INDEX - i].g, color_[MAX_FADE_COLOR_INDEX - i].b);
since_texture_->setColor(color_[MAX_FADE_COLOR_INDEX - i].r, color_[MAX_FADE_COLOR_INDEX - i].g, color_[MAX_FADE_COLOR_INDEX - i].b);
}
}
}
// Actualiza las variables
void Logo::update(float delta_time) {
elapsed_time_s_ += delta_time; // Acumula el tiempo transcurrido
static auto* const SCREEN = Screen::get();
SCREEN->update(delta_time); // Actualiza el objeto screen
Audio::update(); // Actualiza el objeto audio
handleSound(); // Maneja la reproducción del sonido
updateTextureColors(delta_time); // Actualiza los colores de las texturas
updateJAILGAMES(delta_time); // Actualiza el logo de JAILGAMES
}
// Dibuja en pantalla
void Logo::render() {
static auto* const SCREEN = Screen::get();
SCREEN->start();
SCREEN->clean();
renderJAILGAMES();
SCREEN->render();
}
// Calcula el tiempo transcurrido desde el último frame
auto Logo::calculateDeltaTime() -> float {
const Uint64 CURRENT_TIME = SDL_GetTicks();
const float DELTA_TIME = static_cast<float>(CURRENT_TIME - last_time_) / 1000.0F; // Convertir ms a segundos
last_time_ = CURRENT_TIME;
return DELTA_TIME;
}
// Avanza un frame del logo (llamado desde Director::iterate)
void Logo::iterate() {
const float DELTA_TIME = calculateDeltaTime();
checkInput();
update(DELTA_TIME);
render();
}
// Procesa un evento (llamado desde Director::handleEvent)
void Logo::handleEvent(const SDL_Event& /*event*/) {
// Eventos globales (QUIT, resize, hotplug) ya gestionados por Director::handleEvent
}
// Bucle para el logo del juego (fallback legacy)
void Logo::run() {
last_time_ = SDL_GetTicks();
while (Section::name == Section::Name::LOGO) {
const float DELTA_TIME = calculateDeltaTime();
checkInput();
update(DELTA_TIME);
checkEvents(); // Tiene que ir antes del render
render();
}
}
// Renderiza el logo de JAILGAMES
void Logo::renderJAILGAMES() {
// Dibuja los sprites
for (auto& sprite : jail_sprite_) {
sprite->render();
}
if (elapsed_time_s_ >= SHOW_SINCE_SPRITE_TIME_S) {
since_sprite_->render();
}
}

View File

@@ -0,0 +1,95 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FPoint, Uint64
#include <memory> // Para shared_ptr, unique_ptr
#include <vector> // Para vector
#include "color.hpp" // for Color
class Sprite;
class Texture;
// --- Clase Logo: pantalla de presentación de JAILGAMES con efectos retro ---
//
// Esta clase gestiona el estado inicial del programa, mostrando el logo corporativo
// de JAILGAMES con efectos visuales inspirados en el ZX Spectrum.
//
// Funcionalidades principales:
// • Animación de convergencia: cada línea del logo entra desde los laterales
// • Efectos de color: transiciones automáticas usando la paleta ZX Spectrum
// • Audio sincronizado: reproduce sonido del logo en momento específico
// • Transición temporal: duración controlada con paso automático al siguiente estado
// • Sistema delta-time: animaciones suaves independientes del framerate
//
// La clase utiliza un sistema de tiempo basado en segundos para garantizar
// consistencia visual en diferentes velocidades de procesamiento.
class Logo {
public:
// --- Constructor y destructor ---
Logo();
~Logo();
// --- Callbacks para el bucle SDL_MAIN_USE_CALLBACKS ---
void iterate(); // Ejecuta un frame
void handleEvent(const SDL_Event& event); // Procesa un evento
// --- Bucle principal legacy (fallback) ---
void run();
private:
// --- Constantes de tiempo (en segundos) ---
static constexpr float SOUND_TRIGGER_TIME_S = 0.5F; // Tiempo para activar el sonido del logo
static constexpr float SHOW_SINCE_SPRITE_TIME_S = 1.167F; // Tiempo para mostrar el sprite "SINCE 1998"
static constexpr float INIT_FADE_TIME_S = 5.0F; // Tiempo de inicio del fade a negro
static constexpr float END_LOGO_TIME_S = 6.668F; // Tiempo de finalización del logo
static constexpr float POST_LOGO_DURATION_S = 0.333F; // Duración adicional después del fade
static constexpr float LOGO_SPEED_PX_PER_S = 480.0F; // Velocidad de desplazamiento (píxeles por segundo) - 8.0f/16.67f*1000
static constexpr float COLOR_CHANGE_INTERVAL_S = 0.0667F; // Intervalo entre cambios de color (~4 frames a 60fps)
// --- Constantes de layout ---
static constexpr int SINCE_SPRITE_Y_OFFSET = 83; // Posición Y base del sprite "Since 1998"
static constexpr int LOGO_SPACING = 5; // Espaciado entre elementos del logo
static constexpr int LINE_OFFSET_FACTOR = 3; // Factor de desplazamiento inicial por línea
static constexpr int SPRITE_LINE_HEIGHT = 1; // Altura de cada línea sprite
// --- Constantes de colores ---
static constexpr int MAX_SINCE_COLOR_INDEX = 7; // Índice máximo para colores del sprite "Since"
static constexpr int MAX_FADE_COLOR_INDEX = 6; // Índice máximo para colores del fade
// --- Paleta ZX Spectrum para efectos de logo ---
static constexpr Color SPECTRUM_BLACK = Color(0x00, 0x00, 0x00); // Negro
static constexpr Color SPECTRUM_BLUE = Color(0x00, 0x00, 0xd8); // Azul
static constexpr Color SPECTRUM_RED = Color(0xd8, 0x00, 0x00); // Rojo
static constexpr Color SPECTRUM_MAGENTA = Color(0xd8, 0x00, 0xd8); // Magenta
static constexpr Color SPECTRUM_GREEN = Color(0x00, 0xd8, 0x00); // Verde
static constexpr Color SPECTRUM_CYAN = Color(0x00, 0xd8, 0xd8); // Cian
static constexpr Color SPECTRUM_YELLOW = Color(0xd8, 0xd8, 0x00); // Amarillo
static constexpr Color SPECTRUM_WHITE = Color(0xFF, 0xFF, 0xFF); // Blanco brillante
static constexpr Color RESET_COLOR = Color(255, 255, 255); // Color de reset
// --- Objetos y punteros ---
std::shared_ptr<Texture> since_texture_; // Textura con los gráficos "Since 1998"
std::unique_ptr<Sprite> since_sprite_; // Sprite para manejar la since_texture
std::shared_ptr<Texture> jail_texture_; // Textura con los gráficos "JAILGAMES"
std::vector<std::unique_ptr<Sprite>> jail_sprite_; // Vector con los sprites de cada línea que forman el bitmap JAILGAMES
// --- Variables ---
std::vector<Color> color_; // Vector con los colores para el fade
float elapsed_time_s_ = 0.0F; // Tiempo transcurrido en segundos
Uint64 last_time_ = 0; // Último timestamp para calcular delta-time
SDL_FPoint dest_; // Posición donde dibujar el logo
bool sound_triggered_ = false; // Indica si el sonido del logo ya se reprodujo
// --- Métodos internos ---
void update(float delta_time); // Actualiza las variables
void render(); // Dibuja en pantalla
static void checkEvents(); // Comprueba el manejador de eventos
static void checkInput(); // Comprueba las entradas
void updateJAILGAMES(float delta_time); // Gestiona el logo de JAILGAMES
void renderJAILGAMES(); // Renderiza el logo de JAILGAMES
void updateTextureColors(float delta_time); // Gestiona el color de las texturas
void handleSound(); // Maneja la reproducción del sonido del logo
auto calculateDeltaTime() -> float; // Calcula el tiempo transcurrido desde el último frame
};

View File

@@ -0,0 +1,524 @@
#include "title.hpp"
#include <SDL3/SDL.h> // Para SDL_GetTicks, SDL_Event, SDL_Keycode, SDL_PollEvent, SDLK_A, SDLK_C, SDLK_D, SDLK_F, SDLK_S, SDLK_V, SDLK_X, SDLK_Z, SDL_EventType, Uint64
#include <ranges> // Para __find_if_fn, find_if
#include <string> // Para basic_string, char_traits, operator+, to_string, string
#include <vector> // Para vector
#include "audio.hpp" // Para Audio
#include "color.hpp" // Para Color, NO_COLOR_MOD, TITLE_SHADOW_TEXT
#include "fade.hpp" // Para Fade
#include "game_logo.hpp" // Para GameLogo
#include "global_events.hpp" // Para handle
#include "global_inputs.hpp" // Para check
#include "input.hpp" // Para Input
#include "input_types.hpp" // Para InputAction
#include "lang.hpp" // Para getText
#include "options.hpp" // Para Gamepad, GamepadManager, gamepad_manager, Settings, settings, Keyboard, keyboard, getPlayerWhoUsesKeyboard, swapControllers, swapKeyboard
#include "param.hpp" // Para Param, param, ParamGame, ParamTitle, ParamFade
#include "player.hpp" // Para Player
#include "resource.hpp" // Para Resource
#include "screen.hpp" // Para Screen
#include "section.hpp" // Para Name, name, Options, options, AttractMode, attract_mode
#include "sprite.hpp" // Para Sprite
#include "text.hpp" // Para Text
#include "tiled_bg.hpp" // Para TiledBG, TiledBGMode
#include "ui/notifier.hpp" // Para Notifier
#include "ui/service_menu.hpp" // Para ServiceMenu
#include "utils.hpp" // Para Zone, BLOCK
class Texture;
// Constructor
Title::Title()
: text_(Resource::get()->getText("smb2_grad")),
fade_(std::make_unique<Fade>()),
tiled_bg_(std::make_unique<TiledBG>(param.game.game_area.rect, TiledBGMode::RANDOM)),
game_logo_(std::make_unique<GameLogo>(param.game.game_area.center_x, param.title.title_c_c_position)),
mini_logo_sprite_(std::make_unique<Sprite>(Resource::get()->getTexture("logo_jailgames_mini.png"))),
state_(State::LOGO_ANIMATING),
num_controllers_(Input::get()->getNumGamepads()) {
// Configura objetos
tiled_bg_->setColor(param.title.bg_color);
tiled_bg_->setSpeed(0.0F);
game_logo_->enable();
mini_logo_sprite_->setX(param.game.game_area.center_x - (mini_logo_sprite_->getWidth() / 2));
fade_->setColor(param.fade.color);
fade_->setType(Fade::Type::RANDOM_SQUARE2);
fade_->setPostDuration(param.fade.post_duration_ms);
initPlayers();
// Asigna valores a otras variables
Section::options = Section::Options::TITLE_1;
const bool IS_TITLE_TO_DEMO = (Section::attract_mode == Section::AttractMode::TITLE_TO_DEMO);
next_section_ = IS_TITLE_TO_DEMO ? Section::Name::GAME_DEMO : Section::Name::LOGO;
Section::attract_mode = IS_TITLE_TO_DEMO ? Section::AttractMode::TITLE_TO_LOGO : Section::AttractMode::TITLE_TO_DEMO;
// Define los anclajes de los elementos
anchor_.mini_logo = (param.game.height / MINI_LOGO_Y_DIVISOR * MINI_LOGO_Y_FACTOR) + BLOCK;
mini_logo_sprite_->setY(anchor_.mini_logo);
anchor_.copyright_text = anchor_.mini_logo + mini_logo_sprite_->getHeight() + COPYRIGHT_TEXT_SPACING;
// Inicializa el timer de delta time para el primer frame del callback
last_time_ = SDL_GetTicks();
}
// Destructor
Title::~Title() {
Audio::get()->stopAllSounds();
if (Section::name == Section::Name::LOGO) {
Audio::get()->fadeOutMusic(MUSIC_FADE_OUT_SHORT_MS);
}
// Desregistra los jugadores de Options
Options::keyboard.clearPlayers();
Options::gamepad_manager.clearPlayers();
}
// Actualiza las variables del objeto
void Title::update(float delta_time) {
static auto* const SCREEN = Screen::get();
SCREEN->update(delta_time); // Actualiza el objeto screen
Audio::update(); // Actualiza el objeto audio
updateFade();
updateState(delta_time);
updateStartPrompt(delta_time);
updatePlayers(delta_time);
}
// Calcula el tiempo transcurrido desde el último frame
auto Title::calculateDeltaTime() -> float {
const Uint64 CURRENT_TIME = SDL_GetTicks();
const float DELTA_TIME = static_cast<float>(CURRENT_TIME - last_time_) / 1000.0F; // Convert ms to seconds
last_time_ = CURRENT_TIME;
return DELTA_TIME;
}
// Dibuja el objeto en pantalla
void Title::render() {
static auto* const SCREEN = Screen::get();
SCREEN->start();
SCREEN->clean();
tiled_bg_->render();
game_logo_->render();
renderPlayers();
renderStartPrompt();
renderCopyright();
fade_->render();
SCREEN->render();
}
// Comprueba los eventos
void Title::checkEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_EVENT_KEY_DOWN) {
handleKeyDownEvent(event);
}
GlobalEvents::handle(event);
}
}
void Title::handleKeyDownEvent(const SDL_Event& /*event*/) {
}
// Comprueba las entradas
void Title::checkInput() {
Input::get()->update();
if (!ServiceMenu::get()->isEnabled()) {
processControllerInputs();
processKeyboardStart();
}
GlobalInputs::check();
}
void Title::processKeyboardStart() {
if (!canProcessStartButton()) {
return;
}
const bool START_PRESSED = Input::get()->checkAction(
Input::Action::START,
Input::DO_NOT_ALLOW_REPEAT,
Input::CHECK_KEYBOARD);
if (START_PRESSED) {
switch (Options::keyboard.player_id) {
case Player::Id::PLAYER1:
processPlayer1Start();
break;
case Player::Id::PLAYER2:
processPlayer2Start();
break;
default:
break;
}
}
}
void Title::processControllerInputs() {
for (const auto& controller : Options::gamepad_manager) {
if (isStartButtonPressed(&controller)) {
handleStartButtonPress(&controller);
}
}
}
auto Title::isStartButtonPressed(const Options::Gamepad* controller) -> bool {
return Input::get()->checkAction(
Input::Action::START,
Input::DO_NOT_ALLOW_REPEAT,
Input::DO_NOT_CHECK_KEYBOARD,
controller->instance);
}
void Title::handleStartButtonPress(const Options::Gamepad* controller) {
if (!canProcessStartButton()) {
return;
}
if (controller->player_id == Player::Id::PLAYER1) {
processPlayer1Start();
} else if (controller->player_id == Player::Id::PLAYER2) {
processPlayer2Start();
}
}
auto Title::canProcessStartButton() const -> bool {
return (state_ != State::LOGO_ANIMATING || ALLOW_TITLE_ANIMATION_SKIP);
}
void Title::processPlayer1Start() {
if (!player1_start_pressed_) {
player1_start_pressed_ = true;
activatePlayerAndSetState(Player::Id::PLAYER1);
}
}
void Title::processPlayer2Start() {
if (!player2_start_pressed_) {
player2_start_pressed_ = true;
activatePlayerAndSetState(Player::Id::PLAYER2);
}
}
void Title::activatePlayerAndSetState(Player::Id player_id) {
getPlayer(player_id)->setPlayingState(Player::State::TITLE_ANIMATION);
setState(State::START_HAS_BEEN_PRESSED);
counter_time_ = 0.0F;
}
// Avanza un frame (llamado desde Director::iterate)
void Title::iterate() {
const float DELTA_TIME = calculateDeltaTime();
checkInput();
update(DELTA_TIME);
render();
}
// Procesa un evento (llamado desde Director::handleEvent)
void Title::handleEvent(const SDL_Event& event) {
if (event.type == SDL_EVENT_KEY_DOWN) {
handleKeyDownEvent(event);
}
}
// Bucle para el titulo del juego (fallback legacy)
void Title::run() {
last_time_ = SDL_GetTicks();
while (Section::name == Section::Name::TITLE) {
const float DELTA_TIME = calculateDeltaTime();
checkInput();
update(DELTA_TIME);
checkEvents(); // Tiene que ir antes del render
render();
}
}
// Reinicia el contador interno
void Title::resetCounter() { counter_time_ = 0.0F; }
// Intercambia la asignación de mandos a los jugadores
void Title::swapControllers() {
if (Input::get()->getNumGamepads() == 0) {
return;
}
Options::swapControllers();
showControllers();
}
// Intercambia el teclado de jugador
void Title::swapKeyboard() {
Options::swapKeyboard();
std::string text = Lang::getText("[DEFINE_BUTTONS] PLAYER") + std::to_string(static_cast<int>(Options::getPlayerWhoUsesKeyboard())) + ": " + Lang::getText("[DEFINE_BUTTONS] KEYBOARD");
Notifier::get()->show({text});
}
// Muestra información sobre los controles y los jugadores
void Title::showControllers() {
// Crea los textos
std::string text1 = Lang::getText("[DEFINE_BUTTONS] PLAYER") + std::to_string(static_cast<int>(Player::Id::PLAYER1)) + ": " + Options::gamepad_manager.getGamepad(Player::Id::PLAYER1).name;
std::string text2 = Lang::getText("[DEFINE_BUTTONS] PLAYER") + std::to_string(static_cast<int>(Player::Id::PLAYER2)) + ": " + Options::gamepad_manager.getGamepad(Player::Id::PLAYER2).name;
// Muestra la notificación
Notifier::get()->show({text1, text2});
}
// Actualiza el fade
void Title::updateFade() {
fade_->update();
if (fade_->hasEnded()) {
const int COMBO = (player1_start_pressed_ ? 1 : 0) | (player2_start_pressed_ ? 2 : 0);
switch (COMBO) {
case 0: // Ningún jugador ha pulsado Start
Section::name = next_section_;
break;
case 1: // Solo el jugador 1 ha pulsado Start
Section::name = Section::Name::GAME;
Section::options = Section::Options::GAME_PLAY_1P;
Audio::get()->stopMusic();
break;
case 2: // Solo el jugador 2 ha pulsado Start
Section::name = Section::Name::GAME;
Section::options = Section::Options::GAME_PLAY_2P;
Audio::get()->stopMusic();
break;
case 3: // Ambos jugadores han pulsado Start
Section::name = Section::Name::GAME;
Section::options = Section::Options::GAME_PLAY_BOTH;
Audio::get()->stopMusic();
break;
}
}
}
// Actualiza el estado
void Title::updateState(float delta_time) {
game_logo_->update(delta_time);
tiled_bg_->update(delta_time);
// Establece la lógica según el estado
switch (state_) {
case State::LOGO_ANIMATING: {
if (game_logo_->hasFinished()) {
setState(State::LOGO_FINISHED);
}
break;
}
case State::LOGO_FINISHED: {
counter_time_ += delta_time;
if (counter_time_ >= param.title.title_duration) {
// El menu ha hecho time out
fade_->setPostDuration(0);
fade_->activate();
selection_ = Section::Options::TITLE_TIME_OUT;
}
break;
}
case State::START_HAS_BEEN_PRESSED: {
counter_time_ += delta_time;
if (counter_time_ >= START_PRESSED_DELAY_S) {
fade_->activate();
}
break;
}
default:
break;
}
}
void Title::updateStartPrompt(float delta_time) {
blink_accumulator_ += delta_time;
bool condition_met = false;
float period = 0.0F;
float on_time = 0.0F;
switch (state_) {
case State::LOGO_FINISHED:
period = LOGO_BLINK_PERIOD_S;
on_time = LOGO_BLINK_ON_TIME_S;
break;
case State::START_HAS_BEEN_PRESSED:
period = START_BLINK_PERIOD_S;
on_time = START_BLINK_ON_TIME_S;
break;
default:
break;
}
if (period > 0.0F) {
// Reset accumulator when it exceeds the period
if (blink_accumulator_ >= period) {
blink_accumulator_ -= period;
}
// Check if we're in the "on" time of the blink cycle
condition_met = blink_accumulator_ >= (period - on_time);
}
should_render_start_prompt_ = condition_met;
}
void Title::renderStartPrompt() {
if (should_render_start_prompt_) {
text_->writeDX(Text::CENTER | Text::SHADOW,
param.game.game_area.center_x,
param.title.press_start_position,
Lang::getText("[TITLE] PRESS_BUTTON_TO_PLAY"),
1,
Colors::NO_COLOR_MOD,
1,
Colors::TITLE_SHADOW_TEXT);
}
}
void Title::renderCopyright() {
if (state_ != State::LOGO_ANIMATING) {
// Mini logo
mini_logo_sprite_->render();
// Texto con el copyright
text_->writeDX(Text::CENTER | Text::SHADOW,
param.game.game_area.center_x,
anchor_.copyright_text,
std::string(TEXT_COPYRIGHT),
1,
Colors::NO_COLOR_MOD,
1,
Colors::TITLE_SHADOW_TEXT);
}
}
// Cambia el estado
void Title::setState(State state) {
if (state_ == state) {
return;
}
state_ = state;
switch (state_) {
case State::LOGO_ANIMATING:
break;
case State::LOGO_FINISHED:
Audio::get()->playMusic("title.ogg");
tiled_bg_->changeSpeedTo(60.0F, 0.5F);
blink_accumulator_ = 0.0F; // Resetea el timer para empezar el parpadeo desde el inicio
break;
case State::START_HAS_BEEN_PRESSED:
Audio::get()->fadeOutMusic(MUSIC_FADE_OUT_LONG_MS);
blink_accumulator_ = 0.0F; // Resetea el timer para empezar el parpadeo desde el inicio
break;
}
}
// Inicializa los jugadores
void Title::initPlayers() {
std::vector<std::vector<std::shared_ptr<Texture>>> player_textures; // Vector con todas las texturas de los jugadores;
std::vector<std::vector<std::string>> player1_animations; // Vector con las animaciones del jugador 1
std::vector<std::vector<std::string>> player2_animations; // Vector con las animaciones del jugador 2
// Texturas - Player1
std::vector<std::shared_ptr<Texture>> player1_textures;
player1_textures.emplace_back(Resource::get()->getTexture("player1_pal0"));
player1_textures.emplace_back(Resource::get()->getTexture("player1_pal1"));
player1_textures.emplace_back(Resource::get()->getTexture("player1_pal2"));
player1_textures.emplace_back(Resource::get()->getTexture("player1_pal3"));
player1_textures.emplace_back(Resource::get()->getTexture("player1_power.png"));
player_textures.push_back(player1_textures);
// Texturas - Player2
std::vector<std::shared_ptr<Texture>> player2_textures;
player2_textures.emplace_back(Resource::get()->getTexture("player2_pal0"));
player2_textures.emplace_back(Resource::get()->getTexture("player2_pal1"));
player2_textures.emplace_back(Resource::get()->getTexture("player2_pal2"));
player2_textures.emplace_back(Resource::get()->getTexture("player2_pal3"));
player2_textures.emplace_back(Resource::get()->getTexture("player2_power.png"));
player_textures.push_back(player2_textures);
// Animaciones -- Jugador
player1_animations.emplace_back(Resource::get()->getAnimation("player1.ani"));
player1_animations.emplace_back(Resource::get()->getAnimation("player_power.ani"));
player2_animations.emplace_back(Resource::get()->getAnimation("player2.ani"));
player2_animations.emplace_back(Resource::get()->getAnimation("player_power.ani"));
// Crea los dos jugadores
const int Y = param.title.press_start_position - (Player::HEIGHT / 2);
constexpr bool DEMO = false;
Player::Config config_player1;
config_player1.id = Player::Id::PLAYER1;
config_player1.x = param.game.game_area.center_x - (Player::WIDTH / 2);
config_player1.y = Y;
config_player1.demo = DEMO;
config_player1.play_area = &param.game.play_area.rect;
config_player1.texture = player_textures.at(0);
config_player1.animations = player1_animations;
config_player1.hi_score_table = &Options::settings.hi_score_table;
config_player1.glowing_entry = &Options::settings.glowing_entries.at(static_cast<int>(Player::Id::PLAYER1) - 1);
players_.emplace_back(std::make_unique<Player>(config_player1));
players_.back()->setPlayingState(Player::State::TITLE_HIDDEN);
Player::Config config_player2;
config_player2.id = Player::Id::PLAYER2;
config_player2.x = param.game.game_area.center_x - (Player::WIDTH / 2);
config_player2.y = Y;
config_player2.demo = DEMO;
config_player2.play_area = &param.game.play_area.rect;
config_player2.texture = player_textures.at(1);
config_player2.animations = player2_animations;
config_player2.hi_score_table = &Options::settings.hi_score_table;
config_player2.glowing_entry = &Options::settings.glowing_entries.at(static_cast<int>(Player::Id::PLAYER2) - 1);
players_.emplace_back(std::make_unique<Player>(config_player2));
players_.back()->setPlayingState(Player::State::TITLE_HIDDEN);
// Registra los jugadores en Options
for (const auto& player : players_) {
Options::keyboard.addPlayer(player);
Options::gamepad_manager.addPlayer(player);
}
}
// Actualiza los jugadores
void Title::updatePlayers(float delta_time) {
for (auto& player : players_) {
player->update(delta_time);
}
}
// Renderiza los jugadores
void Title::renderPlayers() {
for (auto const& player : players_) {
player->render();
}
}
// Obtiene un jugador a partir de su "id"
auto Title::getPlayer(Player::Id id) -> std::shared_ptr<Player> {
auto it = std::ranges::find_if(players_, [id](const auto& player) -> auto { return player->getId() == id; });
if (it != players_.end()) {
return *it;
}
return nullptr;
}

View File

@@ -0,0 +1,142 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_Keycode, SDL_Event, Uint64
#include <memory> // Para shared_ptr, unique_ptr
#include <string_view> // Para string_view
#include <vector> // Para vector
#include "player.hpp" // for Player
#include "section.hpp" // for Options, Name (ptr only)
class Fade;
class GameLogo;
class Sprite;
class Text;
class TiledBG;
namespace Options {
struct Gamepad;
} // namespace Options
// --- Clase Title: pantalla de título y menú principal del juego ---
//
// Esta clase gestiona la pantalla de título del juego, incluyendo el menú principal
// y la transición entre diferentes modos de juego.
//
// Funcionalidades principales:
// • Logo animado: muestra y anima el logotipo principal del juego
// • Selección de jugadores: permite iniciar partidas de 1 o 2 jugadores
// • Modo attract: cicla automáticamente entre título y demo
// • Efectos visuales: parpadeos, transiciones y efectos de fondo
// • Gestión de controles: soporte para teclado y múltiples gamepads
// • Timeouts automáticos: transición automática si no hay interacción
//
// La clase utiliza un sistema de tiempo basado en segundos para garantizar
// comportamiento consistente independientemente del framerate.
class Title {
public:
// --- Constructor y destructor ---
Title();
~Title();
// --- Callbacks para el bucle SDL_MAIN_USE_CALLBACKS ---
void iterate(); // Ejecuta un frame
void handleEvent(const SDL_Event& event); // Procesa un evento
// --- Bucle principal legacy (fallback) ---
void run();
private:
// --- Constantes de tiempo (en segundos) ---
static constexpr float START_PRESSED_DELAY_S = 1666.67F / 1000.0F; // Tiempo antes de fade tras pulsar start (100 frames a 60fps)
static constexpr int MUSIC_FADE_OUT_LONG_MS = 1500; // Fade out largo de música
static constexpr int MUSIC_FADE_OUT_SHORT_MS = 300; // Fade out corto de música
// --- Constantes de parpadeo (en segundos) ---
static constexpr float LOGO_BLINK_PERIOD_S = 833.0F / 1000.0F; // Período de parpadeo del logo (833ms)
static constexpr float LOGO_BLINK_ON_TIME_S = 583.0F / 1000.0F; // Tiempo encendido del logo (583ms)
static constexpr float START_BLINK_PERIOD_S = 167.0F / 1000.0F; // Período de parpadeo del start (167ms)
static constexpr float START_BLINK_ON_TIME_S = 83.0F / 1000.0F; // Tiempo encendido del start (83ms)
// --- Constantes de layout ---
static constexpr int MINI_LOGO_Y_DIVISOR = 5; // Divisor para posición Y del mini logo
static constexpr int MINI_LOGO_Y_FACTOR = 4; // Factor para posición Y del mini logo
static constexpr int COPYRIGHT_TEXT_SPACING = 3; // Espaciado del texto de copyright
// --- Constantes de texto y configuración ---
static constexpr std::string_view TEXT_COPYRIGHT = "@2020,2025 JailDesigner"; // Texto de copyright
static constexpr bool ALLOW_TITLE_ANIMATION_SKIP = false; // Permite saltar la animación del título
// --- Enums ---
enum class State {
LOGO_ANIMATING, // El logo está animándose
LOGO_FINISHED, // El logo ha terminado de animarse
START_HAS_BEEN_PRESSED, // Se ha pulsado el botón de start
};
// --- Estructuras privadas ---
struct Anchor {
int mini_logo; // Ancla del logo mini
int copyright_text; // Ancla del texto de copyright
};
// --- Objetos y punteros ---
std::shared_ptr<Text> text_; // Objeto de texto para escribir en pantalla
std::unique_ptr<Fade> fade_; // Fundido en pantalla
std::unique_ptr<TiledBG> tiled_bg_; // Fondo animado de tiles
std::unique_ptr<GameLogo> game_logo_; // Logo del juego
std::unique_ptr<Sprite> mini_logo_sprite_; // Logo JailGames mini
std::vector<std::shared_ptr<Player>> players_; // Vector de jugadores
// --- Variables de estado ---
Anchor anchor_; // Anclas para definir la posición de los elementos del título
Section::Name next_section_; // Siguiente sección a cargar
Section::Options selection_ = Section::Options::TITLE_TIME_OUT; // Opción elegida en el título
State state_; // Estado actual de la sección
Uint64 last_time_ = 0; // Último timestamp para calcular delta-time
float counter_time_ = 0.0F; // Temporizador para la pantalla de título (en segundos)
float blink_accumulator_ = 0.0F; // Acumulador para el parpadeo (en segundos)
int num_controllers_; // Número de mandos conectados
bool should_render_start_prompt_ = false; // Indica si se muestra el texto de PRESS START BUTTON TO PLAY
bool player1_start_pressed_ = false; // Indica si se ha pulsado el botón de empezar para el jugador 1
bool player2_start_pressed_ = false; // Indica si se ha pulsado el botón de empezar para el jugador 2
// --- Ciclo de vida del título ---
void update(float delta_time); // Actualiza las variables del objeto
auto calculateDeltaTime() -> float; // Calcula el tiempo transcurrido desde el último frame
void updateState(float delta_time); // Actualiza el estado actual del título
void setState(State state); // Cambia el estado del título
void resetCounter(); // Reinicia el contador interno
// --- Entrada de usuario ---
void checkEvents(); // Comprueba los eventos
void checkInput(); // Comprueba las entradas
void handleKeyDownEvent(const SDL_Event& event); // Maneja el evento de tecla presionada
void processKeyboardStart(); // Procesa las entradas del teclado
void processControllerInputs(); // Procesa las entradas de los mandos
[[nodiscard]] static auto isStartButtonPressed(const Options::Gamepad* controller) -> bool; // Comprueba si se ha pulsado el botón Start
void handleStartButtonPress(const Options::Gamepad* controller); // Maneja la pulsación del botón Start
[[nodiscard]] auto canProcessStartButton() const -> bool; // Verifica si se puede procesar la pulsación del botón Start
void processPlayer1Start(); // Procesa el inicio del jugador 1
void processPlayer2Start(); // Procesa el inicio del jugador 2
void activatePlayerAndSetState(Player::Id player_id); // Activa al jugador y cambia el estado del título
// --- Gestión de jugadores ---
void initPlayers(); // Inicializa los jugadores
void updatePlayers(float delta_time); // Actualiza los jugadores
void renderPlayers(); // Renderiza los jugadores
auto getPlayer(Player::Id id) -> std::shared_ptr<Player>; // Obtiene un jugador a partir de su "id"
// --- Visualización / Renderizado ---
void render(); // Dibuja el objeto en pantalla
void updateFade(); // Actualiza el efecto de fundido (fade in/out)
void updateStartPrompt(float delta_time); // Actualiza el mensaje de "Pulsa Start"
void renderStartPrompt(); // Dibuja el mensaje de "Pulsa Start" en pantalla
void renderCopyright(); // Dibuja el aviso de copyright
// --- Utilidades estáticas ---
static void swapControllers(); // Intercambia la asignación de mandos a los jugadores
static void swapKeyboard(); // Intercambia el teclado de jugador
static void showControllers(); // Muestra información sobre los controles y los jugadores
};

View File

@@ -0,0 +1,74 @@
#include "menu_option.hpp"
#include <algorithm> // Para max
#include <iterator> // Para distance
#include <ranges> // Para __find_fn, find
#include "text.hpp" // Para Text
auto ActionListOption::getValueAsString() const -> std::string {
if (value_getter_) {
return value_getter_();
}
if (current_index_ < options_.size()) {
return options_[current_index_];
}
return options_.empty() ? "" : options_[0];
}
auto ActionListOption::getMaxValueWidth(Text* text) const -> int {
int max_width = 0;
for (const auto& option : options_) {
int width = text->length(option, -2);
max_width = std::max(width, max_width);
}
return max_width;
}
void ActionListOption::adjustValue(bool up) {
if (options_.empty()) {
return;
}
if (up) {
current_index_ = (current_index_ + 1) % options_.size();
} else {
current_index_ = (current_index_ == 0) ? options_.size() - 1 : current_index_ - 1;
}
// Aplicar el cambio usando el setter
if (value_setter_ && current_index_ < options_.size()) {
value_setter_(options_[current_index_]);
}
}
void ActionListOption::executeAction() {
if (action_executor_) {
action_executor_();
}
}
void ActionListOption::sync() {
updateCurrentIndex();
}
void ActionListOption::updateCurrentIndex() {
current_index_ = findCurrentIndex();
}
auto ActionListOption::findCurrentIndex() const -> size_t {
if (!value_getter_ || options_.empty()) {
return 0;
}
const std::string CURRENT_VALUE = value_getter_();
auto it = std::ranges::find(options_, CURRENT_VALUE);
if (it != options_.end()) {
return static_cast<size_t>(std::distance(options_.begin(), it));
}
return 0; // Valor por defecto si no se encuentra
}

View File

@@ -0,0 +1,241 @@
#pragma once
#include <algorithm> // Para max, clamp
#include <cstddef> // Para size_t
#include <functional> // Para function
#include <string> // Para allocator, string, basic_string, to_string, operator==, char_traits
#include <utility> // Para move
#include <vector> // Para vector
#include "lang.hpp" // Para getText
#include "text.hpp" // Para Text
#include "ui/service_menu.hpp" // Para ServiceMenu
// --- Clase MenuOption: interfaz base para todas las opciones del menú ---
class MenuOption {
public:
// --- Enums ---
enum class Behavior {
ADJUST, // Solo puede ajustar valor (como IntOption, BoolOption, ListOption)
SELECT, // Solo puede ejecutar acción (como ActionOption, FolderOption)
BOTH // Puede tanto ajustar como ejecutar acción (como ActionListOption)
};
// --- Constructor y destructor ---
MenuOption(std::string caption, ServiceMenu::SettingsGroup group, bool hidden = false)
: caption_(std::move(caption)),
group_(group),
hidden_(hidden) {}
virtual ~MenuOption() = default;
// --- Getters ---
[[nodiscard]] auto getCaption() const -> const std::string& { return caption_; }
[[nodiscard]] auto getGroup() const -> ServiceMenu::SettingsGroup { return group_; }
[[nodiscard]] auto isHidden() const -> bool { return hidden_; }
void setHidden(bool hidden) { hidden_ = hidden; }
[[nodiscard]] virtual auto getBehavior() const -> Behavior = 0;
[[nodiscard]] virtual auto getValueAsString() const -> std::string { return ""; }
virtual void adjustValue(bool adjust_up) {}
[[nodiscard]] virtual auto getTargetGroup() const -> ServiceMenu::SettingsGroup { return ServiceMenu::SettingsGroup::MAIN; }
virtual void executeAction() {}
virtual auto getMaxValueWidth(Text* text_renderer) const -> int { return 0; } // Método virtual para que cada opción calcule el ancho de su valor más largo
protected:
// --- Variables ---
std::string caption_;
ServiceMenu::SettingsGroup group_;
bool hidden_;
};
// --- Clases Derivadas ---
class BoolOption : public MenuOption {
public:
BoolOption(const std::string& cap, ServiceMenu::SettingsGroup grp, bool* var)
: MenuOption(cap, grp),
linked_variable_(var) {}
[[nodiscard]] auto getBehavior() const -> Behavior override { return Behavior::ADJUST; }
[[nodiscard]] auto getValueAsString() const -> std::string override {
return *linked_variable_ ? Lang::getText("[SERVICE_MENU] ON") : Lang::getText("[SERVICE_MENU] OFF");
}
void adjustValue(bool /*adjust_up*/) override {
*linked_variable_ = !*linked_variable_;
}
auto getMaxValueWidth(Text* text_renderer) const -> int override {
return std::max(
text_renderer->length(Lang::getText("[SERVICE_MENU] ON"), -2),
text_renderer->length(Lang::getText("[SERVICE_MENU] OFF"), -2));
}
private:
bool* linked_variable_;
};
class IntOption : public MenuOption {
public:
IntOption(const std::string& cap, ServiceMenu::SettingsGroup grp, int* var, int min, int max, int step)
: MenuOption(cap, grp),
linked_variable_(var),
min_value_(min),
max_value_(max),
step_value_(step) {}
[[nodiscard]] auto getBehavior() const -> Behavior override { return Behavior::ADJUST; }
[[nodiscard]] auto getValueAsString() const -> std::string override { return std::to_string(*linked_variable_); }
void adjustValue(bool adjust_up) override {
int new_value = *linked_variable_ + (adjust_up ? step_value_ : -step_value_);
*linked_variable_ = std::clamp(new_value, min_value_, max_value_);
}
auto getMaxValueWidth(Text* text_renderer) const -> int override {
int max_width = 0;
// Iterar por todos los valores posibles en el rango
for (int value = min_value_; value <= max_value_; value += step_value_) {
int width = text_renderer->length(std::to_string(value), -2);
max_width = std::max(max_width, width);
}
return max_width;
}
private:
int* linked_variable_;
int min_value_, max_value_, step_value_;
};
class ListOption : public MenuOption {
public:
ListOption(const std::string& cap, ServiceMenu::SettingsGroup grp, std::vector<std::string> values, std::function<std::string()> current_value_getter, std::function<void(const std::string&)> new_value_setter)
: MenuOption(cap, grp),
value_list_(std::move(values)),
getter_(std::move(current_value_getter)),
setter_(std::move(new_value_setter)) {
sync();
}
void sync() {
std::string current_value = getter_();
for (size_t i = 0; i < value_list_.size(); ++i) {
if (value_list_[i] == current_value) {
list_index_ = i;
return;
}
}
}
[[nodiscard]] auto getBehavior() const -> Behavior override { return Behavior::ADJUST; }
[[nodiscard]] auto getValueAsString() const -> std::string override {
return value_list_.empty() ? "" : value_list_[list_index_];
}
void adjustValue(bool adjust_up) override {
if (value_list_.empty()) {
return;
}
size_t size = value_list_.size();
list_index_ = adjust_up ? (list_index_ + 1) % size
: (list_index_ + size - 1) % size;
setter_(value_list_[list_index_]);
}
auto getMaxValueWidth(Text* text_renderer) const -> int override {
int max_w = 0;
for (const auto& val : value_list_) {
max_w = std::max(max_w, text_renderer->length(val, -2));
}
return max_w;
}
private:
std::vector<std::string> value_list_;
std::function<std::string()> getter_;
std::function<void(const std::string&)> setter_;
size_t list_index_{0};
};
class FolderOption : public MenuOption {
public:
FolderOption(const std::string& cap, ServiceMenu::SettingsGroup grp, ServiceMenu::SettingsGroup target)
: MenuOption(cap, grp),
target_group_(target) {}
[[nodiscard]] auto getBehavior() const -> Behavior override { return Behavior::SELECT; }
[[nodiscard]] auto getTargetGroup() const -> ServiceMenu::SettingsGroup override { return target_group_; }
private:
ServiceMenu::SettingsGroup target_group_;
};
class ActionOption : public MenuOption {
public:
ActionOption(const std::string& cap, ServiceMenu::SettingsGroup grp, std::function<void()> action, bool hidden = false)
: MenuOption(cap, grp, hidden),
action_(std::move(action)) {}
[[nodiscard]] auto getBehavior() const -> Behavior override { return Behavior::SELECT; }
void executeAction() override {
if (action_) {
action_();
}
}
private:
std::function<void()> action_;
};
// Opción de lista con acción
class ActionListOption : public MenuOption {
public:
using ValueGetter = std::function<std::string()>;
using ValueSetter = std::function<void(const std::string&)>;
using ActionExecutor = std::function<void()>;
ActionListOption(const std::string& caption, ServiceMenu::SettingsGroup group, std::vector<std::string> options, ValueGetter getter, ValueSetter setter, ActionExecutor action_executor, bool hidden = false)
: MenuOption(caption, group, hidden),
options_(std::move(options)),
value_getter_(std::move(getter)),
value_setter_(std::move(setter)),
action_executor_(std::move(action_executor)) {
updateCurrentIndex();
}
[[nodiscard]] auto getBehavior() const -> Behavior override { return Behavior::BOTH; }
[[nodiscard]] auto getValueAsString() const -> std::string override;
[[nodiscard]] auto getMaxValueWidth(Text* text) const -> int override;
void adjustValue(bool up) override;
void executeAction() override;
void sync(); // Sincroniza con el valor actual
private:
std::vector<std::string> options_;
ValueGetter value_getter_;
ValueSetter value_setter_;
ActionExecutor action_executor_;
size_t current_index_{0};
void updateCurrentIndex();
[[nodiscard]] auto findCurrentIndex() const -> size_t;
};
// Opción genérica con callbacks: getter para mostrar, adjuster(bool up) para cambiar valor
class CallbackOption : public MenuOption {
public:
CallbackOption(const std::string& cap, ServiceMenu::SettingsGroup grp, std::function<std::string()> getter, std::function<void(bool)> adjuster, std::function<int(Text*)> max_width_fn = nullptr)
: MenuOption(cap, grp),
getter_(std::move(getter)),
adjuster_(std::move(adjuster)),
max_width_fn_(std::move(max_width_fn)) {}
[[nodiscard]] auto getBehavior() const -> Behavior override { return Behavior::ADJUST; }
[[nodiscard]] auto getValueAsString() const -> std::string override { return getter_(); }
void adjustValue(bool adjust_up) override { adjuster_(adjust_up); }
auto getMaxValueWidth(Text* text_renderer) const -> int override {
return max_width_fn_ ? max_width_fn_(text_renderer) : 0;
}
private:
std::function<std::string()> getter_;
std::function<void(bool)> adjuster_;
std::function<int(Text*)> max_width_fn_;
};

View File

@@ -0,0 +1,417 @@
#include "menu_renderer.hpp"
#include <algorithm>
#include <utility>
#include "color.hpp"
#include "menu_option.hpp"
#include "param.hpp"
#include "screen.hpp"
#include "text.hpp"
#include "utils.hpp"
// --- Implementación de las estructuras de animación ---
void MenuRenderer::ResizeAnimation::start(float from_w, float from_h, float to_w, float to_h) {
start_width = from_w;
start_height = from_h;
target_width = to_w;
target_height = to_h;
elapsed = 0.0F;
active = true;
}
void MenuRenderer::ResizeAnimation::stop() {
active = false;
elapsed = 0.0F;
}
void MenuRenderer::ShowHideAnimation::startShow(float to_w, float to_h) {
type = Type::SHOWING;
target_width = to_w;
target_height = to_h;
elapsed = 0.0F;
active = true;
}
void MenuRenderer::ShowHideAnimation::startHide() {
type = Type::HIDING;
elapsed = 0.0F;
active = true;
}
void MenuRenderer::ShowHideAnimation::stop() {
type = Type::NONE;
active = false;
elapsed = 0.0F;
}
MenuRenderer::MenuRenderer(const ServiceMenu* menu_state, std::shared_ptr<Text> element_text, std::shared_ptr<Text> title_text)
: element_text_(std::move(element_text)),
title_text_(std::move(title_text)) {
initializeMaxSizes();
setPosition(param.game.game_area.center_x, param.game.game_area.center_y, PositionMode::CENTERED);
}
void MenuRenderer::render(const ServiceMenu* menu_state) {
if (!visible_) {
return;
}
// Dibuja la sombra
if (param.service_menu.drop_shadow) {
SDL_FRect shadow_rect = {.x = rect_.x + 5, .y = rect_.y + 5, .w = rect_.w, .h = rect_.h};
SDL_SetRenderDrawColor(Screen::get()->getRenderer(), 0, 0, 0, 64);
SDL_RenderFillRect(Screen::get()->getRenderer(), &shadow_rect);
}
// Dibuja el fondo
SDL_SetRenderDrawColor(Screen::get()->getRenderer(), param.service_menu.bg_color.r, param.service_menu.bg_color.g, param.service_menu.bg_color.b, param.service_menu.bg_color.a);
SDL_RenderFillRect(Screen::get()->getRenderer(), &rect_);
// Dibuja el borde
const Color BORDER_COLOR = param.service_menu.title_color.DARKEN();
SDL_SetRenderDrawColor(Screen::get()->getRenderer(), BORDER_COLOR.r, BORDER_COLOR.g, BORDER_COLOR.b, 255);
SDL_RenderRect(Screen::get()->getRenderer(), &rect_);
SDL_RenderRect(Screen::get()->getRenderer(), &border_rect_);
// Solo renderizar contenido si la animación lo permite
if (shouldShowContent()) {
// Dibuja el título
float y = rect_.y + title_padding_;
title_text_->writeDX(Text::COLOR | Text::CENTER, rect_.x + (rect_.w / 2.0F), y, menu_state->getTitle(), -4, param.service_menu.title_color);
// Dibuja la línea separadora
y = rect_.y + upper_height_;
SDL_SetRenderDrawColor(Screen::get()->getRenderer(), BORDER_COLOR.r, BORDER_COLOR.g, BORDER_COLOR.b, 255);
SDL_RenderLine(Screen::get()->getRenderer(), rect_.x + ServiceMenu::OPTIONS_HORIZONTAL_PADDING, y, rect_.x + rect_.w - ServiceMenu::OPTIONS_HORIZONTAL_PADDING, y);
// Dibuja las opciones
y = options_y_;
const auto& option_pairs = menu_state->getOptionPairs();
for (size_t i = 0; i < option_pairs.size(); ++i) {
const bool IS_SELECTED = (i == menu_state->getSelectedIndex());
const Color& current_color = IS_SELECTED ? param.service_menu.selected_color : param.service_menu.text_color;
if (menu_state->getCurrentGroupAlignment() == ServiceMenu::GroupAlignment::LEFT) {
const int AVAILABLE_WIDTH = rect_.w - (ServiceMenu::OPTIONS_HORIZONTAL_PADDING * 2) - element_text_->length(option_pairs.at(i).first, -2) - ServiceMenu::MIN_GAP_OPTION_VALUE;
std::string truncated_value = getTruncatedValue(option_pairs.at(i).second, AVAILABLE_WIDTH);
element_text_->writeColored(rect_.x + ServiceMenu::OPTIONS_HORIZONTAL_PADDING, y, option_pairs.at(i).first, current_color, -2);
const int X = rect_.x + rect_.w - ServiceMenu::OPTIONS_HORIZONTAL_PADDING - element_text_->length(truncated_value, -2);
element_text_->writeColored(X, y, truncated_value, current_color, -2);
} else {
const int AVAILABLE_WIDTH = rect_.w - (ServiceMenu::OPTIONS_HORIZONTAL_PADDING * 2);
std::string truncated_caption = getTruncatedValue(option_pairs.at(i).first, AVAILABLE_WIDTH);
element_text_->writeDX(Text::CENTER | Text::COLOR, rect_.x + (rect_.w / 2.0F), y, truncated_caption, -2, current_color);
}
y += options_height_ + options_padding_;
}
}
}
void MenuRenderer::update(const ServiceMenu* menu_state, float delta_time) {
updateAnimations(delta_time);
if (visible_) {
updateColorCounter();
param.service_menu.selected_color = getAnimatedSelectedColor();
}
}
// --- Nuevos métodos de control ---
void MenuRenderer::show(const ServiceMenu* menu_state) {
if (visible_) {
return;
}
visible_ = true;
// Calcula el tamaño final y lo usa para la animación
SDL_FRect target_rect = calculateNewRect(menu_state);
// Detener cualquier animación anterior
resize_animation_.stop();
// Iniciar animación de mostrar
show_hide_animation_.startShow(target_rect.w, target_rect.h);
// El tamaño inicial es cero para la animación
rect_.w = 0.0F;
rect_.h = 0.0F;
updatePosition();
}
void MenuRenderer::hide() {
if (!visible_ || show_hide_animation_.type == ShowHideAnimation::Type::HIDING) {
return;
}
// Detener animación de resize si la hubiera
resize_animation_.stop();
// Guardar tamaño actual para la animación de ocultar
show_hide_animation_.target_width = rect_.w;
show_hide_animation_.target_height = rect_.h;
show_hide_animation_.startHide();
}
void MenuRenderer::setPosition(float x, float y, PositionMode mode) {
anchor_x_ = x;
anchor_y_ = y;
position_mode_ = mode;
updatePosition();
}
// --- Métodos de layout ---
void MenuRenderer::onLayoutChanged(const ServiceMenu* menu_state) {
precalculateMenuWidths(menu_state->getAllOptions(), menu_state);
setAnchors(menu_state);
resize(menu_state);
}
void MenuRenderer::setLayout(const ServiceMenu* menu_state) {
precalculateMenuWidths(menu_state->getAllOptions(), menu_state);
setAnchors(menu_state);
setSize(menu_state);
}
void MenuRenderer::initializeMaxSizes() {
max_menu_width_ = static_cast<size_t>(param.game.game_area.rect.w * 0.9F);
max_menu_height_ = static_cast<size_t>(param.game.game_area.rect.h * 0.9F);
}
void MenuRenderer::setAnchors(const ServiceMenu* menu_state) {
size_t max_entries = 0;
for (int i = 0; i < 5; ++i) {
max_entries = std::max(max_entries, menu_state->countOptionsInGroup(static_cast<ServiceMenu::SettingsGroup>(i)));
}
options_height_ = element_text_->getCharacterSize();
options_padding_ = 5;
title_height_ = title_text_->getCharacterSize();
title_padding_ = title_height_ / 2;
upper_height_ = (title_padding_ * 2) + title_height_;
lower_padding_ = (options_padding_ * 3);
lower_height_ = ((max_entries > 0 ? max_entries - 1 : 0) * (options_height_ + options_padding_)) + options_height_ + (lower_padding_ * 2);
width_ = ServiceMenu::MIN_WIDTH;
height_ = upper_height_ + lower_height_;
}
auto MenuRenderer::calculateNewRect(const ServiceMenu* menu_state) -> SDL_FRect {
width_ = std::min(static_cast<size_t>(getMenuWidthForGroup(menu_state->getCurrentGroup())), max_menu_width_);
const auto& display_options = menu_state->getDisplayOptions();
lower_height_ = ((!display_options.empty() ? display_options.size() - 1 : 0) * (options_height_ + options_padding_)) + options_height_ + (lower_padding_ * 2);
height_ = std::min(upper_height_ + lower_height_, max_menu_height_);
SDL_FRect new_rect = {.x = 0, .y = 0, .w = static_cast<float>(width_), .h = static_cast<float>(height_)};
// La posición x, y se establecerá en `updatePosition`
return new_rect;
}
void MenuRenderer::resize(const ServiceMenu* menu_state) {
SDL_FRect new_rect = calculateNewRect(menu_state);
if (rect_.w != new_rect.w || rect_.h != new_rect.h) {
// En lugar de la animación antigua, usamos la nueva
resize_animation_.start(rect_.w, rect_.h, new_rect.w, new_rect.h);
} else {
// Si no hay cambio de tamaño, solo actualizamos la posición
updatePosition();
}
options_y_ = rect_.y + upper_height_ + lower_padding_;
}
void MenuRenderer::setSize(const ServiceMenu* menu_state) {
SDL_FRect new_rect = calculateNewRect(menu_state);
rect_.w = new_rect.w;
rect_.h = new_rect.h;
show_hide_animation_.stop();
resize_animation_.stop();
updatePosition();
options_y_ = rect_.y + upper_height_ + lower_padding_;
border_rect_ = {.x = rect_.x - 1, .y = rect_.y + 1, .w = rect_.w + 2, .h = rect_.h - 2};
}
// --- Métodos de animación y posición ---
void MenuRenderer::updateAnimations(float delta_time) {
if (show_hide_animation_.active) {
updateShowHideAnimation(delta_time);
}
if (resize_animation_.active) {
updateResizeAnimation(delta_time);
}
}
void MenuRenderer::updateShowHideAnimation(float delta_time) {
show_hide_animation_.elapsed += delta_time;
float duration = show_hide_animation_.duration;
if (show_hide_animation_.elapsed >= duration) {
if (show_hide_animation_.type == ShowHideAnimation::Type::SHOWING) {
rect_.w = show_hide_animation_.target_width;
rect_.h = show_hide_animation_.target_height;
} else if (show_hide_animation_.type == ShowHideAnimation::Type::HIDING) {
rect_.w = 0.0F;
rect_.h = 0.0F;
visible_ = false;
}
show_hide_animation_.stop();
updatePosition();
} else {
float progress = easeOut(show_hide_animation_.elapsed / duration);
if (show_hide_animation_.type == ShowHideAnimation::Type::SHOWING) {
rect_.w = show_hide_animation_.target_width * progress;
rect_.h = show_hide_animation_.target_height * progress;
} else if (show_hide_animation_.type == ShowHideAnimation::Type::HIDING) {
rect_.w = show_hide_animation_.target_width * (1.0F - progress);
rect_.h = show_hide_animation_.target_height * (1.0F - progress);
}
updatePosition();
}
options_y_ = rect_.y + upper_height_ + lower_padding_;
}
void MenuRenderer::updateResizeAnimation(float delta_time) {
resize_animation_.elapsed += delta_time;
float duration = resize_animation_.duration;
if (resize_animation_.elapsed >= duration) {
rect_.w = resize_animation_.target_width;
rect_.h = resize_animation_.target_height;
resize_animation_.stop();
updatePosition();
} else {
float progress = easeOut(resize_animation_.elapsed / duration);
rect_.w = resize_animation_.start_width + ((resize_animation_.target_width - resize_animation_.start_width) * progress);
rect_.h = resize_animation_.start_height + ((resize_animation_.target_height - resize_animation_.start_height) * progress);
updatePosition();
}
options_y_ = rect_.y + upper_height_ + lower_padding_;
}
void MenuRenderer::updatePosition() {
switch (position_mode_) {
case PositionMode::CENTERED:
rect_.x = anchor_x_ - (rect_.w / 2.0F);
rect_.y = anchor_y_ - (rect_.h / 2.0F);
break;
case PositionMode::FIXED:
rect_.x = anchor_x_;
rect_.y = anchor_y_;
break;
}
// Actualizar el rectángulo del borde junto con el principal
border_rect_ = {.x = rect_.x - 1, .y = rect_.y + 1, .w = rect_.w + 2, .h = rect_.h - 2};
}
// Resto de métodos (sin cambios significativos)
void MenuRenderer::precalculateMenuWidths(const std::vector<std::unique_ptr<MenuOption>>& all_options, const ServiceMenu* menu_state) { // NOLINT(readability-named-parameter)
for (int& w : group_menu_widths_) {
w = ServiceMenu::MIN_WIDTH;
}
for (int group = 0; group < 5; ++group) {
auto sg = static_cast<ServiceMenu::SettingsGroup>(group);
int max_option_width = 0;
int max_value_width = 0;
for (const auto& option : all_options) {
if (option->getGroup() != sg) {
continue;
}
max_option_width = std::max(max_option_width, element_text_->length(option->getCaption(), -2));
if (menu_state->getCurrentGroupAlignment() == ServiceMenu::GroupAlignment::LEFT) {
// Usar getMaxValueWidth() para considerar TODOS los valores posibles de la opción
int option_max_value_width = option->getMaxValueWidth(element_text_.get());
int max_available_value_width = static_cast<int>(max_menu_width_) - max_option_width - (ServiceMenu::OPTIONS_HORIZONTAL_PADDING * 2) - ServiceMenu::MIN_GAP_OPTION_VALUE;
if (option_max_value_width <= max_available_value_width) {
// Si el valor más largo cabe, usar su ancho real
max_value_width = std::max(max_value_width, option_max_value_width);
} else {
// Si no cabe, usar el ancho disponible (será truncado)
max_value_width = std::max(max_value_width, max_available_value_width);
}
}
}
size_t total_width = max_option_width + (ServiceMenu::OPTIONS_HORIZONTAL_PADDING * 2);
if (menu_state->getCurrentGroupAlignment() == ServiceMenu::GroupAlignment::LEFT) {
total_width += ServiceMenu::MIN_GAP_OPTION_VALUE + max_value_width;
}
group_menu_widths_[group] = std::min(std::max(static_cast<int>(ServiceMenu::MIN_WIDTH), static_cast<int>(total_width)), static_cast<int>(max_menu_width_));
}
}
auto MenuRenderer::getMenuWidthForGroup(ServiceMenu::SettingsGroup group) const -> int { return group_menu_widths_[static_cast<int>(group)]; }
void MenuRenderer::updateColorCounter() {
static Uint64 last_update_ = SDL_GetTicks();
if (SDL_GetTicks() - last_update_ >= 50) {
color_counter_++;
last_update_ = SDL_GetTicks();
}
}
auto MenuRenderer::getAnimatedSelectedColor() const -> Color {
static auto color_cycle_ = Colors::generateMirroredCycle(param.service_menu.selected_color, ColorCycleStyle::HUE_WAVE);
return color_cycle_.at(color_counter_ % color_cycle_.size());
}
auto MenuRenderer::setRect(SDL_FRect rect) -> SDL_FRect {
border_rect_ = {.x = rect.x - 1, .y = rect.y + 1, .w = rect.w + 2, .h = rect.h - 2};
return rect;
}
auto MenuRenderer::getTruncatedValueWidth(const std::string& value, int available_width) const -> int {
int value_width = element_text_->length(value, -2);
if (value_width <= available_width) {
return value_width;
}
// Calculamos cuántos caracteres podemos mostrar más los puntos suspensivos
// Estimamos el ancho de los puntos suspensivos como 3 caracteres promedio
int ellipsis_width = element_text_->length("...", -2);
int available_for_text = available_width - ellipsis_width;
if (available_for_text <= 0) {
return ellipsis_width; // Solo mostramos los puntos suspensivos
}
// Calculamos aproximadamente cuántos caracteres caben
float char_width = static_cast<float>(value_width) / value.length();
auto max_chars = static_cast<size_t>(available_for_text / char_width);
// Verificamos el ancho real del texto truncado
std::string truncated = truncateWithEllipsis(value, max_chars);
return element_text_->length(truncated, -2);
}
auto MenuRenderer::getTruncatedValue(const std::string& value, int available_width) const -> std::string {
int value_width = element_text_->length(value, -2);
if (value_width <= available_width) {
return value;
}
// Calculamos cuántos caracteres podemos mostrar
int ellipsis_width = element_text_->length("...", -2);
int available_for_text = available_width - ellipsis_width;
if (available_for_text <= 0) {
return "..."; // Solo mostramos los puntos suspensivos
}
// Calculamos aproximadamente cuántos caracteres caben
float char_width = static_cast<float>(value_width) / value.length();
auto max_chars = static_cast<size_t>(available_for_text / char_width);
// Ajustamos iterativamente hasta que el texto quepa
std::string truncated = truncateWithEllipsis(value, max_chars);
while (element_text_->length(truncated, -2) > available_width && max_chars > 1) {
max_chars--;
truncated = truncateWithEllipsis(value, max_chars);
}
return truncated;
}
auto MenuRenderer::easeOut(float t) -> float { return 1.0F - ((1.0F - t) * (1.0F - t)); }
auto MenuRenderer::shouldShowContent() const -> bool { return !show_hide_animation_.active; }

View File

@@ -0,0 +1,129 @@
#pragma once
#include <SDL3/SDL.h>
#include <array>
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "color.hpp"
#include "ui/service_menu.hpp"
class MenuOption;
class Text;
class MenuRenderer {
public:
// --- Nuevo: Enum para el modo de posicionamiento ---
enum class PositionMode {
CENTERED, // La ventana se centra en el punto especificado
FIXED // La esquina superior izquierda coincide con el punto
};
MenuRenderer(const ServiceMenu* menu_state, std::shared_ptr<Text> element_text, std::shared_ptr<Text> title_text);
// --- Métodos principales de la vista ---
void render(const ServiceMenu* menu_state);
void update(const ServiceMenu* menu_state, float delta_time);
// --- Nuevos: Métodos de control de visibilidad y animación ---
void show(const ServiceMenu* menu_state);
void hide();
[[nodiscard]] auto isVisible() const -> bool { return visible_; }
[[nodiscard]] auto isFullyVisible() const -> bool { return visible_ && !show_hide_animation_.active && !resize_animation_.active; }
[[nodiscard]] auto isAnimating() const -> bool { return resize_animation_.active || show_hide_animation_.active; }
// --- Nuevos: Métodos de configuración de posición ---
void setPosition(float x, float y, PositionMode mode);
// Método para notificar al renderer que el layout puede haber cambiado
void onLayoutChanged(const ServiceMenu* menu_state);
void setLayout(const ServiceMenu* menu_state);
// Getters
[[nodiscard]] auto getRect() const -> const SDL_FRect& { return rect_; }
private:
// --- Referencias a los renderizadores de texto ---
std::shared_ptr<Text> element_text_;
std::shared_ptr<Text> title_text_;
// --- Variables de estado de la vista (layout y animación) ---
SDL_FRect rect_{};
SDL_FRect border_rect_{};
size_t width_ = 0;
size_t height_ = 0;
size_t options_height_ = 0;
size_t options_padding_ = 0;
size_t options_y_ = 0;
size_t title_height_ = 0;
size_t title_padding_ = 0;
size_t upper_height_ = 0;
size_t lower_height_ = 0;
size_t lower_padding_ = 0;
Uint32 color_counter_ = 0;
bool visible_ = false;
// --- Posicionamiento ---
PositionMode position_mode_ = PositionMode::CENTERED;
float anchor_x_ = 0.0F;
float anchor_y_ = 0.0F;
// --- Límites de tamaño máximo ---
size_t max_menu_width_ = 0;
size_t max_menu_height_ = 0;
// --- Estructuras de Animación ---
struct ResizeAnimation {
bool active = false;
float start_width, start_height;
float target_width, target_height;
float elapsed = 0.0F;
float duration = 0.2F;
void start(float from_w, float from_h, float to_w, float to_h);
void stop();
} resize_animation_;
struct ShowHideAnimation {
enum class Type { NONE,
SHOWING,
HIDING };
Type type = Type::NONE;
bool active = false;
float target_width, target_height;
float elapsed = 0.0F;
float duration = 0.25F;
void startShow(float to_w, float to_h);
void startHide();
void stop();
} show_hide_animation_;
// --- Anchos precalculados ---
std::array<int, ServiceMenu::SETTINGS_GROUP_SIZE> group_menu_widths_ = {};
// --- Métodos privados de la vista ---
void initializeMaxSizes();
void setAnchors(const ServiceMenu* menu_state);
auto calculateNewRect(const ServiceMenu* menu_state) -> SDL_FRect;
void resize(const ServiceMenu* menu_state);
void setSize(const ServiceMenu* menu_state);
void updateAnimations(float delta_time);
void updateResizeAnimation(float delta_time);
void updateShowHideAnimation(float delta_time);
void updatePosition();
void precalculateMenuWidths(const std::vector<std::unique_ptr<MenuOption>>& all_options, const ServiceMenu* menu_state); // NOLINT(readability-avoid-const-params-in-decls)
[[nodiscard]] auto getMenuWidthForGroup(ServiceMenu::SettingsGroup group) const -> int;
[[nodiscard]] auto getAnimatedSelectedColor() const -> Color;
void updateColorCounter();
auto setRect(SDL_FRect rect) -> SDL_FRect;
[[nodiscard]] auto getTruncatedValueWidth(const std::string& value, int available_width) const -> int;
[[nodiscard]] auto getTruncatedValue(const std::string& value, int available_width) const -> std::string;
[[nodiscard]] static auto easeOut(float t) -> float;
[[nodiscard]] auto shouldShowContent() const -> bool;
};

310
source/game/ui/notifier.cpp Normal file
View File

@@ -0,0 +1,310 @@
#include "notifier.hpp"
#include <SDL3/SDL.h> // Para SDL_RenderFillRect, SDL_FRect, SDL_RenderClear
#include <algorithm> // Para remove_if, min
#include <string> // Para basic_string, string
#include <utility>
#include <vector> // Para vector
#include "audio.hpp" // Para Audio
#include "param.hpp" // Para Param, param, ParamNotification, ParamGame
#include "screen.hpp" // Para Screen
#include "sprite.hpp" // Para Sprite
#include "text.hpp" // Para Text
#include "texture.hpp" // Para Texture
// Singleton
Notifier* Notifier::instance = nullptr;
// Inicializa la instancia única del singleton
void Notifier::init(const std::string& icon_file, std::shared_ptr<Text> text) { Notifier::instance = new Notifier(icon_file, std::move(text)); }
// Libera la instancia
void Notifier::destroy() { delete Notifier::instance; }
// Obtiene la instancia
auto Notifier::get() -> Notifier* { return Notifier::instance; }
// Constructor
Notifier::Notifier(const std::string& icon_file, std::shared_ptr<Text> text)
: renderer_(Screen::get()->getRenderer()),
icon_texture_(!icon_file.empty() ? std::make_unique<Texture>(renderer_, icon_file) : nullptr),
text_(std::move(text)),
bg_color_(param.notification.color),
stack_(false),
has_icons_(!icon_file.empty()) {}
// Dibuja las notificaciones por pantalla
void Notifier::render() {
for (int i = static_cast<int>(notifications_.size()) - 1; i >= 0; --i) {
notifications_[i].sprite->render();
}
}
// Actualiza el estado de las notificaciones
void Notifier::update(float delta_time) {
for (int i = 0; std::cmp_less(i, notifications_.size()); ++i) {
if (!shouldProcessNotification(i)) {
break;
}
processNotification(i, delta_time);
}
clearFinishedNotifications();
}
auto Notifier::shouldProcessNotification(int index) const -> bool {
// Si la notificación anterior está "saliendo", no hagas nada
return index <= 0 || notifications_[index - 1].state != State::RISING;
}
void Notifier::processNotification(int index, float delta_time) {
auto& notification = notifications_[index];
notification.timer += delta_time;
playNotificationSoundIfNeeded(notification);
updateNotificationState(index, delta_time);
notification.sprite->setPosition(notification.rect);
}
void Notifier::playNotificationSoundIfNeeded(const Notification& notification) {
// Hace sonar la notificación al inicio
if (notification.timer <= 0.016F &&
param.notification.sound &&
notification.state == State::RISING) {
Audio::get()->playSound("notify.wav", Audio::Group::INTERFACE);
}
}
void Notifier::updateNotificationState(int index, float delta_time) {
auto& notification = notifications_[index];
switch (notification.state) {
case State::RISING:
handleRisingState(index, delta_time);
break;
case State::STAY:
handleStayState(index);
break;
case State::VANISHING:
handleVanishingState(index, delta_time);
break;
default:
break;
}
}
void Notifier::handleRisingState(int index, float delta_time) {
auto& notification = notifications_[index];
const float PIXELS_TO_MOVE = ANIMATION_SPEED_PX_PER_S * delta_time;
const float PROGRESS = notification.timer * ANIMATION_SPEED_PX_PER_S / notification.travel_dist;
const int ALPHA = static_cast<int>(255 * std::min(PROGRESS, 1.0F));
moveNotificationVertically(notification, param.notification.pos_v == Position::TOP ? PIXELS_TO_MOVE : -PIXELS_TO_MOVE);
notification.texture->setAlpha(ALPHA);
if ((param.notification.pos_v == Position::TOP && notification.rect.y >= notification.y) ||
(param.notification.pos_v == Position::BOTTOM && notification.rect.y <= notification.y)) {
transitionToStayState(index);
}
}
void Notifier::handleStayState(int index) {
auto& notification = notifications_[index];
if (notification.timer >= STAY_DURATION_S) {
notification.state = State::VANISHING;
notification.timer = 0.0F;
}
}
void Notifier::handleVanishingState(int index, float delta_time) {
auto& notification = notifications_[index];
const float PIXELS_TO_MOVE = ANIMATION_SPEED_PX_PER_S * delta_time;
const float PROGRESS = notification.timer * ANIMATION_SPEED_PX_PER_S / notification.travel_dist;
const int ALPHA = static_cast<int>(255 * (1 - std::min(PROGRESS, 1.0F)));
moveNotificationVertically(notification, param.notification.pos_v == Position::TOP ? -PIXELS_TO_MOVE : PIXELS_TO_MOVE);
notification.texture->setAlpha(ALPHA);
if (PROGRESS >= 1.0F) {
notification.state = State::FINISHED;
}
}
void Notifier::moveNotificationVertically(Notification& notification, float pixels_to_move) {
notification.rect.y += pixels_to_move;
}
void Notifier::transitionToStayState(int index) {
auto& notification = notifications_[index];
notification.state = State::STAY;
notification.texture->setAlpha(255);
notification.rect.y = static_cast<float>(notification.y); // Asegurar posición exacta
notification.timer = 0.0F;
}
// Elimina las notificaciones finalizadas
void Notifier::clearFinishedNotifications() {
for (int i = static_cast<int>(notifications_.size()) - 1; i >= 0; --i) {
if (notifications_[i].state == State::FINISHED) {
notifications_.erase(notifications_.begin() + i);
}
}
}
void Notifier::show(std::vector<std::string> texts, int icon, const std::string& code) {
// Si no hay texto, acaba
if (texts.empty()) {
return;
}
// Si las notificaciones no se apilan, elimina las anteriores
if (!stack_) {
clearAllNotifications();
}
// Elimina las cadenas vacías
texts.erase(std::ranges::remove_if(texts, [](const std::string& s) -> bool { return s.empty(); }).begin(), texts.end());
// Encuentra la cadena más larga
std::string longest;
for (const auto& text : texts) {
if (text.length() > longest.length()) {
longest = text;
}
}
// Inicializa variables
constexpr int ICON_SIZE = 16;
constexpr int PADDING_OUT = 1;
const float PADDING_IN_H = text_->getCharacterSize();
const float PADDING_IN_V = text_->getCharacterSize() / 2;
const int ICON_SPACE = icon >= 0 ? ICON_SIZE + PADDING_IN_H : 0;
const float WIDTH = text_->length(longest) + (PADDING_IN_H * 2) + ICON_SPACE;
const float HEIGHT = (text_->getCharacterSize() * texts.size()) + (PADDING_IN_V * 2);
const auto SHAPE = Shape::SQUARED;
// Posición horizontal
float desp_h = 0;
switch (param.notification.pos_h) {
case Position::LEFT:
desp_h = PADDING_OUT;
break;
case Position::MIDDLE:
desp_h = ((param.game.width / 2) - (WIDTH / 2));
break;
case Position::RIGHT:
desp_h = param.game.width - WIDTH - PADDING_OUT;
break;
default:
desp_h = 0;
break;
}
// Posición vertical
const int DESP_V = (param.notification.pos_v == Position::TOP) ? PADDING_OUT : (param.game.height - HEIGHT - PADDING_OUT);
// Offset
const auto TRAVEL_DIST = HEIGHT + PADDING_OUT;
auto offset = notifications_.empty()
? DESP_V
: notifications_.back().y + (param.notification.pos_v == Position::TOP ? TRAVEL_DIST : -TRAVEL_DIST);
// Crea la notificacion
Notification n;
// Inicializa variables
n.code = code;
n.y = offset;
n.travel_dist = TRAVEL_DIST;
n.texts = texts;
n.shape = SHAPE;
const float POS_Y = offset + (param.notification.pos_v == Position::TOP ? -TRAVEL_DIST : TRAVEL_DIST);
n.rect = {.x = desp_h, .y = POS_Y, .w = WIDTH, .h = HEIGHT};
// Crea la textura
n.texture = std::make_shared<Texture>(renderer_);
n.texture->createBlank(WIDTH, HEIGHT, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET);
n.texture->setBlendMode(SDL_BLENDMODE_BLEND);
// Prepara para dibujar en la textura
n.texture->setAsRenderTarget(renderer_);
// Dibuja el fondo de la notificación
SDL_SetRenderDrawColor(renderer_, bg_color_.r, bg_color_.g, bg_color_.b, 255);
SDL_FRect rect;
if (SHAPE == Shape::ROUNDED) {
rect = {.x = 4, .y = 0, .w = WIDTH - (4 * 2), .h = HEIGHT};
SDL_RenderFillRect(renderer_, &rect);
rect = {.x = 4 / 2, .y = 1, .w = WIDTH - 4, .h = HEIGHT - 2};
SDL_RenderFillRect(renderer_, &rect);
rect = {.x = 1, .y = 4 / 2, .w = WIDTH - 2, .h = HEIGHT - 4};
SDL_RenderFillRect(renderer_, &rect);
rect = {.x = 0, .y = 4, .w = WIDTH, .h = HEIGHT - (4 * 2)};
SDL_RenderFillRect(renderer_, &rect);
}
else if (SHAPE == Shape::SQUARED) {
SDL_RenderClear(renderer_);
}
// Dibuja el icono de la notificación
if (has_icons_ && icon >= 0 && texts.size() >= 2) {
auto sp = std::make_unique<Sprite>(icon_texture_, (SDL_FRect){.x = 0, .y = 0, .w = ICON_SIZE, .h = ICON_SIZE});
sp->setPosition({.x = PADDING_IN_H, .y = PADDING_IN_V, .w = ICON_SIZE, .h = ICON_SIZE});
sp->setSpriteClip(SDL_FRect{
.x = static_cast<float>(ICON_SIZE * (icon % 10)),
.y = static_cast<float>(ICON_SIZE * (icon / 10)),
.w = ICON_SIZE,
.h = ICON_SIZE});
sp->render();
}
// Escribe el texto de la notificación
const Color COLOR{255, 255, 255};
int iterator = 0;
for (const auto& text : texts) {
text_->writeColored(PADDING_IN_H + ICON_SPACE, PADDING_IN_V + (iterator * (text_->getCharacterSize() + 1)), text, COLOR);
++iterator;
}
// Deja de dibujar en la textura
SDL_SetRenderTarget(renderer_, nullptr);
// Crea el sprite de la notificación
n.sprite = std::make_shared<Sprite>(n.texture, n.rect);
// Deja la notificación invisible
n.texture->setAlpha(0);
// Añade la notificación a la lista
notifications_.emplace_back(n);
}
// Finaliza y elimnina todas las notificaciones activas
void Notifier::clearAllNotifications() {
for (auto& notification : notifications_) {
notification.state = State::FINISHED;
}
clearFinishedNotifications();
}
// Obtiene los códigos de las notificaciones
auto Notifier::getCodes() -> std::vector<std::string> {
std::vector<std::string> codes;
codes.reserve(notifications_.size());
for (const auto& notification : notifications_) {
codes.emplace_back(notification.code);
}
return codes;
}

112
source/game/ui/notifier.hpp Normal file
View File

@@ -0,0 +1,112 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FRect, SDL_Renderer
#include <memory> // Para shared_ptr
#include <string> // Para basic_string, string
#include <vector> // Para vector
#include "color.hpp" // Para stringInVector, Color
#include "utils.hpp"
class Sprite;
class Text;
class Texture;
// --- Clase Notifier: gestiona las notificaciones en pantalla (singleton) ---
class Notifier {
public:
// --- Enums ---
enum class Position {
TOP, // Parte superior
BOTTOM, // Parte inferior
LEFT, // Lado izquierdo
MIDDLE, // Centro
RIGHT, // Lado derecho
};
// --- Métodos de singleton ---
static void init(const std::string& icon_file, std::shared_ptr<Text> text); // Inicializa el singleton
static void destroy(); // Libera el singleton
static auto get() -> Notifier*; // Obtiene la instancia
// --- Métodos principales ---
void render(); // Dibuja las notificaciones por pantalla
void update(float delta_time); // Actualiza el estado de las notificaciones
// --- Gestión de notificaciones ---
void show(std::vector<std::string> texts, int icon = -1, const std::string& code = std::string()); // Muestra una notificación de texto por pantalla
[[nodiscard]] auto isActive() const -> bool { return !notifications_.empty(); } // Indica si hay notificaciones activas
auto getCodes() -> std::vector<std::string>; // Obtiene los códigos de las notificaciones activas
auto checkCode(const std::string& code) -> bool { return stringInVector(getCodes(), code); } // Comprueba si hay alguna notificación con un código concreto
private:
// --- Constantes de tiempo (en segundos) ---
static constexpr float STAY_DURATION_S = 2.5F; // Tiempo que se ve la notificación (150 frames @ 60fps)
static constexpr float ANIMATION_SPEED_PX_PER_S = 60.0F; // Velocidad de animación (1 pixel/frame @ 60fps)
// --- Enums privados ---
enum class State {
RISING, // Apareciendo
STAY, // Visible
VANISHING, // Desapareciendo
FINISHED, // Terminada
};
enum class Shape {
ROUNDED, // Forma redondeada
SQUARED, // Forma cuadrada
};
// --- Estructuras privadas ---
struct Notification {
std::shared_ptr<Texture> texture; // Textura de la notificación
std::shared_ptr<Sprite> sprite; // Sprite asociado
std::vector<std::string> texts; // Textos a mostrar
SDL_FRect rect; // Rectángulo de la notificación
std::string code; // Código identificador de la notificación
State state{State::RISING}; // Estado de la notificación
Shape shape{Shape::SQUARED}; // Forma de la notificación
float timer{0.0F}; // Timer en segundos
int y{0}; // Posición vertical
int travel_dist{0}; // Distancia a recorrer
// Constructor
explicit Notification()
: texture(nullptr),
sprite(nullptr),
rect{.x = 0, .y = 0, .w = 0, .h = 0} {}
};
// --- Objetos y punteros ---
SDL_Renderer* renderer_; // El renderizador de la ventana
std::shared_ptr<Texture> icon_texture_; // Textura para los iconos de las notificaciones
std::shared_ptr<Text> text_; // Objeto para dibujar texto
// --- Variables de estado ---
std::vector<Notification> notifications_; // Lista de notificaciones activas
Color bg_color_; // Color de fondo de las notificaciones
// Nota: wait_time_ eliminado, ahora se usa STAY_DURATION_S
bool stack_; // Indica si las notificaciones se apilan
bool has_icons_; // Indica si el notificador tiene textura para iconos
// --- Métodos internos ---
void clearFinishedNotifications(); // Elimina las notificaciones cuyo estado es FINISHED
void clearAllNotifications(); // Elimina todas las notificaciones activas, sin importar el estado
[[nodiscard]] auto shouldProcessNotification(int index) const -> bool; // Determina si una notificación debe ser procesada (según su estado y posición)
void processNotification(int index, float delta_time); // Procesa una notificación en la posición dada: actualiza su estado y comportamiento visual
static void playNotificationSoundIfNeeded(const Notification& notification); // Reproduce sonido asociado si es necesario (dependiendo del estado o contenido)
void updateNotificationState(int index, float delta_time); // Actualiza el estado interno de una notificación (ej. de RISING a STAY)
void handleRisingState(int index, float delta_time); // Lógica de animación para el estado RISING (apareciendo)
void handleStayState(int index); // Lógica para mantener una notificación visible en el estado STAY
void handleVanishingState(int index, float delta_time); // Lógica de animación para el estado VANISHING (desapareciendo)
static void moveNotificationVertically(Notification& notification, float pixels_to_move); // Mueve verticalmente una notificación con la cantidad de pixels especificada
void transitionToStayState(int index); // Cambia el estado de una notificación de RISING a STAY cuando ha alcanzado su posición final
// --- Constructores y destructor privados (singleton) ---
Notifier(const std::string& icon_file, std::shared_ptr<Text> text); // Constructor privado
~Notifier() = default; // Destructor privado
// --- Instancia singleton ---
static Notifier* instance; // Instancia única de Notifier
};

View File

@@ -0,0 +1,733 @@
#include "ui/service_menu.hpp"
#include <utility>
#include "audio.hpp" // Para Audio
#include "define_buttons.hpp" // Para DefineButtons
#include "difficulty.hpp" // Para getCodeFromName, getNameFromCode
#include "input.hpp" // Para Input
#include "input_types.hpp" // Para InputAction
#include "lang.hpp" // Para getText, getCodeFromName, getNameFromCode
#include "menu_option.hpp" // Para MenuOption, ActionOption, BoolOption, ListOption, FolderOption, IntOption, ActionListOption
#include "menu_renderer.hpp" // Para MenuRenderer
#include "options.hpp" // Para GamepadManager, gamepad_manager, PendingChanges, Video, pending_changes, video, Audio, Gamepad, Settings, audio, checkPendingChanges, settings, Window, getPlayerWhoUsesKeyboard, playerIdToString, stringToPlayerId, window, Keyboard, Music, Sound, keyboard
#include "param.hpp" // Para Param, param, ParamGame, ParamServiceMenu
#include "player.hpp" // Para Player
#include "resource.hpp" // Para Resource
#include "screen.hpp" // Para Screen
#include "section.hpp" // Para Name, name, Options, options
#include "ui/ui_message.hpp" // Para UIMessage
#include "utils.hpp" // Para Zone
// Singleton
ServiceMenu* ServiceMenu::instance = nullptr;
void ServiceMenu::init() { ServiceMenu::instance = new ServiceMenu(); }
void ServiceMenu::destroy() { delete ServiceMenu::instance; }
auto ServiceMenu::get() -> ServiceMenu* { return ServiceMenu::instance; }
// Constructor
ServiceMenu::ServiceMenu()
: current_settings_group_(SettingsGroup::MAIN),
previous_settings_group_(current_settings_group_) {
auto element_text = Resource::get()->getText("04b_25_flat");
auto title_text = Resource::get()->getText("04b_25_flat_2x");
// El renderer ahora se inicializa con su configuración
renderer_ = std::make_unique<MenuRenderer>(this, element_text, title_text);
restart_message_ui_ = std::make_unique<UIMessage>(element_text, Lang::getText("[SERVICE_MENU] NEED_RESTART_MESSAGE"), param.service_menu.title_color);
define_buttons_ = std::make_unique<DefineButtons>();
reset();
}
void ServiceMenu::toggle() {
if (define_buttons_ && define_buttons_->isEnabled()) {
return;
}
if (isAnimating() && !define_buttons_->isEnabled()) {
return;
}
if (!enabled_) { // Si está cerrado, abrir
reset();
Options::gamepad_manager.assignAndLinkGamepads();
renderer_->show(this);
setEnabledInternal(true);
playSelectSound();
} else { // Si está abierto, cerrar
renderer_->hide();
setEnabledInternal(false);
playBackSound();
}
}
void ServiceMenu::render() {
// Condición corregida: renderiza si está habilitado O si se está animando
if (enabled_ || isAnimating()) {
renderer_->render(this);
} else {
return; // Si no está ni habilitado ni animándose, no dibujes nada.
}
// El mensaje de reinicio y otros elementos solo deben aparecer si está completamente visible,
// no durante la animación.
if (enabled_ && !isAnimating()) {
const float MSG_X = param.game.game_area.center_x;
const float MSG_Y = renderer_->getRect().y + 39.0F;
restart_message_ui_->setPosition(MSG_X, MSG_Y);
restart_message_ui_->render();
if (define_buttons_ && define_buttons_->isEnabled()) {
define_buttons_->render();
}
}
}
void ServiceMenu::update(float delta_time) {
// El renderer siempre se actualiza para manejar sus animaciones
renderer_->update(this, delta_time);
if (!enabled_) {
return;
}
// Lógica de actualización del mensaje de reinicio y botones
bool now_pending = Options::pending_changes.has_pending_changes;
if (now_pending != last_pending_changes_) {
now_pending ? restart_message_ui_->show() : restart_message_ui_->hide();
last_pending_changes_ = now_pending;
}
restart_message_ui_->update(delta_time);
if (define_buttons_) {
define_buttons_->update(delta_time);
if (define_buttons_->isEnabled() && define_buttons_->isReadyToClose()) {
define_buttons_->disable();
}
}
}
void ServiceMenu::reset() {
selected_ = 0;
main_menu_selected_ = 0;
current_settings_group_ = SettingsGroup::MAIN;
previous_settings_group_ = SettingsGroup::MAIN;
initializeOptions();
updateMenu();
renderer_->setLayout(this);
}
void ServiceMenu::moveBack() {
// Si estamos en una subpantalla, no llamamos a toggle
if (current_settings_group_ != SettingsGroup::MAIN) {
playBackSound();
current_settings_group_ = previous_settings_group_;
selected_ = (current_settings_group_ == SettingsGroup::MAIN) ? main_menu_selected_ : 0;
updateMenu();
return;
}
// Si estamos en la pantalla principal, llamamos a toggle() para cerrar con animación.
toggle();
}
// --- Lógica de Navegación ---
void ServiceMenu::setSelectorUp() {
if (display_options_.empty()) {
return;
}
selected_ = (selected_ > 0) ? selected_ - 1 : display_options_.size() - 1;
playMoveSound();
}
void ServiceMenu::setSelectorDown() {
if (display_options_.empty()) {
return;
}
selected_ = (selected_ + 1) % display_options_.size();
playMoveSound();
}
void ServiceMenu::adjustOption(bool adjust_up) {
if (display_options_.empty()) {
return;
}
auto& selected_option = display_options_.at(selected_);
if (selected_option->getBehavior() == MenuOption::Behavior::ADJUST) {
selected_option->adjustValue(adjust_up);
applySettings();
playAdjustSound();
}
}
void ServiceMenu::selectOption() {
if (display_options_.empty()) {
return;
}
if (current_settings_group_ == SettingsGroup::MAIN) {
main_menu_selected_ = selected_;
}
auto* selected_option = display_options_.at(selected_);
if (selected_option == nullptr) {
// This shouldn't happen in normal operation, but protects against null pointer
return;
}
if (auto* folder = dynamic_cast<FolderOption*>(selected_option)) {
previous_settings_group_ = current_settings_group_;
current_settings_group_ = folder->getTargetGroup();
selected_ = 0;
updateMenu();
} else if (selected_option->getBehavior() == MenuOption::Behavior::SELECT or selected_option->getBehavior() == MenuOption::Behavior::BOTH) {
selected_option->executeAction();
}
playSelectSound();
}
// --- Lógica Interna ---
void ServiceMenu::updateDisplayOptions() {
display_options_.clear();
for (auto& option : options_) {
if (option->getGroup() == current_settings_group_ && !option->isHidden()) {
display_options_.push_back(option.get());
}
}
updateOptionPairs();
}
void ServiceMenu::updateOptionPairs() {
option_pairs_.clear();
for (const auto& option : display_options_) {
option_pairs_.emplace_back(option->getCaption(), option->getValueAsString());
}
}
void ServiceMenu::updateMenu() {
title_ = settingsGroupToString(current_settings_group_);
adjustListValues();
// Actualiza las opciones visibles
updateDisplayOptions();
// Notifica al renderer del cambio de layout
renderer_->onLayoutChanged(this);
}
void ServiceMenu::applySettings() {
if (current_settings_group_ == SettingsGroup::CONTROLS) {
applyControlsSettings();
}
if (current_settings_group_ == SettingsGroup::VIDEO) {
applyVideoSettings();
}
if (current_settings_group_ == SettingsGroup::AUDIO) {
applyAudioSettings();
}
if (current_settings_group_ == SettingsGroup::SETTINGS) {
applySettingsSettings();
}
// Actualiza los valores de las opciones
updateOptionPairs();
}
void ServiceMenu::applyControlsSettings() {}
void ServiceMenu::applyVideoSettings() {
Screen::get()->applySettings();
setHiddenOptions();
}
void ServiceMenu::applyAudioSettings() {
Audio::get()->applySettings();
}
void ServiceMenu::applySettingsSettings() {
setHiddenOptions();
}
auto ServiceMenu::getOptionByCaption(const std::string& caption) const -> MenuOption* {
for (const auto& option : options_) {
if (option->getCaption() == caption) {
return option.get();
}
}
return nullptr;
}
// --- Getters y otros ---
auto ServiceMenu::getCurrentGroupAlignment() const -> ServiceMenu::GroupAlignment {
switch (current_settings_group_) {
case SettingsGroup::CONTROLS:
case SettingsGroup::VIDEO:
case SettingsGroup::AUDIO:
case SettingsGroup::SETTINGS:
return GroupAlignment::LEFT;
default:
return GroupAlignment::CENTERED;
}
}
auto ServiceMenu::countOptionsInGroup(SettingsGroup group) const -> size_t {
size_t count = 0;
for (const auto& option : options_) {
if (option->getGroup() == group && !option->isHidden()) {
count++;
}
}
return count;
}
// Inicializa todas las opciones del menú
void ServiceMenu::initializeOptions() {
options_.clear();
// CONTROLS - Usando ActionListOption para mandos
options_.push_back(std::make_unique<ActionListOption>(
Lang::getText("[SERVICE_MENU] CONTROLLER1"),
SettingsGroup::CONTROLS,
Input::get()->getControllerNames(),
[]() -> std::string {
return Options::gamepad_manager.getGamepad(Player::Id::PLAYER1).name;
},
[](const std::string& val) -> void {
Options::gamepad_manager.assignGamepadToPlayer(Player::Id::PLAYER1, Input::get()->getGamepadByName(val), val);
},
[this]() -> void {
// Acción: configurar botones del mando del jugador 1
auto* gamepad = &Options::gamepad_manager.getGamepad(Player::Id::PLAYER1);
if ((gamepad != nullptr) && gamepad->instance) {
define_buttons_->enable(gamepad);
}
}));
options_.push_back(std::make_unique<ActionListOption>(
Lang::getText("[SERVICE_MENU] CONTROLLER2"),
SettingsGroup::CONTROLS,
Input::get()->getControllerNames(),
[]() -> std::string {
return Options::gamepad_manager.getGamepad(Player::Id::PLAYER2).name;
},
[](const std::string& val) -> void {
Options::gamepad_manager.assignGamepadToPlayer(Player::Id::PLAYER2, Input::get()->getGamepadByName(val), val);
},
[this]() -> void {
// Acción: configurar botones del mando del jugador 2
auto* gamepad = &Options::gamepad_manager.getGamepad(Player::Id::PLAYER2);
if ((gamepad != nullptr) && gamepad->instance) {
define_buttons_->enable(gamepad);
}
}));
// CONTROLS - Opción para teclado (solo lista, sin acción)
options_.push_back(std::make_unique<ListOption>(
Lang::getText("[SERVICE_MENU] KEYBOARD"),
SettingsGroup::CONTROLS,
std::vector<std::string>{
Lang::getText("[SERVICE_MENU] PLAYER1"),
Lang::getText("[SERVICE_MENU] PLAYER2")},
[]() -> std::string {
// Devolver el jugador actual asignado al teclado
return Options::playerIdToString(Options::getPlayerWhoUsesKeyboard());
},
[](const std::string& val) -> void {
// Asignar el teclado al jugador seleccionado
Options::keyboard.assignTo(Options::stringToPlayerId(val));
}));
// CONTROLS - Acción para intercambiar mandos
options_.push_back(std::make_unique<ActionOption>(
Lang::getText("[SERVICE_MENU] SWAP_CONTROLLERS"),
SettingsGroup::CONTROLS,
[this]() -> void {
Options::gamepad_manager.swapPlayers();
adjustListValues(); // Sincroniza el valor de las opciones de lista (como MANDO1) con los datos reales
updateOptionPairs(); // Actualiza los pares de texto <opción, valor> que se van a dibujar
}));
// VIDEO
options_.push_back(std::make_unique<BoolOption>(
Lang::getText("[SERVICE_MENU] FULLSCREEN"),
SettingsGroup::VIDEO,
&Options::video.fullscreen));
options_.push_back(std::make_unique<IntOption>(
Lang::getText("[SERVICE_MENU] WINDOW_SIZE"),
SettingsGroup::VIDEO,
&Options::window.zoom,
1,
Options::window.max_zoom,
1));
// Shader: Desactivat / PostFX / CrtPi
{
std::string disabled_text = Lang::getText("[SERVICE_MENU] SHADER_DISABLED");
std::vector<std::string> shader_values = {disabled_text, "PostFX", "CrtPi"};
auto shader_getter = [disabled_text]() -> std::string {
// NOLINTNEXTLINE(performance-no-automatic-move) -- captura por valor en lambda const, no se puede mover
if (!Options::video.shader.enabled) { return disabled_text; }
return (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) ? "CrtPi" : "PostFX";
};
auto shader_setter = [disabled_text](const std::string& val) {
if (val == disabled_text) {
Options::video.shader.enabled = false;
} else {
Options::video.shader.enabled = true;
const auto TYPE = (val == "CrtPi") ? Rendering::ShaderType::CRTPI : Rendering::ShaderType::POSTFX;
Options::video.shader.current_shader = TYPE;
auto* screen = Screen::get();
if (screen != nullptr) {
screen->applySettings();
}
}
Screen::initShaders();
};
options_.push_back(std::make_unique<ListOption>(
Lang::getText("[SERVICE_MENU] SHADER"),
SettingsGroup::VIDEO,
shader_values,
shader_getter,
shader_setter));
}
// Preset: muestra nombre, cicla circularmente entre presets del shader activo
{
auto preset_getter = []() -> std::string {
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
if (Options::crtpi_presets.empty()) { return ""; }
return Options::crtpi_presets.at(static_cast<size_t>(Options::video.shader.current_crtpi_preset)).name;
}
if (Options::postfx_presets.empty()) { return ""; }
return Options::postfx_presets.at(static_cast<size_t>(Options::video.shader.current_postfx_preset)).name;
};
auto preset_adjuster = [](bool up) {
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
if (Options::crtpi_presets.empty()) { return; }
const int SIZE = static_cast<int>(Options::crtpi_presets.size());
Options::video.shader.current_crtpi_preset = up
? (Options::video.shader.current_crtpi_preset + 1) % SIZE
: (Options::video.shader.current_crtpi_preset + SIZE - 1) % SIZE;
} else {
if (Options::postfx_presets.empty()) { return; }
const int SIZE = static_cast<int>(Options::postfx_presets.size());
Options::video.shader.current_postfx_preset = up
? (Options::video.shader.current_postfx_preset + 1) % SIZE
: (Options::video.shader.current_postfx_preset + SIZE - 1) % SIZE;
}
Screen::initShaders();
};
auto preset_max_width = [](Text* text) -> int {
int max_w = 0;
for (const auto& p : Options::postfx_presets) { max_w = std::max(max_w, text->length(p.name, -2)); }
for (const auto& p : Options::crtpi_presets) { max_w = std::max(max_w, text->length(p.name, -2)); }
return max_w;
};
options_.push_back(std::make_unique<CallbackOption>(
Lang::getText("[SERVICE_MENU] SHADER_PRESET"),
SettingsGroup::VIDEO,
preset_getter,
preset_adjuster,
preset_max_width));
}
options_.push_back(std::make_unique<BoolOption>(
Lang::getText("[SERVICE_MENU] SUPERSAMPLING"),
SettingsGroup::VIDEO,
&Options::video.supersampling.enabled));
options_.push_back(std::make_unique<BoolOption>(
Lang::getText("[SERVICE_MENU] VSYNC"),
SettingsGroup::VIDEO,
&Options::video.vsync));
options_.push_back(std::make_unique<BoolOption>(
Lang::getText("[SERVICE_MENU] INTEGER_SCALE"),
SettingsGroup::VIDEO,
&Options::video.integer_scale));
// AUDIO
options_.push_back(std::make_unique<BoolOption>(
Lang::getText("[SERVICE_MENU] AUDIO"),
SettingsGroup::AUDIO,
&Options::audio.enabled));
options_.push_back(std::make_unique<IntOption>(
Lang::getText("[SERVICE_MENU] MAIN_VOLUME"),
SettingsGroup::AUDIO,
&Options::audio.volume,
0,
100,
5));
options_.push_back(std::make_unique<IntOption>(
Lang::getText("[SERVICE_MENU] MUSIC_VOLUME"),
SettingsGroup::AUDIO,
&Options::audio.music.volume,
0,
100,
5));
options_.push_back(std::make_unique<IntOption>(
Lang::getText("[SERVICE_MENU] SFX_VOLUME"),
SettingsGroup::AUDIO,
&Options::audio.sound.volume,
0,
100,
5));
// SETTINGS
options_.push_back(std::make_unique<BoolOption>(
Lang::getText("[SERVICE_MENU] AUTOFIRE"),
SettingsGroup::SETTINGS,
&Options::settings.autofire));
options_.push_back(std::make_unique<ListOption>(
Lang::getText("[SERVICE_MENU] LANGUAGE"),
SettingsGroup::SETTINGS,
std::vector<std::string>{
Lang::getText("[SERVICE_MENU] LANG_ES"),
Lang::getText("[SERVICE_MENU] LANG_BA"),
Lang::getText("[SERVICE_MENU] LANG_EN")},
[]() -> std::string {
return Lang::getNameFromCode(Options::pending_changes.new_language);
},
[](const std::string& val) -> void {
Options::pending_changes.new_language = Lang::getCodeFromName(val);
Options::checkPendingChanges();
}));
options_.push_back(std::make_unique<ListOption>(
Lang::getText("[SERVICE_MENU] DIFFICULTY"),
SettingsGroup::SETTINGS,
std::vector<std::string>{
Lang::getText("[SERVICE_MENU] EASY"),
Lang::getText("[SERVICE_MENU] NORMAL"),
Lang::getText("[SERVICE_MENU] HARD")},
[]() -> std::string {
return Difficulty::getNameFromCode(Options::pending_changes.new_difficulty);
},
[](const std::string& val) -> void {
Options::pending_changes.new_difficulty = Difficulty::getCodeFromName(val);
Options::checkPendingChanges();
}));
options_.push_back(std::make_unique<BoolOption>(
Lang::getText("[SERVICE_MENU] ENABLE_SHUTDOWN"),
SettingsGroup::SETTINGS,
&Options::settings.shutdown_enabled));
// SYSTEM
options_.push_back(std::make_unique<ActionOption>(
Lang::getText("[SERVICE_MENU] RESET"),
SettingsGroup::SYSTEM,
[this]() -> void {
Section::name = Section::Name::RESET;
toggle();
}));
options_.push_back(std::make_unique<ActionOption>(
Lang::getText("[SERVICE_MENU] QUIT"),
SettingsGroup::SYSTEM,
[]() -> void {
Section::name = Section::Name::QUIT;
Section::options = Section::Options::NONE;
}));
options_.push_back(std::make_unique<ActionOption>(
Lang::getText("[SERVICE_MENU] SHUTDOWN"),
SettingsGroup::SYSTEM,
[]() -> void {
Section::name = Section::Name::QUIT;
Section::options = Section::Options::SHUTDOWN;
},
!Options::settings.shutdown_enabled));
// MAIN MENU
options_.push_back(std::make_unique<FolderOption>(
Lang::getText("[SERVICE_MENU] CONTROLS"),
SettingsGroup::MAIN,
SettingsGroup::CONTROLS));
options_.push_back(std::make_unique<FolderOption>(
Lang::getText("[SERVICE_MENU] VIDEO"),
SettingsGroup::MAIN,
SettingsGroup::VIDEO));
options_.push_back(std::make_unique<FolderOption>(
Lang::getText("[SERVICE_MENU] AUDIO"),
SettingsGroup::MAIN,
SettingsGroup::AUDIO));
options_.push_back(std::make_unique<FolderOption>(
Lang::getText("[SERVICE_MENU] SETTINGS"),
SettingsGroup::MAIN,
SettingsGroup::SETTINGS));
options_.push_back(std::make_unique<FolderOption>(
Lang::getText("[SERVICE_MENU] SYSTEM"),
SettingsGroup::MAIN,
SettingsGroup::SYSTEM));
// Oculta opciones según configuración
setHiddenOptions();
}
// Sincroniza los valores de las opciones tipo lista
void ServiceMenu::adjustListValues() {
for (auto& option : options_) {
if (auto* list_option = dynamic_cast<ListOption*>(option.get())) {
list_option->sync();
}
}
}
// Reproduce el sonido de navegación del menú
void ServiceMenu::playAdjustSound() { Audio::get()->playSound("service_menu_adjust.wav", Audio::Group::INTERFACE); }
void ServiceMenu::playMoveSound() { Audio::get()->playSound("service_menu_move.wav", Audio::Group::INTERFACE); }
void ServiceMenu::playSelectSound() { Audio::get()->playSound("service_menu_select.wav", Audio::Group::INTERFACE); }
void ServiceMenu::playBackSound() { Audio::get()->playSound("service_menu_back.wav", Audio::Group::INTERFACE); }
// Devuelve el nombre del grupo como string para el título
auto ServiceMenu::settingsGroupToString(SettingsGroup group) -> std::string {
switch (group) {
case SettingsGroup::MAIN:
return Lang::getText("[SERVICE_MENU] TITLE");
case SettingsGroup::CONTROLS:
return Lang::getText("[SERVICE_MENU] CONTROLS");
case SettingsGroup::VIDEO:
return Lang::getText("[SERVICE_MENU] VIDEO");
case SettingsGroup::AUDIO:
return Lang::getText("[SERVICE_MENU] AUDIO");
case SettingsGroup::SETTINGS:
return Lang::getText("[SERVICE_MENU] SETTINGS");
case SettingsGroup::SYSTEM:
return Lang::getText("[SERVICE_MENU] SYSTEM");
default:
return Lang::getText("[SERVICE_MENU] TITLE");
}
}
// Establece el estado de oculto de ciertas opciones
void ServiceMenu::setHiddenOptions() {
{
auto* option = getOptionByCaption(Lang::getText("[SERVICE_MENU] WINDOW_SIZE"));
if (option != nullptr) {
option->setHidden(Options::video.fullscreen);
}
}
{
auto* option = getOptionByCaption(Lang::getText("[SERVICE_MENU] SHUTDOWN"));
if (option != nullptr) {
option->setHidden(!Options::settings.shutdown_enabled);
}
}
{
auto* option = getOptionByCaption(Lang::getText("[SERVICE_MENU] SHADER_PRESET"));
if (option != nullptr) {
option->setHidden(!Options::video.shader.enabled);
}
}
{
auto* option = getOptionByCaption(Lang::getText("[SERVICE_MENU] SUPERSAMPLING"));
if (option != nullptr) {
option->setHidden(!Options::video.shader.enabled || Options::video.shader.current_shader != Rendering::ShaderType::POSTFX);
}
}
updateMenu(); // El menú debe refrescarse si algo se oculta
}
void ServiceMenu::handleEvent(const SDL_Event& event) {
if (!enabled_) {
return;
}
// Si DefineButtons está activo, que maneje todos los eventos
if (define_buttons_ && define_buttons_->isEnabled()) {
define_buttons_->handleEvents(event);
}
}
auto ServiceMenu::checkInput() -> bool {
// --- Guardas ---
// No procesar input si el menú no está habilitado, si se está animando o si se definen botones
if (!enabled_ || isAnimating() || (define_buttons_ && define_buttons_->isEnabled())) {
return false;
}
static auto* input_ = Input::get();
using Action = Input::Action;
const std::vector<std::pair<Action, std::function<void()>>> ACTIONS = {
{Action::UP, [this]() -> void { setSelectorUp(); }},
{Action::DOWN, [this]() -> void { setSelectorDown(); }},
{Action::RIGHT, [this]() -> void { adjustOption(true); }},
{Action::LEFT, [this]() -> void { adjustOption(false); }},
{Action::SM_SELECT, [this]() -> void { selectOption(); }},
{Action::SM_BACK, [this]() -> void { moveBack(); }},
};
// Teclado
for (const auto& [action, func] : ACTIONS) {
if (input_->checkAction(action, Input::DO_NOT_ALLOW_REPEAT, Input::CHECK_KEYBOARD)) {
func();
return true;
}
}
// Mandos
for (const auto& gamepad : input_->getGamepads()) {
for (const auto& [action, func] : ACTIONS) {
if (input_->checkAction(action, Input::DO_NOT_ALLOW_REPEAT, Input::DO_NOT_CHECK_KEYBOARD, gamepad)) {
func();
return true;
}
}
}
return false;
}
// --- Nuevo Getter ---
auto ServiceMenu::isAnimating() const -> bool {
return renderer_ && renderer_->isAnimating();
}
auto ServiceMenu::isDefiningButtons() const -> bool {
return define_buttons_ && define_buttons_->isEnabled();
}
void ServiceMenu::refresh() {
// Este método está diseñado para ser llamado desde fuera, por ejemplo,
// cuando un mando se conecta o desconecta mientras el menú está abierto.
// La función updateMenu() es la forma más completa de refrescar, ya que
// sincroniza los valores, actualiza la lista de opciones visibles y notifica
// al renderer de cualquier cambio de layout que pueda haber ocurrido.
updateMenu();
}
// Método para registrar callback
void ServiceMenu::setStateChangeCallback(StateChangeCallback callback) {
state_change_callback_ = std::move(callback);
}
// Método interno que cambia estado y notifica
void ServiceMenu::setEnabledInternal(bool enabled) {
if (enabled_ != enabled) { // Solo si realmente cambia
enabled_ = enabled;
// Notifica el cambio si hay callback registrado
if (state_change_callback_) {
state_change_callback_(enabled_);
}
}
}

View File

@@ -0,0 +1,129 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_Event
#include <cstddef> // Para size_t
#include <cstdint> // Para std::uint8_t
#include <functional> // Para function
#include <iterator> // Para pair
#include <memory> // Para unique_ptr
#include <string> // Para basic_string, string
#include <utility> // Para pair
#include <vector> // Para vector
#include "define_buttons.hpp" // for DefineButtons
#include "ui_message.hpp" // for UIMessage
class MenuOption;
class MenuRenderer;
class ServiceMenu {
public:
// --- Enums y constantes ---
enum class SettingsGroup : std::uint8_t {
CONTROLS,
VIDEO,
AUDIO,
SETTINGS,
SYSTEM,
MAIN
};
enum class GroupAlignment : std::uint8_t {
CENTERED,
LEFT
};
static constexpr size_t OPTIONS_HORIZONTAL_PADDING = 20;
static constexpr size_t MIN_WIDTH = 240;
static constexpr size_t MIN_GAP_OPTION_VALUE = 30;
static constexpr size_t SETTINGS_GROUP_SIZE = 6;
using StateChangeCallback = std::function<void(bool is_active)>;
// --- Métodos de singleton ---
static void init();
static void destroy();
static auto get() -> ServiceMenu*;
ServiceMenu(const ServiceMenu&) = delete;
auto operator=(const ServiceMenu&) -> ServiceMenu& = delete;
// --- Métodos principales ---
void toggle();
void render();
void update(float delta_time);
void reset();
// --- Lógica de navegación ---
void setSelectorUp();
void setSelectorDown();
void adjustOption(bool adjust_up);
void selectOption();
void moveBack();
// --- Método para manejar eventos ---
void handleEvent(const SDL_Event& event);
auto checkInput() -> bool;
// --- Método principal para refresco externo ---
void refresh(); // Refresca los valores y el layout del menú bajo demanda
// --- Método para registrar el callback ---
void setStateChangeCallback(StateChangeCallback callback);
// --- Getters para el estado ---
[[nodiscard]] auto isDefiningButtons() const -> bool;
[[nodiscard]] auto isAnimating() const -> bool; // Nuevo getter
// --- Getters para que el Renderer pueda leer el estado ---
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
[[nodiscard]] auto getTitle() const -> const std::string& { return title_; }
[[nodiscard]] auto getCurrentGroup() const -> SettingsGroup { return current_settings_group_; }
[[nodiscard]] auto getCurrentGroupAlignment() const -> GroupAlignment;
[[nodiscard]] auto getDisplayOptions() const -> const std::vector<MenuOption*>& { return display_options_; }
[[nodiscard]] auto getAllOptions() const -> const std::vector<std::unique_ptr<MenuOption>>& { return options_; }
[[nodiscard]] auto getSelectedIndex() const -> size_t { return selected_; }
[[nodiscard]] auto getOptionPairs() const -> const std::vector<std::pair<std::string, std::string>>& { return option_pairs_; }
[[nodiscard]] auto countOptionsInGroup(SettingsGroup group) const -> size_t;
private:
bool enabled_ = false;
std::vector<std::unique_ptr<MenuOption>> options_;
std::vector<MenuOption*> display_options_;
std::vector<std::pair<std::string, std::string>> option_pairs_;
SettingsGroup current_settings_group_;
SettingsGroup previous_settings_group_;
std::string title_;
size_t selected_ = 0;
size_t main_menu_selected_ = 0;
std::unique_ptr<UIMessage> restart_message_ui_;
bool last_pending_changes_ = false;
std::unique_ptr<DefineButtons> define_buttons_;
std::unique_ptr<MenuRenderer> renderer_;
StateChangeCallback state_change_callback_;
// --- Métodos de lógica interna ---
void updateDisplayOptions();
void updateOptionPairs();
void initializeOptions();
void updateMenu();
void applySettings();
void applyControlsSettings();
void applyVideoSettings();
static void applyAudioSettings();
void applySettingsSettings();
[[nodiscard]] auto getOptionByCaption(const std::string& caption) const -> MenuOption*;
void adjustListValues();
static void playMoveSound();
static void playAdjustSound();
static void playSelectSound();
static void playBackSound();
[[nodiscard]] static auto settingsGroupToString(SettingsGroup group) -> std::string;
void setHiddenOptions();
void setEnabledInternal(bool enabled); // Método privado para cambiar estado y notificar
// --- Constructores y destructor privados (singleton) ---
ServiceMenu();
~ServiceMenu() = default;
// --- Instancia singleton ---
static ServiceMenu* instance;
};

View File

@@ -0,0 +1,98 @@
#include "ui_message.hpp"
#include <algorithm>
#include <cmath> // Para pow
#include <utility>
#include "text.hpp" // Para Text::CENTER, Text::COLOR, Text
// Constructor: inicializa el renderizador, el texto y el color del mensaje
UIMessage::UIMessage(std::shared_ptr<Text> text_renderer, std::string message_text, const Color& color)
: text_renderer_(std::move(text_renderer)),
text_(std::move(message_text)),
color_(color) {}
// Muestra el mensaje en la posición base_x, base_y con animación de entrada desde arriba
void UIMessage::show() {
if (visible_ && target_y_ == 0.0F) {
return; // Ya está visible y quieto
}
start_y_ = DESP; // Empieza 8 píxeles arriba de la posición base
target_y_ = 0.0F; // La posición final es la base
y_offset_ = start_y_;
animation_timer_ = 0.0F;
animating_ = true;
visible_ = true;
}
// Oculta el mensaje con animación de salida hacia arriba
void UIMessage::hide() {
if (!visible_) {
return;
}
start_y_ = y_offset_; // Comienza desde la posición actual
target_y_ = DESP; // Termina 8 píxeles arriba de la base
animation_timer_ = 0.0F;
animating_ = true;
}
// Actualiza el estado de la animación (debe llamarse cada frame)
void UIMessage::update(float delta_time) {
if (animating_) {
updateAnimation(delta_time);
}
}
// Interpola la posición vertical del mensaje usando ease out cubic
void UIMessage::updateAnimation(float delta_time) {
animation_timer_ += delta_time;
float t = animation_timer_ / ANIMATION_DURATION_S;
// Clamp t entre 0 y 1
t = std::min(t, 1.0F);
if (target_y_ > start_y_) {
// Animación de entrada (ease out cubic)
t = 1 - pow(1 - t, 3);
} else {
// Animación de salida (ease in cubic)
t = pow(t, 3);
}
y_offset_ = start_y_ + ((target_y_ - start_y_) * t);
if (animation_timer_ >= ANIMATION_DURATION_S) {
y_offset_ = target_y_;
animating_ = false;
animation_timer_ = 0.0F; // Reset timer
if (target_y_ < 0.0F) {
visible_ = false;
}
}
}
// Dibuja el mensaje en pantalla si está visible
void UIMessage::render() {
if (visible_) {
text_renderer_->writeDX(
Text::COLOR | Text::CENTER,
base_x_,
base_y_ + y_offset_,
text_,
-2,
color_);
}
}
// Devuelve true si el mensaje está visible actualmente
auto UIMessage::isVisible() const -> bool {
return visible_;
}
// Permite actualizar la posición del mensaje (por ejemplo, si el menú se mueve)
void UIMessage::setPosition(float new_base_x, float new_base_y) {
base_x_ = new_base_x;
base_y_ = new_base_y;
}

View File

@@ -0,0 +1,56 @@
#pragma once
#include <memory> // Para shared_ptr
#include <string> // Para string
#include "color.hpp" // Para Color
class Text;
// Clase para mostrar mensajes animados en la interfaz de usuario
class UIMessage {
public:
// Constructor: recibe el renderizador de texto, el mensaje y el color
UIMessage(std::shared_ptr<Text> text_renderer, std::string message_text, const Color& color);
// Muestra el mensaje con animación de entrada
void show();
// Oculta el mensaje con animación de salida
void hide();
// Actualiza el estado de la animación (debe llamarse cada frame)
void update(float delta_time);
// Dibuja el mensaje en pantalla si está visible
void render();
// Indica si el mensaje está visible actualmente
[[nodiscard]] auto isVisible() const -> bool;
// Permite actualizar la posición del mensaje (por ejemplo, si el menú se mueve)
void setPosition(float new_base_x, float new_base_y);
private:
// --- Configuración ---
std::shared_ptr<Text> text_renderer_; // Renderizador de texto
std::string text_; // Texto del mensaje a mostrar
Color color_; // Color del texto
// --- Estado ---
bool visible_ = false; // Indica si el mensaje está visible
bool animating_ = false; // Indica si el mensaje está en proceso de animación
float base_x_ = 0.0F; // Posición X base donde se muestra el mensaje
float base_y_ = 0.0F; // Posición Y base donde se muestra el mensaje
float y_offset_ = 0.0F; // Desplazamiento vertical actual del mensaje (para animación)
// --- Animación ---
float start_y_ = 0.0F; // Posición Y inicial de la animación
float target_y_ = 0.0F; // Posición Y objetivo de la animación
float animation_timer_ = 0.0F; // Timer actual de la animación en segundos
static constexpr float ANIMATION_DURATION_S = 0.133F; // Duración total de la animación (8 frames @ 60fps)
static constexpr float DESP = -8.0F; // Distancia a desplazarse
// Actualiza la interpolación de la animación (ease out/in cubic)
void updateAnimation(float delta_time);
};

View File

@@ -0,0 +1,416 @@
#include "window_message.hpp"
#include <algorithm>
#include <utility>
#include "param.hpp"
#include "screen.hpp"
#include "text.hpp"
WindowMessage::WindowMessage(
std::shared_ptr<Text> text_renderer,
std::string title,
const Config& config)
: text_renderer_(std::move(text_renderer)),
config_(config),
title_(std::move(title)),
title_style_(Text::CENTER | Text::COLOR, config_.title_color, config_.title_color, 0, -2),
text_style_(Text::CENTER | Text::COLOR, config_.text_color, config_.text_color, 0, -2) {
}
void WindowMessage::render() {
if (!visible_) {
return;
}
SDL_Renderer* renderer = Screen::get()->getRenderer();
// Dibujar fondo con transparencia
SDL_SetRenderDrawColor(renderer, config_.bg_color.r, config_.bg_color.g, config_.bg_color.b, config_.bg_color.a);
SDL_RenderFillRect(renderer, &rect_);
// Dibujar borde
SDL_SetRenderDrawColor(renderer, config_.border_color.r, config_.border_color.g, config_.border_color.b, config_.border_color.a);
SDL_RenderRect(renderer, &rect_);
// Solo mostrar contenido si no estamos en animación de show/hide
if (shouldShowContent()) {
float current_y = rect_.y + config_.padding;
float available_width = getAvailableTextWidth();
// Dibujar título si existe
if (!title_.empty()) {
std::string visible_title = getTruncatedText(title_, available_width);
if (!visible_title.empty()) {
text_renderer_->writeStyle(
rect_.x + (rect_.w / 2.0F),
current_y,
visible_title,
title_style_);
}
current_y += text_renderer_->getCharacterSize() + config_.title_separator_spacing;
// Línea separadora debajo del título (solo si hay título visible)
if (!visible_title.empty()) {
SDL_SetRenderDrawColor(renderer, config_.border_color.r, config_.border_color.g, config_.border_color.b, config_.border_color.a);
SDL_RenderLine(renderer,
rect_.x + config_.padding,
current_y - (config_.title_separator_spacing / 2.0F),
rect_.x + rect_.w - config_.padding,
current_y - (config_.title_separator_spacing / 2.0F));
}
}
// Dibujar textos
for (const auto& text : texts_) {
std::string visible_text = getTruncatedText(text, available_width);
if (!visible_text.empty()) {
text_renderer_->writeStyle(
rect_.x + (rect_.w / 2.0F),
current_y,
visible_text,
text_style_);
}
current_y += text_renderer_->getCharacterSize() + config_.line_spacing;
}
}
}
void WindowMessage::update(float delta_time) {
// Actualizar animaciones
if (show_hide_animation_.active || resize_animation_.active) {
updateAnimation(delta_time);
}
}
void WindowMessage::show() {
if (visible_) {
return; // Ya visible
}
visible_ = true;
ensureTextFits();
// Detener cualquier animación anterior
resize_animation_.stop();
// Iniciar animación de mostrar desde tamaño 0
show_hide_animation_.startShow(rect_.w, rect_.h);
rect_.w = 0.0F;
rect_.h = 0.0F;
updatePosition(); // Reposicionar con tamaño 0
}
void WindowMessage::hide() {
if (!visible_) {
return; // Ya oculto
}
// Detener cualquier animación anterior
resize_animation_.stop();
// Guardar el tamaño actual para la animación
show_hide_animation_.target_width = rect_.w;
show_hide_animation_.target_height = rect_.h;
// Iniciar animación de ocultar hacia tamaño 0
show_hide_animation_.startHide();
}
void WindowMessage::setTitle(const std::string& title) {
title_ = title;
triggerAutoResize();
}
void WindowMessage::setText(const std::string& text) {
texts_.clear();
texts_.push_back(text);
triggerAutoResize();
}
void WindowMessage::setTexts(const std::vector<std::string>& texts) {
texts_ = texts;
triggerAutoResize();
}
void WindowMessage::addText(const std::string& text) {
texts_.push_back(text);
triggerAutoResize();
}
void WindowMessage::clearTexts() {
texts_.clear();
triggerAutoResize();
}
void WindowMessage::setPosition(float x, float y, PositionMode mode) {
anchor_ = {.x = x, .y = y};
position_mode_ = mode;
updatePosition();
}
void WindowMessage::setSize(float width, float height) {
rect_.w = width;
rect_.h = height;
updatePosition(); // Reposicionar después de cambiar el tamaño
}
void WindowMessage::centerOnScreen() {
setPosition(getScreenWidth() / 2.0F, getScreenHeight() / 2.0F, PositionMode::CENTERED);
}
void WindowMessage::autoSize() {
if (show_hide_animation_.active) {
return; // No redimensionar durante show/hide
}
if (resize_animation_.active) {
resize_animation_.stop(); // Detener animación anterior
}
float old_width = rect_.w;
float old_height = rect_.h;
calculateAutoSize();
// Solo animar si hay cambio en el tamaño y la ventana está visible
if (visible_ && (old_width != rect_.w || old_height != rect_.h)) {
resize_animation_.start(old_width, old_height, rect_.w, rect_.h);
// Restaurar el tamaño anterior para que la animación funcione
rect_.w = old_width;
rect_.h = old_height;
} else {
updatePosition(); // Reposicionar después de ajustar el tamaño
}
}
void WindowMessage::updateStyles() {
title_style_ = Text::Style(Text::CENTER | Text::COLOR, config_.title_color, config_.title_color, 0, -2);
text_style_ = Text::Style(Text::CENTER | Text::COLOR, config_.text_color, config_.text_color, 0, -2);
}
void WindowMessage::updatePosition() {
switch (position_mode_) {
case PositionMode::CENTERED:
rect_.x = anchor_.x - (rect_.w / 2.0F);
rect_.y = anchor_.y - (rect_.h / 2.0F);
break;
case PositionMode::FIXED:
rect_.x = anchor_.x;
rect_.y = anchor_.y;
break;
}
// Asegurar que la ventana esté dentro de los límites de la pantalla
rect_.x = std::max(0.0F, std::min(rect_.x, getScreenWidth() - rect_.w));
rect_.y = std::max(0.0F, std::min(rect_.y, getScreenHeight() - rect_.h));
}
void WindowMessage::ensureTextFits() {
float required_width = calculateContentWidth() + (config_.padding * 2) + config_.text_safety_margin;
float required_height = calculateContentHeight() + (config_.padding * 2) + config_.text_safety_margin;
// Verificar si el tamaño actual es suficiente
if (rect_.w < required_width || rect_.h < required_height) {
autoSize(); // Recalcular tamaño automáticamente
}
}
void WindowMessage::calculateAutoSize() {
float content_width = calculateContentWidth();
float content_height = calculateContentHeight();
// Calcular dimensiones con padding y margen de seguridad
rect_.w = content_width + (config_.padding * 2) + config_.text_safety_margin;
rect_.h = content_height + (config_.padding * 2);
// Aplicar límites mínimos
rect_.w = std::max(rect_.w, config_.min_width);
rect_.h = std::max(rect_.h, config_.min_height);
// Aplicar límites máximos basados en el tamaño de pantalla
float max_width = getScreenWidth() * config_.max_width_ratio;
float max_height = getScreenHeight() * config_.max_height_ratio;
rect_.w = std::min(rect_.w, max_width);
rect_.h = std::min(rect_.h, max_height);
}
auto WindowMessage::calculateContentHeight() const -> float {
float height = 0;
// Altura del título
if (!title_.empty()) {
height += text_renderer_->getCharacterSize() + config_.title_separator_spacing;
}
// Altura de los textos
if (!texts_.empty()) {
height += (texts_.size() * text_renderer_->getCharacterSize());
if (texts_.size() > 1) {
height += ((texts_.size() - 1) * config_.line_spacing);
}
}
return height;
}
auto WindowMessage::calculateContentWidth() const -> float {
float max_width = config_.min_width - (config_.padding * 2); // Ancho mínimo sin padding
// Ancho del título
if (!title_.empty()) {
float title_width = text_renderer_->length(title_, -2);
max_width = std::max(max_width, title_width);
}
// Ancho de los textos
for (const auto& text : texts_) {
float text_width = text_renderer_->length(text, -2);
max_width = std::max(max_width, text_width);
}
return max_width;
}
auto WindowMessage::getScreenWidth() -> float {
return param.game.width;
}
auto WindowMessage::getScreenHeight() -> float {
return param.game.height;
}
void WindowMessage::triggerAutoResize() {
if (auto_resize_enabled_) {
autoSize();
}
}
void WindowMessage::updateAnimation(float delta_time) {
if (show_hide_animation_.active) {
updateShowHideAnimation(delta_time);
}
if (resize_animation_.active) {
updateResizeAnimation(delta_time);
}
}
void WindowMessage::updateShowHideAnimation(float delta_time) {
if (!show_hide_animation_.active) {
return;
}
show_hide_animation_.elapsed += delta_time;
if (show_hide_animation_.isFinished(config_.animation_duration)) {
// Animación terminada
if (show_hide_animation_.type == ShowHideAnimation::Type::SHOWING) {
// Mostrar completado
rect_.w = show_hide_animation_.target_width;
rect_.h = show_hide_animation_.target_height;
} else if (show_hide_animation_.type == ShowHideAnimation::Type::HIDING) {
// Ocultar completado
rect_.w = 0.0F;
rect_.h = 0.0F;
visible_ = false;
}
show_hide_animation_.stop();
updatePosition();
} else {
// Interpolar el tamaño
float progress = easeOut(show_hide_animation_.getProgress(config_.animation_duration));
if (show_hide_animation_.type == ShowHideAnimation::Type::SHOWING) {
// Crecer desde 0 hasta el tamaño objetivo
rect_.w = show_hide_animation_.target_width * progress;
rect_.h = show_hide_animation_.target_height * progress;
} else if (show_hide_animation_.type == ShowHideAnimation::Type::HIDING) {
// Decrecer desde el tamaño actual hasta 0
rect_.w = show_hide_animation_.target_width * (1.0F - progress);
rect_.h = show_hide_animation_.target_height * (1.0F - progress);
}
updatePosition(); // Mantener la posición centrada durante la animación
}
}
void WindowMessage::updateResizeAnimation(float delta_time) {
if (!resize_animation_.active) {
return;
}
resize_animation_.elapsed += delta_time;
if (resize_animation_.isFinished(config_.animation_duration)) {
// Animación terminada
rect_.w = resize_animation_.target_width;
rect_.h = resize_animation_.target_height;
resize_animation_.stop();
updatePosition();
} else {
// Interpolar el tamaño
float progress = easeOut(resize_animation_.getProgress(config_.animation_duration));
rect_.w = resize_animation_.start_width +
((resize_animation_.target_width - resize_animation_.start_width) * progress);
rect_.h = resize_animation_.start_height +
((resize_animation_.target_height - resize_animation_.start_height) * progress);
updatePosition(); // Mantener la posición centrada durante la animación
}
}
auto WindowMessage::shouldShowContent() const -> bool {
// No mostrar contenido durante animaciones de show/hide
return !show_hide_animation_.active;
}
auto WindowMessage::easeOut(float t) -> float {
// Función de suavizado ease-out cuadrática
return 1.0F - ((1.0F - t) * (1.0F - t));
}
auto WindowMessage::getAvailableTextWidth() const -> float {
// Ancho disponible = ancho total - padding en ambos lados
return rect_.w - (config_.padding * 2.0F);
}
auto WindowMessage::getTruncatedText(const std::string& text, float available_width) const -> std::string {
if (text.empty()) {
return text;
}
// Si el texto completo cabe, devolverlo tal como está
int text_width = text_renderer_->length(text, -2);
if (text_width <= available_width) {
return text;
}
// Si no hay espacio suficiente, devolver string vacío
if (available_width < 10.0F) { // Mínimo espacio para al menos un carácter
return "";
}
// Buscar cuántos caracteres caben usando búsqueda binaria
int left = 0;
int right = text.length();
int best_length = 0;
while (left <= right) {
int mid = (left + right) / 2;
std::string partial = text.substr(0, mid);
int partial_width = text_renderer_->length(partial, -2);
if (partial_width <= available_width) {
best_length = mid;
left = mid + 1;
} else {
right = mid - 1;
}
}
return text.substr(0, best_length);
}

View File

@@ -0,0 +1,244 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FPoint, SDL_FRect
#include <algorithm> // Para min
#include <cstdint> // Para std::uint8_t
#include <memory> // Para allocator, shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include "color.hpp" // Para Color
#include "param.hpp" // Para param
#include "text.hpp" // Para Text
class WindowMessage {
public:
enum class PositionMode : std::uint8_t {
CENTERED, // La ventana se centra en el punto especificado
FIXED // La esquina superior izquierda coincide con el punto
};
struct Config {
// Colores
Color bg_color;
Color border_color;
Color title_color;
Color text_color;
// Espaciado y dimensiones
float padding{15.0F};
float line_spacing{5.0F};
float title_separator_spacing{10.0F}; // Espacio extra para separador del título
// Límites de tamaño
float min_width{200.0F};
float min_height{100.0F};
float max_width_ratio{0.8F}; // % máximo de ancho de pantalla
float max_height_ratio{0.8F}; // % máximo de alto de pantalla
// Margen de seguridad para texto
float text_safety_margin{20.0F}; // Margen extra para evitar texto cortado
// Animaciones
float animation_duration{0.3F}; // Duración en segundos para todas las animaciones
// Constructor con valores por defecto
Config()
: bg_color{40, 40, 60, 220},
border_color{100, 100, 120, 255},
title_color{255, 255, 255, 255},
text_color{200, 200, 200, 255} {}
// Constructor que convierte desde ParamServiceMenu::WindowMessage
Config(const ParamServiceMenu::WindowMessage& param_config)
: bg_color(param_config.bg_color),
border_color(param_config.border_color),
title_color(param_config.title_color),
text_color(param_config.text_color),
padding(param_config.padding),
line_spacing(param_config.line_spacing),
title_separator_spacing(param_config.title_separator_spacing),
min_width(param_config.min_width),
min_height(param_config.min_height),
max_width_ratio(param_config.max_width_ratio),
max_height_ratio(param_config.max_height_ratio),
text_safety_margin(param_config.text_safety_margin),
animation_duration(param_config.animation_duration) {}
};
WindowMessage(
std::shared_ptr<Text> text_renderer,
std::string title = "",
const Config& config = Config{});
// Métodos principales
void render();
void update(float delta_time);
// Control de visibilidad
void show();
void hide();
[[nodiscard]] auto isVisible() const -> bool { return visible_; }
[[nodiscard]] auto isFullyVisible() const -> bool { return visible_ && !show_hide_animation_.active; }
[[nodiscard]] auto isAnimating() const -> bool { return resize_animation_.active || show_hide_animation_.active; }
// Configuración de contenido
void setTitle(const std::string& title);
void setText(const std::string& text);
void setTexts(const std::vector<std::string>& texts);
void addText(const std::string& text);
void clearTexts();
// Control de redimensionado automático
void enableAutoResize(bool enabled) { auto_resize_enabled_ = enabled; }
[[nodiscard]] auto isAutoResizeEnabled() const -> bool { return auto_resize_enabled_; }
// Configuración de posición y tamaño
void setPosition(float x, float y, PositionMode mode = PositionMode::CENTERED);
void setSize(float width, float height);
void centerOnScreen();
void autoSize(); // Ajusta automáticamente al contenido y reposiciona si es necesario
// Configuración de colores
void setBackgroundColor(const Color& color) { config_.bg_color = color; }
void setBorderColor(const Color& color) { config_.border_color = color; }
void setTitleColor(const Color& color) {
config_.title_color = color;
updateStyles();
}
void setTextColor(const Color& color) {
config_.text_color = color;
updateStyles();
}
// Configuración de espaciado
void setPadding(float padding) { config_.padding = padding; }
void setLineSpacing(float spacing) { config_.line_spacing = spacing; }
// Configuración avanzada
void setConfig(const Config& config) {
config_ = config;
updateStyles();
}
[[nodiscard]] auto getConfig() const -> const Config& { return config_; }
// Getters
[[nodiscard]] auto getRect() const -> const SDL_FRect& { return rect_; }
[[nodiscard]] auto getPositionMode() const -> PositionMode { return position_mode_; }
[[nodiscard]] auto getAnchorPoint() const -> SDL_FPoint { return anchor_; }
private:
std::shared_ptr<Text> text_renderer_;
Config config_;
// Estado de visibilidad y redimensionado
bool visible_ = false;
bool auto_resize_enabled_ = true; // Por defecto habilitado
// Contenido
std::string title_;
std::vector<std::string> texts_;
// Posición y tamaño
SDL_FRect rect_{.x = 0, .y = 0, .w = 300, .h = 200};
PositionMode position_mode_ = PositionMode::CENTERED;
SDL_FPoint anchor_{.x = 0.0F, .y = 0.0F};
// Animación de redimensionado
struct ResizeAnimation {
bool active = false;
float start_width, start_height;
float target_width, target_height;
float elapsed = 0.0F;
void start(float from_w, float from_h, float to_w, float to_h) {
start_width = from_w;
start_height = from_h;
target_width = to_w;
target_height = to_h;
elapsed = 0.0F;
active = true;
}
void stop() {
active = false;
elapsed = 0.0F;
}
[[nodiscard]] auto isFinished(float duration) const -> bool {
return elapsed >= duration;
}
[[nodiscard]] auto getProgress(float duration) const -> float {
return std::min(elapsed / duration, 1.0F);
}
} resize_animation_;
// Animación de mostrar/ocultar
struct ShowHideAnimation {
enum class Type : std::uint8_t { NONE,
SHOWING,
HIDING };
Type type = Type::NONE;
bool active = false;
float target_width, target_height; // Tamaño final al mostrar
float elapsed = 0.0F;
void startShow(float to_w, float to_h) {
type = Type::SHOWING;
target_width = to_w;
target_height = to_h;
elapsed = 0.0F;
active = true;
}
void startHide() {
type = Type::HIDING;
elapsed = 0.0F;
active = true;
}
void stop() {
type = Type::NONE;
active = false;
elapsed = 0.0F;
}
[[nodiscard]] auto isFinished(float duration) const -> bool {
return elapsed >= duration;
}
[[nodiscard]] auto getProgress(float duration) const -> float {
return std::min(elapsed / duration, 1.0F);
}
} show_hide_animation_;
// Estilos
Text::Style title_style_;
Text::Style text_style_;
// Métodos privados
void calculateAutoSize();
void updatePosition(); // Actualiza la posición según el modo y punto de anclaje
void updateStyles(); // Actualiza los estilos de texto cuando cambian los colores
void ensureTextFits(); // Verifica y ajusta para que todo el texto sea visible
void triggerAutoResize(); // Inicia redimensionado automático si está habilitado
void updateAnimation(float delta_time); // Actualiza la animación de redimensionado
void updateShowHideAnimation(float delta_time); // Actualiza la animación de mostrar/ocultar
void updateResizeAnimation(float delta_time); // Actualiza la animación de redimensionado
// Función de suavizado (ease-out)
[[nodiscard]] static auto easeOut(float t) -> float;
// Métodos para manejo de texto durante animación
[[nodiscard]] auto getTruncatedText(const std::string& text, float available_width) const -> std::string;
[[nodiscard]] auto getAvailableTextWidth() const -> float;
[[nodiscard]] auto shouldShowContent() const -> bool; // Si mostrar el contenido (texto, líneas, etc.)
[[nodiscard]] auto calculateContentHeight() const -> float;
[[nodiscard]] auto calculateContentWidth() const -> float;
[[nodiscard]] static auto getScreenWidth() -> float;
[[nodiscard]] static auto getScreenHeight() -> float;
};