#include "game/entities/animation_component.hpp" #include #include #include namespace { // 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)), speeds_(fitSpeeds(std::move(speeds), frames_.size())), loop_(loop), flip_h_(flip_h), flip_v_(flip_v) { // Una animació d'1 sol frame amb loop=false és una animació que ja s'ha // acabat de reproduir: no hi ha cap transició per mostrar. Marcant-la // com a completada des de l'inici permet als consumidors (p.ex. Door) // usar `completed()` com a temporitzador de transició d'estat. 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; while (accumulator_ >= speeds_[static_cast(current_)]) { accumulator_ -= speeds_[static_cast(current_)]; ++current_; if (static_cast(current_) >= frames_.size()) { if (loop_) { current_ = 0; } else { current_ = static_cast(frames_.size()) - 1; completed_ = true; return; } } } } void AnimationComponent::reset() { current_ = 0; accumulator_ = 0.0F; // Invariant: animacions d'1 frame no-loop neixen completades i tornen // a aquest estat després de reset (no hi ha progressió que desfer). completed_ = (frames_.size() <= 1 && !loop_); } auto AnimationComponent::currentFrame() const -> int { if (frames_.empty()) { return 0; } return frames_[static_cast(current_)]; } auto AnimationComponent::flipMode() const -> SDL_FlipMode { if (flip_h_) { return SDL_FLIP_HORIZONTAL; } if (flip_v_) { return SDL_FLIP_VERTICAL; } return SDL_FLIP_NONE; }