diff --git a/data/config/lighting.yaml b/data/config/lighting.yaml index 77f0193..2c1802b 100644 --- a/data/config/lighting.yaml +++ b/data/config/lighting.yaml @@ -39,14 +39,18 @@ gradient_additive: # Modulació del radi quan una entitat declara `flicker: true` al seu # bloc `light:`. El radi efectiu per frame és: -# r_eff = r_base + A1·sin(2π·f1·t + φ1) + A2·sin(2π·f2·t + φ2) -# on (φ1, φ2) es deriven de la posició de l'emissor per a que cada +# r_eff = r_base + (r_amp1·r_base)·sin(2π·f1·t + φ1) + (r_amp2·r_base)·sin(2π·f2·t + φ2) +# on (φ1, φ2) es deriven de la seed de l'emissor per a que cada # antorxa balancegi en una fase diferent (sense estat per emissor). -# Combinant dues sinusoides amb freqüències incommensurables s'obté -# un patró "fluid" que el ull llegeix com a foc; pujar `noise_*` -# aporta més inquietud, baixar-ho fa la flama més serena. +# Les amplituds són **ràtios** sobre el radi base: així el feel del +# flicker és independent del radi (una torxa de 24 px oscil·la el +# mateix % que una de 40 px). Combinant dues sinusoides amb freqüències +# incommensurables s'obté un patró "fluid" que el ull llegeix com a +# foc; pujar `noise_*` aporta més inquietud, baixar-ho fa la flama +# més serena. Aquests són els defaults globals; cada tipus d'entitat +# (o cada spawn / sala) pot sobreescriure'ls al seu `light.flicker_params:`. flicker: - wave_amplitude_px: 2 + wave_amplitude_ratio: 0.05 # 5% del radi (per a radi 40 → ±2 px, feel actual) wave_frequency_hz: 3.0 - noise_amplitude_px: 1 + noise_amplitude_ratio: 0.025 # 2.5% (per a radi 40 → ±1 px, feel actual) noise_frequency_hz: 7.0 diff --git a/data/maps/p1/r1.yaml b/data/maps/p1/r1.yaml index 10e1a00..675b992 100644 --- a/data/maps/p1/r1.yaml +++ b/data/maps/p1/r1.yaml @@ -10,35 +10,35 @@ # la cel·la buida (transparent), útil per caselles que ocuparà un actor. pyramid: 1 -room: 1 +room: 2 tileset: data/tilesets/pyramid_1.gif tile_size: 16 origin: [-8, 0] -# 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. +# Banc de proves del mode `additive` d'il·luminació. La sala es veu +# "normal" (level 1.0, el GIF intacte fora del cercle de les torxes) +# i només les antorxes hi sumen un halo càlid suau. Player i mummies +# desactivats explícitament perquè aquí no volem cap halo al voltant +# d'actors — el toc cromàtic és només l'antorxa. La corba additiva +# (intensitats baixes a propòsit perquè additive satura ràpid) viu a +# `gradient_additive:` de data/config/lighting.yaml. lighting: - level: 0.75 + mode: additive + level: 1.0 emitters: + torches: + radius: 20 + color: [255, 170, 60] + # Valors default del flicker explicitats per anar-los ajustant + # in-situ. Coincideixen amb els del global a data/config/lighting.yaml; + # baixar wave_frequency_hz fa el "palpitar" més lent, baixar les + # ratios fa l'oscil·lació més subtil. Qualsevol camp es pot + # eliminar i caurà al valor del global. + flicker_params: + wave_amplitude_ratio: 0.04 + wave_frequency_hz: 3.0 + noise_amplitude_ratio: 0.025 + noise_frequency_hz: 3.0 players: false mummies: false @@ -109,12 +109,16 @@ data: # d'obertura (262/263). La momia al corredor central. L'antorxa al costat # superior esquerre per verificar render d'una entitat decorativa. entities: + - type: torch + position: [4, 2] - type: torch position: [8, 2] - type: torch position: [12, 2] - type: torch position: [16, 2] + - type: sarcophagus + sarc_position: [0, 0] - type: sarcophagus sarc_position: [1, 0] - type: sarcophagus @@ -141,6 +145,8 @@ entities: sarc_position: [0, 3] - type: sarcophagus sarc_position: [1, 3] + - type: sarcophagus + sarc_position: [2, 3] - type: sarcophagus sarc_position: [3, 3] - type: sarcophagus @@ -151,40 +157,8 @@ entities: sarc_position: [2, 4] - type: sarcophagus sarc_position: [3, 4] - - type: torch - position: [4, 2] - type: door - - type: column - position: [3, 4] - frame: 2 - - type: stone - position: [5, 4] - frame: 16 - - type: stone - position: [4, 4] - frame: 19 - - type: column - position: [13, 10] - frame: 4 - - type: stone - position: [12, 10] - frame: 13 - - type: stone - position: [11, 10] - frame: 23 - - type: hole - position: [5, 9] - - type: hole - position: [9, 11] - - type: stone - position: [10, 10] - 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 + position: [10, 7] + - type: mummy + position: [7, 9] diff --git a/data/maps/p1/r2.yaml b/data/maps/p1/r2.yaml index ada2223..b1e55af 100644 --- a/data/maps/p1/r2.yaml +++ b/data/maps/p1/r2.yaml @@ -10,24 +10,36 @@ # la cel·la buida (transparent), útil per caselles que ocuparà un actor. pyramid: 1 -room: 2 +room: 1 tileset: data/tilesets/pyramid_1.gif tile_size: 16 origin: [-8, 0] -# Banc de proves del mode `additive` d'il·luminació. La sala es veu -# "normal" (level 1.0, el GIF intacte fora del cercle de les torxes) -# i només les antorxes hi sumen un halo càlid suau. Player i mummies -# desactivats explícitament perquè aquí no volem cap halo al voltant -# d'actors — el toc cromàtic és només l'antorxa. La corba additiva -# (intensitats baixes a propòsit perquè additive satura ràpid) viu a -# `gradient_additive:` de data/config/lighting.yaml. +# 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: - mode: additive - level: 1.0 + level: 0.15 emitters: - torches: { radius: 24, color: [255, 170, 60] } - players: false + players: true mummies: false # Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl @@ -97,16 +109,12 @@ data: # d'obertura (262/263). La momia al corredor central. L'antorxa al costat # superior esquerre per verificar render d'una entitat decorativa. entities: - - type: torch - position: [4, 2] - type: torch position: [8, 2] - type: torch position: [12, 2] - type: torch position: [16, 2] - - type: sarcophagus - sarc_position: [0, 0] - type: sarcophagus sarc_position: [1, 0] - type: sarcophagus @@ -133,8 +141,6 @@ entities: sarc_position: [0, 3] - type: sarcophagus sarc_position: [1, 3] - - type: sarcophagus - sarc_position: [2, 3] - type: sarcophagus sarc_position: [3, 3] - type: sarcophagus @@ -145,8 +151,33 @@ entities: sarc_position: [2, 4] - type: sarcophagus sarc_position: [3, 4] + - type: torch + position: [4, 2] - type: door + - type: column + position: [3, 4] + frame: 2 + - type: stone + position: [5, 4] + frame: 16 + - type: stone + position: [4, 4] + frame: 19 + - type: column + position: [13, 10] + frame: 4 + - type: stone + position: [12, 10] + frame: 13 + - type: stone + position: [11, 10] + frame: 23 + - type: hole + position: [5, 9] + - type: hole + position: [9, 11] + - type: stone + position: [10, 10] + frame: 18 - type: mummy - position: [10, 7] - - type: mummy - position: [7, 9] + position: [10, 13] diff --git a/source/core/rendering/lighting.cpp b/source/core/rendering/lighting.cpp index 1d2fa6c..708bf58 100644 --- a/source/core/rendering/lighting.cpp +++ b/source/core/rendering/lighting.cpp @@ -57,9 +57,9 @@ namespace Lighting { auto activeFlicker() -> FlickerConfig& { static FlickerConfig fc_{ - .wave_amplitude_px = Defaults::Lighting::FLICKER_WAVE_AMPLITUDE_PX, + .wave_amplitude_ratio = Defaults::Lighting::FLICKER_WAVE_AMPLITUDE_RATIO, .wave_frequency_hz = Defaults::Lighting::FLICKER_WAVE_FREQUENCY_HZ, - .noise_amplitude_px = Defaults::Lighting::FLICKER_NOISE_AMPLITUDE_PX, + .noise_amplitude_ratio = Defaults::Lighting::FLICKER_NOISE_AMPLITUDE_RATIO, .noise_frequency_hz = Defaults::Lighting::FLICKER_NOISE_FREQUENCY_HZ, }; return fc_; @@ -70,13 +70,16 @@ namespace Lighting { // identificador estable de l'emissor (no canvia amb la posició) — // així el patró no "deriva" quan l'emissor es mou. El factor // ~φ (golden ratio) dispersa les fases per a seeds consecutives. + // Les amplituds són ràtios sobre el radi base, multiplicades aquí + // → mateix percentatge d'oscil·lació per a qualsevol mida. auto computeFlickerRadius(int base_radius, int seed, float t_s, const FlickerConfig& fc) -> int { constexpr float TWO_PI = 2.0F * std::numbers::pi_v; constexpr float GOLDEN_FRACTION = 0.6180339F; const float PHASE = static_cast(seed) * GOLDEN_FRACTION; const float W1 = std::sin((TWO_PI * fc.wave_frequency_hz * t_s) + PHASE); const float W2 = std::sin((TWO_PI * fc.noise_frequency_hz * t_s) + (PHASE * 2.7F)); - const float DELTA = (fc.wave_amplitude_px * W1) + (fc.noise_amplitude_px * W2); + const auto R_F = static_cast(base_radius); + const float DELTA = (fc.wave_amplitude_ratio * R_F * W1) + (fc.noise_amplitude_ratio * R_F * W2); const int R = base_radius + static_cast(std::lround(DELTA)); return std::max(1, R); } @@ -251,6 +254,10 @@ namespace Lighting { return activeSubtractiveConfig(); } + auto flickerConfig() -> const FlickerConfig& { + return activeFlicker(); + } + namespace { // Llegeix els 5 camps de `GradientConfig` d'un node YAML — qualsevol // camp absent conserva el valor compilat dels defaults. Compartit @@ -283,9 +290,9 @@ namespace Lighting { if (yaml.contains("flicker")) { const auto& f = yaml["flicker"]; auto& fc = activeFlicker(); - readField(f, "wave_amplitude_px", fc.wave_amplitude_px); + readField(f, "wave_amplitude_ratio", fc.wave_amplitude_ratio); readField(f, "wave_frequency_hz", fc.wave_frequency_hz); - readField(f, "noise_amplitude_px", fc.noise_amplitude_px); + readField(f, "noise_amplitude_ratio", fc.noise_amplitude_ratio); readField(f, "noise_frequency_hz", fc.noise_frequency_hz); } } catch (const std::exception& ex) { @@ -297,9 +304,17 @@ namespace Lighting { // Helper: emissor "patxat" amb el radi efectiu (post-flicker) per // a aquest frame. Mantenim l'`Emitter` del caller intacte i la - // resta de camps (color, seed, ...) propagats. + // resta de camps (color, seed, flicker_params, ...) propagats. auto withEffectiveRadius(const Emitter& e, int r_eff) -> Emitter { - return Emitter{.cx = e.cx, .cy = e.cy, .radius = r_eff, .seed = e.seed, .flicker = e.flicker, .color = e.color}; + return Emitter{ + .cx = e.cx, + .cy = e.cy, + .radius = r_eff, + .seed = e.seed, + .flicker = e.flicker, + .color = e.color, + .flicker_params = e.flicker_params, + }; } // Camí del mode subtractive (històric): omple un buffer de @@ -312,11 +327,10 @@ namespace Lighting { std::ranges::fill(lum, room); const auto& cfg = activeSubtractiveConfig(); - const auto& fc = activeFlicker(); const float T_S = static_cast(SDL_GetTicks()) / 1000.0F; for (const auto& e : emitters) { if (e.radius <= 0) { continue; } - const int R_EFF = e.flicker ? computeFlickerRadius(e.radius, e.seed, T_S, fc) : e.radius; + const int R_EFF = e.flicker ? computeFlickerRadius(e.radius, e.seed, T_S, e.flicker_params) : e.radius; const auto RINGS = computeRings(R_EFF, cfg); const Emitter EFF = withEffectiveRadius(e, R_EFF); rasterizeEmitter(EFF, RINGS, lum.data(), width, height); @@ -330,11 +344,10 @@ namespace Lighting { void applyAdditive(std::uint32_t* canvas, int width, int height, std::uint8_t room, std::span emitters) { darkenCanvas(canvas, width, height, room); const auto& cfg = activeAdditiveConfig(); - const auto& fc = activeFlicker(); const float T_S = static_cast(SDL_GetTicks()) / 1000.0F; for (const auto& e : emitters) { if (e.radius <= 0) { continue; } - const int R_EFF = e.flicker ? computeFlickerRadius(e.radius, e.seed, T_S, fc) : e.radius; + const int R_EFF = e.flicker ? computeFlickerRadius(e.radius, e.seed, T_S, e.flicker_params) : e.radius; const auto RINGS = computeRings(R_EFF, cfg); const Emitter EFF = withEffectiveRadius(e, R_EFF); rasterizeEmitterAdditive(EFF, RINGS, canvas, width, height); diff --git a/source/core/rendering/lighting.hpp b/source/core/rendering/lighting.hpp index c1e16aa..e63187f 100644 --- a/source/core/rendering/lighting.hpp +++ b/source/core/rendering/lighting.hpp @@ -67,6 +67,32 @@ namespace Lighting { std::uint8_t b{255}; }; + // Forma del gradient. Compartida per tots els emissors per evitar + // duplicar la corba a cada YAML d'entitat. Càrrega via loadConfig. + struct GradientConfig { + float inner_ratio{0.8F}; + float mid_ratio{0.9F}; + float inner_intensity{1.0F}; + float mid_intensity{0.9F}; + float outer_intensity{0.8F}; + }; + + // Paràmetres del flicker per a emissors amb `flicker=true`. La + // forma final del radi és la suma d'una sinusoide "respiració" + // (wave_*) i una sinusoide secundària de freqüència més alta + // (noise_*) que aporta la sensació d'inquietud. Les fases es + // deriven de la seed estable de cada emissor — sense estat per + // emissor. Les amplituds són **ràtios** sobre el radi base (p.ex. + // 0.05 = 5% del radi): així una torxa de radi 40 i una de radi 24 + // tenen el mateix percentatge d'oscil·lació visual, no els mateixos + // píxels (que es notarien més en la petita). + struct FlickerConfig { + float wave_amplitude_ratio{0.05F}; + float wave_frequency_hz{3.0F}; + float noise_amplitude_ratio{0.025F}; + float noise_frequency_hz{7.0F}; + }; + // Posició al canvas (en píxels, mateixa unitat que la resta del // motor) i radi efectiu de l'emissor. El centre és el del sprite // visible: és l'entitat qui sap on cau (veure `Entity::getLightCenter`). @@ -82,28 +108,12 @@ namespace Lighting { int seed{0}; bool flicker{false}; Color color{}; - }; - - // Forma del gradient. Compartida per tots els emissors per evitar - // duplicar la corba a cada YAML d'entitat. Càrrega via loadConfig. - struct GradientConfig { - float inner_ratio{0.8F}; - float mid_ratio{0.9F}; - float inner_intensity{1.0F}; - float mid_intensity{0.9F}; - float outer_intensity{0.8F}; - }; - - // Paràmetres del flicker per a emissors amb `flicker=true`. La - // forma final del radi és la suma d'una sinusoide "respiració" - // (wave_*) i una sinusoide secundària de freqüència més alta - // (noise_*) que aporta la sensació d'inquietud. Les fases es - // deriven de la posició de cada emissor — sense estat per emissor. - struct FlickerConfig { - float wave_amplitude_px{2.0F}; - float wave_frequency_hz{3.0F}; - float noise_amplitude_px{1.0F}; - float noise_frequency_hz{7.0F}; + // Paràmetres del flicker resolts per a aquest emissor (combinació + // global + override del tipus/sala/spawn). Només té sentit si + // `flicker == true`; en cas contrari s'ignoren. Per defecte + // s'inicialitza amb els defaults compilats — el caller (game_scene) + // hi posa el resolt correcte. + FlickerConfig flicker_params{}; }; // Llegeix `yaml_path` (data/config/lighting.yaml) i pobla la @@ -119,6 +129,12 @@ namespace Lighting { // Defaults::Lighting fins que loadConfig() es crida. [[nodiscard]] auto config() -> const GradientConfig&; + // Defaults globals del flicker (data/config/lighting.yaml, bloc + // `flicker:`). Els callers del construct path d'entitats el llegeixen + // per inicialitzar la `FlickerConfig` de cada emissor, i aplicar a + // sobre els overrides per tipus/sala/spawn. + [[nodiscard]] auto flickerConfig() -> const FlickerConfig&; + // Aplica la pasada d'il·luminació al canvas in-place. `mode` // selecciona la composició (vegeu el comentari de la capçalera). // `room_light` és l'atenuació ambient de la sala (0..1, 1 = sense diff --git a/source/game/entities/entity.hpp b/source/game/entities/entity.hpp index d3e7ff8..b58a437 100644 --- a/source/game/entities/entity.hpp +++ b/source/game/entities/entity.hpp @@ -7,6 +7,7 @@ #include #include +#include "core/rendering/lighting.hpp" #include "game/entities/animation_component.hpp" // Forward-declaracions: mantenim aquest header lleuger. `Player` viu a @@ -296,6 +297,15 @@ class Entity { light_offset_y_ = y; } + // Paràmetres del flicker resolts per a aquest emissor (combinació + // del global de `data/config/lighting.yaml` + overrides del tipus, + // sala i spawn). Només té efecte si `light_flicker_` és true. El + // construct path li passa una còpia ja resolta — el raster (lighting.cpp) + // només la llegeix. Es publica per camps perquè el bloc YAML + // `flicker_params:` permet sobreescriure qualsevol subconjunt. + [[nodiscard]] auto getFlickerParams() const -> const Lighting::FlickerConfig& { return flicker_params_; } + void setFlickerParams(const Lighting::FlickerConfig& fp) { flicker_params_ = fp; } + // Identificador estable per al flicker. Cada Entity rep un valor // únic a la construcció (contador estàtic) i el conserva fins a la // mort — així el patró del flicker no "deriva" quan l'entitat es @@ -339,6 +349,7 @@ class Entity { std::uint8_t light_color_b_{255}; int light_offset_x_{0}; int light_offset_y_{0}; + Lighting::FlickerConfig flicker_params_{}; bool dead_{false}; SDL_FlipMode flip_{SDL_FLIP_NONE}; EventBus* bus_{nullptr}; diff --git a/source/game/entities/entity_manager_loadlevel.cpp b/source/game/entities/entity_manager_loadlevel.cpp index 1d341b4..2d73116 100644 --- a/source/game/entities/entity_manager_loadlevel.cpp +++ b/source/game/entities/entity_manager_loadlevel.cpp @@ -174,6 +174,9 @@ namespace { std::uint8_t color_b{255}; int offset_x{0}; int offset_y{0}; + // Inicialitzat al global a `readLight`; overrides parcials del + // tipus/sala/spawn modifiquen camps concrets sense tocar la resta. + Lighting::FlickerConfig flicker_params{}; }; // Helper compartit entre les tres capes (tipus, sala via Map, spawn) @@ -196,14 +199,28 @@ namespace { return true; } + // Aplica els camps presents al bloc `flicker_params: { ... }` sobre + // una `FlickerConfig` ja inicialitzada (típicament amb el global o + // amb el resultat d'una capa anterior). Camps absents es respecten + // — per a poder sobreescriure'n només un. + void applyFlickerOverrides(const fkyaml::node& fp, Lighting::FlickerConfig& out) { + if (fp.contains("wave_amplitude_ratio")) { out.wave_amplitude_ratio = fp["wave_amplitude_ratio"].get_value(); } + if (fp.contains("wave_frequency_hz")) { out.wave_frequency_hz = fp["wave_frequency_hz"].get_value(); } + if (fp.contains("noise_amplitude_ratio")) { out.noise_amplitude_ratio = fp["noise_amplitude_ratio"].get_value(); } + if (fp.contains("noise_frequency_hz")) { out.noise_frequency_hz = fp["noise_frequency_hz"].get_value(); } + } + auto readLight(const fkyaml::node& tmpl) -> LightTemplate { - if (!tmpl.contains("light")) { return {}; } - const auto& l = tmpl["light"]; LightTemplate out; + // Punt de partida del flicker: el global de lighting.yaml. + out.flicker_params = Lighting::flickerConfig(); + if (!tmpl.contains("light")) { return out; } + const auto& l = tmpl["light"]; if (l.contains("radius")) { out.radius = l["radius"].get_value(); } if (l.contains("flicker")) { out.flicker = l["flicker"].get_value(); } if (l.contains("color")) { readColorTriplet(l["color"], out.color_r, out.color_g, out.color_b); } if (l.contains("offset")) { readOffsetPair(l["offset"], out.offset_x, out.offset_y); } + if (l.contains("flicker_params")) { applyFlickerOverrides(l["flicker_params"], out.flicker_params); } return out; } @@ -215,10 +232,14 @@ namespace { // 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 { + auto resolveLightWithRoom(const std::string& type, const LightTemplate& type_default) -> LightTemplate { const auto* room = Map::getRoomEmitterOverride(type); if (room == nullptr) { return type_default; } - if (!room->enabled) { return LightTemplate{}; } + if (!room->enabled) { + LightTemplate empty; + empty.flicker_params = Lighting::flickerConfig(); + return empty; + } LightTemplate out = type_default; if (room->radius.has_value()) { out.radius = *room->radius; } if (room->flicker.has_value()) { out.flicker = *room->flicker; } @@ -231,6 +252,10 @@ namespace { out.offset_x = (*room->offset)[0]; out.offset_y = (*room->offset)[1]; } + if (room->flicker_wave_amplitude_ratio.has_value()) { out.flicker_params.wave_amplitude_ratio = *room->flicker_wave_amplitude_ratio; } + if (room->flicker_wave_frequency_hz.has_value()) { out.flicker_params.wave_frequency_hz = *room->flicker_wave_frequency_hz; } + if (room->flicker_noise_amplitude_ratio.has_value()) { out.flicker_params.noise_amplitude_ratio = *room->flicker_noise_amplitude_ratio; } + if (room->flicker_noise_frequency_hz.has_value()) { out.flicker_params.noise_frequency_hz = *room->flicker_noise_frequency_hz; } return out; } @@ -266,6 +291,11 @@ namespace { int y{}; if (readOffsetPair(l["offset"], x, y)) { entity.setLightOffset(x, y); } } + if (l.contains("flicker_params")) { + Lighting::FlickerConfig fp = entity.getFlickerParams(); + applyFlickerOverrides(l["flicker_params"], fp); + entity.setFlickerParams(fp); + } } // Bloc `collision: { size: [w, h], base_size: [bw, bh]?, offset: [x, y]? }` @@ -399,6 +429,7 @@ namespace { entity->setLightFlicker(LIGHT.flicker); entity->setLightColor(LIGHT.color_r, LIGHT.color_g, LIGHT.color_b); entity->setLightOffset(LIGHT.offset_x, LIGHT.offset_y); + entity->setFlickerParams(LIGHT.flicker_params); anchor_out = readAnchor(tmpl); return entity; } diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 83548a4..c743632 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -155,6 +155,9 @@ void Player::readLightBlock(const fkyaml::node& yaml) { light_color_b = 255; light_offset_x = 0; light_offset_y = 0; + // Punt de partida del flicker: el global de lighting.yaml. El bloc + // opcional `flicker_params:` del personatge sobreescriu camps concrets. + flicker_params = Lighting::flickerConfig(); if (!yaml.contains("light")) { return; } const auto& l = yaml["light"]; if (l.contains("radius")) { light_radius = l["radius"].get_value(); } @@ -178,6 +181,15 @@ void Player::readLightBlock(const fkyaml::node& yaml) { light_offset_y = o[1].get_value(); } } + // `flicker_params: { ... }` opcional. Qualsevol dels 4 camps absent + // conserva el valor heretat del global (vegeu inicialització a sobre). + if (l.contains("flicker_params")) { + const auto& fp = l["flicker_params"]; + if (fp.contains("wave_amplitude_ratio")) { flicker_params.wave_amplitude_ratio = fp["wave_amplitude_ratio"].get_value(); } + if (fp.contains("wave_frequency_hz")) { flicker_params.wave_frequency_hz = fp["wave_frequency_hz"].get_value(); } + if (fp.contains("noise_amplitude_ratio")) { flicker_params.noise_amplitude_ratio = fp["noise_amplitude_ratio"].get_value(); } + if (fp.contains("noise_frequency_hz")) { flicker_params.noise_frequency_hz = fp["noise_frequency_hz"].get_value(); } + } } void Player::readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path) { diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index 72a310c..149b59c 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -7,6 +7,7 @@ #include #include +#include "core/rendering/lighting.hpp" #include "core/rendering/sprite/animated_sprite.hpp" #include "core/rendering/surface.hpp" #include "external/fkyaml_node.hpp" @@ -140,6 +141,10 @@ class Player : public AnimatedSprite { inline static std::uint8_t light_color_b = 255; inline static int light_offset_x = 0; inline static int light_offset_y = 0; + // Paràmetres del flicker resolts per al jugador (combinació del + // global + override del YAML del personatge + override de sala). + // Només té efecte si `light_flicker` és true. + inline static Lighting::FlickerConfig flicker_params{}; // Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor. static void loadFrom(const std::string& yaml_path); @@ -160,6 +165,7 @@ class Player : public AnimatedSprite { [[nodiscard]] static auto getLightColorB() -> std::uint8_t { return light_color_b; } [[nodiscard]] static auto getLightOffsetX() -> int { return light_offset_x; } [[nodiscard]] static auto getLightOffsetY() -> int { return light_offset_y; } + [[nodiscard]] static auto getFlickerParams() -> const Lighting::FlickerConfig& { return flicker_params; } // Seed estable per al flicker: reusem `id_` (0 o 1 en multijugador // local) amb un offset gran per evitar col·lisions amb el contador // d'Entity (que arrenca des de 1). Així P1 i P2 oscil·len en fases diff --git a/source/game/map/map.cpp b/source/game/map/map.cpp index cedef2b..e4986c4 100644 --- a/source/game/map/map.cpp +++ b/source/game/map/map.cpp @@ -78,6 +78,52 @@ namespace Map { }; } + // Llegeix el sub-bloc opcional `flicker_params:` d'un emissor de + // sala. Camps individuals — qualsevol absent conserva el valor + // del global (`Lighting::flickerConfig()`) o de la capa que ho + // precedeixi (tipus / sala). + void readFlickerOverrides(const fkyaml::node& fp, RoomEmitterOverride& ov) { + if (fp.contains("wave_amplitude_ratio")) { ov.flicker_wave_amplitude_ratio = fp["wave_amplitude_ratio"].get_value(); } + if (fp.contains("wave_frequency_hz")) { ov.flicker_wave_frequency_hz = fp["wave_frequency_hz"].get_value(); } + if (fp.contains("noise_amplitude_ratio")) { ov.flicker_noise_amplitude_ratio = fp["noise_amplitude_ratio"].get_value(); } + if (fp.contains("noise_frequency_hz")) { ov.flicker_noise_frequency_hz = fp["noise_frequency_hz"].get_value(); } + } + + // Llegeix una entrada del mapping `lighting.emitters.` en + // qualsevol de les seves 3 formes (bool curt o mapping detallat). + // Extret per mantenir `loadLightingBlock` sota el llindar de + // complexitat cognitiva de clang-tidy. + auto readEmitterEntry(const fkyaml::node& val) -> RoomEmitterOverride { + RoomEmitterOverride ov; + if (val.is_boolean()) { + ov.enabled = val.get_value(); + return ov; + } + if (!val.is_mapping()) { return ov; } + 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"]); } + if (val.contains("offset")) { ov.offset = parseIntPair(val["offset"]); } + if (val.contains("flicker_params")) { readFlickerOverrides(val["flicker_params"], ov); } + return ov; + } + + // Llegeix la subclau `mode:` del bloc `lighting:` i actualitza + // `lighting_mode`. Avisa via Log si el valor no és reconegut. + // Extret de `loadLightingBlock` per la mateixa raó. + void readLightingMode(const fkyaml::node& lighting) { + if (!lighting.contains("mode")) { return; } + 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); + } + } + // 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 @@ -91,32 +137,12 @@ namespace Map { 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); - } - } + readLightingMode(lighting); 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"]); } - if (val.contains("offset")) { ov.offset = parseIntPair(val["offset"]); } - } - g_room_emitters[KEY] = ov; + g_room_emitters[KEY] = readEmitterEntry(it.value()); } } } // namespace diff --git a/source/game/map/map.hpp b/source/game/map/map.hpp index e1f40d0..f001f55 100644 --- a/source/game/map/map.hpp +++ b/source/game/map/map.hpp @@ -99,6 +99,14 @@ namespace Map { // sprite (píxels enters; Y creix avall). `nullopt` → conservar // l'offset del tipus (default [0, 0]). Forma YAML: `offset: [x, y]`. std::optional> offset; + // Overrides parcials per cada camp del `FlickerConfig` (ràtio + // d'amplitud i freqüència del wave + noise). `nullopt` → no + // toca aquell camp (conserva el del tipus, que al seu torn pot + // ser el global). Forma YAML: `flicker_params: { wave_frequency_hz: 1.0, ... }`. + std::optional flicker_wave_amplitude_ratio; + std::optional flicker_wave_frequency_hz; + std::optional flicker_noise_amplitude_ratio; + std::optional flicker_noise_frequency_hz; }; // Consulta l'override de sala per a un tipus d'entitat (clau singular, diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 2ee7264..bc8d7ad 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -259,6 +259,10 @@ namespace { Player::light_offset_x = (*room->offset)[0]; Player::light_offset_y = (*room->offset)[1]; } + if (room->flicker_wave_amplitude_ratio.has_value()) { Player::flicker_params.wave_amplitude_ratio = *room->flicker_wave_amplitude_ratio; } + if (room->flicker_wave_frequency_hz.has_value()) { Player::flicker_params.wave_frequency_hz = *room->flicker_wave_frequency_hz; } + if (room->flicker_noise_amplitude_ratio.has_value()) { Player::flicker_params.noise_amplitude_ratio = *room->flicker_noise_amplitude_ratio; } + if (room->flicker_noise_frequency_hz.has_value()) { Player::flicker_params.noise_frequency_hz = *room->flicker_noise_frequency_hz; } } } // namespace @@ -590,6 +594,14 @@ void GameScene::loadRoom(const std::string& yaml_path) { // CLOSED amb visited buit, momies i altres actors a la posició inicial. config_.map_yaml = yaml_path; Map::loadFrom(yaml_path); + // Refresquem els statics del Player a partir del YAML i tornem a + // aplicar l'override d'aquesta sala. Sense això, una sala amb + // `players: false` deixa `light_radius=0` i, en transicionar a una + // altra sala que no toqui el camp (o digui `players: true`), els + // jugadors no tornen a emetre — l'override actual és "additiu" + // sobre els statics existents, no un reset. + Player::loadFrom(config_.player_yaml); + applyRoomLightOverrideToPlayer(); // Tilemap pre-blita els tiles a un buffer ARGB intern al ctor — sense // reconstruir-lo, la imatge de la sala anterior es queda fixada per a // sempre. PaintLayer captura `tileset_path_`/`tileset_cols_` per valor @@ -1054,17 +1066,18 @@ void GameScene::applyLightingPass() { .g = Player::getLightColorG(), .b = Player::getLightColorB(), }; + const Lighting::FlickerConfig PLAYER_FP = Player::getFlickerParams(); for (const auto& p : players_) { if (p->isOutOfPlay()) { continue; } const auto C = p->getLightCenter(); - emitters.push_back({.cx = C.x, .cy = C.y, .radius = PLAYER_R, .seed = p->getLightSeed(), .flicker = PLAYER_FLICKER, .color = PLAYER_COLOR}); + emitters.push_back({.cx = C.x, .cy = C.y, .radius = PLAYER_R, .seed = p->getLightSeed(), .flicker = PLAYER_FLICKER, .color = PLAYER_COLOR, .flicker_params = PLAYER_FP}); } for (const auto* e : entity_manager_.getEntities()) { const int R = e->getLightRadius(); if (R <= 0) { continue; } const auto C = e->getLightCenter(); const Lighting::Color COL{.r = e->getLightColorR(), .g = e->getLightColorG(), .b = e->getLightColorB()}; - emitters.push_back({.cx = C.x, .cy = C.y, .radius = R, .seed = e->getLightSeed(), .flicker = e->getLightFlicker(), .color = COL}); + emitters.push_back({.cx = C.x, .cy = C.y, .radius = R, .seed = e->getLightSeed(), .flicker = e->getLightFlicker(), .color = COL, .flicker_params = e->getFlickerParams()}); } Lighting::apply(Screen::get().getCanvasArgb(), Screen::get().getCanvasWidth(), diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index 8081191..194d096 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -134,10 +134,13 @@ namespace Defaults { constexpr float ROOM_LIGHT_DEFAULT = 1.0F; // Defaults del flicker. Els reemplaça el bloc `flicker:` de // data/config/lighting.yaml. Veure `Lighting::FlickerConfig` - // per a la semàntica de cada camp. - constexpr float FLICKER_WAVE_AMPLITUDE_PX = 2.0F; + // per a la semàntica de cada camp. Les amplituds són **ràtios** + // sobre el radi base de l'emissor: així el feel del flicker és + // independent del radi. Els valors per defecte conserven el feel + // anterior per a una torxa de radi 40 (2/40 = 0.05, 1/40 = 0.025). + constexpr float FLICKER_WAVE_AMPLITUDE_RATIO = 0.05F; constexpr float FLICKER_WAVE_FREQUENCY_HZ = 3.0F; - constexpr float FLICKER_NOISE_AMPLITUDE_PX = 1.0F; + constexpr float FLICKER_NOISE_AMPLITUDE_RATIO = 0.025F; constexpr float FLICKER_NOISE_FREQUENCY_HZ = 7.0F; } // namespace Lighting