diff --git a/data/entities/column/column.yaml b/data/entities/column/column.yaml index 28969c7..0001e81 100644 --- a/data/entities/column/column.yaml +++ b/data/entities/column/column.yaml @@ -19,4 +19,4 @@ flags: collides: true interactable: false animations: - default: { frames: [0], fps: 1, loop: false } + default: { frames: [0], frame_ms: 1000, loop: false } diff --git a/data/entities/door/door.yaml b/data/entities/door/door.yaml index b69bf66..f6ea47b 100644 --- a/data/entities/door/door.yaml +++ b/data/entities/door/door.yaml @@ -21,9 +21,9 @@ flags: collides: true interactable: true animations: - closed: { frames: [1], fps: 1, loop: true } - opening: { frames: [0], fps: 6, loop: false } - open: { frames: [0], fps: 1, loop: true } + closed: { frames: [1], frame_ms: 1000, loop: true } + opening: { frames: [0], frame_ms: 167, loop: false } + open: { frames: [0], frame_ms: 1000, loop: true } behaviour: states: [CLOSED, OPENING, OPEN] initial_state: CLOSED diff --git a/data/entities/dust/dust.yaml b/data/entities/dust/dust.yaml index 612cd5b..693b680 100644 --- a/data/entities/dust/dust.yaml +++ b/data/entities/dust/dust.yaml @@ -16,7 +16,7 @@ flags: # apropiada via `setVariant` abans del primer tick. # jump: frames 0..6 (7 frames) → reproduït al despegue. # 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: - jump: { frames: [0, 1, 2, 3, 4, 5, 6], fps: 15, loop: false } - land: { frames: [7, 8, 9, 10, 11, 12], 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], frame_ms: 67, loop: false } diff --git a/data/entities/floor_trap/floor_trap.yaml b/data/entities/floor_trap/floor_trap.yaml index d99c006..2fd1cb2 100644 --- a/data/entities/floor_trap/floor_trap.yaml +++ b/data/entities/floor_trap/floor_trap.yaml @@ -13,8 +13,8 @@ flags: collides: false interactable: false animations: - inactive: { frames: [0], fps: 1, loop: true } - active: { frames: [0], fps: 6, loop: true } + inactive: { frames: [0], frame_ms: 1000, loop: true } + active: { frames: [0], frame_ms: 167, loop: true } cycle: active_duration: 0.3 inactive_duration: 2.0 diff --git a/data/entities/hole/hole.yaml b/data/entities/hole/hole.yaml index 900f59e..dbcbe6e 100644 --- a/data/entities/hole/hole.yaml +++ b/data/entities/hole/hole.yaml @@ -22,6 +22,6 @@ flags: # lectura genèrica de flags. triggers_on_overlap: true animations: - idle: { frames: [0], fps: 1, loop: true } + idle: { frames: [0], frame_ms: 1000, loop: true } stats: damage: 0 diff --git a/data/entities/mummy/mummy.yaml b/data/entities/mummy/mummy.yaml index c783094..117b7bf 100644 --- a/data/entities/mummy/mummy.yaml +++ b/data/entities/mummy/mummy.yaml @@ -32,11 +32,18 @@ flags: # no decideix flip per direcció (només selecciona quina animació mostrar). # La momia no té variants `stand_*` perquè mai no para. animations: - walk_south: { frames: [0, 1, 2, 3], fps: 8, loop: true } - walk_north: { frames: [4, 5, 6, 7], fps: 8, loop: true } - walk_east: { frames: [8, 9, 10, 11], fps: 8, loop: true } - walk_west: { frames: [8, 9, 10, 11], fps: 8, 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 } + walk_south: { frames: [0, 1, 2, 3], frame_ms: 125, loop: true } + walk_north: { frames: [4, 5, 6, 7], frame_ms: 125, loop: true } + walk_east: { frames: [8, 9, 10, 11], frame_ms: 125, loop: true } + walk_west: { frames: [8, 9, 10, 11], frame_ms: 125, loop: true, flip_h: true } + # 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: type: corridor_wanderer direction_choice: random diff --git a/data/entities/rolling_rock/rolling_rock.yaml b/data/entities/rolling_rock/rolling_rock.yaml index 95f3745..5d30ec1 100644 --- a/data/entities/rolling_rock/rolling_rock.yaml +++ b/data/entities/rolling_rock/rolling_rock.yaml @@ -13,7 +13,7 @@ flags: collides: true interactable: false animations: - roll: { frames: [0], fps: 10, loop: true } + roll: { frames: [0], frame_ms: 100, loop: true } ai: type: straight_line initial_direction: right diff --git a/data/entities/sarcophagus/sarcophagus.yaml b/data/entities/sarcophagus/sarcophagus.yaml index e6466b3..2884ea0 100644 --- a/data/entities/sarcophagus/sarcophagus.yaml +++ b/data/entities/sarcophagus/sarcophagus.yaml @@ -19,9 +19,9 @@ flags: collides: true interactable: true animations: - closed: { frames: [0], fps: 1, loop: true } - opening: { frames: [1, 2], fps: 6, loop: false } - open: { frames: [3], fps: 1, loop: true } + closed: { frames: [0], frame_ms: 1000, loop: true } + opening: { frames: [1, 2], frame_ms: 167, loop: false } + open: { frames: [3], frame_ms: 1000, loop: true } behaviour: states: [CLOSED, OPENING, OPEN] initial_state: CLOSED diff --git a/data/entities/stone/stone.yaml b/data/entities/stone/stone.yaml index 8017d33..2252111 100644 --- a/data/entities/stone/stone.yaml +++ b/data/entities/stone/stone.yaml @@ -19,4 +19,4 @@ flags: # Animació "default" amb un sol frame: el spawn YAML pot sobreescriure-la # via `frame: N` per fixar la variant escollida al editor. animations: - default: { frames: [0], fps: 1, loop: false } + default: { frames: [0], frame_ms: 1000, loop: false } diff --git a/data/entities/torch/torch.yaml b/data/entities/torch/torch.yaml index 488cdb9..3afa6f4 100644 --- a/data/entities/torch/torch.yaml +++ b/data/entities/torch/torch.yaml @@ -15,7 +15,7 @@ flags: # 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. 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; # el gradient global (data/config/lighting.yaml) hi posa la forma (3 diff --git a/data/entities/wall_trap/wall_trap.yaml b/data/entities/wall_trap/wall_trap.yaml index 39461c1..b385387 100644 --- a/data/entities/wall_trap/wall_trap.yaml +++ b/data/entities/wall_trap/wall_trap.yaml @@ -13,8 +13,8 @@ flags: collides: false interactable: false animations: - inactive: { frames: [0], fps: 1, loop: true } - active: { frames: [0], fps: 6, loop: true } + inactive: { frames: [0], frame_ms: 1000, loop: true } + active: { frames: [0], frame_ms: 167, loop: true } cycle: active_duration: 0.5 inactive_duration: 1.5 diff --git a/source/game/entities/animation_component.cpp b/source/game/entities/animation_component.cpp index e63b62f..2071595 100644 --- a/source/game/entities/animation_component.cpp +++ b/source/game/entities/animation_component.cpp @@ -1,14 +1,87 @@ #include "game/entities/animation_component.hpp" #include +#include #include -AnimationComponent::AnimationComponent(std::vector frames, float fps, bool loop) - : AnimationComponent(std::move(frames), fps, loop, false, false) {} +namespace { -AnimationComponent::AnimationComponent(std::vector 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 speeds, std::size_t frame_count) -> std::vector { + if (frame_count == 0) { return {}; } + if (speeds.empty()) { + return std::vector(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..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 { + std::vector frames; + for (const auto& f : anim["frames"]) { + // cppcheck-suppress useStlAlgorithm + frames.push_back(f.get_value()); + } + if (frames.empty()) { frames.push_back(0); } + return frames; + } + + // Llig `animations..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 { + std::vector 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(v.get_value()) / 1000.0F); + } + } else { + const float SECS = static_cast(fm.get_value()) / 1000.0F; + speeds_secs.assign(frame_count, SECS); + } + return speeds_secs; + } + +} // namespace + +AnimationComponent::AnimationComponent(std::vector frames, std::vector speeds, bool loop, bool flip_h, bool flip_v) : frames_(std::move(frames)), - fps_(fps > 0.0F ? fps : 1.0F), + speeds_(fitSpeeds(std::move(speeds), frames_.size())), loop_(loop), flip_h_(flip_h), flip_v_(flip_v) { @@ -19,13 +92,52 @@ AnimationComponent::AnimationComponent(std::vector frames, float fps, bool if (frames_.size() <= 1 && !loop_) { completed_ = true; } } +auto AnimationComponent::uniform(std::vector frames, float frame_ms, bool loop) -> AnimationComponent { + return uniform(std::move(frames), frame_ms, loop, false, false); +} + +auto AnimationComponent::uniform(std::vector frames, float frame_ms, bool loop, bool flip_h, bool flip_v) -> AnimationComponent { + const float SECS = sanitizeSpeed(frame_ms / 1000.0F); + std::vector 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.` no + // existeix: 1 frame loopant a 1s — visualment estàtic, semàntica neutra. + std::vector frames = {0}; + std::vector 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(); } + if (anim.contains("flip_h")) { flip_h = anim["flip_h"].get_value(); } + if (anim.contains("flip_v")) { flip_v = anim["flip_v"].get_value(); } + + return {std::move(frames), std::move(speeds_secs), loop, flip_h, flip_v}; +} + void AnimationComponent::tick(float delta_time) { if (frames_.size() <= 1 || completed_) { return; } accumulator_ += delta_time; - const float FRAME_TIME = 1.0F / fps_; - while (accumulator_ >= FRAME_TIME) { - accumulator_ -= FRAME_TIME; + while (accumulator_ >= speeds_[static_cast(current_)]) { + accumulator_ -= speeds_[static_cast(current_)]; ++current_; if (static_cast(current_) >= frames_.size()) { if (loop_) { diff --git a/source/game/entities/animation_component.hpp b/source/game/entities/animation_component.hpp index 1343eef..2095600 100644 --- a/source/game/entities/animation_component.hpp +++ b/source/game/entities/animation_component.hpp @@ -3,8 +3,11 @@ #include #include +#include #include +#include "external/fkyaml_node.hpp" + // Component reutilitzable d'animació: qualsevol entitat que necessiti // 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 @@ -22,16 +25,41 @@ // 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`) // 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 { public: AnimationComponent() = default; - AnimationComponent(std::vector frames, float fps, bool loop); - AnimationComponent(std::vector frames, float fps, bool loop, bool flip_h, bool flip_v); + + // Factory canònica: llig el bloc `animations.` 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 frames, float frame_ms, bool loop) -> AnimationComponent; + static auto uniform(std::vector 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 - // res. En mode loop, `current_frame_` rebota a 0 en passar del final; - // en mode no-loop, es queda al darrer frame i `completed()` passa a - // true. + // res. En mode loop, `current_` rebota a 0 en passar del final; en + // mode no-loop, es queda al darrer frame i `completed()` passa a true. + // Cada frame consumeix `speeds_[current_]` segons. void tick(float delta_time); // Torna al primer frame i reinicia l'acumulador. Útil en transicions @@ -39,8 +67,7 @@ class AnimationComponent { void reset(); // Índex actual dins `frames_`. Si `frames_` està buit retorna 0 com - // a fallback tolerant (el loader ja ha emès warning si hi havia - // frames fora de rang; aquí preferim no crashejar). + // a fallback tolerant. [[nodiscard]] auto currentFrame() const -> int; [[nodiscard]] auto frameCount() const -> std::size_t { return frames_.size(); } @@ -55,8 +82,10 @@ class AnimationComponent { [[nodiscard]] auto flipMode() const -> SDL_FlipMode; private: + AnimationComponent(std::vector frames, std::vector speeds, bool loop, bool flip_h, bool flip_v); + std::vector frames_; - float fps_{1.0F}; + std::vector speeds_; // segons per frame; size == frames_.size() bool loop_{true}; bool flip_h_{false}; bool flip_v_{false}; diff --git a/source/game/entities/concrete/door.cpp b/source/game/entities/concrete/door.cpp index d1f9c43..3f9aaaa 100644 --- a/source/game/entities/concrete/door.cpp +++ b/source/game/entities/concrete/door.cpp @@ -1,45 +1,16 @@ #include "game/entities/concrete/door.hpp" #include -#include #include "game/entities/entity_factory.hpp" #include "game/entities/event_bus.hpp" #include "game/entities/player.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 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()); - } - if (frames.empty()) { frames.push_back(0); } - } - if (anim.contains("fps")) { fps = anim["fps"].get_value(); } - if (anim.contains("loop")) { loop = anim["loop"].get_value(); } - } - return {frames, fps, loop}; - } - -} // namespace - Door::Door(const fkyaml::node& yaml) - : anim_closed_(buildAnimation(yaml, "closed")), - anim_opening_(buildAnimation(yaml, "opening")), - anim_open_(buildAnimation(yaml, "open")) { + : anim_closed_(AnimationComponent::fromYaml(yaml, "closed")), + anim_opening_(AnimationComponent::fromYaml(yaml, "opening")), + anim_open_(AnimationComponent::fromYaml(yaml, "open")) { name_ = yaml.contains("name") ? yaml["name"].get_value() : std::string("door"); collides_ = true; interactable_ = true; @@ -99,7 +70,7 @@ void Door::open() { // placeholders defineixen `loop: false` explícitament al YAML, així // que aquest fallback només actua si algú dissenya malament. if (anim_.has_value() && anim_->frameCount() <= 1 && anim_->loops()) { - anim_ = AnimationComponent({0}, 6.0F, false); + anim_ = AnimationComponent::uniform({0}, 167.0F, false); } } diff --git a/source/game/entities/concrete/dust.cpp b/source/game/entities/concrete/dust.cpp index 29aedc5..5797f44 100644 --- a/source/game/entities/concrete/dust.cpp +++ b/source/game/entities/concrete/dust.cpp @@ -1,42 +1,12 @@ #include "game/entities/concrete/dust.hpp" #include -#include #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 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()); - } - if (frames.empty()) { frames.push_back(0); } - } - if (anim.contains("fps")) { fps = anim["fps"].get_value(); } - if (anim.contains("loop")) { loop = anim["loop"].get_value(); } - } - return {frames, fps, loop}; - } - -} // namespace - Dust::Dust(const fkyaml::node& yaml) - : anim_jump_(buildAnim(yaml, "jump")), - anim_land_(buildAnim(yaml, "land")) { + : anim_jump_(AnimationComponent::fromYaml(yaml, "jump")), + anim_land_(AnimationComponent::fromYaml(yaml, "land")) { name_ = yaml.contains("name") ? yaml["name"].get_value() : std::string("dust"); collides_ = false; interactable_ = false; diff --git a/source/game/entities/concrete/mummy.cpp b/source/game/entities/concrete/mummy.cpp index 91e9e67..b4faf81 100644 --- a/source/game/entities/concrete/mummy.cpp +++ b/source/game/entities/concrete/mummy.cpp @@ -29,36 +29,6 @@ namespace { 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 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()); - } - if (frames.empty()) { frames.push_back(0); } - } - if (anim.contains("fps")) { fps = anim["fps"].get_value(); } - if (anim.contains("loop")) { loop = anim["loop"].get_value(); } - if (anim.contains("flip_h")) { flip_h = anim["flip_h"].get_value(); } - if (anim.contains("flip_v")) { flip_v = anim["flip_v"].get_value(); } - } - return {frames, fps, loop, flip_h, flip_v}; - } - // 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 // per arribar exactament a la primera línia travessada. Iterem en el @@ -103,11 +73,11 @@ namespace { } // namespace Mummy::Mummy(const fkyaml::node& yaml) - : anim_walk_south_(buildAnimation(yaml, "walk_south")), - anim_walk_north_(buildAnimation(yaml, "walk_north")), - anim_walk_east_(buildAnimation(yaml, "walk_east")), - anim_walk_west_(buildAnimation(yaml, "walk_west")), - anim_raise_(buildAnimation(yaml, "raise")) { + : anim_walk_south_(AnimationComponent::fromYaml(yaml, "walk_south")), + anim_walk_north_(AnimationComponent::fromYaml(yaml, "walk_north")), + anim_walk_east_(AnimationComponent::fromYaml(yaml, "walk_east")), + anim_walk_west_(AnimationComponent::fromYaml(yaml, "walk_west")), + anim_raise_(AnimationComponent::fromYaml(yaml, "raise")) { name_ = yaml.contains("name") ? yaml["name"].get_value() : std::string("mummy"); collides_ = true; interactable_ = false; diff --git a/source/game/entities/concrete/sarcophagus.cpp b/source/game/entities/concrete/sarcophagus.cpp index 2bea0dd..2fec707 100644 --- a/source/game/entities/concrete/sarcophagus.cpp +++ b/source/game/entities/concrete/sarcophagus.cpp @@ -2,7 +2,6 @@ #include #include -#include #include "core/audio/audio.hpp" #include "core/resources/resource_cache.hpp" @@ -21,26 +20,6 @@ namespace { return k_rng_; } - auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent { - std::vector 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()); - } - if (frames.empty()) { frames.push_back(0); } - } - if (anim.contains("fps")) { fps = anim["fps"].get_value(); } - if (anim.contains("loop")) { loop = anim["loop"].get_value(); } - } - return {frames, fps, loop}; - } - // Llig `tile_size: [w, h]` del YAML. Fallback [1,1] si no existeix. void readTileSize(const fkyaml::node& yaml, int& w, int& h) { w = 1; @@ -61,9 +40,9 @@ Sarcophagus::Sarcophagus(const fkyaml::node& yaml, int anchor_col, int anchor_ro } Sarcophagus::Sarcophagus(const fkyaml::node& yaml) - : anim_closed_(buildAnimation(yaml, "closed")), - anim_opening_(buildAnimation(yaml, "opening")), - anim_open_(buildAnimation(yaml, "open")) { + : anim_closed_(AnimationComponent::fromYaml(yaml, "closed")), + anim_opening_(AnimationComponent::fromYaml(yaml, "opening")), + anim_open_(AnimationComponent::fromYaml(yaml, "open")) { name_ = yaml.contains("name") ? yaml["name"].get_value() : std::string("sarcophagus"); collides_ = true; interactable_ = true; diff --git a/source/game/entities/concrete/torch.cpp b/source/game/entities/concrete/torch.cpp index 8702cee..1fd4a54 100644 --- a/source/game/entities/concrete/torch.cpp +++ b/source/game/entities/concrete/torch.cpp @@ -1,41 +1,14 @@ #include "game/entities/concrete/torch.hpp" #include -#include #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 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()); - } - if (frames.empty()) { frames.push_back(0); } - } - if (anim.contains("fps")) { fps = anim["fps"].get_value(); } - if (anim.contains("loop")) { loop = anim["loop"].get_value(); } - } - return {frames, fps, loop}; - } - -} // namespace - Torch::Torch(const fkyaml::node& yaml) { name_ = yaml.contains("name") ? yaml["name"].get_value() : std::string("torch"); collides_ = false; interactable_ = false; - anim_ = buildIdleAnimation(yaml); + anim_ = AnimationComponent::fromYaml(yaml, "idle"); } REGISTER_ENTITY("torch", Torch) diff --git a/source/game/entities/concrete/whip_attack.cpp b/source/game/entities/concrete/whip_attack.cpp index aa03d57..e39ecc1 100644 --- a/source/game/entities/concrete/whip_attack.cpp +++ b/source/game/entities/concrete/whip_attack.cpp @@ -33,42 +33,18 @@ namespace { return sprite; } - // Construeix l'`AnimationComponent` del swing a partir del bloc - // `animations.swing`. Convenció igual que la resta d'entitats: - // - `frames` : llista d'índexs dins l'spritesheet (obligatori). - // - `frame_ms` : durada uniforme per frame (ms). Alternativament `fps`. - // - `loop` : per defecte true; al látigo el volem false (mor en - // completar). - auto buildSwingAnim(const fkyaml::node& yaml, std::vector& active_out) - -> AnimationComponent { - std::vector frames = {0}; - float fps = 8.0F; - bool loop = false; - 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()); - } - if (frames.empty()) { frames.push_back(0); } - } - if (sw.contains("frame_ms")) { - const auto MS = static_cast(sw["frame_ms"].get_value()); - if (MS > 0.0F) { fps = 1000.0F / MS; } - } else if (sw.contains("fps")) { - fps = sw["fps"].get_value(); - } - if (sw.contains("loop")) { loop = sw["loop"].get_value(); } - if (sw.contains("active_frames")) { - for (const auto& f : sw["active_frames"]) { - // cppcheck-suppress useStlAlgorithm - active_out.push_back(f.get_value()); - } - } + // Llig el camp `active_frames` del bloc `animations.swing` — específic + // del látigo: els índexs llistats són els frames on la hitbox aplica + // dany (wind-up i recovery no danyen). La resta de l'animació la + // construeix `AnimationComponent::fromYaml`. + void readActiveFrames(const fkyaml::node& yaml, std::vector& active_out) { + if (!yaml.contains("animations") || !yaml["animations"].contains("swing")) { return; } + const auto& sw = yaml["animations"]["swing"]; + if (!sw.contains("active_frames")) { return; } + for (const auto& f : sw["active_frames"]) { + // cppcheck-suppress useStlAlgorithm + active_out.push_back(f.get_value()); } - return {frames, fps, loop}; } } // namespace @@ -86,7 +62,8 @@ WhipAttack::WhipAttack(const fkyaml::node& yaml) { 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_; } diff --git a/source/game/entities/entity_manager_loadlevel.cpp b/source/game/entities/entity_manager_loadlevel.cpp index cde7a88..7563671 100644 --- a/source/game/entities/entity_manager_loadlevel.cpp +++ b/source/game/entities/entity_manager_loadlevel.cpp @@ -425,7 +425,7 @@ namespace { // els muros decoratius amb variants (stone, column, ...) l'usen. if (spawn.contains("frame")) { const int FRAME = spawn["frame"].get_value(); - entity->setAnimation(AnimationComponent({FRAME}, 1.0F, false)); + entity->setAnimation(AnimationComponent::uniform({FRAME}, 1000.0F, false)); } manager.add(std::move(entity)); diff --git a/tests/test_entities_smoke.cpp b/tests/test_entities_smoke.cpp index 7b6be0f..7d2e1ce 100644 --- a/tests/test_entities_smoke.cpp +++ b/tests/test_entities_smoke.cpp @@ -242,11 +242,12 @@ namespace { void testAnimationComponent() { 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.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); anim.tick(0.10F); @@ -257,16 +258,101 @@ namespace { REQUIRE(anim.currentFrame() == 10); // 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. REQUIRE(once.currentFrame() == 2); REQUIRE(once.completed()); // 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()); } + // 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() { std::cout << "[T7] plantilles YAML\n"; const std::vector TYPES = { @@ -697,6 +783,7 @@ auto main() -> int { testSarcophagusPerimeter(); testEntityManagerEndToEnd(); testAnimationComponent(); + testAnimationFromYaml(); testAllTemplatesLoad(); testDecorationFlags(); testLoadFromLevel();