feat(lighting): flicker proporcional al radi amb overrides per emissor
This commit is contained in:
1 parent
3a42c7958a
commit
2272d3a37a
13 files changed
+298
-150
No files matched your search
@@ -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<float>;
|
||||
constexpr float GOLDEN_FRACTION = 0.6180339F;
|
||||
const float PHASE = static_cast<float>(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<float>(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<int>(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<float>(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<const Emitter> emitters) {
|
||||
darkenCanvas(canvas, width, height, room);
|
||||
const auto& cfg = activeAdditiveConfig();
|
||||
const auto& fc = activeFlicker();
|
||||
const float T_S = static_cast<float>(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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#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};
|
||||
|
||||
@@ -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<float>(); }
|
||||
if (fp.contains("wave_frequency_hz")) { out.wave_frequency_hz = fp["wave_frequency_hz"].get_value<float>(); }
|
||||
if (fp.contains("noise_amplitude_ratio")) { out.noise_amplitude_ratio = fp["noise_amplitude_ratio"].get_value<float>(); }
|
||||
if (fp.contains("noise_frequency_hz")) { out.noise_frequency_hz = fp["noise_frequency_hz"].get_value<float>(); }
|
||||
}
|
||||
|
||||
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<int>(); }
|
||||
if (l.contains("flicker")) { out.flicker = l["flicker"].get_value<bool>(); }
|
||||
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 `<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 {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<int>(); }
|
||||
@@ -178,6 +181,15 @@ void Player::readLightBlock(const fkyaml::node& yaml) {
|
||||
light_offset_y = o[1].get_value<int>();
|
||||
}
|
||||
}
|
||||
// `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<float>(); }
|
||||
if (fp.contains("wave_frequency_hz")) { flicker_params.wave_frequency_hz = fp["wave_frequency_hz"].get_value<float>(); }
|
||||
if (fp.contains("noise_amplitude_ratio")) { flicker_params.noise_amplitude_ratio = fp["noise_amplitude_ratio"].get_value<float>(); }
|
||||
if (fp.contains("noise_frequency_hz")) { flicker_params.noise_frequency_hz = fp["noise_frequency_hz"].get_value<float>(); }
|
||||
}
|
||||
}
|
||||
|
||||
void Player::readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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
|
||||
|
||||
+48
-22
@@ -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<float>(); }
|
||||
if (fp.contains("wave_frequency_hz")) { ov.flicker_wave_frequency_hz = fp["wave_frequency_hz"].get_value<float>(); }
|
||||
if (fp.contains("noise_amplitude_ratio")) { ov.flicker_noise_amplitude_ratio = fp["noise_amplitude_ratio"].get_value<float>(); }
|
||||
if (fp.contains("noise_frequency_hz")) { ov.flicker_noise_frequency_hz = fp["noise_frequency_hz"].get_value<float>(); }
|
||||
}
|
||||
|
||||
// Llegeix una entrada del mapping `lighting.emitters.<tipus>` 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<bool>();
|
||||
return ov;
|
||||
}
|
||||
if (!val.is_mapping()) { return ov; }
|
||||
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>(); }
|
||||
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<std::string>();
|
||||
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<float>();
|
||||
}
|
||||
if (lighting.contains("mode")) {
|
||||
const auto MODE_STR = lighting["mode"].get_value<std::string>();
|
||||
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<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>(); }
|
||||
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
|
||||
|
||||
@@ -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<std::array<int, 2>> 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<float> flicker_wave_amplitude_ratio;
|
||||
std::optional<float> flicker_wave_frequency_hz;
|
||||
std::optional<float> flicker_noise_amplitude_ratio;
|
||||
std::optional<float> flicker_noise_frequency_hz;
|
||||
};
|
||||
|
||||
// Consulta l'override de sala per a un tipus d'entitat (clau singular,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user