reestructuració
This commit is contained in:
386
source/game/entities/balloon.cpp
Normal file
386
source/game/entities/balloon.cpp
Normal 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;
|
||||
}
|
||||
305
source/game/entities/balloon.hpp
Normal file
305
source/game/entities/balloon.hpp
Normal 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)
|
||||
};
|
||||
130
source/game/entities/bullet.cpp
Normal file
130
source/game/entities/bullet.cpp
Normal 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_);
|
||||
}
|
||||
76
source/game/entities/bullet.hpp
Normal file
76
source/game/entities/bullet.hpp
Normal 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
|
||||
};
|
||||
58
source/game/entities/explosions.cpp
Normal file
58
source/game/entities/explosions.cpp
Normal 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;
|
||||
}
|
||||
47
source/game/entities/explosions.hpp
Normal file
47
source/game/entities/explosions.hpp
Normal 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
|
||||
};
|
||||
228
source/game/entities/item.cpp
Normal file
228
source/game/entities/item.cpp
Normal 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;
|
||||
}
|
||||
100
source/game/entities/item.hpp
Normal file
100
source/game/entities/item.hpp
Normal 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é
|
||||
};
|
||||
1192
source/game/entities/player.cpp
Normal file
1192
source/game/entities/player.cpp
Normal file
File diff suppressed because it is too large
Load Diff
405
source/game/entities/player.hpp
Normal file
405
source/game/entities/player.hpp
Normal 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
|
||||
};
|
||||
225
source/game/entities/tabe.cpp
Normal file
225
source/game/entities/tabe.cpp
Normal 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);
|
||||
}
|
||||
150
source/game/entities/tabe.hpp
Normal file
150
source/game/entities/tabe.hpp
Normal 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
|
||||
};
|
||||
424
source/game/gameplay/balloon_formations.cpp
Normal file
424
source/game/gameplay/balloon_formations.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
110
source/game/gameplay/balloon_formations.hpp
Normal file
110
source/game/gameplay/balloon_formations.hpp
Normal 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
|
||||
};
|
||||
427
source/game/gameplay/balloon_manager.cpp
Normal file
427
source/game/gameplay/balloon_manager.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
115
source/game/gameplay/balloon_manager.hpp
Normal file
115
source/game/gameplay/balloon_manager.hpp
Normal 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();
|
||||
};
|
||||
104
source/game/gameplay/bullet_manager.cpp
Normal file
104
source/game/gameplay/bullet_manager.cpp
Normal 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);
|
||||
}
|
||||
76
source/game/gameplay/bullet_manager.hpp
Normal file
76
source/game/gameplay/bullet_manager.hpp
Normal 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
|
||||
};
|
||||
49
source/game/gameplay/cooldown.hpp
Normal file
49
source/game/gameplay/cooldown.hpp
Normal 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};
|
||||
};
|
||||
38
source/game/gameplay/difficulty.cpp
Normal file
38
source/game/gameplay/difficulty.cpp
Normal 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
|
||||
28
source/game/gameplay/difficulty.hpp
Normal file
28
source/game/gameplay/difficulty.hpp
Normal 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
|
||||
126
source/game/gameplay/enter_name.cpp
Normal file
126
source/game/gameplay/enter_name.cpp
Normal 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_;
|
||||
}
|
||||
42
source/game/gameplay/enter_name.hpp
Normal file
42
source/game/gameplay/enter_name.hpp
Normal 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
|
||||
};
|
||||
279
source/game/gameplay/game_logo.cpp
Normal file
279
source/game/gameplay/game_logo.cpp
Normal 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);
|
||||
}
|
||||
125
source/game/gameplay/game_logo.hpp
Normal file
125
source/game/gameplay/game_logo.hpp
Normal 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)
|
||||
};
|
||||
326
source/game/gameplay/manage_hiscore_table.cpp
Normal file
326
source/game/gameplay/manage_hiscore_table.cpp
Normal 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;
|
||||
}
|
||||
59
source/game/gameplay/manage_hiscore_table.hpp
Normal file
59
source/game/gameplay/manage_hiscore_table.hpp
Normal 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;
|
||||
};
|
||||
809
source/game/gameplay/scoreboard.cpp
Normal file
809
source/game/gameplay/scoreboard.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
169
source/game/gameplay/scoreboard.hpp
Normal file
169
source/game/gameplay/scoreboard.hpp
Normal 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
|
||||
};
|
||||
367
source/game/gameplay/stage.cpp
Normal file
367
source/game/gameplay/stage.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
114
source/game/gameplay/stage.hpp
Normal file
114
source/game/gameplay/stage.hpp
Normal 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
868
source/game/options.cpp
Normal 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
360
source/game/options.hpp
Normal 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
|
||||
742
source/game/scenes/credits.cpp
Normal file
742
source/game/scenes/credits.cpp
Normal 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_);
|
||||
}
|
||||
}
|
||||
171
source/game/scenes/credits.hpp
Normal file
171
source/game/scenes/credits.hpp
Normal 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
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
353
source/game/scenes/game.hpp
Normal 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
|
||||
};
|
||||
399
source/game/scenes/hiscore_table.cpp
Normal file
399
source/game/scenes/hiscore_table.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
93
source/game/scenes/hiscore_table.hpp
Normal file
93
source/game/scenes/hiscore_table.hpp
Normal 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
|
||||
};
|
||||
387
source/game/scenes/instructions.cpp
Normal file
387
source/game/scenes/instructions.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
102
source/game/scenes/instructions.hpp
Normal file
102
source/game/scenes/instructions.hpp
Normal 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
|
||||
};
|
||||
570
source/game/scenes/intro.cpp
Normal file
570
source/game/scenes/intro.cpp
Normal 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_);
|
||||
}
|
||||
133
source/game/scenes/intro.hpp
Normal file
133
source/game/scenes/intro.hpp
Normal 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
212
source/game/scenes/logo.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
95
source/game/scenes/logo.hpp
Normal file
95
source/game/scenes/logo.hpp
Normal 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
|
||||
};
|
||||
524
source/game/scenes/title.cpp
Normal file
524
source/game/scenes/title.cpp
Normal 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 = ¶m.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 = ¶m.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;
|
||||
}
|
||||
142
source/game/scenes/title.hpp
Normal file
142
source/game/scenes/title.hpp
Normal 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
|
||||
};
|
||||
74
source/game/ui/menu_option.cpp
Normal file
74
source/game/ui/menu_option.cpp
Normal 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
|
||||
}
|
||||
241
source/game/ui/menu_option.hpp
Normal file
241
source/game/ui/menu_option.hpp
Normal 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_;
|
||||
};
|
||||
417
source/game/ui/menu_renderer.cpp
Normal file
417
source/game/ui/menu_renderer.cpp
Normal 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; }
|
||||
129
source/game/ui/menu_renderer.hpp
Normal file
129
source/game/ui/menu_renderer.hpp
Normal 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
310
source/game/ui/notifier.cpp
Normal 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
112
source/game/ui/notifier.hpp
Normal 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
|
||||
};
|
||||
733
source/game/ui/service_menu.cpp
Normal file
733
source/game/ui/service_menu.cpp
Normal 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_);
|
||||
}
|
||||
}
|
||||
}
|
||||
129
source/game/ui/service_menu.hpp
Normal file
129
source/game/ui/service_menu.hpp
Normal 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;
|
||||
};
|
||||
98
source/game/ui/ui_message.cpp
Normal file
98
source/game/ui/ui_message.cpp
Normal 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;
|
||||
}
|
||||
56
source/game/ui/ui_message.hpp
Normal file
56
source/game/ui/ui_message.hpp
Normal 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);
|
||||
};
|
||||
416
source/game/ui/window_message.cpp
Normal file
416
source/game/ui/window_message.cpp
Normal 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);
|
||||
}
|
||||
244
source/game/ui/window_message.hpp
Normal file
244
source/game/ui/window_message.hpp
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user