// Implementació d'`EntityManager::loadFromLevel`. Viu en un TU separat // perquè aquesta és l'única via per la qual el manager toca // `Resource::Cache`, `Surface` i les seues dependències (GIF, àudio). Els // consumidors que només volen la base del manager (p.ex. el smoke test // stand-alone) enllacen `entity_manager.cpp` sense arrossegar aquest fitxer. #include #include #include #include #include #include "core/rendering/surface.hpp" #include "core/resources/resource_cache.hpp" #include "external/fkyaml_node.hpp" #include "game/entities/concrete/door.hpp" #include "game/entities/concrete/sarcophagus.hpp" #include "game/entities/entity.hpp" #include "game/entities/entity_factory.hpp" #include "game/entities/entity_loader.hpp" #include "game/entities/entity_manager.hpp" #include "game/map/map.hpp" // Render centralitzat al manager per confinar la dependència cap a // `Surface`. Les entitats exposen `getSurface()` + `getPixelRect()` i el // manager dispatcha el blit. Si una entitat no té surface (trigger // invisible, encara sense sprite) simplement es salta. // // Convenció dels GIFs d'actors: els frames s'estenen en una graella // left-to-right, top-to-bottom, amb mida de frame igual a (pixel_rect.w, // pixel_rect.h). El nombre de columnes es dedueix dividint l'amplada del // surface per l'amplada del frame — així un GIF de 4 frames pot venir com // a 1×4, 4×1 o 2×2 indistintament. `renderAll` clipa la cel·la del frame // actual amb un `src_rect`. Entitats sense animació es dibuixen al frame 0. void EntityManager::renderAll() { for (const auto& e : entities_) { const auto& surface = e->getSurface(); if (surface == nullptr) { continue; } const auto RECT = e->getPixelRect(); const auto& anim = e->getAnimation(); const int FRAME = anim.has_value() ? anim->currentFrame() : 0; const int FRAME_W = static_cast(RECT.w); const int FRAME_H = static_cast(RECT.h); const int COLS = (FRAME_W > 0) ? (surface->getWidth() / FRAME_W) : 1; const int COLS_SAFE = (COLS > 0) ? COLS : 1; const int COL = FRAME % COLS_SAFE; const int ROW = FRAME / COLS_SAFE; const SDL_FRect SRC{ .x = static_cast(COL * FRAME_W), .y = static_cast(ROW * FRAME_H), .w = RECT.w, .h = RECT.h, }; surface->render(static_cast(RECT.x), static_cast(RECT.y), &SRC); } } namespace { // Llegeix `sprite.size` o `sprite.path` d'una plantilla i retorna // (surface_key, pixel_size). Si falta algun camp, torna valors // raonables (empty key → no carrega; size [16,16] per defecte). struct SpriteInfo { std::string key; // clau al Resource::Cache (basename del GIF) std::string full_path; // ruta absoluta per a loadSurface float width{16.0F}; float height{16.0F}; bool valid{false}; }; auto readSpriteInfo(const fkyaml::node& tmpl, const std::string& type) -> SpriteInfo { SpriteInfo info; if (!tmpl.contains("sprite")) { return info; } const auto& s = tmpl["sprite"]; if (s.contains("path")) { const auto REL = s["path"].get_value(); info.key = std::filesystem::path(REL).filename().string(); info.full_path = "data/entities/" + type + "/" + REL; info.valid = true; } if (s.contains("size")) { const auto& sz = s["size"]; if (sz.is_sequence() && sz.size() == 2) { info.width = static_cast(sz[0].get_value()); info.height = static_cast(sz[1].get_value()); } } return info; } // Coordenades píxel (canvas) d'una cel·la (col, row) del tilemap. El // grid del tilemap està 16-alineat respecte a `tilemap_origin_*`, que // pot ser negatiu per overscan CPC. auto tileToPixel(int col, int row) -> SDL_FPoint { return { .x = static_cast((col * Map::tile_size) + Map::tilemap_origin_x), .y = static_cast((row * Map::tile_size) + Map::tilemap_origin_y), }; } // Converteix un rect píxel (canvas) a la seua cantonada superior-esquerra // en coordenades de tile del tilemap. Només per a sarcòfags, que naixen // ancorats a una cel·la exacta de la graella. struct TileAnchor { int col; int row; }; auto pixelToTileAnchor(const SDL_FRect& rect) -> TileAnchor { return { .col = (static_cast(rect.x) - Map::tilemap_origin_x) / Map::tile_size, .row = (static_cast(rect.y) - Map::tilemap_origin_y) / Map::tile_size, }; } // Crea l'entitat per factory i li adjunta la surface si n'hi ha. Retorna // nullptr si la plantilla no es pot carregar o si el tipus no està // registrat al factory — el loop principal ja imprimeix el missatge. auto buildEntity(const std::string& type, SpriteInfo& sprite_out) -> std::unique_ptr { fkyaml::node tmpl; try { tmpl = EntityLoader::loadTemplate(type); } catch (const std::exception& e) { std::cerr << "EntityManager: " << e.what() << '\n'; return nullptr; } auto entity = EntityFactory::instance().create(type, tmpl); if (entity == nullptr) { std::cerr << "EntityManager: tipus desconegut '" << type << "'\n"; return nullptr; } sprite_out = readSpriteInfo(tmpl, type); if (sprite_out.valid) { Resource::Cache::get().loadSurface(sprite_out.key, sprite_out.full_path); entity->setSurface(Resource::Cache::get().getSurface(sprite_out.key)); } return entity; } } // namespace // Col·loca i afegeix un Sarcophagus a partir del spawn. `entity` es consumeix // en èxit (ownership transferit al manager); en error es destrueix aquí. // Membre privat amagat dins del TU: `EntityManager::` qualifica, però com // que no hi ha declaració al header, només és visible a aquest fitxer. namespace { auto placeSarcophagus(EntityManager& manager, std::unique_ptr entity, const SpriteInfo& sprite, const fkyaml::node& spawn) -> bool { if (!spawn.contains("sarc_position")) { std::cerr << "EntityManager: sarcophagus sense sarc_position\n"; return false; } const auto& sp = spawn["sarc_position"]; const auto LOGICAL = Map::getSarcRect(sp[0].get_value(), sp[1].get_value()); const auto ANCHOR = pixelToTileAnchor(LOGICAL); // Convenció de falsa perspectiva (igual que el jugador): el rect de // render és bottom-aligned amb la caixa lògica de col·lisió. Si el // sprite fa sprite.height > LOGICAL.h (p.ex. 48×32 sobre caixa 48×16), // els píxels sobresortits queden per damunt sense afectar la // col·lisió ni el perímetre — només decoren en alçada. Si sprite.h // coincideix amb LOGICAL.h, el render coincideix exactament amb la // caixa. const SDL_FRect RENDER{ .x = LOGICAL.x, .y = LOGICAL.y + LOGICAL.h - sprite.height, .w = sprite.width, .h = sprite.height, }; entity->setPixelRect(RENDER); // Downcast segur: la factory ens garanteix que "sarcophagus" crea // un `Sarcophagus`. Usant el retorn de `release` al ctor del nou // `unique_ptr` evitem el lint `bugprone-unused-return-value`. if (dynamic_cast(entity.get()) == nullptr) { std::cerr << "EntityManager: la factory no ha creat un Sarcophagus\n"; return false; } std::unique_ptr sarc(static_cast(entity.release())); sarc->place(ANCHOR.col, ANCHOR.row, {}); manager.addSarcophagus(std::move(sarc)); return true; } auto placeGeneric(EntityManager& manager, std::unique_ptr entity, const std::string& type, const SpriteInfo& sprite, const fkyaml::node& spawn) -> bool { if (!spawn.contains("position")) { std::cerr << "EntityManager: " << type << " sense position\n"; return false; } const auto& p = spawn["position"]; const auto PX = tileToPixel(p[0].get_value(), p[1].get_value()); entity->setPixelRect({.x = PX.x, .y = PX.y, .w = sprite.width, .h = sprite.height}); if (type == "door") { auto* door_ptr = dynamic_cast(entity.get()); manager.add(std::move(entity)); if (door_ptr != nullptr) { manager.setDoor(door_ptr); } } else { manager.add(std::move(entity)); } return true; } } // namespace void EntityManager::loadFromLevel(const std::string& level_yaml_path) { const auto SPAWNS = EntityLoader::readSpawns(level_yaml_path); int loaded = 0; for (const auto& spawn : SPAWNS) { if (!spawn.contains("type")) { continue; } const auto TYPE = spawn["type"].get_value(); SpriteInfo sprite; auto entity = buildEntity(TYPE, sprite); if (entity == nullptr) { continue; } const bool OK = (TYPE == "sarcophagus") ? placeSarcophagus(*this, std::move(entity), sprite, spawn) : placeGeneric(*this, std::move(entity), TYPE, sprite, spawn); if (OK) { ++loaded; } } std::cout << "EntityManager: " << loaded << " entitats carregades des de " << level_yaml_path << '\n'; }