Merge branch 'feature/lighting'
This commit is contained in:
commit
9bd5e7db33
20 files changed
+595
No files matched your search
@@ -81,6 +81,7 @@ set(APP_SOURCES
|
|||||||
source/core/input/mouse.cpp
|
source/core/input/mouse.cpp
|
||||||
source/core/rendering/screen.cpp
|
source/core/rendering/screen.cpp
|
||||||
source/core/rendering/canvas.cpp
|
source/core/rendering/canvas.cpp
|
||||||
|
source/core/rendering/lighting.cpp
|
||||||
source/core/rendering/notifier.cpp
|
source/core/rendering/notifier.cpp
|
||||||
source/core/rendering/render_info.cpp
|
source/core/rendering/render_info.cpp
|
||||||
source/core/rendering/gif.cpp
|
source/core/rendering/gif.cpp
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Configuració global del sistema d'il·luminació.
|
||||||
|
#
|
||||||
|
# Cada llum emissora (player, torxa, mòmia, ...) té un únic paràmetre
|
||||||
|
# propi: el radi en píxels. La forma de la llum dins d'aquest radi
|
||||||
|
# (els tres anells del gradient) viu en aquest fitxer perquè és una
|
||||||
|
# decisió de "feel" comuna a totes les entitats — així pots tocar la
|
||||||
|
# corba aquí sense haver de modificar 10 YAMLs.
|
||||||
|
#
|
||||||
|
# Forma del gradient (centre → vora):
|
||||||
|
# 0..inner_ratio del radi → inner_intensity (zona totalment il·luminada)
|
||||||
|
# inner_ratio..mid_ratio → mid_intensity (anell mig)
|
||||||
|
# mid_ratio..1.0 → outer_intensity (anell exterior)
|
||||||
|
# fora del radi → light_level de la sala
|
||||||
|
#
|
||||||
|
# Les intensitats són factors 0..1 que es multipliquen sobre el píxel
|
||||||
|
# final del canvas. inner=1.0 vol dir "píxel sense alterar".
|
||||||
|
gradient:
|
||||||
|
inner_ratio: 0.5
|
||||||
|
mid_ratio: 0.85
|
||||||
|
inner_intensity: 1.0
|
||||||
|
mid_intensity: 0.8
|
||||||
|
outer_intensity: 0.6
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# 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.
|
||||||
|
flicker:
|
||||||
|
wave_amplitude_px: 2
|
||||||
|
wave_frequency_hz: 3.0
|
||||||
|
noise_amplitude_px: 1
|
||||||
|
noise_frequency_hz: 7.0
|
||||||
@@ -68,3 +68,10 @@ hurt:
|
|||||||
# So opcional: basename del fitxer dins `data/audio/sounds/`. Es dispara
|
# So opcional: basename del fitxer dins `data/audio/sounds/`. Es dispara
|
||||||
# un sol cop per fuetada quan la mòmia entra a HURT. Si manca, no sona.
|
# un sol cop per fuetada quan la mòmia entra a HURT. Si manca, no sona.
|
||||||
sound: mummy_hurt.wav
|
sound: mummy_hurt.wav
|
||||||
|
|
||||||
|
# Halo tènue al voltant de la mòmia. Radi més curt que el del jugador
|
||||||
|
# perquè el rastre lumínic delati la seva posició sense "il·luminar
|
||||||
|
# la sala" — vol ser una pista visual a la foscor.
|
||||||
|
light:
|
||||||
|
radius: 24
|
||||||
|
flicker: true
|
||||||
@@ -140,3 +140,11 @@ immunity:
|
|||||||
duration_ms: 3000
|
duration_ms: 3000
|
||||||
blink_period_ms: 100
|
blink_period_ms: 100
|
||||||
palette: morcus_wash
|
palette: morcus_wash
|
||||||
|
|
||||||
|
# Llum emesa pel jugador. `radius` és el radi del cercle de llum
|
||||||
|
# en píxels del canvas; el gradient (3 anells: 1.0 / 0.9 / 0.8) viu
|
||||||
|
# a data/config/lighting.yaml i és compartit per tots els emissors.
|
||||||
|
# Omet el bloc per desactivar la llum d'aquest personatge.
|
||||||
|
light:
|
||||||
|
radius: 64
|
||||||
|
flicker: true
|
||||||
@@ -140,3 +140,11 @@ immunity:
|
|||||||
duration_ms: 3000
|
duration_ms: 3000
|
||||||
blink_period_ms: 100
|
blink_period_ms: 100
|
||||||
palette: sam_wash
|
palette: sam_wash
|
||||||
|
|
||||||
|
# Llum emesa pel jugador. `radius` és el radi del cercle de llum
|
||||||
|
# en píxels del canvas; el gradient (3 anells: 1.0 / 0.9 / 0.8) viu
|
||||||
|
# a data/config/lighting.yaml i és compartit per tots els emissors.
|
||||||
|
# Omet el bloc per desactivar la llum d'aquest personatge.
|
||||||
|
light:
|
||||||
|
radius: 64
|
||||||
|
flicker: true
|
||||||
@@ -16,3 +16,14 @@ flags:
|
|||||||
# El render clipa cada frame per `anim.currentFrame()` × 16 píxels en Y.
|
# El render clipa cada frame per `anim.currentFrame()` × 16 píxels en Y.
|
||||||
animations:
|
animations:
|
||||||
idle: { frames: [0, 1, 2, 3], fps: 8, loop: true }
|
idle: { frames: [0, 1, 2, 3], fps: 8, loop: true }
|
||||||
|
|
||||||
|
# Cercle de llum centrat a l'antorxa. El radi és en píxels del canvas;
|
||||||
|
# el gradient global (data/config/lighting.yaml) hi posa la forma (3
|
||||||
|
# anells). Amb `flicker: true` el radi efectiu oscil·la per frame
|
||||||
|
# (suma de dues sinusoides amb fase derivada de la posició) per a
|
||||||
|
# que el cercle "respiri" com una flama; els paràmetres del flicker
|
||||||
|
# també viuen al config global. Omet el bloc per fer una antorxa
|
||||||
|
# "apagada".
|
||||||
|
light:
|
||||||
|
radius: 40
|
||||||
|
flicker: true
|
||||||
@@ -15,6 +15,12 @@ tileset: data/tilesets/pyramid_1.gif
|
|||||||
tile_size: 16
|
tile_size: 16
|
||||||
origin: [-8, 0]
|
origin: [-8, 0]
|
||||||
|
|
||||||
|
# Il·luminació base de la sala (0..1). 1.0 = sala totalment il·luminada
|
||||||
|
# (com hui, sense efecte visual); 0.0 = completament negra fora del radi
|
||||||
|
# d'un emissor. El gradient dels cercles d'emissor viu a
|
||||||
|
# data/config/lighting.yaml. Omet aquesta clau per deixar la sala neta.
|
||||||
|
light_level: 0.25
|
||||||
|
|
||||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
#include "core/rendering/lighting.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <numbers>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/resources/resource_helper.hpp"
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "utils/defaults.hpp"
|
||||||
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
|
namespace Lighting {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Buffer de luminància compartit entre frames. Es redimensiona
|
||||||
|
// només si canvia la mida del canvas (mai en un run normal: el
|
||||||
|
// canvas del joc és 320×240 fix). El reusem perquè una `apply`
|
||||||
|
// allotjant cada frame seria un cost evitable.
|
||||||
|
auto luminanceBuffer() -> std::vector<std::uint8_t>& {
|
||||||
|
static std::vector<std::uint8_t> buf_;
|
||||||
|
return buf_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuració global, inicialitzada amb els defaults compilats.
|
||||||
|
// loadConfig() la reemplaça si troba el fitxer.
|
||||||
|
auto activeConfig() -> GradientConfig& {
|
||||||
|
static GradientConfig cfg_{
|
||||||
|
.inner_ratio = Defaults::Lighting::INNER_RATIO,
|
||||||
|
.mid_ratio = Defaults::Lighting::MID_RATIO,
|
||||||
|
.inner_intensity = Defaults::Lighting::INNER_INTENSITY,
|
||||||
|
.mid_intensity = Defaults::Lighting::MID_INTENSITY,
|
||||||
|
.outer_intensity = Defaults::Lighting::OUTER_INTENSITY,
|
||||||
|
};
|
||||||
|
return cfg_;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto activeFlicker() -> FlickerConfig& {
|
||||||
|
static FlickerConfig fc_{
|
||||||
|
.wave_amplitude_px = Defaults::Lighting::FLICKER_WAVE_AMPLITUDE_PX,
|
||||||
|
.wave_frequency_hz = Defaults::Lighting::FLICKER_WAVE_FREQUENCY_HZ,
|
||||||
|
.noise_amplitude_px = Defaults::Lighting::FLICKER_NOISE_AMPLITUDE_PX,
|
||||||
|
.noise_frequency_hz = Defaults::Lighting::FLICKER_NOISE_FREQUENCY_HZ,
|
||||||
|
};
|
||||||
|
return fc_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modula el radi base d'un emissor amb la suma de dues sinusoides
|
||||||
|
// (`wave` + `noise`) i una fase derivada de `seed`. La seed és un
|
||||||
|
// 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.
|
||||||
|
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 int R = base_radius + static_cast<int>(std::lround(DELTA));
|
||||||
|
return std::max(1, R);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quantitza un factor 0..1 a 0..255. `clamp` per defensar-nos de
|
||||||
|
// YAMLs que entreguen valors fora de rang sense forçar la mà.
|
||||||
|
auto toByte(float v) -> std::uint8_t {
|
||||||
|
const float CLAMPED = std::clamp(v, 0.0F, 1.0F);
|
||||||
|
return static_cast<std::uint8_t>(std::lround(CLAMPED * 255.0F));
|
||||||
|
}
|
||||||
|
|
||||||
|
void readField(const fkyaml::node& node, const char* key, float& dst) {
|
||||||
|
if (node.contains(key)) { dst = node[key].get_value<float>(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-calcula els 3 quadrats de radi i les intensitats que farem
|
||||||
|
// servir dins del bucle per píxel. Es resol una sola vegada per
|
||||||
|
// emissor i s'evita arrastrar 6 paràmetres a `rasterizeEmitter`.
|
||||||
|
struct EmitterRings {
|
||||||
|
int radius_sq{0};
|
||||||
|
int mid_sq{0};
|
||||||
|
int inner_sq{0};
|
||||||
|
std::uint8_t inner{0};
|
||||||
|
std::uint8_t mid{0};
|
||||||
|
std::uint8_t outer{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
auto computeRings(int radius, const GradientConfig& cfg) -> EmitterRings {
|
||||||
|
const auto R_F = static_cast<float>(radius);
|
||||||
|
const float R_INNER = R_F * cfg.inner_ratio;
|
||||||
|
const float R_MID = R_F * cfg.mid_ratio;
|
||||||
|
return EmitterRings{
|
||||||
|
.radius_sq = radius * radius,
|
||||||
|
.mid_sq = static_cast<int>(R_MID * R_MID),
|
||||||
|
.inner_sq = static_cast<int>(R_INNER * R_INNER),
|
||||||
|
.inner = toByte(cfg.inner_intensity),
|
||||||
|
.mid = toByte(cfg.mid_intensity),
|
||||||
|
.outer = toByte(cfg.outer_intensity),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Itera la bounding box de l'emissor clipada al canvas i actualitza
|
||||||
|
// la fila de luminància corresponent amb `max(actual, ring)`. Es
|
||||||
|
// crida un cop per emissor, així extreure-la deixa `apply` simple.
|
||||||
|
void rasterizeEmitter(const Emitter& e, const EmitterRings& rings, std::uint8_t* lum, int width, int height) {
|
||||||
|
const int R = e.radius;
|
||||||
|
const int CX = static_cast<int>(e.cx);
|
||||||
|
const int CY = static_cast<int>(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::uint8_t* row = lum + (static_cast<std::ptrdiff_t>(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;
|
||||||
|
}
|
||||||
|
row[x] = std::max(intensity, row[x]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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).
|
||||||
|
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<std::ptrdiff_t>(y) * width);
|
||||||
|
const std::uint8_t* lrow = lum + (static_cast<std::ptrdiff_t>(y) * width);
|
||||||
|
for (int x = 0; x < width; ++x) {
|
||||||
|
const std::uint8_t L = lrow[x];
|
||||||
|
if (L == 255) { continue; } // píxel intacte.
|
||||||
|
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<std::uint8_t>((SR * L) / 255);
|
||||||
|
const auto G = static_cast<std::uint8_t>((SG * L) / 255);
|
||||||
|
const auto B = static_cast<std::uint8_t>((SB * L) / 255);
|
||||||
|
px[x] = (SRC & 0xFF000000U) |
|
||||||
|
(static_cast<std::uint32_t>(R) << 16) |
|
||||||
|
(static_cast<std::uint32_t>(G) << 8) |
|
||||||
|
static_cast<std::uint32_t>(B);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto config() -> const GradientConfig& {
|
||||||
|
return activeConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadConfig(const std::string& yaml_path) {
|
||||||
|
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||||
|
if (bytes.empty()) {
|
||||||
|
Log::warn("Lighting", "no s'ha pogut obrir {} — s'usaran els defaults compilats", yaml_path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const auto* begin = reinterpret_cast<const char*>(bytes.data());
|
||||||
|
const auto* end = begin + bytes.size();
|
||||||
|
auto yaml = fkyaml::node::deserialize(begin, end);
|
||||||
|
if (yaml.contains("gradient")) {
|
||||||
|
const auto& g = yaml["gradient"];
|
||||||
|
auto& cfg = activeConfig();
|
||||||
|
readField(g, "inner_ratio", cfg.inner_ratio);
|
||||||
|
readField(g, "mid_ratio", cfg.mid_ratio);
|
||||||
|
readField(g, "inner_intensity", cfg.inner_intensity);
|
||||||
|
readField(g, "mid_intensity", cfg.mid_intensity);
|
||||||
|
readField(g, "outer_intensity", cfg.outer_intensity);
|
||||||
|
}
|
||||||
|
if (yaml.contains("flicker")) {
|
||||||
|
const auto& f = yaml["flicker"];
|
||||||
|
auto& fc = activeFlicker();
|
||||||
|
readField(f, "wave_amplitude_px", fc.wave_amplitude_px);
|
||||||
|
readField(f, "wave_frequency_hz", fc.wave_frequency_hz);
|
||||||
|
readField(f, "noise_amplitude_px", fc.noise_amplitude_px);
|
||||||
|
readField(f, "noise_frequency_hz", fc.noise_frequency_hz);
|
||||||
|
}
|
||||||
|
} catch (const std::exception& ex) {
|
||||||
|
Log::warn("Lighting", "error parsejant {}: {} — s'usaran els defaults compilats", yaml_path, ex.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply(std::uint32_t* canvas, int width, int height, float room_light, std::span<const Emitter> 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.
|
||||||
|
const std::uint8_t ROOM = toByte(room_light);
|
||||||
|
if (ROOM == 255 && emitters.empty()) { return; }
|
||||||
|
|
||||||
|
auto& lum = luminanceBuffer();
|
||||||
|
const std::size_t TOTAL = static_cast<std::size_t>(width) * static_cast<std::size_t>(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<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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
modulateCanvas(canvas, lum.data(), width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Lighting
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// Forma 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.
|
||||||
|
namespace Lighting {
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
struct Emitter {
|
||||||
|
float cx{0.0F};
|
||||||
|
float cy{0.0F};
|
||||||
|
int radius{0};
|
||||||
|
int seed{0};
|
||||||
|
bool flicker{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Llegeix `yaml_path` (data/config/lighting.yaml) i pobla la
|
||||||
|
// configuració global. Qualsevol clau absent cau als Defaults::Lighting.
|
||||||
|
// No es crida més d'una vegada en un run normal; tornar-la a cridar
|
||||||
|
// simplement reemplaça la configuració activa.
|
||||||
|
void loadConfig(const std::string& yaml_path);
|
||||||
|
|
||||||
|
// Configuració activa (per a debug/inspectors). Inicialitzada amb els
|
||||||
|
// 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<const Emitter> emitters);
|
||||||
|
|
||||||
|
} // namespace Lighting
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
#include "core/input/mouse.hpp"
|
#include "core/input/mouse.hpp"
|
||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
#include "core/rendering/effects/shadow.hpp"
|
#include "core/rendering/effects/shadow.hpp"
|
||||||
|
#include "core/rendering/lighting.hpp"
|
||||||
#include "core/rendering/notifier.hpp"
|
#include "core/rendering/notifier.hpp"
|
||||||
#include "core/rendering/render_info.hpp"
|
#include "core/rendering/render_info.hpp"
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
@@ -131,6 +132,10 @@ Director::Director() {
|
|||||||
// Tema d'UI (colors, timings, fonts, flags). Qualsevol clau absent a
|
// Tema d'UI (colors, timings, fonts, flags). Qualsevol clau absent a
|
||||||
// data/config/ui.yaml cau al valor compilat a Defaults::UI.
|
// data/config/ui.yaml cau al valor compilat a Defaults::UI.
|
||||||
UiConfig::get().load(Defaults::Paths::UI_YAML);
|
UiConfig::get().load(Defaults::Paths::UI_YAML);
|
||||||
|
// Forma del gradient d'il·luminació (3 anells radials). El radi
|
||||||
|
// per emissor i el `light_level` per sala viuen als YAMLs propis;
|
||||||
|
// ací només la corba compartida per a tots els emissors.
|
||||||
|
Lighting::loadConfig(Defaults::Paths::LIGHTING_YAML);
|
||||||
// Configuració específica de les escenes TITLE i LOGO. Es carreguen
|
// Configuració específica de les escenes TITLE i LOGO. Es carreguen
|
||||||
// additivament sobre el mateix UiConfig: cada fitxer només conté la
|
// additivament sobre el mateix UiConfig: cada fitxer només conté la
|
||||||
// seua subclau (`title:` o `logo:`), i les claus absents a la base
|
// seua subclau (`title:` o `logo:`), i les claus absents a la base
|
||||||
|
|||||||
@@ -63,6 +63,16 @@ auto Entity::getCollisionRect() const -> SDL_FRect {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto Entity::getLightCenter() const -> SDL_FPoint {
|
||||||
|
// Centre del rect de render. Per a entitats que es desplacen
|
||||||
|
// visualment sobre la seva posició lògica (p.ex. el Player saltant),
|
||||||
|
// la subclasse override aquest mètode per restar el desplaçament.
|
||||||
|
return {
|
||||||
|
.x = pixel_rect_.x + (pixel_rect_.w * 0.5F),
|
||||||
|
.y = pixel_rect_.y + (pixel_rect_.h * 0.5F),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
auto Entity::getFeetY() const -> float {
|
auto Entity::getFeetY() const -> float {
|
||||||
// Decal de sòl: sentinel mínim per quedar al principi del Y-sort
|
// Decal de sòl: sentinel mínim per quedar al principi del Y-sort
|
||||||
// ascendent de `GameScene` (stable_sort) i així pintar-se ABANS de
|
// ascendent de `GameScene` (stable_sort) i així pintar-se ABANS de
|
||||||
|
|||||||
@@ -253,6 +253,34 @@ class Entity {
|
|||||||
[[nodiscard]] auto getFlip() const -> SDL_FlipMode { return flip_; }
|
[[nodiscard]] auto getFlip() const -> SDL_FlipMode { return flip_; }
|
||||||
void setFlip(SDL_FlipMode flip) { flip_ = flip; }
|
void setFlip(SDL_FlipMode flip) { flip_ = flip; }
|
||||||
|
|
||||||
|
// Radi de la llum emesa per aquesta entitat (en píxels del canvas).
|
||||||
|
// 0 = no emet (cas habitual). El bloc opcional `light: { radius: N }`
|
||||||
|
// del YAML de la plantilla el pobla via `EntityManager::buildEntity`.
|
||||||
|
[[nodiscard]] auto getLightRadius() const -> int { return light_radius_; }
|
||||||
|
void setLightRadius(int r) { light_radius_ = r; }
|
||||||
|
|
||||||
|
// Activa la modulació "tipus foc" del radi a la pasada d'il·luminació
|
||||||
|
// (per a antorxes, fogueres, etc.). El bloc opcional
|
||||||
|
// `light: { flicker: true }` del YAML el pobla. Per defecte false:
|
||||||
|
// la llum queda perfectament estable (cas player, mòmia).
|
||||||
|
[[nodiscard]] auto getLightFlicker() const -> bool { return light_flicker_; }
|
||||||
|
void setLightFlicker(bool f) { light_flicker_ = f; }
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// mou. Si l'entitat respawneja, la nova instància tindrà una seed
|
||||||
|
// nova; com que el cicle visual ja queda trencat per la mort, no és
|
||||||
|
// un problema.
|
||||||
|
[[nodiscard]] auto getLightSeed() const -> int { return light_seed_; }
|
||||||
|
|
||||||
|
// Centre de la llum en píxels del canvas. Per defecte és el centre del
|
||||||
|
// `pixel_rect_` (que és la caixa de render: sprite top-left + mida).
|
||||||
|
// Virtual perquè entitats amb desplaçament visual sobre la posició
|
||||||
|
// lògica (p.ex. el Player en l'aire, que rebaixa visualment `pos_.y`
|
||||||
|
// amb `jump_z_`) puguin retornar el centre que realment es veu.
|
||||||
|
[[nodiscard]] virtual auto getLightCenter() const -> SDL_FPoint;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::string name_;
|
std::string name_;
|
||||||
TilePos tile_{};
|
TilePos tile_{};
|
||||||
@@ -266,6 +294,14 @@ class Entity {
|
|||||||
bool is_floor_decal_{false};
|
bool is_floor_decal_{false};
|
||||||
bool blocks_movement_{false};
|
bool blocks_movement_{false};
|
||||||
int height_{0};
|
int height_{0};
|
||||||
|
int light_radius_{0};
|
||||||
|
// Comptador global d'instàncies; cada Entity nova s'emporta el
|
||||||
|
// valor incrementat com a seed propi. Estàtic perquè no calgui
|
||||||
|
// estat extern (sense risc de threads: les entitats es construeixen
|
||||||
|
// sempre des del fil del joc).
|
||||||
|
static inline int s_next_light_seed{0};
|
||||||
|
int light_seed_{++s_next_light_seed};
|
||||||
|
bool light_flicker_{false};
|
||||||
bool dead_{false};
|
bool dead_{false};
|
||||||
SDL_FlipMode flip_{SDL_FLIP_NONE};
|
SDL_FlipMode flip_{SDL_FLIP_NONE};
|
||||||
EventBus* bus_{nullptr};
|
EventBus* bus_{nullptr};
|
||||||
|
|||||||
@@ -155,6 +155,25 @@ namespace {
|
|||||||
return c["height"].get_value<int>();
|
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.
|
||||||
|
struct LightTemplate {
|
||||||
|
int radius{0};
|
||||||
|
bool flicker{false};
|
||||||
|
};
|
||||||
|
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>(); }
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// Bloc `collision: { size: [w, h], base_size: [bw, bh]?, offset: [x, y]? }`
|
// Bloc `collision: { size: [w, h], base_size: [bw, bh]?, offset: [x, y]? }`
|
||||||
// de la plantilla d'entitat.
|
// de la plantilla d'entitat.
|
||||||
// - Sense `base_size` ni `offset`: la caja queda alineada a la vora
|
// - Sense `base_size` ni `offset`: la caja queda alineada a la vora
|
||||||
@@ -277,6 +296,9 @@ namespace {
|
|||||||
entity->setCollisionBox(*BOX);
|
entity->setCollisionBox(*BOX);
|
||||||
}
|
}
|
||||||
entity->setHeight(readHeight(tmpl));
|
entity->setHeight(readHeight(tmpl));
|
||||||
|
const auto LIGHT = readLight(tmpl);
|
||||||
|
entity->setLightRadius(LIGHT.radius);
|
||||||
|
entity->setLightFlicker(LIGHT.flicker);
|
||||||
anchor_out = readAnchor(tmpl);
|
anchor_out = readAnchor(tmpl);
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,19 @@ void Player::loadFrom(const std::string& yaml_path) {
|
|||||||
// `duration_ms` és 0 (o el bloc manca) la mecànica queda
|
// `duration_ms` és 0 (o el bloc manca) la mecànica queda
|
||||||
// desactivada i els hazards toquen al primer frame, com abans.
|
// desactivada i els hazards toquen al primer frame, com abans.
|
||||||
readImmunityBlock(yaml);
|
readImmunityBlock(yaml);
|
||||||
|
|
||||||
|
// Bloc opcional `light:` — radi i flicker. Extret a un mètode per
|
||||||
|
// mantenir `loadFrom` sota el llindar de complexitat de clang-tidy.
|
||||||
|
readLightBlock(yaml);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Player::readLightBlock(const fkyaml::node& yaml) {
|
||||||
|
light_radius = 0;
|
||||||
|
light_flicker = false;
|
||||||
|
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>(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path) {
|
void Player::readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path) {
|
||||||
@@ -1462,6 +1475,16 @@ void Player::render() {
|
|||||||
if (CLIP_HOLE) { Rendering::popClip(); }
|
if (CLIP_HOLE) { Rendering::popClip(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto Player::getLightCenter() const -> SDL_FPoint {
|
||||||
|
// Sprite top-left = (pos_.x, pos_.y). El render desplaça verticalment
|
||||||
|
// `jump_z_` píxels durant el salt; aplicar el mateix offset aquí fa
|
||||||
|
// que la llum "salti" amb el jugador en lloc de quedar-se al sòl.
|
||||||
|
return {
|
||||||
|
.x = getPosX() + (static_cast<float>(sprite_w) * 0.5F),
|
||||||
|
.y = getPosY() - jump_z_ + (static_cast<float>(sprite_h) * 0.5F),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
void Player::renderShadow() const {
|
void Player::renderShadow() const {
|
||||||
// Sense salt actiu, no hi ha sombra: això simplifica els callsites,
|
// Sense salt actiu, no hi ha sombra: això simplifica els callsites,
|
||||||
// que poden cridar `renderShadow` per a tots els jugadors sense
|
// que poden cridar `renderShadow` per a tots els jugadors sense
|
||||||
|
|||||||
@@ -124,6 +124,14 @@ class Player : public AnimatedSprite {
|
|||||||
// creua mentre `jump_z_` és superior a aquest valor, no col·lisionem.
|
// creua mentre `jump_z_` és superior a aquest valor, no col·lisionem.
|
||||||
inline static int body_height = 0;
|
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.
|
||||||
|
inline static int light_radius = 0;
|
||||||
|
inline static bool light_flicker = false;
|
||||||
|
|
||||||
// Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor.
|
// Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor.
|
||||||
static void loadFrom(const std::string& yaml_path);
|
static void loadFrom(const std::string& yaml_path);
|
||||||
|
|
||||||
@@ -132,6 +140,19 @@ class Player : public AnimatedSprite {
|
|||||||
// marcador i `Player::resetForJoin` per a re-entrades mid-partida.
|
// marcador i `Player::resetForJoin` per a re-entrades mid-partida.
|
||||||
[[nodiscard]] static auto getInitialLives() -> int { return death_config.initial_lives; }
|
[[nodiscard]] static auto getInitialLives() -> int { return death_config.initial_lives; }
|
||||||
|
|
||||||
|
// Emissió de llum (mateix paper que `Entity::getLightRadius` per a
|
||||||
|
// les entitats). El centre puja amb el salt: durant `jump_z_>0` el
|
||||||
|
// sprite es dibuixa amb `pos_.y - jump_z_`, i la llum ha de seguir-lo
|
||||||
|
// 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; }
|
||||||
|
// 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
|
||||||
|
// pròpies que no s'encavalquen amb cap entitat.
|
||||||
|
[[nodiscard]] auto getLightSeed() const -> int { return id_ + 1000; }
|
||||||
|
[[nodiscard]] auto getLightCenter() const -> SDL_FPoint;
|
||||||
|
|
||||||
Player(const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1);
|
Player(const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1);
|
||||||
|
|
||||||
// Update en una sola crida: manté l'API anterior per als tests i
|
// Update en una sola crida: manté l'API anterior per als tests i
|
||||||
@@ -742,6 +763,11 @@ class Player : public AnimatedSprite {
|
|||||||
// readers — `loadFrom` el crida al final.
|
// readers — `loadFrom` el crida al final.
|
||||||
static void readImmunityBlock(const fkyaml::node& yaml);
|
static void readImmunityBlock(const fkyaml::node& yaml);
|
||||||
|
|
||||||
|
// Llig el bloc opcional `light: { radius: N, flicker: bool }` del
|
||||||
|
// YAML i pobla `light_radius` / `light_flicker`. Si manca, els
|
||||||
|
// valors queden a 0 / false (jugador sense llum).
|
||||||
|
static void readLightBlock(const fkyaml::node& yaml);
|
||||||
|
|
||||||
// Activa l'estat ATTACKING en la direcció cardinal indicada (la
|
// Activa l'estat ATTACKING en la direcció cardinal indicada (la
|
||||||
// direcció cap on mira el jugador) i publica `whip_spawn` al bus
|
// direcció cap on mira el jugador) i publica `whip_spawn` al bus
|
||||||
// perquè el GameScene cree el `WhipAttack` corresponent. Posa el
|
// perquè el GameScene cree el `WhipAttack` corresponent. Posa el
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "utils/defaults.hpp"
|
||||||
#include "utils/log.hpp"
|
#include "utils/log.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -102,6 +103,10 @@ namespace Map {
|
|||||||
// canviarà el flag a true quan transiti a OPEN.
|
// canviarà el flag a true quan transiti a OPEN.
|
||||||
door_open = false;
|
door_open = false;
|
||||||
|
|
||||||
|
// Llum base de la sala. La clau és opcional a l'arrel del YAML;
|
||||||
|
// per defecte 1.0 (sala totalment il·luminada, comportament hui).
|
||||||
|
light_level = yaml.contains("light_level") ? yaml["light_level"].get_value<float>() : Defaults::Lighting::ROOM_LIGHT_DEFAULT;
|
||||||
|
|
||||||
Log::info("Map", "geometria {}x{} sarcs {}x{} pitch {}x{} door_col {} des de {}", grid_cols, grid_rows, sarc_w, sarc_h, pitch_x, pitch_y, door_col, yaml_path);
|
Log::info("Map", "geometria {}x{} sarcs {}x{} pitch {}x{} door_col {} des de {}", grid_cols, grid_rows, sarc_w, sarc_h, pitch_x, pitch_y, door_col, yaml_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,13 @@ namespace Map {
|
|||||||
inline int back_wall_h = 0;
|
inline int back_wall_h = 0;
|
||||||
inline int side_wall_w = 0;
|
inline int side_wall_w = 0;
|
||||||
|
|
||||||
|
// Nivell d'il·luminació base de la sala. 1.0 = totalment il·luminada
|
||||||
|
// (com hui, sense efecte visual); 0.0 = completament negra fora del
|
||||||
|
// radi d'un emissor. La clau `light_level:` al YAML de la sala el
|
||||||
|
// sobreescriu — si manca, queda al default 1.0 i la pasada
|
||||||
|
// d'il·luminació del frame es salta com a no-op.
|
||||||
|
inline float light_level = 1.0F;
|
||||||
|
|
||||||
// Derivats de la geometria base: el pas entre sarcòfags ha de cabre
|
// Derivats de la geometria base: el pas entre sarcòfags ha de cabre
|
||||||
// exactament el jugador, i el mateix passadís s'usa a l'exterior.
|
// exactament el jugador, i el mateix passadís s'usa a l'exterior.
|
||||||
inline int corridor_w = 0;
|
inline int corridor_w = 0;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "core/input/global_inputs.hpp"
|
#include "core/input/global_inputs.hpp"
|
||||||
#include "core/input/key_config.hpp"
|
#include "core/input/key_config.hpp"
|
||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
|
#include "core/rendering/lighting.hpp"
|
||||||
#include "core/rendering/notifier.hpp"
|
#include "core/rendering/notifier.hpp"
|
||||||
#include "core/rendering/render_info.hpp"
|
#include "core/rendering/render_info.hpp"
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
@@ -1005,6 +1006,37 @@ void GameScene::updateRoomTransition(int delta_ms) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameScene::applyLightingPass() {
|
||||||
|
// Atajo: si la sala està totalment il·luminada i ningú emet,
|
||||||
|
// `Lighting::apply` ja s'autosalta amb les mateixes condicions.
|
||||||
|
// Recollim igualment perquè el cas comú durant el desenvolupament
|
||||||
|
// serà tocar `Map::light_level` per sales concretes.
|
||||||
|
std::vector<Lighting::Emitter> emitters;
|
||||||
|
emitters.reserve(players_.size() + entity_manager_.entityCount());
|
||||||
|
const int PLAYER_R = Player::getLightRadius();
|
||||||
|
// Quan el YAML del jugador no declara `light:`, `PLAYER_R` és 0 i
|
||||||
|
// `Lighting::apply` salta el cercle al filtre intern. No fem un
|
||||||
|
// 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();
|
||||||
|
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});
|
||||||
|
}
|
||||||
|
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()});
|
||||||
|
}
|
||||||
|
Lighting::apply(Screen::get().getCanvasArgb(),
|
||||||
|
Screen::get().getCanvasWidth(),
|
||||||
|
Screen::get().getCanvasHeight(),
|
||||||
|
Map::light_level,
|
||||||
|
emitters);
|
||||||
|
}
|
||||||
|
|
||||||
void GameScene::renderRoomFade() {
|
void GameScene::renderRoomFade() {
|
||||||
// Delegació als FadeToBlack: les durades, la direcció i el progrés
|
// Delegació als FadeToBlack: les durades, la direcció i el progrés
|
||||||
// viuen dins de cada instància. Comportament:
|
// viuen dins de cada instància. Comportament:
|
||||||
@@ -1708,6 +1740,12 @@ void GameScene::render() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pasada d'il·luminació: oscureix el canvas segons `Map::light_level`
|
||||||
|
// i hi obre cercles brillants al voltant dels emissors (jugadors,
|
||||||
|
// antorxes, mòmies, ...). Va abans del fade i del HUD perquè el
|
||||||
|
// fade pinta negre encima i la UI ha de quedar llegible.
|
||||||
|
applyLightingPass();
|
||||||
|
|
||||||
// Fade del món: pintat ENTRE actors i HUD perquè el marcador, les
|
// Fade del món: pintat ENTRE actors i HUD perquè el marcador, les
|
||||||
// notificacions i el service menu queden per damunt sense afectar-se.
|
// notificacions i el service menu queden per damunt sense afectar-se.
|
||||||
// Inactiu si l'estat és PLAYING.
|
// Inactiu si l'estat és PLAYING.
|
||||||
|
|||||||
@@ -271,6 +271,13 @@ class GameScene : public Scene {
|
|||||||
void renderRoomFade();
|
void renderRoomFade();
|
||||||
void renderRoomText();
|
void renderRoomText();
|
||||||
|
|
||||||
|
// Pasada d'il·luminació CPU sobre el canvas, entre els actors i el
|
||||||
|
// HUD: recull els emissors actius (jugadors + entitats amb
|
||||||
|
// `light_radius>0`) i aplica el gradient amb `Map::light_level`
|
||||||
|
// com a llum de sala. El HUD/notificacions queden fora de la
|
||||||
|
// multiplicació perquè es pinten després.
|
||||||
|
void applyLightingPass();
|
||||||
|
|
||||||
// Configuració, estat i transició de la sub-FSM de game over
|
// Configuració, estat i transició de la sub-FSM de game over
|
||||||
// (CONTINUE? + GAME OVER). Lligada al mateix `game.yaml` que la
|
// (CONTINUE? + GAME OVER). Lligada al mateix `game.yaml` que la
|
||||||
// RoomFsm via `GameOverFsm::loadConfig`.
|
// RoomFsm via `GameOverFsm::loadConfig`.
|
||||||
|
|||||||
@@ -106,6 +106,28 @@ namespace Defaults {
|
|||||||
constexpr float PUSH_FACTOR = 0.30F;
|
constexpr float PUSH_FACTOR = 0.30F;
|
||||||
} // namespace Player
|
} // namespace Player
|
||||||
|
|
||||||
|
// Fallbacks del sistema d'il·luminació. El gradient en viu viu a
|
||||||
|
// data/config/lighting.yaml; aquests valors només s'apliquen si el
|
||||||
|
// fitxer falta o li manca alguna clau. La forma final (3 anells) es
|
||||||
|
// descriu al YAML; veure `Lighting::GradientConfig` per a la semàntica.
|
||||||
|
namespace Lighting {
|
||||||
|
constexpr float INNER_RATIO = 0.8F;
|
||||||
|
constexpr float MID_RATIO = 0.9F;
|
||||||
|
constexpr float INNER_INTENSITY = 1.0F;
|
||||||
|
constexpr float MID_INTENSITY = 0.9F;
|
||||||
|
constexpr float OUTER_INTENSITY = 0.8F;
|
||||||
|
// Nivell d'il·luminació de sala per defecte. Una sala sense la
|
||||||
|
// clau `light_level` queda totalment il·luminada (com hui).
|
||||||
|
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;
|
||||||
|
constexpr float FLICKER_WAVE_FREQUENCY_HZ = 3.0F;
|
||||||
|
constexpr float FLICKER_NOISE_AMPLITUDE_PX = 1.0F;
|
||||||
|
constexpr float FLICKER_NOISE_FREQUENCY_HZ = 7.0F;
|
||||||
|
} // namespace Lighting
|
||||||
|
|
||||||
// Fallbacks del tema d'UI: índexs de paleta, timings (ms) i fonts.
|
// Fallbacks del tema d'UI: índexs de paleta, timings (ms) i fonts.
|
||||||
// El tema en viu és a data/config/ui.yaml, carregat per UiConfig a
|
// El tema en viu és a data/config/ui.yaml, carregat per UiConfig a
|
||||||
// l'arrencada; aquests valors només s'apliquen si el fitxer falta o
|
// l'arrencada; aquests valors només s'apliquen si el fitxer falta o
|
||||||
@@ -498,6 +520,7 @@ namespace Defaults {
|
|||||||
constexpr const char* SOUNDS_YAML = "data/config/sounds.yaml";
|
constexpr const char* SOUNDS_YAML = "data/config/sounds.yaml";
|
||||||
constexpr const char* INPUT_YAML = "data/config/input.yaml";
|
constexpr const char* INPUT_YAML = "data/config/input.yaml";
|
||||||
constexpr const char* EDITOR_YAML = "data/config/editor.yaml";
|
constexpr const char* EDITOR_YAML = "data/config/editor.yaml";
|
||||||
|
constexpr const char* LIGHTING_YAML = "data/config/lighting.yaml";
|
||||||
constexpr const char* UI_PALETTE = "data/palettes/uzebox.pal";
|
constexpr const char* UI_PALETTE = "data/palettes/uzebox.pal";
|
||||||
|
|
||||||
// Configuració d'escenes (yaml). El de la GameScene viu a
|
// Configuració d'escenes (yaml). El de la GameScene viu a
|
||||||
|
|||||||
Reference in new issue
Block a user