segon commit

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

View File

@@ -0,0 +1,163 @@
#include "game/gameplay/cheevos.hpp"
#include <SDL3/SDL.h>
#include <algorithm> // Para std::count_if
#include <cstddef> // Para NULL
#include <fstream> // Para basic_ostream, operator<<, basic_ofstream
#include <iostream> // Para cout, cerr
#include <utility>
#include "core/locale/locale.hpp" // Para Locale
#include "game/options.hpp" // Para Options, options
#include "game/ui/notifier.hpp" // Para Notifier
// [SINGLETON]
Cheevos* Cheevos::cheevos = nullptr;
// [SINGLETON] Crearemos el objeto con esta función estática
void Cheevos::init(const std::string& file) { // NOLINT(readability-convert-member-functions-to-static)
Cheevos::cheevos = new Cheevos(file);
}
// [SINGLETON] Destruiremos el objeto con esta función estática
void Cheevos::destroy() {
delete Cheevos::cheevos;
}
// [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
auto Cheevos::get() -> Cheevos* {
return Cheevos::cheevos;
}
// Constructor
Cheevos::Cheevos(std::string file)
: file_(std::move(file)) {
init();
loadFromFile();
}
// Destructor
Cheevos::~Cheevos() {
saveToFile();
}
// Inicializa los logros
void Cheevos::init() { // NOLINT(readability-convert-member-functions-to-static)
cheevos_list_.clear();
auto* loc = Locale::get();
cheevos_list_.emplace_back(Achievement{.id = 1, .caption = loc->get("achievements.c1"), .description = loc->get("achievements.d1"), .icon = 2});
cheevos_list_.emplace_back(Achievement{.id = 2, .caption = loc->get("achievements.c2"), .description = loc->get("achievements.d2"), .icon = 2});
cheevos_list_.emplace_back(Achievement{.id = 3, .caption = loc->get("achievements.c3"), .description = loc->get("achievements.d3"), .icon = 2});
cheevos_list_.emplace_back(Achievement{.id = 4, .caption = loc->get("achievements.c4"), .description = loc->get("achievements.d4"), .icon = 2});
cheevos_list_.emplace_back(Achievement{.id = 5, .caption = loc->get("achievements.c5"), .description = loc->get("achievements.d5"), .icon = 2});
cheevos_list_.emplace_back(Achievement{.id = 6, .caption = loc->get("achievements.c6"), .description = loc->get("achievements.d6"), .icon = 2});
cheevos_list_.emplace_back(Achievement{.id = 7, .caption = loc->get("achievements.c7"), .description = loc->get("achievements.d7"), .icon = 2});
cheevos_list_.emplace_back(Achievement{.id = 8, .caption = loc->get("achievements.c8"), .description = loc->get("achievements.d8"), .icon = 2});
cheevos_list_.emplace_back(Achievement{.id = 9, .caption = loc->get("achievements.c9"), .description = loc->get("achievements.d9"), .icon = 2});
cheevos_list_.emplace_back(Achievement{.id = 10, .caption = loc->get("achievements.c10"), .description = loc->get("achievements.d10"), .icon = 2});
cheevos_list_.emplace_back(Achievement{.id = 11, .caption = loc->get("achievements.c11"), .description = loc->get("achievements.d11"), .icon = 2});
cheevos_list_.emplace_back(Achievement{.id = 12, .caption = loc->get("achievements.c12"), .description = loc->get("achievements.d12"), .icon = 2});
}
// Busca un logro por id y devuelve el indice
auto Cheevos::find(int id) -> int { // NOLINT(readability-convert-member-functions-to-static)
for (int i = 0; i < (int)cheevos_list_.size(); ++i) {
if (cheevos_list_[i].id == id) {
return i;
}
}
return -1;
}
// Desbloquea un logro
void Cheevos::unlock(int id) {
const int INDEX = find(id);
// Si el índice es inválido, el logro no es válido, ya está completado o el sistema de logros no está habilitado, no hacemos nada
if (INDEX == -1 || !cheevos_list_.at(INDEX).obtainable || cheevos_list_.at(INDEX).completed || !enabled_) {
return;
}
// Marcar el logro como completado
cheevos_list_.at(INDEX).completed = true;
// Mostrar notificación en la pantalla
Notifier::get()->show({Locale::get()->get("achievements.header"), cheevos_list_.at(INDEX).caption}, Notifier::Style::CHEEVO, -1, false);
// Guardar el estado de los logros
saveToFile();
}
// Invalida un logro
void Cheevos::setUnobtainable(int id) {
const int INDEX = find(id);
// Si el índice es válido, se invalida el logro
if (INDEX != -1) {
cheevos_list_.at(INDEX).obtainable = false;
}
}
// Carga el estado de los logros desde un fichero
void Cheevos::loadFromFile() { // NOLINT(readability-convert-member-functions-to-static)
std::ifstream file(file_, std::ios::binary);
// El fichero no existe
if (!file) {
std::cout << "Warning: Unable to open " << file_ << "! Creating new file..." << '\n';
// Crea el fichero en modo escritura (binario)
std::ofstream new_file(file_, std::ios::binary);
if (new_file) {
std::cout << "New " << file_ << " created!" << '\n';
// Guarda la información
for (const auto& cheevo : cheevos_list_) {
new_file.write(reinterpret_cast<const char*>(&cheevo.completed), sizeof(bool));
}
} else {
std::cerr << "Error: Unable to create " << file_ << "!" << '\n';
}
}
// El fichero existe
else {
std::cout << "Reading " << file_ << '\n';
// Carga los datos
for (auto& cheevo : cheevos_list_) {
file.read(reinterpret_cast<char*>(&cheevo.completed), sizeof(bool));
}
}
}
// Guarda el estado de los logros en un fichero
void Cheevos::saveToFile() {
// Abre el fichero en modo escritura (binario)
SDL_IOStream* file = SDL_IOFromFile(this->file_.c_str(), "w+b");
if (file != nullptr) {
// Guarda la información
for (auto& i : cheevos_list_) {
SDL_WriteIO(file, &i.completed, sizeof(bool));
}
// Cierra el fichero
SDL_CloseIO(file);
} else {
std::cout << "Error: Unable to save file! " << SDL_GetError() << '\n';
}
}
// Devuelve el número total de logros desbloqueados
auto Cheevos::getTotalUnlockedAchievements() -> int {
return std::count_if(cheevos_list_.begin(), cheevos_list_.end(), [](const auto& cheevo) -> bool { return cheevo.completed; });
}
// Elimina el estado "no obtenible"
void Cheevos::clearUnobtainableState() {
for (auto& cheevo : cheevos_list_) {
cheevo.obtainable = true;
}
}

View File

@@ -0,0 +1,55 @@
#pragma once
#include <string> // Para string
#include <utility>
#include <vector> // Para vector
class Cheevos {
public:
// Tipos anidados (públicos porque se usan en la interfaz)
struct Achievement {
int id{0}; // Identificador del logro
std::string caption; // Texto con el nombre del logro
std::string description; // Texto que describe el logro
int icon{0}; // Indice del icono a utilizar en la notificación
bool completed{false}; // Indica si se ha obtenido el logro
bool obtainable{true}; // Indica si se puede obtener el logro
};
using Achievements = std::vector<Achievement>; // Type alias para vector de logros
// Gestión singleton
static void init(const std::string& file); // Inicialización
static void destroy(); // Destrucción
static auto get() -> Cheevos*; // Acceso al singleton
// Gestión de logros
void unlock(int id); // Desbloquea un logro
void setUnobtainable(int id); // Invalida un logro
void clearUnobtainableState(); // Elimina el estado "no obtenible"
void enable(bool value) { enabled_ = value; } // Habilita o deshabilita los logros
// Consultas
[[nodiscard]] auto list() const -> const Achievements& { return cheevos_list_; } // Lista los logros
auto getTotalUnlockedAchievements() -> int; // Devuelve logros desbloqueados
auto size() -> int { return cheevos_list_.size(); } // Devuelve número total de logros
private:
// Constantes singleton
static Cheevos* cheevos; // [SINGLETON] Objeto privado
// Métodos privados
void init(); // Inicializa los logros
auto find(int id) -> int; // Busca un logro por id y devuelve el índice
void loadFromFile(); // Carga el estado de los logros desde un fichero
void saveToFile(); // Guarda el estado de los logros en un fichero
// Constructor y destructor privados [SINGLETON]
explicit Cheevos(std::string file);
~Cheevos();
// Variables miembro
Achievements cheevos_list_; // Listado de logros
bool enabled_{true}; // Indica si los logros se pueden obtener
std::string file_; // Fichero donde leer/almacenar el estado de los logros
};

View File

@@ -0,0 +1,510 @@
#include "collision_map.hpp"
#include <algorithm> // Para std::ranges::any_of
#ifdef _DEBUG
#include "core/system/debug.hpp" // Para Debug
#endif
#include "utils/defines.hpp" // Para Collision
// Constructor
CollisionMap::CollisionMap(std::vector<int> tile_map, int tile_set_width, int conveyor_belt_direction)
: tile_map_(std::move(tile_map)),
tile_set_width_(tile_set_width),
conveyor_belt_direction_(conveyor_belt_direction) {
// Inicializa todas las superficies de colisión
initializeSurfaces();
}
// Inicializa todas las superficies de colisión
void CollisionMap::initializeSurfaces() {
setBottomSurfaces();
setTopSurfaces();
setLeftSurfaces();
setRightSurfaces();
setLeftSlopes();
setRightSlopes();
setAutoSurfaces();
}
// Devuelve el tipo de tile que hay en ese pixel
auto CollisionMap::getTile(SDL_FPoint point) const -> Tile {
const int ROW = static_cast<int>(point.y / TILE_SIZE);
const int COL = static_cast<int>(point.x / TILE_SIZE);
const int POS = (ROW * MAP_WIDTH) + COL;
return getTile(POS);
}
// Devuelve el tipo de tile que hay en ese indice
auto CollisionMap::getTile(int index) const -> Tile { // NOLINT(readability-convert-member-functions-to-static)
const bool ON_RANGE = (index > -1) && (index < (int)tile_map_.size());
if (ON_RANGE) {
// Las filas 0-8 son de tiles t_wall
if ((tile_map_[index] >= 0) && (tile_map_[index] < 9 * tile_set_width_)) {
return Tile::WALL;
}
// Las filas 9-17 son de tiles t_passable
if ((tile_map_[index] >= 9 * tile_set_width_) && (tile_map_[index] < 18 * tile_set_width_)) {
return Tile::PASSABLE;
}
// Las filas 18-20 es de tiles t_animated
if ((tile_map_[index] >= 18 * tile_set_width_) && (tile_map_[index] < 21 * tile_set_width_)) {
return Tile::ANIMATED;
}
// La fila 21 es de tiles t_slope_r
if ((tile_map_[index] >= 21 * tile_set_width_) && (tile_map_[index] < 22 * tile_set_width_)) {
return Tile::SLOPE_R;
}
// La fila 22 es de tiles t_slope_l
if ((tile_map_[index] >= 22 * tile_set_width_) && (tile_map_[index] < 23 * tile_set_width_)) {
return Tile::SLOPE_L;
}
// La fila 23 es de tiles t_kill
if ((tile_map_[index] >= 23 * tile_set_width_) && (tile_map_[index] < 24 * tile_set_width_)) {
return Tile::KILL;
}
}
return Tile::EMPTY;
}
// Obten la coordenada de la cuesta a partir de un punto perteneciente a ese tile
auto CollisionMap::getSlopeHeight(SDL_FPoint p, Tile slope) -> int {
// Calcula la base del tile
int base = ((p.y / TILE_SIZE) * TILE_SIZE) + TILE_SIZE;
#ifdef _DEBUG
Debug::get()->set("slope.BASE", std::to_string(base));
#endif
// Calcula cuanto se ha entrado en el tile horizontalmente
const int POS = (static_cast<int>(p.x) % TILE_SIZE); // Esto da un valor entre 0 y 7
#ifdef _DEBUG
Debug::get()->set("slope.POS", std::to_string(POS));
#endif
// Se resta a la base la cantidad de pixeles pos en funcion de la rampa
if (slope == Tile::SLOPE_R) {
base -= POS + 1;
#ifdef _DEBUG
Debug::get()->set("slope.result", "BASE_R=" + std::to_string(base));
#endif
} else {
base -= (TILE_SIZE - POS);
#ifdef _DEBUG
Debug::get()->set("slope.result", "BASE_L=" + std::to_string(base));
#endif
}
return base;
}
// === Queries de colisión ===
// Comprueba las colisiones con paredes derechas
auto CollisionMap::checkRightSurfaces(const SDL_FRect& rect) -> int { // NOLINT(readability-convert-member-functions-to-static)
for (const auto& s : right_walls_) {
if (checkCollision(s, rect)) {
return s.x;
}
}
return Collision::NONE;
}
// Comprueba las colisiones con paredes izquierdas
auto CollisionMap::checkLeftSurfaces(const SDL_FRect& rect) -> int { // NOLINT(readability-convert-member-functions-to-static)
for (const auto& s : left_walls_) {
if (checkCollision(s, rect)) {
return s.x;
}
}
return Collision::NONE;
}
// Comprueba las colisiones con techos
auto CollisionMap::checkTopSurfaces(const SDL_FRect& rect) -> int { // NOLINT(readability-convert-member-functions-to-static)
for (const auto& s : top_floors_) {
if (checkCollision(s, rect)) {
return s.y;
}
}
return Collision::NONE;
}
// Comprueba las colisiones punto con techos
auto CollisionMap::checkTopSurfaces(const SDL_FPoint& p) -> bool {
return std::ranges::any_of(top_floors_, [&](const auto& s) -> bool {
return checkCollision(s, p);
});
}
// Comprueba las colisiones con suelos
auto CollisionMap::checkBottomSurfaces(const SDL_FRect& rect) -> int { // NOLINT(readability-convert-member-functions-to-static)
for (const auto& s : bottom_floors_) {
if (checkCollision(s, rect)) {
return s.y;
}
}
return Collision::NONE;
}
// Comprueba las colisiones con conveyor belts
auto CollisionMap::checkAutoSurfaces(const SDL_FRect& rect) -> int { // NOLINT(readability-convert-member-functions-to-static)
for (const auto& s : conveyor_belt_floors_) {
if (checkCollision(s, rect)) {
return s.y;
}
}
return Collision::NONE;
}
// Comprueba las colisiones punto con conveyor belts
auto CollisionMap::checkConveyorBelts(const SDL_FPoint& p) -> bool {
return std::ranges::any_of(conveyor_belt_floors_, [&](const auto& s) -> bool {
return checkCollision(s, p);
});
}
// Comprueba las colisiones línea con rampas izquierdas
auto CollisionMap::checkLeftSlopes(const LineVertical& line) -> int { // NOLINT(readability-convert-member-functions-to-static)
for (const auto& slope : left_slopes_) {
const auto P = checkCollision(slope, line);
if (P.x != -1) {
return P.y;
}
}
return Collision::NONE;
}
// Comprueba las colisiones punto con rampas izquierdas
auto CollisionMap::checkLeftSlopes(const SDL_FPoint& p) -> bool {
return std::ranges::any_of(left_slopes_, [&](const auto& slope) -> bool {
return checkCollision(p, slope);
});
}
// Comprueba las colisiones línea con rampas derechas
auto CollisionMap::checkRightSlopes(const LineVertical& line) -> int { // NOLINT(readability-convert-member-functions-to-static)
for (const auto& slope : right_slopes_) {
const auto P = checkCollision(slope, line);
if (P.x != -1) {
return P.y;
}
}
return Collision::NONE;
}
// Comprueba las colisiones punto con rampas derechas
auto CollisionMap::checkRightSlopes(const SDL_FPoint& p) -> bool {
return std::ranges::any_of(right_slopes_, [&](const auto& slope) -> bool {
return checkCollision(p, slope);
});
}
// Obtiene puntero a slope en un punto (prioriza left_slopes_ sobre right_slopes_)
auto CollisionMap::getSlopeAtPoint(const SDL_FPoint& p) const -> const LineDiagonal* { // NOLINT(readability-convert-member-functions-to-static)
// Primero busca en rampas izquierdas
for (const auto& slope : left_slopes_) {
if (checkCollision(p, slope)) {
return &slope;
}
}
// Luego busca en rampas derechas
for (const auto& slope : right_slopes_) {
if (checkCollision(p, slope)) {
return &slope;
}
}
// No hay colisión con ninguna slope
return nullptr;
}
// === Helpers para recopilar tiles ===
// Helper: recopila tiles inferiores (muros sin muro debajo)
auto CollisionMap::collectBottomTiles() -> std::vector<int> { // NOLINT(readability-make-member-function-const)
std::vector<int> tile;
// Busca todos los tiles de tipo muro que no tengan debajo otro muro
// Hay que recorrer la habitación por filas (excepto los de la última fila)
for (int i = 0; i < (int)tile_map_.size() - MAP_WIDTH; ++i) {
if (getTile(i) == Tile::WALL && getTile(i + MAP_WIDTH) != Tile::WALL) {
tile.push_back(i);
// Si llega al final de la fila, introduce un separador
if (i % MAP_WIDTH == MAP_WIDTH - 1) {
tile.push_back(-1);
}
}
}
// Añade un terminador
tile.push_back(-1);
return tile;
}
// Helper: recopila tiles superiores (muros o pasables sin muro encima)
auto CollisionMap::collectTopTiles() -> std::vector<int> { // NOLINT(readability-make-member-function-const)
std::vector<int> tile;
// Busca todos los tiles de tipo muro o pasable que no tengan encima un muro
// Hay que recorrer la habitación por filas (excepto los de la primera fila)
for (int i = MAP_WIDTH; i < (int)tile_map_.size(); ++i) {
if ((getTile(i) == Tile::WALL || getTile(i) == Tile::PASSABLE) && getTile(i - MAP_WIDTH) != Tile::WALL) {
tile.push_back(i);
// Si llega al final de la fila, introduce un separador
if (i % MAP_WIDTH == MAP_WIDTH - 1) {
tile.push_back(-1);
}
}
}
// Añade un terminador
tile.push_back(-1);
return tile;
}
// Helper: recopila tiles animados (para superficies automaticas/conveyor belts)
auto CollisionMap::collectAnimatedTiles() -> std::vector<int> { // NOLINT(readability-make-member-function-const)
std::vector<int> tile;
// Busca todos los tiles de tipo animado
// Hay que recorrer la habitación por filas (excepto los de la primera fila)
for (int i = MAP_WIDTH; i < (int)tile_map_.size(); ++i) {
if (getTile(i) == Tile::ANIMATED) {
tile.push_back(i);
// Si llega al final de la fila, introduce un separador
if (i % MAP_WIDTH == MAP_WIDTH - 1) {
tile.push_back(-1);
}
}
}
// Añade un terminador si hay tiles
if (!tile.empty()) {
tile.push_back(-1);
}
return tile;
}
// Helper: construye lineas horizontales a partir de tiles consecutivos
void CollisionMap::buildHorizontalLines(const std::vector<int>& tiles, std::vector<LineHorizontal>& lines, bool is_bottom_surface) { // NOLINT(readability-convert-member-functions-to-static)
if (tiles.size() <= 1) {
return;
}
int i = 0;
while (i < static_cast<int>(tiles.size()) - 1) {
LineHorizontal line;
line.x1 = (tiles[i] % MAP_WIDTH) * TILE_SIZE;
// Calcula Y segun si es superficie inferior o superior
if (is_bottom_surface) {
line.y = ((tiles[i] / MAP_WIDTH) * TILE_SIZE) + TILE_SIZE - 1;
} else {
line.y = (tiles[i] / MAP_WIDTH) * TILE_SIZE;
}
int last_one = i;
i++;
// Encuentra tiles consecutivos
if (i < static_cast<int>(tiles.size())) {
while (tiles[i] == tiles[i - 1] + 1) {
last_one = i;
i++;
if (i >= static_cast<int>(tiles.size())) {
break;
}
}
}
line.x2 = ((tiles[last_one] % MAP_WIDTH) * TILE_SIZE) + TILE_SIZE - 1;
lines.push_back(line);
// Salta separadores
if (i < static_cast<int>(tiles.size()) && tiles[i] == -1) {
i++;
}
}
}
// === Métodos de generación de geometría ===
// Calcula las superficies inferiores
void CollisionMap::setBottomSurfaces() {
std::vector<int> tile = collectBottomTiles();
buildHorizontalLines(tile, bottom_floors_, true);
}
// Calcula las superficies superiores
void CollisionMap::setTopSurfaces() {
std::vector<int> tile = collectTopTiles();
buildHorizontalLines(tile, top_floors_, false);
}
// Calcula las superficies laterales izquierdas
void CollisionMap::setLeftSurfaces() { // NOLINT(readability-make-member-function-const)
std::vector<int> tile;
// Busca todos los tiles de tipo muro que no tienen a su izquierda un tile de tipo muro
// Hay que recorrer la habitación por columnas (excepto los de la primera columna)
for (int i = 1; i < MAP_WIDTH; ++i) {
for (int j = 0; j < MAP_HEIGHT; ++j) {
const int POS = ((j * MAP_WIDTH) + i);
if (getTile(POS) == Tile::WALL && getTile(POS - 1) != Tile::WALL) {
tile.push_back(POS);
}
}
}
// Añade un terminador
tile.push_back(-1);
// Recorre el vector de tiles buscando tiles consecutivos
// (Los tiles de la misma columna, la diferencia entre ellos es de mapWidth)
// para localizar las superficies
if ((int)tile.size() > 1) {
int i = 0;
do {
LineVertical line;
line.x = (tile[i] % MAP_WIDTH) * TILE_SIZE;
line.y1 = ((tile[i] / MAP_WIDTH) * TILE_SIZE);
while (tile[i] + MAP_WIDTH == tile[i + 1]) {
if (i == (int)tile.size() - 1) {
break;
}
i++;
}
line.y2 = ((tile[i] / MAP_WIDTH) * TILE_SIZE) + TILE_SIZE - 1;
left_walls_.push_back(line);
i++;
} while (i < (int)tile.size() - 1);
}
}
// Calcula las superficies laterales derechas
void CollisionMap::setRightSurfaces() { // NOLINT(readability-make-member-function-const)
std::vector<int> tile;
// Busca todos los tiles de tipo muro que no tienen a su derecha un tile de tipo muro
// Hay que recorrer la habitación por columnas (excepto los de la última columna)
for (int i = 0; i < MAP_WIDTH - 1; ++i) {
for (int j = 0; j < MAP_HEIGHT; ++j) {
const int POS = ((j * MAP_WIDTH) + i);
if (getTile(POS) == Tile::WALL && getTile(POS + 1) != Tile::WALL) {
tile.push_back(POS);
}
}
}
// Añade un terminador
tile.push_back(-1);
// Recorre el vector de tiles buscando tiles consecutivos
// (Los tiles de la misma columna, la diferencia entre ellos es de mapWidth)
// para localizar las superficies
if ((int)tile.size() > 1) {
int i = 0;
do {
LineVertical line;
line.x = ((tile[i] % MAP_WIDTH) * TILE_SIZE) + TILE_SIZE - 1;
line.y1 = ((tile[i] / MAP_WIDTH) * TILE_SIZE);
while (tile[i] + MAP_WIDTH == tile[i + 1]) {
if (i == (int)tile.size() - 1) {
break;
}
i++;
}
line.y2 = ((tile[i] / MAP_WIDTH) * TILE_SIZE) + TILE_SIZE - 1;
right_walls_.push_back(line);
i++;
} while (i < (int)tile.size() - 1);
}
}
// Encuentra todas las rampas que suben hacia la izquierda
void CollisionMap::setLeftSlopes() { // NOLINT(readability-make-member-function-const)
// Recorre la habitación entera por filas buscando tiles de tipo t_slope_l
std::vector<int> found;
for (int i = 0; i < (int)tile_map_.size(); ++i) {
if (getTile(i) == Tile::SLOPE_L) {
found.push_back(i);
}
}
// El primer elemento es el inicio de una rampa. Se añade ese elemento y se buscan los siguientes,
// que seran i + mapWidth + 1. Conforme se añaden se eliminan y se vuelve a escudriñar el vector de
// tiles encontrados hasta que esté vacío
while (!found.empty()) {
LineDiagonal line;
line.x1 = (found[0] % MAP_WIDTH) * TILE_SIZE;
line.y1 = (found[0] / MAP_WIDTH) * TILE_SIZE;
int looking_for = found[0] + MAP_WIDTH + 1;
int last_one_found = found[0];
found.erase(found.begin());
for (int i = 0; i < (int)found.size(); ++i) {
if (found[i] == looking_for) {
last_one_found = looking_for;
looking_for += MAP_WIDTH + 1;
found.erase(found.begin() + i);
i--;
}
}
line.x2 = ((last_one_found % MAP_WIDTH) * TILE_SIZE) + TILE_SIZE - 1;
line.y2 = ((last_one_found / MAP_WIDTH) * TILE_SIZE) + TILE_SIZE - 1;
left_slopes_.push_back(line);
}
}
// Encuentra todas las rampas que suben hacia la derecha
void CollisionMap::setRightSlopes() { // NOLINT(readability-make-member-function-const)
// Recorre la habitación entera por filas buscando tiles de tipo t_slope_r
std::vector<int> found;
for (int i = 0; i < (int)tile_map_.size(); ++i) {
if (getTile(i) == Tile::SLOPE_R) {
found.push_back(i);
}
}
// El primer elemento es el inicio de una rampa. Se añade ese elemento y se buscan los siguientes,
// que seran i + mapWidth - 1. Conforme se añaden se eliminan y se vuelve a escudriñar el vector de
// tiles encontrados hasta que esté vacío
while (!found.empty()) {
LineDiagonal line;
line.x1 = ((found[0] % MAP_WIDTH) * TILE_SIZE) + TILE_SIZE - 1;
line.y1 = (found[0] / MAP_WIDTH) * TILE_SIZE;
int looking_for = found[0] + MAP_WIDTH - 1;
int last_one_found = found[0];
found.erase(found.begin());
for (int i = 0; i < (int)found.size(); ++i) {
if (found[i] == looking_for) {
last_one_found = looking_for;
looking_for += MAP_WIDTH - 1;
found.erase(found.begin() + i);
i--;
}
}
line.x2 = (last_one_found % MAP_WIDTH) * TILE_SIZE;
line.y2 = ((last_one_found / MAP_WIDTH) * TILE_SIZE) + TILE_SIZE - 1;
right_slopes_.push_back(line);
}
}
// Calcula las superficies automaticas (conveyor belts)
void CollisionMap::setAutoSurfaces() {
std::vector<int> tile = collectAnimatedTiles();
buildHorizontalLines(tile, conveyor_belt_floors_, false);
}

View File

@@ -0,0 +1,121 @@
#pragma once
#include <SDL3/SDL.h>
#include <vector> // Para vector
#include "utils/utils.hpp" // Para LineHorizontal, LineDiagonal, LineVertical
/**
* @brief Mapa de colisiones de una habitación
*
* Responsabilidades:
* - Almacenar la geometría de colisión (superficies, rampas, conveyor belts)
* - Generar geometría a partir del tilemap
* - Proporcionar queries de colisión para Player y otras entidades
* - Determinar tipo de tile en posiciones específicas
*/
class CollisionMap {
public:
// Enumeración de tipos de tile (para colisiones)
enum class Tile {
EMPTY,
WALL,
PASSABLE,
SLOPE_L,
SLOPE_R,
KILL,
ANIMATED
};
/**
* @brief Constructor
* @param tile_map Vector con índices de tiles de la habitación
* @param tile_set_width Ancho del tileset en tiles (para calcular tipo de tile)
* @param conveyor_belt_direction Dirección de las cintas transportadoras (-1, 0, +1)
*/
CollisionMap(std::vector<int> tile_map, int tile_set_width, int conveyor_belt_direction);
~CollisionMap() = default;
// Prohibir copia y movimiento
CollisionMap(const CollisionMap&) = delete;
auto operator=(const CollisionMap&) -> CollisionMap& = delete;
CollisionMap(CollisionMap&&) = delete;
auto operator=(CollisionMap&&) -> CollisionMap& = delete;
// --- Queries de tipo de tile ---
[[nodiscard]] auto getTile(SDL_FPoint point) const -> Tile; // Devuelve el tipo de tile en un punto (pixel)
[[nodiscard]] auto getTile(int index) const -> Tile; // Devuelve el tipo de tile en un índice del tilemap
// --- Queries de colisión con superficies ---
auto checkRightSurfaces(const SDL_FRect& rect) -> int; // Colisión con paredes derechas (retorna X)
auto checkLeftSurfaces(const SDL_FRect& rect) -> int; // Colisión con paredes izquierdas (retorna X)
auto checkTopSurfaces(const SDL_FRect& rect) -> int; // Colisión con techos (retorna Y)
auto checkTopSurfaces(const SDL_FPoint& p) -> bool; // Colisión punto con techos
auto checkBottomSurfaces(const SDL_FRect& rect) -> int; // Colisión con suelos (retorna Y)
// --- Queries de colisión con superficies automáticas (conveyor belts) ---
auto checkAutoSurfaces(const SDL_FRect& rect) -> int; // Colisión con conveyor belts (retorna Y)
auto checkConveyorBelts(const SDL_FPoint& p) -> bool; // Colisión punto con conveyor belts
// --- Queries de colisión con rampas ---
auto checkLeftSlopes(const LineVertical& line) -> int; // Colisión línea con rampas izquierdas (retorna Y)
auto checkLeftSlopes(const SDL_FPoint& p) -> bool; // Colisión punto con rampas izquierdas
auto checkRightSlopes(const LineVertical& line) -> int; // Colisión línea con rampas derechas (retorna Y)
auto checkRightSlopes(const SDL_FPoint& p) -> bool; // Colisión punto con rampas derechas
[[nodiscard]] auto getSlopeAtPoint(const SDL_FPoint& p) const -> const LineDiagonal*; // Obtiene puntero a slope en un punto
// --- Métodos estáticos ---
static auto getTileSize() -> int { return TILE_SIZE; } // Tamaño del tile en pixels
static auto getSlopeHeight(SDL_FPoint p, Tile slope) -> int; // Altura de rampa en un punto
// --- Getters ---
[[nodiscard]] auto getConveyorBeltDirection() const -> int { return conveyor_belt_direction_; }
// Getters para debug visualization
[[nodiscard]] auto getBottomFloors() const -> const std::vector<LineHorizontal>& { return bottom_floors_; }
[[nodiscard]] auto getTopFloors() const -> const std::vector<LineHorizontal>& { return top_floors_; }
[[nodiscard]] auto getLeftWalls() const -> const std::vector<LineVertical>& { return left_walls_; }
[[nodiscard]] auto getRightWalls() const -> const std::vector<LineVertical>& { return right_walls_; }
[[nodiscard]] auto getLeftSlopes() const -> const std::vector<LineDiagonal>& { return left_slopes_; }
[[nodiscard]] auto getRightSlopes() const -> const std::vector<LineDiagonal>& { return right_slopes_; }
[[nodiscard]] auto getConveyorBeltFloors() const -> const std::vector<LineHorizontal>& { return conveyor_belt_floors_; }
private:
// --- Constantes ---
static constexpr int TILE_SIZE = 8; // Tamaño del tile en pixels
static constexpr int MAP_WIDTH = 32; // Ancho del mapa en tiles
static constexpr int MAP_HEIGHT = 16; // Alto del mapa en tiles
// --- Datos de la habitación ---
std::vector<int> tile_map_; // Índices de tiles de la habitación
int tile_set_width_; // Ancho del tileset en tiles
int conveyor_belt_direction_; // Dirección de conveyor belts
// --- Geometría de colisión ---
std::vector<LineHorizontal> bottom_floors_; // Superficies inferiores (suelos)
std::vector<LineHorizontal> top_floors_; // Superficies superiores (techos)
std::vector<LineVertical> left_walls_; // Paredes izquierdas
std::vector<LineVertical> right_walls_; // Paredes derechas
std::vector<LineDiagonal> left_slopes_; // Rampas que suben hacia la izquierda
std::vector<LineDiagonal> right_slopes_; // Rampas que suben hacia la derecha
std::vector<LineHorizontal> conveyor_belt_floors_; // Superficies automáticas (conveyor belts)
// --- Métodos privados de generación de geometría ---
void initializeSurfaces(); // Inicializa todas las superficies de colisión
// Helpers para recopilar tiles
auto collectBottomTiles() -> std::vector<int>; // Tiles con superficie inferior
auto collectTopTiles() -> std::vector<int>; // Tiles con superficie superior
auto collectAnimatedTiles() -> std::vector<int>; // Tiles animados (conveyor belts)
// Construcción de geometría
static void buildHorizontalLines(const std::vector<int>& tiles, std::vector<LineHorizontal>& lines, bool is_bottom_surface);
void setBottomSurfaces(); // Calcula superficies inferiores
void setTopSurfaces(); // Calcula superficies superiores
void setLeftSurfaces(); // Calcula paredes izquierdas
void setRightSurfaces(); // Calcula paredes derechas
void setLeftSlopes(); // Calcula rampas izquierdas
void setRightSlopes(); // Calcula rampas derechas
void setAutoSurfaces(); // Calcula conveyor belts
};

View File

@@ -0,0 +1,76 @@
#include "enemy_manager.hpp"
#include <algorithm> // Para std::ranges::any_of
#include "game/entities/enemy.hpp" // Para Enemy
#include "utils/utils.hpp" // Para checkCollision
// Añade un enemigo a la colección
void EnemyManager::addEnemy(std::shared_ptr<Enemy> enemy) { // NOLINT(readability-identifier-naming)
enemies_.push_back(std::move(enemy));
}
// Elimina todos los enemigos
void EnemyManager::clear() {
enemies_.clear();
}
// Elimina el último enemigo de la colección
void EnemyManager::removeLastEnemy() { // NOLINT(readability-convert-member-functions-to-static)
if (!enemies_.empty()) {
enemies_.pop_back();
}
}
// Comprueba si no hay enemigos
auto EnemyManager::isEmpty() const -> bool {
return enemies_.empty();
}
// Actualiza todos los enemigos
void EnemyManager::update(float delta_time) {
for (const auto& enemy : enemies_) {
enemy->update(delta_time);
}
}
// Renderiza todos los enemigos
void EnemyManager::render() {
for (const auto& enemy : enemies_) {
enemy->render();
}
}
#ifdef _DEBUG
// Solo actualiza animaciones sin mover enemigos
void EnemyManager::updateAnimations(float delta_time) {
for (const auto& enemy : enemies_) {
enemy->updateAnimation(delta_time);
}
}
// Resetea todos los enemigos a su posición inicial
void EnemyManager::resetPositions(const std::vector<Enemy::Data>& enemy_data) {
const int COUNT = std::min(static_cast<int>(enemies_.size()), static_cast<int>(enemy_data.size()));
for (int i = 0; i < COUNT; ++i) {
enemies_[i]->resetToInitialPosition(enemy_data[i]);
}
}
// Número de enemigos
auto EnemyManager::getCount() const -> int {
return static_cast<int>(enemies_.size());
}
// Acceso a un enemigo por índice
auto EnemyManager::getEnemy(int index) -> std::shared_ptr<Enemy>& {
return enemies_.at(index);
}
#endif
// Comprueba si hay colisión con algún enemigo
auto EnemyManager::checkCollision(SDL_FRect& rect) -> bool {
return std::ranges::any_of(enemies_, [&rect](const auto& enemy) {
return ::checkCollision(rect, enemy->getCollider());
});
}

View File

@@ -0,0 +1,52 @@
#pragma once
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr
#include <vector> // Para vector
#include "game/entities/enemy.hpp" // Para Enemy, Enemy::Data
/**
* @brief Gestor de enemigos de una habitación
*
* Responsabilidades:
* - Almacenar y gestionar la colección de enemigos
* - Actualizar todos los enemigos
* - Renderizar todos los enemigos
* - Detectar colisiones con enemigos
*/
class EnemyManager {
public:
EnemyManager() = default;
~EnemyManager() = default;
// Prohibir copia y movimiento para evitar duplicación accidental
EnemyManager(const EnemyManager&) = delete;
auto operator=(const EnemyManager&) -> EnemyManager& = delete;
EnemyManager(EnemyManager&&) = delete;
auto operator=(EnemyManager&&) -> EnemyManager& = delete;
// Gestión de enemigos
void addEnemy(std::shared_ptr<Enemy> enemy); // Añade un enemigo a la colección
void clear(); // Elimina todos los enemigos
void removeLastEnemy(); // Elimina el último enemigo de la colección
[[nodiscard]] auto isEmpty() const -> bool; // Comprueba si no hay enemigos
// Actualización y renderizado
void update(float delta_time); // Actualiza todos los enemigos
void render(); // Renderiza todos los enemigos
// Detección de colisiones
auto checkCollision(SDL_FRect& rect) -> bool; // Comprueba si hay colisión con algún enemigo
#ifdef _DEBUG
void updateAnimations(float delta_time); // Solo actualiza animaciones sin mover enemigos
void resetPositions(const std::vector<Enemy::Data>& enemy_data); // Resetea todos los enemigos a su posición inicial
[[nodiscard]] auto getCount() const -> int; // Número de enemigos
auto getEnemy(int index) -> std::shared_ptr<Enemy>&; // Acceso a un enemigo por índice
#endif
private:
std::vector<std::shared_ptr<Enemy>> enemies_; // Colección de enemigos
};

View File

@@ -0,0 +1,69 @@
#include "item_manager.hpp"
#include "core/audio/audio.hpp" // Para Audio
#include "game/entities/item.hpp" // Para Item
#include "game/options.hpp" // Para Options
#include "item_tracker.hpp" // Para ItemTracker
#include "scoreboard.hpp" // Para Scoreboard::Data
#include "utils/utils.hpp" // Para checkCollision
// Constructor
ItemManager::ItemManager(std::string room_name, std::shared_ptr<Scoreboard::Data> scoreboard_data)
: room_name_(std::move(room_name)),
data_(std::move(scoreboard_data)) {
}
// Añade un item a la colección
void ItemManager::addItem(std::shared_ptr<Item> item) { // NOLINT(readability-identifier-naming)
items_.push_back(std::move(item));
}
// Elimina todos los items
void ItemManager::clear() {
items_.clear();
}
// Actualiza todos los items
void ItemManager::update(float delta_time) {
for (const auto& item : items_) {
item->update(delta_time);
}
}
// Renderiza todos los items
void ItemManager::render() {
for (const auto& item : items_) {
item->render();
}
}
// Pausa/despausa todos los items
void ItemManager::setPaused(bool paused) {
for (const auto& item : items_) {
item->setPaused(paused);
}
}
// Comprueba si hay colisión con algún item
auto ItemManager::checkCollision(SDL_FRect& rect) -> bool { // NOLINT(readability-convert-member-functions-to-static)
for (int i = 0; i < static_cast<int>(items_.size()); ++i) {
if (::checkCollision(rect, items_.at(i)->getCollider())) {
// Registra el item como recogido
ItemTracker::get()->addItem(room_name_, items_.at(i)->getPos());
// Elimina el item de la colección
items_.erase(items_.begin() + i);
// Reproduce el sonido de pickup
Audio::get()->playSound("item.wav", Audio::Group::GAME);
// Actualiza el scoreboard y estadísticas
data_->items++;
Options::stats.items = data_->items;
return true;
}
}
return false;
}

View File

@@ -0,0 +1,73 @@
#pragma once
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include "game/entities/item.hpp" // Para Item, Item::Data
#include "scoreboard.hpp" // Para Scoreboard::Data
/**
* @brief Gestor de items de una habitación
*
* Responsabilidades:
* - Almacenar y gestionar la colección de items
* - Actualizar todos los items
* - Renderizar todos los items
* - Detectar colisiones con items y gestionar pickup
* - Integración con ItemTracker, Scoreboard y Audio
*/
class ItemManager {
public:
/**
* @brief Constructor
* @param room_name Nombre de la habitación (para ItemTracker)
* @param scoreboard_data Puntero compartido a los datos del scoreboard
*/
ItemManager(std::string room_name, std::shared_ptr<Scoreboard::Data> scoreboard_data);
~ItemManager() = default;
// Prohibir copia y movimiento para evitar duplicación accidental
ItemManager(const ItemManager&) = delete;
auto operator=(const ItemManager&) -> ItemManager& = delete;
ItemManager(ItemManager&&) = delete;
auto operator=(ItemManager&&) -> ItemManager& = delete;
// Gestión de items
void addItem(std::shared_ptr<Item> item); // Añade un item a la colección
void clear(); // Elimina todos los items
// Actualización y renderizado
void update(float delta_time); // Actualiza todos los items
void render(); // Renderiza todos los items
// Estado
void setPaused(bool paused); // Pausa/despausa todos los items
#ifdef _DEBUG
[[nodiscard]] auto getCount() const -> int { return static_cast<int>(items_.size()); } // Número de items
auto getItem(int index) -> std::shared_ptr<Item>& { return items_.at(index); } // Acceso a un item por índice
#endif
// Detección de colisiones
/**
* @brief Comprueba si hay colisión con algún item
*
* Si hay colisión:
* - Añade el item a ItemTracker
* - Elimina el item de la colección
* - Reproduce el sonido de pickup
* - Actualiza el scoreboard y estadísticas
*
* @param rect Rectángulo de colisión
* @return true si hubo colisión, false en caso contrario
*/
auto checkCollision(SDL_FRect& rect) -> bool;
private:
std::vector<std::shared_ptr<Item>> items_; // Colección de items
std::string room_name_; // Nombre de la habitación
std::shared_ptr<Scoreboard::Data> data_; // Datos del scoreboard
};

View File

@@ -0,0 +1,75 @@
#include "game/gameplay/item_tracker.hpp"
// [SINGLETON]
ItemTracker* ItemTracker::item_tracker = nullptr;
// [SINGLETON] Crearemos el objeto con esta función estática
void ItemTracker::init() {
ItemTracker::item_tracker = new ItemTracker();
}
// [SINGLETON] Destruiremos el objeto con esta función estática
void ItemTracker::destroy() {
delete ItemTracker::item_tracker;
}
// [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
auto ItemTracker::get() -> ItemTracker* {
return ItemTracker::item_tracker;
}
// Comprueba si el objeto ya ha sido cogido
auto ItemTracker::hasBeenPicked(const std::string& name, SDL_FPoint pos) -> bool {
// Primero busca si ya hay una entrada con ese nombre
if (const int INDEX = findByName(name); INDEX != NOT_FOUND) {
// Luego busca si existe ya una entrada con esa posición
if (findByPos(INDEX, pos) != NOT_FOUND) {
return true;
}
}
return false;
}
// Añade el objeto a la lista de objetos cogidos
void ItemTracker::addItem(const std::string& name, SDL_FPoint pos) { // NOLINT(readability-convert-member-functions-to-static)
// Comprueba si el objeto no ha sido recogido con anterioridad
if (!hasBeenPicked(name, pos)) {
// Primero busca si ya hay una entrada con ese nombre
if (const int INDEX = findByName(name); INDEX != NOT_FOUND) {
items_.at(INDEX).pos.push_back(pos);
}
// En caso contrario crea la entrada
else {
items_.emplace_back(name, pos);
}
}
}
// Busca una entrada en la lista por nombre
auto ItemTracker::findByName(const std::string& name) -> int { // NOLINT(readability-convert-member-functions-to-static)
int i = 0;
for (const auto& item : items_) {
if (item.name == name) {
return i;
}
i++;
}
return NOT_FOUND;
}
// Busca una entrada en la lista por posición
auto ItemTracker::findByPos(int index, SDL_FPoint pos) -> int { // NOLINT(readability-convert-member-functions-to-static)
int i = 0;
for (const auto& item : items_[index].pos) {
if ((item.x == pos.x) && (item.y == pos.y)) {
return i;
}
i++;
}
return NOT_FOUND;
}

View File

@@ -0,0 +1,49 @@
#pragma once
#include <SDL3/SDL.h>
#include <string> // Para string, basic_string
#include <utility>
#include <vector> // Para vector
class ItemTracker {
public:
// Gestión singleton
static void init(); // Inicialización
static void destroy(); // Destrucción
static auto get() -> ItemTracker*; // Acceso al singleton
// Gestión de items
auto hasBeenPicked(const std::string& name, SDL_FPoint pos) -> bool; // Comprueba si el objeto ya ha sido cogido
void addItem(const std::string& name, SDL_FPoint pos); // Añade el objeto a la lista
private:
// Tipos anidados privados
struct Data {
std::string name; // Nombre de la habitación donde se encuentra el objeto
std::vector<SDL_FPoint> pos; // Lista de objetos cogidos de la habitación
// Constructor para facilitar creación con posición inicial
Data(std::string name, const SDL_FPoint& position)
: name(std::move(name)) {
pos.push_back(position);
}
};
// Constantes privadas
static constexpr int NOT_FOUND = -1; // Valor de retorno cuando no se encuentra un elemento
// Constantes singleton
static ItemTracker* item_tracker; // [SINGLETON] Objeto privado
// Métodos privados
auto findByName(const std::string& name) -> int; // Busca una entrada en la lista por nombre
auto findByPos(int index, SDL_FPoint pos) -> int; // Busca una entrada en la lista por posición
// Constructor y destructor privados [SINGLETON]
ItemTracker() = default;
~ItemTracker() = default;
// Variables miembro
std::vector<Data> items_; // Lista con todos los objetos recogidos
};

View File

@@ -0,0 +1,301 @@
#include "game/gameplay/room.hpp"
#include <utility> // Para std::move
#include "core/rendering/screen.hpp" // Para Screen
#include "core/rendering/surface.hpp" // Para Surface
#include "core/resources/resource_cache.hpp" // Para Resource
#include "game/defaults.hpp" // Para Defaults::Game
#include "game/gameplay/collision_map.hpp" // Para CollisionMap
#include "game/gameplay/enemy_manager.hpp" // Para EnemyManager
#include "game/gameplay/item_manager.hpp" // Para ItemManager
#include "game/gameplay/item_tracker.hpp" // Para ItemTracker
#include "game/gameplay/room_loader.hpp" // Para RoomLoader
#include "game/gameplay/scoreboard.hpp" // Para Scoreboard::Data
#include "game/gameplay/tilemap_renderer.hpp" // Para TilemapRenderer
#include "utils/defines.hpp" // Para TILE_SIZE
#include "utils/utils.hpp" // Para stringToColor
// Constructor
Room::Room(const std::string& room_path, std::shared_ptr<Scoreboard::Data> data)
: data_(std::move(data)) {
auto room = Resource::Cache::get()->getRoom(room_path);
// Crea los managers de enemigos e items
enemy_manager_ = std::make_unique<EnemyManager>();
item_manager_ = std::make_unique<ItemManager>(room->name, data_);
initializeRoom(*room);
openTheJail(); // Abre la Jail si se da el caso
// Crea el mapa de colisiones (necesita tile_map_, tile_set_width_, conveyor_belt_direction_)
collision_map_ = std::make_unique<CollisionMap>(tile_map_, tile_set_width_, conveyor_belt_direction_);
// Crea el renderizador del tilemap (necesita tile_map_, tile_set_width_, surface_, bg_color_, conveyor_belt_direction_)
tilemap_renderer_ = std::make_unique<TilemapRenderer>(tile_map_, tile_set_width_, surface_, bg_color_, conveyor_belt_direction_);
tilemap_renderer_->initialize(collision_map_.get()); // Inicializa (crea map_surface, pinta tiles, busca animados)
Screen::get()->setBorderColor(stringToColor(border_color_)); // Establece el color del borde
}
// Destructor
Room::~Room() = default;
void Room::initializeRoom(const Data& room) {
// Asignar valores a las variables miembro
number_ = room.number;
name_ = room.name;
bg_color_ = room.bg_color;
border_color_ = room.border_color;
item_color1_ = room.item_color1.empty() ? "yellow" : room.item_color1;
item_color2_ = room.item_color2.empty() ? "magenta" : room.item_color2;
upper_room_ = room.upper_room;
lower_room_ = room.lower_room;
left_room_ = room.left_room;
right_room_ = room.right_room;
tile_set_file_ = room.tile_set_file;
conveyor_belt_direction_ = room.conveyor_belt_direction;
tile_map_ = room.tile_map; // Tilemap viene embebido en el YAML
surface_ = Resource::Cache::get()->getSurface(room.tile_set_file);
tile_set_width_ = surface_->getWidth() / TILE_SIZE;
is_paused_ = false;
// Crear los enemigos usando el manager
for (const auto& enemy_data : room.enemies) {
enemy_manager_->addEnemy(std::make_shared<Enemy>(enemy_data));
}
// Crear los items usando el manager
for (const auto& item : room.items) {
const SDL_FPoint ITEM_POS = {item.x, item.y};
if (!ItemTracker::get()->hasBeenPicked(room.name, ITEM_POS)) {
// Crear una copia local de los datos del item
Item::Data item_copy = item;
item_copy.color1 = stringToColor(item_color1_);
item_copy.color2 = stringToColor(item_color2_);
// Crear el objeto Item usando la copia modificada
item_manager_->addItem(std::make_shared<Item>(item_copy));
}
}
}
// Abre la jail para poder entrar
void Room::openTheJail() { // NOLINT(readability-convert-member-functions-to-static)
if (data_->jail_is_open && number_ == Defaults::Game::Room::END_ROOM) {
// Elimina el último enemigo (Bry debe ser el último enemigo definido en el fichero)
if (!enemy_manager_->isEmpty()) {
enemy_manager_->removeLastEnemy();
}
// Abre las puertas
constexpr int TILE_A = 16 + (13 * 32);
constexpr int TILE_B = 16 + (14 * 32);
if (TILE_A < tile_map_.size()) {
tile_map_[TILE_A] = -1;
}
if (TILE_B < tile_map_.size()) {
tile_map_[TILE_B] = -1;
}
}
}
// Dibuja el mapa en pantalla
void Room::renderMap() {
tilemap_renderer_->render();
}
// Dibuja los enemigos en pantalla
void Room::renderEnemies() {
enemy_manager_->render();
}
// Dibuja los objetos en pantalla
void Room::renderItems() {
item_manager_->render();
}
#ifdef _DEBUG
// Redibuja el mapa (para actualizar modo debug)
void Room::redrawMap() {
tilemap_renderer_->redrawMap(collision_map_.get());
}
// Actualiza animaciones sin mover enemigos (para editor de mapas)
void Room::updateEditorMode(float delta_time) {
tilemap_renderer_->update(delta_time);
enemy_manager_->updateAnimations(delta_time);
item_manager_->update(delta_time);
}
// Resetea enemigos a posiciones iniciales (para editor de mapas)
void Room::resetEnemyPositions(const std::vector<Enemy::Data>& enemy_data) {
enemy_manager_->resetPositions(enemy_data);
}
// Cambia un tile y repinta la celda (para editor)
void Room::setTile(int index, int tile_value) {
if (index >= 0 && index < static_cast<int>(tile_map_.size())) {
tile_map_[index] = tile_value;
tilemap_renderer_->setTile(index, tile_value);
}
}
// Cambia color de fondo y redibuja el mapa (para editor)
void Room::setBgColor(const std::string& color) {
bg_color_ = color;
tilemap_renderer_->setBgColor(color);
tilemap_renderer_->redrawMap(collision_map_.get());
}
// Cambia colores de items en vivo (para editor)
void Room::setItemColors(const std::string& color1, const std::string& color2) {
item_color1_ = color1;
item_color2_ = color2;
Uint8 c1 = stringToColor(color1);
Uint8 c2 = stringToColor(color2);
auto* item_mgr = item_manager_.get();
for (int i = 0; i < item_mgr->getCount(); ++i) {
item_mgr->getItem(i)->setColors(c1, c2);
}
}
#endif
// Actualiza las variables y objetos de la habitación
void Room::update(float delta_time) { // NOLINT(readability-make-member-function-const)
if (is_paused_) {
// Si está en modo pausa no se actualiza nada
return;
}
// Actualiza los tiles animados usando el renderer
tilemap_renderer_->update(delta_time);
// Actualiza los enemigos usando el manager
enemy_manager_->update(delta_time);
// Actualiza los items usando el manager
item_manager_->update(delta_time);
}
// Pone el mapa en modo pausa
void Room::setPaused(bool value) {
is_paused_ = value;
tilemap_renderer_->setPaused(value);
item_manager_->setPaused(value);
}
// Devuelve la cadena del fichero de la habitación contigua segun el borde
auto Room::getRoom(Border border) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
switch (border) {
case Border::TOP:
return upper_room_;
case Border::BOTTOM:
return lower_room_;
case Border::RIGHT:
return right_room_;
case Border::LEFT:
return left_room_;
default:
return "";
}
}
// Devuelve el tipo de tile que hay en ese pixel
auto Room::getTile(SDL_FPoint point) -> Tile { // NOLINT(readability-convert-member-functions-to-static)
// Delega a CollisionMap y convierte el resultado
const auto COLLISION_TILE = collision_map_->getTile(point);
return static_cast<Tile>(COLLISION_TILE);
}
// Devuelve el tipo de tile en un índice del tilemap
auto Room::getTile(int index) -> Tile { // NOLINT(readability-convert-member-functions-to-static)
// Delega a CollisionMap y convierte el resultado
const auto COLLISION_TILE = collision_map_->getTile(index);
return static_cast<Tile>(COLLISION_TILE);
}
// Indica si hay colision con un enemigo a partir de un rectangulo
auto Room::enemyCollision(SDL_FRect& rect) -> bool {
return enemy_manager_->checkCollision(rect);
}
// Indica si hay colision con un objeto a partir de un rectangulo
auto Room::itemCollision(SDL_FRect& rect) -> bool {
return item_manager_->checkCollision(rect);
}
// Obten la coordenada de la cuesta a partir de un punto perteneciente a ese tile
auto Room::getSlopeHeight(SDL_FPoint p, Tile slope) -> int {
// Delega a CollisionMap (método estático)
const auto COLLISION_TILE = static_cast<CollisionMap::Tile>(slope);
return CollisionMap::getSlopeHeight(p, COLLISION_TILE);
}
// === Métodos de colisión (delegados a CollisionMap) ===
// Comprueba las colisiones con paredes derechas
auto Room::checkRightSurfaces(const SDL_FRect& rect) -> int {
return collision_map_->checkRightSurfaces(rect);
}
// Comprueba las colisiones con paredes izquierdas
auto Room::checkLeftSurfaces(const SDL_FRect& rect) -> int {
return collision_map_->checkLeftSurfaces(rect);
}
// Comprueba las colisiones con techos
auto Room::checkTopSurfaces(const SDL_FRect& rect) -> int {
return collision_map_->checkTopSurfaces(rect);
}
// Comprueba las colisiones punto con techos
auto Room::checkTopSurfaces(const SDL_FPoint& p) -> bool {
return collision_map_->checkTopSurfaces(p);
}
// Comprueba las colisiones con suelos
auto Room::checkBottomSurfaces(const SDL_FRect& rect) -> int {
return collision_map_->checkBottomSurfaces(rect);
}
// Comprueba las colisiones con conveyor belts
auto Room::checkAutoSurfaces(const SDL_FRect& rect) -> int {
return collision_map_->checkAutoSurfaces(rect);
}
// Comprueba las colisiones punto con conveyor belts
auto Room::checkConveyorBelts(const SDL_FPoint& p) -> bool {
return collision_map_->checkConveyorBelts(p);
}
// Comprueba las colisiones línea con rampas izquierdas
auto Room::checkLeftSlopes(const LineVertical& line) -> int {
return collision_map_->checkLeftSlopes(line);
}
// Comprueba las colisiones punto con rampas izquierdas
auto Room::checkLeftSlopes(const SDL_FPoint& p) -> bool {
return collision_map_->checkLeftSlopes(p);
}
// Comprueba las colisiones línea con rampas derechas
auto Room::checkRightSlopes(const LineVertical& line) -> int {
return collision_map_->checkRightSlopes(line);
}
// Comprueba las colisiones punto con rampas derechas
auto Room::checkRightSlopes(const SDL_FPoint& p) -> bool {
return collision_map_->checkRightSlopes(p);
}
// Obtiene puntero a slope en un punto
auto Room::getSlopeAtPoint(const SDL_FPoint& p) const -> const LineDiagonal* {
return collision_map_->getSlopeAtPoint(p);
}
// Carga una habitación desde un archivo YAML (delegado a RoomLoader)
auto Room::loadYAML(const std::string& file_path, bool verbose) -> Data { // NOLINT(readability-convert-member-functions-to-static)
return RoomLoader::loadYAML(file_path, verbose);
}

View File

@@ -0,0 +1,143 @@
#pragma once
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include "game/entities/enemy.hpp" // Para EnemyData
#include "game/entities/item.hpp" // Para ItemData
#include "game/gameplay/scoreboard.hpp" // Para Scoreboard::Data
#include "utils/utils.hpp" // Para LineHorizontal, LineDiagonal, LineVertical
class Sprite; // lines 12-12
class Surface; // lines 13-13
class EnemyManager;
class ItemManager;
class CollisionMap;
class TilemapRenderer;
class Room {
public:
// -- Enumeraciones y estructuras ---
enum class Border : int {
TOP = 0,
RIGHT = 1,
BOTTOM = 2,
LEFT = 3,
NONE = 4
};
enum class Tile {
EMPTY,
WALL,
PASSABLE,
SLOPE_L,
SLOPE_R,
KILL,
ANIMATED
};
struct Data {
std::string number; // Numero de la habitación
std::string name; // Nombre de la habitación
std::string bg_color; // Color de fondo de la habitación
std::string border_color; // Color del borde de la pantalla
std::string item_color1; // Color 1 para los items de la habitación
std::string item_color2; // Color 2 para los items de la habitación
std::string upper_room; // Identificador de la habitación que se encuentra arriba
std::string lower_room; // Identificador de la habitación que se encuentra abajo
std::string left_room; // Identificador de la habitación que se encuentra a la izquierda
std::string right_room; // Identificador de la habitación que se encuentra a la derecha
std::string tile_set_file; // Imagen con los gráficos para la habitación
int conveyor_belt_direction{0}; // Sentido en el que arrastran las superficies automáticas de la habitación
std::vector<int> tile_map; // Índice de los tiles a dibujar en la habitación (embebido desde YAML)
std::vector<Enemy::Data> enemies; // Listado con los enemigos de la habitación
std::vector<Item::Data> items; // Listado con los items que hay en la habitación
};
// Constructor y destructor
Room(const std::string& room_path, std::shared_ptr<Scoreboard::Data> data);
~Room(); // NOLINT(modernize-use-equals-default, performance-trivially-destructible) -- defined in .cpp for unique_ptr with forward declarations
// --- Funciones ---
[[nodiscard]] auto getNumber() const -> const std::string& { return number_; } // Devuelve el numero de la habitación
[[nodiscard]] auto getName() const -> const std::string& { return name_; } // Devuelve el nombre de la habitación
[[nodiscard]] auto getBGColor() const -> Uint8 { return stringToColor(bg_color_); } // Devuelve el color de la habitación
[[nodiscard]] auto getBorderColor() const -> Uint8 { return stringToColor(border_color_); } // Devuelve el color del borde
void renderMap(); // Dibuja el mapa en pantalla
void renderEnemies(); // Dibuja los enemigos en pantalla
void renderItems(); // Dibuja los objetos en pantalla
#ifdef _DEBUG
void redrawMap(); // Redibuja el mapa (para actualizar modo debug)
void updateEditorMode(float delta_time); // Actualiza animaciones sin mover enemigos (para editor)
void resetEnemyPositions(const std::vector<Enemy::Data>& enemy_data); // Resetea enemigos a posiciones iniciales
auto getEnemyManager() -> EnemyManager* { return enemy_manager_.get(); } // Acceso al gestor de enemigos (para editor)
auto getItemManager() -> ItemManager* { return item_manager_.get(); } // Acceso al gestor de items (para editor)
void setBgColor(const std::string& color); // Cambia color de fondo y redibuja (para editor)
void setItemColors(const std::string& color1, const std::string& color2); // Cambia colores de items (para editor)
void setTile(int index, int tile_value); // Cambia un tile y redibuja (para editor)
[[nodiscard]] auto getTileSetFile() const -> const std::string& { return tile_set_file_; }
[[nodiscard]] auto getTileSetWidth() const -> int { return tile_set_width_; }
#endif
void update(float delta_time); // Actualiza las variables y objetos de la habitación
auto getRoom(Border border) -> std::string; // Devuelve la cadena del fichero de la habitación contigua segun el borde
auto getTile(SDL_FPoint point) -> Tile; // Devuelve el tipo de tile que hay en ese pixel
auto getTile(int index) -> Tile; // Devuelve el tipo de tile en un índice del tilemap
auto enemyCollision(SDL_FRect& rect) -> bool; // Indica si hay colision con un enemigo a partir de un rectangulo
auto itemCollision(SDL_FRect& rect) -> bool; // Indica si hay colision con un objeto a partir de un rectangulo
static auto getTileSize() -> int { return TILE_SIZE; } // Obten el tamaño del tile
static auto getSlopeHeight(SDL_FPoint p, Tile slope) -> int; // Obten la coordenada de la cuesta a partir de un punto perteneciente a ese tile
auto checkRightSurfaces(const SDL_FRect& rect) -> int; // Comprueba las colisiones
auto checkLeftSurfaces(const SDL_FRect& rect) -> int; // Comprueba las colisiones
auto checkTopSurfaces(const SDL_FRect& rect) -> int; // Comprueba las colisiones
auto checkBottomSurfaces(const SDL_FRect& rect) -> int; // Comprueba las colisiones
auto checkAutoSurfaces(const SDL_FRect& rect) -> int; // Comprueba las colisiones
auto checkTopSurfaces(const SDL_FPoint& p) -> bool; // Comprueba las colisiones
auto checkConveyorBelts(const SDL_FPoint& p) -> bool; // Comprueba las colisiones
auto checkLeftSlopes(const LineVertical& line) -> int; // Comprueba las colisiones
auto checkLeftSlopes(const SDL_FPoint& p) -> bool; // Comprueba las colisiones
auto checkRightSlopes(const LineVertical& line) -> int; // Comprueba las colisiones
auto checkRightSlopes(const SDL_FPoint& p) -> bool; // Comprueba las colisiones
[[nodiscard]] auto getSlopeAtPoint(const SDL_FPoint& p) const -> const LineDiagonal*; // Obtiene puntero a slope en un punto
void setPaused(bool value); // Pone el mapa en modo pausa
[[nodiscard]] auto getConveyorBeltDirection() const -> int { return conveyor_belt_direction_; } // Obten la direccion de las superficies automaticas
// Método de carga de archivos YAML (delegado a RoomLoader)
static auto loadYAML(const std::string& file_path, bool verbose = false) -> Data; // Carga habitación desde archivo YAML unificado
private:
// Constantes
static constexpr int TILE_SIZE = 8; // Ancho del tile en pixels
static constexpr int MAP_WIDTH = 32; // Ancho del mapa en tiles
static constexpr int MAP_HEIGHT = 16; // Alto del mapa en tiles
// Objetos y punteros
std::unique_ptr<EnemyManager> enemy_manager_; // Gestor de enemigos de la habitación
std::unique_ptr<ItemManager> item_manager_; // Gestor de items de la habitación
std::unique_ptr<CollisionMap> collision_map_; // Mapa de colisiones de la habitación
std::unique_ptr<TilemapRenderer> tilemap_renderer_; // Renderizador del mapa de tiles
std::shared_ptr<Surface> surface_; // Textura con los graficos de la habitación
std::shared_ptr<Scoreboard::Data> data_; // Puntero a los datos del marcador
// --- Variables ---
std::string number_; // Numero de la habitación
std::string name_; // Nombre de la habitación
std::string bg_color_; // Color de fondo de la habitación
std::string border_color_; // Color del borde de la pantalla
std::string item_color1_; // Color 1 para los items de la habitación
std::string item_color2_; // Color 2 para los items de la habitación
std::string upper_room_; // Identificador de la habitación que se encuentra arriba
std::string lower_room_; // Identificador de la habitación que se encuentra abajp
std::string left_room_; // Identificador de la habitación que se encuentra a la izquierda
std::string right_room_; // Identificador de la habitación que se encuentra a la derecha
std::string tile_set_file_; // Imagen con los graficos para la habitación
std::vector<int> tile_map_; // Indice de los tiles a dibujar en la habitación (embebido desde YAML)
int conveyor_belt_direction_{0}; // Sentido en el que arrastran las superficies automáticas de la habitación
bool is_paused_{false}; // Indica si el mapa esta en modo pausa
int tile_set_width_{0}; // Ancho del tileset en tiles
// --- Funciones ---
void initializeRoom(const Data& room); // Inicializa los valores
void openTheJail(); // Abre la jail para poder entrar
};

View File

@@ -0,0 +1,375 @@
#include "room_loader.hpp"
#include <exception> // Para exception
#include <iostream> // Para cout, cerr
#include "core/resources/resource_helper.hpp" // Para Resource::Helper
#include "external/fkyaml_node.hpp" // Para fkyaml::node
#include "game/options.hpp" // Para Options::language
#include "utils/defines.hpp" // Para Tile::SIZE
#include "utils/utils.hpp" // Para stringToColor
// Convierte room connection de YAML a formato interno
auto RoomLoader::convertRoomConnection(const std::string& value) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
if (value == "null" || value.empty()) {
return "0";
}
// Si ya tiene .yaml, devolverlo tal cual; si no, añadirlo
if (value.size() > 5 && value.substr(value.size() - 5) == ".yaml") {
return value;
}
return value + ".yaml";
}
// Convierte string de autoSurface a int
auto RoomLoader::convertAutoSurface(const fkyaml::node& node) -> int { // NOLINT(readability-convert-member-functions-to-static)
if (node.is_integer()) {
return node.get_value<int>();
}
if (node.is_string()) {
const auto VALUE = node.get_value<std::string>();
if (VALUE == "left") {
return -1;
}
if (VALUE == "right") {
return 1;
}
}
return 0; // "none" o default
}
// Convierte un tilemap 2D a vector 1D flat
auto RoomLoader::flattenTilemap(const std::vector<std::vector<int>>& tilemap_2d) -> std::vector<int> { // NOLINT(readability-convert-member-functions-to-static, readability-named-parameter)
std::vector<int> tilemap_flat;
tilemap_flat.reserve(512); // 16 rows × 32 cols
for (const auto& row : tilemap_2d) {
for (int tile : row) {
tilemap_flat.push_back(tile);
}
}
return tilemap_flat;
}
// Parsea la configuración general de la habitación
void RoomLoader::parseRoomConfig(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name) { // NOLINT(readability-convert-member-functions-to-static)
if (!yaml.contains("room")) {
return;
}
const auto& room_node = yaml["room"];
// Extract room number from filename (e.g., "01.yaml" → "01")
room.number = file_name.substr(0, file_name.find_last_of('.'));
// Basic properties
const std::string LANG_KEY = "name_" + Options::language;
if (room_node.contains(LANG_KEY)) {
room.name = room_node[LANG_KEY].get_value<std::string>();
} else if (room_node.contains("name")) {
room.name = room_node["name"].get_value<std::string>();
}
if (room_node.contains("bgColor")) {
room.bg_color = room_node["bgColor"].get_value<std::string>();
}
if (room_node.contains("border")) {
room.border_color = room_node["border"].get_value<std::string>();
}
if (room_node.contains("tileSetFile")) {
room.tile_set_file = room_node["tileSetFile"].get_value<std::string>();
}
// Room connections
if (room_node.contains("connections")) {
parseRoomConnections(room_node["connections"], room);
}
// Item colors
room.item_color1 = room_node.contains("itemColor1")
? room_node["itemColor1"].get_value_or<std::string>("yellow")
: "yellow";
room.item_color2 = room_node.contains("itemColor2")
? room_node["itemColor2"].get_value_or<std::string>("magenta")
: "magenta";
// Dirección de la cinta transportadora (left/none/right)
room.conveyor_belt_direction = room_node.contains("conveyorBelt")
? convertAutoSurface(room_node["conveyorBelt"])
: 0;
}
// Parsea las conexiones de la habitación (arriba/abajo/izq/der)
void RoomLoader::parseRoomConnections(const fkyaml::node& conn_node, Room::Data& room) {
room.upper_room = conn_node.contains("up")
? convertRoomConnection(conn_node["up"].get_value_or<std::string>("null"))
: "0";
room.lower_room = conn_node.contains("down")
? convertRoomConnection(conn_node["down"].get_value_or<std::string>("null"))
: "0";
room.left_room = conn_node.contains("left")
? convertRoomConnection(conn_node["left"].get_value_or<std::string>("null"))
: "0";
room.right_room = conn_node.contains("right")
? convertRoomConnection(conn_node["right"].get_value_or<std::string>("null"))
: "0";
}
// Parsea el tilemap de la habitación
void RoomLoader::parseTilemap(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name, bool verbose) { // NOLINT(readability-convert-member-functions-to-static)
if (!yaml.contains("tilemap")) {
std::cerr << "Warning: No tilemap found in " << file_name << '\n';
return;
}
const auto& tilemap_node = yaml["tilemap"];
// Read 2D array
std::vector<std::vector<int>> tilemap_2d;
tilemap_2d.reserve(16);
for (const auto& row_node : tilemap_node) {
std::vector<int> row;
row.reserve(32);
for (const auto& tile_node : row_node) {
row.push_back(tile_node.get_value<int>());
}
tilemap_2d.push_back(row);
}
// Convert to 1D flat array
room.tile_map = flattenTilemap(tilemap_2d);
if (verbose) {
std::cout << "Loaded tilemap: " << room.tile_map.size() << " tiles\n";
}
}
// Parsea los límites de movimiento de un enemigo
void RoomLoader::parseEnemyBoundaries(const fkyaml::node& bounds_node, Enemy::Data& enemy) { // NOLINT(readability-convert-member-functions-to-static)
// Nuevo formato: position1 y position2
if (bounds_node.contains("position1")) {
const auto& pos1 = bounds_node["position1"];
if (pos1.contains("x")) {
enemy.x1 = pos1["x"].get_value<int>() * Tile::SIZE;
}
if (pos1.contains("y")) {
enemy.y1 = pos1["y"].get_value<int>() * Tile::SIZE;
}
}
if (bounds_node.contains("position2")) {
const auto& pos2 = bounds_node["position2"];
if (pos2.contains("x")) {
enemy.x2 = pos2["x"].get_value<int>() * Tile::SIZE;
}
if (pos2.contains("y")) {
enemy.y2 = pos2["y"].get_value<int>() * Tile::SIZE;
}
}
// Formato antiguo: x1/y1/x2/y2 (compatibilidad)
if (bounds_node.contains("x1")) {
enemy.x1 = bounds_node["x1"].get_value<int>() * Tile::SIZE;
}
if (bounds_node.contains("y1")) {
enemy.y1 = bounds_node["y1"].get_value<int>() * Tile::SIZE;
}
if (bounds_node.contains("x2")) {
enemy.x2 = bounds_node["x2"].get_value<int>() * Tile::SIZE;
}
if (bounds_node.contains("y2")) {
enemy.y2 = bounds_node["y2"].get_value<int>() * Tile::SIZE;
}
}
// Parsea los datos de un enemigo individual
auto RoomLoader::parseEnemyData(const fkyaml::node& enemy_node) -> Enemy::Data { // NOLINT(readability-convert-member-functions-to-static)
Enemy::Data enemy;
// Animation path
if (enemy_node.contains("animation")) {
enemy.animation_path = enemy_node["animation"].get_value<std::string>();
}
// Position (in tiles, convert to pixels)
if (enemy_node.contains("position")) {
const auto& pos = enemy_node["position"];
if (pos.contains("x")) {
enemy.x = pos["x"].get_value<float>() * Tile::SIZE;
}
if (pos.contains("y")) {
enemy.y = pos["y"].get_value<float>() * Tile::SIZE;
}
}
// Velocity (already in pixels/second)
if (enemy_node.contains("velocity")) {
const auto& vel = enemy_node["velocity"];
if (vel.contains("x")) {
enemy.vx = vel["x"].get_value<float>();
}
if (vel.contains("y")) {
enemy.vy = vel["y"].get_value<float>();
}
}
// Boundaries (in tiles, convert to pixels)
if (enemy_node.contains("boundaries")) {
parseEnemyBoundaries(enemy_node["boundaries"], enemy);
}
// Color
enemy.color = enemy_node.contains("color")
? enemy_node["color"].get_value_or<std::string>("white")
: "white";
// Optional fields
enemy.flip = enemy_node.contains("flip")
? enemy_node["flip"].get_value_or<bool>(false)
: false;
enemy.mirror = enemy_node.contains("mirror")
? enemy_node["mirror"].get_value_or<bool>(false)
: false;
enemy.frame = enemy_node.contains("frame")
? enemy_node["frame"].get_value_or<int>(-1)
: -1;
return enemy;
}
// Parsea la lista de enemigos de la habitación
void RoomLoader::parseEnemies(const fkyaml::node& yaml, Room::Data& room, bool verbose) { // NOLINT(readability-convert-member-functions-to-static)
if (!yaml.contains("enemies") || yaml["enemies"].is_null()) {
return;
}
const auto& enemies_node = yaml["enemies"];
for (const auto& enemy_node : enemies_node) {
room.enemies.push_back(parseEnemyData(enemy_node));
}
if (verbose) {
std::cout << "Loaded " << room.enemies.size() << " enemies\n";
}
}
// Parsea los datos de un item individual
auto RoomLoader::parseItemData(const fkyaml::node& item_node, const Room::Data& room) -> Item::Data { // NOLINT(readability-convert-member-functions-to-static)
Item::Data item;
// Tileset file
if (item_node.contains("tileSetFile")) {
item.tile_set_file = item_node["tileSetFile"].get_value<std::string>();
}
// Tile index
if (item_node.contains("tile")) {
item.tile = item_node["tile"].get_value<int>();
}
// Position (in tiles, convert to pixels)
if (item_node.contains("position")) {
const auto& pos = item_node["position"];
if (pos.contains("x")) {
item.x = pos["x"].get_value<float>() * Tile::SIZE;
}
if (pos.contains("y")) {
item.y = pos["y"].get_value<float>() * Tile::SIZE;
}
}
// Counter
item.counter = item_node.contains("counter")
? item_node["counter"].get_value_or<int>(0)
: 0;
// Colors (assigned from room defaults)
item.color1 = stringToColor(room.item_color1);
item.color2 = stringToColor(room.item_color2);
return item;
}
// Parsea la lista de items de la habitación
void RoomLoader::parseItems(const fkyaml::node& yaml, Room::Data& room, bool verbose) { // NOLINT(readability-convert-member-functions-to-static)
if (!yaml.contains("items") || yaml["items"].is_null()) {
return;
}
const auto& items_node = yaml["items"];
for (const auto& item_node : items_node) {
room.items.push_back(parseItemData(item_node, room));
}
if (verbose) {
std::cout << "Loaded " << room.items.size() << " items\n";
}
}
#ifdef _DEBUG
// Carga una habitación desde un string YAML (para el editor de mapas, evita el resource pack)
auto RoomLoader::loadFromString(const std::string& yaml_content, const std::string& file_name) -> Room::Data {
Room::Data room;
try {
auto yaml = fkyaml::node::deserialize(yaml_content);
parseRoomConfig(yaml, room, file_name);
parseTilemap(yaml, room, file_name, false);
parseEnemies(yaml, room, false);
parseItems(yaml, room, false);
} catch (const fkyaml::exception& e) {
std::cerr << "YAML parsing error in " << file_name << ": " << e.what() << '\n';
} catch (const std::exception& e) {
std::cerr << "Error loading room " << file_name << ": " << e.what() << '\n';
}
return room;
}
#endif
// Carga un archivo de room en formato YAML
auto RoomLoader::loadYAML(const std::string& file_path, bool verbose) -> Room::Data { // NOLINT(readability-convert-member-functions-to-static)
Room::Data room;
// Extract filename for logging
const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1);
try {
// Load YAML file using ResourceHelper (supports both filesystem and pack)
auto file_data = Resource::Helper::loadFile(file_path);
if (file_data.empty()) {
std::cerr << "Error: Unable to load file " << FILE_NAME << '\n';
return room;
}
// Parse YAML from string
std::string yaml_content(file_data.begin(), file_data.end());
auto yaml = fkyaml::node::deserialize(yaml_content);
// Delegación a funciones especializadas
parseRoomConfig(yaml, room, FILE_NAME);
parseTilemap(yaml, room, FILE_NAME, verbose);
parseEnemies(yaml, room, verbose);
parseItems(yaml, room, verbose);
if (verbose) {
std::cout << "Room loaded successfully: " << FILE_NAME << '\n';
}
} catch (const fkyaml::exception& e) {
std::cerr << "YAML parsing error in " << FILE_NAME << ": " << e.what() << '\n';
} catch (const std::exception& e) {
std::cerr << "Error loading room " << FILE_NAME << ": " << e.what() << '\n';
}
return room;
}

View File

@@ -0,0 +1,136 @@
#pragma once
#include <string> // Para string
#include <vector> // Para vector
#include "external/fkyaml_node.hpp" // Para fkyaml::node
#include "game/entities/enemy.hpp" // Para Enemy::Data
#include "game/entities/item.hpp" // Para Item::Data
#include "game/gameplay/room.hpp" // Para Room::Data
/**
* @brief Cargador de archivos de habitaciones en formato YAML
*
* Responsabilidades:
* - Cargar archivos de room en formato YAML unificado (.yaml)
* - Parsear datos de room, tilemap, enemies e items
* - Convertir tipos de datos (tiles, posiciones, colores)
* - Validar y propagar errores de carga
*
* Esta clase contiene solo métodos estáticos y no debe instanciarse.
*/
class RoomLoader {
public:
// Constructor eliminado para prevenir instanciación
RoomLoader() = delete;
~RoomLoader() = delete;
RoomLoader(const RoomLoader&) = delete;
auto operator=(const RoomLoader&) -> RoomLoader& = delete;
RoomLoader(RoomLoader&&) = delete;
auto operator=(RoomLoader&&) -> RoomLoader& = delete;
/**
* @brief Carga un archivo de room en formato YAML
* @param file_path Ruta al archivo YAML de habitación
* @param verbose Si true, muestra información de debug
* @return Room::Data con todos los datos de la habitación incluyendo:
* - Configuración de la habitación (nombre, colores, etc.)
* - Tilemap completo (convertido de 2D a 1D)
* - Lista de enemigos con todas sus propiedades
* - Lista de items con todas sus propiedades
*
* El formato YAML esperado incluye:
* - room: configuración general
* - tilemap: array 2D de 16x32 tiles (convertido a vector 1D de 512 elementos)
* - enemies: lista de enemigos (opcional)
* - items: lista de items (opcional)
*/
static auto loadYAML(const std::string& file_path, bool verbose = false) -> Room::Data;
#ifdef _DEBUG
static auto loadFromString(const std::string& yaml_content, const std::string& file_name) -> Room::Data;
#endif
private:
/**
* @brief Convierte room connection de YAML a formato interno
* @param value Valor del YAML (vacío, "02" o "02.yaml")
* @return "0" para sin conexión, o nombre del archivo con extensión
*/
static auto convertRoomConnection(const std::string& value) -> std::string;
/**
* @brief Convierte autoSurface de YAML a int
* @param node Nodo YAML (puede ser int o string "left"/"none"/"right")
* @return -1 para left, 0 para none, 1 para right
*/
static auto convertAutoSurface(const fkyaml::node& node) -> int;
/**
* @brief Convierte un tilemap 2D a vector 1D flat
* @param tilemap_2d Array 2D de tiles (16 rows × 32 cols)
* @return Vector 1D flat con 512 elementos
*/
static auto flattenTilemap(const std::vector<std::vector<int>>& tilemap_2d) -> std::vector<int>; // NOLINT(readability-avoid-const-params-in-decls)
/**
* @brief Parsea la configuración general de la habitación
* @param yaml Nodo raíz del YAML
* @param room Estructura de datos de la habitación a rellenar
* @param file_name Nombre del archivo para logging
*/
static void parseRoomConfig(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name);
/**
* @brief Parsea las conexiones de la habitación (arriba/abajo/izq/der)
* @param conn_node Nodo YAML con las conexiones
* @param room Estructura de datos de la habitación a rellenar
*/
static void parseRoomConnections(const fkyaml::node& conn_node, Room::Data& room);
/**
* @brief Parsea el tilemap de la habitación
* @param yaml Nodo raíz del YAML
* @param room Estructura de datos de la habitación a rellenar
* @param file_name Nombre del archivo para logging
* @param verbose Si true, muestra información de debug
*/
static void parseTilemap(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name, bool verbose);
/**
* @brief Parsea la lista de enemigos de la habitación
* @param yaml Nodo raíz del YAML
* @param room Estructura de datos de la habitación a rellenar
* @param verbose Si true, muestra información de debug
*/
static void parseEnemies(const fkyaml::node& yaml, Room::Data& room, bool verbose);
/**
* @brief Parsea los datos de un enemigo individual
* @param enemy_node Nodo YAML del enemigo
* @return Estructura Enemy::Data con los datos parseados
*/
static auto parseEnemyData(const fkyaml::node& enemy_node) -> Enemy::Data;
/**
* @brief Parsea los límites de movimiento de un enemigo
* @param bounds_node Nodo YAML con los límites
* @param enemy Estructura del enemigo a rellenar
*/
static void parseEnemyBoundaries(const fkyaml::node& bounds_node, Enemy::Data& enemy);
/**
* @brief Parsea la lista de items de la habitación
* @param yaml Nodo raíz del YAML
* @param room Estructura de datos de la habitación a rellenar
* @param verbose Si true, muestra información de debug
*/
static void parseItems(const fkyaml::node& yaml, Room::Data& room, bool verbose);
/**
* @brief Parsea los datos de un item individual
* @param item_node Nodo YAML del item
* @param room Datos de la habitación (para colores por defecto)
* @return Estructura Item::Data con los datos parseados
*/
static auto parseItemData(const fkyaml::node& item_node, const Room::Data& room) -> Item::Data;
};

View File

@@ -0,0 +1,20 @@
#include "game/gameplay/room_tracker.hpp"
#include <algorithm> // Para std::ranges::any_of
// Comprueba si la habitación ya ha sido visitada
auto RoomTracker::hasBeenVisited(const std::string& name) -> bool { // NOLINT(readability-convert-member-functions-to-static)
return std::ranges::any_of(rooms_, [&name](const auto& l) -> bool { return l == name; });
}
// Añade la habitación a la lista
auto RoomTracker::addRoom(const std::string& name) -> bool { // NOLINT(readability-convert-member-functions-to-static)
// Comprueba si la habitación ya ha sido visitada
if (!hasBeenVisited(name)) {
// En caso contrario añádela a la lista
rooms_.push_back(name);
return true; // NOLINT(readability-simplify-boolean-expr)
}
return false;
}

View File

@@ -0,0 +1,17 @@
#pragma once
#include <string> // Para string
#include <vector> // Para vector
class RoomTracker {
public:
RoomTracker() = default; // Constructor
~RoomTracker() = default; // Destructor
auto addRoom(const std::string& name) -> bool; // Añade la habitación a la lista
private:
auto hasBeenVisited(const std::string& name) -> bool; // Comprueba si ya ha sido visitada
std::vector<std::string> rooms_; // Lista con habitaciones visitadas
};

View File

@@ -0,0 +1,183 @@
#include "game/gameplay/scoreboard.hpp"
#include <SDL3/SDL.h>
#include <utility>
#include "core/locale/locale.hpp" // Para Locale
#include "core/rendering/screen.hpp" // Para Screen
#include "core/rendering/sprite/animated_sprite.hpp" // Para SAnimatedSprite
#include "core/rendering/surface.hpp" // Para Surface
#include "core/rendering/text.hpp" // Para Text
#include "core/resources/resource_cache.hpp" // Para Resource
#include "game/entities/player.hpp" // Para Player::skinToAnimationPath
#include "game/options.hpp" // Para Options, options, Cheat, OptionsGame
#include "utils/defines.hpp" // Para BLOCK
#include "utils/utils.hpp" // Para stringToColor
// Constructor
Scoreboard::Scoreboard(std::shared_ptr<Data> data)
: item_surface_(Resource::Cache::get()->getSurface("items.gif")),
data_(std::move(std::move(data))) {
const float SURFACE_WIDTH = Options::game.width;
constexpr float SURFACE_HEIGHT = 6.0F * Tile::SIZE;
// Reserva memoria para los objetos
const std::string PLAYER_ANIM_PATH = Player::skinToAnimationPath(Options::game.player_skin);
const auto& player_animation_data = Resource::Cache::get()->getAnimationData(PLAYER_ANIM_PATH);
player_sprite_ = std::make_shared<AnimatedSprite>(player_animation_data);
player_sprite_->setCurrentAnimation("default");
surface_ = std::make_shared<Surface>(SURFACE_WIDTH, SURFACE_HEIGHT);
surface_dest_ = {.x = 0, .y = Options::game.height - SURFACE_HEIGHT, .w = SURFACE_WIDTH, .h = SURFACE_HEIGHT};
// Inicializa el color de items
items_color_ = stringToColor("white");
// Inicializa el vector de colores
const std::vector<std::string> COLORS = {"blue", "magenta", "green", "cyan", "yellow", "white", "bright_blue", "bright_magenta", "bright_green", "bright_cyan", "bright_yellow", "bright_white"};
for (const auto& color : COLORS) {
color_.push_back(stringToColor(color));
}
}
// Pinta el objeto en pantalla
void Scoreboard::render() {
surface_->render(nullptr, &surface_dest_);
}
// Actualiza las variables del objeto
void Scoreboard::update(float delta_time) {
// Acumular tiempo para animaciones
time_accumulator_ += delta_time;
// Actualiza el color de la cantidad de items recogidos
updateItemsColor(delta_time);
// Dibuja la textura
fillTexture();
if (!is_paused_) {
// Si está en pausa no se actualiza el reloj
clock_ = getTime();
}
}
// Obtiene el tiempo transcurrido de partida
auto Scoreboard::getTime() -> Scoreboard::ClockData { // NOLINT(readability-convert-member-functions-to-static)
const Uint32 TIME_ELAPSED = SDL_GetTicks() - data_->ini_clock - paused_time_elapsed_;
ClockData time;
time.hours = TIME_ELAPSED / 3600000;
time.minutes = TIME_ELAPSED / 60000;
time.seconds = TIME_ELAPSED / 1000;
time.separator = (TIME_ELAPSED % 1000 <= 500) ? ":" : " ";
return time;
}
// Actualiza el sprite del jugador con la skin actual
void Scoreboard::refreshPlayerSkin() {
const std::string PLAYER_ANIM_PATH = Player::skinToAnimationPath(Options::game.player_skin);
const auto& player_animation_data = Resource::Cache::get()->getAnimationData(PLAYER_ANIM_PATH);
player_sprite_ = std::make_shared<AnimatedSprite>(player_animation_data);
player_sprite_->setCurrentAnimation("default");
}
// Pone el marcador en modo pausa
void Scoreboard::setPaused(bool value) {
if (is_paused_ == value) {
// Evita ejecutar lógica si el estado no cambia
return;
}
is_paused_ = value;
if (is_paused_) {
// Guarda el tiempo actual al pausar
paused_time_ = SDL_GetTicks();
} else {
// Calcula el tiempo pausado acumulado al reanudar
paused_time_elapsed_ += SDL_GetTicks() - paused_time_;
}
}
// Actualiza el color de la cantidad de items recogidos
void Scoreboard::updateItemsColor(float delta_time) {
if (!data_->jail_is_open) {
return;
}
items_color_timer_ += delta_time;
// Resetear timer cada 2 ciclos (0.666s total)
if (items_color_timer_ >= ITEMS_COLOR_BLINK_DURATION * 2.0F) {
items_color_timer_ = 0.0F;
}
// Alternar color cada ITEMS_COLOR_BLINK_DURATION
if (items_color_timer_ < ITEMS_COLOR_BLINK_DURATION) {
items_color_ = stringToColor("white");
} else {
items_color_ = stringToColor("magenta");
}
}
// Devuelve la cantidad de minutos de juego transcurridos
auto Scoreboard::getMinutes() -> int {
return getTime().minutes;
}
// Dibuja los elementos del marcador en la textura
void Scoreboard::fillTexture() {
// Empieza a dibujar en la textura
auto previuos_renderer = Screen::get()->getRendererSurface();
Screen::get()->setRendererSurface(surface_);
// Limpia la textura
surface_->clear(stringToColor("black"));
// Dibuja las vidas
const int WALK_FRAMES = player_sprite_->getCurrentAnimationSize();
const int DESP = static_cast<int>(time_accumulator_ / SPRITE_WALK_CYCLE_DURATION) % (WALK_FRAMES * 2);
const int FRAME = DESP % WALK_FRAMES;
player_sprite_->setCurrentAnimationFrame(FRAME);
player_sprite_->setPosY(LINE2_Y);
for (int i = 0; i < data_->lives; ++i) {
player_sprite_->setPosX(LIVES_START_X + (LIVES_SPACING * i) + DESP);
const int INDEX = i % color_.size();
player_sprite_->render(1, color_.at(INDEX));
}
// Muestra si suena la música
if (data_->music) {
const Uint8 C = data_->color;
SDL_FRect clip = {.x = 0, .y = 8, .w = 8, .h = 8};
item_surface_->renderWithColorReplace(MUSIC_ICON_X, LINE2_Y, 1, C, &clip);
}
// Escribe los textos
auto text = Resource::Cache::get()->getText("smb2");
const std::string TIME_TEXT = std::to_string((clock_.minutes % 100) / 10) + std::to_string(clock_.minutes % 10) + clock_.separator + std::to_string((clock_.seconds % 60) / 10) + std::to_string(clock_.seconds % 10);
const std::string ITEMS_TEXT = std::to_string(data_->items / 100) + std::to_string((data_->items % 100) / 10) + std::to_string(data_->items % 10);
text->writeColored(ITEMS_LABEL_X, LINE1_Y, Locale::get()->get("scoreboard.items"), data_->color); // NOLINT(readability-static-accessed-through-instance)
text->writeColored(ITEMS_VALUE_X, LINE1_Y, ITEMS_TEXT, items_color_);
text->writeColored(TIME_LABEL_X, LINE1_Y, Locale::get()->get("scoreboard.time"), data_->color); // NOLINT(readability-static-accessed-through-instance)
text->writeColored(TIME_VALUE_X, LINE1_Y, TIME_TEXT, stringToColor("white"));
const std::string ROOMS_TEXT = std::to_string(data_->rooms / 100) + std::to_string((data_->rooms % 100) / 10) + std::to_string(data_->rooms % 10);
text->writeColored(ROOMS_LABEL_X, LINE2_Y, Locale::get()->get("scoreboard.rooms"), stringToColor("white")); // NOLINT(readability-static-accessed-through-instance)
text->writeColored(ROOMS_VALUE_X, LINE2_Y, ROOMS_TEXT, stringToColor("white"));
// Indicadores de trucos activos (fuente 8bithud)
auto cheat_text = Resource::Cache::get()->getText("8bithud");
if (Options::cheats.infinite_lives == Options::Cheat::State::ENABLED) {
cheat_text->writeColored(CHEAT_INF_LIVES_X, CHEAT_INF_LIVES_Y, Locale::get()->get("scoreboard.cheat_infinite_lives"), data_->color); // NOLINT(readability-static-accessed-through-instance)
}
if (Options::cheats.invincible == Options::Cheat::State::ENABLED) {
cheat_text->writeColored(CHEAT_INVINCIBLE_X, CHEAT_INVINCIBLE_Y, Locale::get()->get("scoreboard.cheat_invincibility"), data_->color); // NOLINT(readability-static-accessed-through-instance)
}
// Deja el renderizador como estaba
Screen::get()->setRendererSurface(previuos_renderer);
}

View File

@@ -0,0 +1,85 @@
#pragma once
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr
#include <string> // Para string, basic_string
#include <utility>
#include <vector> // Para vector
class AnimatedSprite; // lines 10-10
class Surface; // lines 11-11
class Scoreboard {
public:
// Tipos anidados
struct Data {
int items{0}; // Lleva la cuenta de los objetos recogidos
int lives{0}; // Lleva la cuenta de las vidas restantes del jugador
int rooms{0}; // Lleva la cuenta de las habitaciones visitadas
bool music{true}; // Indica si ha de sonar la música durante el juego
Uint8 color{0}; // Color para escribir el texto del marcador
Uint32 ini_clock{0}; // Tiempo inicial para calcular el tiempo transcurrido
bool jail_is_open{false}; // Indica si se puede entrar a la Jail
};
// Métodos públicos
explicit Scoreboard(std::shared_ptr<Data> data); // Constructor
~Scoreboard() = default; // Destructor
void render(); // Pinta el objeto en pantalla
void update(float delta_time); // Actualiza las variables del objeto
void setPaused(bool value); // Pone el marcador en modo pausa
void refreshPlayerSkin(); // Actualiza el sprite del jugador con la skin actual
auto getMinutes() -> int; // Devuelve la cantidad de minutos de juego transcurridos
private:
// Tipos anidados
struct ClockData {
int hours{0}; // Horas transcurridas
int minutes{0}; // Minutos transcurridos
int seconds{0}; // Segundos transcurridos
std::string separator{":"}; // Separador para mostrar el tiempo
};
// Constantes de tiempo
static constexpr float ITEMS_COLOR_BLINK_DURATION = 0.333F; // Duración de cada estado del parpadeo (era 10 frames @ 60fps)
static constexpr float SPRITE_WALK_CYCLE_DURATION = 0.667F; // Duración del ciclo de caminar (era 40 frames @ 60fps)
// Posición de los elementos del marcador (en pixels, Tile::SIZE = 8)
static constexpr int LINE1_Y = 8; // Fila superior (1 * Tile::SIZE)
static constexpr int LINE2_Y = 24; // Fila inferior (3 * Tile::SIZE)
static constexpr int ITEMS_LABEL_X = 8; // "TRESORS PILLATS" / "ITEMS COLLECTED"
static constexpr int ITEMS_VALUE_X = 136; // Valor numérico de items
static constexpr int TIME_LABEL_X = 160; // "HORA" / "TIME"
static constexpr int TIME_VALUE_X = 208; // Valor numérico del tiempo
static constexpr int LIVES_START_X = 8; // Primera vida (sprite)
static constexpr int LIVES_SPACING = 16; // Separación entre vidas
static constexpr int MUSIC_ICON_X = 160; // Icono de música
static constexpr int ROOMS_LABEL_X = 176; // "SALES" / "ROOMS"
static constexpr int ROOMS_VALUE_X = 224; // Valor numérico de salas
static constexpr int CHEAT_INF_LIVES_X = 176; // Indicador "vides inf" / "inf lives"
static constexpr int CHEAT_INF_LIVES_Y = 34; // Posición Y del indicador de vidas infinitas
static constexpr int CHEAT_INVINCIBLE_X = 231; // Indicador "inv"
static constexpr int CHEAT_INVINCIBLE_Y = 34; // Posición Y del indicador de invencibilidad
// Métodos privados
auto getTime() -> ClockData; // Obtiene el tiempo transcurrido de partida
void updateItemsColor(float delta_time); // Actualiza el color de la cantidad de items recogidos
void fillTexture(); // Dibuja los elementos del marcador en la surface
// Objetos y punteros
std::shared_ptr<AnimatedSprite> player_sprite_; // Sprite para mostrar las vidas en el marcador
std::shared_ptr<Surface> item_surface_; // Surface con los graficos para los elementos del marcador
std::shared_ptr<Data> data_; // Contiene las variables a mostrar en el marcador
std::shared_ptr<Surface> surface_; // Surface donde dibujar el marcador
// Variables de estado
std::vector<Uint8> color_; // Vector con los colores del objeto
bool is_paused_{false}; // Indica si el marcador esta en modo pausa
Uint32 paused_time_{0}; // Milisegundos que ha estado el marcador en pausa
Uint32 paused_time_elapsed_{0}; // Tiempo acumulado en pausa
ClockData clock_{}; // Contiene las horas, minutos y segundos transcurridos desde el inicio de la partida
Uint8 items_color_{0}; // Color de la cantidad de items recogidos
SDL_FRect surface_dest_{}; // Rectangulo donde dibujar la surface del marcador
float time_accumulator_{0.0F}; // Acumulador de tiempo para animaciones
float items_color_timer_{0.0F}; // Timer para parpadeo de color de items
};

View File

@@ -0,0 +1,228 @@
#include "tilemap_renderer.hpp"
#include "core/rendering/screen.hpp"
#include "core/rendering/sprite/sprite.hpp"
#include "core/rendering/surface.hpp"
#ifdef _DEBUG
#include "core/system/debug.hpp"
#endif
#include "game/gameplay/collision_map.hpp"
#include "utils/utils.hpp"
// Constructor
TilemapRenderer::TilemapRenderer(std::vector<int> tile_map, int tile_set_width, std::shared_ptr<Surface> tileset_surface, std::string bg_color, int conveyor_belt_direction)
: tile_map_(std::move(tile_map)),
tile_set_width_(tile_set_width),
tileset_surface_(std::move(tileset_surface)),
bg_color_(std::move(bg_color)),
conveyor_belt_direction_(conveyor_belt_direction) {
// Crear la surface del mapa
map_surface_ = std::make_shared<Surface>(PlayArea::WIDTH, PlayArea::HEIGHT);
}
// Inicializa el renderizador
void TilemapRenderer::initialize(const CollisionMap* collision_map) {
setAnimatedTiles(collision_map);
fillMapTexture(collision_map);
}
// Actualiza las animaciones de tiles
void TilemapRenderer::update(float delta_time) {
if (is_paused_) {
return;
}
// Actualiza el acumulador de tiempo
time_accumulator_ += delta_time;
// Actualiza los tiles animados
updateAnimatedTiles();
}
// Renderiza el mapa completo en pantalla
void TilemapRenderer::render() {
// Dibuja la textura con el mapa en pantalla
SDL_FRect dest = {.x = 0, .y = 0, .w = PlayArea::WIDTH, .h = PlayArea::HEIGHT};
map_surface_->render(nullptr, &dest);
// Dibuja los tiles animados
#ifdef _DEBUG
if (!Debug::get()->isEnabled()) {
renderAnimatedTiles();
}
#else
renderAnimatedTiles();
#endif
}
#ifdef _DEBUG
// Renderiza las superficies de colisión en modo debug (función helper estática)
static void renderDebugCollisionSurfaces(const CollisionMap* collision_map) {
auto surface = Screen::get()->getRendererSurface();
// BottomSurfaces
for (auto l : collision_map->getBottomFloors()) {
surface->drawLine(l.x1, l.y, l.x2, l.y, static_cast<Uint8>(PaletteColor::BLUE));
}
// TopSurfaces
for (auto l : collision_map->getTopFloors()) {
surface->drawLine(l.x1, l.y, l.x2, l.y, static_cast<Uint8>(PaletteColor::RED));
}
// LeftSurfaces
for (auto l : collision_map->getLeftWalls()) {
surface->drawLine(l.x, l.y1, l.x, l.y2, static_cast<Uint8>(PaletteColor::GREEN));
}
// RightSurfaces
for (auto l : collision_map->getRightWalls()) {
surface->drawLine(l.x, l.y1, l.x, l.y2, static_cast<Uint8>(PaletteColor::MAGENTA));
}
// LeftSlopes
for (auto l : collision_map->getLeftSlopes()) {
surface->drawLine(l.x1, l.y1, l.x2, l.y2, static_cast<Uint8>(PaletteColor::CYAN));
}
// RightSlopes
for (auto l : collision_map->getRightSlopes()) {
surface->drawLine(l.x1, l.y1, l.x2, l.y2, static_cast<Uint8>(PaletteColor::YELLOW));
}
// AutoSurfaces (Conveyor Belts)
for (auto l : collision_map->getConveyorBeltFloors()) {
surface->drawLine(l.x1, l.y, l.x2, l.y, static_cast<Uint8>(PaletteColor::WHITE));
}
}
// Redibuja el tilemap (para actualizar modo debug)
void TilemapRenderer::redrawMap(const CollisionMap* collision_map) {
fillMapTexture(collision_map);
}
// Cambia un tile y repinta solo esa celda en la map_surface
void TilemapRenderer::setTile(int index, int tile_value) {
if (index < 0 || index >= static_cast<int>(tile_map_.size())) { return; }
tile_map_[index] = tile_value;
int col = index % MAP_WIDTH;
int row = index / MAP_WIDTH;
auto previous_renderer = Screen::get()->getRendererSurface();
Screen::get()->setRendererSurface(map_surface_);
// Borrar la celda con el color de fondo
SDL_FRect cell = {.x = static_cast<float>(col * TILE_SIZE), .y = static_cast<float>(row * TILE_SIZE), .w = static_cast<float>(TILE_SIZE), .h = static_cast<float>(TILE_SIZE)};
map_surface_->fillRect(&cell, stringToColor(bg_color_));
// Dibujar el nuevo tile (si no es vacío ni animado)
if (tile_value > -1) {
const bool IS_ANIMATED = (tile_value >= 18 * tile_set_width_) && (tile_value < 19 * tile_set_width_);
if (!IS_ANIMATED) {
SDL_FRect clip = {.x = static_cast<float>((tile_value % tile_set_width_) * TILE_SIZE),
.y = static_cast<float>((tile_value / tile_set_width_) * TILE_SIZE),
.w = static_cast<float>(TILE_SIZE),
.h = static_cast<float>(TILE_SIZE)};
tileset_surface_->render(col * TILE_SIZE, row * TILE_SIZE, &clip);
}
}
Screen::get()->setRendererSurface(previous_renderer);
}
#endif
// Pinta el mapa estático y debug lines
void TilemapRenderer::fillMapTexture(const CollisionMap* collision_map) { // NOLINT(readability-convert-member-functions-to-static)
const Uint8 COLOR = stringToColor(bg_color_);
auto previous_renderer = Screen::get()->getRendererSurface();
Screen::get()->setRendererSurface(map_surface_);
map_surface_->clear(COLOR);
// Los tileSetFiles son de 20x20 tiles. El primer tile es el 0. Cuentan hacia la derecha y hacia abajo
SDL_FRect clip = {.x = 0, .y = 0, .w = TILE_SIZE, .h = TILE_SIZE};
for (int y = 0; y < MAP_HEIGHT; ++y) {
for (int x = 0; x < MAP_WIDTH; ++x) {
// Tiled pone los tiles vacios del mapa como cero y empieza a contar de 1 a n.
// Al cargar el mapa en memoria, se resta uno, por tanto los tiles vacios son -1
// Tampoco hay que dibujar los tiles animados que estan en la fila 19 (indices)
const int INDEX = (y * MAP_WIDTH) + x;
const bool A = (tile_map_[INDEX] >= 18 * tile_set_width_) && (tile_map_[INDEX] < 19 * tile_set_width_);
const bool B = tile_map_[INDEX] > -1;
if (B && !A) {
clip.x = (tile_map_[INDEX] % tile_set_width_) * TILE_SIZE;
clip.y = (tile_map_[INDEX] / tile_set_width_) * TILE_SIZE;
#ifdef _DEBUG
if (!Debug::get()->isEnabled()) {
tileset_surface_->render(x * TILE_SIZE, y * TILE_SIZE, &clip);
}
#else
tileset_surface_->render(x * TILE_SIZE, y * TILE_SIZE, &clip);
#endif
}
}
}
#ifdef _DEBUG
// Pinta las superficies en el modo debug
if (Debug::get()->isEnabled()) {
renderDebugCollisionSurfaces(collision_map);
}
#endif // _DEBUG
Screen::get()->setRendererSurface(previous_renderer);
}
// Localiza todos los tiles animados
void TilemapRenderer::setAnimatedTiles(const CollisionMap* collision_map) { // NOLINT(readability-convert-member-functions-to-static)
// Recorre la habitación entera por filas buscando tiles de tipo t_animated
for (int i = 0; i < (int)tile_map_.size(); ++i) {
const auto TILE_TYPE = collision_map->getTile(i);
if (TILE_TYPE == CollisionMap::Tile::ANIMATED) {
// La i es la ubicación
const int X = (i % MAP_WIDTH) * TILE_SIZE;
const int Y = (i / MAP_WIDTH) * TILE_SIZE;
// TileMap[i] es el tile a poner
const int XC = (tile_map_[i] % tile_set_width_) * TILE_SIZE;
const int YC = (tile_map_[i] / tile_set_width_) * TILE_SIZE;
AnimatedTile at;
at.sprite = std::make_shared<Sprite>(tileset_surface_, X, Y, 8, 8);
at.sprite->setClip(XC, YC, 8, 8);
at.x_orig = XC;
animated_tiles_.push_back(at);
}
}
}
// Actualiza tiles animados
void TilemapRenderer::updateAnimatedTiles() { // NOLINT(readability-make-member-function-const)
const int NUM_FRAMES = 4;
// Calcular frame actual basado en tiempo
const int CURRENT_FRAME = static_cast<int>(time_accumulator_ / CONVEYOR_FRAME_DURATION) % NUM_FRAMES;
// Calcular offset basado en dirección
int offset = 0;
if (conveyor_belt_direction_ == -1) {
offset = CURRENT_FRAME * TILE_SIZE;
} else {
offset = (NUM_FRAMES - 1 - CURRENT_FRAME) * TILE_SIZE;
}
for (auto& a : animated_tiles_) {
SDL_FRect rect = a.sprite->getClip();
rect.x = a.x_orig + offset;
a.sprite->setClip(rect);
}
}
// Renderiza tiles animados
void TilemapRenderer::renderAnimatedTiles() {
for (const auto& a : animated_tiles_) {
a.sprite->render();
}
}

View File

@@ -0,0 +1,120 @@
#pragma once
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include "utils/defines.hpp"
class Surface;
class Sprite;
class CollisionMap;
/**
* @brief Renderizador de tilemap de una habitación
*
* Responsabilidades:
* - Renderizar el mapa de tiles estático
* - Gestionar tiles animados (conveyor belts)
* - Actualizar animaciones basadas en tiempo
* - Renderizar debug visualization (en modo DEBUG)
*/
class TilemapRenderer {
public:
/**
* @brief Constructor
* @param tile_map Vector con índices de tiles de la habitación
* @param tile_set_width Ancho del tileset en tiles
* @param tileset_surface Surface con los gráficos del tileset
* @param bg_color Color de fondo de la habitación (como string)
* @param conveyor_belt_direction Dirección de las cintas transportadoras (-1, 0, +1)
*/
TilemapRenderer(std::vector<int> tile_map, int tile_set_width, std::shared_ptr<Surface> tileset_surface, std::string bg_color, int conveyor_belt_direction);
~TilemapRenderer() = default;
// Prohibir copia y movimiento
TilemapRenderer(const TilemapRenderer&) = delete;
auto operator=(const TilemapRenderer&) -> TilemapRenderer& = delete;
TilemapRenderer(TilemapRenderer&&) = delete;
auto operator=(TilemapRenderer&&) -> TilemapRenderer& = delete;
/**
* @brief Inicializa el renderizador
* @param collision_map Mapa de colisiones para determinar tiles animados
*
* Crea la textura del mapa, pinta los tiles estáticos, y localiza tiles animados
*/
void initialize(const CollisionMap* collision_map);
/**
* @brief Actualiza las animaciones de tiles
* @param delta_time Tiempo transcurrido desde el último frame (segundos)
*/
void update(float delta_time);
/**
* @brief Renderiza el mapa completo en pantalla
*
* Dibuja la textura del mapa y los tiles animados
*/
void render();
#ifdef _DEBUG
/**
* @brief Redibuja el tilemap (para actualizar modo debug)
* @param collision_map Mapa de colisiones para dibujar líneas de debug
*
* Llamado cuando se activa/desactiva el modo debug para actualizar la visualización
*/
void redrawMap(const CollisionMap* collision_map);
void setBgColor(const std::string& color) { bg_color_ = color; }
void setTile(int index, int tile_value); // Cambia un tile y repinta esa celda
#endif
/**
* @brief Activa/desactiva modo pausa
* @param paused true para pausar, false para reanudar
*
* Nota: Actualmente no afecta al renderizado, pero mantiene consistencia con Room
*/
void setPaused(bool paused) { is_paused_ = paused; }
// Getter para la surface del mapa (usado por Room para acceso directo si es necesario)
[[nodiscard]] auto getMapSurface() const -> std::shared_ptr<Surface> { return map_surface_; }
private:
// Estructura para tiles animados (conveyor belts)
struct AnimatedTile {
std::shared_ptr<Sprite> sprite{nullptr}; // SurfaceSprite para dibujar el tile
int x_orig{0}; // Posición X del primer tile de la animación en tilesheet
};
// === Constantes ===
static constexpr int TILE_SIZE = Tile::SIZE; // Ancho del tile en pixels
static constexpr int MAP_WIDTH = PlayArea::WIDTH / Tile::SIZE; // Ancho del mapa en tiles
static constexpr int MAP_HEIGHT = PlayArea::HEIGHT / Tile::SIZE; // Alto del mapa en tiles
static constexpr int PLAY_AREA_WIDTH = PlayArea::WIDTH; // Ancho del área de juego en pixels
static constexpr int PLAY_AREA_HEIGHT = PlayArea::HEIGHT; // Alto del área de juego en pixels
static constexpr float CONVEYOR_FRAME_DURATION = 0.05F; // Duración de cada frame (3 frames @ 60fps)
// === Datos de la habitación ===
std::vector<int> tile_map_; // Índices de tiles de la habitación
int tile_set_width_; // Ancho del tileset en tiles
std::shared_ptr<Surface> tileset_surface_; // Gráficos del tileset
std::string bg_color_; // Color de fondo
int conveyor_belt_direction_; // Dirección de conveyor belts
// === Renderizado ===
std::shared_ptr<Surface> map_surface_; // Textura para el mapa de la habitación
std::vector<AnimatedTile> animated_tiles_; // Tiles animados (conveyor belts)
float time_accumulator_{0.0F}; // Acumulador de tiempo para animaciones
bool is_paused_{false}; // Indica si está en modo pausa
// === Métodos privados ===
void fillMapTexture(const CollisionMap* collision_map); // Pinta el mapa estático y debug lines
void setAnimatedTiles(const CollisionMap* collision_map); // Localiza todos los tiles animados
void updateAnimatedTiles(); // Actualiza tiles animados
void renderAnimatedTiles(); // Renderiza tiles animados
};