#include "game/gameplay/room.hpp" #include // Para std::ranges::any_of #include // Para exception #include // Para basic_ostream, operator<<, basic_istream #include // Para cout, cerr #include // Para basic_stringstream #include #include "core/audio/audio.hpp" // Para Audio #include "core/rendering/screen.hpp" // Para Screen #include "core/rendering/surface.hpp" // Para Surface #include "core/rendering/surface_sprite.hpp" // Para SSprite #include "core/resources/resource_cache.hpp" // Para Resource #include "core/resources/resource_helper.hpp" // Para ResourceHelper #include "core/system/debug.hpp" // Para Debug #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/scoreboard.hpp" // Para Scoreboard::Data #include "game/options.hpp" // Para Options, OptionsStats, options #include "utils/defines.hpp" // Para BLOCK, PLAY_AREA_HEIGHT, PLAY_AREA_WIDTH #include "utils/utils.hpp" // Para LineHorizontal, LineDiagonal, LineVertical // Constructor Room::Room(const std::string& room_path, std::shared_ptr data) : data_(std::move(std::move(data))) { auto room = Resource::Cache::get()->getRoom(room_path); // Crea los managers de enemigos e items enemy_manager_ = std::make_unique(); item_manager_ = std::make_unique(room->name, data_); initializeRoom(*room); // Crea el mapa de colisiones (necesita tile_map_, tile_set_width_, conveyor_belt_direction_) collision_map_ = std::make_unique(tile_map_, tile_set_width_, conveyor_belt_direction_); openTheJail(); // Abre la Jail si se da el caso setAnimatedTiles(); // Busca los tiles animados map_surface_ = std::make_shared(PLAY_AREA_WIDTH, PLAY_AREA_HEIGHT); // Crea la textura para el mapa de tiles de la habitación fillMapTexture(); // Pinta el mapa de la habitación en la textura 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; tile_map_file_ = room.tile_map_file; conveyor_belt_direction_ = room.conveyor_belt_direction; tile_map_ = Resource::Cache::get()->getTileMap(room.tile_map_file); surface_ = Resource::Cache::get()->getSurface(room.tile_set_file); tile_set_width_ = surface_->getWidth() / TILE_SIZE; is_paused_ = false; time_accumulator_ = 0.0F; // Crear los enemigos usando el manager for (const auto& enemy_data : room.enemies) { enemy_manager_->addEnemy(std::make_shared(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_copy)); } } } // Abre la jail para poder entrar void Room::openTheJail() { if (data_->jail_is_open && name_ == "THE JAIL") { // 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; } } } // Crea la textura con el mapeado de la habitación void Room::fillMapTexture() { const Uint8 COLOR = stringToColor(bg_color_); auto previuos_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 = {0, 0, TILE_SIZE, 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; surface_->render(x * TILE_SIZE, y * TILE_SIZE, &clip); } } } #ifdef _DEBUG if (Debug::get()->getEnabled()) { auto surface = Screen::get()->getRendererSurface(); // BottomSurfaces { for (auto l : collision_map_->getBottomFloors()) { surface->drawLine(l.x1, l.y, l.x2, l.y, static_cast(PaletteColor::BLUE)); } } // TopSurfaces { for (auto l : collision_map_->getTopFloors()) { surface->drawLine(l.x1, l.y, l.x2, l.y, static_cast(PaletteColor::RED)); } } // LeftSurfaces { for (auto l : collision_map_->getLeftWalls()) { surface->drawLine(l.x, l.y1, l.x, l.y2, static_cast(PaletteColor::GREEN)); } } // RightSurfaces { for (auto l : collision_map_->getRightWalls()) { surface->drawLine(l.x, l.y1, l.x, l.y2, static_cast(PaletteColor::MAGENTA)); } } // LeftSlopes { for (auto l : collision_map_->getLeftSlopes()) { surface->drawLine(l.x1, l.y1, l.x2, l.y2, static_cast(PaletteColor::CYAN)); } } // RightSlopes { for (auto l : collision_map_->getRightSlopes()) { surface->drawLine(l.x1, l.y1, l.x2, l.y2, static_cast(PaletteColor::YELLOW)); } } // AutoSurfaces { for (auto l : collision_map_->getConveyorBeltFloors()) { surface->drawLine(l.x1, l.y, l.x2, l.y, static_cast(PaletteColor::WHITE)); } } } #endif // _DEBUG Screen::get()->setRendererSurface(previuos_renderer); } // Dibuja el mapa en pantalla void Room::renderMap() { // Dibuja la textura con el mapa en pantalla SDL_FRect dest = {0, 0, PLAY_AREA_WIDTH, PLAY_AREA_HEIGHT}; map_surface_->render(nullptr, &dest); // Dibuja los tiles animados #ifdef _DEBUG if (!Debug::get()->getEnabled()) { renderAnimatedTiles(); } #else renderAnimatedTiles(); #endif } // Dibuja los enemigos en pantalla void Room::renderEnemies() { enemy_manager_->render(); } // Dibuja los objetos en pantalla void Room::renderItems() { item_manager_->render(); } // Actualiza las variables y objetos de la habitación void Room::update(float delta_time) { if (is_paused_) { // Si está en modo pausa no se actualiza nada return; } // Actualiza el acumulador de tiempo time_accumulator_ += delta_time; // Actualiza los tiles animados updateAnimatedTiles(); // 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; item_manager_->setPaused(value); } // Localiza todos los tiles animados de la habitación void Room::setAnimatedTiles() { // Recorre la habitación entera por filas buscando tiles de tipo t_animated for (int i = 0; i < (int)tile_map_.size(); ++i) { if (getTile(i) == 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(surface_, X, Y, 8, 8); at.sprite->setClip(XC, YC, 8, 8); at.x_orig = XC; animated_tiles_.push_back(at); } } } // Actualiza los tiles animados void Room::updateAnimatedTiles() { const int NUM_FRAMES = 4; // Calcular frame actual basado en tiempo const int CURRENT_FRAME = static_cast(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); } } // Pinta los tiles animados en pantalla void Room::renderAnimatedTiles() { for (const auto& a : animated_tiles_) { a.sprite->render(); } } // Devuelve la cadena del fichero de la habitación contigua segun el borde auto Room::getRoom(Border border) -> std::string { switch (border) { case Border::TOP: return upper_room_; break; case Border::BOTTOM: return lower_room_; break; case Border::RIGHT: return right_room_; break; case Border::LEFT: return left_room_; break; default: break; } return ""; } // Devuelve el tipo de tile que hay en ese pixel auto Room::getTile(SDL_FPoint point) -> Tile { // Delega a CollisionMap y convierte el resultado const auto collision_tile = collision_map_->getTile(point); return static_cast(collision_tile); } // Devuelve el tipo de tile en un índice del tilemap auto Room::getTile(int index) -> Tile { // Delega a CollisionMap y convierte el resultado const auto collision_tile = collision_map_->getTile(index); return static_cast(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(slope); return CollisionMap::getSlopeHeight(p, collision_tile); } // === Métodos de colisión (delegados a CollisionMap) === // Comprueba las colisiones con paredes derechas auto Room::checkRightSurfaces(SDL_FRect& rect) -> int { return collision_map_->checkRightSurfaces(rect); } // Comprueba las colisiones con paredes izquierdas auto Room::checkLeftSurfaces(SDL_FRect& rect) -> int { return collision_map_->checkLeftSurfaces(rect); } // Comprueba las colisiones con techos auto Room::checkTopSurfaces(SDL_FRect& rect) -> int { return collision_map_->checkTopSurfaces(rect); } // Comprueba las colisiones punto con techos auto Room::checkTopSurfaces(SDL_FPoint& p) -> bool { return collision_map_->checkTopSurfaces(p); } // Comprueba las colisiones con suelos auto Room::checkBottomSurfaces(SDL_FRect& rect) -> int { return collision_map_->checkBottomSurfaces(rect); } // Comprueba las colisiones con conveyor belts auto Room::checkAutoSurfaces(SDL_FRect& rect) -> int { return collision_map_->checkAutoSurfaces(rect); } // Comprueba las colisiones punto con conveyor belts auto Room::checkConveyorBelts(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(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(SDL_FPoint& p) -> bool { return collision_map_->checkRightSlopes(p); } // Asigna variables a una estructura RoomData auto Room::setRoom(Data& room, const std::string& key, const std::string& value) -> bool { // Indicador de éxito en la asignación bool success = true; try { if (key == "tileMapFile") { room.tile_map_file = value; } else if (key == "name") { room.name = value; } else if (key == "bgColor") { room.bg_color = value; } else if (key == "border") { room.border_color = value; } else if (key == "itemColor1") { room.item_color1 = value; } else if (key == "itemColor2") { room.item_color2 = value; } else if (key == "tileSetFile") { room.tile_set_file = value; } else if (key == "roomUp") { room.upper_room = value; } else if (key == "roomDown") { room.lower_room = value; } else if (key == "roomLeft") { room.left_room = value; } else if (key == "roomRight") { room.right_room = value; } else if (key == "autoSurface") { room.conveyor_belt_direction = (value == "right") ? 1 : -1; } else if (key.empty() || key.substr(0, 1) == "#") { // No se realiza ninguna acción para estas claves } else { success = false; } } catch (const std::exception& e) { std::cerr << "Error al asignar la clave " << key << " con valor " << value << ": " << e.what() << '\n'; success = false; } return success; } // Asigna variables a una estructura EnemyData auto Room::setEnemy(Enemy::Data& enemy, const std::string& key, const std::string& value) -> bool { // Indicador de éxito en la asignación bool success = true; try { if (key == "animation") { enemy.animation_path = value; } else if (key == "x") { enemy.x = std::stof(value) * TILE_SIZE; } else if (key == "y") { enemy.y = std::stof(value) * TILE_SIZE; } else if (key == "vx") { enemy.vx = std::stof(value); } else if (key == "vy") { enemy.vy = std::stof(value); } else if (key == "x1") { enemy.x1 = std::stoi(value) * TILE_SIZE; } else if (key == "x2") { enemy.x2 = std::stoi(value) * TILE_SIZE; } else if (key == "y1") { enemy.y1 = std::stoi(value) * TILE_SIZE; } else if (key == "y2") { enemy.y2 = std::stoi(value) * TILE_SIZE; } else if (key == "flip") { enemy.flip = stringToBool(value); } else if (key == "mirror") { enemy.mirror = stringToBool(value); } else if (key == "color") { enemy.color = value; } else if (key == "frame") { enemy.frame = std::stoi(value); } else if (key == "[/enemy]" || key == "tileSetFile" || key.substr(0, 1) == "#") { // No se realiza ninguna acción para estas claves } else { success = false; } } catch (const std::exception& e) { std::cerr << "Error al asignar la clave " << key << " con valor " << value << ": " << e.what() << '\n'; success = false; } return success; } // Asigna variables a una estructura ItemData auto Room::setItem(Item::Data& item, const std::string& key, const std::string& value) -> bool { // Indicador de éxito en la asignación bool success = true; try { if (key == "tileSetFile") { item.tile_set_file = value; } else if (key == "counter") { item.counter = std::stoi(value); } else if (key == "x") { item.x = std::stof(value) * TILE_SIZE; } else if (key == "y") { item.y = std::stof(value) * TILE_SIZE; } else if (key == "tile") { item.tile = std::stof(value); } else if (key == "[/item]") { // No se realiza ninguna acción para esta clave } else { success = false; } } catch (const std::exception& e) { std::cerr << "Error al asignar la clave " << key << " con valor " << value << ": " << e.what() << '\n'; success = false; } return success; } // Carga las variables y texturas desde un fichero de mapa de tiles auto Room::loadRoomTileFile(const std::string& file_path, bool verbose) -> std::vector { std::vector tile_map_file; const std::string FILENAME = file_path.substr(file_path.find_last_of("\\/") + 1); // Load file using ResourceHelper (supports both filesystem and pack) auto file_data = Resource::Helper::loadFile(file_path); if (!file_data.empty()) { // Convert bytes to string and parse std::string content(file_data.begin(), file_data.end()); std::istringstream stream(content); std::string line; // Procesa el fichero linea a linea while (std::getline(stream, line)) { // Lee el fichero linea a linea if (line.find("data encoding") != std::string::npos) { // Lee la primera linea std::getline(stream, line); // Trim line to handle Windows line endings line.erase(0, line.find_first_not_of(" \t\r\n")); line.erase(line.find_last_not_of(" \t\r\n") + 1); while (line != "") { // Procesa lineas mientras haya std::stringstream ss(line); std::string tmp; while (getline(ss, tmp, ',')) { // Trim whitespace (including \r, \n, spaces, tabs) tmp.erase(0, tmp.find_first_not_of(" \t\r\n")); tmp.erase(tmp.find_last_not_of(" \t\r\n") + 1); // Skip empty strings (from trailing commas) if (!tmp.empty()) { tile_map_file.push_back(std::stoi(tmp) - 1); } } // Lee la siguiente linea std::getline(stream, line); // Trim line to handle Windows line endings line.erase(0, line.find_first_not_of(" \t\r\n")); line.erase(line.find_last_not_of(" \t\r\n") + 1); } } } if (verbose) { std::cout << "TileMap loaded: " << FILENAME.c_str() << '\n'; } } else { // El fichero no se puede abrir if (verbose) { std::cout << "Warning: Unable to open " << FILENAME.c_str() << " file" << '\n'; } } return tile_map_file; } // Carga las variables desde un fichero de mapa auto Room::loadRoomFile(const std::string& file_path, bool verbose) -> Data { Data room; room.item_color1 = "yellow"; room.item_color2 = "magenta"; room.conveyor_belt_direction = 1; const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1); room.number = FILE_NAME.substr(0, FILE_NAME.find_last_of('.')); // Load file using ResourceHelper (supports both filesystem and pack) auto file_data = Resource::Helper::loadFile(file_path); if (!file_data.empty()) { // Convert bytes to string and parse std::string content(file_data.begin(), file_data.end()); std::istringstream stream(content); std::string line; // Procesa el fichero linea a linea while (std::getline(stream, line)) { // Remove Windows line ending if present if (!line.empty() && line.back() == '\r') { line.pop_back(); } // Si la linea contiene el texto [enemy] se realiza el proceso de carga de un enemigo if (line == "[enemy]") { room.enemies.push_back(loadEnemyFromFile(stream, FILE_NAME, verbose)); } // Si la linea contiene el texto [item] se realiza el proceso de carga de un item else if (line == "[item]") { room.items.push_back(loadItemFromFile(stream, FILE_NAME, verbose)); } // En caso contrario se parsea el fichero para buscar las variables y los valores else { auto [key, value] = parseKeyValue(line); if (!setRoom(room, key, value)) { logUnknownParameter(FILE_NAME, key, verbose); } } } if (verbose) { std::cout << "Room loaded: " << FILE_NAME.c_str() << '\n'; } } else { // El fichero no se puede abrir std::cout << "Warning: Unable to open " << FILE_NAME.c_str() << " file" << '\n'; } return room; } // Parsea una línea en key y value separados por '=' auto Room::parseKeyValue(const std::string& line) -> std::pair { int pos = line.find('='); std::string key = line.substr(0, pos); std::string value = line.substr(pos + 1, line.length()); return {key, value}; } // Muestra un warning de parámetro desconocido void Room::logUnknownParameter(const std::string& file_name, const std::string& key, bool verbose) { if (verbose) { std::cout << "Warning: file " << file_name.c_str() << "\n, unknown parameter \"" << key.c_str() << "\"" << '\n'; } } // Carga un bloque [enemy]...[/enemy] desde un archivo auto Room::loadEnemyFromFile(std::istream& file, const std::string& file_name, bool verbose) -> Enemy::Data { Enemy::Data enemy; enemy.flip = false; enemy.mirror = false; enemy.frame = -1; std::string line; do { std::getline(file, line); // Remove Windows line ending if present if (!line.empty() && line.back() == '\r') { line.pop_back(); } auto [key, value] = parseKeyValue(line); if (!setEnemy(enemy, key, value)) { logUnknownParameter(file_name, key, verbose); } } while (line != "[/enemy]"); return enemy; } // Carga un bloque [item]...[/item] desde un archivo auto Room::loadItemFromFile(std::istream& file, const std::string& file_name, bool verbose) -> Item::Data { Item::Data item; item.counter = 0; item.color1 = stringToColor("yellow"); item.color2 = stringToColor("magenta"); std::string line; do { std::getline(file, line); // Remove Windows line ending if present if (!line.empty() && line.back() == '\r') { line.pop_back(); } auto [key, value] = parseKeyValue(line); if (!setItem(item, key, value)) { logUnknownParameter(file_name, key, verbose); } } while (line != "[/item]"); return item; }