#include "game/map/map.hpp" #include #include #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; // Overrides d'emissor declarats al bloc `lighting.emitters` del YAML // de la sala. Clau = tipus d'entitat en singular (per concordar amb // el camp `type:` de cada plantilla). Buit per defecte: una sala // sense bloc `lighting.emitters` deixa que cada entitat utilitze els // seus propis defaults del seu YAML de tipus. std::unordered_map g_room_emitters; // Plural → singular per a tipus coneguts. El YAML de sala fa servir // el plural perquè es llegeix natural ("mummies: false"); el codi // treballa amb la forma singular que coincideix amb el camp `type:` // del spawn ("type: mummy"). Si la clau no és reconeguda, passa tal // qual (suposa que l'autor ja l'ha escrit en singular). auto normalizeEmitterKey(const std::string& yaml_key) -> std::string { if (yaml_key == "players") { return "player"; } if (yaml_key == "mummies") { return "mummy"; } if (yaml_key == "torches") { return "torch"; } return yaml_key; } // Parser comú d'un color YAML 3-tupla a bytes. Retorna nullopt // si el node no és una seqüència de 3 elements. Usat pels // overrides de sala i, en `entity_manager_loadlevel.cpp`, també // pels defaults de tipus i overrides de spawn (passa pel mateix // helper per coherència). `clamp` defensiu per a YAMLs que // entreguen valors fora de 0..255. auto parseColorTriplet(const fkyaml::node& node) -> std::optional> { if (!node.is_sequence() || node.size() != 3) { return std::nullopt; } auto clamp = [](int v) { return static_cast(std::clamp(v, 0, 255)); }; return std::array{ clamp(node[0].get_value()), clamp(node[1].get_value()), clamp(node[2].get_value()), }; } // Llegeix el bloc opcional `lighting:` del YAML d'una sala i // actualitza `light_level`, `lighting_mode` i el mapping // `g_room_emitters`. Idempotent: cada `Map::loadFrom` reseteja // tot l'estat abans de poblar-lo, així no s'arrosseguen valors // d'una sala anterior. void loadLightingBlock(const fkyaml::node& yaml) { g_room_emitters.clear(); lighting_mode = Lighting::Mode::SUBTRACTIVE; if (!yaml.contains("lighting")) { return; } const auto& lighting = yaml["lighting"]; if (lighting.contains("level")) { light_level = lighting["level"].get_value(); } if (lighting.contains("mode")) { const auto MODE_STR = lighting["mode"].get_value(); if (MODE_STR == "additive") { lighting_mode = Lighting::Mode::ADDITIVE; } else if (MODE_STR == "subtractive") { lighting_mode = Lighting::Mode::SUBTRACTIVE; } else { Log::warn("Map", "lighting.mode '{}' desconegut — s'usa subtractive", MODE_STR); } } if (!lighting.contains("emitters") || !lighting["emitters"].is_mapping()) { return; } const auto& em = lighting["emitters"]; for (auto it = em.begin(); it != em.end(); ++it) { const auto KEY = normalizeEmitterKey(it.key().get_value()); const auto& val = it.value(); RoomEmitterOverride ov; if (val.is_boolean()) { ov.enabled = val.get_value(); } else if (val.is_mapping()) { ov.enabled = true; if (val.contains("radius")) { ov.radius = val["radius"].get_value(); } if (val.contains("flicker")) { ov.flicker = val["flicker"].get_value(); } if (val.contains("color")) { ov.color = parseColorTriplet(val["color"]); } } g_room_emitters[KEY] = ov; } } } // namespace void setEntityCollider(std::function fn) { g_entity_collider = std::move(fn); } auto getRoomEmitterOverride(const std::string& type) -> const RoomEmitterOverride* { const auto IT = g_room_emitters.find(type); return IT == g_room_emitters.end() ? nullptr : &IT->second; } 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. Dues formes acceptades: // - `light_level: ` a l'arrel (forma antiga, encara // suportada per retrocompat). // - `lighting.level: ` dins del bloc nou `lighting:`, // que també hostatja `lighting.emitters` (overrides per tipus). // Si manca tot, queda al default 1.0 (sala totalment il·luminada). // `loadLightingBlock` aplica el `lighting.level` per damunt del // `light_level:` antic, així el nou bloc guanya si conviuen. light_level = yaml.contains("light_level") ? yaml["light_level"].get_value() : Defaults::Lighting::ROOM_LIGHT_DEFAULT; loadLightingBlock(yaml); 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