#include "core/rendering/sprite/animated_sprite.hpp" #include #include #include #include #include "core/rendering/surface.hpp" #include "core/resources/resource_cache.hpp" #include "external/fkyaml_node.hpp" namespace { auto convertYamlFramesToRects(const fkyaml::node& frames_node, float frame_width, float frame_height, int frames_per_row, int max_tiles) -> std::vector { std::vector frames; SDL_FRect rect = {.x = 0.0F, .y = 0.0F, .w = frame_width, .h = frame_height}; for (const auto& frame_index_node : frames_node) { const int NUM_TILE = frame_index_node.get_value(); if (NUM_TILE <= max_tiles) { rect.x = static_cast(NUM_TILE % frames_per_row) * frame_width; rect.y = static_cast(NUM_TILE / frames_per_row) * frame_height; frames.emplace_back(rect); } } return frames; } struct SheetParams { float frame_width{0.0F}; float frame_height{0.0F}; int frames_per_row{1}; int max_tiles{1}; }; // Llegeix el bloc `sprite: { path, size: [w, h] }` — mateix format que // les plantilles d'entitat. La surface es resol via Resource::Cache amb // el basename del fitxer. auto parseSpriteBlock(const fkyaml::node& yaml, std::shared_ptr& surface) -> SheetParams { SheetParams params; if (!yaml.contains("sprite")) { return params; } const auto& s = yaml["sprite"]; if (s.contains("path")) { surface = Resource::Cache::get().getSurface(s["path"].get_value()); } if (s.contains("size")) { const auto& sz = s["size"]; if (sz.is_sequence() && sz.size() == 2) { params.frame_width = static_cast(sz[0].get_value()); params.frame_height = static_cast(sz[1].get_value()); } } if (surface && params.frame_width > 0.0F && params.frame_height > 0.0F) { params.frames_per_row = surface->getWidth() / static_cast(params.frame_width); const int H_TILES = surface->getHeight() / static_cast(params.frame_height); params.max_tiles = params.frames_per_row * H_TILES; } return params; } // `frame_ms` pot ser un escalar (temps uniforme) o una llista (per-frame). // Convertim sempre a segons perquè `animate()` treballa amb delta_time // en segons. Si falta o val 0, `speeds` queda buit i l'animació es // considera estàtica (no avança). void parseFrameTimes(const fkyaml::node& anim_node, AnimatedSprite::AnimationData& animation) { if (!anim_node.contains("frame_ms")) { return; } const auto& fm = anim_node["frame_ms"]; if (fm.is_sequence()) { for (const auto& v : fm) { animation.speeds.push_back(static_cast(v.get_value()) / 1000.0F); } } else { const float SEC = static_cast(fm.get_value()) / 1000.0F; if (SEC > 0.0F) { animation.speeds.assign(animation.frames.size(), SEC); } } } // Ordre de prioritat per a la política de loop: // 1) `loop_from: N` → rebota al frame N en passar del final (implica loop). // 2) `loop: false` → animació d'un sol tret (es queda al darrer frame). // 3) Res → loop complet des del frame 0 (default). void parseLoopPolicy(const fkyaml::node& anim_node, AnimatedSprite::AnimationData& animation) { if (anim_node.contains("loop_from")) { animation.loop_from = anim_node["loop_from"].get_value(); } else if (anim_node.contains("loop") && !anim_node["loop"].get_value()) { animation.loop_from = -1; } else { animation.loop_from = 0; } } auto parseAnimations(const fkyaml::node& yaml, const SheetParams& params) -> std::unordered_map { std::unordered_map animations; if (!yaml.contains("animations") || !yaml["animations"].is_mapping()) { return animations; } const auto& anims_node = yaml["animations"]; for (auto it = anims_node.begin(); it != anims_node.end(); ++it) { const auto NAME = it.key().get_value(); const auto& anim_node = *it; AnimatedSprite::AnimationData animation; if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) { animation.frames = convertYamlFramesToRects( anim_node["frames"], params.frame_width, params.frame_height, params.frames_per_row, params.max_tiles); } parseFrameTimes(anim_node, animation); parseLoopPolicy(anim_node, animation); animations.emplace(NAME, std::move(animation)); } return animations; } } // namespace AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) { std::string yaml_content(cached_data.yaml_data.begin(), cached_data.yaml_data.end()); try { auto yaml = fkyaml::node::deserialize(yaml_content); auto params = parseSpriteBlock(yaml, surface_); animations_ = parseAnimations(yaml, params); setWidth(params.frame_width); setHeight(params.frame_height); // Clip inicial: qualsevol frame del primer element del mapa serveix // com a placeholder. El consumidor sempre crida `setCurrentAnimation` // abans del primer `update()`, així que el valor real el posa ell. if (!animations_.empty()) { const auto& first = animations_.begin()->second; if (!first.frames.empty()) { setClip(first.frames[0]); } } } catch (const fkyaml::exception& e) { std::cerr << "YAML parsing error in animation " << cached_data.name << ": " << e.what() << '\n'; throw; } catch (const std::exception& e) { std::cerr << "Error loading animation " << cached_data.name << ": " << e.what() << '\n'; throw; } } void AnimatedSprite::animate(float delta_time) { if (current_animation_.empty()) { return; } auto it = animations_.find(current_animation_); if (it == animations_.end()) { return; } auto& anim = it->second; if (anim.speeds.empty() || anim.frames.empty()) { return; } anim.accumulated_time += delta_time; const float TOTAL = std::accumulate(anim.speeds.begin(), anim.speeds.end(), 0.0F); if (anim.accumulated_time >= TOTAL) { if (anim.loop_from < 0) { anim.current_frame = static_cast(anim.frames.size()) - 1; anim.completed = true; setClip(anim.frames[anim.current_frame]); return; } float loop_start = 0.0F; for (int i = 0; i < anim.loop_from; ++i) { loop_start += anim.speeds[i]; } float loop_len = TOTAL - loop_start; anim.accumulated_time = loop_start + std::fmod(anim.accumulated_time - loop_start, loop_len); } float cursor = 0.0F; for (int i = 0; i < static_cast(anim.frames.size()); ++i) { cursor += anim.speeds[i]; if (anim.accumulated_time < cursor) { anim.current_frame = i; setClip(anim.frames[i]); return; } } anim.current_frame = static_cast(anim.frames.size()) - 1; setClip(anim.frames[anim.current_frame]); } auto AnimatedSprite::animationIsCompleted() -> bool { if (current_animation_.empty()) { return false; } auto it = animations_.find(current_animation_); if (it == animations_.end()) { return false; } return it->second.completed; } void AnimatedSprite::setCurrentAnimation(const std::string& name) { if (current_animation_ == name) { return; } auto it = animations_.find(name); if (it == animations_.end()) { std::cout << "** Warning: could not find \"" << name << "\" animation" << '\n'; return; } current_animation_ = name; auto& anim = it->second; anim.current_frame = 0; anim.accumulated_time = 0.0F; anim.completed = false; if (!anim.frames.empty()) { setClip(anim.frames[0]); } } void AnimatedSprite::update(float delta_time) { animate(delta_time); MovingSprite::update(delta_time); } void AnimatedSprite::resetAnimation() { if (current_animation_.empty()) { return; } auto it = animations_.find(current_animation_); if (it == animations_.end()) { return; } auto& anim = it->second; anim.current_frame = 0; anim.accumulated_time = 0.0F; anim.completed = false; }