263 lines
12 KiB
C++
263 lines
12 KiB
C++
#include "game/map/map.hpp"
|
|
|
|
#include <cmath>
|
|
#include <stdexcept>
|
|
#include <unordered_map>
|
|
|
|
#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<bool(const SDL_FRect&)> 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<std::string, RoomEmitterOverride> 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;
|
|
}
|
|
|
|
// Llegeix el bloc opcional `lighting:` del YAML d'una sala i
|
|
// actualitza `light_level` (si declara `level`) i el mapping
|
|
// `g_room_emitters`. Idempotent: cada `Map::loadFrom` neteja el
|
|
// mapping abans de poblar-lo, així no s'arrosseguen overrides
|
|
// d'una sala anterior.
|
|
void loadLightingBlock(const fkyaml::node& yaml) {
|
|
g_room_emitters.clear();
|
|
if (!yaml.contains("lighting")) { return; }
|
|
const auto& lighting = yaml["lighting"];
|
|
if (lighting.contains("level")) {
|
|
light_level = lighting["level"].get_value<float>();
|
|
}
|
|
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<std::string>());
|
|
const auto& val = it.value();
|
|
RoomEmitterOverride ov;
|
|
if (val.is_boolean()) {
|
|
ov.enabled = val.get_value<bool>();
|
|
} else if (val.is_mapping()) {
|
|
ov.enabled = true;
|
|
if (val.contains("radius")) { ov.radius = val["radius"].get_value<int>(); }
|
|
if (val.contains("flicker")) { ov.flicker = val["flicker"].get_value<bool>(); }
|
|
}
|
|
g_room_emitters[KEY] = ov;
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
void setEntityCollider(std::function<bool(const SDL_FRect&)> 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<const char*>(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<int>();
|
|
grid_rows = g["grid_rows"].get_value<int>();
|
|
sarc_w = g["sarc_w"].get_value<int>();
|
|
sarc_h = g["sarc_h"].get_value<int>();
|
|
pitch_x = g["pitch_x"].get_value<int>();
|
|
pitch_y = g["pitch_y"].get_value<int>();
|
|
hud_h = g["hud_h"].get_value<int>();
|
|
back_wall_h = g["back_wall_h"].get_value<int>();
|
|
side_wall_w = g["side_wall_w"].get_value<int>();
|
|
|
|
// 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<int>() : 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<int>();
|
|
tilemap_origin_y = o[1].get_value<int>();
|
|
}
|
|
}
|
|
|
|
// 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<int>() : 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: <float>` a l'arrel (forma antiga, encara
|
|
// suportada per retrocompat).
|
|
// - `lighting.level: <float>` 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<float>() : 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<float>(origin_x + (col * pitch_x)),
|
|
.y = static_cast<float>(origin_y + (row * pitch_y)),
|
|
.w = static_cast<float>(sarc_w),
|
|
.h = static_cast<float>(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<float>(door_corridor_x),
|
|
.y = static_cast<float>(door_corridor_y),
|
|
.w = static_cast<float>(door_corridor_w),
|
|
.h = static_cast<float>(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<float>(playfield_x) && box.y >= static_cast<float>(playfield_y) && box.x + box.w <= static_cast<float>(playfield_x + playfield_w) && box.y + box.h <= static_cast<float>(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<float>(tilemap_origin_x + (col * tile_size)),
|
|
.y = static_cast<float>(tilemap_origin_y + (row * tile_size)),
|
|
.w = static_cast<float>(tile_size),
|
|
.h = static_cast<float>(tile_size),
|
|
};
|
|
if (!insidePlayfield(BOX)) { return false; }
|
|
if (collides(BOX)) { return false; }
|
|
return true;
|
|
}
|
|
|
|
auto pixelToTile(float px, float py) -> std::pair<int, int> {
|
|
const auto TS = static_cast<float>(tile_size);
|
|
const int COL = static_cast<int>(std::floor((px - static_cast<float>(tilemap_origin_x)) / TS));
|
|
const int ROW = static_cast<int>(std::floor((py - static_cast<float>(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
|