From 771ff1f6cd9be5129565d8c60c732681d00ca192 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 13 May 2026 14:11:51 +0200 Subject: [PATCH] feat(lighting): mode additive amb color per emissor --- data/maps/p1/r2.yaml | 15 ++ source/core/rendering/lighting.cpp | 163 ++++++++++++++---- source/core/rendering/lighting.hpp | 77 +++++++-- source/game/entities/entity.hpp | 17 ++ .../entities/entity_manager_loadlevel.cpp | 56 ++++-- source/game/entities/player.cpp | 13 ++ source/game/entities/player.hpp | 16 +- source/game/map/map.cpp | 35 +++- source/game/map/map.hpp | 15 ++ source/game/scenes/game_scene.cpp | 16 +- 10 files changed, 352 insertions(+), 71 deletions(-) diff --git a/data/maps/p1/r2.yaml b/data/maps/p1/r2.yaml index 8d08951..2f5690d 100644 --- a/data/maps/p1/r2.yaml +++ b/data/maps/p1/r2.yaml @@ -15,6 +15,21 @@ tileset: data/tilesets/pyramid_1.gif tile_size: 16 origin: [-8, 0] +# Banc de proves del mode `additive` d'il·luminació. La sala queda +# lleugerament fosca (level 0.9) i els emissors hi sumen color per +# damunt — antorxes càlides, jugador en blanc-blau fred, mòmies amb +# halo verd-fantasma. En mode additive el `color` de cada emissor entra +# en joc (en subtractive s'ignora). Veure docs al header de +# `core/rendering/lighting.hpp` per a la fórmula i el comportament al +# solape (suma → tendència al blanc, físicament correcte). +lighting: + mode: additive + level: 0.9 + emitters: + torches: { color: [255, 170, 60] } + players: { color: [180, 220, 255] } + mummies: { color: [120, 220, 140] } + # Í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 # es distingeix a simple vista. Provisional fins que l'art del pintat diff --git a/source/core/rendering/lighting.cpp b/source/core/rendering/lighting.cpp index 6265c51..c64a15c 100644 --- a/source/core/rendering/lighting.cpp +++ b/source/core/rendering/lighting.cpp @@ -133,9 +133,9 @@ namespace Lighting { } } - // Pasada final: pixel = pixel * lum / 255 per canal. El canal alpha - // es preserva (la resta del pipeline assumeix opacitat total al - // canvas; veure Rendering::OPAQUE_ALPHA). + // Pasada final del mode subtractive: pixel = pixel * lum / 255 + // per canal. El canal alpha es preserva (la resta del pipeline + // assumeix opacitat total al canvas; veure Rendering::OPAQUE_ALPHA). void modulateCanvas(std::uint32_t* canvas, const std::uint8_t* lum, int width, int height) { for (int y = 0; y < height; ++y) { std::uint32_t* px = canvas + (static_cast(y) * width); @@ -158,6 +158,77 @@ namespace Lighting { } } + // Atenuació ambient del mode additive: aplica `pixel × level` per + // canvas abans del bucle d'emissors. Si `level == 255` (sala a + // 1.0) salta com a no-op — el cas comú quan només volem el tint + // dels emissors sense oscurir la sala. + void darkenCanvas(std::uint32_t* canvas, int width, int height, std::uint8_t level) { + if (level == 255) { return; } + for (int y = 0; y < height; ++y) { + std::uint32_t* px = canvas + (static_cast(y) * width); + for (int x = 0; x < width; ++x) { + const std::uint32_t SRC = px[x]; + const std::uint8_t SR = (SRC >> 16) & 0xFF; + const std::uint8_t SG = (SRC >> 8) & 0xFF; + const std::uint8_t SB = SRC & 0xFF; + const auto R = static_cast((SR * level) / 255); + const auto G = static_cast((SG * level) / 255); + const auto B = static_cast((SB * level) / 255); + px[x] = (SRC & 0xFF000000U) | + (static_cast(R) << 16) | + (static_cast(G) << 8) | + static_cast(B); + } + } + } + + // Pasada del mode additive per a un emissor: suma `color × intensity` + // a cada píxel cobert pel cercle, saturant a 255 per canal. A + // diferència del subtractive, NO usa buffer intermig (no cal max()) + // perquè la suma és commutativa; emissors solapats acumulen + // contribucions de manera natural. Si dues llums de colors diferents + // es solapen, el resultat tendeix al blanc en la zona de coincidència + // (suma física): el solape s'aclareix més que un sol emissor. + void rasterizeEmitterAdditive(const Emitter& e, const EmitterRings& rings, std::uint32_t* canvas, int width, int height) { + const int R = e.radius; + const int CX = static_cast(e.cx); + const int CY = static_cast(e.cy); + const int X0 = std::max(0, CX - R); + const int Y0 = std::max(0, CY - R); + const int X1 = std::min(width - 1, CX + R); + const int Y1 = std::min(height - 1, CY + R); + for (int y = Y0; y <= Y1; ++y) { + const int DY = y - CY; + const int DY_SQ = DY * DY; + if (DY_SQ > rings.radius_sq) { continue; } + std::uint32_t* row = canvas + (static_cast(y) * width); + for (int x = X0; x <= X1; ++x) { + const int DX = x - CX; + const int DIST_SQ = (DX * DX) + DY_SQ; + if (DIST_SQ > rings.radius_sq) { continue; } + std::uint8_t intensity = rings.outer; + if (DIST_SQ <= rings.inner_sq) { + intensity = rings.inner; + } else if (DIST_SQ <= rings.mid_sq) { + intensity = rings.mid; + } + // contrib_channel = emitter_color_channel × intensity / 255 + // (intensity ja és 0..255 quantitzat a `toByte`). + const int CR = (e.color.r * intensity) / 255; + const int CG = (e.color.g * intensity) / 255; + const int CB = (e.color.b * intensity) / 255; + const std::uint32_t SRC = row[x]; + const int SR = static_cast((SRC >> 16) & 0xFF); + const int SG = static_cast((SRC >> 8) & 0xFF); + const int SB = static_cast(SRC & 0xFF); + const auto NR = static_cast(std::min(255, SR + CR)); + const auto NG = static_cast(std::min(255, SG + CG)); + const auto NB = static_cast(std::min(255, SB + CB)); + row[x] = (SRC & 0xFF000000U) | (NR << 16) | (NG << 8) | NB; + } + } + } + } // namespace auto config() -> const GradientConfig& { @@ -196,39 +267,71 @@ namespace Lighting { } } - void apply(std::uint32_t* canvas, int width, int height, float room_light, std::span emitters) { + namespace { + + // 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. + 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}; + } + + // Camí del mode subtractive (històric): omple un buffer de + // luminància amb `max(room, emitter_intensity)` i finalment + // multiplica el canvas per ell. Color de l'emissor ignorat. + void applySubtractive(std::uint32_t* canvas, int width, int height, std::uint8_t room, std::span emitters) { + auto& lum = luminanceBuffer(); + const std::size_t TOTAL = static_cast(width) * static_cast(height); + if (lum.size() != TOTAL) { lum.resize(TOTAL); } + std::ranges::fill(lum, room); + + const auto& cfg = activeConfig(); + 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 auto RINGS = computeRings(R_EFF, cfg); + const Emitter EFF = withEffectiveRadius(e, R_EFF); + rasterizeEmitter(EFF, RINGS, lum.data(), width, height); + } + modulateCanvas(canvas, lum.data(), width, height); + } + + // Camí del mode additive: atenua el canvas per `room` (si <255) + // i després suma `color × intensity` per emissor directament al + // canvas, sense buffer intermig. + 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 = activeConfig(); + 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 auto RINGS = computeRings(R_EFF, cfg); + const Emitter EFF = withEffectiveRadius(e, R_EFF); + rasterizeEmitterAdditive(EFF, RINGS, canvas, width, height); + } + } + + } // namespace + + void apply(std::uint32_t* canvas, int width, int height, Mode mode, float room_light, std::span emitters) { if (canvas == nullptr || width <= 0 || height <= 0) { return; } - // Atajo: si la sala està totalment il·luminada i no hi ha - // emissors, la pasada seria un `pixel * 1.0` global — no cal - // tocar res. És el camí curt habitual mentre no posem - // `light_level` a les sales. + // Atajo per a tots dos modes: sala totalment "neutra" + cap + // emissor → la pasada no canviaria res al canvas. + // subtractive: room=1.0 i sense emissors → pixel * 1.0 global. + // additive: room=1.0 i sense emissors → cap aportació. const std::uint8_t ROOM = toByte(room_light); if (ROOM == 255 && emitters.empty()) { return; } - auto& lum = luminanceBuffer(); - const std::size_t TOTAL = static_cast(width) * static_cast(height); - if (lum.size() != TOTAL) { lum.resize(TOTAL); } - std::ranges::fill(lum, ROOM); - - const auto& cfg = activeConfig(); - 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 auto RINGS = computeRings(R_EFF, cfg); - // El rasteritzador llegeix el radi de l'`Emitter` per al - // càlcul de la bounding box, així que li passem un emissor - // "patxat" amb el radi efectiu d'aquest frame. Mantenim el - // valor original al `Emitter` del caller intacte. - const Emitter EFF{.cx = e.cx, .cy = e.cy, .radius = R_EFF, .seed = e.seed, .flicker = e.flicker}; - rasterizeEmitter(EFF, RINGS, lum.data(), width, height); + if (mode == Mode::Additive) { + applyAdditive(canvas, width, height, ROOM, emitters); + } else { + applySubtractive(canvas, width, height, ROOM, emitters); } - - modulateCanvas(canvas, lum.data(), width, height); } } // namespace Lighting diff --git a/source/core/rendering/lighting.hpp b/source/core/rendering/lighting.hpp index 25b6e62..57b092e 100644 --- a/source/core/rendering/lighting.hpp +++ b/source/core/rendering/lighting.hpp @@ -7,36 +7,73 @@ // Sistema d'il·luminació CPU. Treballa sobre el mateix canvas ARGB del // joc (320×240) que tot el pipeline de render: després que GameScene // haja pintat tilemap, paint layer, sombres i actors, una pasada final -// multiplica cada píxel del canvas per un factor de luminància 0..1 -// derivat de la combinació de "llum de la sala" + cercles emissors al -// voltant de certes entitats. +// modifica cada píxel del canvas en funció de la "llum de la sala" i +// dels cercles emissors al voltant de certes entitats. // -// Forma de cada emissor (radial, 3 anells configurables al fitxer -// data/config/lighting.yaml; veure `GradientConfig`): +// Dos modes seleccionables per la sala (`lighting.mode` al YAML): +// +// SUBTRACTIVE (mode històric, default si no es declara mode): +// out = src × max(room_light, max_emitter_intensity_at_pixel) +// Els emissors "trenquen" una capa fosca multiplicativa. Tot el +// trip cromàtic dels emissors s'ignora — només la intensitat +// compta. Pensat per a sales fosques (cripta, pirámide). +// +// ADDITIVE (mode nou): +// out = saturate( (src × room_light) + Σ (emitter_color × intensity) ) +// El canvas s'enfosqueix opcionalment per `room_light` (atenuació +// ambient) i els emissors hi sumen color cap a sobre. Les intensitats +// dels anells es llegeixen com a "alfa" del color afegit (0..1). +// Pensat per a sales clares on les antorxes aporten halo cromàtic +// sense restar contrast — o per a sales lleument fosques (0.85, p.ex.) +// on el halo de l'antorxa es veu càlid en lloc de "blanquinós". +// +// Forma del cercle de cada emissor (radial, 3 anells configurables al +// fitxer data/config/lighting.yaml; veure `GradientConfig`): // 0..inner_ratio del radi → inner_intensity // inner_ratio..mid_ratio → mid_intensity // mid_ratio..1.0 → outer_intensity -// fora del radi → light_level de la sala // -// Combinació entre emissors: per a cada píxel es pren el màxim del -// llum de sala i de la intensitat aportada per cada emissor que el -// solapa — no se "sumen" intensitats. Coherent amb la idea de "centre -// 1.0": el píxel que cau dins de qualsevol nucli no s'enfosqueix. +// Combinació entre emissors: +// SUBTRACTIVE → per a cada píxel es pren el màxim de la intensitat +// aportada per cada emissor que el solapa (no se sumen intensitats). +// ADDITIVE → per a cada píxel se sumen totes les contribucions +// (`color × intensity`), saturant a 255 per canal. Dues llums de +// colors diferents que solapen produeixen la suma natural (orange + +// blue → near-white) — comportament físic. namespace Lighting { + // Mode de composició. La sala el selecciona via `lighting.mode: + // additive | subtractive` (default subtractive si manca, retrocompat). + enum class Mode : std::uint8_t { + Subtractive, + Additive, + }; + + // Color emès. Default blanc (sense modificació quan s'usa en mode + // additive). En mode subtractive s'ignora — la pasada multiplicativa + // només té un canal (intensitat). El YAML accepta `color: [r, g, b]` + // a qualsevol nivell de la cadena de resolució (tipus, sala, spawn). + struct Color { + std::uint8_t r{255}; + std::uint8_t g{255}; + std::uint8_t b{255}; + }; + // 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`). // `flicker` activa la modulació del radi per frame; `seed` és un // identificador estable per emissor (no canvia amb la posició) del // qual es deriva la fase del flicker — així els emissors mòbils - // mantenen un patró propi que no "deriva" pel moviment. + // mantenen un patró propi que no "deriva" pel moviment. `color` + // només té efecte en mode additive. struct Emitter { float cx{0.0F}; float cy{0.0F}; int radius{0}; int seed{0}; bool flicker{false}; + Color color{}; }; // Forma del gradient. Compartida per tots els emissors per evitar @@ -71,12 +108,16 @@ namespace Lighting { // Defaults::Lighting fins que loadConfig() es crida. [[nodiscard]] auto config() -> const GradientConfig&; - // Aplica la pasada d'il·luminació al canvas in-place. `room_light` - // és l'atenuació base de la sala (0..1, 1 = sense atenuació). El - // span d'emissors pot ser buit — en aquest cas tota la sala queda - // multiplicada per `room_light`. Aquesta funció no fa cap allocació - // si es crida amb la mateixa mida de canvas múltiples cops: manté - // un buffer intern reutilitzable. - void apply(std::uint32_t* canvas, int width, int height, float room_light, std::span emitters); + // 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 + // atenuació): en subtractive és el "terra" de la intensitat fora + // dels emissors; en additive és la multiplicació prèvia del canvas + // abans de sumar els emissors (es pot deixar a 1.0 per "només tint", + // o baixar per combinar oscuriment ambient + halo cromàtic). El + // span d'emissors pot ser buit. La funció no fa cap allocació si + // es crida amb la mateixa mida de canvas múltiples cops: manté un + // buffer intern reutilitzable (només l'usa el mode subtractive). + void apply(std::uint32_t* canvas, int width, int height, Mode mode, float room_light, std::span emitters); } // namespace Lighting diff --git a/source/game/entities/entity.hpp b/source/game/entities/entity.hpp index 9faca8c..c1c1b68 100644 --- a/source/game/entities/entity.hpp +++ b/source/game/entities/entity.hpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -266,6 +267,19 @@ class Entity { [[nodiscard]] auto getLightFlicker() const -> bool { return light_flicker_; } void setLightFlicker(bool f) { light_flicker_ = f; } + // Color emès en mode additive (RGB 0..255 per canal). Default blanc: + // en mode subtractive aquest valor s'ignora completament; en mode + // additive és el tint que l'emissor afegeix al canvas. La cadena de + // resolució (tipus → sala → spawn) el pobla via `setLightColor`. + [[nodiscard]] auto getLightColorR() const -> std::uint8_t { return light_color_r_; } + [[nodiscard]] auto getLightColorG() const -> std::uint8_t { return light_color_g_; } + [[nodiscard]] auto getLightColorB() const -> std::uint8_t { return light_color_b_; } + void setLightColor(std::uint8_t r, std::uint8_t g, std::uint8_t b) { + light_color_r_ = r; + light_color_g_ = g; + light_color_b_ = b; + } + // 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 @@ -302,6 +316,9 @@ class Entity { static inline int s_next_light_seed{0}; int light_seed_{++s_next_light_seed}; bool light_flicker_{false}; + std::uint8_t light_color_r_{255}; + std::uint8_t light_color_g_{255}; + std::uint8_t light_color_b_{255}; 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 981bb19..f063961 100644 --- a/source/game/entities/entity_manager_loadlevel.cpp +++ b/source/game/entities/entity_manager_loadlevel.cpp @@ -4,6 +4,8 @@ // consumidors que només volen la base del manager (p.ex. el smoke test // stand-alone) enllacen `entity_manager.cpp` sense arrossegar aquest fitxer. +#include +#include #include #include #include @@ -155,22 +157,38 @@ namespace { return c["height"].get_value(); } - // Bloc opcional `light: { radius: N, flicker: bool }` de la plantilla. - // Si manca, el radi és 0 i l'entitat no entra a la pasada d'il·luminació. - // `flicker` activa la modulació "tipus foc" del radi (només antorxes - // l'haurien d'usar). La forma del gradient (3 anells) i els paràmetres - // del flicker viuen a data/config/lighting.yaml — no cal declarar-los - // per entitat. + // Bloc opcional `light: { radius: N, flicker: bool, color: [r,g,b] }` + // de la plantilla. Si manca, el radi és 0 i l'entitat no entra a la + // pasada d'il·luminació. `flicker` activa la modulació "tipus foc" + // del radi (només antorxes l'haurien d'usar). `color` només té + // efecte en mode additive — defecte blanc (255,255,255). La forma + // del gradient (3 anells) i els paràmetres del flicker viuen a + // data/config/lighting.yaml — no cal declarar-los per entitat. struct LightTemplate { int radius{0}; bool flicker{false}; + std::uint8_t color_r{255}; + std::uint8_t color_g{255}; + std::uint8_t color_b{255}; }; + + // Helper compartit entre les tres capes (tipus, sala via Map, spawn) + // per llegir `color: [r, g, b]` amb clamp defensiu a 0..255. + auto readColorTriplet(const fkyaml::node& seq, std::uint8_t& r, std::uint8_t& g, std::uint8_t& b) -> bool { + if (!seq.is_sequence() || seq.size() != 3) { return false; } + r = static_cast(std::clamp(seq[0].get_value(), 0, 255)); + g = static_cast(std::clamp(seq[1].get_value(), 0, 255)); + b = static_cast(std::clamp(seq[2].get_value(), 0, 255)); + return true; + } + auto readLight(const fkyaml::node& tmpl) -> LightTemplate { if (!tmpl.contains("light")) { return {}; } const auto& l = tmpl["light"]; LightTemplate out; 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); } return out; } @@ -185,23 +203,26 @@ namespace { 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}; } + if (!room->enabled) { return {.radius = 0, .flicker = false, .color_r = 255, .color_g = 255, .color_b = 255}; } LightTemplate out = type_default; if (room->radius.has_value()) { out.radius = *room->radius; } if (room->flicker.has_value()) { out.flicker = *room->flicker; } + if (room->color.has_value()) { + out.color_r = (*room->color)[0]; + out.color_g = (*room->color)[1]; + out.color_b = (*room->color)[2]; + } 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 + // `light: false` → apaga aquesta instància + // `light: true` → informatiu (cau a capes inferiors) + // `light: { radius, flicker, color: [r,g,b] }` → 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"]; @@ -215,6 +236,12 @@ namespace { 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()); } + if (l.contains("color")) { + std::uint8_t r{}; + std::uint8_t g{}; + std::uint8_t b{}; + if (readColorTriplet(l["color"], r, g, b)) { entity.setLightColor(r, g, b); } + } } // Bloc `collision: { size: [w, h], base_size: [bw, bh]?, offset: [x, y]? }` @@ -346,6 +373,7 @@ namespace { const auto LIGHT = resolveLightWithRoom(type, readLight(tmpl)); entity->setLightRadius(LIGHT.radius); entity->setLightFlicker(LIGHT.flicker); + entity->setLightColor(LIGHT.color_r, LIGHT.color_g, LIGHT.color_b); anchor_out = readAnchor(tmpl); return entity; } diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index e23564b..4c64062 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -150,10 +150,23 @@ void Player::loadFrom(const std::string& yaml_path) { void Player::readLightBlock(const fkyaml::node& yaml) { light_radius = 0; light_flicker = false; + light_color_r = 255; + light_color_g = 255; + light_color_b = 255; if (!yaml.contains("light")) { return; } const auto& l = yaml["light"]; if (l.contains("radius")) { light_radius = l["radius"].get_value(); } if (l.contains("flicker")) { light_flicker = l["flicker"].get_value(); } + // `color: [r, g, b]` opcional, bytes 0..255. Si manca, blanc (sense + // tint) — comportament idèntic al mode subtractive de hui. + if (l.contains("color")) { + const auto& c = l["color"]; + if (c.is_sequence() && c.size() == 3) { + light_color_r = static_cast(std::clamp(c[0].get_value(), 0, 255)); + light_color_g = static_cast(std::clamp(c[1].get_value(), 0, 255)); + light_color_b = static_cast(std::clamp(c[2].get_value(), 0, 255)); + } + } } 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 d7b535e..ed6a1c1 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -125,12 +125,17 @@ class Player : public AnimatedSprite { inline static int body_height = 0; // Radi de la llum emesa pel jugador (en píxels del canvas), llegit - // del bloc opcional `light: { radius: N, flicker: bool }` del YAML. - // 0 = no emet. Es declara estàtic com la resta de configs del jugador - // (immunity, jump, ...): tots els jugadors del mateix YAML comparteixen - // el valor. `light_flicker` activa la modulació "tipus foc" del radi. + // del bloc opcional `light: { radius: N, flicker: bool, color: [r,g,b] }` + // del YAML. 0 = no emet. Es declara estàtic com la resta de configs + // del jugador (immunity, jump, ...): tots els jugadors del mateix YAML + // comparteixen el valor. `light_flicker` activa la modulació "tipus + // foc" del radi; `light_color_*` són el tint quan la sala usa mode + // additive (en subtractive s'ignora). inline static int light_radius = 0; inline static bool light_flicker = false; + inline static std::uint8_t light_color_r = 255; + inline static std::uint8_t light_color_g = 255; + inline static std::uint8_t light_color_b = 255; // Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor. static void loadFrom(const std::string& yaml_path); @@ -146,6 +151,9 @@ class Player : public AnimatedSprite { // per a que el cercle "salti" amb el jugador, no es quedi al sòl. [[nodiscard]] static auto getLightRadius() -> int { return light_radius; } [[nodiscard]] static auto getLightFlicker() -> bool { return light_flicker; } + [[nodiscard]] static auto getLightColorR() -> std::uint8_t { return light_color_r; } + [[nodiscard]] static auto getLightColorG() -> std::uint8_t { return light_color_g; } + [[nodiscard]] static auto getLightColorB() -> std::uint8_t { return light_color_b; } // 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 9bdcd65..66974dd 100644 --- a/source/game/map/map.cpp +++ b/source/game/map/map.cpp @@ -1,5 +1,6 @@ #include "game/map/map.hpp" +#include #include #include #include @@ -49,18 +50,45 @@ namespace Map { return yaml_key; } + // Parser comú d'un color YAML 3-tupla a bytes. Retorna nullopt + // si el node no és una seqüència de 3 elements. Usat pels + // overrides de sala i, en `entity_manager_loadlevel.cpp`, també + // pels defaults de tipus i overrides de spawn (passa pel mateix + // helper per coherència). `clamp` defensiu per a YAMLs que + // entreguen valors fora de 0..255. + auto parseColorTriplet(const fkyaml::node& node) -> std::optional> { + if (!node.is_sequence() || node.size() != 3) { return std::nullopt; } + auto clamp = [](int v) { return static_cast(std::clamp(v, 0, 255)); }; + return std::array{ + clamp(node[0].get_value()), + clamp(node[1].get_value()), + clamp(node[2].get_value()), + }; + } + // 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 + // actualitza `light_level`, `lighting_mode` i el mapping + // `g_room_emitters`. Idempotent: cada `Map::loadFrom` reseteja + // tot l'estat abans de poblar-lo, així no s'arrosseguen valors // d'una sala anterior. void loadLightingBlock(const fkyaml::node& yaml) { g_room_emitters.clear(); + lighting_mode = Lighting::Mode::Subtractive; if (!yaml.contains("lighting")) { return; } const auto& lighting = yaml["lighting"]; 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); + } + } if (!lighting.contains("emitters") || !lighting["emitters"].is_mapping()) { return; } const auto& em = lighting["emitters"]; for (auto it = em.begin(); it != em.end(); ++it) { @@ -73,6 +101,7 @@ namespace Map { 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"]); } } g_room_emitters[KEY] = ov; } diff --git a/source/game/map/map.hpp b/source/game/map/map.hpp index 3e84868..68c1b0c 100644 --- a/source/game/map/map.hpp +++ b/source/game/map/map.hpp @@ -2,11 +2,15 @@ #include +#include +#include #include #include #include #include +#include "core/rendering/lighting.hpp" + // Mapa d'una piràmide: sala rectangular amb marcador superior, muros pels // quatre costats i graella de sarcòfags envoltada per un passadís a cada // cantó. La geometria (graella, pitch, amplada de murs) es carrega des del @@ -60,6 +64,13 @@ namespace Map { // al root es manté per compatibilitat enrere. inline float light_level = 1.0F; + // Mode de composició de la pasada d'il·luminació (subtractive per + // defecte; additive si la sala declara `lighting.mode: additive`). + // Veure el comentari de `core/rendering/lighting.hpp` per a la + // semàntica de cada mode. En subtractive, els colors dels emissors + // s'ignoren — el camí és el de sempre. + inline Lighting::Mode lighting_mode = Lighting::Mode::Subtractive; + // 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 @@ -80,6 +91,10 @@ namespace Map { bool enabled{true}; std::optional radius; std::optional flicker; + // Color emès en mode additive. `nullopt` → conservar el color + // del tipus (default blanc 255,255,255 si el tipus no en declara). + // La forma YAML és `color: [r, g, b]` (bytes 0..255). + std::optional> color; }; // 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 1a93382..34f3785 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -250,6 +250,11 @@ namespace { } if (room->radius.has_value()) { Player::light_radius = *room->radius; } if (room->flicker.has_value()) { Player::light_flicker = *room->flicker; } + if (room->color.has_value()) { + Player::light_color_r = (*room->color)[0]; + Player::light_color_g = (*room->color)[1]; + Player::light_color_b = (*room->color)[2]; + } } } // namespace @@ -1040,20 +1045,27 @@ void GameScene::applyLightingPass() { // gating extern aquí (els jugadors són només 1-2 actors i el cost // d'afegir-los al vector és menyspreable). const bool PLAYER_FLICKER = Player::getLightFlicker(); + const Lighting::Color PLAYER_COLOR{ + .r = Player::getLightColorR(), + .g = Player::getLightColorG(), + .b = Player::getLightColorB(), + }; 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}); + emitters.push_back({.cx = C.x, .cy = C.y, .radius = PLAYER_R, .seed = p->getLightSeed(), .flicker = PLAYER_FLICKER, .color = PLAYER_COLOR}); } for (const auto* e : entity_manager_.getEntities()) { const int R = e->getLightRadius(); if (R <= 0) { continue; } const auto C = e->getLightCenter(); - emitters.push_back({.cx = C.x, .cy = C.y, .radius = R, .seed = e->getLightSeed(), .flicker = e->getLightFlicker()}); + 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}); } Lighting::apply(Screen::get().getCanvasArgb(), Screen::get().getCanvasWidth(), Screen::get().getCanvasHeight(), + Map::lighting_mode, Map::light_level, emitters); }