Merge branch 'feature/animation-loader-unification'
This commit is contained in:
commit
24609d6c5c
25 files changed
+509
-240
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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,7 +15,7 @@ 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+33
-5
@@ -15,11 +15,32 @@ 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
|
# Il·luminació de la sala. Bloc opcional; si manca, la sala queda
|
||||||
# (com hui, sense efecte visual); 0.0 = completament negra fora del radi
|
# totalment il·luminada (1.0) i tots els tipus d'entitat emeten amb els
|
||||||
# d'un emissor. El gradient dels cercles d'emissor viu a
|
# valors declarats al seu YAML de tipus (comportament històric).
|
||||||
# data/config/lighting.yaml. Omet aquesta clau per deixar la sala neta.
|
#
|
||||||
light_level: 0.25
|
# 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.75
|
||||||
|
emitters:
|
||||||
|
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
|
||||||
@@ -160,3 +181,10 @@ entities:
|
|||||||
frame: 18
|
frame: 18
|
||||||
- type: mummy
|
- type: mummy
|
||||||
position: [10, 13]
|
position: [10, 13]
|
||||||
|
# Override per-instància: la sala apaga les momies (emitters.mummies:
|
||||||
|
# false), però aquesta concreta sí emet, amb un radi més curt que el
|
||||||
|
# default del seu tipus (24 → 16). Bona prova de la tercera capa de
|
||||||
|
# resolució: tipus → sala → spawn.
|
||||||
|
light:
|
||||||
|
radius: 16
|
||||||
|
flicker: true
|
||||||
@@ -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,42 +33,18 @@ namespace {
|
|||||||
return sprite;
|
return sprite;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construeix l'`AnimationComponent` del swing a partir del bloc
|
// Llig el camp `active_frames` del bloc `animations.swing` — específic
|
||||||
// `animations.swing`. Convenció igual que la resta d'entitats:
|
// del látigo: els índexs llistats són els frames on la hitbox aplica
|
||||||
// - `frames` : llista d'índexs dins l'spritesheet (obligatori).
|
// dany (wind-up i recovery no danyen). La resta de l'animació la
|
||||||
// - `frame_ms` : durada uniforme per frame (ms). Alternativament `fps`.
|
// construeix `AnimationComponent::fromYaml`.
|
||||||
// - `loop` : per defecte true; al látigo el volem false (mor en
|
void readActiveFrames(const fkyaml::node& yaml, std::vector<int>& active_out) {
|
||||||
// completar).
|
if (!yaml.contains("animations") || !yaml["animations"].contains("swing")) { return; }
|
||||||
auto buildSwingAnim(const fkyaml::node& yaml, std::vector<int>& active_out)
|
const auto& sw = yaml["animations"]["swing"];
|
||||||
-> AnimationComponent {
|
if (!sw.contains("active_frames")) { return; }
|
||||||
std::vector<int> frames = {0};
|
for (const auto& f : sw["active_frames"]) {
|
||||||
float fps = 8.0F;
|
// cppcheck-suppress useStlAlgorithm
|
||||||
bool loop = false;
|
active_out.push_back(f.get_value<int>());
|
||||||
if (yaml.contains("animations") && yaml["animations"].contains("swing")) {
|
|
||||||
const auto& sw = yaml["animations"]["swing"];
|
|
||||||
if (sw.contains("frames")) {
|
|
||||||
frames.clear();
|
|
||||||
for (const auto& f : sw["frames"]) {
|
|
||||||
// cppcheck-suppress useStlAlgorithm
|
|
||||||
frames.push_back(f.get_value<int>());
|
|
||||||
}
|
|
||||||
if (frames.empty()) { frames.push_back(0); }
|
|
||||||
}
|
|
||||||
if (sw.contains("frame_ms")) {
|
|
||||||
const auto MS = static_cast<float>(sw["frame_ms"].get_value<int>());
|
|
||||||
if (MS > 0.0F) { fps = 1000.0F / MS; }
|
|
||||||
} else if (sw.contains("fps")) {
|
|
||||||
fps = sw["fps"].get_value<float>();
|
|
||||||
}
|
|
||||||
if (sw.contains("loop")) { loop = sw["loop"].get_value<bool>(); }
|
|
||||||
if (sw.contains("active_frames")) {
|
|
||||||
for (const auto& f : sw["active_frames"]) {
|
|
||||||
// cppcheck-suppress useStlAlgorithm
|
|
||||||
active_out.push_back(f.get_value<int>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {frames, fps, loop};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -86,7 +62,8 @@ WhipAttack::WhipAttack(const fkyaml::node& yaml) {
|
|||||||
if (s.contains("west")) { sprite_west_ = readDirSprite(s["west"]); }
|
if (s.contains("west")) { sprite_west_ = readDirSprite(s["west"]); }
|
||||||
}
|
}
|
||||||
|
|
||||||
anim_swing_ = buildSwingAnim(yaml, active_frames_);
|
anim_swing_ = AnimationComponent::fromYaml(yaml, "swing");
|
||||||
|
readActiveFrames(yaml, active_frames_);
|
||||||
anim_ = anim_swing_;
|
anim_ = anim_swing_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -174,6 +174,49 @@ namespace {
|
|||||||
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, LightTemplate type_default) -> LightTemplate {
|
||||||
|
const auto* room = Map::getRoomEmitterOverride(type);
|
||||||
|
if (room == nullptr) { return type_default; }
|
||||||
|
if (!room->enabled) { return {.radius = 0, .flicker = false}; }
|
||||||
|
LightTemplate out = type_default;
|
||||||
|
if (room->radius.has_value()) { out.radius = *room->radius; }
|
||||||
|
if (room->flicker.has_value()) { out.flicker = *room->flicker; }
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplica l'override per-spawn (si existeix) per damunt del que ja
|
||||||
|
// viu a l'entitat (defaults del tipus + override de sala). Tres formes
|
||||||
|
// a l'entry del YAML de nivell:
|
||||||
|
// `light: false` → apaga aquesta instància
|
||||||
|
// `light: true` → no canvia res; la línia és
|
||||||
|
// informativa (la llum ja la donen
|
||||||
|
// les capes inferiors)
|
||||||
|
// `light: { radius, flicker }` → els camps presents sobreescriuen;
|
||||||
|
// els absents respecten el que ja
|
||||||
|
// duia l'entitat
|
||||||
|
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>()); }
|
||||||
|
}
|
||||||
|
|
||||||
// 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,7 +339,11 @@ 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);
|
||||||
anchor_out = readAnchor(tmpl);
|
anchor_out = readAnchor(tmpl);
|
||||||
@@ -425,7 +472,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 +508,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;
|
||||||
|
|||||||
+63
-2
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#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 +29,65 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Llegeix el bloc opcional `lighting:` del YAML d'una sala i
|
||||||
|
// actualitza `light_level` (si declara `level`) i el mapping
|
||||||
|
// `g_room_emitters`. Idempotent: cada `Map::loadFrom` neteja el
|
||||||
|
// mapping abans de poblar-lo, així no s'arrosseguen overrides
|
||||||
|
// d'una sala anterior.
|
||||||
|
void loadLightingBlock(const fkyaml::node& yaml) {
|
||||||
|
g_room_emitters.clear();
|
||||||
|
if (!yaml.contains("lighting")) { return; }
|
||||||
|
const auto& lighting = yaml["lighting"];
|
||||||
|
if (lighting.contains("level")) {
|
||||||
|
light_level = lighting["level"].get_value<float>();
|
||||||
|
}
|
||||||
|
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>());
|
||||||
|
const auto& val = it.value();
|
||||||
|
RoomEmitterOverride ov;
|
||||||
|
if (val.is_boolean()) {
|
||||||
|
ov.enabled = val.get_value<bool>();
|
||||||
|
} else if (val.is_mapping()) {
|
||||||
|
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>(); }
|
||||||
|
}
|
||||||
|
g_room_emitters[KEY] = ov;
|
||||||
|
}
|
||||||
|
}
|
||||||
} // 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 +157,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);
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-1
@@ -3,6 +3,7 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
@@ -54,9 +55,41 @@ 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;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|||||||
@@ -232,6 +232,26 @@ namespace {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override de sala per a la llum del jugador. Si el YAML de la sala
|
||||||
|
// declara `lighting.emitters.players`, substitueix els valors carregats
|
||||||
|
// per `Player::loadFrom`. Extret aquí per no inflar la complexitat
|
||||||
|
// cognitiva del ctor de GameScene (clang-tidy: cognitive-complexity).
|
||||||
|
// No s'aplica una "tercera capa" per-instància al jugador perquè no
|
||||||
|
// neix com a spawn del nivell; el dia d'un ítem "linterna" aquell
|
||||||
|
// sistema mutarà els mateixos camps en runtime — ortogonal a aquest
|
||||||
|
// override.
|
||||||
|
void applyRoomLightOverrideToPlayer() {
|
||||||
|
const auto* room = Map::getRoomEmitterOverride("player");
|
||||||
|
if (room == nullptr) { return; }
|
||||||
|
if (!room->enabled) {
|
||||||
|
Player::light_radius = 0;
|
||||||
|
Player::light_flicker = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (room->radius.has_value()) { Player::light_radius = *room->radius; }
|
||||||
|
if (room->flicker.has_value()) { Player::light_flicker = *room->flicker; }
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
auto GameScene::defaultConfig() -> Config {
|
auto GameScene::defaultConfig() -> Config {
|
||||||
@@ -289,6 +309,7 @@ GameScene::GameScene(Config config)
|
|||||||
// del YAML de P1 és suficient (els dos personatges comparteixen
|
// del YAML de P1 és suficient (els dos personatges comparteixen
|
||||||
// geometria — morcus és un repaletat de sam).
|
// geometria — morcus és un repaletat de sam).
|
||||||
Player::loadFrom(config_.player_yaml);
|
Player::loadFrom(config_.player_yaml);
|
||||||
|
applyRoomLightOverrideToPlayer();
|
||||||
|
|
||||||
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
|
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
|
||||||
// etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa
|
// etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa
|
||||||
|
|||||||
@@ -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