167 lines
7.0 KiB
C++
167 lines
7.0 KiB
C++
#include "game/entities/animation_component.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <iterator>
|
|
#include <stdexcept>
|
|
#include <utility>
|
|
|
|
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<float> speeds, std::size_t frame_count) -> std::vector<float> {
|
|
if (frame_count == 0) { return {}; }
|
|
if (speeds.empty()) {
|
|
return std::vector<float>(frame_count, K_DEFAULT_FRAME_SECONDS);
|
|
}
|
|
std::ranges::transform(speeds, speeds.begin(), sanitizeSpeed);
|
|
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.<key>.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<int> {
|
|
std::vector<int> frames;
|
|
std::ranges::transform(anim["frames"], std::back_inserter(frames), [](const fkyaml::node& f) { return f.template get_value<int>(); });
|
|
if (frames.empty()) { frames.push_back(0); }
|
|
return frames;
|
|
}
|
|
|
|
// Llig `animations.<key>.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<float> {
|
|
std::vector<float> speeds_secs;
|
|
const auto& fm = anim["frame_ms"];
|
|
if (fm.is_sequence()) {
|
|
std::ranges::transform(fm, std::back_inserter(speeds_secs), [](const fkyaml::node& v) { return static_cast<float>(v.template get_value<int>()) / 1000.0F; });
|
|
} else {
|
|
const float SECS = static_cast<float>(fm.get_value<int>()) / 1000.0F;
|
|
speeds_secs.assign(frame_count, SECS);
|
|
}
|
|
return speeds_secs;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
AnimationComponent::AnimationComponent(std::vector<int> frames, std::vector<float> 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<int> frames, float frame_ms, bool loop) -> AnimationComponent {
|
|
return uniform(std::move(frames), frame_ms, loop, false, false);
|
|
}
|
|
|
|
auto AnimationComponent::uniform(std::vector<int> frames, float frame_ms, bool loop, bool flip_h, bool flip_v) -> AnimationComponent {
|
|
const float SECS = sanitizeSpeed(frame_ms / 1000.0F);
|
|
std::vector<float> 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.<key>` no
|
|
// existeix: 1 frame loopant a 1s — visualment estàtic, semàntica neutra.
|
|
std::vector<int> frames = {0};
|
|
std::vector<float> 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<bool>(); }
|
|
if (anim.contains("flip_h")) { flip_h = anim["flip_h"].get_value<bool>(); }
|
|
if (anim.contains("flip_v")) { flip_v = anim["flip_v"].get_value<bool>(); }
|
|
|
|
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<std::size_t>(current_)]) {
|
|
accumulator_ -= speeds_[static_cast<std::size_t>(current_)];
|
|
++current_;
|
|
if (static_cast<std::size_t>(current_) >= frames_.size()) {
|
|
if (loop_) {
|
|
current_ = 0;
|
|
} else {
|
|
current_ = static_cast<int>(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<std::size_t>(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;
|
|
}
|