#include "game/map/map.hpp" #include #include #include "core/resources/resource_helper.hpp" #include "external/fkyaml_node.hpp" #include "utils/defaults.hpp" #include "utils/log.hpp" namespace { // AABB amb inequitat estricta: dos rects que es toquen exactament (la // cantonada o una vora al píxel) NO es consideren intersecció. SDL3 // SDL_HasRectIntersectionFloat tracta el toc com a intersecció, cosa // que bloqueja el moviment als encreuaments del nostre laberint. auto overlapStrict(const SDL_FRect& a, const SDL_FRect& b) -> bool { return (a.x + a.w) > b.x && (b.x + b.w) > a.x && (a.y + a.h) > b.y && (b.y + b.h) > a.y; } } // namespace namespace Map { namespace { // Predicat extra que el `collides` consulta com a sobre-capa, // injectat des de `EntityManager` per a entitats sòlides genèriques // (debris, columnes). El TU local manté l'estat sense exposar-lo // al header. std::function g_entity_collider; } // namespace void setEntityCollider(std::function fn) { g_entity_collider = std::move(fn); } void loadFrom(const std::string& yaml_path) { auto bytes = Resource::Helper::loadFile(yaml_path); if (bytes.empty()) { throw std::runtime_error("Map: no s'ha pogut obrir " + yaml_path); } const auto* begin = reinterpret_cast(bytes.data()); const auto* end = begin + bytes.size(); auto yaml = fkyaml::node::deserialize(begin, end); if (!yaml.contains("geometry")) { throw std::runtime_error("Map: manca bloc `geometry:` a " + yaml_path); } const auto& g = yaml["geometry"]; grid_cols = g["grid_cols"].get_value(); grid_rows = g["grid_rows"].get_value(); sarc_w = g["sarc_w"].get_value(); sarc_h = g["sarc_h"].get_value(); pitch_x = g["pitch_x"].get_value(); pitch_y = g["pitch_y"].get_value(); hud_h = g["hud_h"].get_value(); back_wall_h = g["back_wall_h"].get_value(); side_wall_w = g["side_wall_w"].get_value(); // Tile size + origen del tilemap (per a consumidors externs que // converteixen px ↔ tile). Fallbacks conservadors: tile_size per // defecte 16, origen [0, 0]. Si falten del YAML, només el sistema // d'entitats es veu afectat; la lògica de col·lisions actual no. tile_size = yaml.contains("tile_size") ? yaml["tile_size"].get_value() : 16; tilemap_origin_x = 0; tilemap_origin_y = 0; if (yaml.contains("origin")) { const auto& o = yaml["origin"]; if (o.is_sequence() && o.size() == 2) { tilemap_origin_x = o[0].get_value(); tilemap_origin_y = o[1].get_value(); } } // Derivats: el passadís és el sobrant del pitch sobre el sarcòfag, i // l'exterior reutilitza la mateixa amplada per mantenir el pas lliure. corridor_w = pitch_x - sarc_w; corridor_h = pitch_y - sarc_h; outer_corridor = corridor_w; board_w = ((grid_cols - 1) * pitch_x) + sarc_w; board_h = ((grid_rows - 1) * pitch_y) + sarc_h; origin_x = side_wall_w + outer_corridor; origin_y = hud_h + back_wall_h + outer_corridor; playfield_x = side_wall_w; playfield_y = hud_h + back_wall_h; playfield_w = board_w + (2 * outer_corridor); playfield_h = board_h + (2 * outer_corridor); // Porta i passadís de sortida. `door_col` és la columna central // transitable; el passadís són exactament els 16 px d'eixa columna // dins del strip del mur trasser. Si manca la clau, `door_col=0` // i el passadís queda a x=0 amb mida zero — pràcticament inactiu // (el bloqueig del clamp continua sent el rectangle del playfield). door_col = g.contains("door_col") ? g["door_col"].get_value() : 0; door_corridor_x = (door_col * tile_size) + tilemap_origin_x; door_corridor_y = hud_h; door_corridor_w = tile_size; door_corridor_h = back_wall_h; // Cada càrrega de nivell deixa la porta tancada; l'entitat Door // canviarà el flag a true quan transiti a OPEN. door_open = false; // Llum base de la sala. La clau és opcional a l'arrel del YAML; // per defecte 1.0 (sala totalment il·luminada, comportament hui). light_level = yaml.contains("light_level") ? yaml["light_level"].get_value() : Defaults::Lighting::ROOM_LIGHT_DEFAULT; Log::info("Map", "geometria {}x{} sarcs {}x{} pitch {}x{} door_col {} des de {}", grid_cols, grid_rows, sarc_w, sarc_h, pitch_x, pitch_y, door_col, yaml_path); } auto getSarcRect(int col, int row) -> SDL_FRect { return { .x = static_cast(origin_x + (col * pitch_x)), .y = static_cast(origin_y + (row * pitch_y)), .w = static_cast(sarc_w), .h = static_cast(sarc_h), }; } auto collides(const SDL_FRect& base_box) -> bool { // Recorregut trivial dels sarcòfags. Amb una graella petita no val // la pena indexar per col/row prèviament; si algun dia creix, es pot // calcular el rang de columnes/files candidates a partir de base_box. for (int row = 0; row < grid_rows; ++row) { for (int col = 0; col < grid_cols; ++col) { if (overlapStrict(base_box, getSarcRect(col, row))) { return true; } } } // Porta tancada: el passadís de sortida actua com a paret. Quan la // porta passa a OPEN, l'entitat Door posa `door_open = true` i // aquesta clàusula deixa de bloquejar — el jugador pot creuar. if (!door_open && door_corridor_w > 0 && door_corridor_h > 0) { const SDL_FRect CORRIDOR{ .x = static_cast(door_corridor_x), .y = static_cast(door_corridor_y), .w = static_cast(door_corridor_w), .h = static_cast(door_corridor_h), }; if (overlapStrict(base_box, CORRIDOR)) { return true; } } // Capa final: entitats sòlides genèriques registrades pel manager. // S'avalua al final perquè els obstacles fixos (sarcs, porta) són // l'origen del laberint i el camí més curt; les entitats addicionals // són normalment poques. if (g_entity_collider && g_entity_collider(base_box)) { return true; } return false; } auto insidePlayfield(const SDL_FRect& box) -> bool { return box.x >= static_cast(playfield_x) && box.y >= static_cast(playfield_y) && box.x + box.w <= static_cast(playfield_x + playfield_w) && box.y + box.h <= static_cast(playfield_y + playfield_h); } auto isPaintable(int col, int row) -> bool { // Construïm la caixa píxel del tile a partir de l'origen del tilemap. // El grid és 16-alineat respecte a `tilemap_origin_*`; per a un tile // (col,row) la cantonada superior-esquerra cau a: // x = tilemap_origin_x + col * tile_size // y = tilemap_origin_y + row * tile_size const SDL_FRect BOX{ .x = static_cast(tilemap_origin_x + (col * tile_size)), .y = static_cast(tilemap_origin_y + (row * tile_size)), .w = static_cast(tile_size), .h = static_cast(tile_size), }; if (!insidePlayfield(BOX)) { return false; } if (collides(BOX)) { return false; } return true; } auto pixelToTile(float px, float py) -> std::pair { const auto TS = static_cast(tile_size); const int COL = static_cast(std::floor((px - static_cast(tilemap_origin_x)) / TS)); const int ROW = static_cast(std::floor((py - static_cast(tilemap_origin_y)) / TS)); return {COL, ROW}; } auto makeInfo() -> Info { return { .playfield_x = playfield_x, .playfield_y = playfield_y, .playfield_w = playfield_w, .playfield_h = playfield_h, .pitch_x = pitch_x, .pitch_y = pitch_y, .grid_cols = grid_cols, .grid_rows = grid_rows, .tile_size = tile_size, .tilemap_origin_x = tilemap_origin_x, .tilemap_origin_y = tilemap_origin_y, .door_corridor_x = door_corridor_x, .door_corridor_y = door_corridor_y, .door_corridor_w = door_corridor_w, .door_corridor_h = door_corridor_h, .collides = &collides, .inside_playfield = &insidePlayfield, }; } } // namespace Map