Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa77f4cee3 | ||
|
|
a02706ac8b | ||
|
|
2aef925286 | ||
|
|
8eba52353b | ||
|
|
15a4016407 | ||
|
|
2272d3a37a | ||
|
|
3a42c7958a | ||
|
|
dacc4a9909 | ||
|
|
771ff1f6cd | ||
|
|
24609d6c5c | ||
|
|
9d0dde38f9 | ||
|
|
b76a3eb1f3 | ||
|
|
a779533d61 |
No files matched your search
@@ -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.
@@ -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 }
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
@@ -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 }
|
||||||
@@ -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]
|
||||||
@@ -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
@@ -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
@@ -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]
|
|
||||||
@@ -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 {
|
||||||
if (canvas == nullptr || width <= 0 || height <= 0) { return; }
|
|
||||||
|
|
||||||
// Atajo: si la sala està totalment il·luminada i no hi ha
|
// Helper: emissor "patxat" amb el radi efectiu (post-flicker) per
|
||||||
// emissors, la pasada seria un `pixel * 1.0` global — no cal
|
// a aquest frame. Mantenim l'`Emitter` del caller intacte i la
|
||||||
// tocar res. És el camí curt habitual mentre no posem
|
// resta de camps (color, seed, flicker_params, ...) propagats.
|
||||||
// `light_level` a les sales.
|
auto withEffectiveRadius(const Emitter& e, int r_eff) -> Emitter {
|
||||||
const std::uint8_t ROOM = toByte(room_light);
|
return Emitter{
|
||||||
if (ROOM == 255 && emitters.empty()) { return; }
|
.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();
|
auto& lum = luminanceBuffer();
|
||||||
const std::size_t TOTAL = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
|
const std::size_t TOTAL = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
|
||||||
if (lum.size() != TOTAL) { lum.resize(TOTAL); }
|
if (lum.size() != TOTAL) { lum.resize(TOTAL); }
|
||||||
std::ranges::fill(lum, ROOM);
|
std::ranges::fill(lum, room);
|
||||||
|
|
||||||
const auto& cfg = activeConfig();
|
const auto& cfg = activeSubtractiveConfig();
|
||||||
const auto& fc = activeFlicker();
|
|
||||||
const float T_S = static_cast<float>(SDL_GetTicks()) / 1000.0F;
|
const float T_S = static_cast<float>(SDL_GetTicks()) / 1000.0F;
|
||||||
for (const auto& e : emitters) {
|
for (const auto& e : emitters) {
|
||||||
if (e.radius <= 0) { continue; }
|
if (e.radius <= 0) { continue; }
|
||||||
const int R_EFF = e.flicker
|
const int R_EFF = e.flicker ? computeFlickerRadius(e.radius, e.seed, T_S, e.flicker_params) : e.radius;
|
||||||
? computeFlickerRadius(e.radius, e.seed, T_S, fc)
|
|
||||||
: e.radius;
|
|
||||||
const auto RINGS = computeRings(R_EFF, cfg);
|
const auto RINGS = computeRings(R_EFF, cfg);
|
||||||
// El rasteritzador llegeix el radi de l'`Emitter` per al
|
const Emitter EFF = withEffectiveRadius(e, R_EFF);
|
||||||
// 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);
|
rasterizeEmitter(EFF, RINGS, lum.data(), width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
modulateCanvas(canvas, 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; }
|
||||||
|
|
||||||
|
// Atajo per a tots dos modes: sala totalment "neutra" + cap
|
||||||
|
// emissor → la pasada no canviaria res al canvas.
|
||||||
|
// subtractive: room=1.0 i sense emissors → pixel * 1.0 global.
|
||||||
|
// additive: room=1.0 i sense emissors → cap aportació.
|
||||||
|
const std::uint8_t ROOM = toByte(room_light);
|
||||||
|
if (ROOM == 255 && emitters.empty()) { return; }
|
||||||
|
|
||||||
|
if (mode == Mode::ADDITIVE) {
|
||||||
|
applyAdditive(canvas, width, height, ROOM, emitters);
|
||||||
|
} else {
|
||||||
|
applySubtractive(canvas, width, height, ROOM, emitters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Lighting
|
} // namespace Lighting
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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_) {
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,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)
|
||||||
@@ -33,43 +33,19 @@ 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)
|
|
||||||
-> AnimationComponent {
|
|
||||||
std::vector<int> frames = {0};
|
|
||||||
float fps = 8.0F;
|
|
||||||
bool loop = false;
|
|
||||||
if (yaml.contains("animations") && yaml["animations"].contains("swing")) {
|
|
||||||
const auto& sw = yaml["animations"]["swing"];
|
const auto& sw = yaml["animations"]["swing"];
|
||||||
if (sw.contains("frames")) {
|
if (!sw.contains("active_frames")) { return; }
|
||||||
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"]) {
|
for (const auto& f : sw["active_frames"]) {
|
||||||
// cppcheck-suppress useStlAlgorithm
|
// cppcheck-suppress useStlAlgorithm
|
||||||
active_out.push_back(f.get_value<int>());
|
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_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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_),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+275
-266
@@ -37,19 +37,6 @@ namespace {
|
|||||||
// repòs i no contribueix a la velocitat.
|
// repòs i no contribueix a la velocitat.
|
||||||
constexpr Sint16 STICK_DEADZONE = 12000;
|
constexpr Sint16 STICK_DEADZONE = 12000;
|
||||||
|
|
||||||
// Llegeix la mida del frame des del bloc `sprite: { path, size: [w, h] }`
|
|
||||||
// (mateix format que la resta de plantilles d'entitat, vegeu
|
|
||||||
// AnimatedSprite). Extracció per mantenir `Player::loadFrom` sota el
|
|
||||||
// llindar de complexitat cognitiva (25).
|
|
||||||
void readSpriteSize(const fkyaml::node& yaml, const std::string& yaml_path, int& w, int& h) {
|
|
||||||
if (!yaml.contains("sprite") || !yaml["sprite"].contains("size")) {
|
|
||||||
throw std::runtime_error("Player: manca `sprite.size` a " + yaml_path);
|
|
||||||
}
|
|
||||||
const auto& size_node = yaml["sprite"]["size"];
|
|
||||||
w = size_node[0].get_value<int>();
|
|
||||||
h = size_node[1].get_value<int>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Acumula la contribució del mando assignat al jugador (cruceta +
|
// Acumula la contribució del mando assignat al jugador (cruceta +
|
||||||
// stick esquerre) sobre els acumuladors de velocitat. Extracció
|
// stick esquerre) sobre els acumuladors de velocitat. Extracció
|
||||||
// d'`update()` per a mantenir la complexitat cognitiva sota llindar
|
// d'`update()` per a mantenir la complexitat cognitiva sota llindar
|
||||||
@@ -73,15 +60,156 @@ namespace {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Player::TurnConfig Player::turn_config;
|
namespace {
|
||||||
Player::InertiaConfig Player::inertia_config;
|
|
||||||
Player::JumpConfig Player::jump_config;
|
|
||||||
Player::HurtConfig Player::hurt_config;
|
|
||||||
Player::WhipConfig Player::whip_config;
|
|
||||||
Player::DeathConfig Player::death_config;
|
|
||||||
Player::ImmunityConfig Player::immunity_config;
|
|
||||||
|
|
||||||
void Player::loadFrom(const std::string& yaml_path) {
|
// ---- Helpers de lectura per sub-blocs del YAML del personatge ----
|
||||||
|
// Cada helper rep el bloc YAML root i un `Player::Config&` a poblar.
|
||||||
|
// Camps absents respecten els defaults del Config (no reinicialitzen
|
||||||
|
// a 0 — la Config ja ve construïda amb els valors default abans).
|
||||||
|
|
||||||
|
void readSpriteSizeInto(const fkyaml::node& yaml, const std::string& yaml_path, Player::Config& out) {
|
||||||
|
if (!yaml.contains("sprite") || !yaml["sprite"].contains("size")) {
|
||||||
|
throw std::runtime_error("Player: manca `sprite.size` a " + yaml_path);
|
||||||
|
}
|
||||||
|
const auto& size_node = yaml["sprite"]["size"];
|
||||||
|
out.sprite_w = size_node[0].get_value<int>();
|
||||||
|
out.sprite_h = size_node[1].get_value<int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
void readCollisionInto(const fkyaml::node& yaml, const std::string& yaml_path, Player::Config& out) {
|
||||||
|
if (!yaml.contains("collision")) {
|
||||||
|
throw std::runtime_error("Player: manca bloc `collision:` a " + yaml_path);
|
||||||
|
}
|
||||||
|
const auto& c = yaml["collision"];
|
||||||
|
out.collision_size = c["size"].get_value<int>();
|
||||||
|
out.base_size = c["base_size"].get_value<int>();
|
||||||
|
|
||||||
|
// Col·lisió centrada en el quadrat inferior del sprite.
|
||||||
|
out.collision_padding = (out.collision_size - out.base_size) / 2;
|
||||||
|
out.collision_x_offset = -out.collision_padding;
|
||||||
|
out.collision_y_offset = (out.sprite_h - out.base_size) - out.collision_padding;
|
||||||
|
|
||||||
|
// Hit-box opcional (actor-actor): default = caixa de col·lisió.
|
||||||
|
out.hit_size = c.contains("hit_size") ? c["hit_size"].get_value<int>() : out.collision_size;
|
||||||
|
const int HIT_INSET = (out.collision_size - out.hit_size) / 2;
|
||||||
|
out.hit_x_offset = out.collision_x_offset + HIT_INSET;
|
||||||
|
out.hit_y_offset = out.collision_y_offset + HIT_INSET;
|
||||||
|
|
||||||
|
out.body_height = c.contains("height") ? c["height"].get_value<int>() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void readInertiaInto(const fkyaml::node& yaml, Player::Config& out) {
|
||||||
|
if (!yaml.contains("inertia")) { return; }
|
||||||
|
const auto& i = yaml["inertia"];
|
||||||
|
if (i.contains("brake_time_ms")) { out.inertia.brake_time_ms = i["brake_time_ms"].get_value<int>(); }
|
||||||
|
if (i.contains("threshold_ms")) { out.inertia.threshold_ms = i["threshold_ms"].get_value<int>(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void readTurnInto(const fkyaml::node& yaml, Player::Config& out) {
|
||||||
|
if (!yaml.contains("turn")) { return; }
|
||||||
|
const auto& t = yaml["turn"];
|
||||||
|
if (t.contains("duration_ms")) { out.turn.duration_ms = t["duration_ms"].get_value<int>(); }
|
||||||
|
if (!t.contains("transitions")) { return; }
|
||||||
|
const auto& tr = t["transitions"];
|
||||||
|
if (tr.contains("west_to_east")) { out.turn.w_to_e = Player::cardinalFromString(tr["west_to_east"].get_value<std::string>()); }
|
||||||
|
if (tr.contains("east_to_west")) { out.turn.e_to_w = Player::cardinalFromString(tr["east_to_west"].get_value<std::string>()); }
|
||||||
|
if (tr.contains("north_to_south")) { out.turn.n_to_s = Player::cardinalFromString(tr["north_to_south"].get_value<std::string>()); }
|
||||||
|
if (tr.contains("south_to_north")) { out.turn.s_to_n = Player::cardinalFromString(tr["south_to_north"].get_value<std::string>()); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void readJumpInto(const fkyaml::node& yaml, Player::Config& out) {
|
||||||
|
if (!yaml.contains("jump")) { return; }
|
||||||
|
const auto& j = yaml["jump"];
|
||||||
|
if (j.contains("impulse")) { out.jump.impulse_px_per_s = j["impulse"].get_value<float>(); }
|
||||||
|
if (j.contains("gravity")) { out.jump.gravity_px_per_s2 = j["gravity"].get_value<float>(); }
|
||||||
|
if (j.contains("landing_stun_ms")) { out.jump.landing_stun_ms = j["landing_stun_ms"].get_value<int>(); }
|
||||||
|
if (j.contains("sound")) {
|
||||||
|
out.jump.sound_key = j["sound"].get_value<std::string>();
|
||||||
|
Resource::Cache::get().loadSound(out.jump.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + out.jump.sound_key);
|
||||||
|
}
|
||||||
|
if (j.contains("land_sound")) {
|
||||||
|
out.jump.land_sound_key = j["land_sound"].get_value<std::string>();
|
||||||
|
Resource::Cache::get().loadSound(out.jump.land_sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + out.jump.land_sound_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void readHurtInto(const fkyaml::node& yaml, Player::Config& out) {
|
||||||
|
if (!yaml.contains("hurt")) { return; }
|
||||||
|
const auto& h = yaml["hurt"];
|
||||||
|
if (h.contains("speed")) { out.hurt.initial_speed_px_per_s = h["speed"].get_value<float>(); }
|
||||||
|
if (h.contains("brake_time_ms")) { out.hurt.brake_time_ms = h["brake_time_ms"].get_value<int>(); }
|
||||||
|
if (h.contains("sound")) {
|
||||||
|
out.hurt.sound_key = h["sound"].get_value<std::string>();
|
||||||
|
Resource::Cache::get().loadSound(out.hurt.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + out.hurt.sound_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void readWhipInto(const fkyaml::node& yaml, Player::Config& out) {
|
||||||
|
if (!yaml.contains("whip")) { return; }
|
||||||
|
const auto& w = yaml["whip"];
|
||||||
|
if (w.contains("range_tiles")) { out.whip.range_tiles = w["range_tiles"].get_value<float>(); }
|
||||||
|
if (w.contains("swing_ms")) { out.whip.swing_ms = w["swing_ms"].get_value<int>(); }
|
||||||
|
if (w.contains("cooldown_ms")) { out.whip.cooldown_ms = w["cooldown_ms"].get_value<int>(); }
|
||||||
|
if (w.contains("damage")) { out.whip.damage = w["damage"].get_value<int>(); }
|
||||||
|
if (w.contains("sound")) {
|
||||||
|
out.whip.sound_key = w["sound"].get_value<std::string>();
|
||||||
|
Resource::Cache::get().loadSound(out.whip.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + out.whip.sound_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void readDeathInto(const fkyaml::node& yaml, Player::Config& out) {
|
||||||
|
if (!yaml.contains("death")) { return; }
|
||||||
|
const auto& d = yaml["death"];
|
||||||
|
if (d.contains("respawn_delay_ms")) { out.death.respawn_delay_ms = d["respawn_delay_ms"].get_value<int>(); }
|
||||||
|
if (d.contains("lives")) { out.death.initial_lives = d["lives"].get_value<int>(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void readImmunityInto(const fkyaml::node& yaml, Player::Config& out) {
|
||||||
|
if (!yaml.contains("immunity")) { return; }
|
||||||
|
const auto& im = yaml["immunity"];
|
||||||
|
if (im.contains("duration_ms")) { out.immunity.duration_ms = im["duration_ms"].get_value<float>(); }
|
||||||
|
if (im.contains("blink_period_ms")) { out.immunity.blink_period_ms = im["blink_period_ms"].get_value<int>(); }
|
||||||
|
if (im.contains("palette")) {
|
||||||
|
out.immunity.palette_key = im["palette"].get_value<std::string>();
|
||||||
|
Resource::Cache::get().loadPalette(out.immunity.palette_key, std::string{Defaults::Paths::PLAYER_DIR} + out.immunity.palette_key + ".pal");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void readLightInto(const fkyaml::node& yaml, Player::Config& out) {
|
||||||
|
// Punt de partida del flicker: el global de lighting.yaml. El bloc
|
||||||
|
// opcional `flicker_params:` del personatge sobreescriu camps concrets.
|
||||||
|
out.flicker_params = Lighting::flickerConfig();
|
||||||
|
if (!yaml.contains("light")) { return; }
|
||||||
|
const auto& l = yaml["light"];
|
||||||
|
if (l.contains("radius")) { out.light_radius = l["radius"].get_value<int>(); }
|
||||||
|
if (l.contains("flicker")) { out.light_flicker = l["flicker"].get_value<bool>(); }
|
||||||
|
if (l.contains("color")) {
|
||||||
|
const auto& c = l["color"];
|
||||||
|
if (c.is_sequence() && c.size() == 3) {
|
||||||
|
out.light_color_r = static_cast<std::uint8_t>(std::clamp(c[0].get_value<int>(), 0, 255));
|
||||||
|
out.light_color_g = static_cast<std::uint8_t>(std::clamp(c[1].get_value<int>(), 0, 255));
|
||||||
|
out.light_color_b = static_cast<std::uint8_t>(std::clamp(c[2].get_value<int>(), 0, 255));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (l.contains("offset")) {
|
||||||
|
const auto& o = l["offset"];
|
||||||
|
if (o.is_sequence() && o.size() == 2) {
|
||||||
|
out.light_offset_x = o[0].get_value<int>();
|
||||||
|
out.light_offset_y = o[1].get_value<int>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (l.contains("flicker_params")) {
|
||||||
|
const auto& fp = l["flicker_params"];
|
||||||
|
if (fp.contains("wave_amplitude_ratio")) { out.flicker_params.wave_amplitude_ratio = fp["wave_amplitude_ratio"].get_value<float>(); }
|
||||||
|
if (fp.contains("wave_frequency_hz")) { out.flicker_params.wave_frequency_hz = fp["wave_frequency_hz"].get_value<float>(); }
|
||||||
|
if (fp.contains("noise_amplitude_ratio")) { out.flicker_params.noise_amplitude_ratio = fp["noise_amplitude_ratio"].get_value<float>(); }
|
||||||
|
if (fp.contains("noise_frequency_hz")) { out.flicker_params.noise_frequency_hz = fp["noise_frequency_hz"].get_value<float>(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto Player::Config::loadFromYaml(const std::string& yaml_path) -> Player::Config {
|
||||||
auto bytes = Resource::Helper::loadFile(yaml_path);
|
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||||
if (bytes.empty()) {
|
if (bytes.empty()) {
|
||||||
throw std::runtime_error("Player: no s'ha pogut obrir " + yaml_path);
|
throw std::runtime_error("Player: no s'ha pogut obrir " + yaml_path);
|
||||||
@@ -90,172 +218,51 @@ void Player::loadFrom(const std::string& yaml_path) {
|
|||||||
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);
|
||||||
|
|
||||||
readSpriteSize(yaml, yaml_path, sprite_w, sprite_h);
|
Player::Config out;
|
||||||
|
readSpriteSizeInto(yaml, yaml_path, out);
|
||||||
readCollisionBlock(yaml, yaml_path);
|
readCollisionInto(yaml, yaml_path, out);
|
||||||
|
readInertiaInto(yaml, out);
|
||||||
// Bloc opcional `inertia:` — si manca, `brake_time_ms` queda a 0 i
|
readTurnInto(yaml, out);
|
||||||
// la inèrcia de frenada no s'activa (parada instantània com abans).
|
readJumpInto(yaml, out);
|
||||||
inertia_config = InertiaConfig{};
|
readHurtInto(yaml, out);
|
||||||
if (yaml.contains("inertia")) {
|
readWhipInto(yaml, out);
|
||||||
const auto& i = yaml["inertia"];
|
readDeathInto(yaml, out);
|
||||||
if (i.contains("brake_time_ms")) { inertia_config.brake_time_ms = i["brake_time_ms"].get_value<int>(); }
|
readImmunityInto(yaml, out);
|
||||||
if (i.contains("threshold_ms")) { inertia_config.threshold_ms = i["threshold_ms"].get_value<int>(); }
|
readLightInto(yaml, out);
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bloc opcional `turn:` — si manca, `duration_ms` queda a 0 i la
|
void Player::Config::applyRoomLightOverride() {
|
||||||
// mecànica del gir de 180° no s'activa (canvis de direcció instantanis).
|
// Override de sala per al jugador (bloc `lighting.emitters.player`
|
||||||
turn_config = TurnConfig{};
|
// del YAML del mapa). Si la sala no en declara, els valors base del
|
||||||
if (yaml.contains("turn")) {
|
// YAML del personatge es conserven. Si la sala desactiva
|
||||||
const auto& t = yaml["turn"];
|
// (`players: false`), `light_radius = 0` (no emet).
|
||||||
if (t.contains("duration_ms")) { turn_config.duration_ms = t["duration_ms"].get_value<int>(); }
|
const auto* room = Map::getRoomEmitterOverride("player");
|
||||||
if (t.contains("transitions")) {
|
if (room == nullptr) { return; }
|
||||||
const auto& tr = t["transitions"];
|
if (!room->enabled) {
|
||||||
if (tr.contains("west_to_east")) { turn_config.w_to_e = cardinalFromString(tr["west_to_east"].get_value<std::string>()); }
|
|
||||||
if (tr.contains("east_to_west")) { turn_config.e_to_w = cardinalFromString(tr["east_to_west"].get_value<std::string>()); }
|
|
||||||
if (tr.contains("north_to_south")) { turn_config.n_to_s = cardinalFromString(tr["north_to_south"].get_value<std::string>()); }
|
|
||||||
if (tr.contains("south_to_north")) { turn_config.s_to_n = cardinalFromString(tr["south_to_north"].get_value<std::string>()); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bloc opcional `jump:` — si manca, `impulse_px_per_s` queda a 0 i el
|
|
||||||
// botó de salt no fa res. Extret a un mètode estàtic per acotar la
|
|
||||||
// complexitat cognitiva de `loadFrom` sota el llindar de clang-tidy.
|
|
||||||
readJumpBlock(yaml);
|
|
||||||
|
|
||||||
// Bloc opcional `hurt:` — estat post-impacte. Si manca o el speed
|
|
||||||
// és 0, l'estat HURT mai s'activa (cas degenerat).
|
|
||||||
readHurtBlock(yaml);
|
|
||||||
|
|
||||||
// Bloc opcional `whip:` — paràmetres del látigo. Si manca, el
|
|
||||||
// jugador no atacarà efectivament (ATTACKING dura 0 ms i el flanc
|
|
||||||
// d'input es perd).
|
|
||||||
readWhipBlock(yaml);
|
|
||||||
|
|
||||||
// Bloc opcional `death:` — pausa de respawn i comptador inicial de
|
|
||||||
// vides. Si manca, el respawn és immediat i les vides queden a 3
|
|
||||||
// per defecte (no és un valor crític: avui no hi ha game over).
|
|
||||||
readDeathBlock(yaml);
|
|
||||||
|
|
||||||
// Bloc opcional `immunity:` — finestra de gràcia post-spawn. Si
|
|
||||||
// `duration_ms` és 0 (o el bloc manca) la mecànica queda
|
|
||||||
// desactivada i els hazards toquen al primer frame, com abans.
|
|
||||||
readImmunityBlock(yaml);
|
|
||||||
|
|
||||||
// Bloc opcional `light:` — radi i flicker. Extret a un mètode per
|
|
||||||
// mantenir `loadFrom` sota el llindar de complexitat de clang-tidy.
|
|
||||||
readLightBlock(yaml);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::readLightBlock(const fkyaml::node& yaml) {
|
|
||||||
light_radius = 0;
|
light_radius = 0;
|
||||||
light_flicker = false;
|
light_flicker = false;
|
||||||
if (!yaml.contains("light")) { return; }
|
return;
|
||||||
const auto& l = yaml["light"];
|
}
|
||||||
if (l.contains("radius")) { light_radius = l["radius"].get_value<int>(); }
|
if (room->radius.has_value()) { light_radius = *room->radius; }
|
||||||
if (l.contains("flicker")) { light_flicker = l["flicker"].get_value<bool>(); }
|
if (room->flicker.has_value()) { light_flicker = *room->flicker; }
|
||||||
|
if (room->color.has_value()) {
|
||||||
|
light_color_r = (*room->color)[0];
|
||||||
|
light_color_g = (*room->color)[1];
|
||||||
|
light_color_b = (*room->color)[2];
|
||||||
|
}
|
||||||
|
if (room->offset.has_value()) {
|
||||||
|
light_offset_x = (*room->offset)[0];
|
||||||
|
light_offset_y = (*room->offset)[1];
|
||||||
|
}
|
||||||
|
if (room->flicker_wave_amplitude_ratio.has_value()) { flicker_params.wave_amplitude_ratio = *room->flicker_wave_amplitude_ratio; }
|
||||||
|
if (room->flicker_wave_frequency_hz.has_value()) { flicker_params.wave_frequency_hz = *room->flicker_wave_frequency_hz; }
|
||||||
|
if (room->flicker_noise_amplitude_ratio.has_value()) { flicker_params.noise_amplitude_ratio = *room->flicker_noise_amplitude_ratio; }
|
||||||
|
if (room->flicker_noise_frequency_hz.has_value()) { flicker_params.noise_frequency_hz = *room->flicker_noise_frequency_hz; }
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path) {
|
void Player::setConfig(Config cfg) {
|
||||||
if (!yaml.contains("collision")) {
|
cfg_ = std::move(cfg);
|
||||||
throw std::runtime_error("Player: manca bloc `collision:` a " + yaml_path);
|
|
||||||
}
|
|
||||||
const auto& c = yaml["collision"];
|
|
||||||
collision_size = c["size"].get_value<int>();
|
|
||||||
base_size = c["base_size"].get_value<int>();
|
|
||||||
|
|
||||||
// Col·lisió centrada en el quadrat inferior del sprite. Si la caixa és
|
|
||||||
// més gran que la base (cas habitual, 16 vs 12) el padding és simètric
|
|
||||||
// a cada costat; si són iguals, padding=0 i la caixa coincideix amb la
|
|
||||||
// base sense offsets.
|
|
||||||
collision_padding = (collision_size - base_size) / 2;
|
|
||||||
collision_x_offset = -collision_padding;
|
|
||||||
collision_y_offset = (sprite_h - base_size) - collision_padding;
|
|
||||||
|
|
||||||
// Hit-box opcional (actor-actor). Si no hi és, s'iguala a la base
|
|
||||||
// per preservar compatibilitat amb YAMLs antics. Concèntrica amb la
|
|
||||||
// caixa de col·lisió: offset interior = (collision_size - hit_size) / 2.
|
|
||||||
hit_size = c.contains("hit_size") ? c["hit_size"].get_value<int>() : collision_size;
|
|
||||||
const int HIT_INSET = (collision_size - hit_size) / 2;
|
|
||||||
hit_x_offset = collision_x_offset + HIT_INSET;
|
|
||||||
hit_y_offset = collision_y_offset + HIT_INSET;
|
|
||||||
|
|
||||||
// Alçada del cos, llegida del mateix bloc per coherència amb la
|
|
||||||
// resta d'entitats (mummy, sarcophagus, etc.). 0 = sense filtre per
|
|
||||||
// altura — el salt no esquiva l'altre jugador.
|
|
||||||
body_height = c.contains("height") ? c["height"].get_value<int>() : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::readJumpBlock(const fkyaml::node& yaml) {
|
|
||||||
jump_config = JumpConfig{};
|
|
||||||
if (!yaml.contains("jump")) { return; }
|
|
||||||
const auto& j = yaml["jump"];
|
|
||||||
if (j.contains("impulse")) { jump_config.impulse_px_per_s = j["impulse"].get_value<float>(); }
|
|
||||||
if (j.contains("gravity")) { jump_config.gravity_px_per_s2 = j["gravity"].get_value<float>(); }
|
|
||||||
if (j.contains("landing_stun_ms")) { jump_config.landing_stun_ms = j["landing_stun_ms"].get_value<int>(); }
|
|
||||||
// Sons opcionals: el nom del fitxer (relatiu a Defaults::Paths::AUDIO_SOUNDS_DIR)
|
|
||||||
// val tant de clau al Resource::Cache com de basename per a la càrrega.
|
|
||||||
// Si el camp manca, la clau queda buida i `playSound` no es crida.
|
|
||||||
if (j.contains("sound")) {
|
|
||||||
jump_config.sound_key = j["sound"].get_value<std::string>();
|
|
||||||
Resource::Cache::get().loadSound(jump_config.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + jump_config.sound_key);
|
|
||||||
}
|
|
||||||
if (j.contains("land_sound")) {
|
|
||||||
jump_config.land_sound_key = j["land_sound"].get_value<std::string>();
|
|
||||||
Resource::Cache::get().loadSound(jump_config.land_sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + jump_config.land_sound_key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::readHurtBlock(const fkyaml::node& yaml) {
|
|
||||||
hurt_config = HurtConfig{};
|
|
||||||
if (!yaml.contains("hurt")) { return; }
|
|
||||||
const auto& h = yaml["hurt"];
|
|
||||||
if (h.contains("speed")) { hurt_config.initial_speed_px_per_s = h["speed"].get_value<float>(); }
|
|
||||||
if (h.contains("brake_time_ms")) { hurt_config.brake_time_ms = h["brake_time_ms"].get_value<int>(); }
|
|
||||||
// So d'entrada a HURT — mateix patró que `jump.sound` i `whip.sound`.
|
|
||||||
if (h.contains("sound")) {
|
|
||||||
hurt_config.sound_key = h["sound"].get_value<std::string>();
|
|
||||||
Resource::Cache::get().loadSound(hurt_config.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + hurt_config.sound_key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::readDeathBlock(const fkyaml::node& yaml) {
|
|
||||||
death_config = DeathConfig{};
|
|
||||||
if (!yaml.contains("death")) { return; }
|
|
||||||
const auto& d = yaml["death"];
|
|
||||||
if (d.contains("respawn_delay_ms")) { death_config.respawn_delay_ms = d["respawn_delay_ms"].get_value<int>(); }
|
|
||||||
if (d.contains("lives")) { death_config.initial_lives = d["lives"].get_value<int>(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::readImmunityBlock(const fkyaml::node& yaml) {
|
|
||||||
immunity_config = ImmunityConfig{};
|
|
||||||
if (!yaml.contains("immunity")) { return; }
|
|
||||||
const auto& im = yaml["immunity"];
|
|
||||||
if (im.contains("duration_ms")) { immunity_config.duration_ms = im["duration_ms"].get_value<float>(); }
|
|
||||||
if (im.contains("blink_period_ms")) { immunity_config.blink_period_ms = im["blink_period_ms"].get_value<int>(); }
|
|
||||||
// Paleta del blink: el basename val tant de clau al Resource::Cache
|
|
||||||
// com de path (`Defaults::Paths::PLAYER_DIR + key + .pal`). Mateix patró
|
|
||||||
// que els sons.
|
|
||||||
if (im.contains("palette")) {
|
|
||||||
immunity_config.palette_key = im["palette"].get_value<std::string>();
|
|
||||||
Resource::Cache::get().loadPalette(immunity_config.palette_key, std::string{Defaults::Paths::PLAYER_DIR} + immunity_config.palette_key + ".pal");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::readWhipBlock(const fkyaml::node& yaml) {
|
|
||||||
whip_config = WhipConfig{};
|
|
||||||
if (!yaml.contains("whip")) { return; }
|
|
||||||
const auto& w = yaml["whip"];
|
|
||||||
if (w.contains("range_tiles")) { whip_config.range_tiles = w["range_tiles"].get_value<float>(); }
|
|
||||||
if (w.contains("swing_ms")) { whip_config.swing_ms = w["swing_ms"].get_value<int>(); }
|
|
||||||
if (w.contains("cooldown_ms")) { whip_config.cooldown_ms = w["cooldown_ms"].get_value<int>(); }
|
|
||||||
if (w.contains("damage")) { whip_config.damage = w["damage"].get_value<int>(); }
|
|
||||||
// So del swing: mateix patró que `jump.sound` — basename val de
|
|
||||||
// clau al cache i de path. Si manca, queda buit i no es dispara.
|
|
||||||
if (w.contains("sound")) {
|
|
||||||
whip_config.sound_key = w["sound"].get_value<std::string>();
|
|
||||||
Resource::Cache::get().loadSound(whip_config.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + whip_config.sound_key);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::applyCardinal(Cardinal c) {
|
void Player::applyCardinal(Cardinal c) {
|
||||||
@@ -314,16 +321,16 @@ auto Player::opposite(Cardinal c) -> Cardinal {
|
|||||||
return Cardinal::NONE;
|
return Cardinal::NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Player::intermediateFor(Cardinal target) -> Cardinal {
|
auto Player::intermediateFor(Cardinal target) const -> Cardinal {
|
||||||
switch (target) {
|
switch (target) {
|
||||||
case Cardinal::EAST:
|
case Cardinal::EAST:
|
||||||
return turn_config.w_to_e;
|
return cfg_.turn.w_to_e;
|
||||||
case Cardinal::WEST:
|
case Cardinal::WEST:
|
||||||
return turn_config.e_to_w;
|
return cfg_.turn.e_to_w;
|
||||||
case Cardinal::SOUTH:
|
case Cardinal::SOUTH:
|
||||||
return turn_config.n_to_s;
|
return cfg_.turn.n_to_s;
|
||||||
case Cardinal::NORTH:
|
case Cardinal::NORTH:
|
||||||
return turn_config.s_to_n;
|
return cfg_.turn.s_to_n;
|
||||||
case Cardinal::NONE:
|
case Cardinal::NONE:
|
||||||
return Cardinal::NONE;
|
return Cardinal::NONE;
|
||||||
}
|
}
|
||||||
@@ -361,11 +368,11 @@ void Player::updateFacingAndTurn(float delta_time, float vx, float vy) {
|
|||||||
// a la prèvia. Requereix també que el bloc `turn:` del YAML
|
// a la prèvia. Requereix també que el bloc `turn:` del YAML
|
||||||
// estiga activat (`duration_ms > 0`).
|
// estiga activat (`duration_ms > 0`).
|
||||||
const bool TRIGGER_TURN =
|
const bool TRIGGER_TURN =
|
||||||
(turn_config.duration_ms > 0) && INPUT_CARD != Cardinal::NONE && last_input_cardinal_ != Cardinal::NONE && INPUT_CARD == opposite(last_input_cardinal_);
|
(cfg_.turn.duration_ms > 0) && INPUT_CARD != Cardinal::NONE && last_input_cardinal_ != Cardinal::NONE && INPUT_CARD == opposite(last_input_cardinal_);
|
||||||
if (TRIGGER_TURN) {
|
if (TRIGGER_TURN) {
|
||||||
pending_cardinal_ = INPUT_CARD;
|
pending_cardinal_ = INPUT_CARD;
|
||||||
intermediate_cardinal_ = intermediateFor(INPUT_CARD);
|
intermediate_cardinal_ = intermediateFor(INPUT_CARD);
|
||||||
turning_remaining_ms_ = static_cast<float>(turn_config.duration_ms);
|
turning_remaining_ms_ = static_cast<float>(cfg_.turn.duration_ms);
|
||||||
applyCardinal(intermediate_cardinal_);
|
applyCardinal(intermediate_cardinal_);
|
||||||
} else if (vy < 0.0F) {
|
} else if (vy < 0.0F) {
|
||||||
// Tria de facing amb prevalença vertical: si hi ha component
|
// Tria de facing amb prevalença vertical: si hi ha component
|
||||||
@@ -389,8 +396,9 @@ void Player::updateFacingAndTurn(float delta_time, float vx, float vy) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Player::Player(const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index)
|
Player::Player(Config cfg, const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index)
|
||||||
: AnimatedSprite(Resource::Cache::get().getAnimationData(anim_key)),
|
: AnimatedSprite(Resource::Cache::get().getAnimationData(anim_key)),
|
||||||
|
cfg_(std::move(cfg)),
|
||||||
id_(id),
|
id_(id),
|
||||||
controls_(controls),
|
controls_(controls),
|
||||||
map_(map_info),
|
map_(map_info),
|
||||||
@@ -404,7 +412,7 @@ Player::Player(const std::string& anim_key, int id, float x, float y, const Cont
|
|||||||
setCurrentAnimation("stand_south");
|
setCurrentAnimation("stand_south");
|
||||||
// Comptador d'intents inicialitzat amb el valor del YAML (default 3
|
// Comptador d'intents inicialitzat amb el valor del YAML (default 3
|
||||||
// si el bloc `death:` manca). Fora del cicle DYING no toca a ningú.
|
// si el bloc `death:` manca). Fora del cicle DYING no toca a ningú.
|
||||||
lives_remaining_ = death_config.initial_lives;
|
lives_remaining_ = cfg_.death.initial_lives;
|
||||||
// Spawn point inicial: la posició del ctor val per defecte; el
|
// Spawn point inicial: la posició del ctor val per defecte; el
|
||||||
// GameScene el reescriu via `setSpawnPoint` quan canvia de sala.
|
// GameScene el reescriu via `setSpawnPoint` quan canvia de sala.
|
||||||
spawn_x_ = x;
|
spawn_x_ = x;
|
||||||
@@ -447,7 +455,7 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
|||||||
// travessa net del caminant; ningú actua de "muro" durant el
|
// travessa net del caminant; ningú actua de "muro" durant el
|
||||||
// vol, ni a la pujada ni a la baixada.
|
// vol, ni a la pujada ni a la baixada.
|
||||||
// - Tots dos en l'aire → col·lisió només si els rangs verticals
|
// - Tots dos en l'aire → col·lisió només si els rangs verticals
|
||||||
// [jump_z, jump_z + body_height] solapen. Així permetem el xoc
|
// [jump_z, jump_z + cfg_.body_height] solapen. Així permetem el xoc
|
||||||
// mid-aire quan tots dos van un contra l'altre i estan a la
|
// mid-aire quan tots dos van un contra l'altre i estan a la
|
||||||
// mateixa franja d'alçada.
|
// mateixa franja d'alçada.
|
||||||
const SDL_FRect PROPOSED_HIT = hitBoxFromBase(box);
|
const SDL_FRect PROPOSED_HIT = hitBoxFromBase(box);
|
||||||
@@ -466,7 +474,7 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
|||||||
// Aquí els dos estats coincideixen. Si tots dos volen, el filtre
|
// Aquí els dos estats coincideixen. Si tots dos volen, el filtre
|
||||||
// per altura mira que els rangs verticals encara solapin; si no,
|
// per altura mira que els rangs verticals encara solapin; si no,
|
||||||
// estan a prou distància en Y "real" i no es toquen.
|
// estan a prou distància en Y "real" i no es toquen.
|
||||||
if (MY_AIR && body_height > 0 && std::fabs(MY_Z - p->jump_z_) >= static_cast<float>(body_height)) {
|
if (MY_AIR && cfg_.body_height > 0 && std::fabs(MY_Z - p->jump_z_) >= static_cast<float>(cfg_.body_height)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const auto OTHER = p->getHitBox();
|
const auto OTHER = p->getHitBox();
|
||||||
@@ -474,7 +482,7 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::integrateAxis(float& phys, float input, float delta_time, float motion_time_ms, bool any_input) {
|
void Player::integrateAxis(float& phys, float input, float delta_time, float motion_time_ms, bool any_input) const {
|
||||||
// Acceleració instantània: mentre hi ha input, la velocitat salta a
|
// Acceleració instantània: mentre hi ha input, la velocitat salta a
|
||||||
// ±SPEED sense transició. Cobreix també els canvis de direcció (un
|
// ±SPEED sense transició. Cobreix també els canvis de direcció (un
|
||||||
// coast cap a la dreta passa a -SPEED immediatament si es prem esquerra).
|
// coast cap a la dreta passa a -SPEED immediatament si es prem esquerra).
|
||||||
@@ -492,12 +500,12 @@ void Player::integrateAxis(float& phys, float input, float delta_time, float mot
|
|||||||
phys = 0.0F;
|
phys = 0.0F;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const bool INERTIA_ACTIVE = (inertia_config.brake_time_ms > 0) && (motion_time_ms >= static_cast<float>(inertia_config.threshold_ms));
|
const bool INERTIA_ACTIVE = (cfg_.inertia.brake_time_ms > 0) && (motion_time_ms >= static_cast<float>(cfg_.inertia.threshold_ms));
|
||||||
if (!INERTIA_ACTIVE) {
|
if (!INERTIA_ACTIVE) {
|
||||||
phys = 0.0F;
|
phys = 0.0F;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const float DECEL = SPEED_PX_PER_S / (static_cast<float>(inertia_config.brake_time_ms) / 1000.0F);
|
const float DECEL = SPEED_PX_PER_S / (static_cast<float>(cfg_.inertia.brake_time_ms) / 1000.0F);
|
||||||
const float STEP = DECEL * delta_time;
|
const float STEP = DECEL * delta_time;
|
||||||
if (std::fabs(phys) <= STEP) {
|
if (std::fabs(phys) <= STEP) {
|
||||||
phys = 0.0F;
|
phys = 0.0F;
|
||||||
@@ -620,8 +628,8 @@ void Player::tick(float delta_time) {
|
|||||||
if (state_ == State::HURT) {
|
if (state_ == State::HURT) {
|
||||||
(void)consumeJumpEdge();
|
(void)consumeJumpEdge();
|
||||||
(void)consumeAttackEdge();
|
(void)consumeAttackEdge();
|
||||||
const float DECEL = (hurt_config.brake_time_ms > 0)
|
const float DECEL = (cfg_.hurt.brake_time_ms > 0)
|
||||||
? hurt_config.initial_speed_px_per_s / (static_cast<float>(hurt_config.brake_time_ms) / 1000.0F)
|
? cfg_.hurt.initial_speed_px_per_s / (static_cast<float>(cfg_.hurt.brake_time_ms) / 1000.0F)
|
||||||
: 0.0F;
|
: 0.0F;
|
||||||
const float STEP = DECEL * delta_time;
|
const float STEP = DECEL * delta_time;
|
||||||
auto decay = [STEP](float& v) {
|
auto decay = [STEP](float& v) {
|
||||||
@@ -706,7 +714,7 @@ void Player::tick(float delta_time) {
|
|||||||
// doble per excés que perdre l'atac per un toc accidental al salt).
|
// doble per excés que perdre l'atac per un toc accidental al salt).
|
||||||
// En entrar a ATTACKING, descartem el flanc del salt per no que
|
// En entrar a ATTACKING, descartem el flanc del salt per no que
|
||||||
// queda penjat per a quan es desbloque.
|
// queda penjat per a quan es desbloque.
|
||||||
if (consumeAttackEdge() && whip_config.swing_ms > 0) {
|
if (consumeAttackEdge() && cfg_.whip.swing_ms > 0) {
|
||||||
enterAttacking(currentFacingCardinal());
|
enterAttacking(currentFacingCardinal());
|
||||||
(void)consumeJumpEdge();
|
(void)consumeJumpEdge();
|
||||||
return;
|
return;
|
||||||
@@ -719,7 +727,7 @@ void Player::tick(float delta_time) {
|
|||||||
// ho gestionem deixant que el resolver vinga, i si en el moment del
|
// ho gestionem deixant que el resolver vinga, i si en el moment del
|
||||||
// despegue (consumeJumpEdge → startJump) ja estem AIRBORNE, l'altre
|
// despegue (consumeJumpEdge → startJump) ja estem AIRBORNE, l'altre
|
||||||
// braç del switch no canvia res.
|
// braç del switch no canvia res.
|
||||||
if (consumeJumpEdge() && jump_config.impulse_px_per_s > 0.0F) {
|
if (consumeJumpEdge() && cfg_.jump.impulse_px_per_s > 0.0F) {
|
||||||
startJump();
|
startJump();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -795,7 +803,7 @@ auto Player::currentFacingCardinal() const -> Cardinal {
|
|||||||
|
|
||||||
void Player::enterAttacking(Cardinal facing) {
|
void Player::enterAttacking(Cardinal facing) {
|
||||||
state_ = State::ATTACKING;
|
state_ = State::ATTACKING;
|
||||||
attacking_remaining_ms_ = static_cast<float>(whip_config.swing_ms + whip_config.cooldown_ms);
|
attacking_remaining_ms_ = static_cast<float>(cfg_.whip.swing_ms + cfg_.whip.cooldown_ms);
|
||||||
|
|
||||||
// Garantim que facing/flip i cardinal queden coherents amb la
|
// Garantim que facing/flip i cardinal queden coherents amb la
|
||||||
// direcció triada (per si l'atac s'ha disparat amb facing residual,
|
// direcció triada (per si l'atac s'ha disparat amb facing residual,
|
||||||
@@ -807,8 +815,8 @@ void Player::enterAttacking(Cardinal facing) {
|
|||||||
// So del swing — disparat ací (no des del WhipAttack) perquè és el
|
// So del swing — disparat ací (no des del WhipAttack) perquè és el
|
||||||
// jugador qui executa l'acció; si demà el spawn falla per qualsevol
|
// jugador qui executa l'acció; si demà el spawn falla per qualsevol
|
||||||
// motiu, el so del gest segueix sonant.
|
// motiu, el so del gest segueix sonant.
|
||||||
if (!whip_config.sound_key.empty() && Audio::get() != nullptr) {
|
if (!cfg_.whip.sound_key.empty() && Audio::get() != nullptr) {
|
||||||
Audio::get()->playSound(whip_config.sound_key, Audio::Group::GAME);
|
Audio::get()->playSound(cfg_.whip.sound_key, Audio::Group::GAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mapa de Cardinal → int al payload (vegeu `WhipSpawnPayload`).
|
// Mapa de Cardinal → int al payload (vegeu `WhipSpawnPayload`).
|
||||||
@@ -834,7 +842,7 @@ void Player::enterAttacking(Cardinal facing) {
|
|||||||
// en deriva tant la draw_rect (sprite del látigo, més gran) com la
|
// en deriva tant la draw_rect (sprite del látigo, més gran) com la
|
||||||
// damage_rect (al nivell dels peus, retallada per bloquejadors).
|
// damage_rect (al nivell dels peus, retallada per bloquejadors).
|
||||||
if (bus_ != nullptr) {
|
if (bus_ != nullptr) {
|
||||||
const float RANGE_PX = whip_config.range_tiles * static_cast<float>(Map::tile_size);
|
const float RANGE_PX = cfg_.whip.range_tiles * static_cast<float>(Map::tile_size);
|
||||||
bus_->publish("whip_spawn", WhipSpawnPayload{
|
bus_->publish("whip_spawn", WhipSpawnPayload{
|
||||||
.owner = this,
|
.owner = this,
|
||||||
.direction = dir_int,
|
.direction = dir_int,
|
||||||
@@ -868,16 +876,16 @@ void Player::startJump() {
|
|||||||
|
|
||||||
state_ = State::AIRBORNE;
|
state_ = State::AIRBORNE;
|
||||||
jump_z_ = 0.0F;
|
jump_z_ = 0.0F;
|
||||||
jump_vz_ = jump_config.impulse_px_per_s;
|
jump_vz_ = cfg_.jump.impulse_px_per_s;
|
||||||
if (!jump_config.sound_key.empty() && Audio::get() != nullptr) {
|
if (!cfg_.jump.sound_key.empty() && Audio::get() != nullptr) {
|
||||||
Audio::get()->playSound(jump_config.sound_key, Audio::Group::GAME);
|
Audio::get()->playSound(cfg_.jump.sound_key, Audio::Group::GAME);
|
||||||
}
|
}
|
||||||
// Avisem al bus perquè el GameScene puga spawnejar el polvet de
|
// Avisem al bus perquè el GameScene puga spawnejar el polvet de
|
||||||
// despegue al peu del jugador.
|
// despegue al peu del jugador.
|
||||||
if (bus_ != nullptr) {
|
if (bus_ != nullptr) {
|
||||||
bus_->publish(Event{
|
bus_->publish(Event{
|
||||||
.name = "player_jumped",
|
.name = "player_jumped",
|
||||||
.payload = WorldPos{.x = getPosX() + (static_cast<float>(sprite_w) * 0.5F), .y = getGroundFeetY()},
|
.payload = WorldPos{.x = getPosX() + (static_cast<float>(cfg_.sprite_w) * 0.5F), .y = getGroundFeetY()},
|
||||||
.player_id = id_,
|
.player_id = id_,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -921,7 +929,7 @@ auto Player::chooseHurtDirection(Cardinal preferred) const -> Cardinal {
|
|||||||
// abans d'aquesta distància, l'usem; d'altra manera, busquem la
|
// abans d'aquesta distància, l'usem; d'altra manera, busquem la
|
||||||
// millor.
|
// millor.
|
||||||
const auto REQUIRED = static_cast<int>(
|
const auto REQUIRED = static_cast<int>(
|
||||||
0.5F * hurt_config.initial_speed_px_per_s * (static_cast<float>(hurt_config.brake_time_ms) / 1000.0F));
|
0.5F * cfg_.hurt.initial_speed_px_per_s * (static_cast<float>(cfg_.hurt.brake_time_ms) / 1000.0F));
|
||||||
|
|
||||||
if (preferred != Cardinal::NONE) {
|
if (preferred != Cardinal::NONE) {
|
||||||
const auto [DX, DY] = deltasFor(preferred);
|
const auto [DX, DY] = deltasFor(preferred);
|
||||||
@@ -950,14 +958,14 @@ void Player::enterHurt(Cardinal preferred) {
|
|||||||
|
|
||||||
// Si la mecànica no està configurada (speed = 0), no fem res — el
|
// Si la mecànica no està configurada (speed = 0), no fem res — el
|
||||||
// comportament queda equivalent al d'abans, sense empenta.
|
// comportament queda equivalent al d'abans, sense empenta.
|
||||||
if (hurt_config.initial_speed_px_per_s <= 0.0F || hurt_config.brake_time_ms <= 0) { return; }
|
if (cfg_.hurt.initial_speed_px_per_s <= 0.0F || cfg_.hurt.brake_time_ms <= 0) { return; }
|
||||||
|
|
||||||
const Cardinal CHOSEN = chooseHurtDirection(preferred);
|
const Cardinal CHOSEN = chooseHurtDirection(preferred);
|
||||||
|
|
||||||
// Activem l'estat i fixem la velocitat segons la direcció triada.
|
// Activem l'estat i fixem la velocitat segons la direcció triada.
|
||||||
state_ = State::HURT;
|
state_ = State::HURT;
|
||||||
if (!hurt_config.sound_key.empty() && Audio::get() != nullptr) {
|
if (!cfg_.hurt.sound_key.empty() && Audio::get() != nullptr) {
|
||||||
Audio::get()->playSound(hurt_config.sound_key, Audio::Group::GAME);
|
Audio::get()->playSound(cfg_.hurt.sound_key, Audio::Group::GAME);
|
||||||
}
|
}
|
||||||
applyCardinal(CHOSEN);
|
applyCardinal(CHOSEN);
|
||||||
last_input_cardinal_ = CHOSEN;
|
last_input_cardinal_ = CHOSEN;
|
||||||
@@ -965,7 +973,7 @@ void Player::enterHurt(Cardinal preferred) {
|
|||||||
pending_cardinal_ = Cardinal::NONE;
|
pending_cardinal_ = Cardinal::NONE;
|
||||||
intermediate_cardinal_ = Cardinal::NONE;
|
intermediate_cardinal_ = Cardinal::NONE;
|
||||||
being_pushed_ = false;
|
being_pushed_ = false;
|
||||||
const float V = hurt_config.initial_speed_px_per_s;
|
const float V = cfg_.hurt.initial_speed_px_per_s;
|
||||||
const auto [DX, DY] = deltasFor(CHOSEN);
|
const auto [DX, DY] = deltasFor(CHOSEN);
|
||||||
intended_vx_ = DX * V;
|
intended_vx_ = DX * V;
|
||||||
intended_vy_ = DY * V;
|
intended_vy_ = DY * V;
|
||||||
@@ -976,7 +984,7 @@ void Player::enterHurt(Cardinal preferred) {
|
|||||||
|
|
||||||
auto Player::integrateJump(float delta_time) -> bool {
|
auto Player::integrateJump(float delta_time) -> bool {
|
||||||
if (state_ != State::AIRBORNE) { return false; }
|
if (state_ != State::AIRBORNE) { return false; }
|
||||||
jump_vz_ -= jump_config.gravity_px_per_s2 * delta_time;
|
jump_vz_ -= cfg_.jump.gravity_px_per_s2 * delta_time;
|
||||||
jump_z_ += jump_vz_ * delta_time;
|
jump_z_ += jump_vz_ * delta_time;
|
||||||
// Caiguda per agujero: el sòl ja no és present sota el jugador, així
|
// Caiguda per agujero: el sòl ja no és present sota el jugador, així
|
||||||
// que NO clampem `jump_z_` a 0. Deixem baixar a negatius (visualment
|
// que NO clampem `jump_z_` a 0. Deixem baixar a negatius (visualment
|
||||||
@@ -1015,22 +1023,22 @@ auto Player::integrateJump(float delta_time) -> bool {
|
|||||||
jump_z_ = 0.0F;
|
jump_z_ = 0.0F;
|
||||||
jump_vz_ = 0.0F;
|
jump_vz_ = 0.0F;
|
||||||
state_ = State::LANDING_STUN;
|
state_ = State::LANDING_STUN;
|
||||||
landing_stun_remaining_ms_ = static_cast<float>(jump_config.landing_stun_ms);
|
landing_stun_remaining_ms_ = static_cast<float>(cfg_.jump.landing_stun_ms);
|
||||||
if (!jump_config.land_sound_key.empty() && Audio::get() != nullptr) {
|
if (!cfg_.jump.land_sound_key.empty() && Audio::get() != nullptr) {
|
||||||
// Consumeix el preset armat per GameScene al spawn aeri
|
// Consumeix el preset armat per GameScene al spawn aeri
|
||||||
// d'INTRO (single-shot): el primer LAND el sona reverberat,
|
// d'INTRO (single-shot): el primer LAND el sona reverberat,
|
||||||
// els salts següents cauen al camí sec.
|
// els salts següents cauen al camí sec.
|
||||||
if (next_land_reverb_preset_.empty()) {
|
if (next_land_reverb_preset_.empty()) {
|
||||||
Audio::get()->playSound(jump_config.land_sound_key, Audio::Group::GAME);
|
Audio::get()->playSound(cfg_.jump.land_sound_key, Audio::Group::GAME);
|
||||||
} else {
|
} else {
|
||||||
Audio::get()->playSoundWithReverb(jump_config.land_sound_key, next_land_reverb_preset_, Audio::Group::GAME);
|
Audio::get()->playSoundWithReverb(cfg_.jump.land_sound_key, next_land_reverb_preset_, Audio::Group::GAME);
|
||||||
next_land_reverb_preset_.clear();
|
next_land_reverb_preset_.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (bus_ != nullptr) {
|
if (bus_ != nullptr) {
|
||||||
bus_->publish(Event{
|
bus_->publish(Event{
|
||||||
.name = "player_landed",
|
.name = "player_landed",
|
||||||
.payload = WorldPos{.x = getPosX() + (static_cast<float>(sprite_w) * 0.5F), .y = getGroundFeetY()},
|
.payload = WorldPos{.x = getPosX() + (static_cast<float>(cfg_.sprite_w) * 0.5F), .y = getGroundFeetY()},
|
||||||
.player_id = id_,
|
.player_id = id_,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1259,8 +1267,8 @@ void Player::enterDying(DeathCause cause) {
|
|||||||
// So del cop letal: reaprofitem `hurt.wav` (ja precarregat per
|
// So del cop letal: reaprofitem `hurt.wav` (ja precarregat per
|
||||||
// `readHurtBlock`). El bloc `death:` no té camp de so propi —
|
// `readHurtBlock`). El bloc `death:` no té camp de so propi —
|
||||||
// semànticament l'impacte mortal és la versió forta de `hurt`.
|
// semànticament l'impacte mortal és la versió forta de `hurt`.
|
||||||
if (!hurt_config.sound_key.empty() && Audio::get() != nullptr) {
|
if (!cfg_.hurt.sound_key.empty() && Audio::get() != nullptr) {
|
||||||
Audio::get()->playSound(hurt_config.sound_key, Audio::Group::GAME);
|
Audio::get()->playSound(cfg_.hurt.sound_key, Audio::Group::GAME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1273,7 +1281,7 @@ void Player::applyDyingMovement(float delta_time) {
|
|||||||
animate(delta_time);
|
animate(delta_time);
|
||||||
if (!death_animation_complete_ && animationIsCompleted()) {
|
if (!death_animation_complete_ && animationIsCompleted()) {
|
||||||
death_animation_complete_ = true;
|
death_animation_complete_ = true;
|
||||||
respawn_remaining_ms_ = static_cast<float>(death_config.respawn_delay_ms);
|
respawn_remaining_ms_ = static_cast<float>(cfg_.death.respawn_delay_ms);
|
||||||
}
|
}
|
||||||
if (death_animation_complete_) {
|
if (death_animation_complete_) {
|
||||||
respawn_remaining_ms_ -= delta_time * 1000.0F;
|
respawn_remaining_ms_ -= delta_time * 1000.0F;
|
||||||
@@ -1284,7 +1292,7 @@ void Player::applyDyingMovement(float delta_time) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Player::resetForJoin() {
|
void Player::resetForJoin() {
|
||||||
lives_remaining_ = death_config.initial_lives;
|
lives_remaining_ = cfg_.death.initial_lives;
|
||||||
score_ = 0;
|
score_ = 0;
|
||||||
treasures_ = 0;
|
treasures_ = 0;
|
||||||
// Publica l'estat inicial perquè el HUD reompla el bloc en el moment
|
// Publica l'estat inicial perquè el HUD reompla el bloc en el moment
|
||||||
@@ -1453,12 +1461,12 @@ void Player::render() {
|
|||||||
// swap independentment.
|
// swap independentment.
|
||||||
const bool BLINK_ON =
|
const bool BLINK_ON =
|
||||||
immunity_remaining_ms_ > 0.0F &&
|
immunity_remaining_ms_ > 0.0F &&
|
||||||
immunity_config.blink_period_ms > 0 &&
|
cfg_.immunity.blink_period_ms > 0 &&
|
||||||
!immunity_config.palette_key.empty() &&
|
!cfg_.immunity.palette_key.empty() &&
|
||||||
surface_ != nullptr &&
|
surface_ != nullptr &&
|
||||||
(static_cast<int>(immunity_remaining_ms_) / immunity_config.blink_period_ms) % 2 == 0;
|
(static_cast<int>(immunity_remaining_ms_) / cfg_.immunity.blink_period_ms) % 2 == 0;
|
||||||
if (BLINK_ON) {
|
if (BLINK_ON) {
|
||||||
surface_->setPalette(Resource::Cache::get().getPalette(immunity_config.palette_key));
|
surface_->setPalette(Resource::Cache::get().getPalette(cfg_.immunity.palette_key));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jump_z_ == 0.0F) {
|
if (jump_z_ == 0.0F) {
|
||||||
@@ -1479,9 +1487,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>(cfg_.sprite_w) * 0.5F) + static_cast<float>(cfg_.light_offset_x),
|
||||||
.y = getPosY() - jump_z_ + (static_cast<float>(sprite_h) * 0.5F),
|
.y = getPosY() - jump_z_ + (static_cast<float>(cfg_.sprite_h) * 0.5F) + static_cast<float>(cfg_.light_offset_y),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1496,14 +1505,14 @@ void Player::renderShadow() const {
|
|||||||
if (falling_through_hole_) { return; }
|
if (falling_through_hole_) { return; }
|
||||||
|
|
||||||
// `height_factor`: 0 al terra → fotograma més gran, 1 al pic → més
|
// `height_factor`: 0 al terra → fotograma més gran, 1 al pic → més
|
||||||
// petit. Si el `jump_config` té un divisor zero (cas degenerat) caem
|
// petit. Si el `cfg_.jump` té un divisor zero (cas degenerat) caem
|
||||||
// a 0 per no propagar nan al renderer.
|
// a 0 per no propagar nan al renderer.
|
||||||
const auto PEAK = (jump_config.gravity_px_per_s2 > 0.0F)
|
const auto PEAK = (cfg_.jump.gravity_px_per_s2 > 0.0F)
|
||||||
? (jump_config.impulse_px_per_s * jump_config.impulse_px_per_s) / (2.0F * jump_config.gravity_px_per_s2)
|
? (cfg_.jump.impulse_px_per_s * cfg_.jump.impulse_px_per_s) / (2.0F * cfg_.jump.gravity_px_per_s2)
|
||||||
: 0.0F;
|
: 0.0F;
|
||||||
const float HEIGHT_FACTOR = (PEAK > 0.0F) ? std::clamp(jump_z_ / PEAK, 0.0F, 1.0F) : 0.0F;
|
const float HEIGHT_FACTOR = (PEAK > 0.0F) ? std::clamp(jump_z_ / PEAK, 0.0F, 1.0F) : 0.0F;
|
||||||
|
|
||||||
const auto CENTER_X = static_cast<int>(getPosX() + (static_cast<float>(sprite_w) * 0.5F));
|
const auto CENTER_X = static_cast<int>(getPosX() + (static_cast<float>(cfg_.sprite_w) * 0.5F));
|
||||||
const auto FEET_Y = static_cast<int>(getGroundFeetY());
|
const auto FEET_Y = static_cast<int>(getGroundFeetY());
|
||||||
Effects::Shadow::render(CENTER_X, FEET_Y, HEIGHT_FACTOR);
|
Effects::Shadow::render(CENTER_X, FEET_Y, HEIGHT_FACTOR);
|
||||||
}
|
}
|
||||||
@@ -1553,7 +1562,7 @@ auto Player::statePrefix(State state, bool moving, bool in_attack_swing) -> cons
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto Player::animationName(bool moving) const -> std::string {
|
auto Player::animationName(bool moving) const -> std::string {
|
||||||
const bool IN_ATTACK_SWING = (attacking_remaining_ms_ > static_cast<float>(whip_config.cooldown_ms));
|
const bool IN_ATTACK_SWING = (attacking_remaining_ms_ > static_cast<float>(cfg_.whip.cooldown_ms));
|
||||||
return std::string(statePrefix(state_, moving, IN_ATTACK_SWING)) + facingSuffix(facing_);
|
return std::string(statePrefix(state_, moving, IN_ATTACK_SWING)) + facingSuffix(facing_);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1574,12 +1583,12 @@ auto Player::getCurrentTile() const -> std::pair<int, int> {
|
|||||||
void Player::clampToPlayfield() {
|
void Player::clampToPlayfield() {
|
||||||
// Límits de la zona jugable: la caixa de col·lisió no pot eixir del
|
// Límits de la zona jugable: la caixa de col·lisió no pot eixir del
|
||||||
// playfield (muros). El cap del sprite sí pot sobresortir per damunt.
|
// playfield (muros). El cap del sprite sí pot sobresortir per damunt.
|
||||||
const auto MIN_X = static_cast<float>(map_.playfield_x - collision_x_offset);
|
const auto MIN_X = static_cast<float>(map_.playfield_x - cfg_.collision_x_offset);
|
||||||
const auto MAX_X = static_cast<float>(
|
const auto MAX_X = static_cast<float>(
|
||||||
map_.playfield_x + map_.playfield_w - collision_x_offset - collision_size);
|
map_.playfield_x + map_.playfield_w - cfg_.collision_x_offset - cfg_.collision_size);
|
||||||
auto min_y = static_cast<float>(map_.playfield_y - collision_y_offset);
|
auto min_y = static_cast<float>(map_.playfield_y - cfg_.collision_y_offset);
|
||||||
const auto MAX_Y = static_cast<float>(
|
const auto MAX_Y = static_cast<float>(
|
||||||
map_.playfield_y + map_.playfield_h - collision_y_offset - collision_size);
|
map_.playfield_y + map_.playfield_h - cfg_.collision_y_offset - cfg_.collision_size);
|
||||||
|
|
||||||
// Passadís de sortida: si la base box queda enterament dins del rang
|
// Passadís de sortida: si la base box queda enterament dins del rang
|
||||||
// X del passadís de la porta, deixem que pugi al strip del mur trasser
|
// X del passadís de la porta, deixem que pugi al strip del mur trasser
|
||||||
@@ -1587,12 +1596,12 @@ void Player::clampToPlayfield() {
|
|||||||
// playfield. La regla "enterament dins" exigeix alinear-se al passadís
|
// playfield. La regla "enterament dins" exigeix alinear-se al passadís
|
||||||
// abans de pujar — la base box (12 px) hi cap amb 4 px de marge.
|
// abans de pujar — la base box (12 px) hi cap amb 4 px de marge.
|
||||||
if (map_.door_corridor_w > 0) {
|
if (map_.door_corridor_w > 0) {
|
||||||
const float BASE_X = getPosX() + static_cast<float>(collision_x_offset);
|
const float BASE_X = getPosX() + static_cast<float>(cfg_.collision_x_offset);
|
||||||
const float BASE_RIGHT = BASE_X + static_cast<float>(collision_size);
|
const float BASE_RIGHT = BASE_X + static_cast<float>(cfg_.collision_size);
|
||||||
const auto CORRIDOR_LEFT = static_cast<float>(map_.door_corridor_x);
|
const auto CORRIDOR_LEFT = static_cast<float>(map_.door_corridor_x);
|
||||||
const auto CORRIDOR_RIGHT = static_cast<float>(map_.door_corridor_x + map_.door_corridor_w);
|
const auto CORRIDOR_RIGHT = static_cast<float>(map_.door_corridor_x + map_.door_corridor_w);
|
||||||
if (BASE_X >= CORRIDOR_LEFT && BASE_RIGHT <= CORRIDOR_RIGHT) {
|
if (BASE_X >= CORRIDOR_LEFT && BASE_RIGHT <= CORRIDOR_RIGHT) {
|
||||||
min_y = static_cast<float>(map_.door_corridor_y - collision_y_offset);
|
min_y = static_cast<float>(map_.door_corridor_y - cfg_.collision_y_offset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1637,7 +1646,7 @@ void Player::respawn(float x, float y) {
|
|||||||
// enemics ja estan apareixent i el jugador no ha de tindre gràcia).
|
// enemics ja estan apareixent i el jugador no ha de tindre gràcia).
|
||||||
// Si el bloc `immunity:` manca al YAML, `duration_ms` és 0 i el timer
|
// Si el bloc `immunity:` manca al YAML, `duration_ms` és 0 i el timer
|
||||||
// queda a 0 — sense gràcia, comportament idèntic al d'abans.
|
// queda a 0 — sense gràcia, comportament idèntic al d'abans.
|
||||||
setImmunity(immunity_config.duration_ms);
|
setImmunity(cfg_.immunity.duration_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::disable() {
|
void Player::disable() {
|
||||||
@@ -1668,14 +1677,14 @@ void Player::enterHoleFall(float center_x, float center_y, float floor_y) {
|
|||||||
// cas i obligaria a hacks tipus "forçar GROUNDED abans de cridar".
|
// cas i obligaria a hacks tipus "forçar GROUNDED abans de cridar".
|
||||||
|
|
||||||
// Snap del centre de la hit-box a la coordenada central del forat.
|
// Snap del centre de la hit-box a la coordenada central del forat.
|
||||||
// Resolem `pos = center - (hit_offset + hit_size/2)` per a cada eix:
|
// Resolem `pos = center - (hit_offset + cfg_.hit_size/2)` per a cada eix:
|
||||||
// així el centre exacte de la caja de contacte queda alineat amb
|
// així el centre exacte de la caja de contacte queda alineat amb
|
||||||
// (center_x, center_y), independentment de la geometria interna del
|
// (center_x, center_y), independentment de la geometria interna del
|
||||||
// sprite. `setPosX/Y` és l'API canònica per teleportar el jugador
|
// sprite. `setPosX/Y` és l'API canònica per teleportar el jugador
|
||||||
// (la usen `respawn` i `spawnFromAir`).
|
// (la usen `respawn` i `spawnFromAir`).
|
||||||
const float HIT_HALF = static_cast<float>(hit_size) * 0.5F;
|
const float HIT_HALF = static_cast<float>(cfg_.hit_size) * 0.5F;
|
||||||
setPosX(center_x - static_cast<float>(hit_x_offset) - HIT_HALF);
|
setPosX(center_x - static_cast<float>(cfg_.hit_x_offset) - HIT_HALF);
|
||||||
setPosY(center_y - static_cast<float>(hit_y_offset) - HIT_HALF);
|
setPosY(center_y - static_cast<float>(cfg_.hit_y_offset) - HIT_HALF);
|
||||||
|
|
||||||
// Frenem qualsevol inèrcia prèvia: la caiguda ha de ser estàtica al
|
// Frenem qualsevol inèrcia prèvia: la caiguda ha de ser estàtica al
|
||||||
// centre del forat.
|
// centre del forat.
|
||||||
@@ -1721,7 +1730,7 @@ void Player::spawnFromAir(float x, float y) {
|
|||||||
// visualment es pinta a `pos_.y - jump_z_`). Afegim un xicotet marge
|
// visualment es pinta a `pos_.y - jump_z_`). Afegim un xicotet marge
|
||||||
// perquè el primer frame ja siga clarament off-screen, no just al ras.
|
// perquè el primer frame ja siga clarament off-screen, no just al ras.
|
||||||
constexpr float OFFSCREEN_MARGIN_PX = 8.0F;
|
constexpr float OFFSCREEN_MARGIN_PX = 8.0F;
|
||||||
const float SPAWN_HEIGHT = y + static_cast<float>(sprite_h) + OFFSCREEN_MARGIN_PX;
|
const float SPAWN_HEIGHT = y + static_cast<float>(cfg_.sprite_h) + OFFSCREEN_MARGIN_PX;
|
||||||
|
|
||||||
state_ = State::AIRBORNE;
|
state_ = State::AIRBORNE;
|
||||||
jump_z_ = SPAWN_HEIGHT;
|
jump_z_ = SPAWN_HEIGHT;
|
||||||
@@ -1741,31 +1750,31 @@ void Player::spawnFromAir(float x, float y) {
|
|||||||
|
|
||||||
auto Player::getBaseBox() const -> SDL_FRect {
|
auto Player::getBaseBox() const -> SDL_FRect {
|
||||||
return {
|
return {
|
||||||
.x = getPosX() + static_cast<float>(collision_x_offset),
|
.x = getPosX() + static_cast<float>(cfg_.collision_x_offset),
|
||||||
.y = getPosY() + static_cast<float>(collision_y_offset),
|
.y = getPosY() + static_cast<float>(cfg_.collision_y_offset),
|
||||||
.w = static_cast<float>(collision_size),
|
.w = static_cast<float>(cfg_.collision_size),
|
||||||
.h = static_cast<float>(collision_size),
|
.h = static_cast<float>(cfg_.collision_size),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Player::getHitBox() const -> SDL_FRect {
|
auto Player::getHitBox() const -> SDL_FRect {
|
||||||
return {
|
return {
|
||||||
.x = getPosX() + static_cast<float>(hit_x_offset),
|
.x = getPosX() + static_cast<float>(cfg_.hit_x_offset),
|
||||||
.y = getPosY() + static_cast<float>(hit_y_offset),
|
.y = getPosY() + static_cast<float>(cfg_.hit_y_offset),
|
||||||
.w = static_cast<float>(hit_size),
|
.w = static_cast<float>(cfg_.hit_size),
|
||||||
.h = static_cast<float>(hit_size),
|
.h = static_cast<float>(cfg_.hit_size),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Player::hitBoxFromBase(const SDL_FRect& base) -> SDL_FRect {
|
auto Player::hitBoxFromBase(const SDL_FRect& base) const -> SDL_FRect {
|
||||||
// Desplaçament intern respecte al cantó de la caixa base: mateixos
|
// Desplaçament intern respecte al cantó de la caixa base: mateixos
|
||||||
// offsets que `getHitBox()` però partint d'una caixa arbitrària.
|
// offsets que `getHitBox()` però partint d'una caixa arbitrària.
|
||||||
const int INSET = (collision_size - hit_size) / 2;
|
const int INSET = (cfg_.collision_size - cfg_.hit_size) / 2;
|
||||||
return {
|
return {
|
||||||
.x = base.x + static_cast<float>(INSET),
|
.x = base.x + static_cast<float>(INSET),
|
||||||
.y = base.y + static_cast<float>(INSET),
|
.y = base.y + static_cast<float>(INSET),
|
||||||
.w = static_cast<float>(hit_size),
|
.w = static_cast<float>(cfg_.hit_size),
|
||||||
.h = static_cast<float>(hit_size),
|
.h = static_cast<float>(cfg_.hit_size),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1774,7 +1783,7 @@ void Player::snapTowardsLane(
|
|||||||
float target_lane,
|
float target_lane,
|
||||||
float (Player::*get_pos)() const,
|
float (Player::*get_pos)() const,
|
||||||
void (Player::*set_pos)(float)) {
|
void (Player::*set_pos)(float)) {
|
||||||
const auto THRESHOLD = static_cast<float>(collision_size) / 2.0F;
|
const auto THRESHOLD = static_cast<float>(cfg_.collision_size) / 2.0F;
|
||||||
const float DELTA = target_lane - (this->*get_pos)();
|
const float DELTA = target_lane - (this->*get_pos)();
|
||||||
if (std::abs(DELTA) <= 0.0F || std::abs(DELTA) > THRESHOLD) { return; }
|
if (std::abs(DELTA) <= 0.0F || std::abs(DELTA) > THRESHOLD) { return; }
|
||||||
|
|
||||||
@@ -1874,10 +1883,10 @@ auto Player::wouldHitOuterWall(float dx, float dy) const -> bool {
|
|||||||
// corridor — encara no ho està, perquè el snap és justament qui
|
// corridor — encara no ho està, perquè el snap és justament qui
|
||||||
// ha d'aliniar-la. En lloc d'això comprovem si la base box està
|
// ha d'aliniar-la. En lloc d'això comprovem si la base box està
|
||||||
// prou prop del centre del corridor com per a que el snap
|
// prou prop del centre del corridor com per a que el snap
|
||||||
// (mateix THRESHOLD = collision_size/2) puga completar-lo.
|
// (mateix THRESHOLD = cfg_.collision_size/2) puga completar-lo.
|
||||||
if (Map::door_open && dy < 0.0F && map_.door_corridor_w > 0) {
|
if (Map::door_open && dy < 0.0F && map_.door_corridor_w > 0) {
|
||||||
const float CORRIDOR_TARGET = static_cast<float>(map_.door_corridor_x) + (static_cast<float>(map_.door_corridor_w - collision_size) / 2.0F);
|
const float CORRIDOR_TARGET = static_cast<float>(map_.door_corridor_x) + (static_cast<float>(map_.door_corridor_w - cfg_.collision_size) / 2.0F);
|
||||||
const auto THRESHOLD = static_cast<float>(collision_size) / 2.0F;
|
const auto THRESHOLD = static_cast<float>(cfg_.collision_size) / 2.0F;
|
||||||
if (std::abs(CORRIDOR_TARGET - probe.x) <= THRESHOLD) { return false; }
|
if (std::abs(CORRIDOR_TARGET - probe.x) <= THRESHOLD) { return false; }
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -1891,7 +1900,7 @@ void Player::snapToHorizontalLane(float delta_time) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto BASE_Y = static_cast<float>(map_.playfield_y - collision_y_offset);
|
const auto BASE_Y = static_cast<float>(map_.playfield_y - cfg_.collision_y_offset);
|
||||||
const auto PITCH_Y = static_cast<float>(map_.pitch_y);
|
const auto PITCH_Y = static_cast<float>(map_.pitch_y);
|
||||||
const int IDX = std::clamp(
|
const int IDX = std::clamp(
|
||||||
static_cast<int>(std::round((getPosY() - BASE_Y) / PITCH_Y)),
|
static_cast<int>(std::round((getPosY() - BASE_Y) / PITCH_Y)),
|
||||||
@@ -1911,7 +1920,7 @@ void Player::snapToVerticalLane(float delta_time) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Candidat 1: lane regular del laberint.
|
// Candidat 1: lane regular del laberint.
|
||||||
const auto BASE_X = static_cast<float>(map_.playfield_x - collision_x_offset);
|
const auto BASE_X = static_cast<float>(map_.playfield_x - cfg_.collision_x_offset);
|
||||||
const auto PITCH_X = static_cast<float>(map_.pitch_x);
|
const auto PITCH_X = static_cast<float>(map_.pitch_x);
|
||||||
const int IDX = std::clamp(
|
const int IDX = std::clamp(
|
||||||
static_cast<int>(std::round((getPosX() - BASE_X) / PITCH_X)),
|
static_cast<int>(std::round((getPosX() - BASE_X) / PITCH_X)),
|
||||||
@@ -1924,8 +1933,8 @@ void Player::snapToVerticalLane(float delta_time) {
|
|||||||
// forat per on no pot passar.
|
// forat per on no pot passar.
|
||||||
float best_lane = LANE_REG;
|
float best_lane = LANE_REG;
|
||||||
if (Map::door_open && map_.door_corridor_w > 0) {
|
if (Map::door_open && map_.door_corridor_w > 0) {
|
||||||
const float CORRIDOR_CENTER = static_cast<float>(map_.door_corridor_x) + (static_cast<float>(map_.door_corridor_w - collision_size) / 2.0F);
|
const float CORRIDOR_CENTER = static_cast<float>(map_.door_corridor_x) + (static_cast<float>(map_.door_corridor_w - cfg_.collision_size) / 2.0F);
|
||||||
const float LANE_DOOR = CORRIDOR_CENTER - static_cast<float>(collision_x_offset);
|
const float LANE_DOOR = CORRIDOR_CENTER - static_cast<float>(cfg_.collision_x_offset);
|
||||||
const float DELTA_REG = LANE_REG - getPosX();
|
const float DELTA_REG = LANE_REG - getPosX();
|
||||||
const float DELTA_DOOR = LANE_DOOR - getPosX();
|
const float DELTA_DOOR = LANE_DOOR - getPosX();
|
||||||
if (std::abs(DELTA_DOOR) < std::abs(DELTA_REG)) { best_lane = LANE_DOOR; }
|
if (std::abs(DELTA_DOOR) < std::abs(DELTA_REG)) { best_lane = LANE_DOOR; }
|
||||||
|
|||||||
+182
-192
@@ -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"
|
||||||
@@ -54,6 +55,15 @@ class Player : public AnimatedSprite {
|
|||||||
HOLE, // caiguda per agujero — animació `death_hole`
|
HOLE, // caiguda per agujero — animació `death_hole`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Direcció cardinal completa. Pública perquè el `WhipAttack` la passa a
|
||||||
|
// `enterHurt` quan colpeja un altre jugador (friendly fire). Declarada
|
||||||
|
// aquí (a sobre de `Config` perquè `TurnConfig` l'utilitza).
|
||||||
|
enum class Cardinal : std::uint8_t { NONE,
|
||||||
|
NORTH,
|
||||||
|
SOUTH,
|
||||||
|
EAST,
|
||||||
|
WEST };
|
||||||
|
|
||||||
enum class State : std::uint8_t {
|
enum class State : std::uint8_t {
|
||||||
GROUNDED,
|
GROUNDED,
|
||||||
AIRBORNE,
|
AIRBORNE,
|
||||||
@@ -91,61 +101,172 @@ class Player : public AnimatedSprite {
|
|||||||
DYING,
|
DYING,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Geometria carregada des del YAML del player (p.ex. `data/entities/player/sam.yaml`).
|
// Configuració per-instància del Player. Conté tot el que abans vivia
|
||||||
// `sprite_*` són
|
// com a `inline static` o `static *_config`: així cada jugador (P1=sam,
|
||||||
// la mida del frame (sprite 12×24 amb perspectiva falsa: visualment, els
|
// P2=morcus) pot tenir paràmetres diferents llegits del seu YAML.
|
||||||
// 12 píxels superiors són el cap i els 12 inferiors la base).
|
// GameScene la carrega amb `Config::loadFromYaml` abans d'instanciar
|
||||||
// La caixa de col·lisió és un quadrat `collision_size` concèntric amb el
|
// cada Player i la passa al ctor (mou-la). Per refrescar-la entre
|
||||||
// quadrat inferior del sprite (`base_size`). Els offsets els calculem
|
// sales s'utilitza `Player::setConfig`.
|
||||||
// al load a partir d'aquests tres valors; el mapa de col·lisions i el
|
//
|
||||||
// de dibuix són independents (el sprite pot sobresortir sense afectar).
|
// Els sub-structs (`InertiaConfig`, `TurnConfig`, ...) viuen dins de
|
||||||
inline static int sprite_w = 0;
|
// `Config` per agrupar-los visualment amb la resta del config — no hi
|
||||||
inline static int sprite_h = 0;
|
// ha cap raó tècnica per haver-los a fora.
|
||||||
inline static int base_size = 0;
|
struct Config {
|
||||||
inline static int collision_size = 0;
|
// ---- Geometria del sprite i col·lisió --------------------------
|
||||||
inline static int collision_padding = 0;
|
// `sprite_*` són la mida del frame (sprite 12×24 amb perspectiva
|
||||||
inline static int collision_x_offset = 0;
|
// falsa). La caixa de col·lisió és un quadrat `collision_size`
|
||||||
inline static int collision_y_offset = 0;
|
// concèntric amb el quadrat inferior del sprite (`base_size`).
|
||||||
|
// Els offsets es calculen al load.
|
||||||
|
int sprite_w{0};
|
||||||
|
int sprite_h{0};
|
||||||
|
int base_size{0};
|
||||||
|
int collision_size{0};
|
||||||
|
int collision_padding{0};
|
||||||
|
int collision_x_offset{0};
|
||||||
|
int collision_y_offset{0};
|
||||||
|
|
||||||
// Caixa de "contacte" més petita que la caixa de col·lisió: s'usa per
|
// Caixa de contacte actor-actor (més petita que la col·lisió).
|
||||||
// a les interaccions actor-actor (player↔player, player↔momia/trampa),
|
int hit_size{0};
|
||||||
// on volem que el contacte sigui precís i no dispari a "distància"
|
int hit_x_offset{0};
|
||||||
// de la vora del sprite. La caixa de col·lisió gran continua usant-se
|
int hit_y_offset{0};
|
||||||
// per xocar amb murs i sarcòfags (per mantenir el carril del
|
|
||||||
// passadís). Sense `hit_size` al YAML, s'iguala a `collision_size` i
|
|
||||||
// el comportament és idèntic a l'anterior.
|
|
||||||
inline static int hit_size = 0;
|
|
||||||
inline static int hit_x_offset = 0;
|
|
||||||
inline static int hit_y_offset = 0;
|
|
||||||
|
|
||||||
// Alçada del cos del jugador en píxels (camp `collision.height` del
|
// Alçada del cos en píxels (filtre de col·lisió per altura).
|
||||||
// YAML, mateixa convenció que entitats com mummy o rolling_rock).
|
int body_height{0};
|
||||||
// L'usen els filtres de col·lisió per altura: si un altre actor ens
|
|
||||||
// creua mentre `jump_z_` és superior a aquest valor, no col·lisionem.
|
|
||||||
inline static int body_height = 0;
|
|
||||||
|
|
||||||
// Radi de la llum emesa pel jugador (en píxels del canvas), llegit
|
// ---- Emissió de llum -----------------------------------------
|
||||||
// del bloc opcional `light: { radius: N, flicker: bool }` del YAML.
|
// Llegit del bloc opcional `light:` del YAML.
|
||||||
// 0 = no emet. Es declara estàtic com la resta de configs del jugador
|
// `radius` → 0 = no emet.
|
||||||
// (immunity, jump, ...): tots els jugadors del mateix YAML comparteixen
|
// `flicker` → activa modulació tipus foc.
|
||||||
// el valor. `light_flicker` activa la modulació "tipus foc" del radi.
|
// `color` → tint en mode additive (ignorat en subtractive).
|
||||||
inline static int light_radius = 0;
|
// `offset` → desplaçament del centre respecte el sprite (Y avall).
|
||||||
inline static bool light_flicker = false;
|
int light_radius{0};
|
||||||
|
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};
|
||||||
|
// Paràmetres del flicker resolts (global + override del YAML +
|
||||||
|
// override de sala). Només té efecte si `light_flicker` és true.
|
||||||
|
Lighting::FlickerConfig flicker_params{};
|
||||||
|
|
||||||
// Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor.
|
// ---- Sub-structs de gameplay ---------------------------------
|
||||||
static void loadFrom(const std::string& yaml_path);
|
|
||||||
|
|
||||||
// Vides amb què arrenca un jugador (camp `lives:` del YAML del
|
// Inèrcia: amb `brake_time_ms == 0` el comportament és instantani.
|
||||||
// jugador). El consumeix el HUD per inicialitzar el comptador del
|
struct InertiaConfig {
|
||||||
// marcador i `Player::resetForJoin` per a re-entrades mid-partida.
|
int brake_time_ms{0};
|
||||||
[[nodiscard]] static auto getInitialLives() -> int { return death_config.initial_lives; }
|
int threshold_ms{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gir de 180°: amb `duration_ms == 0` els canvis de direcció són
|
||||||
|
// instantanis. `Cardinal` ha d'estar visible — declarada a sobre.
|
||||||
|
struct TurnConfig {
|
||||||
|
int duration_ms{0};
|
||||||
|
Cardinal w_to_e{Cardinal::SOUTH};
|
||||||
|
Cardinal e_to_w{Cardinal::NORTH};
|
||||||
|
Cardinal n_to_s{Cardinal::EAST};
|
||||||
|
Cardinal s_to_n{Cardinal::WEST};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Salt: amb `impulse_px_per_s == 0` el botó no té efecte.
|
||||||
|
struct JumpConfig {
|
||||||
|
float impulse_px_per_s{0.0F};
|
||||||
|
float gravity_px_per_s2{0.0F};
|
||||||
|
int landing_stun_ms{0};
|
||||||
|
std::string sound_key;
|
||||||
|
std::string land_sound_key;
|
||||||
|
};
|
||||||
|
|
||||||
|
// HURT (estat post-impacte). Amb `initial_speed_px_per_s == 0`
|
||||||
|
// entrada/sortida instantànies (sense moviment).
|
||||||
|
struct HurtConfig {
|
||||||
|
float initial_speed_px_per_s{0.0F};
|
||||||
|
int brake_time_ms{0};
|
||||||
|
std::string sound_key;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Látigo: hitbox `range_tiles`, swing `swing_ms`, cooldown
|
||||||
|
// `cooldown_ms`. Per ara `damage` només es llegeix.
|
||||||
|
struct WhipConfig {
|
||||||
|
float range_tiles{0.0F};
|
||||||
|
int swing_ms{0};
|
||||||
|
int cooldown_ms{0};
|
||||||
|
int damage{0};
|
||||||
|
std::string sound_key;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mort/respawn: `respawn_delay_ms` entre l'animació i la
|
||||||
|
// reaparició; `initial_lives` és el comptador d'intents.
|
||||||
|
struct DeathConfig {
|
||||||
|
int respawn_delay_ms{0};
|
||||||
|
int initial_lives{3};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Immunitat post-spawn. `duration_ms == 0` desactiva la mecànica.
|
||||||
|
// `palette_key` (basename del .pal) és la paleta plana del blink.
|
||||||
|
struct ImmunityConfig {
|
||||||
|
float duration_ms{0.0F};
|
||||||
|
int blink_period_ms{0};
|
||||||
|
std::string palette_key;
|
||||||
|
};
|
||||||
|
|
||||||
|
InertiaConfig inertia{};
|
||||||
|
TurnConfig turn{};
|
||||||
|
JumpConfig jump{};
|
||||||
|
HurtConfig hurt{};
|
||||||
|
WhipConfig whip{};
|
||||||
|
DeathConfig death{};
|
||||||
|
ImmunityConfig immunity{};
|
||||||
|
|
||||||
|
// Llig tot el YAML del personatge i retorna un `Config` poblat.
|
||||||
|
// Substitueix l'antic `Player::loadFrom` que mutava estàtics.
|
||||||
|
// Llança si manca el bloc `collision:` (obligatori).
|
||||||
|
[[nodiscard]] static auto loadFromYaml(const std::string& yaml_path) -> Config;
|
||||||
|
|
||||||
|
// Aplica l'override de sala (bloc `lighting.emitters.player` del
|
||||||
|
// YAML del mapa) sobre els camps de llum d'aquesta Config. Es
|
||||||
|
// crida després de `loadFromYaml` i abans de passar la Config al
|
||||||
|
// ctor de Player (o a `setConfig` en transicions de sala).
|
||||||
|
void applyRoomLightOverride();
|
||||||
|
};
|
||||||
|
|
||||||
|
Player(Config cfg, const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1);
|
||||||
|
|
||||||
|
// Reemplaça el `Config` actual del Player. Cridat per GameScene al
|
||||||
|
// canvi de sala per re-aplicar valors base + override de la sala nova.
|
||||||
|
// No toca cap estat de joc (lives, posició, FSM): només la config.
|
||||||
|
void setConfig(Config cfg);
|
||||||
|
|
||||||
|
// Accés a la config viva (lectura). El callsites que abans accedien
|
||||||
|
// a estàtics fan servir `getters d'instància` (sota); aquest és per
|
||||||
|
// codi que vol l'objecte sencer.
|
||||||
|
[[nodiscard]] auto getConfig() const -> const Config& { return cfg_; }
|
||||||
|
|
||||||
|
// Vides amb què arrenca un jugador (camp `lives:` del YAML). El
|
||||||
|
// consumeix el HUD per inicialitzar el marcador i `resetForJoin`
|
||||||
|
// per a re-entrades mid-partida.
|
||||||
|
[[nodiscard]] auto getInitialLives() const -> int { return cfg_.death.initial_lives; }
|
||||||
|
|
||||||
|
// Geometria del cos — instance getters dels camps de `cfg_` que
|
||||||
|
// alguns callsites externs (game_scene) consulten directament.
|
||||||
|
[[nodiscard]] auto getBodyHeight() const -> int { return cfg_.body_height; }
|
||||||
|
[[nodiscard]] auto getSpriteW() const -> int { return cfg_.sprite_w; }
|
||||||
|
[[nodiscard]] auto getSpriteH() const -> int { return cfg_.sprite_h; }
|
||||||
|
[[nodiscard]] auto getCollisionSize() const -> int { return cfg_.collision_size; }
|
||||||
|
[[nodiscard]] auto getCollisionYOffset() const -> int { return cfg_.collision_y_offset; }
|
||||||
|
|
||||||
// Emissió de llum (mateix paper que `Entity::getLightRadius` per a
|
// Emissió de llum (mateix paper que `Entity::getLightRadius` per a
|
||||||
// les entitats). El centre puja amb el salt: durant `jump_z_>0` el
|
// les entitats). El centre puja amb el salt: durant `jump_z_>0` el
|
||||||
// sprite es dibuixa amb `pos_.y - jump_z_`, i la llum ha de seguir-lo
|
// sprite es dibuixa amb `pos_.y - jump_z_`, i la llum ha de seguir-lo
|
||||||
// per a que el cercle "salti" amb el jugador, no es quedi al sòl.
|
// per a que el cercle "salti" amb el jugador, no es quedi al sòl.
|
||||||
[[nodiscard]] static auto getLightRadius() -> int { return light_radius; }
|
[[nodiscard]] auto getLightRadius() const -> int { return cfg_.light_radius; }
|
||||||
[[nodiscard]] static auto getLightFlicker() -> bool { return light_flicker; }
|
[[nodiscard]] auto getLightFlicker() const -> bool { return cfg_.light_flicker; }
|
||||||
|
[[nodiscard]] auto getLightColorR() const -> std::uint8_t { return cfg_.light_color_r; }
|
||||||
|
[[nodiscard]] auto getLightColorG() const -> std::uint8_t { return cfg_.light_color_g; }
|
||||||
|
[[nodiscard]] auto getLightColorB() const -> std::uint8_t { return cfg_.light_color_b; }
|
||||||
|
[[nodiscard]] auto getLightOffsetX() const -> int { return cfg_.light_offset_x; }
|
||||||
|
[[nodiscard]] auto getLightOffsetY() const -> int { return cfg_.light_offset_y; }
|
||||||
|
[[nodiscard]] auto getFlickerParams() const -> const Lighting::FlickerConfig& { return cfg_.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
|
||||||
@@ -153,8 +274,6 @@ class Player : public AnimatedSprite {
|
|||||||
[[nodiscard]] auto getLightSeed() const -> int { return id_ + 1000; }
|
[[nodiscard]] auto getLightSeed() const -> int { return id_ + 1000; }
|
||||||
[[nodiscard]] auto getLightCenter() const -> SDL_FPoint;
|
[[nodiscard]] auto getLightCenter() const -> SDL_FPoint;
|
||||||
|
|
||||||
Player(const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1);
|
|
||||||
|
|
||||||
// Update en una sola crida: manté l'API anterior per als tests i
|
// Update en una sola crida: manté l'API anterior per als tests i
|
||||||
// escenaris amb un sol jugador. Internament és equivalent a cridar
|
// escenaris amb un sol jugador. Internament és equivalent a cridar
|
||||||
// `tick(dt)` seguit d'`applyMovement(dt)`.
|
// `tick(dt)` seguit d'`applyMovement(dt)`.
|
||||||
@@ -255,7 +374,7 @@ class Player : public AnimatedSprite {
|
|||||||
// que coincideix amb aquest mètode i s'usa específicament per
|
// que coincideix amb aquest mètode i s'usa específicament per
|
||||||
// marcar la posició de la sombra a nivell de sòl.
|
// marcar la posició de la sombra a nivell de sòl.
|
||||||
[[nodiscard]] auto getFeetY() const -> float {
|
[[nodiscard]] auto getFeetY() const -> float {
|
||||||
return getPosY() + static_cast<float>(collision_y_offset + collision_size);
|
return getPosY() + static_cast<float>(cfg_.collision_y_offset + cfg_.collision_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alies semàntic: els peus del jugador al sòl. Coincideix amb
|
// Alies semàntic: els peus del jugador al sòl. Coincideix amb
|
||||||
@@ -464,7 +583,7 @@ class Player : public AnimatedSprite {
|
|||||||
// arbitrària (p. ex. la posició proposada per `tryMove` abans
|
// arbitrària (p. ex. la posició proposada per `tryMove` abans
|
||||||
// d'aplicar-la). Evita recalcular `hit_x_offset`/`hit_y_offset` fora
|
// d'aplicar-la). Evita recalcular `hit_x_offset`/`hit_y_offset` fora
|
||||||
// de Player.
|
// de Player.
|
||||||
[[nodiscard]] static auto hitBoxFromBase(const SDL_FRect& base) -> SDL_FRect;
|
[[nodiscard]] auto hitBoxFromBase(const SDL_FRect& base) const -> SDL_FRect;
|
||||||
|
|
||||||
// Override del hook de Sprite: `MovingSprite::tryMove` envia aquesta
|
// Override del hook de Sprite: `MovingSprite::tryMove` envia aquesta
|
||||||
// caixa al callback de col·lisió. La base del Player és més petita
|
// caixa al callback de col·lisió. La base del Player és més petita
|
||||||
@@ -548,6 +667,11 @@ class Player : public AnimatedSprite {
|
|||||||
// efecte immediat sense haver de tornar a entrar a la GameScene.
|
// efecte immediat sense haver de tornar a entrar a la GameScene.
|
||||||
void refreshControlsFromOptions();
|
void refreshControlsFromOptions();
|
||||||
|
|
||||||
|
// Tot el config del personatge (geometria, llum, jump, hurt, whip,
|
||||||
|
// death, immunity, turn, inertia). Llegit del YAML per instància
|
||||||
|
// (P1 i P2 poden tenir valors diferents) i mogut al ctor.
|
||||||
|
Config cfg_;
|
||||||
|
|
||||||
int id_{-1};
|
int id_{-1};
|
||||||
Controls controls_;
|
Controls controls_;
|
||||||
|
|
||||||
@@ -619,26 +743,6 @@ class Player : public AnimatedSprite {
|
|||||||
static auto facingSuffix(Facing facing) -> const char*;
|
static auto facingSuffix(Facing facing) -> const char*;
|
||||||
static auto statePrefix(State state, bool moving, bool in_attack_swing) -> const char*;
|
static auto statePrefix(State state, bool moving, bool in_attack_swing) -> const char*;
|
||||||
|
|
||||||
// Cardinal complet (N/S/E/W) usat per a la detecció de girs de 180°.
|
|
||||||
// Pública perquè el `WhipAttack` la passa a `enterHurt` quan colpeja
|
|
||||||
// un altre jugador (friendly fire).
|
|
||||||
public:
|
|
||||||
enum class Cardinal : std::uint8_t { NONE,
|
|
||||||
NORTH,
|
|
||||||
SOUTH,
|
|
||||||
EAST,
|
|
||||||
WEST };
|
|
||||||
|
|
||||||
private:
|
|
||||||
// Configuració d'inèrcia llegida del bloc `inertia:` del YAML. Amb
|
|
||||||
// `brake_time_ms == 0` (o bloc absent) la inèrcia està desactivada i
|
|
||||||
// el comportament és instantani: soltar input para el jugador en sec.
|
|
||||||
struct InertiaConfig {
|
|
||||||
int brake_time_ms{0}; // Temps en ms perquè la velocitat caiga de ±SPEED a 0.
|
|
||||||
int threshold_ms{0}; // Temps mínim en moviment continu per activar la inèrcia.
|
|
||||||
};
|
|
||||||
static InertiaConfig inertia_config;
|
|
||||||
|
|
||||||
// Integra un eix. Si hi ha input en aquest eix, la velocitat física
|
// Integra un eix. Si hi ha input en aquest eix, la velocitat física
|
||||||
// salta a aquest valor (acceleració instantània). Si no, i hi ha
|
// salta a aquest valor (acceleració instantània). Si no, i hi ha
|
||||||
// input en l'altre eix (`any_input == true`), la velocitat d'aquest
|
// input en l'altre eix (`any_input == true`), la velocitat d'aquest
|
||||||
@@ -646,127 +750,7 @@ class Player : public AnimatedSprite {
|
|||||||
// soltant l'input — no volem que arrossegue en diagonal. Només quan
|
// soltant l'input — no volem que arrossegue en diagonal. Només quan
|
||||||
// l'usuari deixa anar tot l'input (`any_input == false`) s'activa la
|
// l'usuari deixa anar tot l'input (`any_input == false`) s'activa la
|
||||||
// inèrcia de frenada (si `motion_time_ms` ha passat el llindar).
|
// inèrcia de frenada (si `motion_time_ms` ha passat el llindar).
|
||||||
static void integrateAxis(float& phys, float input, float delta_time, float motion_time_ms, bool any_input);
|
void integrateAxis(float& phys, float input, float delta_time, float motion_time_ms, bool any_input) const;
|
||||||
|
|
||||||
// Configuració del gir de 180°, llegida del bloc `turn:` del YAML del
|
|
||||||
// player. Si `duration_ms` és 0 (o el bloc no existeix) la mecànica
|
|
||||||
// queda desactivada i els canvis de direcció són instantanis.
|
|
||||||
struct TurnConfig {
|
|
||||||
int duration_ms{0};
|
|
||||||
Cardinal w_to_e{Cardinal::SOUTH};
|
|
||||||
Cardinal e_to_w{Cardinal::NORTH};
|
|
||||||
Cardinal n_to_s{Cardinal::EAST};
|
|
||||||
Cardinal s_to_n{Cardinal::WEST};
|
|
||||||
};
|
|
||||||
// Definida out-of-class al .cpp: dins de la classe, els initializers
|
|
||||||
// per defecte de TurnConfig encara no són visibles per al compilador.
|
|
||||||
static TurnConfig turn_config;
|
|
||||||
|
|
||||||
// Configuració del salt llegida del bloc `jump:` del YAML del player.
|
|
||||||
// Si `impulse_px_per_s == 0` (o el bloc no existeix) la mecànica del
|
|
||||||
// salt queda desactivada — el botó no té efecte.
|
|
||||||
struct JumpConfig {
|
|
||||||
float impulse_px_per_s{0.0F}; // Velocitat vertical inicial.
|
|
||||||
float gravity_px_per_s2{0.0F}; // Acceleració descendent constant.
|
|
||||||
int landing_stun_ms{0}; // Stun en aterrar (no es mou ni salta).
|
|
||||||
std::string sound_key; // Clau al Resource::Cache del so de despegue (buit = silenci).
|
|
||||||
std::string land_sound_key; // Clau del so d'aterratge (buit = silenci).
|
|
||||||
};
|
|
||||||
static JumpConfig jump_config;
|
|
||||||
|
|
||||||
// Llig el bloc `collision:` del YAML i n'omple les caixes (size/base/
|
|
||||||
// hit) + `body_height`. Llança si el bloc no existeix. Membre estàtic
|
|
||||||
// per accedir als estàtics privats sense exposar-los.
|
|
||||||
static void readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path);
|
|
||||||
|
|
||||||
// Llig el bloc `jump:` del YAML i n'omple `jump_config`. Si el bloc
|
|
||||||
// manca, deixa la mecànica del salt desactivada.
|
|
||||||
static void readJumpBlock(const fkyaml::node& yaml);
|
|
||||||
|
|
||||||
// Configuració de l'estat HURT (estat post-impacte). Si
|
|
||||||
// `initial_speed_px_per_s == 0` el jugador entra i surt
|
|
||||||
// instantàniament (cap moviment).
|
|
||||||
struct HurtConfig {
|
|
||||||
float initial_speed_px_per_s{0.0F}; // Velocitat amb què surt projectat (px/s).
|
|
||||||
int brake_time_ms{0}; // Temps en que la velocitat decau a 0.
|
|
||||||
// Fitxer .wav reproduït en entrar a HURT (relatiu a `data/audio/sounds/`).
|
|
||||||
// Mateixa convenció que la resta de sons del jugador: el basename val
|
|
||||||
// de clau al Resource::Cache i de path. Buit o absent = entrada
|
|
||||||
// silenciosa.
|
|
||||||
std::string sound_key;
|
|
||||||
};
|
|
||||||
static HurtConfig hurt_config;
|
|
||||||
|
|
||||||
// Llig el bloc `hurt:` del YAML i n'omple `hurt_config`.
|
|
||||||
static void readHurtBlock(const fkyaml::node& yaml);
|
|
||||||
|
|
||||||
// Configuració del látigo del jugador llegida del bloc `whip:` del
|
|
||||||
// YAML. `range_tiles` defineix la longitud de la hitbox abans de
|
|
||||||
// retallar; `swing_ms` és la durada de l'animació d'atac (des
|
|
||||||
// d'`enterAttacking` fins a desbloquejar el cooldown); `cooldown_ms`
|
|
||||||
// afegeix una fase posterior durant la qual el jugador segueix
|
|
||||||
// bloquejat però la hitbox ja no fa dany. `damage` queda llegit per
|
|
||||||
// a futur; per ara els objectius reben sempre `enterHurt`.
|
|
||||||
struct WhipConfig {
|
|
||||||
float range_tiles{0.0F};
|
|
||||||
int swing_ms{0};
|
|
||||||
int cooldown_ms{0};
|
|
||||||
int damage{0};
|
|
||||||
// Fitxer .wav reproduït al moment de disparar el swing (relatiu a
|
|
||||||
// `data/audio/sounds/`). Convenció: el basename val tant de clau al
|
|
||||||
// Resource::Cache com de path. Buit o absent = swing silenciós.
|
|
||||||
std::string sound_key;
|
|
||||||
};
|
|
||||||
static WhipConfig whip_config;
|
|
||||||
|
|
||||||
// Llig el bloc `whip:` del YAML i n'omple `whip_config`. Si manca el
|
|
||||||
// bloc, els camps queden a 0 i el jugador no pot atacar (el flanc
|
|
||||||
// d'input es perd silenciosament).
|
|
||||||
static void readWhipBlock(const fkyaml::node& yaml);
|
|
||||||
|
|
||||||
// Configuració de mort/respawn llegida del bloc `death:` del YAML.
|
|
||||||
// `respawn_delay_ms` és la pausa entre la fi de `death_spinning` i la
|
|
||||||
// reaparició al spawn; `initial_lives` és el comptador d'intents amb
|
|
||||||
// què naix cada Player. Si el bloc manca, defaults segurs (0/3).
|
|
||||||
struct DeathConfig {
|
|
||||||
int respawn_delay_ms{0};
|
|
||||||
int initial_lives{3};
|
|
||||||
};
|
|
||||||
static DeathConfig death_config;
|
|
||||||
|
|
||||||
// Llig el bloc `death:` del YAML i n'omple `death_config`. Si manca,
|
|
||||||
// `respawn_delay_ms` queda a 0 (el respawn és immediat al final de
|
|
||||||
// l'animació) i `initial_lives` es manté al default (3).
|
|
||||||
static void readDeathBlock(const fkyaml::node& yaml);
|
|
||||||
|
|
||||||
// Configuració de la immunitat post-spawn llegida del bloc `immunity:`
|
|
||||||
// del YAML. `duration_ms` és el temps per defecte que dura cada
|
|
||||||
// immunitat (l'invoca `respawn`). `blink_period_ms` és el mig-cicle del
|
|
||||||
// parpadeig visual: cada `blink_period_ms` ms, el render alterna
|
|
||||||
// entre la paleta normal i la paleta `palette_key`. Si `duration_ms`
|
|
||||||
// és 0 (o el bloc manca) la immunitat queda desactivada i el
|
|
||||||
// comportament és l'antic.
|
|
||||||
struct ImmunityConfig {
|
|
||||||
float duration_ms{0.0F};
|
|
||||||
int blink_period_ms{0};
|
|
||||||
// Clau al Resource::Cache de la paleta plana usada al blink (basename
|
|
||||||
// del .pal a `data/entities/player/`). Buida = no hi ha alternança visual,
|
|
||||||
// només la finestra lògica d'immunitat (que sense feedback visual
|
|
||||||
// pot ser confusa, però no és incorrecte).
|
|
||||||
std::string palette_key;
|
|
||||||
};
|
|
||||||
static ImmunityConfig immunity_config;
|
|
||||||
|
|
||||||
// Llig el bloc `immunity:` del YAML i n'omple `immunity_config`. Si
|
|
||||||
// hi ha `palette`, registra la paleta corresponent al Resource::Cache
|
|
||||||
// (path: `data/entities/player/<palette_key>.pal`). Mateix patró que els altres
|
|
||||||
// readers — `loadFrom` el crida al final.
|
|
||||||
static void readImmunityBlock(const fkyaml::node& yaml);
|
|
||||||
|
|
||||||
// Llig el bloc opcional `light: { radius: N, flicker: bool }` del
|
|
||||||
// YAML i pobla `light_radius` / `light_flicker`. Si manca, els
|
|
||||||
// valors queden a 0 / false (jugador sense llum).
|
|
||||||
static void readLightBlock(const fkyaml::node& yaml);
|
|
||||||
|
|
||||||
// Activa l'estat ATTACKING en la direcció cardinal indicada (la
|
// Activa l'estat ATTACKING en la direcció cardinal indicada (la
|
||||||
// direcció cap on mira el jugador) i publica `whip_spawn` al bus
|
// direcció cap on mira el jugador) i publica `whip_spawn` al bus
|
||||||
@@ -972,10 +956,16 @@ class Player : public AnimatedSprite {
|
|||||||
// Extret de `update` per acotar la complexitat cognitiva (clang-tidy).
|
// Extret de `update` per acotar la complexitat cognitiva (clang-tidy).
|
||||||
void updateFacingAndTurn(float delta_time, float vx, float vy);
|
void updateFacingAndTurn(float delta_time, float vx, float vy);
|
||||||
static auto cardinalFromInput(float vx, float vy) -> Cardinal;
|
static auto cardinalFromInput(float vx, float vy) -> Cardinal;
|
||||||
static auto cardinalFromString(const std::string& s) -> Cardinal;
|
|
||||||
static auto opposite(Cardinal c) -> Cardinal;
|
static auto opposite(Cardinal c) -> Cardinal;
|
||||||
static auto intermediateFor(Cardinal target) -> Cardinal;
|
auto intermediateFor(Cardinal target) const -> Cardinal;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Helper pur: tradueix un string YAML ("north", "south", ...) al
|
||||||
|
// Cardinal corresponent. Públic perquè `Config::loadFromYaml` el crida
|
||||||
|
// des d'un helper de la TU anònima a player.cpp.
|
||||||
|
static auto cardinalFromString(const std::string& s) -> Cardinal;
|
||||||
|
|
||||||
|
private:
|
||||||
// Bus del nivell (opcional). Propietat externa (EntityManager); el
|
// Bus del nivell (opcional). Propietat externa (EntityManager); el
|
||||||
// Player només en guarda un punter per publicar events.
|
// Player només en guarda un punter per publicar events.
|
||||||
EventBus* bus_{nullptr};
|
EventBus* bus_{nullptr};
|
||||||
|
|||||||
+131
-2
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -171,9 +171,10 @@ namespace {
|
|||||||
// velocitats horitzontals i els deixem rebotar fins a aterrar
|
// velocitats horitzontals i els deixem rebotar fins a aterrar
|
||||||
// — els dos cauen en stun, com a càstig per abusar del salt.
|
// — els dos cauen en stun, com a càstig per abusar del salt.
|
||||||
if (A_AIR) {
|
if (A_AIR) {
|
||||||
// cppcheck-suppress knownConditionTrueFalse
|
// Usem el body_height d'A (els dos haurien de ser idèntics en
|
||||||
const auto BODY_H = static_cast<float>(Player::body_height);
|
// multijugador local actual; si en algun moment difereixen entre
|
||||||
// cppcheck-suppress knownConditionTrueFalse
|
// P1 i P2, prenem el del primer participant per consistència).
|
||||||
|
const auto BODY_H = static_cast<float>(a->getBodyHeight());
|
||||||
if (BODY_H <= 0.0F) { return; }
|
if (BODY_H <= 0.0F) { return; }
|
||||||
if (std::fabs(a->getJumpZ() - b->getJumpZ()) >= BODY_H) { return; }
|
if (std::fabs(a->getJumpZ() - b->getJumpZ()) >= BODY_H) { return; }
|
||||||
// Saltadors estàtics horitzontalment (cas degenerat: tots dos
|
// Saltadors estàtics horitzontalment (cas degenerat: tots dos
|
||||||
@@ -232,6 +233,16 @@ namespace {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Carrega la config d'un jugador des del seu YAML i aplica l'override
|
||||||
|
// de sala (si el bloc `lighting.emitters.player` el reescriu). Helper
|
||||||
|
// compartit entre ctor i `loadRoom` per fer la mateixa cosa als dos
|
||||||
|
// moments: arrencada de partida i transició de sala.
|
||||||
|
auto loadPlayerConfigWithRoomOverride(const std::string& yaml_path) -> Player::Config {
|
||||||
|
auto cfg = Player::Config::loadFromYaml(yaml_path);
|
||||||
|
cfg.applyRoomLightOverride();
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
auto GameScene::defaultConfig() -> Config {
|
auto GameScene::defaultConfig() -> Config {
|
||||||
@@ -282,13 +293,14 @@ GameScene::GameScene(Config config)
|
|||||||
const std::string ANIM_P1 = load_player_assets(config_.player_sprite, config_.player_yaml);
|
const std::string ANIM_P1 = load_player_assets(config_.player_sprite, config_.player_yaml);
|
||||||
const std::string ANIM_P2 = load_player_assets(config_.player2_sprite, config_.player2_yaml);
|
const std::string ANIM_P2 = load_player_assets(config_.player2_sprite, config_.player2_yaml);
|
||||||
|
|
||||||
// Geometria de col·lisió del jugador (caixa 16×16, base 12): carregada
|
// Geometria de col·lisió i tota la resta de config del jugador
|
||||||
// del mateix YAML que l'animació. Ha d'estar feta abans d'instanciar
|
// (caixa 16×16, base 12, jump, hurt, llum, etc.) carregada per
|
||||||
// Player perquè el spawn consulta `collision_*`. Els membres de Player
|
// instància des del YAML del personatge corresponent. Cada Player
|
||||||
// són `static inline`, compartits per tots els jugadors; carregar des
|
// rep el seu `Config` al ctor (moure'l avall). L'override de sala
|
||||||
// del YAML de P1 és suficient (els dos personatges comparteixen
|
// s'aplica aquí abans de moure'l perquè el jugador ja arrenqui
|
||||||
// geometria — morcus és un repaletat de sam).
|
// amb la llum correcta per a la sala actual.
|
||||||
Player::loadFrom(config_.player_yaml);
|
Player::Config cfg_p1 = loadPlayerConfigWithRoomOverride(config_.player_yaml);
|
||||||
|
Player::Config cfg_p2 = loadPlayerConfigWithRoomOverride(config_.player2_yaml);
|
||||||
|
|
||||||
// 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
|
||||||
@@ -384,8 +396,9 @@ GameScene::GameScene(Config config)
|
|||||||
// Spawn lligat a la porta: tots els jugadors apareixen davant del
|
// Spawn lligat a la porta: tots els jugadors apareixen davant del
|
||||||
// passadís de sortida. `computeSpawn` deriva les coordenades de
|
// passadís de sortida. `computeSpawn` deriva les coordenades de
|
||||||
// `Map::door_col` perquè canviar la columna de la porta al YAML
|
// `Map::door_col` perquè canviar la columna de la porta al YAML
|
||||||
// moga automàticament l'spawn — no hi ha cap valor hardcoded.
|
// moga automàticament l'spawn — no hi ha cap valor hardcoded. P1
|
||||||
const auto SPAWN = computeSpawn();
|
// dicta el centrament (consistent amb el comportament pre-refactor).
|
||||||
|
const auto SPAWN = computeSpawn(cfg_p1);
|
||||||
const bool BOTH_ACTIVE = (USE_P1 && USE_P2);
|
const bool BOTH_ACTIVE = (USE_P1 && USE_P2);
|
||||||
|
|
||||||
// Índex de tile amb què pinta cada jugador — llegit del YAML del mapa
|
// Índex de tile amb què pinta cada jugador — llegit del YAML del mapa
|
||||||
@@ -411,8 +424,8 @@ GameScene::GameScene(Config config)
|
|||||||
if (USE_P1) { p1_x = SPAWN.p1_x + TILE_F; }
|
if (USE_P1) { p1_x = SPAWN.p1_x + TILE_F; }
|
||||||
if (USE_P2) { p2_x = SPAWN.p2_x - TILE_F; }
|
if (USE_P2) { p2_x = SPAWN.p2_x - TILE_F; }
|
||||||
}
|
}
|
||||||
players_.emplace_back(std::make_unique<Player>(ANIM_P1, ID_P1, p1_x, SPAWN.y, CONTROLS_P1, Map::makeInfo(), P1_PAINT));
|
players_.emplace_back(std::make_unique<Player>(std::move(cfg_p1), ANIM_P1, ID_P1, p1_x, SPAWN.y, CONTROLS_P1, Map::makeInfo(), P1_PAINT));
|
||||||
players_.emplace_back(std::make_unique<Player>(ANIM_P2, ID_P2, p2_x, SPAWN.y, CONTROLS_P2, Map::makeInfo(), P2_PAINT));
|
players_.emplace_back(std::make_unique<Player>(std::move(cfg_p2), ANIM_P2, ID_P2, p2_x, SPAWN.y, CONTROLS_P2, Map::makeInfo(), P2_PAINT));
|
||||||
|
|
||||||
// Tots els slots arrenquen DISABLED. Els actius els reactivarà el
|
// Tots els slots arrenquen DISABLED. Els actius els reactivarà el
|
||||||
// callback de fi de música d'INTRO via `completeIntro()` (spawn aeri
|
// callback de fi de música d'INTRO via `completeIntro()` (spawn aeri
|
||||||
@@ -465,10 +478,12 @@ GameScene::GameScene(Config config)
|
|||||||
auto hud_font_surface = Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY);
|
auto hud_font_surface = Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY);
|
||||||
auto hud_font = std::make_shared<Text>(hud_font_surface, UiConfig::get().hudFont().fnt);
|
auto hud_font = std::make_shared<Text>(hud_font_surface, UiConfig::get().hudFont().fnt);
|
||||||
hud_font->setDefaultKerning(UiConfig::get().hudFont().kerning);
|
hud_font->setDefaultKerning(UiConfig::get().hudFont().kerning);
|
||||||
// Vides inicials del HUD = `initial_lives` del YAML del jugador
|
// Vides inicials del HUD = `initial_lives` del Config del P1.
|
||||||
// (`Player::loadFrom` ja ha omplert `death_config` abans d'aquí).
|
// (Cada Player té el seu propi Config post-refactor a per-instance;
|
||||||
// Sense això, el marcador es queda fixe a 3 i el YAML perd efecte.
|
// el HUD agafa el de P1 com a referència — historicament era el
|
||||||
hud_ = std::make_unique<Hud>(entity_manager_.getBus(), hud_font, Player::getInitialLives());
|
// valor compartit per tots els jugadors). Si en algun futur P1 i P2
|
||||||
|
// arrenquen amb vides diferents, caldria un HUD per slot.
|
||||||
|
hud_ = std::make_unique<Hud>(entity_manager_.getBus(), hud_font, players_[0]->getInitialLives());
|
||||||
// Activem el bloc HUD per als slots que arranquen en partida. Els
|
// Activem el bloc HUD per als slots que arranquen en partida. Els
|
||||||
// inactius queden mostrant el prompt "POLSA START / PER JUGAR" via
|
// inactius queden mostrant el prompt "POLSA START / PER JUGAR" via
|
||||||
// `refreshJoinPrompt` cada frame. Tots dos s'haurien de poder
|
// `refreshJoinPrompt` cada frame. Tots dos s'haurien de poder
|
||||||
@@ -530,19 +545,19 @@ GameScene::~GameScene() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto GameScene::computeSpawn() -> PlayerSpawn {
|
auto GameScene::computeSpawn(const Player::Config& cfg) -> PlayerSpawn {
|
||||||
// Centre de la columna del passadís en píxels canvas. La caixa de
|
// Centre de la columna del passadís en píxels canvas. La caixa de
|
||||||
// col·lisió del jugador (12 px) cap dins del passadís (16 px) amb 2
|
// col·lisió del jugador (12 px) cap dins del passadís (16 px) amb 2
|
||||||
// px de marge a banda i banda; alinear el sprite al centre de la
|
// px de marge a banda i banda; alinear el sprite al centre de la
|
||||||
// columna garanteix que la base del jugador i la del passadís
|
// columna garanteix que la base del jugador i la del passadís
|
||||||
// coincideixin verticalment.
|
// coincideixin verticalment.
|
||||||
const float DOOR_CENTER_X = static_cast<float>((Map::door_col * Map::tile_size) + Map::tilemap_origin_x) + (static_cast<float>(Map::tile_size) / 2.0F);
|
const float DOOR_CENTER_X = static_cast<float>((Map::door_col * Map::tile_size) + Map::tilemap_origin_x) + (static_cast<float>(Map::tile_size) / 2.0F);
|
||||||
const float CENTER_TL_X = DOOR_CENTER_X - (static_cast<float>(Player::sprite_w) / 2.0F);
|
const float CENTER_TL_X = DOOR_CENTER_X - (static_cast<float>(cfg.sprite_w) / 2.0F);
|
||||||
|
|
||||||
// Y: centre del corredor exterior superior (mateix valor que abans;
|
// Y: centre del corredor exterior superior (mateix valor que abans;
|
||||||
// el corredor és simètric respecte a `playfield_y`).
|
// el corredor és simètric respecte a `playfield_y`).
|
||||||
const float CORRIDOR_CENTER_Y = static_cast<float>(Map::playfield_y) + (static_cast<float>(Map::outer_corridor) / 2.0F);
|
const float CORRIDOR_CENTER_Y = static_cast<float>(Map::playfield_y) + (static_cast<float>(Map::outer_corridor) / 2.0F);
|
||||||
const float SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(Player::collision_y_offset) - (static_cast<float>(Player::collision_size) / 2.0F);
|
const float SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(cfg.collision_y_offset) - (static_cast<float>(cfg.collision_size) / 2.0F);
|
||||||
|
|
||||||
// Mode BOTH: P1 a -tile_size, P2 a +tile_size respecte al centre.
|
// Mode BOTH: P1 a -tile_size, P2 a +tile_size respecte al centre.
|
||||||
// Els callsites individuals decideixen si usar aquests valors o si
|
// Els callsites individuals decideixen si usar aquests valors o si
|
||||||
@@ -560,6 +575,18 @@ 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 la Config de cada jugador llegint el seu YAML i aplicant
|
||||||
|
// l'override d'aquesta sala. Sense això, una sala amb `players: false`
|
||||||
|
// deixaria `light_radius=0` al jugador i, en transicionar a una altra
|
||||||
|
// sala que no toqui el camp (o digui `players: true`), els jugadors
|
||||||
|
// no tornarien a emetre — l'override és "additiu" sobre el Config
|
||||||
|
// actual, no un reset, així que cal partir sempre del YAML.
|
||||||
|
if (!players_.empty() && players_[0] != nullptr) {
|
||||||
|
players_[0]->setConfig(loadPlayerConfigWithRoomOverride(config_.player_yaml));
|
||||||
|
}
|
||||||
|
if (players_.size() >= 2 && players_[1] != nullptr) {
|
||||||
|
players_[1]->setConfig(loadPlayerConfigWithRoomOverride(config_.player2_yaml));
|
||||||
|
}
|
||||||
// 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
|
||||||
@@ -663,7 +690,7 @@ void GameScene::respawnPlayersInstant() {
|
|||||||
// música de sala, sense caiguda aèria). Reusem `computeSpawn` i el
|
// música de sala, sense caiguda aèria). Reusem `computeSpawn` i el
|
||||||
// mateix patró de `triggerPlayerSpawn` però cridem `respawn(x, y)`
|
// mateix patró de `triggerPlayerSpawn` però cridem `respawn(x, y)`
|
||||||
// (teleport instantani) en lloc de `spawnFromAir(x, y)`.
|
// (teleport instantani) en lloc de `spawnFromAir(x, y)`.
|
||||||
const auto SPAWN = computeSpawn();
|
const auto SPAWN = computeSpawn(players_[0]->getConfig());
|
||||||
const bool BOTH_ACTIVE = (players_active_[0] && players_active_[1]);
|
const bool BOTH_ACTIVE = (players_active_[0] && players_active_[1]);
|
||||||
const float SINGLE_X = SPAWN.p1_x + static_cast<float>(Map::tile_size);
|
const float SINGLE_X = SPAWN.p1_x + static_cast<float>(Map::tile_size);
|
||||||
for (std::size_t i = 0; i < players_.size(); ++i) {
|
for (std::size_t i = 0; i < players_.size(); ++i) {
|
||||||
@@ -694,7 +721,7 @@ void GameScene::triggerPlayerSpawn(std::size_t slot_index) {
|
|||||||
|
|
||||||
// Recalcula spawn per si door_col canvia entre sales (avui no, però
|
// Recalcula spawn per si door_col canvia entre sales (avui no, però
|
||||||
// la lògica ja queda preparada per a l'itinerari).
|
// la lògica ja queda preparada per a l'itinerari).
|
||||||
const auto SPAWN = computeSpawn();
|
const auto SPAWN = computeSpawn(players_[0]->getConfig());
|
||||||
const bool BOTH_ACTIVE = (players_active_[0] && players_active_[1]);
|
const bool BOTH_ACTIVE = (players_active_[0] && players_active_[1]);
|
||||||
// Mode single-player: l'únic actiu queda centrat sota la porta. Els
|
// Mode single-player: l'únic actiu queda centrat sota la porta. Els
|
||||||
// valors de SPAWN.p1_x/p2_x estan fets per al cas BOTH (offset ±tile);
|
// valors de SPAWN.p1_x/p2_x estan fets per al cas BOTH (offset ±tile);
|
||||||
@@ -799,7 +826,7 @@ void GameScene::joinPlayer(std::size_t slot_index) {
|
|||||||
// porta de sortida. Mai el centre (en single-player la cinemàtica
|
// porta de sortida. Mai el centre (en single-player la cinemàtica
|
||||||
// inicial sí que centra el slot actiu, però en un join mid-partida
|
// inicial sí que centra el slot actiu, però en un join mid-partida
|
||||||
// volem el costat per coherència visual amb la regla 2).
|
// volem el costat per coherència visual amb la regla 2).
|
||||||
const auto SPAWN = computeSpawn();
|
const auto SPAWN = computeSpawn(players_[0]->getConfig());
|
||||||
const float X = (slot_index == 0) ? SPAWN.p1_x : SPAWN.p2_x;
|
const float X = (slot_index == 0) ? SPAWN.p1_x : SPAWN.p2_x;
|
||||||
players_[slot_index]->setSpawnPoint(X, SPAWN.y);
|
players_[slot_index]->setSpawnPoint(X, SPAWN.y);
|
||||||
players_[slot_index]->resetForJoin();
|
players_[slot_index]->resetForJoin();
|
||||||
@@ -1013,26 +1040,28 @@ void GameScene::applyLightingPass() {
|
|||||||
// serà tocar `Map::light_level` per sales concretes.
|
// serà tocar `Map::light_level` per sales concretes.
|
||||||
std::vector<Lighting::Emitter> emitters;
|
std::vector<Lighting::Emitter> emitters;
|
||||||
emitters.reserve(players_.size() + entity_manager_.entityCount());
|
emitters.reserve(players_.size() + entity_manager_.entityCount());
|
||||||
const int PLAYER_R = Player::getLightRadius();
|
// Cada jugador té el seu propi config (post-refactor a per-instance):
|
||||||
// Quan el YAML del jugador no declara `light:`, `PLAYER_R` és 0 i
|
// radi, color, flicker_params, etc. poden diferir entre P1 i P2.
|
||||||
// `Lighting::apply` salta el cercle al filtre intern. No fem un
|
// Quan el YAML d'un jugador no declara `light:`, el radi és 0 i
|
||||||
// gating extern aquí (els jugadors són només 1-2 actors i el cost
|
// `Lighting::apply` salta el cercle al filtre intern.
|
||||||
// d'afegir-los al vector és menyspreable).
|
|
||||||
const bool PLAYER_FLICKER = Player::getLightFlicker();
|
|
||||||
for (const auto& p : players_) {
|
for (const auto& p : players_) {
|
||||||
if (p->isOutOfPlay()) { continue; }
|
if (p->isOutOfPlay()) { continue; }
|
||||||
|
const int R = p->getLightRadius();
|
||||||
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});
|
const Lighting::Color COL{.r = p->getLightColorR(), .g = p->getLightColorG(), .b = p->getLightColorB()};
|
||||||
|
emitters.push_back({.cx = C.x, .cy = C.y, .radius = R, .seed = p->getLightSeed(), .flicker = p->getLightFlicker(), .color = COL, .flicker_params = p->getFlickerParams()});
|
||||||
}
|
}
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "core/ui/ui_config.hpp"
|
#include "core/ui/ui_config.hpp"
|
||||||
#include "game/debug_config.hpp"
|
#include "game/debug_config.hpp"
|
||||||
#include "game/entities/entity_manager.hpp"
|
#include "game/entities/entity_manager.hpp"
|
||||||
|
#include "game/entities/player.hpp"
|
||||||
#include "game/map/paint_layer.hpp"
|
#include "game/map/paint_layer.hpp"
|
||||||
#include "game/map/tilemap.hpp"
|
#include "game/map/tilemap.hpp"
|
||||||
#include "game/scenes/game_over_fsm.hpp"
|
#include "game/scenes/game_over_fsm.hpp"
|
||||||
@@ -21,7 +22,6 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
class Hud;
|
class Hud;
|
||||||
class Player;
|
|
||||||
class Text;
|
class Text;
|
||||||
|
|
||||||
class GameScene : public Scene {
|
class GameScene : public Scene {
|
||||||
@@ -382,7 +382,11 @@ class GameScene : public Scene {
|
|||||||
float p2_x{0.0F};
|
float p2_x{0.0F};
|
||||||
float y{0.0F};
|
float y{0.0F};
|
||||||
};
|
};
|
||||||
[[nodiscard]] static auto computeSpawn() -> PlayerSpawn;
|
// Calcula la posició de spawn dels jugadors usant la geometria del
|
||||||
|
// sprite/col·lisió del `Config` passat (típicament la del P1, que
|
||||||
|
// dicta el centrament — si en algun futur P2 té geometria diferent,
|
||||||
|
// el seu propi spawn pot recalcular-se amb el seu `cfg`).
|
||||||
|
[[nodiscard]] static auto computeSpawn(const Player::Config& cfg) -> PlayerSpawn;
|
||||||
|
|
||||||
// Recarrega la sala apuntada per `yaml_path`: refresca Map (geometria,
|
// Recarrega la sala apuntada per `yaml_path`: refresca Map (geometria,
|
||||||
// porta tancada), entitats (sarcs/porta/momies des de zero), neteja
|
// porta tancada), entitats (sarcs/porta/momies des de zero), neteja
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.46.0"; // Single source of truth — Makefile el grep
|
||||||
} // namespace Texts
|
} // namespace Texts
|
||||||
|
|
||||||
namespace GameCanvas {
|
namespace GameCanvas {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in new issue
Block a user