feat(lighting): mode additive amb color per emissor

This commit is contained in:
JailDesigner committed 2026-05-13 14:11:51 +02:00
1 parent 24609d6c5c
commit 771ff1f6cd
10 files changed
+352 -71

No files matched your search

+17
View File
@@ -2,6 +2,7 @@
#include <SDL3/SDL.h>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
@@ -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};
@@ -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 <algorithm>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <string>
@@ -155,22 +157,38 @@ namespace {
return c["height"].get_value<int>();
}
// 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::uint8_t>(std::clamp(seq[0].get_value<int>(), 0, 255));
g = static_cast<std::uint8_t>(std::clamp(seq[1].get_value<int>(), 0, 255));
b = static_cast<std::uint8_t>(std::clamp(seq[2].get_value<int>(), 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<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); }
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<int>()); }
if (l.contains("flicker")) { entity.setLightFlicker(l["flicker"].get_value<bool>()); }
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;
}
+13
View File
@@ -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<int>(); }
if (l.contains("flicker")) { light_flicker = l["flicker"].get_value<bool>(); }
// `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::uint8_t>(std::clamp(c[0].get_value<int>(), 0, 255));
light_color_g = static_cast<std::uint8_t>(std::clamp(c[1].get_value<int>(), 0, 255));
light_color_b = static_cast<std::uint8_t>(std::clamp(c[2].get_value<int>(), 0, 255));
}
}
}
void Player::readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path) {
+12 -4
View File
@@ -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