reestructuració
This commit is contained in:
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
|
||||
};
|
||||
Reference in New Issue
Block a user