220 lines
8.7 KiB
C++
220 lines
8.7 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"
|
|
#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<SDL_FRect> {
|
|
std::vector<SDL_FRect> 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<int>();
|
|
if (NUM_TILE <= max_tiles) {
|
|
rect.x = static_cast<float>(NUM_TILE % frames_per_row) * frame_width;
|
|
rect.y = static_cast<float>(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>& 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<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;
|
|
}
|
|
|
|
// `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<float>(v.get_value<int>()) / 1000.0F);
|
|
}
|
|
} else {
|
|
const float SEC = static_cast<float>(fm.get_value<int>()) / 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<int>();
|
|
} else if (anim_node.contains("loop") && !anim_node["loop"].get_value<bool>()) {
|
|
animation.loop_from = -1;
|
|
} else {
|
|
animation.loop_from = 0;
|
|
}
|
|
}
|
|
|
|
auto parseAnimations(const fkyaml::node& yaml, const SheetParams& params) -> std::unordered_map<std::string, AnimatedSprite::AnimationData> {
|
|
std::unordered_map<std::string, AnimatedSprite::AnimationData> 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<std::string>();
|
|
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<int>(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<int>(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<int>(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;
|
|
}
|