Compare commits

...
10 Commits
38 changed files with 1259 additions and 406 deletions

No files matched your search

+29 -9
View File
@@ -12,8 +12,13 @@
# mid_ratio..1.0 → outer_intensity (anell exterior) # mid_ratio..1.0 → outer_intensity (anell exterior)
# fora del radi → light_level de la sala # fora del radi → light_level de la sala
# #
# Les intensitats són factors 0..1 que es multipliquen sobre el píxel # Hi ha dues corbes paral·leles, una per mode, perquè la mateixa escala
# final del canvas. inner=1.0 vol dir "píxel sense alterar". # 0..1 vol dir coses molt diferents:
# `gradient` → subtractive: 1.0 = "no fosqueix" el píxel
# (l'emissor "trenca" una sala fosca).
# `gradient_additive` → additive: 1.0 = "afegeix el color sencer"
# (satura). Valors molt més baixos donen un
# halo cromàtic subtil sobre una sala normal.
gradient: gradient:
inner_ratio: 0.5 inner_ratio: 0.5
mid_ratio: 0.85 mid_ratio: 0.85
@@ -21,16 +26,31 @@ gradient:
mid_intensity: 0.8 mid_intensity: 0.8
outer_intensity: 0.6 outer_intensity: 0.6
# Corba del mode additive. Valors petits a propòsit: només volem un
# toc de tint cromàtic prop de l'emissor — fora del nucli, la sala
# original ha de quedar pràcticament intacta. Si pugeu massa l'inner,
# els colors de l'emissor "inunden" la sala (additiu satura ràpid).
gradient_additive:
inner_ratio: 0.5
mid_ratio: 0.85
inner_intensity: 0.15
mid_intensity: 0.10
outer_intensity: 0.05
# Modulació del radi quan una entitat declara `flicker: true` al seu # Modulació del radi quan una entitat declara `flicker: true` al seu
# bloc `light:`. El radi efectiu per frame és: # bloc `light:`. El radi efectiu per frame és:
# r_eff = r_base + A1·sin(2π·f1·t + φ1) + A2·sin(2π·f2·t + φ2) # r_eff = r_base + (r_amp1·r_base)·sin(2π·f1·t + φ1) + (r_amp2·r_base)·sin(2π·f2·t + φ2)
# on (φ1, φ2) es deriven de la posició de l'emissor per a que cada # on (φ1, φ2) es deriven de la seed de l'emissor per a que cada
# antorxa balancegi en una fase diferent (sense estat per emissor). # antorxa balancegi en una fase diferent (sense estat per emissor).
# Combinant dues sinusoides amb freqüències incommensurables s'obté # Les amplituds són **ràtios** sobre el radi base: així el feel del
# un patró "fluid" que el ull llegeix com a foc; pujar `noise_*` # flicker és independent del radi (una torxa de 24 px oscil·la el
# aporta més inquietud, baixar-ho fa la flama més serena. # mateix % que una de 40 px). Combinant dues sinusoides amb freqüències
# incommensurables s'obté un patró "fluid" que el ull llegeix com a
# foc; pujar `noise_*` aporta més inquietud, baixar-ho fa la flama
# més serena. Aquests són els defaults globals; cada tipus d'entitat
# (o cada spawn / sala) pot sobreescriure'ls al seu `light.flicker_params:`.
flicker: flicker:
wave_amplitude_px: 2 wave_amplitude_ratio: 0.05 # 5% del radi (per a radi 40 → ±2 px, feel actual)
wave_frequency_hz: 3.0 wave_frequency_hz: 3.0
noise_amplitude_px: 1 noise_amplitude_ratio: 0.025 # 2.5% (per a radi 40 → ±1 px, feel actual)
noise_frequency_hz: 7.0 noise_frequency_hz: 7.0
File renamed without changes.
+2 -2
View File
@@ -4,7 +4,7 @@ editable: true
# Sub-picker de frames a l'editor: 6 variants (atlas 2×3 de sub-sprites 16×32). # Sub-picker de frames a l'editor: 6 variants (atlas 2×3 de sub-sprites 16×32).
editable_variants: true editable_variants: true
sprite: sprite:
path: ../debris/column.gif path: column.gif
size: [16, 32] size: [16, 32]
# Ancoratge "feet": el tile [col, row] del spawn és la base de la columna. # Ancoratge "feet": el tile [col, row] del spawn és la base de la columna.
# El sprite de 16×32 sobresurt 16 px cap amunt sense afectar col·lisió ni # El sprite de 16×32 sobresurt 16 px cap amunt sense afectar col·lisió ni
@@ -19,4 +19,4 @@ flags:
collides: true collides: true
interactable: false interactable: false
animations: animations:
default: { frames: [0], fps: 1, loop: false } default: { frames: [0], frame_ms: 1000, loop: false }
+3 -3
View File
@@ -21,9 +21,9 @@ flags:
collides: true collides: true
interactable: true interactable: true
animations: animations:
closed: { frames: [1], fps: 1, loop: true } closed: { frames: [1], frame_ms: 1000, loop: true }
opening: { frames: [0], fps: 6, loop: false } opening: { frames: [0], frame_ms: 167, loop: false }
open: { frames: [0], fps: 1, loop: true } open: { frames: [0], frame_ms: 1000, loop: true }
behaviour: behaviour:
states: [CLOSED, OPENING, OPEN] states: [CLOSED, OPENING, OPEN]
initial_state: CLOSED initial_state: CLOSED
+3 -3
View File
@@ -16,7 +16,7 @@ flags:
# apropiada via `setVariant` abans del primer tick. # apropiada via `setVariant` abans del primer tick.
# jump: frames 0..6 (7 frames) → reproduït al despegue. # jump: frames 0..6 (7 frames) → reproduït al despegue.
# land: frames 7..12 (6 frames) → reproduït a l'aterratge. # land: frames 7..12 (6 frames) → reproduït a l'aterratge.
# fps: 10 = 100 ms per frame. # frame_ms: 67 ≈ 15 fps (1000 / 67 ≈ 14.9 fps).
animations: animations:
jump: { frames: [0, 1, 2, 3, 4, 5, 6], fps: 15, loop: false } jump: { frames: [0, 1, 2, 3, 4, 5, 6], frame_ms: 67, loop: false }
land: { frames: [7, 8, 9, 10, 11, 12], fps: 15, loop: false } land: { frames: [7, 8, 9, 10, 11, 12], frame_ms: 67, loop: false }
+2 -2
View File
@@ -13,8 +13,8 @@ flags:
collides: false collides: false
interactable: false interactable: false
animations: animations:
inactive: { frames: [0], fps: 1, loop: true } inactive: { frames: [0], frame_ms: 1000, loop: true }
active: { frames: [0], fps: 6, loop: true } active: { frames: [0], frame_ms: 167, loop: true }
cycle: cycle:
active_duration: 0.3 active_duration: 0.3
inactive_duration: 2.0 inactive_duration: 2.0
+1 -1
View File
@@ -22,6 +22,6 @@ flags:
# lectura genèrica de flags. # lectura genèrica de flags.
triggers_on_overlap: true triggers_on_overlap: true
animations: animations:
idle: { frames: [0], fps: 1, loop: true } idle: { frames: [0], frame_ms: 1000, loop: true }
stats: stats:
damage: 0 damage: 0
+12 -5
View File
@@ -32,11 +32,18 @@ flags:
# no decideix flip per direcció (només selecciona quina animació mostrar). # no decideix flip per direcció (només selecciona quina animació mostrar).
# La momia no té variants `stand_*` perquè mai no para. # La momia no té variants `stand_*` perquè mai no para.
animations: animations:
walk_south: { frames: [0, 1, 2, 3], fps: 8, loop: true } walk_south: { frames: [0, 1, 2, 3], frame_ms: 125, loop: true }
walk_north: { frames: [4, 5, 6, 7], fps: 8, loop: true } walk_north: { frames: [4, 5, 6, 7], frame_ms: 125, loop: true }
walk_east: { frames: [8, 9, 10, 11], fps: 8, loop: true } walk_east: { frames: [8, 9, 10, 11], frame_ms: 125, loop: true }
walk_west: { frames: [8, 9, 10, 11], fps: 8, loop: true, flip_h: true } walk_west: { frames: [8, 9, 10, 11], frame_ms: 125, loop: true, flip_h: true }
raise: { frames: [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27], fps: 8 , loop: false } # Alçament: els 15 primers frames passen ràpid (125 ms cadascun) i el
# darrer aguanta 1 s perquè el jugador pugui veure la mòmia ja alçada
# abans que comenci a deambular. `frame_ms` accepta una llista per-frame
# que mapeja 1:1 contra `frames`.
raise:
frames: [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]
frame_ms: [125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 1000]
loop: false
ai: ai:
type: corridor_wanderer type: corridor_wanderer
direction_choice: random direction_choice: random
+1 -1
View File
@@ -13,7 +13,7 @@ flags:
collides: true collides: true
interactable: false interactable: false
animations: animations:
roll: { frames: [0], fps: 10, loop: true } roll: { frames: [0], frame_ms: 100, loop: true }
ai: ai:
type: straight_line type: straight_line
initial_direction: right initial_direction: right
+3 -3
View File
@@ -19,9 +19,9 @@ flags:
collides: true collides: true
interactable: true interactable: true
animations: animations:
closed: { frames: [0], fps: 1, loop: true } closed: { frames: [0], frame_ms: 1000, loop: true }
opening: { frames: [1, 2], fps: 6, loop: false } opening: { frames: [1, 2], frame_ms: 167, loop: false }
open: { frames: [3], fps: 1, loop: true } open: { frames: [3], frame_ms: 1000, loop: true }
behaviour: behaviour:
states: [CLOSED, OPENING, OPEN] states: [CLOSED, OPENING, OPEN]
initial_state: CLOSED initial_state: CLOSED
File renamed without changes.
+2 -2
View File
@@ -6,7 +6,7 @@ editable: true
# instància col·locada. # instància col·locada.
editable_variants: true editable_variants: true
sprite: sprite:
path: ../debris/stone.gif path: stone.gif
size: [16, 16] size: [16, 16]
tile_size: [1, 1] tile_size: [1, 1]
collision: collision:
@@ -19,4 +19,4 @@ flags:
# Animació "default" amb un sol frame: el spawn YAML pot sobreescriure-la # Animació "default" amb un sol frame: el spawn YAML pot sobreescriure-la
# via `frame: N` per fixar la variant escollida al editor. # via `frame: N` per fixar la variant escollida al editor.
animations: animations:
default: { frames: [0], fps: 1, loop: false } default: { frames: [0], frame_ms: 1000, loop: false }
+7 -3
View File
@@ -15,15 +15,19 @@ flags:
# El torch.gif és un strip vertical 16x64 amb 4 frames apilats. # El torch.gif és un strip vertical 16x64 amb 4 frames apilats.
# 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], frame_ms: 125, loop: true }
# Cercle de llum centrat a l'antorxa. El radi és en píxels del canvas; # 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 # el gradient global (data/config/lighting.yaml) hi posa la forma (3
# anells). Amb `flicker: true` el radi efectiu oscil·la per frame # anells). Amb `flicker: true` el radi efectiu oscil·la per frame
# (suma de dues sinusoides amb fase derivada de la posició) per a # (suma de dues sinusoides amb fase derivada de la posició) per a
# que el cercle "respiri" com una flama; els paràmetres del flicker # que el cercle "respiri" com una flama; els paràmetres del flicker
# també viuen al config global. Omet el bloc per fer una antorxa # també viuen al config global. `offset: [x, y]` desplaça el centre
# "apagada". # del cercle: la flama del sprite no està al centre del bounding box
# sinó cap a dalt, així que pujem la llum 4 px (Y creix cap avall)
# per a que el halo "neixi" de la flama, no del peu. Omet el bloc
# per fer una antorxa "apagada".
light: light:
radius: 40 radius: 40
flicker: true flicker: true
offset: [0, -4]
+2 -2
View File
@@ -13,8 +13,8 @@ flags:
collides: false collides: false
interactable: false interactable: false
animations: animations:
inactive: { frames: [0], fps: 1, loop: true } inactive: { frames: [0], frame_ms: 1000, loop: true }
active: { frames: [0], fps: 6, loop: true } active: { frames: [0], frame_ms: 167, loop: true }
cycle: cycle:
active_duration: 0.5 active_duration: 0.5
inactive_duration: 1.5 inactive_duration: 1.5
+36 -34
View File
@@ -10,16 +10,37 @@
# la cel·la buida (transparent), útil per caselles que ocuparà un actor. # la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 1 pyramid: 1
room: 1 room: 2
tileset: data/tilesets/pyramid_1.gif 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 # Banc de proves del mode `additive` d'il·luminació. La sala es veu
# (com hui, sense efecte visual); 0.0 = completament negra fora del radi # "normal" (level 1.0, el GIF intacte fora del cercle de les torxes)
# d'un emissor. El gradient dels cercles d'emissor viu a # i només les antorxes hi sumen un halo càlid suau. Player i mummies
# data/config/lighting.yaml. Omet aquesta clau per deixar la sala neta. # desactivats explícitament perquè aquí no volem cap halo al voltant
light_level: 0.25 # d'actors — el toc cromàtic és només l'antorxa. La corba additiva
# (intensitats baixes a propòsit perquè additive satura ràpid) viu a
# `gradient_additive:` de data/config/lighting.yaml.
lighting:
mode: additive
level: 1.0
emitters:
torches:
radius: 20
color: [255, 170, 60]
# Valors default del flicker explicitats per anar-los ajustant
# in-situ. Coincideixen amb els del global a data/config/lighting.yaml;
# baixar wave_frequency_hz fa el "palpitar" més lent, baixar les
# ratios fa l'oscil·lació més subtil. Qualsevol camp es pot
# eliminar i caurà al valor del global.
flicker_params:
wave_amplitude_ratio: 0.04
wave_frequency_hz: 3.0
noise_amplitude_ratio: 0.025
noise_frequency_hz: 3.0
players: false
mummies: false
# Í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
@@ -88,12 +109,16 @@ data:
# d'obertura (262/263). La momia al corredor central. L'antorxa al costat # d'obertura (262/263). La momia al corredor central. L'antorxa al costat
# superior esquerre per verificar render d'una entitat decorativa. # superior esquerre per verificar render d'una entitat decorativa.
entities: entities:
- type: torch
position: [4, 2]
- type: torch - type: torch
position: [8, 2] position: [8, 2]
- type: torch - type: torch
position: [12, 2] position: [12, 2]
- type: torch - type: torch
position: [16, 2] position: [16, 2]
- type: sarcophagus
sarc_position: [0, 0]
- type: sarcophagus - type: sarcophagus
sarc_position: [1, 0] sarc_position: [1, 0]
- type: sarcophagus - type: sarcophagus
@@ -120,6 +145,8 @@ entities:
sarc_position: [0, 3] sarc_position: [0, 3]
- type: sarcophagus - type: sarcophagus
sarc_position: [1, 3] sarc_position: [1, 3]
- type: sarcophagus
sarc_position: [2, 3]
- type: sarcophagus - type: sarcophagus
sarc_position: [3, 3] sarc_position: [3, 3]
- type: sarcophagus - type: sarcophagus
@@ -130,33 +157,8 @@ entities:
sarc_position: [2, 4] sarc_position: [2, 4]
- type: sarcophagus - type: sarcophagus
sarc_position: [3, 4] sarc_position: [3, 4]
- type: torch
position: [4, 2]
- type: door - type: door
- type: column
position: [3, 4]
frame: 2
- type: stone
position: [5, 4]
frame: 16
- type: stone
position: [4, 4]
frame: 19
- type: column
position: [13, 10]
frame: 4
- type: stone
position: [12, 10]
frame: 13
- type: stone
position: [11, 10]
frame: 23
- type: hole
position: [5, 9]
- type: hole
position: [9, 11]
- type: stone
position: [10, 10]
frame: 18
- type: mummy - type: mummy
position: [10, 13] position: [10, 7]
- type: mummy
position: [7, 9]
+56 -10
View File
@@ -10,11 +10,38 @@
# la cel·la buida (transparent), útil per caselles que ocuparà un actor. # la cel·la buida (transparent), útil per caselles que ocuparà un actor.
pyramid: 1 pyramid: 1
room: 2 room: 1
tileset: data/tilesets/pyramid_1.gif tileset: data/tilesets/pyramid_1.gif
tile_size: 16 tile_size: 16
origin: [-8, 0] origin: [-8, 0]
# Il·luminació de la sala. Bloc opcional; si manca, la sala queda
# totalment il·luminada (1.0) i tots els tipus d'entitat emeten amb els
# valors declarats al seu YAML de tipus (comportament històric).
#
# level → atenuació base de la sala (0..1). 1.0 = res
# d'efecte, 0.0 = negre fora del radi dels emissors.
# El gradient dels emissors viu a
# data/config/lighting.yaml.
# emitters.<tipus> → override per tipus d'entitat. Tres formes:
# false → apaga el tipus
# true → defaults del tipus
# { radius: N, flicker: B } → camps presents
# sobreescriuen el
# default; els
# absents cauen al
# default del tipus.
# Plural per llegir-se natural ("totes les momies de la sala"). Per a
# casos particulars, un spawn pot dur el seu propi `light:` (mateixes
# formes) que té prioritat sobre l'override de sala. La forma antiga
# (`light_level:` plà a l'arrel) també segueix sent vàlida per
# retrocompatibilitat.
lighting:
level: 0.15
emitters:
players: true
mummies: false
# Í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
@@ -82,16 +109,12 @@ data:
# d'obertura (262/263). La momia al corredor central. L'antorxa al costat # d'obertura (262/263). La momia al corredor central. L'antorxa al costat
# superior esquerre per verificar render d'una entitat decorativa. # superior esquerre per verificar render d'una entitat decorativa.
entities: entities:
- type: torch
position: [4, 2]
- type: torch - type: torch
position: [8, 2] position: [8, 2]
- type: torch - type: torch
position: [12, 2] position: [12, 2]
- type: torch - type: torch
position: [16, 2] position: [16, 2]
- type: sarcophagus
sarc_position: [0, 0]
- type: sarcophagus - type: sarcophagus
sarc_position: [1, 0] sarc_position: [1, 0]
- type: sarcophagus - type: sarcophagus
@@ -118,8 +141,6 @@ entities:
sarc_position: [0, 3] sarc_position: [0, 3]
- type: sarcophagus - type: sarcophagus
sarc_position: [1, 3] sarc_position: [1, 3]
- type: sarcophagus
sarc_position: [2, 3]
- type: sarcophagus - type: sarcophagus
sarc_position: [3, 3] sarc_position: [3, 3]
- type: sarcophagus - type: sarcophagus
@@ -130,8 +151,33 @@ entities:
sarc_position: [2, 4] sarc_position: [2, 4]
- type: sarcophagus - type: sarcophagus
sarc_position: [3, 4] sarc_position: [3, 4]
- type: torch
position: [4, 2]
- type: door - type: door
- type: column
position: [3, 4]
frame: 2
- type: stone
position: [5, 4]
frame: 16
- type: stone
position: [4, 4]
frame: 19
- type: column
position: [13, 10]
frame: 4
- type: stone
position: [12, 10]
frame: 13
- type: stone
position: [11, 10]
frame: 23
- type: hole
position: [5, 9]
- type: hole
position: [9, 11]
- type: stone
position: [10, 10]
frame: 18
- type: mummy - type: mummy
position: [10, 7] position: [10, 13]
- type: mummy
position: [7, 9]
+188 -46
View File
@@ -26,9 +26,9 @@ namespace Lighting {
return buf_; return buf_;
} }
// Configuració global, inicialitzada amb els defaults compilats. // Corba activa del mode subtractive. loadConfig() la reemplaça
// loadConfig() la reemplaça si troba el fitxer. // si troba el bloc `gradient:` al YAML.
auto activeConfig() -> GradientConfig& { auto activeSubtractiveConfig() -> GradientConfig& {
static GradientConfig cfg_{ static GradientConfig cfg_{
.inner_ratio = Defaults::Lighting::INNER_RATIO, .inner_ratio = Defaults::Lighting::INNER_RATIO,
.mid_ratio = Defaults::Lighting::MID_RATIO, .mid_ratio = Defaults::Lighting::MID_RATIO,
@@ -39,11 +39,27 @@ namespace Lighting {
return cfg_; return cfg_;
} }
// Corba activa del mode additive. Defaults baixos a propòsit:
// en additive la intensitat és l'alfa del color afegit, així
// que valors petits (0.05..0.15) donen el "halo subtil" que
// sol voler-se. loadConfig() la reemplaça si troba el bloc
// `gradient_additive:` al YAML.
auto activeAdditiveConfig() -> GradientConfig& {
static GradientConfig cfg_{
.inner_ratio = Defaults::Lighting::INNER_RATIO_ADDITIVE,
.mid_ratio = Defaults::Lighting::MID_RATIO_ADDITIVE,
.inner_intensity = Defaults::Lighting::INNER_INTENSITY_ADDITIVE,
.mid_intensity = Defaults::Lighting::MID_INTENSITY_ADDITIVE,
.outer_intensity = Defaults::Lighting::OUTER_INTENSITY_ADDITIVE,
};
return cfg_;
}
auto activeFlicker() -> FlickerConfig& { auto activeFlicker() -> FlickerConfig& {
static FlickerConfig fc_{ 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, .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, .noise_frequency_hz = Defaults::Lighting::FLICKER_NOISE_FREQUENCY_HZ,
}; };
return fc_; return fc_;
@@ -54,13 +70,16 @@ namespace Lighting {
// identificador estable de l'emissor (no canvia amb la posició) — // identificador estable de l'emissor (no canvia amb la posició) —
// així el patró no "deriva" quan l'emissor es mou. El factor // així el patró no "deriva" quan l'emissor es mou. El factor
// ~φ (golden ratio) dispersa les fases per a seeds consecutives. // ~φ (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 { 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 TWO_PI = 2.0F * std::numbers::pi_v<float>;
constexpr float GOLDEN_FRACTION = 0.6180339F; constexpr float GOLDEN_FRACTION = 0.6180339F;
const float PHASE = static_cast<float>(seed) * GOLDEN_FRACTION; 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 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 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)); const int R = base_radius + static_cast<int>(std::lround(DELTA));
return std::max(1, R); return std::max(1, R);
} }
@@ -133,9 +152,9 @@ namespace Lighting {
} }
} }
// Pasada final: pixel = pixel * lum / 255 per canal. El canal alpha // Pasada final del mode subtractive: pixel = pixel * lum / 255
// es preserva (la resta del pipeline assumeix opacitat total al // per canal. El canal alpha es preserva (la resta del pipeline
// canvas; veure Rendering::OPAQUE_ALPHA). // assumeix opacitat total al canvas; veure Rendering::OPAQUE_ALPHA).
void modulateCanvas(std::uint32_t* canvas, const std::uint8_t* lum, int width, int height) { void modulateCanvas(std::uint32_t* canvas, const std::uint8_t* lum, int width, int height) {
for (int y = 0; y < height; ++y) { for (int y = 0; y < height; ++y) {
std::uint32_t* px = canvas + (static_cast<std::ptrdiff_t>(y) * width); std::uint32_t* px = canvas + (static_cast<std::ptrdiff_t>(y) * width);
@@ -158,12 +177,100 @@ 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<std::ptrdiff_t>(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<std::uint8_t>((SR * level) / 255);
const auto G = static_cast<std::uint8_t>((SG * level) / 255);
const auto B = static_cast<std::uint8_t>((SB * level) / 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);
}
}
}
// 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<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::uint32_t* row = canvas + (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;
}
// 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<int>((SRC >> 16) & 0xFF);
const int SG = static_cast<int>((SRC >> 8) & 0xFF);
const int SB = static_cast<int>(SRC & 0xFF);
const auto NR = static_cast<std::uint32_t>(std::min(255, SR + CR));
const auto NG = static_cast<std::uint32_t>(std::min(255, SG + CG));
const auto NB = static_cast<std::uint32_t>(std::min(255, SB + CB));
row[x] = (SRC & 0xFF000000U) | (NR << 16) | (NG << 8) | NB;
}
}
}
} // namespace } // namespace
auto config() -> const GradientConfig& { auto config() -> const GradientConfig& {
return activeConfig(); 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
// entre els blocs `gradient:` i `gradient_additive:`.
void readGradient(const fkyaml::node& g, GradientConfig& cfg) {
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);
}
} // namespace
void loadConfig(const std::string& yaml_path) { void loadConfig(const std::string& yaml_path) {
auto bytes = Resource::Helper::loadFile(yaml_path); auto bytes = Resource::Helper::loadFile(yaml_path);
if (bytes.empty()) { if (bytes.empty()) {
@@ -175,20 +282,17 @@ namespace Lighting {
const auto* end = begin + bytes.size(); const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end); auto yaml = fkyaml::node::deserialize(begin, end);
if (yaml.contains("gradient")) { if (yaml.contains("gradient")) {
const auto& g = yaml["gradient"]; readGradient(yaml["gradient"], activeSubtractiveConfig());
auto& cfg = activeConfig(); }
readField(g, "inner_ratio", cfg.inner_ratio); if (yaml.contains("gradient_additive")) {
readField(g, "mid_ratio", cfg.mid_ratio); readGradient(yaml["gradient_additive"], activeAdditiveConfig());
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")) { if (yaml.contains("flicker")) {
const auto& f = yaml["flicker"]; const auto& f = yaml["flicker"];
auto& fc = activeFlicker(); 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, "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); readField(f, "noise_frequency_hz", fc.noise_frequency_hz);
} }
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
@@ -196,39 +300,77 @@ namespace Lighting {
} }
} }
void apply(std::uint32_t* canvas, int width, int height, float room_light, std::span<const Emitter> 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, 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,
.flicker_params = e.flicker_params,
};
}
// 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<const Emitter> emitters) {
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 = activeSubtractiveConfig();
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, 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);
}
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<const Emitter> emitters) {
darkenCanvas(canvas, width, height, room);
const auto& cfg = activeAdditiveConfig();
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, 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);
}
}
} // namespace
void apply(std::uint32_t* canvas, int width, int height, Mode mode, float room_light, std::span<const Emitter> emitters) {
if (canvas == nullptr || width <= 0 || height <= 0) { return; } if (canvas == nullptr || width <= 0 || height <= 0) { return; }
// Atajo: si la sala està totalment il·luminada i no hi ha // Atajo per a tots dos modes: sala totalment "neutra" + cap
// emissors, la pasada seria un `pixel * 1.0` global — no cal // emissor → la pasada no canviaria res al canvas.
// tocar res. És el camí curt habitual mentre no posem // subtractive: room=1.0 i sense emissors → pixel * 1.0 global.
// `light_level` a les sales. // additive: room=1.0 i sense emissors → cap aportació.
const std::uint8_t ROOM = toByte(room_light); const std::uint8_t ROOM = toByte(room_light);
if (ROOM == 255 && emitters.empty()) { return; } if (ROOM == 255 && emitters.empty()) { return; }
auto& lum = luminanceBuffer(); if (mode == Mode::ADDITIVE) {
const std::size_t TOTAL = static_cast<std::size_t>(width) * static_cast<std::size_t>(height); applyAdditive(canvas, width, height, ROOM, emitters);
if (lum.size() != TOTAL) { lum.resize(TOTAL); } } else {
std::ranges::fill(lum, ROOM); applySubtractive(canvas, width, height, ROOM, emitters);
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 } // namespace Lighting
+102 -34
View File
@@ -7,36 +7,64 @@
// Sistema d'il·luminació CPU. Treballa sobre el mateix canvas ARGB del // 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 // joc (320×240) que tot el pipeline de render: després que GameScene
// haja pintat tilemap, paint layer, sombres i actors, una pasada final // 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 // modifica cada píxel del canvas en funció de la "llum de la sala" i
// derivat de la combinació de "llum de la sala" + cercles emissors al // dels cercles emissors al voltant de certes entitats.
// voltant de certes entitats.
// //
// Forma de cada emissor (radial, 3 anells configurables al fitxer // Dos modes seleccionables per la sala (`lighting.mode` al YAML):
// data/config/lighting.yaml; veure `GradientConfig`): //
// 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 // 0..inner_ratio del radi → inner_intensity
// inner_ratio..mid_ratio → mid_intensity // inner_ratio..mid_ratio → mid_intensity
// mid_ratio..1.0 → outer_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 // La corba viu en dos blocs separats al YAML perquè la mateixa escala
// llum de sala i de la intensitat aportada per cada emissor que el // 0..1 té significats molt diferents per mode:
// solapa — no se "sumen" intensitats. Coherent amb la idea de "centre // `gradient:` → corba del mode subtractive (1.0 = "no fosqueix")
// 1.0": el píxel que cau dins de qualsevol nucli no s'enfosqueix. // `gradient_additive:` → corba del mode additive (1.0 = "afegeix
// el color sencer i satura"). Es vol molt més
// baixa per a no inundar la sala de color.
// Cada mode llegeix la seva pròpia corba — no es comparteixen valors.
//
// 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 { namespace Lighting {
// Posició al canvas (en píxels, mateixa unitat que la resta del // Mode de composició. La sala el selecciona via `lighting.mode:
// motor) i radi efectiu de l'emissor. El centre és el del sprite // additive | subtractive` (default subtractive si manca, retrocompat).
// visible: és l'entitat qui sap on cau (veure `Entity::getLightCenter`). enum class Mode : std::uint8_t {
// `flicker` activa la modulació del radi per frame; `seed` és un SUBTRACTIVE,
// identificador estable per emissor (no canvia amb la posició) del ADDITIVE,
// qual es deriva la fase del flicker — així els emissors mòbils };
// mantenen un patró propi que no "deriva" pel moviment.
struct Emitter { // Color emès. Default blanc (sense modificació quan s'usa en mode
float cx{0.0F}; // additive). En mode subtractive s'ignora — la pasada multiplicativa
float cy{0.0F}; // només té un canal (intensitat). El YAML accepta `color: [r, g, b]`
int radius{0}; // a qualsevol nivell de la cadena de resolució (tipus, sala, spawn).
int seed{0}; struct Color {
bool flicker{false}; std::uint8_t r{255};
std::uint8_t g{255};
std::uint8_t b{255};
}; };
// Forma del gradient. Compartida per tots els emissors per evitar // Forma del gradient. Compartida per tots els emissors per evitar
@@ -53,30 +81,70 @@ namespace Lighting {
// forma final del radi és la suma d'una sinusoide "respiració" // forma final del radi és la suma d'una sinusoide "respiració"
// (wave_*) i una sinusoide secundària de freqüència més alta // (wave_*) i una sinusoide secundària de freqüència més alta
// (noise_*) que aporta la sensació d'inquietud. Les fases es // (noise_*) que aporta la sensació d'inquietud. Les fases es
// deriven de la posició de cada emissor — sense estat per emissor. // 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 { struct FlickerConfig {
float wave_amplitude_px{2.0F}; float wave_amplitude_ratio{0.05F};
float wave_frequency_hz{3.0F}; float wave_frequency_hz{3.0F};
float noise_amplitude_px{1.0F}; float noise_amplitude_ratio{0.025F};
float noise_frequency_hz{7.0F}; 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`).
// `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. `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{};
// 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 // Llegeix `yaml_path` (data/config/lighting.yaml) i pobla la
// configuració global. Qualsevol clau absent cau als Defaults::Lighting. // 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 // No es crida més d'una vegada en un run normal; tornar-la a cridar
// simplement reemplaça la configuració activa. // simplement reemplaça la configuració activa.
void loadConfig(const std::string& yaml_path); void loadConfig(const std::string& yaml_path);
// Configuració activa (per a debug/inspectors). Inicialitzada amb els // Configuració activa (per a debug/inspectors). Retorna sempre la
// corba subtractive — la corba additive viu en paral·lel al cpp i,
// si algun dia es necessita inspeccionar des de fora, caldrà afegir
// una variant `configAdditive()`. Inicialitzada amb els
// Defaults::Lighting fins que loadConfig() es crida. // Defaults::Lighting fins que loadConfig() es crida.
[[nodiscard]] auto config() -> const GradientConfig&; [[nodiscard]] auto config() -> const GradientConfig&;
// Aplica la pasada d'il·luminació al canvas in-place. `room_light` // Defaults globals del flicker (data/config/lighting.yaml, bloc
// és l'atenuació base de la sala (0..1, 1 = sense atenuació). El // `flicker:`). Els callers del construct path d'entitats el llegeixen
// span d'emissors pot ser buit — en aquest cas tota la sala queda // per inicialitzar la `FlickerConfig` de cada emissor, i aplicar a
// multiplicada per `room_light`. Aquesta funció no fa cap allocació // sobre els overrides per tipus/sala/spawn.
// si es crida amb la mateixa mida de canvas múltiples cops: manté [[nodiscard]] auto flickerConfig() -> const FlickerConfig&;
// un buffer intern reutilitzable.
void apply(std::uint32_t* canvas, int width, int height, float room_light, std::span<const Emitter> 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<const Emitter> emitters);
} // namespace Lighting } // namespace Lighting
+4 -4
View File
@@ -354,10 +354,10 @@ Director::~Director() {
void Director::createSystemFolder() { void Director::createSystemFolder() {
// Carpeta de configuració per plataforma: // Carpeta de configuració per plataforma:
// - Windows: %APPDATA%\jailgames\aee_arcade\ // - Windows: %APPDATA%\jailgames\aee_arcade
// - macOS: ~/Library/Application Support/jailgames/aee_arcade/ // - macOS: ~/Library/Application Support/jailgames/aee_arcade
// - Linux: $XDG_CONFIG_HOME/jailgames/aee_arcade/ (defecte // - Linux: $XDG_CONFIG_HOME/jailgames/aee_arcade (defecte
// ~/.config/jailgames/aee_arcade/) // ~/.config/jailgames/aee_arcade)
// No fem servir SDL_GetPrefPath: a Linux apunta a XDG_DATA_HOME // No fem servir SDL_GetPrefPath: a Linux apunta a XDG_DATA_HOME
// (~/.local/share), que és per a dades, no per a configuració. // (~/.local/share), que és per a dades, no per a configuració.
std::string base; std::string base;
+119 -7
View File
@@ -1,14 +1,87 @@
#include "game/entities/animation_component.hpp" #include "game/entities/animation_component.hpp"
#include <cmath> #include <cmath>
#include <stdexcept>
#include <utility> #include <utility>
AnimationComponent::AnimationComponent(std::vector<int> frames, float fps, bool loop) namespace {
: AnimationComponent(std::move(frames), fps, loop, false, false) {}
AnimationComponent::AnimationComponent(std::vector<int> frames, float fps, bool loop, bool flip_h, bool flip_v) // Quan el caller no especifica temps, considerem un frame "estàtic" de
// 1s. El valor és arbitrari: amb 1 frame el component mai no avança,
// així que el speed concret és inert. Amb N frames és el fallback que
// assumeix la factory (mateix valor que el default del format antic).
constexpr float K_DEFAULT_FRAME_SECONDS = 1.0F;
// Si el YAML declara temps <= 0 per a algun frame, el saturem a aquest
// mínim per a evitar bucles infinits a `tick` (que avancen frame
// mentre `accumulator >= speeds[i]`). 1 ms és prou petit per a no
// alterar mai una temporització real i prou gran per a no col·lapsar.
constexpr float K_MIN_FRAME_SECONDS = 0.001F;
auto sanitizeSpeed(float seconds) -> float {
return seconds > 0.0F ? seconds : K_MIN_FRAME_SECONDS;
}
// Expandeix una llista de temps a la mida de `frame_count`:
// - mida = N: 1:1.
// - mida < N: emplena la resta amb l'últim valor (o el default si
// la llista era buida).
// - mida > N: talla a N.
// Tots els valors es passen per `sanitizeSpeed`.
auto fitSpeeds(std::vector<float> speeds, std::size_t frame_count) -> std::vector<float> {
if (frame_count == 0) { return {}; }
if (speeds.empty()) {
return std::vector<float>(frame_count, K_DEFAULT_FRAME_SECONDS);
}
// cppcheck-suppress useStlAlgorithm
for (auto& s : speeds) { s = sanitizeSpeed(s); }
if (speeds.size() == frame_count) { return speeds; }
if (speeds.size() > frame_count) {
speeds.resize(frame_count);
return speeds;
}
const float TAIL = speeds.back();
speeds.resize(frame_count, TAIL);
return speeds;
}
// Llig `animations.<key>.frames` com a llista d'enters. Garanteix
// almenys 1 frame (fallback a [0]) per a poder construir sempre un
// component vàlid.
auto parseFrames(const fkyaml::node& anim) -> std::vector<int> {
std::vector<int> frames;
for (const auto& f : anim["frames"]) {
// cppcheck-suppress useStlAlgorithm
frames.push_back(f.get_value<int>());
}
if (frames.empty()) { frames.push_back(0); }
return frames;
}
// Llig `animations.<key>.frame_ms` (escalar o llista) i retorna els
// temps en segons. Si és escalar, retorna una llista uniforme de
// `frame_count` elements; si és llista, copia tal qual (l'ajust de
// mida el farà `fitSpeeds` al constructor).
auto parseFrameMs(const fkyaml::node& anim, std::size_t frame_count) -> std::vector<float> {
std::vector<float> speeds_secs;
const auto& fm = anim["frame_ms"];
if (fm.is_sequence()) {
for (const auto& v : fm) {
// cppcheck-suppress useStlAlgorithm
speeds_secs.push_back(static_cast<float>(v.get_value<int>()) / 1000.0F);
}
} else {
const float SECS = static_cast<float>(fm.get_value<int>()) / 1000.0F;
speeds_secs.assign(frame_count, SECS);
}
return speeds_secs;
}
} // namespace
AnimationComponent::AnimationComponent(std::vector<int> frames, std::vector<float> speeds, bool loop, bool flip_h, bool flip_v)
: frames_(std::move(frames)), : frames_(std::move(frames)),
fps_(fps > 0.0F ? fps : 1.0F), speeds_(fitSpeeds(std::move(speeds), frames_.size())),
loop_(loop), loop_(loop),
flip_h_(flip_h), flip_h_(flip_h),
flip_v_(flip_v) { flip_v_(flip_v) {
@@ -19,13 +92,52 @@ AnimationComponent::AnimationComponent(std::vector<int> frames, float fps, bool
if (frames_.size() <= 1 && !loop_) { completed_ = true; } if (frames_.size() <= 1 && !loop_) { completed_ = true; }
} }
auto AnimationComponent::uniform(std::vector<int> frames, float frame_ms, bool loop) -> AnimationComponent {
return uniform(std::move(frames), frame_ms, loop, false, false);
}
auto AnimationComponent::uniform(std::vector<int> frames, float frame_ms, bool loop, bool flip_h, bool flip_v) -> AnimationComponent {
const float SECS = sanitizeSpeed(frame_ms / 1000.0F);
std::vector<float> speeds(frames.size(), SECS);
return {std::move(frames), std::move(speeds), loop, flip_h, flip_v};
}
auto AnimationComponent::fromYaml(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
// Defaults conservadors per a quan el bloc `animations.<key>` no
// existeix: 1 frame loopant a 1s — visualment estàtic, semàntica neutra.
std::vector<int> frames = {0};
std::vector<float> speeds_secs; // buit ⇒ fitSpeeds emplenarà amb default
bool loop = true;
bool flip_h = false;
bool flip_v = false;
if (!yaml.contains("animations") || !yaml["animations"].contains(key)) {
return {std::move(frames), std::move(speeds_secs), loop, flip_h, flip_v};
}
const auto& anim = yaml["animations"][key];
// L'antic format `fps: N` ja no és suportat. Si apareix, peguem hard
// perquè la migració sigui visible (no silenciosa). El missatge inclou
// la clau d'animació per a facilitar localitzar el YAML problemàtic.
if (anim.contains("fps")) {
throw std::runtime_error("AnimationComponent: 'fps:' is no longer supported, use 'frame_ms:' instead (animation '" + key + "')");
}
if (anim.contains("frames")) { frames = parseFrames(anim); }
if (anim.contains("frame_ms")) { speeds_secs = parseFrameMs(anim, frames.size()); }
if (anim.contains("loop")) { loop = anim["loop"].get_value<bool>(); }
if (anim.contains("flip_h")) { flip_h = anim["flip_h"].get_value<bool>(); }
if (anim.contains("flip_v")) { flip_v = anim["flip_v"].get_value<bool>(); }
return {std::move(frames), std::move(speeds_secs), loop, flip_h, flip_v};
}
void AnimationComponent::tick(float delta_time) { void AnimationComponent::tick(float delta_time) {
if (frames_.size() <= 1 || completed_) { return; } if (frames_.size() <= 1 || completed_) { return; }
accumulator_ += delta_time; accumulator_ += delta_time;
const float FRAME_TIME = 1.0F / fps_; while (accumulator_ >= speeds_[static_cast<std::size_t>(current_)]) {
while (accumulator_ >= FRAME_TIME) { accumulator_ -= speeds_[static_cast<std::size_t>(current_)];
accumulator_ -= FRAME_TIME;
++current_; ++current_;
if (static_cast<std::size_t>(current_) >= frames_.size()) { if (static_cast<std::size_t>(current_) >= frames_.size()) {
if (loop_) { if (loop_) {
+37 -8
View File
@@ -3,8 +3,11 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <cstddef> #include <cstddef>
#include <string>
#include <vector> #include <vector>
#include "external/fkyaml_node.hpp"
// Component reutilitzable d'animació: qualsevol entitat que necessiti // Component reutilitzable d'animació: qualsevol entitat que necessiti
// reproduir una seqüència de frames l'instancia. Es manté com a camp // reproduir una seqüència de frames l'instancia. Es manté com a camp
// opcional dins d'Entity (veure `entity.hpp`) i NO hereta de cap classe // opcional dins d'Entity (veure `entity.hpp`) i NO hereta de cap classe
@@ -22,16 +25,41 @@
// canvien d'animació, sense decidir-lo per direcció. Així una mòmia que va // canvien d'animació, sense decidir-lo per direcció. Així una mòmia que va
// cap a l'oest reprodueix `walk_west` (frames de l'est + `flip_h: true`) // cap a l'oest reprodueix `walk_west` (frames de l'est + `flip_h: true`)
// sense que el codi de la mòmia toqui flip directament. // sense que el codi de la mòmia toqui flip directament.
//
// El temps es modela com a `speeds_[i]` segons per frame (per-frame), no com
// a `fps` uniforme. Això permet expressar holds al YAML via `frame_ms: [..]`
// (llista) sense haver de repetir el mateix índex de frame. Per a casos
// uniformes la factory omple `speeds_` amb el mateix valor a tot el vector.
class AnimationComponent { class AnimationComponent {
public: public:
AnimationComponent() = default; AnimationComponent() = default;
AnimationComponent(std::vector<int> frames, float fps, bool loop);
AnimationComponent(std::vector<int> frames, float fps, bool loop, bool flip_h, bool flip_v); // Factory canònica: llig el bloc `animations.<key>` del YAML.
//
// Camps llegits:
// - `frames` : llista d'índexs sencers (obligatori per animar).
// - `frame_ms` : escalar ms (uniforme) o llista de ms (per-frame).
// Default 1000 ms (animació estàtica).
// - `loop` : bool (default true).
// - `flip_h` : bool (default false).
// - `flip_v` : bool (default false).
//
// Si la clau no existeix retorna un component "estàtic" (1 frame, loop
// true). Si el YAML conté `fps:`, llença `std::runtime_error`: el
// format antic ja no és suportat — cal migrar a `frame_ms:`.
static auto fromYaml(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent;
// Helper uniforme: construeix un component on tots els frames duren
// `frame_ms` mil·lisegons. Útil per a usos programàtics (tests,
// imposició d'un frame estàtic des del loader de nivell). Per al cas
// típic guiat per YAML, usar `fromYaml`.
static auto uniform(std::vector<int> frames, float frame_ms, bool loop) -> AnimationComponent;
static auto uniform(std::vector<int> frames, float frame_ms, bool loop, bool flip_h, bool flip_v) -> AnimationComponent;
// Avanç del temps. Si `frames_` està buit o té un sol element, no fa // Avanç del temps. Si `frames_` està buit o té un sol element, no fa
// res. En mode loop, `current_frame_` rebota a 0 en passar del final; // res. En mode loop, `current_` rebota a 0 en passar del final; en
// en mode no-loop, es queda al darrer frame i `completed()` passa a // mode no-loop, es queda al darrer frame i `completed()` passa a true.
// true. // Cada frame consumeix `speeds_[current_]` segons.
void tick(float delta_time); void tick(float delta_time);
// Torna al primer frame i reinicia l'acumulador. Útil en transicions // Torna al primer frame i reinicia l'acumulador. Útil en transicions
@@ -39,8 +67,7 @@ class AnimationComponent {
void reset(); void reset();
// Índex actual dins `frames_`. Si `frames_` està buit retorna 0 com // Índex actual dins `frames_`. Si `frames_` està buit retorna 0 com
// a fallback tolerant (el loader ja ha emès warning si hi havia // a fallback tolerant.
// frames fora de rang; aquí preferim no crashejar).
[[nodiscard]] auto currentFrame() const -> int; [[nodiscard]] auto currentFrame() const -> int;
[[nodiscard]] auto frameCount() const -> std::size_t { return frames_.size(); } [[nodiscard]] auto frameCount() const -> std::size_t { return frames_.size(); }
@@ -55,8 +82,10 @@ class AnimationComponent {
[[nodiscard]] auto flipMode() const -> SDL_FlipMode; [[nodiscard]] auto flipMode() const -> SDL_FlipMode;
private: private:
AnimationComponent(std::vector<int> frames, std::vector<float> speeds, bool loop, bool flip_h, bool flip_v);
std::vector<int> frames_; std::vector<int> frames_;
float fps_{1.0F}; std::vector<float> speeds_; // segons per frame; size == frames_.size()
bool loop_{true}; bool loop_{true};
bool flip_h_{false}; bool flip_h_{false};
bool flip_v_{false}; bool flip_v_{false};
+4 -33
View File
@@ -1,45 +1,16 @@
#include "game/entities/concrete/door.hpp" #include "game/entities/concrete/door.hpp"
#include <string> #include <string>
#include <vector>
#include "game/entities/entity_factory.hpp" #include "game/entities/entity_factory.hpp"
#include "game/entities/event_bus.hpp" #include "game/entities/event_bus.hpp"
#include "game/entities/player.hpp" #include "game/entities/player.hpp"
#include "game/map/map.hpp" #include "game/map/map.hpp"
namespace {
// Carrega una animació d'un bloc concret (closed/opening/open) amb
// fallbacks tolerants. Si la clau no existeix, retorna una animació
// d'un sol frame loopant: no fa estat invàlid, simplement no hi ha res
// per reproduir.
auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
std::vector<int> frames = {0};
float fps = 1.0F;
bool loop = true;
if (yaml.contains("animations") && yaml["animations"].contains(key)) {
const auto& anim = yaml["animations"][key];
if (anim.contains("frames")) {
frames.clear();
for (const auto& f : anim["frames"]) {
// cppcheck-suppress useStlAlgorithm
frames.push_back(f.get_value<int>());
}
if (frames.empty()) { frames.push_back(0); }
}
if (anim.contains("fps")) { fps = anim["fps"].get_value<float>(); }
if (anim.contains("loop")) { loop = anim["loop"].get_value<bool>(); }
}
return {frames, fps, loop};
}
} // namespace
Door::Door(const fkyaml::node& yaml) Door::Door(const fkyaml::node& yaml)
: anim_closed_(buildAnimation(yaml, "closed")), : anim_closed_(AnimationComponent::fromYaml(yaml, "closed")),
anim_opening_(buildAnimation(yaml, "opening")), anim_opening_(AnimationComponent::fromYaml(yaml, "opening")),
anim_open_(buildAnimation(yaml, "open")) { anim_open_(AnimationComponent::fromYaml(yaml, "open")) {
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("door"); name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("door");
collides_ = true; collides_ = true;
interactable_ = true; interactable_ = true;
@@ -99,7 +70,7 @@ void Door::open() {
// placeholders defineixen `loop: false` explícitament al YAML, així // placeholders defineixen `loop: false` explícitament al YAML, així
// que aquest fallback només actua si algú dissenya malament. // que aquest fallback només actua si algú dissenya malament.
if (anim_.has_value() && anim_->frameCount() <= 1 && anim_->loops()) { if (anim_.has_value() && anim_->frameCount() <= 1 && anim_->loops()) {
anim_ = AnimationComponent({0}, 6.0F, false); anim_ = AnimationComponent::uniform({0}, 167.0F, false);
} }
} }
+2 -32
View File
@@ -1,42 +1,12 @@
#include "game/entities/concrete/dust.hpp" #include "game/entities/concrete/dust.hpp"
#include <string> #include <string>
#include <vector>
#include "game/entities/entity_factory.hpp" #include "game/entities/entity_factory.hpp"
namespace {
// Llig una animació opcional del bloc `animations` del YAML, amb
// valors per defecte segurs (1 frame, fps=1, sense loop). Si la
// clau no existeix, retornem una animació "ja completada" — així,
// un Dust spawnejat amb una variant inexistent al YAML mor al
// pròxim frame sense petar.
auto buildAnim(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
std::vector<int> frames = {0};
float fps = 1.0F;
bool loop = false;
if (yaml.contains("animations") && yaml["animations"].contains(key)) {
const auto& anim = yaml["animations"][key];
if (anim.contains("frames")) {
frames.clear();
for (const auto& f : anim["frames"]) {
// cppcheck-suppress useStlAlgorithm
frames.push_back(f.get_value<int>());
}
if (frames.empty()) { frames.push_back(0); }
}
if (anim.contains("fps")) { fps = anim["fps"].get_value<float>(); }
if (anim.contains("loop")) { loop = anim["loop"].get_value<bool>(); }
}
return {frames, fps, loop};
}
} // namespace
Dust::Dust(const fkyaml::node& yaml) Dust::Dust(const fkyaml::node& yaml)
: anim_jump_(buildAnim(yaml, "jump")), : anim_jump_(AnimationComponent::fromYaml(yaml, "jump")),
anim_land_(buildAnim(yaml, "land")) { anim_land_(AnimationComponent::fromYaml(yaml, "land")) {
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("dust"); name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("dust");
collides_ = false; collides_ = false;
interactable_ = false; interactable_ = false;
+5 -35
View File
@@ -29,36 +29,6 @@ namespace {
return k_rng_; return k_rng_;
} }
// Llegeix un bloc d'animació del YAML amb fallbacks tolerants (mateix
// patró que Door/Sarcophagus). Si la clau no apareix, retorna una
// animació d'1 frame loopant, neutra — la momia es podrà moure però
// visualment es quedarà estàtica. Llegeix `flip_h`/`flip_v` (default
// false) i els passa al component perquè el motor sàpiga aplicar-los
// al sprite quan aquesta animació estigui activa.
auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
std::vector<int> frames = {0};
float fps = 8.0F;
bool loop = true;
bool flip_h = false;
bool flip_v = false;
if (yaml.contains("animations") && yaml["animations"].contains(key)) {
const auto& anim = yaml["animations"][key];
if (anim.contains("frames")) {
frames.clear();
for (const auto& f : anim["frames"]) {
// cppcheck-suppress useStlAlgorithm
frames.push_back(f.get_value<int>());
}
if (frames.empty()) { frames.push_back(0); }
}
if (anim.contains("fps")) { fps = anim["fps"].get_value<float>(); }
if (anim.contains("loop")) { loop = anim["loop"].get_value<bool>(); }
if (anim.contains("flip_h")) { flip_h = anim["flip_h"].get_value<bool>(); }
if (anim.contains("flip_v")) { flip_v = anim["flip_v"].get_value<bool>(); }
}
return {frames, fps, loop, flip_h, flip_v};
}
// Busca si el segment [prev, prev+delta] travessa una lane del patró // Busca si el segment [prev, prev+delta] travessa una lane del patró
// `base + k·pitch` (k ∈ [0..max_idx]). Retorna el delta que cal aplicar // `base + k·pitch` (k ∈ [0..max_idx]). Retorna el delta que cal aplicar
// per arribar exactament a la primera línia travessada. Iterem en el // per arribar exactament a la primera línia travessada. Iterem en el
@@ -103,11 +73,11 @@ namespace {
} // namespace } // namespace
Mummy::Mummy(const fkyaml::node& yaml) Mummy::Mummy(const fkyaml::node& yaml)
: anim_walk_south_(buildAnimation(yaml, "walk_south")), : anim_walk_south_(AnimationComponent::fromYaml(yaml, "walk_south")),
anim_walk_north_(buildAnimation(yaml, "walk_north")), anim_walk_north_(AnimationComponent::fromYaml(yaml, "walk_north")),
anim_walk_east_(buildAnimation(yaml, "walk_east")), anim_walk_east_(AnimationComponent::fromYaml(yaml, "walk_east")),
anim_walk_west_(buildAnimation(yaml, "walk_west")), anim_walk_west_(AnimationComponent::fromYaml(yaml, "walk_west")),
anim_raise_(buildAnimation(yaml, "raise")) { anim_raise_(AnimationComponent::fromYaml(yaml, "raise")) {
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("mummy"); name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("mummy");
collides_ = true; collides_ = true;
interactable_ = false; interactable_ = false;
+3 -24
View File
@@ -2,7 +2,6 @@
#include <random> #include <random>
#include <string> #include <string>
#include <vector>
#include "core/audio/audio.hpp" #include "core/audio/audio.hpp"
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
@@ -21,26 +20,6 @@ namespace {
return k_rng_; return k_rng_;
} }
auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
std::vector<int> frames = {0};
float fps = 1.0F;
bool loop = true;
if (yaml.contains("animations") && yaml["animations"].contains(key)) {
const auto& anim = yaml["animations"][key];
if (anim.contains("frames")) {
frames.clear();
for (const auto& f : anim["frames"]) {
// cppcheck-suppress useStlAlgorithm
frames.push_back(f.get_value<int>());
}
if (frames.empty()) { frames.push_back(0); }
}
if (anim.contains("fps")) { fps = anim["fps"].get_value<float>(); }
if (anim.contains("loop")) { loop = anim["loop"].get_value<bool>(); }
}
return {frames, fps, loop};
}
// Llig `tile_size: [w, h]` del YAML. Fallback [1,1] si no existeix. // Llig `tile_size: [w, h]` del YAML. Fallback [1,1] si no existeix.
void readTileSize(const fkyaml::node& yaml, int& w, int& h) { void readTileSize(const fkyaml::node& yaml, int& w, int& h) {
w = 1; w = 1;
@@ -61,9 +40,9 @@ Sarcophagus::Sarcophagus(const fkyaml::node& yaml, int anchor_col, int anchor_ro
} }
Sarcophagus::Sarcophagus(const fkyaml::node& yaml) Sarcophagus::Sarcophagus(const fkyaml::node& yaml)
: anim_closed_(buildAnimation(yaml, "closed")), : anim_closed_(AnimationComponent::fromYaml(yaml, "closed")),
anim_opening_(buildAnimation(yaml, "opening")), anim_opening_(AnimationComponent::fromYaml(yaml, "opening")),
anim_open_(buildAnimation(yaml, "open")) { anim_open_(AnimationComponent::fromYaml(yaml, "open")) {
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("sarcophagus"); name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("sarcophagus");
collides_ = true; collides_ = true;
interactable_ = true; interactable_ = true;
+1 -28
View File
@@ -1,41 +1,14 @@
#include "game/entities/concrete/torch.hpp" #include "game/entities/concrete/torch.hpp"
#include <string> #include <string>
#include <vector>
#include "game/entities/entity_factory.hpp" #include "game/entities/entity_factory.hpp"
namespace {
// Ajut: extreu l'animació `idle` del bloc `animations` del YAML si existeix,
// amb valors per defecte que sempre produïxen una animació vàlida (1 frame).
auto buildIdleAnimation(const fkyaml::node& yaml) -> AnimationComponent {
std::vector<int> frames = {0};
float fps = 1.0F;
bool loop = true;
if (yaml.contains("animations") && yaml["animations"].contains("idle")) {
const auto& anim = yaml["animations"]["idle"];
if (anim.contains("frames")) {
frames.clear();
for (const auto& f : anim["frames"]) {
// cppcheck-suppress useStlAlgorithm
frames.push_back(f.get_value<int>());
}
if (frames.empty()) { frames.push_back(0); }
}
if (anim.contains("fps")) { fps = anim["fps"].get_value<float>(); }
if (anim.contains("loop")) { loop = anim["loop"].get_value<bool>(); }
}
return {frames, fps, loop};
}
} // namespace
Torch::Torch(const fkyaml::node& yaml) { Torch::Torch(const fkyaml::node& yaml) {
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("torch"); name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("torch");
collides_ = false; collides_ = false;
interactable_ = false; interactable_ = false;
anim_ = buildIdleAnimation(yaml); anim_ = AnimationComponent::fromYaml(yaml, "idle");
} }
REGISTER_ENTITY("torch", Torch) REGISTER_ENTITY("torch", Torch)
+13 -36
View File
@@ -33,42 +33,18 @@ namespace {
return sprite; return sprite;
} }
// Construeix l'`AnimationComponent` del swing a partir del bloc // Llig el camp `active_frames` del bloc `animations.swing` — específic
// `animations.swing`. Convenció igual que la resta d'entitats: // del látigo: els índexs llistats són els frames on la hitbox aplica
// - `frames` : llista d'índexs dins l'spritesheet (obligatori). // dany (wind-up i recovery no danyen). La resta de l'animació la
// - `frame_ms` : durada uniforme per frame (ms). Alternativament `fps`. // construeix `AnimationComponent::fromYaml`.
// - `loop` : per defecte true; al látigo el volem false (mor en void readActiveFrames(const fkyaml::node& yaml, std::vector<int>& active_out) {
// completar). if (!yaml.contains("animations") || !yaml["animations"].contains("swing")) { return; }
auto buildSwingAnim(const fkyaml::node& yaml, std::vector<int>& active_out) const auto& sw = yaml["animations"]["swing"];
-> AnimationComponent { if (!sw.contains("active_frames")) { return; }
std::vector<int> frames = {0}; for (const auto& f : sw["active_frames"]) {
float fps = 8.0F; // cppcheck-suppress useStlAlgorithm
bool loop = false; active_out.push_back(f.get_value<int>());
if (yaml.contains("animations") && yaml["animations"].contains("swing")) {
const auto& sw = yaml["animations"]["swing"];
if (sw.contains("frames")) {
frames.clear();
for (const auto& f : sw["frames"]) {
// cppcheck-suppress useStlAlgorithm
frames.push_back(f.get_value<int>());
}
if (frames.empty()) { frames.push_back(0); }
}
if (sw.contains("frame_ms")) {
const auto MS = static_cast<float>(sw["frame_ms"].get_value<int>());
if (MS > 0.0F) { fps = 1000.0F / MS; }
} else if (sw.contains("fps")) {
fps = sw["fps"].get_value<float>();
}
if (sw.contains("loop")) { loop = sw["loop"].get_value<bool>(); }
if (sw.contains("active_frames")) {
for (const auto& f : sw["active_frames"]) {
// cppcheck-suppress useStlAlgorithm
active_out.push_back(f.get_value<int>());
}
}
} }
return {frames, fps, loop};
} }
} // namespace } // namespace
@@ -86,7 +62,8 @@ WhipAttack::WhipAttack(const fkyaml::node& yaml) {
if (s.contains("west")) { sprite_west_ = readDirSprite(s["west"]); } if (s.contains("west")) { sprite_west_ = readDirSprite(s["west"]); }
} }
anim_swing_ = buildSwingAnim(yaml, active_frames_); anim_swing_ = AnimationComponent::fromYaml(yaml, "swing");
readActiveFrames(yaml, active_frames_);
anim_ = anim_swing_; anim_ = anim_swing_;
} }
+6 -5
View File
@@ -64,12 +64,13 @@ auto Entity::getCollisionRect() const -> SDL_FRect {
} }
auto Entity::getLightCenter() const -> SDL_FPoint { auto Entity::getLightCenter() const -> SDL_FPoint {
// Centre del rect de render. Per a entitats que es desplacen // Centre del rect de render + offset configurat al YAML. Per a
// visualment sobre la seva posició lògica (p.ex. el Player saltant), // entitats que es desplacen visualment sobre la seva posició lògica
// la subclasse override aquest mètode per restar el desplaçament. // (p.ex. el Player saltant), la subclasse override aquest mètode
// per restar el desplaçament — i ha d'incloure l'offset igualment.
return { return {
.x = pixel_rect_.x + (pixel_rect_.w * 0.5F), .x = pixel_rect_.x + (pixel_rect_.w * 0.5F) + static_cast<float>(light_offset_x_),
.y = pixel_rect_.y + (pixel_rect_.h * 0.5F), .y = pixel_rect_.y + (pixel_rect_.h * 0.5F) + static_cast<float>(light_offset_y_),
}; };
} }
+53 -5
View File
@@ -2,10 +2,12 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <cstdint>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <string> #include <string>
#include "core/rendering/lighting.hpp"
#include "game/entities/animation_component.hpp" #include "game/entities/animation_component.hpp"
// Forward-declaracions: mantenim aquest header lleuger. `Player` viu a // Forward-declaracions: mantenim aquest header lleuger. `Player` viu a
@@ -266,6 +268,44 @@ class Entity {
[[nodiscard]] auto getLightFlicker() const -> bool { return light_flicker_; } [[nodiscard]] auto getLightFlicker() const -> bool { return light_flicker_; }
void setLightFlicker(bool f) { light_flicker_ = f; } 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;
}
// Desplaçament en píxels del centre del cercle de llum respecte el
// centre visual del sprite (que és el que retornaria per defecte
// `getLightCenter`). Útil quan el "punt de llum" iconogràfic no
// coincideix amb el centre del bounding box — per exemple, la flama
// d'una antorxa està a la part de dalt del sprite i interessa pujar
// l'emissor 3-4 px per a que el cercle de llum surti de la flama,
// no del peu. Y creix cap avall al canvas, així que `y: -3` puja
// la llum 3 píxels. Cadena de resolució idèntica al color: tipus →
// sala → spawn.
[[nodiscard]] auto getLightOffsetX() const -> int { return light_offset_x_; }
[[nodiscard]] auto getLightOffsetY() const -> int { return light_offset_y_; }
void setLightOffset(int x, int y) {
light_offset_x_ = x;
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 // Identificador estable per al flicker. Cada Entity rep un valor
// únic a la construcció (contador estàtic) i el conserva fins a la // ú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 // mort — així el patró del flicker no "deriva" quan l'entitat es
@@ -274,11 +314,13 @@ class Entity {
// un problema. // un problema.
[[nodiscard]] auto getLightSeed() const -> int { return light_seed_; } [[nodiscard]] auto getLightSeed() const -> int { return light_seed_; }
// Centre de la llum en píxels del canvas. Per defecte és el centre del // Centre de la llum en píxels del canvas. Per defecte és el centre
// `pixel_rect_` (que és la caixa de render: sprite top-left + mida). // del `pixel_rect_` (caixa de render) més l'offset configurat al
// Virtual perquè entitats amb desplaçament visual sobre la posició // YAML (`light.offset: [x, y]`). Virtual perquè entitats amb
// lògica (p.ex. el Player en l'aire, que rebaixa visualment `pos_.y` // desplaçament visual sobre la posició lògica (p.ex. el Player en
// amb `jump_z_`) puguin retornar el centre que realment es veu. // l'aire, que rebaixa visualment `pos_.y` amb `jump_z_`) puguin
// retornar el centre que realment es veu — l'override n'és
// responsable d'incloure l'offset (com fa `Player::getLightCenter`).
[[nodiscard]] virtual auto getLightCenter() const -> SDL_FPoint; [[nodiscard]] virtual auto getLightCenter() const -> SDL_FPoint;
protected: protected:
@@ -302,6 +344,12 @@ class Entity {
static inline int s_next_light_seed{0}; static inline int s_next_light_seed{0};
int light_seed_{++s_next_light_seed}; int light_seed_{++s_next_light_seed};
bool light_flicker_{false}; 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};
int light_offset_x_{0};
int light_offset_y_{0};
Lighting::FlickerConfig flicker_params_{};
bool dead_{false}; bool dead_{false};
SDL_FlipMode flip_{SDL_FLIP_NONE}; SDL_FlipMode flip_{SDL_FLIP_NONE};
EventBus* bus_{nullptr}; EventBus* bus_{nullptr};
+145 -10
View File
@@ -4,6 +4,8 @@
// consumidors que només volen la base del manager (p.ex. el smoke test // consumidors que només volen la base del manager (p.ex. el smoke test
// stand-alone) enllacen `entity_manager.cpp` sense arrossegar aquest fitxer. // stand-alone) enllacen `entity_manager.cpp` sense arrossegar aquest fitxer.
#include <algorithm>
#include <cstdint>
#include <filesystem> #include <filesystem>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -155,25 +157,147 @@ namespace {
return c["height"].get_value<int>(); return c["height"].get_value<int>();
} }
// Bloc opcional `light: { radius: N, flicker: bool }` de la plantilla. // Bloc opcional `light: { radius: N, flicker: bool, color: [r,g,b], offset: [x,y] }`
// Si manca, el radi és 0 i l'entitat no entra a la pasada d'il·luminació. // de la plantilla. Si manca, el radi és 0 i l'entitat no entra a la
// `flicker` activa la modulació "tipus foc" del radi (només antorxes // pasada d'il·luminació. `flicker` activa la modulació "tipus foc"
// l'haurien d'usar). La forma del gradient (3 anells) i els paràmetres // del radi (només antorxes l'haurien d'usar). `color` només té
// del flicker viuen a data/config/lighting.yaml — no cal declarar-los // efecte en mode additive — defecte blanc (255,255,255). `offset`
// per entitat. // desplaça el centre del cercle de llum respecte el centre del
// sprite (default (0, 0); Y creix cap avall). 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 { struct LightTemplate {
int radius{0}; int radius{0};
bool flicker{false}; bool flicker{false};
std::uint8_t color_r{255};
std::uint8_t color_g{255};
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)
// 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;
}
// Helper paral·lel a `readColorTriplet` per a `offset: [x, y]` —
// píxels enters, sense clamp (offsets negatius són legítims; Y
// creix cap avall).
auto readOffsetPair(const fkyaml::node& seq, int& x, int& y) -> bool {
if (!seq.is_sequence() || seq.size() != 2) { return false; }
x = seq[0].get_value<int>();
y = seq[1].get_value<int>();
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 { auto readLight(const fkyaml::node& tmpl) -> LightTemplate {
if (!tmpl.contains("light")) { return {}; }
const auto& l = tmpl["light"];
LightTemplate out; 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("radius")) { out.radius = l["radius"].get_value<int>(); }
if (l.contains("flicker")) { out.flicker = l["flicker"].get_value<bool>(); } 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; return out;
} }
// Combina els defaults del tipus amb l'override de sala (si n'hi ha).
// Forma 1: la sala no diu res del tipus → es retornen els defaults
// del tipus tal qual (cas habitual / retrocompat).
// Forma 2: la sala declara `<tipus>: false` → radi 0 (entitat sense
// llum encara que el seu YAML en declari).
// 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, const LightTemplate& type_default) -> LightTemplate {
const auto* room = Map::getRoomEmitterOverride(type);
if (room == nullptr) { return type_default; }
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; }
if (room->color.has_value()) {
out.color_r = (*room->color)[0];
out.color_g = (*room->color)[1];
out.color_b = (*room->color)[2];
}
if (room->offset.has_value()) {
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;
}
// 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` → informatiu (cau a capes inferiors)
// `light: { radius, flicker, color: [r,g,b], offset: [x,y] }` → 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"];
if (l.is_boolean()) {
if (!l.get_value<bool>()) {
entity.setLightRadius(0);
entity.setLightFlicker(false);
}
return;
}
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); }
}
if (l.contains("offset")) {
int x{};
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]? }` // 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
@@ -296,9 +420,16 @@ namespace {
entity->setCollisionBox(*BOX); entity->setCollisionBox(*BOX);
} }
entity->setHeight(readHeight(tmpl)); entity->setHeight(readHeight(tmpl));
const auto LIGHT = readLight(tmpl); // Defaults del tipus → resolució amb override de sala. El layer
// del spawn (per-instància) viu fora de `buildEntity`, perquè
// aquí encara no tenim el node del spawn — el sobreposem a
// `addSpawn` just després.
const auto LIGHT = resolveLightWithRoom(type, readLight(tmpl));
entity->setLightRadius(LIGHT.radius); entity->setLightRadius(LIGHT.radius);
entity->setLightFlicker(LIGHT.flicker); 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); anchor_out = readAnchor(tmpl);
return entity; return entity;
} }
@@ -425,7 +556,7 @@ namespace {
// els muros decoratius amb variants (stone, column, ...) l'usen. // els muros decoratius amb variants (stone, column, ...) l'usen.
if (spawn.contains("frame")) { if (spawn.contains("frame")) {
const int FRAME = spawn["frame"].get_value<int>(); const int FRAME = spawn["frame"].get_value<int>();
entity->setAnimation(AnimationComponent({FRAME}, 1.0F, false)); entity->setAnimation(AnimationComponent::uniform({FRAME}, 1000.0F, false));
} }
manager.add(std::move(entity)); manager.add(std::move(entity));
@@ -461,6 +592,10 @@ auto EntityManager::addSpawn(const fkyaml::node& spawn) -> Entity* {
std::string anchor; std::string anchor;
auto entity = buildEntity(TYPE, sprite, anchor); auto entity = buildEntity(TYPE, sprite, anchor);
if (entity == nullptr) { return nullptr; } if (entity == nullptr) { return nullptr; }
// Tercera capa de resolució de llum: override per-instància declarat
// al spawn del YAML de la sala. Capes prèvies (tipus, sala) ja viuen
// dins de l'entitat. Aplica abans del std::move per no perdre l'accés.
applySpawnLightOverride(*entity, spawn);
Entity* observer = entity.get(); Entity* observer = entity.get();
bool ok = false; bool ok = false;
+39 -2
View File
@@ -150,10 +150,46 @@ void Player::loadFrom(const std::string& yaml_path) {
void Player::readLightBlock(const fkyaml::node& yaml) { void Player::readLightBlock(const fkyaml::node& yaml) {
light_radius = 0; light_radius = 0;
light_flicker = false; light_flicker = false;
light_color_r = 255;
light_color_g = 255;
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; } if (!yaml.contains("light")) { return; }
const auto& l = yaml["light"]; const auto& l = yaml["light"];
if (l.contains("radius")) { light_radius = l["radius"].get_value<int>(); } if (l.contains("radius")) { light_radius = l["radius"].get_value<int>(); }
if (l.contains("flicker")) { light_flicker = l["flicker"].get_value<bool>(); } 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));
}
}
// `offset: [x, y]` opcional, píxels enters. Default (0, 0) → centre
// del sprite. Y creix cap avall, així que `y: -3` puja la llum 3 px.
if (l.contains("offset")) {
const auto& o = l["offset"];
if (o.is_sequence() && o.size() == 2) {
light_offset_x = o[0].get_value<int>();
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) { void Player::readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path) {
@@ -1479,9 +1515,10 @@ auto Player::getLightCenter() const -> SDL_FPoint {
// Sprite top-left = (pos_.x, pos_.y). El render desplaça verticalment // 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 // `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. // que la llum "salti" amb el jugador en lloc de quedar-se al sòl.
// Sumem també l'offset estàtic configurat al YAML del personatge.
return { return {
.x = getPosX() + (static_cast<float>(sprite_w) * 0.5F), .x = getPosX() + (static_cast<float>(sprite_w) * 0.5F) + static_cast<float>(light_offset_x),
.y = getPosY() - jump_z_ + (static_cast<float>(sprite_h) * 0.5F), .y = getPosY() - jump_z_ + (static_cast<float>(sprite_h) * 0.5F) + static_cast<float>(light_offset_y),
}; };
} }
+24 -4
View File
@@ -7,6 +7,7 @@
#include <utility> #include <utility>
#include <vector> #include <vector>
#include "core/rendering/lighting.hpp"
#include "core/rendering/sprite/animated_sprite.hpp" #include "core/rendering/sprite/animated_sprite.hpp"
#include "core/rendering/surface.hpp" #include "core/rendering/surface.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
@@ -125,12 +126,25 @@ class Player : public AnimatedSprite {
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 // Radi de la llum emesa pel jugador (en píxels del canvas), llegit
// del bloc opcional `light: { radius: N, flicker: bool }` del YAML. // del bloc opcional `light: { radius: N, flicker: bool, color: [r,g,b], offset: [x,y] }`
// 0 = no emet. Es declara estàtic com la resta de configs del jugador // del YAML. 0 = no emet. Es declara estàtic com la resta de configs
// (immunity, jump, ...): tots els jugadors del mateix YAML comparteixen // del jugador (immunity, jump, ...): tots els jugadors del mateix YAML
// el valor. `light_flicker` activa la modulació "tipus foc" del radi. // 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); `light_offset_*` desplaça el
// centre del cercle respecte el centre del sprite (Y creix avall:
// `y: -3` puja la llum 3 px).
inline static int light_radius = 0; inline static int light_radius = 0;
inline static bool light_flicker = false; 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;
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. // 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);
@@ -146,6 +160,12 @@ class Player : public AnimatedSprite {
// per a que el cercle "salti" amb el jugador, no es quedi al sòl. // 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 getLightRadius() -> int { return light_radius; }
[[nodiscard]] static auto getLightFlicker() -> bool { return light_flicker; } [[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; }
[[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 // Seed estable per al flicker: reusem `id_` (0 o 1 en multijugador
// local) amb un offset gran per evitar col·lisions amb el contador // 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 // d'Entity (que arrenca des de 1). Així P1 i P2 oscil·len en fases
+131 -2
View File
@@ -1,7 +1,9 @@
#include "game/map/map.hpp" #include "game/map/map.hpp"
#include <algorithm>
#include <cmath> #include <cmath>
#include <stdexcept> #include <stdexcept>
#include <unordered_map>
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
@@ -28,12 +30,132 @@ namespace Map {
// (debris, columnes). El TU local manté l'estat sense exposar-lo // (debris, columnes). El TU local manté l'estat sense exposar-lo
// al header. // al header.
std::function<bool(const SDL_FRect&)> g_entity_collider; std::function<bool(const SDL_FRect&)> g_entity_collider;
// Overrides d'emissor declarats al bloc `lighting.emitters` del YAML
// de la sala. Clau = tipus d'entitat en singular (per concordar amb
// el camp `type:` de cada plantilla). Buit per defecte: una sala
// sense bloc `lighting.emitters` deixa que cada entitat utilitze els
// seus propis defaults del seu YAML de tipus.
std::unordered_map<std::string, RoomEmitterOverride> g_room_emitters;
// Plural → singular per a tipus coneguts. El YAML de sala fa servir
// el plural perquè es llegeix natural ("mummies: false"); el codi
// treballa amb la forma singular que coincideix amb el camp `type:`
// del spawn ("type: mummy"). Si la clau no és reconeguda, passa tal
// qual (suposa que l'autor ja l'ha escrit en singular).
auto normalizeEmitterKey(const std::string& yaml_key) -> std::string {
if (yaml_key == "players") { return "player"; }
if (yaml_key == "mummies") { return "mummy"; }
if (yaml_key == "torches") { return "torch"; }
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<std::array<std::uint8_t, 3>> {
if (!node.is_sequence() || node.size() != 3) { return std::nullopt; }
auto clamp = [](int v) { return static_cast<std::uint8_t>(std::clamp(v, 0, 255)); };
return std::array<std::uint8_t, 3>{
clamp(node[0].get_value<int>()),
clamp(node[1].get_value<int>()),
clamp(node[2].get_value<int>()),
};
}
// Parser comú d'una 2-tupla d'enters (offset de llum). Retorna
// nullopt si el node no és una seqüència de 2 elements. Sense
// clamp: els offsets negatius són legítims (Y creix cap avall,
// així que `y: -3` puja la llum 3 px).
auto parseIntPair(const fkyaml::node& node) -> std::optional<std::array<int, 2>> {
if (!node.is_sequence() || node.size() != 2) { return std::nullopt; }
return std::array<int, 2>{
node[0].get_value<int>(),
node[1].get_value<int>(),
};
}
// 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
// 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<float>();
}
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>());
g_room_emitters[KEY] = readEmitterEntry(it.value());
}
}
} // namespace } // namespace
void setEntityCollider(std::function<bool(const SDL_FRect&)> fn) { void setEntityCollider(std::function<bool(const SDL_FRect&)> fn) {
g_entity_collider = std::move(fn); g_entity_collider = std::move(fn);
} }
auto getRoomEmitterOverride(const std::string& type) -> const RoomEmitterOverride* {
const auto IT = g_room_emitters.find(type);
return IT == g_room_emitters.end() ? nullptr : &IT->second;
}
void loadFrom(const std::string& yaml_path) { void loadFrom(const std::string& yaml_path) {
auto bytes = Resource::Helper::loadFile(yaml_path); auto bytes = Resource::Helper::loadFile(yaml_path);
if (bytes.empty()) { if (bytes.empty()) {
@@ -103,9 +225,16 @@ 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; // Llum base de la sala. Dues formes acceptades:
// per defecte 1.0 (sala totalment il·luminada, comportament hui). // - `light_level: <float>` a l'arrel (forma antiga, encara
// suportada per retrocompat).
// - `lighting.level: <float>` dins del bloc nou `lighting:`,
// que també hostatja `lighting.emitters` (overrides per tipus).
// Si manca tot, queda al default 1.0 (sala totalment il·luminada).
// `loadLightingBlock` aplica el `lighting.level` per damunt del
// `light_level:` antic, així el nou bloc guanya si conviuen.
light_level = yaml.contains("light_level") ? yaml["light_level"].get_value<float>() : Defaults::Lighting::ROOM_LIGHT_DEFAULT; light_level = yaml.contains("light_level") ? yaml["light_level"].get_value<float>() : Defaults::Lighting::ROOM_LIGHT_DEFAULT;
loadLightingBlock(yaml);
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);
} }
+61 -1
View File
@@ -2,10 +2,15 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <array>
#include <cstdint>
#include <functional> #include <functional>
#include <optional>
#include <string> #include <string>
#include <utility> #include <utility>
#include "core/rendering/lighting.hpp"
// Mapa d'una piràmide: sala rectangular amb marcador superior, muros pels // 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 // 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 // cantó. La geometria (graella, pitch, amplada de murs) es carrega des del
@@ -54,9 +59,64 @@ namespace Map {
// (com hui, sense efecte visual); 0.0 = completament negra fora del // (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 // 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 // sobreescriu — si manca, queda al default 1.0 i la pasada
// d'il·luminació del frame es salta com a no-op. // d'il·luminació del frame es salta com a no-op. La forma nova és
// declarar-ho dins del bloc `lighting:` (vegeu més avall); `light_level:`
// al root es manté per compatibilitat enrere.
inline float light_level = 1.0F; 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.<tipus>`
// de la sala. Tres formes acceptades al YAML:
// <tipus>: false → desactiva la llum per a tot el tipus
// <tipus>: true → activa amb els defaults del tipus
// <tipus>: { radius: N, flicker: B } → activa i sobreescriu camps
// Els camps absents (`std::nullopt`) cauen al default del tipus
// d'entitat (radi/flicker del seu YAML). Els tipus no presents al
// mapping conserven el seu comportament per defecte (com avui).
//
// Conversió de clau YAML → tipus d'entitat: el YAML usa plural
// ("players", "mummies", "torches") perquè es llegeix natural per a
// l'autor de la sala; `loadFrom` normalitza a la clau singular
// ("player", "mummy", "torch") que coincideix amb el camp `type:` de
// cada plantilla d'entitat. Tipus no reconeguts s'accepten "tal qual"
// (singular pass-through) per si en el futur s'hi afegeixen emissors
// nous sense tocar el mapatge.
struct RoomEmitterOverride {
bool enabled{true};
std::optional<int> radius;
std::optional<bool> 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<std::array<std::uint8_t, 3>> color;
// Offset del centre del cercle de llum respecte el centre del
// 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,
// p.ex. "player" / "mummy" / "torch"). Retorna `nullptr` si la sala no
// declara res per aquest tipus — el caller usa els defaults del tipus
// sense modificació. El punter retornat és vàlid fins a la propera
// crida a `Map::loadFrom` (cada càrrega de nivell substitueix el
// mapping intern).
[[nodiscard]] auto getRoomEmitterOverride(const std::string& type) -> const RoomEmitterOverride*;
// 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;
+52 -2
View File
@@ -232,6 +232,39 @@ namespace {
return out; return out;
} }
// Override de sala per a la llum del jugador. Si el YAML de la sala
// declara `lighting.emitters.players`, substitueix els valors carregats
// per `Player::loadFrom`. Extret aquí per no inflar la complexitat
// cognitiva del ctor de GameScene (clang-tidy: cognitive-complexity).
// No s'aplica una "tercera capa" per-instància al jugador perquè no
// neix com a spawn del nivell; el dia d'un ítem "linterna" aquell
// sistema mutarà els mateixos camps en runtime — ortogonal a aquest
// override.
void applyRoomLightOverrideToPlayer() {
const auto* room = Map::getRoomEmitterOverride("player");
if (room == nullptr) { return; }
if (!room->enabled) {
Player::light_radius = 0;
Player::light_flicker = false;
return;
}
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];
}
if (room->offset.has_value()) {
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 } // namespace
auto GameScene::defaultConfig() -> Config { auto GameScene::defaultConfig() -> Config {
@@ -289,6 +322,7 @@ GameScene::GameScene(Config config)
// del YAML de P1 és suficient (els dos personatges comparteixen // del YAML de P1 és suficient (els dos personatges comparteixen
// geometria — morcus és un repaletat de sam). // geometria — morcus és un repaletat de sam).
Player::loadFrom(config_.player_yaml); Player::loadFrom(config_.player_yaml);
applyRoomLightOverrideToPlayer();
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia, // Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
// etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa // etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa
@@ -560,6 +594,14 @@ void GameScene::loadRoom(const std::string& yaml_path) {
// CLOSED amb visited buit, momies i altres actors a la posició inicial. // CLOSED amb visited buit, momies i altres actors a la posició inicial.
config_.map_yaml = yaml_path; config_.map_yaml = yaml_path;
Map::loadFrom(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 // 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 // reconstruir-lo, la imatge de la sala anterior es queda fixada per a
// sempre. PaintLayer captura `tileset_path_`/`tileset_cols_` per valor // sempre. PaintLayer captura `tileset_path_`/`tileset_cols_` per valor
@@ -1019,20 +1061,28 @@ void GameScene::applyLightingPass() {
// gating extern aquí (els jugadors són només 1-2 actors i el cost // gating extern aquí (els jugadors són només 1-2 actors i el cost
// d'afegir-los al vector és menyspreable). // d'afegir-los al vector és menyspreable).
const bool PLAYER_FLICKER = Player::getLightFlicker(); const bool PLAYER_FLICKER = Player::getLightFlicker();
const Lighting::Color PLAYER_COLOR{
.r = Player::getLightColorR(),
.g = Player::getLightColorG(),
.b = Player::getLightColorB(),
};
const Lighting::FlickerConfig PLAYER_FP = Player::getFlickerParams();
for (const auto& p : players_) { for (const auto& p : players_) {
if (p->isOutOfPlay()) { continue; } if (p->isOutOfPlay()) { continue; }
const auto C = p->getLightCenter(); 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, .flicker_params = PLAYER_FP});
} }
for (const auto* e : entity_manager_.getEntities()) { for (const auto* e : entity_manager_.getEntities()) {
const int R = e->getLightRadius(); const int R = e->getLightRadius();
if (R <= 0) { continue; } if (R <= 0) { continue; }
const auto C = e->getLightCenter(); 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, .flicker_params = e->getFlickerParams()});
} }
Lighting::apply(Screen::get().getCanvasArgb(), Lighting::apply(Screen::get().getCanvasArgb(),
Screen::get().getCanvasWidth(), Screen::get().getCanvasWidth(),
Screen::get().getCanvasHeight(), Screen::get().getCanvasHeight(),
Map::lighting_mode,
Map::light_level, Map::light_level,
emitters); emitters);
} }
+19 -3
View File
@@ -111,20 +111,36 @@ namespace Defaults {
// fitxer falta o li manca alguna clau. La forma final (3 anells) es // fitxer falta o li manca alguna clau. La forma final (3 anells) es
// descriu al YAML; veure `Lighting::GradientConfig` per a la semàntica. // descriu al YAML; veure `Lighting::GradientConfig` per a la semàntica.
namespace Lighting { namespace Lighting {
// Defaults del gradient subtractive (mode històric). Les
// intensitats són multiplicadors 0..1 que es multipliquen sobre
// el píxel: 1.0 = no toca, 0.0 = negre.
constexpr float INNER_RATIO = 0.8F; constexpr float INNER_RATIO = 0.8F;
constexpr float MID_RATIO = 0.9F; constexpr float MID_RATIO = 0.9F;
constexpr float INNER_INTENSITY = 1.0F; constexpr float INNER_INTENSITY = 1.0F;
constexpr float MID_INTENSITY = 0.9F; constexpr float MID_INTENSITY = 0.9F;
constexpr float OUTER_INTENSITY = 0.8F; constexpr float OUTER_INTENSITY = 0.8F;
// Defaults del gradient additive. Les intensitats són "alfa"
// del color afegit (0..1): 1.0 = afegeix el color sencer i
// satura, 0.0 = no afegeix res. Valors baixos a propòsit perquè
// l'additive es nota fort molt fàcilment — només un toc de tint
// cromàtic al voltant de l'emissor.
constexpr float INNER_RATIO_ADDITIVE = 0.5F;
constexpr float MID_RATIO_ADDITIVE = 0.85F;
constexpr float INNER_INTENSITY_ADDITIVE = 0.15F;
constexpr float MID_INTENSITY_ADDITIVE = 0.10F;
constexpr float OUTER_INTENSITY_ADDITIVE = 0.05F;
// Nivell d'il·luminació de sala per defecte. Una sala sense la // Nivell d'il·luminació de sala per defecte. Una sala sense la
// clau `light_level` queda totalment il·luminada (com hui). // clau `light_level` queda totalment il·luminada (com hui).
constexpr float ROOM_LIGHT_DEFAULT = 1.0F; constexpr float ROOM_LIGHT_DEFAULT = 1.0F;
// Defaults del flicker. Els reemplaça el bloc `flicker:` de // Defaults del flicker. Els reemplaça el bloc `flicker:` de
// data/config/lighting.yaml. Veure `Lighting::FlickerConfig` // data/config/lighting.yaml. Veure `Lighting::FlickerConfig`
// per a la semàntica de cada camp. // per a la semàntica de cada camp. Les amplituds són **ràtios**
constexpr float FLICKER_WAVE_AMPLITUDE_PX = 2.0F; // 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_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; constexpr float FLICKER_NOISE_FREQUENCY_HZ = 7.0F;
} // namespace Lighting } // namespace Lighting
+1 -1
View File
@@ -3,7 +3,7 @@
namespace Texts { namespace Texts {
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte ARCADE — JailDesigner"; constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte ARCADE — JailDesigner";
constexpr const char* COPYRIGHT = "© 2026 JailDesigner"; constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
constexpr const char* VERSION = "0.44.0"; // Single source of truth — Makefile el grep constexpr const char* VERSION = "0.45.0"; // Single source of truth — Makefile el grep
} // namespace Texts } // namespace Texts
namespace GameCanvas { namespace GameCanvas {
+91 -4
View File
@@ -242,11 +242,12 @@ namespace {
void testAnimationComponent() { void testAnimationComponent() {
std::cout << "[T6] animation component\n"; std::cout << "[T6] animation component\n";
AnimationComponent anim({10, 20, 30}, 10.0F, true); // 100 ms per frame: a tick(0.15) avança 1 frame; tick(0.10) un altre.
AnimationComponent anim = AnimationComponent::uniform({10, 20, 30}, 100.0F, true);
REQUIRE(anim.frameCount() == 3); REQUIRE(anim.frameCount() == 3);
REQUIRE(anim.currentFrame() == 10); REQUIRE(anim.currentFrame() == 10);
anim.tick(0.15F); // 1.5 frames de 10 fps → hauria d'avançar a l'índex 1 anim.tick(0.15F); // 1.5 frames de 100 ms → hauria d'avançar a l'índex 1
REQUIRE(anim.currentFrame() == 20); REQUIRE(anim.currentFrame() == 20);
anim.tick(0.10F); anim.tick(0.10F);
@@ -257,16 +258,101 @@ namespace {
REQUIRE(anim.currentFrame() == 10); REQUIRE(anim.currentFrame() == 10);
// Variant no-loop: es para al darrer i queda completed. // Variant no-loop: es para al darrer i queda completed.
AnimationComponent once({1, 2}, 10.0F, false); AnimationComponent once = AnimationComponent::uniform({1, 2}, 100.0F, false);
once.tick(1.0F); // molts frames; ha d'arribar al final i quedar-se. once.tick(1.0F); // molts frames; ha d'arribar al final i quedar-se.
REQUIRE(once.currentFrame() == 2); REQUIRE(once.currentFrame() == 2);
REQUIRE(once.completed()); REQUIRE(once.completed());
// Variant d'1 frame no-loop: completed des del principi. // Variant d'1 frame no-loop: completed des del principi.
AnimationComponent single({5}, 1.0F, false); AnimationComponent single = AnimationComponent::uniform({5}, 1000.0F, false);
REQUIRE(single.completed()); REQUIRE(single.completed());
} }
// Cobertura específica de `AnimationComponent::fromYaml`: format
// `frame_ms` escalar i llista per-frame, gestió de `fps:` (rebutjat),
// i defaults quan la clau no existeix.
void testAnimationFromYaml() {
std::cout << "[T6b] animation fromYaml\n";
// 1) Format escalar `frame_ms`: tots els frames duren el mateix.
{
const std::string YAML = R"(
animations:
walk: { frames: [0, 1, 2, 3], frame_ms: 100, loop: true }
)";
auto node = fkyaml::node::deserialize(YAML);
auto anim = AnimationComponent::fromYaml(node, "walk");
REQUIRE(anim.frameCount() == 4);
REQUIRE(anim.currentFrame() == 0);
anim.tick(0.10F);
REQUIRE(anim.currentFrame() == 1);
}
// 2) Format llista `frame_ms`: el darrer frame aguanta més.
{
const std::string YAML = R"(
animations:
raise:
frames: [10, 11, 12]
frame_ms: [100, 100, 500]
loop: false
)";
auto node = fkyaml::node::deserialize(YAML);
auto anim = AnimationComponent::fromYaml(node, "raise");
REQUIRE(anim.frameCount() == 3);
anim.tick(0.10F);
REQUIRE(anim.currentFrame() == 11);
anim.tick(0.10F);
REQUIRE(anim.currentFrame() == 12);
// El darrer frame dura 500 ms: tick de 400 ms no completa l'anim.
anim.tick(0.40F);
REQUIRE(!anim.completed());
// 100 ms més (total 500 ms al frame 12) sí la completa.
anim.tick(0.10F);
REQUIRE(anim.completed());
}
// 3) Clau no existent: defaults segurs (1 frame loop, no peta).
{
const std::string YAML = R"(
animations:
walk: { frames: [0], frame_ms: 100, loop: true }
)";
auto node = fkyaml::node::deserialize(YAML);
auto anim = AnimationComponent::fromYaml(node, "missing");
REQUIRE(anim.frameCount() == 1);
REQUIRE(anim.loops());
}
// 4) `fps:` no és suportat: ha de llençar excepció.
{
const std::string YAML = R"(
animations:
walk: { frames: [0, 1], fps: 8, loop: true }
)";
auto node = fkyaml::node::deserialize(YAML);
bool threw = false;
try {
(void) AnimationComponent::fromYaml(node, "walk");
} catch (const std::runtime_error&) {
threw = true;
}
REQUIRE(threw);
}
// 5) Flip_h al YAML es preserva al component.
{
const std::string YAML = R"(
animations:
west: { frames: [0, 1], frame_ms: 100, loop: true, flip_h: true }
)";
auto node = fkyaml::node::deserialize(YAML);
auto anim = AnimationComponent::fromYaml(node, "west");
REQUIRE(anim.flipH());
REQUIRE(anim.flipMode() == SDL_FLIP_HORIZONTAL);
}
}
void testAllTemplatesLoad() { void testAllTemplatesLoad() {
std::cout << "[T7] plantilles YAML\n"; std::cout << "[T7] plantilles YAML\n";
const std::vector<std::string> TYPES = { const std::vector<std::string> TYPES = {
@@ -697,6 +783,7 @@ auto main() -> int {
testSarcophagusPerimeter(); testSarcophagusPerimeter();
testEntityManagerEndToEnd(); testEntityManagerEndToEnd();
testAnimationComponent(); testAnimationComponent();
testAnimationFromYaml();
testAllTemplatesLoad(); testAllTemplatesLoad();
testDecorationFlags(); testDecorationFlags();
testLoadFromLevel(); testLoadFromLevel();