From 9d0dde38f96de65daf1172ca9a1bca7b8a8aae95 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 13 May 2026 12:52:08 +0200 Subject: [PATCH] feat(lighting): override d'emissors per sala i per spawn --- data/maps/p1/r1.yaml | 38 +++++++++-- .../entities/entity_manager_loadlevel.cpp | 53 ++++++++++++++- source/game/map/map.cpp | 65 ++++++++++++++++++- source/game/map/map.hpp | 35 +++++++++- source/game/scenes/game_scene.cpp | 21 ++++++ 5 files changed, 203 insertions(+), 9 deletions(-) diff --git a/data/maps/p1/r1.yaml b/data/maps/p1/r1.yaml index a636d6e..10e1a00 100644 --- a/data/maps/p1/r1.yaml +++ b/data/maps/p1/r1.yaml @@ -15,11 +15,32 @@ tileset: data/tilesets/pyramid_1.gif tile_size: 16 origin: [-8, 0] -# Il·luminació base de la sala (0..1). 1.0 = sala totalment il·luminada -# (com hui, sense efecte visual); 0.0 = completament negra fora del radi -# d'un emissor. El gradient dels cercles d'emissor viu a -# data/config/lighting.yaml. Omet aquesta clau per deixar la sala neta. -light_level: 0.25 +# Il·luminació de la sala. Bloc opcional; si manca, la sala queda +# totalment il·luminada (1.0) i tots els tipus d'entitat emeten amb els +# valors declarats al seu YAML de tipus (comportament històric). +# +# level → atenuació base de la sala (0..1). 1.0 = res +# d'efecte, 0.0 = negre fora del radi dels emissors. +# El gradient dels emissors viu a +# data/config/lighting.yaml. +# emitters. → override per tipus d'entitat. Tres formes: +# false → apaga el tipus +# true → defaults del tipus +# { radius: N, flicker: B } → camps presents +# sobreescriuen el +# default; els +# absents cauen al +# default del tipus. +# Plural per llegir-se natural ("totes les momies de la sala"). Per a +# casos particulars, un spawn pot dur el seu propi `light:` (mateixes +# formes) que té prioritat sobre l'override de sala. La forma antiga +# (`light_level:` plà a l'arrel) també segueix sent vàlida per +# retrocompatibilitat. +lighting: + level: 0.75 + emitters: + players: false + mummies: false # Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl # pintable. Un índex per jugador perquè a multijugador local el recorregut @@ -160,3 +181,10 @@ entities: frame: 18 - type: mummy position: [10, 13] + # Override per-instància: la sala apaga les momies (emitters.mummies: + # false), però aquesta concreta sí emet, amb un radi més curt que el + # default del seu tipus (24 → 16). Bona prova de la tercera capa de + # resolució: tipus → sala → spawn. + light: + radius: 16 + flicker: true diff --git a/source/game/entities/entity_manager_loadlevel.cpp b/source/game/entities/entity_manager_loadlevel.cpp index 7563671..981bb19 100644 --- a/source/game/entities/entity_manager_loadlevel.cpp +++ b/source/game/entities/entity_manager_loadlevel.cpp @@ -174,6 +174,49 @@ namespace { return out; } + // Combina els defaults del tipus amb l'override de sala (si n'hi ha). + // Forma 1: la sala no diu res del tipus → es retornen els defaults + // del tipus tal qual (cas habitual / retrocompat). + // Forma 2: la sala declara `: false` → radi 0 (entitat sense + // llum encara que el seu YAML en declari). + // Forma 3: la sala declara `: true` o `: { ... }` → els + // camps presents a l'override sobreescriuen el default; els absents + // (`std::nullopt`) cauen al default del tipus. + auto resolveLightWithRoom(const std::string& type, LightTemplate type_default) -> LightTemplate { + const auto* room = Map::getRoomEmitterOverride(type); + if (room == nullptr) { return type_default; } + if (!room->enabled) { return {.radius = 0, .flicker = false}; } + LightTemplate out = type_default; + if (room->radius.has_value()) { out.radius = *room->radius; } + if (room->flicker.has_value()) { out.flicker = *room->flicker; } + return out; + } + + // Aplica l'override per-spawn (si existeix) per damunt del que ja + // viu a l'entitat (defaults del tipus + override de sala). Tres formes + // a l'entry del YAML de nivell: + // `light: false` → apaga aquesta instància + // `light: true` → no canvia res; la línia és + // informativa (la llum ja la donen + // les capes inferiors) + // `light: { radius, flicker }` → els camps presents sobreescriuen; + // els absents respecten el que ja + // duia l'entitat + void applySpawnLightOverride(Entity& entity, const fkyaml::node& spawn) { + if (!spawn.contains("light")) { return; } + const auto& l = spawn["light"]; + if (l.is_boolean()) { + if (!l.get_value()) { + entity.setLightRadius(0); + entity.setLightFlicker(false); + } + return; + } + if (!l.is_mapping()) { return; } + if (l.contains("radius")) { entity.setLightRadius(l["radius"].get_value()); } + if (l.contains("flicker")) { entity.setLightFlicker(l["flicker"].get_value()); } + } + // Bloc `collision: { size: [w, h], base_size: [bw, bh]?, offset: [x, y]? }` // de la plantilla d'entitat. // - Sense `base_size` ni `offset`: la caja queda alineada a la vora @@ -296,7 +339,11 @@ namespace { entity->setCollisionBox(*BOX); } entity->setHeight(readHeight(tmpl)); - const auto LIGHT = readLight(tmpl); + // Defaults del tipus → resolució amb override de sala. El layer + // del spawn (per-instància) viu fora de `buildEntity`, perquè + // aquí encara no tenim el node del spawn — el sobreposem a + // `addSpawn` just després. + const auto LIGHT = resolveLightWithRoom(type, readLight(tmpl)); entity->setLightRadius(LIGHT.radius); entity->setLightFlicker(LIGHT.flicker); anchor_out = readAnchor(tmpl); @@ -461,6 +508,10 @@ auto EntityManager::addSpawn(const fkyaml::node& spawn) -> Entity* { std::string anchor; auto entity = buildEntity(TYPE, sprite, anchor); if (entity == nullptr) { return nullptr; } + // Tercera capa de resolució de llum: override per-instància declarat + // al spawn del YAML de la sala. Capes prèvies (tipus, sala) ja viuen + // dins de l'entitat. Aplica abans del std::move per no perdre l'accés. + applySpawnLightOverride(*entity, spawn); Entity* observer = entity.get(); bool ok = false; diff --git a/source/game/map/map.cpp b/source/game/map/map.cpp index 36b6397..9bdcd65 100644 --- a/source/game/map/map.cpp +++ b/source/game/map/map.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "core/resources/resource_helper.hpp" #include "external/fkyaml_node.hpp" @@ -28,12 +29,65 @@ namespace Map { // (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; + } + + // 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(); + } + 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(); } + } + 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()) { @@ -103,9 +157,16 @@ namespace Map { // 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). + // 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); } diff --git a/source/game/map/map.hpp b/source/game/map/map.hpp index 7804086..3e84868 100644 --- a/source/game/map/map.hpp +++ b/source/game/map/map.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -54,9 +55,41 @@ namespace Map { // (com hui, sense efecte visual); 0.0 = completament negra fora del // radi d'un emissor. La clau `light_level:` al YAML de la sala el // sobreescriu — si manca, queda al default 1.0 i la pasada - // d'il·luminació del frame es salta com a no-op. + // d'il·luminació del frame es salta com a no-op. La forma nova és + // declarar-ho dins del bloc `lighting:` (vegeu més avall); `light_level:` + // al root es manté per compatibilitat enrere. inline float light_level = 1.0F; + // Override d'emissor declarat al bloc opcional `lighting.emitters.` + // de la sala. Tres formes acceptades al YAML: + // : false → desactiva la llum per a tot el tipus + // : true → activa amb els defaults del tipus + // : { radius: N, flicker: B } → activa i sobreescriu camps + // Els camps absents (`std::nullopt`) cauen al default del tipus + // d'entitat (radi/flicker del seu YAML). Els tipus no presents al + // mapping conserven el seu comportament per defecte (com avui). + // + // Conversió de clau YAML → tipus d'entitat: el YAML usa plural + // ("players", "mummies", "torches") perquè es llegeix natural per a + // l'autor de la sala; `loadFrom` normalitza a la clau singular + // ("player", "mummy", "torch") que coincideix amb el camp `type:` de + // cada plantilla d'entitat. Tipus no reconeguts s'accepten "tal qual" + // (singular pass-through) per si en el futur s'hi afegeixen emissors + // nous sense tocar el mapatge. + struct RoomEmitterOverride { + bool enabled{true}; + std::optional radius; + std::optional flicker; + }; + + // Consulta l'override de sala per a un tipus d'entitat (clau singular, + // p.ex. "player" / "mummy" / "torch"). Retorna `nullptr` si la sala no + // declara res per aquest tipus — el caller usa els defaults del tipus + // sense modificació. El punter retornat és vàlid fins a la propera + // crida a `Map::loadFrom` (cada càrrega de nivell substitueix el + // mapping intern). + [[nodiscard]] auto getRoomEmitterOverride(const std::string& type) -> const RoomEmitterOverride*; + // Derivats de la geometria base: el pas entre sarcòfags ha de cabre // exactament el jugador, i el mateix passadís s'usa a l'exterior. inline int corridor_w = 0; diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 137d851..1a93382 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -232,6 +232,26 @@ namespace { return out; } + // Override de sala per a la llum del jugador. Si el YAML de la sala + // declara `lighting.emitters.players`, substitueix els valors carregats + // per `Player::loadFrom`. Extret aquí per no inflar la complexitat + // cognitiva del ctor de GameScene (clang-tidy: cognitive-complexity). + // No s'aplica una "tercera capa" per-instància al jugador perquè no + // neix com a spawn del nivell; el dia d'un ítem "linterna" aquell + // sistema mutarà els mateixos camps en runtime — ortogonal a aquest + // override. + void applyRoomLightOverrideToPlayer() { + const auto* room = Map::getRoomEmitterOverride("player"); + if (room == nullptr) { return; } + if (!room->enabled) { + Player::light_radius = 0; + Player::light_flicker = false; + return; + } + if (room->radius.has_value()) { Player::light_radius = *room->radius; } + if (room->flicker.has_value()) { Player::light_flicker = *room->flicker; } + } + } // namespace auto GameScene::defaultConfig() -> Config { @@ -289,6 +309,7 @@ GameScene::GameScene(Config config) // del YAML de P1 és suficient (els dos personatges comparteixen // geometria — morcus és un repaletat de sam). Player::loadFrom(config_.player_yaml); + applyRoomLightOverrideToPlayer(); // Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia, // etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa