feat(lighting): override d'emissors per sala i per spawn
This commit is contained in:
1 parent
b76a3eb1f3
commit
9d0dde38f9
5 files changed
+203
-9
No files matched your search
+33
-5
@@ -15,11 +15,32 @@ tileset: data/tilesets/pyramid_1.gif
|
|||||||
tile_size: 16
|
tile_size: 16
|
||||||
origin: [-8, 0]
|
origin: [-8, 0]
|
||||||
|
|
||||||
# Il·luminació base de la sala (0..1). 1.0 = sala totalment il·luminada
|
# Il·luminació de la sala. Bloc opcional; si manca, la sala queda
|
||||||
# (com hui, sense efecte visual); 0.0 = completament negra fora del radi
|
# totalment il·luminada (1.0) i tots els tipus d'entitat emeten amb els
|
||||||
# d'un emissor. El gradient dels cercles d'emissor viu a
|
# valors declarats al seu YAML de tipus (comportament històric).
|
||||||
# data/config/lighting.yaml. Omet aquesta clau per deixar la sala neta.
|
#
|
||||||
light_level: 0.25
|
# 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.<tipus> → 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
|
# Í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
|
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||||
@@ -160,3 +181,10 @@ entities:
|
|||||||
frame: 18
|
frame: 18
|
||||||
- type: mummy
|
- type: mummy
|
||||||
position: [10, 13]
|
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
|
||||||
@@ -174,6 +174,49 @@ namespace {
|
|||||||
return out;
|
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 `<tipus>: false` → radi 0 (entitat sense
|
||||||
|
// llum encara que el seu YAML en declari).
|
||||||
|
// Forma 3: la sala declara `<tipus>: true` o `<tipus>: { ... }` → 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<bool>()) {
|
||||||
|
entity.setLightRadius(0);
|
||||||
|
entity.setLightFlicker(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!l.is_mapping()) { return; }
|
||||||
|
if (l.contains("radius")) { entity.setLightRadius(l["radius"].get_value<int>()); }
|
||||||
|
if (l.contains("flicker")) { entity.setLightFlicker(l["flicker"].get_value<bool>()); }
|
||||||
|
}
|
||||||
|
|
||||||
// Bloc `collision: { size: [w, h], base_size: [bw, bh]?, offset: [x, y]? }`
|
// Bloc `collision: { size: [w, h], base_size: [bw, bh]?, offset: [x, y]? }`
|
||||||
// de la plantilla d'entitat.
|
// de la plantilla d'entitat.
|
||||||
// - Sense `base_size` ni `offset`: la caja queda alineada a la vora
|
// - Sense `base_size` ni `offset`: la caja queda alineada a la vora
|
||||||
@@ -296,7 +339,11 @@ namespace {
|
|||||||
entity->setCollisionBox(*BOX);
|
entity->setCollisionBox(*BOX);
|
||||||
}
|
}
|
||||||
entity->setHeight(readHeight(tmpl));
|
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->setLightRadius(LIGHT.radius);
|
||||||
entity->setLightFlicker(LIGHT.flicker);
|
entity->setLightFlicker(LIGHT.flicker);
|
||||||
anchor_out = readAnchor(tmpl);
|
anchor_out = readAnchor(tmpl);
|
||||||
@@ -461,6 +508,10 @@ auto EntityManager::addSpawn(const fkyaml::node& spawn) -> Entity* {
|
|||||||
std::string anchor;
|
std::string anchor;
|
||||||
auto entity = buildEntity(TYPE, sprite, anchor);
|
auto entity = buildEntity(TYPE, sprite, anchor);
|
||||||
if (entity == nullptr) { return nullptr; }
|
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();
|
Entity* observer = entity.get();
|
||||||
|
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
|
|||||||
+63
-2
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
@@ -28,12 +29,65 @@ namespace Map {
|
|||||||
// (debris, columnes). El TU local manté l'estat sense exposar-lo
|
// (debris, columnes). El TU local manté l'estat sense exposar-lo
|
||||||
// al header.
|
// al header.
|
||||||
std::function<bool(const SDL_FRect&)> g_entity_collider;
|
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
|
} // namespace
|
||||||
|
|
||||||
void setEntityCollider(std::function<bool(const SDL_FRect&)> fn) {
|
void setEntityCollider(std::function<bool(const SDL_FRect&)> fn) {
|
||||||
g_entity_collider = std::move(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) {
|
void loadFrom(const std::string& yaml_path) {
|
||||||
auto bytes = Resource::Helper::loadFile(yaml_path);
|
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||||
if (bytes.empty()) {
|
if (bytes.empty()) {
|
||||||
@@ -103,9 +157,16 @@ namespace Map {
|
|||||||
// canviarà el flag a true quan transiti a OPEN.
|
// canviarà el flag a true quan transiti a OPEN.
|
||||||
door_open = false;
|
door_open = false;
|
||||||
|
|
||||||
// Llum base de la sala. La clau és opcional a l'arrel del YAML;
|
// Llum base de la sala. Dues formes acceptades:
|
||||||
// per defecte 1.0 (sala totalment il·luminada, comportament hui).
|
// - `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;
|
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);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-1
@@ -3,6 +3,7 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
@@ -54,9 +55,41 @@ namespace Map {
|
|||||||
// (com hui, sense efecte visual); 0.0 = completament negra fora del
|
// (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
|
// 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
|
// 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;
|
inline float light_level = 1.0F;
|
||||||
|
|
||||||
|
// Override d'emissor declarat al bloc opcional `lighting.emitters.<tipus>`
|
||||||
|
// de la sala. Tres formes acceptades al YAML:
|
||||||
|
// <tipus>: false → desactiva la llum per a tot el tipus
|
||||||
|
// <tipus>: true → activa amb els defaults del tipus
|
||||||
|
// <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<int> radius;
|
||||||
|
std::optional<bool> 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
|
// 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.
|
// exactament el jugador, i el mateix passadís s'usa a l'exterior.
|
||||||
inline int corridor_w = 0;
|
inline int corridor_w = 0;
|
||||||
|
|||||||
@@ -232,6 +232,26 @@ namespace {
|
|||||||
return out;
|
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
|
} // namespace
|
||||||
|
|
||||||
auto GameScene::defaultConfig() -> Config {
|
auto GameScene::defaultConfig() -> Config {
|
||||||
@@ -289,6 +309,7 @@ GameScene::GameScene(Config config)
|
|||||||
// del YAML de P1 és suficient (els dos personatges comparteixen
|
// del YAML de P1 és suficient (els dos personatges comparteixen
|
||||||
// geometria — morcus és un repaletat de sam).
|
// geometria — morcus és un repaletat de sam).
|
||||||
Player::loadFrom(config_.player_yaml);
|
Player::loadFrom(config_.player_yaml);
|
||||||
|
applyRoomLightOverrideToPlayer();
|
||||||
|
|
||||||
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
|
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
|
||||||
// etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa
|
// etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa
|
||||||
|
|||||||
Reference in new issue
Block a user