diff --git a/data/entities/mummy/mummy.yaml b/data/entities/mummy/mummy.yaml index f2c68af..9c00d8e 100644 --- a/data/entities/mummy/mummy.yaml +++ b/data/entities/mummy/mummy.yaml @@ -25,12 +25,16 @@ flags: # Strip de 12 frames en graella 4×3 (mateix esquema que el jugador): # 0..3 → walk_south (cara avall) # 4..7 → walk_north (cara amunt) -# 8..11 → walk_east (lateral; flip horitzontal segons el sentit) +# 8..11 → walk_east (lateral cap a l'est) +# La variant `walk_west` reaprofita els frames de l'est amb `flip_h: true`: +# el motor d'animació aplica el flip al sprite, així el codi de la mòmia +# no decideix flip per direcció (només selecciona quina animació mostrar). # La momia no té variants `stand_*` perquè mai no para. animations: walk_south: { frames: [0, 1, 2, 3], fps: 8, loop: true } walk_north: { frames: [4, 5, 6, 7], fps: 8, loop: true } walk_east: { frames: [8, 9, 10, 11], fps: 8, loop: true } + walk_west: { frames: [8, 9, 10, 11], fps: 8, loop: true, flip_h: true } ai: type: corridor_wanderer direction_choice: random diff --git a/data/music/playing.ogg b/data/music/playing.ogg index 5808941..a4d8ca0 100644 Binary files a/data/music/playing.ogg and b/data/music/playing.ogg differ diff --git a/data/player/morcus.yaml b/data/player/morcus.yaml index 58e16a4..6ae578d 100644 --- a/data/player/morcus.yaml +++ b/data/player/morcus.yaml @@ -12,33 +12,43 @@ collision: height: 12 animations: + # Les variants `*_west` reaprofiten els frames de l'est i declaren + # `flip_h: true` perquè el motor d'animació apliqui el flip al sprite — + # el codi del joc no toca mai `flip_` directament. stand_south: { frames: [0] } stand_north: { frames: [4] } stand_east: { frames: [8] } + stand_west: { frames: [8], flip_h: true } jump_south: { frames: [12] } jump_north: { frames: [13] } jump_east: { frames: [14] } + jump_west: { frames: [14], flip_h: true } stun_south: { frames: [16] } stun_north: { frames: [17] } stun_east: { frames: [18] } + stun_west: { frames: [18], flip_h: true } walk_south: { frames: [0, 1, 2, 3], frame_ms: 100 } walk_north: { frames: [4, 5, 6, 7], frame_ms: 100 } walk_east: { frames: [8, 9, 10, 11], frame_ms: 100 } + walk_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true } hurt_south: { frames: [0, 1, 2, 3], frame_ms: 100 } hurt_north: { frames: [4, 5, 6, 7], frame_ms: 100 } hurt_east: { frames: [8, 9, 10, 11], frame_ms: 100 } + hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true } attack_south: { frames: [20, 21, 22, 23], frame_ms: 50 } attack_north: { frames: [24, 25, 26, 27], frame_ms: 50 } attack_east: { frames: [28, 29, 30, 31], frame_ms: 50 } + attack_west: { frames: [28, 29, 30, 31], frame_ms: 50, flip_h: true } recover_south: { frames: [23] } recover_north: { frames: [27] } recover_east: { frames: [31] } + recover_west: { frames: [31], flip_h: true } # Inèrcia de frenada en soltar la direcció. `threshold_ms` filtra # tocs curts (passets d'un tile sense rellisquera). diff --git a/data/player/sam.yaml b/data/player/sam.yaml index a015539..f008835 100644 --- a/data/player/sam.yaml +++ b/data/player/sam.yaml @@ -12,33 +12,43 @@ collision: height: 12 animations: + # Les variants `*_west` reaprofiten els frames de l'est i declaren + # `flip_h: true` perquè el motor d'animació apliqui el flip al sprite — + # el codi del joc no toca mai `flip_` directament. stand_south: { frames: [0] } stand_north: { frames: [4] } stand_east: { frames: [8] } + stand_west: { frames: [8], flip_h: true } jump_south: { frames: [12] } jump_north: { frames: [13] } jump_east: { frames: [14] } + jump_west: { frames: [14], flip_h: true } stun_south: { frames: [16] } stun_north: { frames: [17] } stun_east: { frames: [18] } + stun_west: { frames: [18], flip_h: true } walk_south: { frames: [0, 1, 2, 3], frame_ms: 100 } walk_north: { frames: [4, 5, 6, 7], frame_ms: 100 } walk_east: { frames: [8, 9, 10, 11], frame_ms: 100 } + walk_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true } hurt_south: { frames: [0, 1, 2, 3], frame_ms: 100 } hurt_north: { frames: [4, 5, 6, 7], frame_ms: 100 } hurt_east: { frames: [8, 9, 10, 11], frame_ms: 100 } + hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true } attack_south: { frames: [20, 21, 22, 23], frame_ms: 50 } attack_north: { frames: [24, 25, 26, 27], frame_ms: 50 } attack_east: { frames: [28, 29, 30, 31], frame_ms: 50 } + attack_west: { frames: [28, 29, 30, 31], frame_ms: 50, flip_h: true } recover_south: { frames: [23] } recover_north: { frames: [27] } recover_east: { frames: [31] } + recover_west: { frames: [31], flip_h: true } # Inèrcia de frenada en soltar la direcció. `threshold_ms` filtra # tocs curts (passets d'un tile sense rellisquera). diff --git a/source/core/rendering/sprite/animated_sprite.cpp b/source/core/rendering/sprite/animated_sprite.cpp index 25b07f1..4ab8aa1 100644 --- a/source/core/rendering/sprite/animated_sprite.cpp +++ b/source/core/rendering/sprite/animated_sprite.cpp @@ -93,14 +93,13 @@ void AnimatedSprite::animate(float delta_time) { const float TOTAL = std::accumulate(anim.speeds.begin(), anim.speeds.end(), 0.0F); - // Helper local per a propagar el frame actual (clip + opcionalment flip) - // al sprite. El flip només s'escriu si l'animació declara `controls_flip`, - // per a no trepitjar el flip que el codi del joc encara controla - // externament en animacions sense declaració de flip al YAML. + // 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); - if (anim.controls_flip) { setFlip(toFlipMode(frame.flip_h, frame.flip_v)); } + setFlip(toFlipMode(frame.flip_h, frame.flip_v)); }; if (anim.accumulated_time >= TOTAL) { @@ -151,7 +150,7 @@ void AnimatedSprite::setCurrentAnimation(const std::string& name) { anim.completed = false; if (!anim.frames.empty()) { setClip(anim.frames[0].rect); - if (anim.controls_flip) { setFlip(toFlipMode(anim.frames[0].flip_h, anim.frames[0].flip_v)); } + setFlip(toFlipMode(anim.frames[0].flip_h, anim.frames[0].flip_v)); } } diff --git a/source/core/rendering/sprite/animated_sprite.hpp b/source/core/rendering/sprite/animated_sprite.hpp index a80969c..f11af9a 100644 --- a/source/core/rendering/sprite/animated_sprite.hpp +++ b/source/core/rendering/sprite/animated_sprite.hpp @@ -34,6 +34,13 @@ class AnimatedSprite : public MovingSprite { // 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ó. + // + // El flip és sempre propietat del motor: a cada `animate()` s'escriu el + // flip del frame actual al sprite (default `SDL_FLIP_NONE` quan el YAML + // no declara res). El codi del joc no pot — ni ha de — tocar el flip + // manualment; ho expressa al YAML declarant `flip_h: true` a l'animació + // simètrica (p. ex. `walk_west`) o, per a casos puntuals, per frame + // amb la forma mapping `{ index, flip_h?, flip_v? }`. struct AnimationData { std::vector frames; // Durada de cada frame en segons (convertit des de `frame_ms` al YAML). @@ -44,13 +51,6 @@ class AnimatedSprite : public MovingSprite { // 0 = loop complet des del primer frame. // N>0 = rebota al frame N en passar del final. int loop_from{0}; - // Si true, `animate()` escriu el flip del frame actual al sprite. - // Si false (cas backward-compat per a animacions sense cap declaració - // de flip), `animate()` no toca el flip — el codi del joc el segueix - // controlant externament via `setFlip(...)`. Es marca a true quan - // l'animació declara `flip_h`/`flip_v`, o quan algun frame es declara - // amb la forma mapping `{ index, flip_h?, flip_v? }`. - bool controls_flip{false}; bool completed{false}; int current_frame{0}; float accumulated_time{0.0F}; diff --git a/source/core/rendering/sprite/animated_sprite_parser.cpp b/source/core/rendering/sprite/animated_sprite_parser.cpp index 3c19721..9604850 100644 --- a/source/core/rendering/sprite/animated_sprite_parser.cpp +++ b/source/core/rendering/sprite/animated_sprite_parser.cpp @@ -1,6 +1,3 @@ -#include -#include - #include "core/rendering/sprite/animated_sprite.hpp" // Implementació del parser pure de YAML d'animacions. Viu en una unitat @@ -53,16 +50,6 @@ namespace { return frames; } - // Detecta si dins de `frames_node` hi ha algun element en forma mapping. - // Si n'hi ha (encara que els flags interns siguin false), l'animació - // passa a controlar el flip — el mapping és senyal explícit que l'autor - // del YAML vol governar el flip per frame. - auto framesContainMapping(const fkyaml::node& frames_node) -> bool { - return std::ranges::any_of(frames_node, [](const auto& frame_node) { - return frame_node.is_mapping(); - }); - } - // `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 @@ -116,14 +103,7 @@ auto AnimatedSprite::parseAnimationsFromYaml(const fkyaml::node& yaml, const She AnimationData animation; if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) { - const auto& frames_node = anim_node["frames"]; - animation.frames = convertYamlFrames(frames_node, params, anim_flip_h, anim_flip_v); - // L'animació passa a "controlar" el flip si l'autor ha - // declarat qualsevol cosa relacionada amb flip — sigui a - // nivell d'anim o a través d'un frame en forma mapping. - // Sense cap declaració, ens mantenim passius i deixem el - // flip al codi del joc (backward-compat amb sam.yaml). - animation.controls_flip = anim_flip_h || anim_flip_v || framesContainMapping(frames_node); + animation.frames = convertYamlFrames(anim_node["frames"], params, anim_flip_h, anim_flip_v); } parseFrameTimes(anim_node, animation); parseLoopPolicy(anim_node, animation); diff --git a/source/game/entities/animation_component.cpp b/source/game/entities/animation_component.cpp index 65ba3a2..e63b62f 100644 --- a/source/game/entities/animation_component.cpp +++ b/source/game/entities/animation_component.cpp @@ -4,9 +4,14 @@ #include AnimationComponent::AnimationComponent(std::vector frames, float fps, bool loop) + : AnimationComponent(std::move(frames), fps, loop, false, false) {} + +AnimationComponent::AnimationComponent(std::vector frames, float fps, bool loop, bool flip_h, bool flip_v) : frames_(std::move(frames)), fps_(fps > 0.0F ? fps : 1.0F), - loop_(loop) { + 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) @@ -46,3 +51,9 @@ auto AnimationComponent::currentFrame() const -> int { if (frames_.empty()) { return 0; } return frames_[static_cast(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; +} diff --git a/source/game/entities/animation_component.hpp b/source/game/entities/animation_component.hpp index 11f851f..1343eef 100644 --- a/source/game/entities/animation_component.hpp +++ b/source/game/entities/animation_component.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -13,10 +15,18 @@ // algun dia aterri l'art definitiu, el renderer consultarà `currentFrame()` // i traduirà l'índex al rectangle del sprite corresponent. De moment els // placeholders tenen un únic frame i `currentFrame()` retorna sempre 0. +// +// El flip (H/V) és propietat de l'animació, no del codi del joc: el YAML +// declara `flip_h`/`flip_v` al bloc d'animació i el component els exposa +// via `flipMode()`. Les entitats apliquen aquest valor al seu `flip_` quan +// canvien d'animació, sense decidir-lo per direcció. Així una mòmia que va +// cap a l'oest reprodueix `walk_west` (frames de l'est + `flip_h: true`) +// sense que el codi de la mòmia toqui flip directament. class AnimationComponent { public: AnimationComponent() = default; AnimationComponent(std::vector frames, float fps, bool loop); + AnimationComponent(std::vector frames, float fps, bool loop, bool flip_h, bool flip_v); // Avanç del temps. Si `frames_` està buit o té un sol element, no fa // res. En mode loop, `current_frame_` rebota a 0 en passar del final; @@ -37,10 +47,19 @@ class AnimationComponent { [[nodiscard]] auto completed() const -> bool { return completed_; } [[nodiscard]] auto loops() const -> bool { return loop_; } + [[nodiscard]] auto flipH() const -> bool { return flip_h_; } + [[nodiscard]] auto flipV() const -> bool { return flip_v_; } + // Combinació dels dos flags al `SDL_FlipMode` que entén el render. + // SDL3 no admet bitmask: si fos VH simultani, prioritzem H (cap cas + // pràctic actual el necessita combinat). + [[nodiscard]] auto flipMode() const -> SDL_FlipMode; + private: std::vector frames_; float fps_{1.0F}; bool loop_{true}; + bool flip_h_{false}; + bool flip_v_{false}; int current_{0}; float accumulator_{0.0F}; bool completed_{false}; diff --git a/source/game/entities/concrete/mummy.cpp b/source/game/entities/concrete/mummy.cpp index 788e811..9b1cc0c 100644 --- a/source/game/entities/concrete/mummy.cpp +++ b/source/game/entities/concrete/mummy.cpp @@ -26,11 +26,15 @@ namespace { // Llegeix un bloc d'animació del YAML amb fallbacks tolerants (mateix // patró que Door/Sarcophagus). Si la clau no apareix, retorna una // animació d'1 frame loopant, neutra — la momia es podrà moure però - // visualment es quedarà estàtica. + // visualment es quedarà estàtica. Llegeix `flip_h`/`flip_v` (default + // false) i els passa al component perquè el motor sàpiga aplicar-los + // al sprite quan aquesta animació estigui activa. auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent { std::vector frames = {0}; float fps = 8.0F; bool loop = true; + bool flip_h = false; + bool flip_v = false; if (yaml.contains("animations") && yaml["animations"].contains(key)) { const auto& anim = yaml["animations"][key]; if (anim.contains("frames")) { @@ -43,8 +47,10 @@ namespace { } if (anim.contains("fps")) { fps = anim["fps"].get_value(); } if (anim.contains("loop")) { loop = anim["loop"].get_value(); } + if (anim.contains("flip_h")) { flip_h = anim["flip_h"].get_value(); } + if (anim.contains("flip_v")) { flip_v = anim["flip_v"].get_value(); } } - return {frames, fps, loop}; + return {frames, fps, loop, flip_h, flip_v}; } // Busca si el segment [prev, prev+delta] travessa una lane del patró @@ -75,7 +81,8 @@ namespace { Mummy::Mummy(const fkyaml::node& yaml) : anim_walk_south_(buildAnimation(yaml, "walk_south")), anim_walk_north_(buildAnimation(yaml, "walk_north")), - anim_walk_east_(buildAnimation(yaml, "walk_east")) { + anim_walk_east_(buildAnimation(yaml, "walk_east")), + anim_walk_west_(buildAnimation(yaml, "walk_west")) { name_ = yaml.contains("name") ? yaml["name"].get_value() : std::string("mummy"); collides_ = true; interactable_ = false; @@ -125,22 +132,26 @@ Mummy::Mummy(const fkyaml::node& yaml) void Mummy::changeDirection(float dx, float dy) { setDirection(dx, dy); - // Prevalença lateral: si hi ha component horitzontal s'usa l'anim - // est + flip segons el signe. Només direccions purament verticals - // disparen walk_north/walk_south. Quan la direcció queda (0,0) - // (cul-de-sac amb NONE) no toquem l'animació: la momia es quedarà al - // frame actual fins que step() li torni a assignar direcció. - if (dx != 0.0F) { + // Una animació per cada cardinal — el flip viu al YAML + // (`walk_west: flip_h: true`) i el codi només selecciona quina + // animació mostrar. Prevalença lateral en diagonals; quan la direcció + // queda (0,0) (cul-de-sac amb NONE) no toquem l'animació: la mòmia es + // quedarà al frame actual fins que step() li torni a assignar direcció. + if (dx > 0.0F) { anim_ = anim_walk_east_; - flip_ = (dx < 0.0F) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE; + } else if (dx < 0.0F) { + anim_ = anim_walk_west_; } else if (dy < 0.0F) { anim_ = anim_walk_north_; - flip_ = SDL_FLIP_NONE; } else if (dy > 0.0F) { anim_ = anim_walk_south_; - flip_ = SDL_FLIP_NONE; } - if (anim_.has_value()) { anim_->reset(); } + if (anim_.has_value()) { + anim_->reset(); + // Propaguem el flip que l'animació declara al YAML al camp + // `flip_` que el render llegeix (entity_manager_loadlevel.cpp). + flip_ = anim_->flipMode(); + } } auto Mummy::chooseDirection(const std::vector& allowed) -> Direction { diff --git a/source/game/entities/concrete/mummy.hpp b/source/game/entities/concrete/mummy.hpp index 1161d49..c99e0c7 100644 --- a/source/game/entities/concrete/mummy.hpp +++ b/source/game/entities/concrete/mummy.hpp @@ -91,10 +91,11 @@ class Mummy : public DynamicHazard { }; [[nodiscard]] auto collectDirectionPools(float step) const -> DirectionPools; - // Canvi de direcció + aplicació de l'animació i flip corresponents. - // Tots els camins que trien nova direcció (ctor amb `initial_direction`, - // gir després d'un bloqueig) passen per ací per mantenir anim_ i - // flip_ en sincronia amb (dir_x_, dir_y_). + // Canvi de direcció + selecció de l'animació corresponent (un cardinal + // per branch). Tots els camins que trien nova direcció (ctor amb + // `initial_direction`, gir després d'un bloqueig) passen per ací; el + // flip viu al YAML (`walk_west: flip_h: true`) i es propaga des de + // l'animació al `flip_` heretat. void changeDirection(float dx, float dy); // Aplica la direcció triada (wrapper del switch en una sola crida). @@ -128,11 +129,14 @@ class Mummy : public DynamicHazard { // totes ho estan, retorna `NONE` (la mòmia queda quieta en HURT). [[nodiscard]] auto chooseHurtDirection(Direction preferred) const -> Direction; - // Animacions carregades del YAML, una per cada facing. Per a horitzontal - // només en cal una (`walk_east`) i el flip ja diferencia esquerra/dreta. + // Animacions carregades del YAML, una per cada cardinal. La variant + // `walk_west` reaprofita els frames de l'est però amb `flip_h: true` + // declarat al YAML — el motor d'animació aplica el flip al sprite + // automàticament; aquesta classe no toca mai `flip_` directament. AnimationComponent anim_walk_south_; AnimationComponent anim_walk_north_; AnimationComponent anim_walk_east_; + AnimationComponent anim_walk_west_; // Estat HURT i la seua configuració. `hurt_speed_` és l'efectiva // (resultat d'aplicar el `speed` del bloc `hurt:` o, si val 0, la diff --git a/source/game/entities/entity.hpp b/source/game/entities/entity.hpp index f7c7709..fb44080 100644 --- a/source/game/entities/entity.hpp +++ b/source/game/entities/entity.hpp @@ -155,10 +155,11 @@ class Entity { // el sprite) amb el tile. Nullopt si l'entitat no en té. [[nodiscard]] auto getCollisionBox() const -> const std::optional& { return collision_box_; } - // Flip del sprite. Les entitats estàtiques mai no el toquen (queda a - // SDL_FLIP_NONE); les dinàmiques que miren cap als costats (p. ex. - // Mummy en direcció horitzontal) el canvien quan gira. El manager - // el passa a Surface::render. + // Flip del sprite, propagat des del motor d'animació + // (`AnimationComponent::flipMode()`): cap entitat el decideix per + // direcció — el YAML declara `flip_h: true` a l'animació simètrica + // (p. ex. `walk_west`) i el setter d'animació l'aplica al `flip_`. + // El manager el passa a `Surface::render`. [[nodiscard]] auto getFlip() const -> SDL_FlipMode { return flip_; } void setFlip(SDL_FlipMode flip) { flip_ = flip; } diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 47086bf..01e8861 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -203,22 +203,21 @@ void Player::readWhipBlock(const fkyaml::node& yaml) { } void Player::applyCardinal(Cardinal c) { + // El flip del sprite el governa l'animació al motor: només cal posar + // el `facing_` que tocaria; `animationName()` ja resol el sufix + // (`_west` per Cardinal::WEST) i el YAML hi declara `flip_h: true`. switch (c) { case Cardinal::NORTH: facing_ = Facing::NORTH; - setFlip(SDL_FLIP_NONE); break; case Cardinal::SOUTH: facing_ = Facing::SOUTH; - setFlip(SDL_FLIP_NONE); break; case Cardinal::EAST: facing_ = Facing::EAST; - setFlip(SDL_FLIP_NONE); break; case Cardinal::WEST: - facing_ = Facing::EAST; - setFlip(SDL_FLIP_HORIZONTAL); + facing_ = Facing::WEST; break; case Cardinal::NONE: break; @@ -313,17 +312,14 @@ void Player::updateFacingAndTurn(float delta_time, float vx, float vy) { turning_remaining_ms_ = static_cast(turn_config.duration_ms); applyCardinal(intermediate_cardinal_); } else if (vx != 0.0F) { - // Tria de facing original: prevalença lateral, flip segons el - // signe de vx; verticals purs van a NORTH/SOUTH amb flip NONE; - // en aturar-se conservem `facing_` per a l'animació stand. - facing_ = Facing::EAST; - setFlip(vx < 0.0F ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE); + // Tria de facing: prevalença lateral, EAST/WEST segons el signe + // de vx; verticals purs van a NORTH/SOUTH. En aturar-se + // conservem `facing_` per a l'animació stand. + facing_ = (vx < 0.0F) ? Facing::WEST : Facing::EAST; } else if (vy < 0.0F) { facing_ = Facing::NORTH; - setFlip(SDL_FLIP_NONE); } else if (vy > 0.0F) { facing_ = Facing::SOUTH; - setFlip(SDL_FLIP_NONE); } } @@ -655,11 +651,11 @@ auto Player::currentFacingCardinal() const -> Cardinal { case Facing::SOUTH: return Cardinal::SOUTH; case Facing::EAST: - break; + return Cardinal::EAST; + case Facing::WEST: + return Cardinal::WEST; } - // Lateral: el flip discrimina E/W. Sense flip = mira a la dreta - // (convenció estable a tots els assets actuals). - return (getFlip() == SDL_FLIP_HORIZONTAL) ? Cardinal::WEST : Cardinal::EAST; + return Cardinal::EAST; } void Player::enterAttacking(Cardinal facing) { @@ -1106,89 +1102,48 @@ void Player::renderShadow() const { Screen::get().blendRect(SX, SY, SHADOW_W, SHADOW_H, SHADOW_ARGB, SHADOW_ALPHA); } -auto Player::animationName(bool moving) const -> const char* { - // En l'aire forcem la variant `jump_*` (per direcció), independent de - // si hi ha velocitat horitzontal: per al jugador, l'estat visual del - // salt és per direcció, no per velocitat. - if (state_ == State::AIRBORNE) { - switch (facing_) { - case Facing::NORTH: - return "jump_north"; - case Facing::SOUTH: - return "jump_south"; - case Facing::EAST: - break; - } - return "jump_east"; - } - // En LANDING_STUN, `stun_*` (per direcció): mostrem el frame - // d'aterratge fins que s'esgota el timer. La direcció ve fixada del - // moment del despegue (no es canvia mentre estem en l'aire). - if (state_ == State::LANDING_STUN) { - switch (facing_) { - case Facing::NORTH: - return "stun_north"; - case Facing::SOUTH: - return "stun_south"; - case Facing::EAST: - break; - } - return "stun_east"; - } - // En HURT, `hurt_*` (per direcció): el jugador llisca sense - // control en la direcció triada per `enterHurt`. Mateixa estructura - // que walk per facilitat (ara mateix els frames són clons). - if (state_ == State::HURT) { - switch (facing_) { - case Facing::NORTH: - return "hurt_north"; - case Facing::SOUTH: - return "hurt_south"; - case Facing::EAST: - break; - } - return "hurt_east"; - } - // ATTACKING té dues sub-fases: durant `swing_ms` el sprite és - // `attack_*` (animació del braç llançant el látigo); durant - // `cooldown_ms` posterior canviem a `recover_*` per a frames - // pròpis de recuperació. Mentre l'art no estiga dibuixat, els - // YAMLs apunten `recover_*` als mateixos frames que `stand_*` — - // visualment és idèntic, però la separació deixa l'arquitectura - // preparada perquè en afegir frames reals només calga tocar el - // YAML, no el codi. - if (state_ == State::ATTACKING) { - const bool IN_SWING = (attacking_remaining_ms_ > static_cast(whip_config.cooldown_ms)); - if (IN_SWING) { - switch (facing_) { - case Facing::NORTH: - return "attack_north"; - case Facing::SOUTH: - return "attack_south"; - case Facing::EAST: - break; - } - return "attack_east"; - } - switch (facing_) { - case Facing::NORTH: - return "recover_north"; - case Facing::SOUTH: - return "recover_south"; - case Facing::EAST: - break; - } - return "recover_east"; - } - switch (facing_) { +// Sufix d'animació per cardinal — empat 1:1 amb l'enum `Facing` i el +// sufix declarat als YAMLs de jugador (sam.yaml, morcus.yaml). +auto Player::facingSuffix(Facing facing) -> const char* { + switch (facing) { case Facing::NORTH: - return moving ? "walk_north" : "stand_north"; + return "_north"; case Facing::SOUTH: - return moving ? "walk_south" : "stand_south"; + return "_south"; case Facing::EAST: + return "_east"; + case Facing::WEST: + return "_west"; + } + return "_east"; +} + +// Prefix d'animació per estat. ATTACKING té dues sub-fases: durant +// `swing_ms` és `attack_*` (animació del braç llançant el látigo); durant +// `cooldown_ms` posterior, `recover_*`. A AIRBORNE/LANDING_STUN/HURT, la +// variant és per direcció, no per velocitat: l'estat bloqueja el `moving` +// argument i no fa servir walk/stand. +auto Player::statePrefix(State state, bool moving, bool in_attack_swing) -> const char* { + switch (state) { + case State::AIRBORNE: + return "jump"; + case State::LANDING_STUN: + return "stun"; + case State::HURT: + return "hurt"; + case State::ATTACKING: + return in_attack_swing ? "attack" : "recover"; + case State::GROUNDED: + case State::EXITED: + case State::DISABLED: break; } - return moving ? "walk_east" : "stand_east"; + return moving ? "walk" : "stand"; +} + +auto Player::animationName(bool moving) const -> std::string { + const bool IN_ATTACK_SWING = (attacking_remaining_ms_ > static_cast(whip_config.cooldown_ms)); + return std::string(statePrefix(state_, moving, IN_ATTACK_SWING)) + facingSuffix(facing_); } auto Player::getCurrentTile() const -> std::pair { @@ -1252,7 +1207,6 @@ void Player::respawn(float x, float y) { // sense això, un EXITED que entrava per la porta amb facing NORTH // re-aparix d'esquena al pròxim cicle. Mateixa lògica que el ctor. facing_ = Facing::SOUTH; - setFlip(SDL_FLIP_NONE); setCurrentAnimation("stand_south"); setPosX(x); setPosY(y); diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index 350edb6..3d19d7d 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -351,9 +351,12 @@ class Player : public AnimatedSprite { void snapToHorizontalLane(float delta_time); void snapToVerticalLane(float delta_time); - // Nom d'animació per al `facing_` actual. Si `moving` és cert s'usa - // la variant `walk*`, altrament la variant `stand*`. - [[nodiscard]] auto animationName(bool moving) const -> const char*; + // Nom d'animació per al `facing_` actual. Composat com a `prefix + + // suffix` via els dos helpers estàtics (declarats junt amb `Facing` + // més avall) — el prefix depèn de l'estat (walk/stand/jump/stun/hurt/ + // attack/recover), el sufix del cardinal (`_north`/`_south`/`_east`/ + // `_west`). + [[nodiscard]] auto animationName(bool moving) const -> std::string; void snapToLane( float delta_time, float base, @@ -428,21 +431,24 @@ class Player : public AnimatedSprite { // reinstanciar-lo. Map::Info map_; - // Direcció "cap on mira" el jugador. Governa quin conjunt d'animacions - // s'aplica (lateral, amunt, avall) i si el sprite es flipeja o no. Els - // moviments purament horitzontals o diagonals prenen `EAST` (prevalença - // lateral; el flip horitzontal converteix l'animació EAST en la - // simètrica oest), els purament verticals fan `NORTH`/`SOUTH`. En - // aturar-se es preserva l'última orientació perquè la versió stand - // coincideixi. + // Direcció "cap on mira" el jugador. Una entrada per cada cardinal: el + // codi del joc no toca mai el flip — l'oest reprodueix `*_west`, que al + // YAML declara `flip_h: true`, i és el motor d'animació qui aplica el + // flip al sprite. Els moviments diagonals prenen el component lateral + // (EAST/WEST); en aturar-se es preserva l'última orientació perquè la + // versió stand coincideixi. enum class Facing : std::uint8_t { EAST, + WEST, NORTH, SOUTH }; Facing facing_{Facing::EAST}; + // Helpers de composició de `animationName`. Han de viure ací (després + // de la declaració de `Facing`) perquè la signatura el referencia. + static auto facingSuffix(Facing facing) -> const char*; + static auto statePrefix(State state, bool moving, bool in_attack_swing) -> const char*; + // Cardinal complet (N/S/E/W) usat per a la detecció de girs de 180°. - // El parell (Facing, flip) només té 3+1 estats; aquesta enum n'afegeix - // la 4a sense reorganitzar el sistema d'animacions. // Pública perquè el `WhipAttack` la passa a `enterHurt` quan colpeja // un altre jugador (friendly fire). public: @@ -554,9 +560,8 @@ class Player : public AnimatedSprite { // timer `attacking_remaining_ms_` a `swing_ms + cooldown_ms`. void enterAttacking(Cardinal facing); - // Cardinal que correspon al `(facing_, flip_)` actual del sprite — - // la direcció cap on mira el jugador. Si `facing_ == EAST` el - // resultat depèn del flip; NORTH/SOUTH són trivials. Mai retorna NONE. + // Cardinal corresponent al `facing_` actual del sprite — la direcció + // cap on mira el jugador. Mapeig 1:1 amb `Facing` (mai NONE). [[nodiscard]] auto currentFacingCardinal() const -> Cardinal; // Activa l'estat HURT: el jugador queda projectat amb velocitat diff --git a/tests/test_animated_sprite_flip.cpp b/tests/test_animated_sprite_flip.cpp index fd2e0df..1ec9e1c 100644 --- a/tests/test_animated_sprite_flip.cpp +++ b/tests/test_animated_sprite_flip.cpp @@ -4,9 +4,9 @@ // de càrrega del YAML, no el render real. // // Tests coberts: -// T1 backward compat — YAML sense cap flip declarat carrega tots els -// frames amb (flip_h=false, flip_v=false) i sense -// que l'animació prengui control del flip. +// T1 default — YAML sense cap flip declarat carrega tots els frames +// amb (flip_h=false, flip_v=false). L'aplicació al sprite +// resulta en `SDL_FLIP_NONE`. // T2 flip a nivell d'animació — `flip_h: true` amb frames `int` → // tots els frames hereten flip_h=true. // T3 override per-frame — frame mapping `{ index: N, flip_h: true }` @@ -55,8 +55,8 @@ namespace { fkyaml::node::deserialize(yaml_str), sheetParams()); } - void testBackwardCompat() { - std::cout << "[T1] backward compat: cap flip declarat\n"; + void testDefaultNoFlip() { + std::cout << "[T1] default: cap flip declarat → frames sense flip\n"; const std::string YAML = R"( animations: walk: { frames: [0, 1, 2, 3], frame_ms: 100 } @@ -70,7 +70,6 @@ animations: if (WALK_IT != ANIMS.end()) { const auto& WALK = WALK_IT->second; REQUIRE(WALK.frames.size() == 4); - REQUIRE(!WALK.controls_flip); for (const auto& F : WALK.frames) { REQUIRE(!F.flip_h); REQUIRE(!F.flip_v); @@ -82,7 +81,6 @@ animations: if (STAND_IT != ANIMS.end()) { const auto& STAND = STAND_IT->second; REQUIRE(STAND.frames.size() == 1); - REQUIRE(!STAND.controls_flip); REQUIRE(!STAND.frames[0].flip_h); REQUIRE(!STAND.frames[0].flip_v); } @@ -100,7 +98,6 @@ animations: if (IT != ANIMS.end()) { const auto& A = IT->second; REQUIRE(A.frames.size() == 4); - REQUIRE(A.controls_flip); for (const auto& F : A.frames) { REQUIRE(F.flip_h); REQUIRE(!F.flip_v); @@ -126,8 +123,6 @@ animations: if (IT != ANIMS.end()) { const auto& A = IT->second; REQUIRE(A.frames.size() == 4); - // Hi ha un mapping → l'animació pren el control del flip. - REQUIRE(A.controls_flip); // Els tres primers (int, sense flip a l'anim) → tot a false. REQUIRE(!A.frames[0].flip_h); @@ -160,7 +155,6 @@ animations: if (IT != ANIMS.end()) { const auto& A = IT->second; REQUIRE(A.frames.size() == 2); - REQUIRE(A.controls_flip); // Frame int hereta el flip_h true de l'animació. REQUIRE(A.frames[0].flip_h); @@ -188,7 +182,6 @@ animations: if (IT != ANIMS.end()) { const auto& A = IT->second; REQUIRE(A.frames.size() == 1); - REQUIRE(A.controls_flip); REQUIRE(A.frames[0].flip_h); REQUIRE(A.frames[0].flip_v); } @@ -199,7 +192,7 @@ animations: auto main() -> int { std::cout << "== smoke tests: animation flip parser ==\n"; - testBackwardCompat(); + testDefaultNoFlip(); testFlipAtAnimationLevel(); testOverridePerFrame(); testMappingDoesNotInherit();