diff --git a/data/player/morcus.yaml b/data/player/morcus.yaml index a0c54ce..6911f45 100644 --- a/data/player/morcus.yaml +++ b/data/player/morcus.yaml @@ -1,7 +1,7 @@ # player animation -tileSetFile: morcus.gif -frameWidth: 16 -frameHeight: 32 +sprite: + path: morcus.gif + size: [16, 32] # Geometria de col·lisió del jugador (independent de l'animació): # size → costat del quadrat de col·lisió (16×16) @@ -16,30 +16,29 @@ collision: # els passadissos. Centrada en el mateix punt que la caixa base. hit_size: 8 +# Animacions en mapa (clau = nom). Camps per animació: +# frames → llista d'índexs dins de l'spritesheet. +# frame_ms → durada de cada frame en ms. Escalar = uniforme; llista = +# per-frame (mateixa mida que `frames`). +# loop → true per defecte. `loop: false` = un sol tret (es queda al +# darrer frame i `animationIsCompleted()` passa a true). +# loop_from → rebota a aquest índex en passar del final (implica loop). animations: - - name: stand_down - frames: [0] + stand_down: { frames: [0] } + stand_up: { frames: [4] } + stand: { frames: [8] } - - name: stand_up - frames: [4] - - - name: stand - frames: [8] - - - name: walk_down - speed: 0.1 - loopFrom: 0 + walk_down: frames: [0, 1, 2, 3] + frame_ms: 100 - - name: walk_up - speed: 0.1 - loopFrom: 0 + walk_up: frames: [4, 5, 6, 7] + frame_ms: 100 - - name: walk - speed: 0.1 - loopFrom: 0 + walk: frames: [8, 9, 10, 11] + frame_ms: 100 # Inèrcia només de frenada. L'acceleració és sempre instantània (soltar # el teclat + prémer una direcció = velocitat plena); en canvi, quan es diff --git a/data/player/sam.yaml b/data/player/sam.yaml index 894773a..3481f3e 100644 --- a/data/player/sam.yaml +++ b/data/player/sam.yaml @@ -1,7 +1,7 @@ # player animation -tileSetFile: sam.gif -frameWidth: 16 -frameHeight: 32 +sprite: + path: sam.gif + size: [16, 32] # Geometria de col·lisió del jugador (independent de l'animació): # size → costat del quadrat de col·lisió (16×16) @@ -16,30 +16,29 @@ collision: # els passadissos. Centrada en el mateix punt que la caixa base. hit_size: 8 +# Animacions en mapa (clau = nom). Camps per animació: +# frames → llista d'índexs dins de l'spritesheet. +# frame_ms → durada de cada frame en ms. Escalar = uniforme; llista = +# per-frame (mateixa mida que `frames`). +# loop → true per defecte. `loop: false` = un sol tret (es queda al +# darrer frame i `animationIsCompleted()` passa a true). +# loop_from → rebota a aquest índex en passar del final (implica loop). animations: - - name: stand_down - frames: [1] + stand_down: { frames: [1] } + stand_up: { frames: [5] } + stand: { frames: [9] } - - name: stand_up - frames: [5] - - - name: stand - frames: [9] - - - name: walk_down - speed: 0.1 - loopFrom: 0 + walk_down: frames: [0, 1, 2, 3] + frame_ms: 100 - - name: walk_up - speed: 0.1 - loopFrom: 0 + walk_up: frames: [4, 5, 6, 7] + frame_ms: 100 - - name: walk - speed: 0.1 - loopFrom: 0 + walk: frames: [8, 9, 10, 11] + frame_ms: 100 # Inèrcia només de frenada. L'acceleració és sempre instantània (soltar # el teclat + prémer una direcció = velocitat plena); en canvi, quan es diff --git a/source/core/rendering/sprite/animated_sprite.cpp b/source/core/rendering/sprite/animated_sprite.cpp index 1b3c91e..b50684c 100644 --- a/source/core/rendering/sprite/animated_sprite.cpp +++ b/source/core/rendering/sprite/animated_sprite.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "core/rendering/surface.hpp" #include "core/resources/resource_cache.hpp" @@ -24,46 +25,6 @@ namespace { return frames; } - auto parseAnimations(const fkyaml::node& yaml, float frame_width, float frame_height, int frames_per_row, int max_tiles) -> std::vector { - std::vector animations; - if (!yaml.contains("animations") || !yaml["animations"].is_sequence()) { - return animations; - } - - for (const auto& anim_node : yaml["animations"]) { - AnimatedSprite::AnimationData animation; - - if (anim_node.contains("name")) { - animation.name = anim_node["name"].get_value(); - } - if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) { - animation.frames = convertYamlFramesToRects( - anim_node["frames"], - frame_width, - frame_height, - frames_per_row, - max_tiles); - } - if (anim_node.contains("speed")) { - const auto& speed_node = anim_node["speed"]; - if (speed_node.is_sequence()) { - for (const auto& s : speed_node) { - animation.speeds.push_back(s.get_value()); - } - } else { - auto spd = speed_node.get_value(); - if (spd > 0.0F) { animation.speeds.assign(animation.frames.size(), spd); } - } - } - if (anim_node.contains("loopFrom")) { - animation.loop_from = anim_node["loopFrom"].get_value(); - } - - animations.push_back(animation); - } - return animations; - } - struct SheetParams { float frame_width{0.0F}; float frame_height{0.0F}; @@ -71,16 +32,22 @@ namespace { int max_tiles{1}; }; - auto parseGlobalConfig(const fkyaml::node& yaml, std::shared_ptr& surface) -> SheetParams { + // 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("tileSetFile")) { - surface = Resource::Cache::get().getSurface(yaml["tileSetFile"].get_value()); + 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 (yaml.contains("frameWidth")) { - params.frame_width = static_cast(yaml["frameWidth"].get_value()); - } - if (yaml.contains("frameHeight")) { - params.frame_height = static_cast(yaml["frameHeight"].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); @@ -90,21 +57,82 @@ namespace { 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 = parseGlobalConfig(yaml, surface_); - animations_ = parseAnimations(yaml, params.frame_width, params.frame_height, params.frames_per_row, params.max_tiles); - buildNameIndex(); + auto params = parseSpriteBlock(yaml, surface_); + animations_ = parseAnimations(yaml, params); setWidth(params.frame_width); setHeight(params.frame_height); - if (!animations_.empty() && !animations_[0].frames.empty()) { - setClip(animations_[0].frames[0]); + // 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'; @@ -115,17 +143,12 @@ AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) { } } -void AnimatedSprite::buildNameIndex() { - animation_index_.clear(); - for (int i = 0; i < static_cast(animations_.size()); ++i) { - animation_index_[animations_[i].name] = i; - } -} - void AnimatedSprite::animate(float delta_time) { - if (animations_.empty()) { return; } - auto& anim = animations_[current_animation_]; - if (anim.speeds.empty()) { return; } + 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; @@ -159,26 +182,25 @@ void AnimatedSprite::animate(float delta_time) { } auto AnimatedSprite::animationIsCompleted() -> bool { - return animations_[current_animation_].completed; + 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) { - auto it = animation_index_.find(name); - if (it == animation_index_.end()) { + if (current_animation_ == name) { return; } + auto it = animations_.find(name); + if (it == animations_.end()) { std::cout << "** Warning: could not find \"" << name << "\" animation" << '\n'; return; } - setCurrentAnimation(it->second); -} - -void AnimatedSprite::setCurrentAnimation(int index) { - if (current_animation_ != index) { - current_animation_ = index; - animations_[current_animation_].current_frame = 0; - animations_[current_animation_].accumulated_time = 0.0F; - animations_[current_animation_].completed = false; - setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]); - } + 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) { @@ -187,15 +209,11 @@ void AnimatedSprite::update(float delta_time) { } void AnimatedSprite::resetAnimation() { - animations_[current_animation_].current_frame = 0; - animations_[current_animation_].accumulated_time = 0.0F; - animations_[current_animation_].completed = false; -} - -void AnimatedSprite::setCurrentAnimationFrame(int num) { - if (num < 0 || num >= static_cast(animations_[current_animation_].frames.size())) { - num = 0; - } - animations_[current_animation_].current_frame = num; - setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]); + 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; } diff --git a/source/core/rendering/sprite/animated_sprite.hpp b/source/core/rendering/sprite/animated_sprite.hpp index b05cd38..c781aaa 100644 --- a/source/core/rendering/sprite/animated_sprite.hpp +++ b/source/core/rendering/sprite/animated_sprite.hpp @@ -11,11 +11,19 @@ class AnimatedSprite : public MovingSprite { public: + // Dades d'una animació concreta dins d'un spritesheet. Les claus del + // mapa (`animations_`) són els noms; aquí només es guarden els fotogrames + // i l'estat de reproducció. struct AnimationData { - std::string name; std::vector frames; + // Durada de cada frame en segons (convertit des de `frame_ms` al YAML). + // Si té un sol valor, val per a tots els frames; altrament ha de tenir + // la mateixa mida que `frames`. std::vector speeds; - int loop_from{-1}; + // -1 = no loop (es queda al darrer frame i marca `completed`). + // 0 = loop complet des del primer frame. + // N>0 = rebota al frame N en passar del final. + int loop_from{0}; bool completed{false}; int current_frame{0}; float accumulated_time{0.0F}; @@ -28,16 +36,12 @@ class AnimatedSprite : public MovingSprite { auto animationIsCompleted() -> bool; - void setCurrentAnimation(const std::string& name = "default"); - void setCurrentAnimation(int index = 0); + void setCurrentAnimation(const std::string& name); void resetAnimation(); - void setCurrentAnimationFrame(int num); void animate(float delta_time); private: - void buildNameIndex(); - - std::vector animations_; - std::unordered_map animation_index_; - int current_animation_{0}; + std::unordered_map animations_; + // Nom de l'animació activa. Buit fins que algú crida `setCurrentAnimation`. + std::string current_animation_; };