#include "core/rendering/sprite/animated_sprite.hpp" #include #include #include #include "core/rendering/surface.hpp" #include "core/resources/resource_cache.hpp" #include "utils/log.hpp" // El parser pure de YAML d'animacions (parseAnimationsFromYaml + helpers) // viu a `animated_sprite_parser.cpp` perquè és testable sense Surface ni // Resource::Cache. Aquesta unitat es queda amb el runtime de la classe i // amb el que sí toca el cache de surfaces (parseSpriteBlock). namespace { // 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) -> AnimatedSprite::SheetParams { AnimatedSprite::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; } // Tradueix els flags lògics flip_h/flip_v al `SDL_FlipMode` que admet // `Surface::render`. Important: a SDL3, `SDL_FlipMode` és un enum // exclusiu (NONE/HORIZONTAL/VERTICAL), no un bitmask combinable. Per // tant, V+H simultani al mateix frame no es pot expressar amb una // única crida — prioritzem H (suficient per als casos pràctics // d'animació, com `walk_west` o `death_spin`). Si en algun moment fa // falta combinar, caldrà ampliar `Surface::render` per a doble-flipar // a baix nivell. auto toFlipMode(bool flip_h, bool flip_v) -> SDL_FlipMode { if (flip_h) { return SDL_FLIP_HORIZONTAL; } if (flip_v) { return SDL_FLIP_VERTICAL; } return SDL_FLIP_NONE; } } // 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_ = parseAnimationsFromYaml(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].rect); } } } catch (const fkyaml::exception& e) { Log::error("AnimatedSprite", "YAML parsing error in animation {}: {}", cached_data.name, e.what()); throw; } catch (const std::exception& e) { Log::error("AnimatedSprite", "Error loading animation {}: {}", cached_data.name, e.what()); 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); // Helper local per a propagar el frame actual (clip + flip) al sprite. // El motor sempre sobreescriu el flip — si el YAML no declara `flip_h`/ // `flip_v`, el frame té els flags a false i el resultat és `SDL_FLIP_NONE`. auto apply_current_frame = [&](int frame_index) { const auto& frame = anim.frames[frame_index]; setClip(frame.rect); setFlip(toFlipMode(frame.flip_h, frame.flip_v)); }; if (anim.accumulated_time >= TOTAL) { if (anim.loop_from < 0) { anim.current_frame = static_cast(anim.frames.size()) - 1; anim.completed = true; apply_current_frame(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; apply_current_frame(i); return; } } anim.current_frame = static_cast(anim.frames.size()) - 1; apply_current_frame(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()) { Log::warn("AnimatedSprite", "could not find \"{}\" animation", name); 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].rect); setFlip(toFlipMode(anim.frames[0].flip_h, anim.frames[0].flip_v)); } } 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; }