171 lines
6.6 KiB
C++
171 lines
6.6 KiB
C++
#include "core/rendering/sprite/animated_sprite.hpp"
|
|
|
|
#include <cmath>
|
|
#include <iostream>
|
|
#include <numeric>
|
|
#include <utility>
|
|
|
|
#include "core/rendering/surface.hpp"
|
|
#include "core/resources/resource_cache.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>& 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<std::string>());
|
|
}
|
|
if (s.contains("size")) {
|
|
const auto& sz = s["size"];
|
|
if (sz.is_sequence() && sz.size() == 2) {
|
|
params.frame_width = static_cast<float>(sz[0].get_value<int>());
|
|
params.frame_height = static_cast<float>(sz[1].get_value<int>());
|
|
}
|
|
}
|
|
if (surface && params.frame_width > 0.0F && params.frame_height > 0.0F) {
|
|
params.frames_per_row = surface->getWidth() / static_cast<int>(params.frame_width);
|
|
const int H_TILES = surface->getHeight() / static_cast<int>(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) {
|
|
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);
|
|
|
|
// 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<int>(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<int>(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<int>(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()) {
|
|
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].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;
|
|
}
|