Merge branch 'feat/animation-flip' — flip H/V al motor i cardinals N/S/E/W

This commit is contained in:
JailDesigner committed 2026-04-29 10:17:46 +02:00
commit 6397f9d4eb
18 files changed
+710 -409

No files matched your search

+18
View File
@@ -88,6 +88,7 @@ set(APP_SOURCES
source/core/rendering/sprite/sprite.cpp source/core/rendering/sprite/sprite.cpp
source/core/rendering/sprite/moving_sprite.cpp source/core/rendering/sprite/moving_sprite.cpp
source/core/rendering/sprite/animated_sprite.cpp source/core/rendering/sprite/animated_sprite.cpp
source/core/rendering/sprite/animated_sprite_parser.cpp
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
source/core/rendering/sdl3gpu/viewport.cpp source/core/rendering/sdl3gpu/viewport.cpp
source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp
@@ -444,4 +445,21 @@ if(BUILD_TESTS)
add_test(NAME game_fsm add_test(NAME game_fsm
COMMAND test_game_fsm COMMAND test_game_fsm
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
# Smoke del parser d'animacions amb suport de flip H/V. Stand-alone:
# només enllaça `animated_sprite_parser.cpp`, que és la part del motor
# que NO depèn de Resource::Cache ni de Surface — el parser és pure
# YAML-in / dades-out, així que no cal arrossegar tot l'stack.
add_executable(test_animated_sprite_flip
tests/test_animated_sprite_flip.cpp
source/core/rendering/sprite/animated_sprite_parser.cpp
)
target_include_directories(test_animated_sprite_flip PRIVATE
"${CMAKE_SOURCE_DIR}/source"
)
target_link_libraries(test_animated_sprite_flip PRIVATE SDL3::SDL3)
target_compile_options(test_animated_sprite_flip PRIVATE -Wall -Wextra -Wpedantic)
add_test(NAME animated_sprite_flip
COMMAND test_animated_sprite_flip
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
endif() endif()
+11 -7
View File
@@ -23,18 +23,22 @@ flags:
collides: true collides: true
interactable: false interactable: false
# Strip de 12 frames en graella 4×3 (mateix esquema que el jugador): # Strip de 12 frames en graella 4×3 (mateix esquema que el jugador):
# 0..3 → walk_down (cara avall) # 0..3 → walk_south (cara avall)
# 4..7 → walk_up (cara amunt) # 4..7 → walk_north (cara amunt)
# 8..11 → walk (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. # La momia no té variants `stand_*` perquè mai no para.
animations: animations:
walk_down: { frames: [0, 1, 2, 3], fps: 8, loop: true } walk_south: { frames: [0, 1, 2, 3], fps: 8, loop: true }
walk_up: { frames: [4, 5, 6, 7], fps: 8, loop: true } walk_north: { frames: [4, 5, 6, 7], fps: 8, loop: true }
walk: { frames: [8, 9, 10, 11], 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: ai:
type: corridor_wanderer type: corridor_wanderer
direction_choice: random direction_choice: random
initial_direction: right initial_direction: east
stats: stats:
speed: 30 speed: 30
damage: 1 damage: 1
Binary file not shown.
+33 -50
View File
@@ -12,60 +12,43 @@ collision:
height: 12 height: 12
animations: animations:
stand_down: { frames: [0] } # Les variants `*_west` reaprofiten els frames de l'est i declaren
stand_up: { frames: [4] } # `flip_h: true` perquè el motor d'animació apliqui el flip al sprite —
stand: { frames: [8] } # el codi del joc no toca mai `flip_` directament.
jump_down: { frames: [12] } stand_south: { frames: [0] }
jump_up: { frames: [13] } stand_north: { frames: [4] }
jump: { frames: [14] } stand_east: { frames: [8] }
stun_down: { frames: [16] } stand_west: { frames: [8], flip_h: true }
stun_up: { frames: [17] }
stun: { frames: [18] }
walk_down: jump_south: { frames: [12] }
frames: [0, 1, 2, 3] jump_north: { frames: [13] }
frame_ms: 100 jump_east: { frames: [14] }
jump_west: { frames: [14], flip_h: true }
walk_up: stun_south: { frames: [16] }
frames: [4, 5, 6, 7] stun_north: { frames: [17] }
frame_ms: 100 stun_east: { frames: [18] }
stun_west: { frames: [18], flip_h: true }
walk: walk_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
frames: [8, 9, 10, 11] walk_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
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_down: hurt_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
frames: [0, 1, 2, 3] hurt_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
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 }
hurt_up: attack_south: { frames: [20, 21, 22, 23], frame_ms: 50 }
frames: [4, 5, 6, 7] attack_north: { frames: [24, 25, 26, 27], frame_ms: 50 }
frame_ms: 100 attack_east: { frames: [28, 29, 30, 31], frame_ms: 50 }
attack_west: { frames: [28, 29, 30, 31], frame_ms: 50, flip_h: true }
hurt: recover_south: { frames: [23] }
frames: [8, 9, 10, 11] recover_north: { frames: [27] }
frame_ms: 100 recover_east: { frames: [31] }
recover_west: { frames: [31], flip_h: true }
attack_down:
frames: [20, 21, 22, 23]
frame_ms: 50
attack_up:
frames: [24, 25, 26, 27]
frame_ms: 50
attack:
frames: [28, 29, 30, 31]
frame_ms: 50
recover_down:
frames: [23]
recover_up:
frames: [27]
recover:
frames: [31]
# Inèrcia de frenada en soltar la direcció. `threshold_ms` filtra # Inèrcia de frenada en soltar la direcció. `threshold_ms` filtra
# tocs curts (passets d'un tile sense rellisquera). # tocs curts (passets d'un tile sense rellisquera).
@@ -78,8 +61,8 @@ inertia:
turn: turn:
duration_ms: 80 duration_ms: 80
transitions: transitions:
west_to_east: down west_to_east: south
east_to_west: up east_to_west: north
north_to_south: east north_to_south: east
south_to_north: west south_to_north: west
+33 -50
View File
@@ -12,60 +12,43 @@ collision:
height: 12 height: 12
animations: animations:
stand_down: { frames: [0] } # Les variants `*_west` reaprofiten els frames de l'est i declaren
stand_up: { frames: [4] } # `flip_h: true` perquè el motor d'animació apliqui el flip al sprite —
stand: { frames: [8] } # el codi del joc no toca mai `flip_` directament.
jump_down: { frames: [12] } stand_south: { frames: [0] }
jump_up: { frames: [13] } stand_north: { frames: [4] }
jump: { frames: [14] } stand_east: { frames: [8] }
stun_down: { frames: [16] } stand_west: { frames: [8], flip_h: true }
stun_up: { frames: [17] }
stun: { frames: [18] }
walk_down: jump_south: { frames: [12] }
frames: [0, 1, 2, 3] jump_north: { frames: [13] }
frame_ms: 100 jump_east: { frames: [14] }
jump_west: { frames: [14], flip_h: true }
walk_up: stun_south: { frames: [16] }
frames: [4, 5, 6, 7] stun_north: { frames: [17] }
frame_ms: 100 stun_east: { frames: [18] }
stun_west: { frames: [18], flip_h: true }
walk: walk_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
frames: [8, 9, 10, 11] walk_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
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_down: hurt_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
frames: [0, 1, 2, 3] hurt_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
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 }
hurt_up: attack_south: { frames: [20, 21, 22, 23], frame_ms: 50 }
frames: [4, 5, 6, 7] attack_north: { frames: [24, 25, 26, 27], frame_ms: 50 }
frame_ms: 100 attack_east: { frames: [28, 29, 30, 31], frame_ms: 50 }
attack_west: { frames: [28, 29, 30, 31], frame_ms: 50, flip_h: true }
hurt: recover_south: { frames: [23] }
frames: [8, 9, 10, 11] recover_north: { frames: [27] }
frame_ms: 100 recover_east: { frames: [31] }
recover_west: { frames: [31], flip_h: true }
attack_down:
frames: [20, 21, 22, 23]
frame_ms: 50
attack_up:
frames: [24, 25, 26, 27]
frame_ms: 50
attack:
frames: [28, 29, 30, 31]
frame_ms: 50
recover_down:
frames: [23]
recover_up:
frames: [27]
recover:
frames: [31]
# Inèrcia de frenada en soltar la direcció. `threshold_ms` filtra # Inèrcia de frenada en soltar la direcció. `threshold_ms` filtra
# tocs curts (passets d'un tile sense rellisquera). # tocs curts (passets d'un tile sense rellisquera).
@@ -78,8 +61,8 @@ inertia:
turn: turn:
duration_ms: 80 duration_ms: 80
transitions: transitions:
west_to_east: down west_to_east: south
east_to_west: up east_to_west: north
north_to_south: east north_to_south: east
south_to_north: west south_to_north: west
@@ -7,36 +7,19 @@
#include "core/rendering/surface.hpp" #include "core/rendering/surface.hpp"
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "external/fkyaml_node.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 { 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 // Llegeix el bloc `sprite: { path, size: [w, h] }` — mateix format que
// les plantilles d'entitat. La surface es resol via Resource::Cache amb // les plantilles d'entitat. La surface es resol via Resource::Cache amb
// el basename del fitxer. // el basename del fitxer.
auto parseSpriteBlock(const fkyaml::node& yaml, std::shared_ptr<Surface>& surface) -> SheetParams { auto parseSpriteBlock(const fkyaml::node& yaml, std::shared_ptr<Surface>& surface) -> AnimatedSprite::SheetParams {
SheetParams params; AnimatedSprite::SheetParams params;
if (!yaml.contains("sprite")) { return params; } if (!yaml.contains("sprite")) { return params; }
const auto& s = yaml["sprite"]; const auto& s = yaml["sprite"];
if (s.contains("path")) { if (s.contains("path")) {
@@ -57,62 +40,18 @@ namespace {
return params; return params;
} }
// `frame_ms` pot ser un escalar (temps uniforme) o una llista (per-frame). // Tradueix els flags lògics flip_h/flip_v al `SDL_FlipMode` que admet
// Convertim sempre a segons perquè `animate()` treballa amb delta_time // `Surface::render`. Important: a SDL3, `SDL_FlipMode` és un enum
// en segons. Si falta o val 0, `speeds` queda buit i l'animació es // exclusiu (NONE/HORIZONTAL/VERTICAL), no un bitmask combinable. Per
// considera estàtica (no avança). // tant, V+H simultani al mateix frame no es pot expressar amb una
void parseFrameTimes(const fkyaml::node& anim_node, AnimatedSprite::AnimationData& animation) { // única crida — prioritzem H (suficient per als casos pràctics
if (!anim_node.contains("frame_ms")) { return; } // d'animació, com `walk_west` o `death_spin`). Si en algun moment fa
const auto& fm = anim_node["frame_ms"]; // falta combinar, caldrà ampliar `Surface::render` per a doble-flipar
if (fm.is_sequence()) { // a baix nivell.
for (const auto& v : fm) { auto toFlipMode(bool flip_h, bool flip_v) -> SDL_FlipMode {
animation.speeds.push_back(static_cast<float>(v.get_value<int>()) / 1000.0F); if (flip_h) { return SDL_FLIP_HORIZONTAL; }
} if (flip_v) { return SDL_FLIP_VERTICAL; }
} else { return SDL_FLIP_NONE;
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 } // namespace
@@ -122,7 +61,7 @@ AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
try { try {
auto yaml = fkyaml::node::deserialize(yaml_content); auto yaml = fkyaml::node::deserialize(yaml_content);
auto params = parseSpriteBlock(yaml, surface_); auto params = parseSpriteBlock(yaml, surface_);
animations_ = parseAnimations(yaml, params); animations_ = parseAnimationsFromYaml(yaml, params);
setWidth(params.frame_width); setWidth(params.frame_width);
setHeight(params.frame_height); setHeight(params.frame_height);
@@ -132,7 +71,7 @@ AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
// abans del primer `update()`, així que el valor real el posa ell. // abans del primer `update()`, així que el valor real el posa ell.
if (!animations_.empty()) { if (!animations_.empty()) {
const auto& first = animations_.begin()->second; const auto& first = animations_.begin()->second;
if (!first.frames.empty()) { setClip(first.frames[0]); } if (!first.frames.empty()) { setClip(first.frames[0].rect); }
} }
} catch (const fkyaml::exception& e) { } catch (const fkyaml::exception& e) {
std::cerr << "YAML parsing error in animation " << cached_data.name << ": " << e.what() << '\n'; std::cerr << "YAML parsing error in animation " << cached_data.name << ": " << e.what() << '\n';
@@ -154,11 +93,20 @@ void AnimatedSprite::animate(float delta_time) {
const float TOTAL = std::accumulate(anim.speeds.begin(), anim.speeds.end(), 0.0F); 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.accumulated_time >= TOTAL) {
if (anim.loop_from < 0) { if (anim.loop_from < 0) {
anim.current_frame = static_cast<int>(anim.frames.size()) - 1; anim.current_frame = static_cast<int>(anim.frames.size()) - 1;
anim.completed = true; anim.completed = true;
setClip(anim.frames[anim.current_frame]); apply_current_frame(anim.current_frame);
return; return;
} }
float loop_start = 0.0F; float loop_start = 0.0F;
@@ -172,13 +120,13 @@ void AnimatedSprite::animate(float delta_time) {
cursor += anim.speeds[i]; cursor += anim.speeds[i];
if (anim.accumulated_time < cursor) { if (anim.accumulated_time < cursor) {
anim.current_frame = i; anim.current_frame = i;
setClip(anim.frames[i]); apply_current_frame(i);
return; return;
} }
} }
anim.current_frame = static_cast<int>(anim.frames.size()) - 1; anim.current_frame = static_cast<int>(anim.frames.size()) - 1;
setClip(anim.frames[anim.current_frame]); apply_current_frame(anim.current_frame);
} }
auto AnimatedSprite::animationIsCompleted() -> bool { auto AnimatedSprite::animationIsCompleted() -> bool {
@@ -200,7 +148,10 @@ void AnimatedSprite::setCurrentAnimation(const std::string& name) {
anim.current_frame = 0; anim.current_frame = 0;
anim.accumulated_time = 0.0F; anim.accumulated_time = 0.0F;
anim.completed = false; anim.completed = false;
if (!anim.frames.empty()) { setClip(anim.frames[0]); } 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) { void AnimatedSprite::update(float delta_time) {
@@ -8,14 +8,41 @@
#include "core/rendering/sprite/moving_sprite.hpp" #include "core/rendering/sprite/moving_sprite.hpp"
#include "core/resources/resource_types.hpp" #include "core/resources/resource_types.hpp"
#include "external/fkyaml_node.hpp"
class AnimatedSprite : public MovingSprite { class AnimatedSprite : public MovingSprite {
public: public:
// Un frame d'animació = rectangle dins el spritesheet + flags de flip
// associats. El flip s'aplica al sprite a `animate()` quan l'animació
// declara que ella controla el flip (vegeu `AnimationData::controls_flip`).
struct Frame {
SDL_FRect rect{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
bool flip_h{false};
bool flip_v{false};
};
// Paràmetres geomètrics del spritesheet, derivats del bloc `sprite:`
// del YAML. Es passen al parser d'animacions per a calcular els
// rectangles de cada frame a partir dels seus índexs.
struct SheetParams {
float frame_width{0.0F};
float frame_height{0.0F};
int frames_per_row{1};
int max_tiles{1};
};
// Dades d'una animació concreta dins d'un spritesheet. Les claus del // 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 // mapa (`animations_`) són els noms; aquí només es guarden els fotogrames
// i l'estat de reproducció. // 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 { struct AnimationData {
std::vector<SDL_FRect> frames; std::vector<Frame> frames;
// Durada de cada frame en segons (convertit des de `frame_ms` al YAML). // 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 // Si té un sol valor, val per a tots els frames; altrament ha de tenir
// la mateixa mida que `frames`. // la mateixa mida que `frames`.
@@ -40,6 +67,12 @@ class AnimatedSprite : public MovingSprite {
void resetAnimation(); void resetAnimation();
void animate(float delta_time); void animate(float delta_time);
// Helper de parsing exposat per a testabilitat. NO toca el cache de
// recursos — només el bloc `animations:` del YAML i els paràmetres de
// full ja calculats. El ctor d'`AnimatedSprite` el crida internament.
static auto parseAnimationsFromYaml(const fkyaml::node& yaml, const SheetParams& params)
-> std::unordered_map<std::string, AnimationData>;
private: private:
std::unordered_map<std::string, AnimationData> animations_; std::unordered_map<std::string, AnimationData> animations_;
// Nom de l'animació activa. Buit fins que algú crida `setCurrentAnimation`. // Nom de l'animació activa. Buit fins que algú crida `setCurrentAnimation`.
@@ -0,0 +1,114 @@
#include "core/rendering/sprite/animated_sprite.hpp"
// Implementació del parser pure de YAML d'animacions. Viu en una unitat
// separada perquè NO depèn de Resource::Cache ni de Surface — només de
// fkyaml — i això permet als tests stand-alone enllaçar només aquest .cpp
// per a verificar el parsing sense haver d'arrossegar tot l'stack de
// recursos i renderitzat.
namespace {
// Construeix el rectangle d'un frame a partir del seu índex dins del
// spritesheet. Centralitzat per a no duplicar la fórmula als dos
// formats (escalar / mapping) que admet `frames:` al YAML.
auto rectFromIndex(int index, const AnimatedSprite::SheetParams& params) -> SDL_FRect {
return SDL_FRect{
.x = static_cast<float>(index % params.frames_per_row) * params.frame_width,
.y = static_cast<float>(index / params.frames_per_row) * params.frame_height,
.w = params.frame_width,
.h = params.frame_height,
};
}
// Llig un node `frames` (seqüència) i el converteix a la nova
// representació `vector<Frame>`. Cada element pot ser:
// - escalar (int) → hereta `default_flip_h`/`default_flip_v` de l'animació.
// - mapping `{ index: N, flip_h?: bool, flip_v?: bool }` → fully
// explícit. Els flags absents defaulten a false (NO hereta de l'anim).
auto convertYamlFrames(const fkyaml::node& frames_node, const AnimatedSprite::SheetParams& params, bool default_flip_h, bool default_flip_v) -> std::vector<AnimatedSprite::Frame> {
std::vector<AnimatedSprite::Frame> frames;
for (const auto& frame_node : frames_node) {
if (frame_node.is_mapping()) {
if (!frame_node.contains("index")) { continue; }
const int NUM_TILE = frame_node["index"].get_value<int>();
if (NUM_TILE > params.max_tiles) { continue; }
AnimatedSprite::Frame frame;
frame.rect = rectFromIndex(NUM_TILE, params);
if (frame_node.contains("flip_h")) { frame.flip_h = frame_node["flip_h"].get_value<bool>(); }
if (frame_node.contains("flip_v")) { frame.flip_v = frame_node["flip_v"].get_value<bool>(); }
frames.emplace_back(frame);
} else {
const int NUM_TILE = frame_node.get_value<int>();
if (NUM_TILE > params.max_tiles) { continue; }
frames.emplace_back(AnimatedSprite::Frame{
.rect = rectFromIndex(NUM_TILE, params),
.flip_h = default_flip_h,
.flip_v = default_flip_v,
});
}
}
return frames;
}
// `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;
}
}
} // namespace
auto AnimatedSprite::parseAnimationsFromYaml(const fkyaml::node& yaml, const SheetParams& params)
-> std::unordered_map<std::string, AnimationData> {
std::unordered_map<std::string, 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;
// Flags de flip a nivell d'animació (defaults a false). Els frames
// declarats com a int els hereten; els declarats com a mapping no.
bool anim_flip_h = false;
bool anim_flip_v = false;
if (anim_node.contains("flip_h")) { anim_flip_h = anim_node["flip_h"].get_value<bool>(); }
if (anim_node.contains("flip_v")) { anim_flip_v = anim_node["flip_v"].get_value<bool>(); }
AnimationData animation;
if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
animation.frames = convertYamlFrames(anim_node["frames"], params, anim_flip_h, anim_flip_v);
}
parseFrameTimes(anim_node, animation);
parseLoopPolicy(anim_node, animation);
animations.emplace(NAME, std::move(animation));
}
return animations;
}
+12 -1
View File
@@ -4,9 +4,14 @@
#include <utility> #include <utility>
AnimationComponent::AnimationComponent(std::vector<int> frames, float fps, bool loop) AnimationComponent::AnimationComponent(std::vector<int> frames, float fps, bool loop)
: AnimationComponent(std::move(frames), fps, loop, false, false) {}
AnimationComponent::AnimationComponent(std::vector<int> frames, float fps, bool loop, bool flip_h, bool flip_v)
: frames_(std::move(frames)), : frames_(std::move(frames)),
fps_(fps > 0.0F ? fps : 1.0F), 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 // 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 // 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) // 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; } if (frames_.empty()) { return 0; }
return frames_[static_cast<std::size_t>(current_)]; 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;
}
@@ -1,5 +1,7 @@
#pragma once #pragma once
#include <SDL3/SDL.h>
#include <cstddef> #include <cstddef>
#include <vector> #include <vector>
@@ -13,10 +15,18 @@
// algun dia aterri l'art definitiu, el renderer consultarà `currentFrame()` // algun dia aterri l'art definitiu, el renderer consultarà `currentFrame()`
// i traduirà l'índex al rectangle del sprite corresponent. De moment els // i traduirà l'índex al rectangle del sprite corresponent. De moment els
// placeholders tenen un únic frame i `currentFrame()` retorna sempre 0. // 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 { class AnimationComponent {
public: public:
AnimationComponent() = default; AnimationComponent() = default;
AnimationComponent(std::vector<int> frames, float fps, bool loop); AnimationComponent(std::vector<int> frames, float fps, bool loop);
AnimationComponent(std::vector<int> 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 // 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; // 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 completed() const -> bool { return completed_; }
[[nodiscard]] auto loops() const -> bool { return loop_; } [[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: private:
std::vector<int> frames_; std::vector<int> frames_;
float fps_{1.0F}; float fps_{1.0F};
bool loop_{true}; bool loop_{true};
bool flip_h_{false};
bool flip_v_{false};
int current_{0}; int current_{0};
float accumulator_{0.0F}; float accumulator_{0.0F};
bool completed_{false}; bool completed_{false};
+67 -55
View File
@@ -26,11 +26,15 @@ namespace {
// Llegeix un bloc d'animació del YAML amb fallbacks tolerants (mateix // Llegeix un bloc d'animació del YAML amb fallbacks tolerants (mateix
// patró que Door/Sarcophagus). Si la clau no apareix, retorna una // patró que Door/Sarcophagus). Si la clau no apareix, retorna una
// animació d'1 frame loopant, neutra — la momia es podrà moure però // 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 { auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
std::vector<int> frames = {0}; std::vector<int> frames = {0};
float fps = 8.0F; float fps = 8.0F;
bool loop = true; bool loop = true;
bool flip_h = false;
bool flip_v = false;
if (yaml.contains("animations") && yaml["animations"].contains(key)) { if (yaml.contains("animations") && yaml["animations"].contains(key)) {
const auto& anim = yaml["animations"][key]; const auto& anim = yaml["animations"][key];
if (anim.contains("frames")) { if (anim.contains("frames")) {
@@ -43,8 +47,10 @@ namespace {
} }
if (anim.contains("fps")) { fps = anim["fps"].get_value<float>(); } if (anim.contains("fps")) { fps = anim["fps"].get_value<float>(); }
if (anim.contains("loop")) { loop = anim["loop"].get_value<bool>(); } 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 {frames, fps, loop}; return {frames, fps, loop, flip_h, flip_v};
} }
// Busca si el segment [prev, prev+delta] travessa una lane del patró // Busca si el segment [prev, prev+delta] travessa una lane del patró
@@ -73,9 +79,10 @@ namespace {
} // namespace } // namespace
Mummy::Mummy(const fkyaml::node& yaml) Mummy::Mummy(const fkyaml::node& yaml)
: anim_walk_down_(buildAnimation(yaml, "walk_down")), : anim_walk_south_(buildAnimation(yaml, "walk_south")),
anim_walk_up_(buildAnimation(yaml, "walk_up")), anim_walk_north_(buildAnimation(yaml, "walk_north")),
anim_walk_lateral_(buildAnimation(yaml, "walk")) { anim_walk_east_(buildAnimation(yaml, "walk_east")),
anim_walk_west_(buildAnimation(yaml, "walk_west")) {
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("mummy"); name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("mummy");
collides_ = true; collides_ = true;
interactable_ = false; interactable_ = false;
@@ -87,21 +94,22 @@ Mummy::Mummy(const fkyaml::node& yaml)
if (s.contains("damage")) { setDamage(s["damage"].get_value<int>()); } if (s.contains("damage")) { setDamage(s["damage"].get_value<int>()); }
} }
// Direcció inicial llegida del bloc `ai:`. Per defecte dreta, per // Direcció inicial llegida del bloc `ai:`. Per defecte est, per
// coherència amb el que fa RollingRock. Valors acceptats: right, left, // coherència amb el que fa RollingRock. Valors acceptats:
// up, down. Qualsevol altre s'interpreta com a "sense direcció" (la // east, west, north, south. Qualsevol altre s'interpreta com a
// momia no es mou fins que step() no triï una direcció). // "sense direcció" (la momia no es mou fins que step() no triï una
std::string initial = "right"; // direcció).
std::string initial = "east";
if (yaml.contains("ai") && yaml["ai"].contains("initial_direction")) { if (yaml.contains("ai") && yaml["ai"].contains("initial_direction")) {
initial = yaml["ai"]["initial_direction"].get_value<std::string>(); initial = yaml["ai"]["initial_direction"].get_value<std::string>();
} }
if (initial == "right") { if (initial == "east") {
changeDirection(1.0F, 0.0F); changeDirection(1.0F, 0.0F);
} else if (initial == "left") { } else if (initial == "west") {
changeDirection(-1.0F, 0.0F); changeDirection(-1.0F, 0.0F);
} else if (initial == "up") { } else if (initial == "north") {
changeDirection(0.0F, -1.0F); changeDirection(0.0F, -1.0F);
} else if (initial == "down") { } else if (initial == "south") {
changeDirection(0.0F, 1.0F); changeDirection(0.0F, 1.0F);
} }
@@ -124,22 +132,26 @@ Mummy::Mummy(const fkyaml::node& yaml)
void Mummy::changeDirection(float dx, float dy) { void Mummy::changeDirection(float dx, float dy) {
setDirection(dx, dy); setDirection(dx, dy);
// Prevalença lateral: si hi ha component horitzontal s'usa l'anim // Una animació per cada cardinal — el flip viu al YAML
// lateral + flip segons el signe. Només direccions purament verticals // (`walk_west: flip_h: true`) i el codi només selecciona quina
// disparen walk_up/walk_down. Quan la direcció queda (0,0) (cul-de-sac // animació mostrar. Prevalença lateral en diagonals; quan la direcció
// amb NONE) no toquem l'animació: la momia es quedarà al frame actual // queda (0,0) (cul-de-sac amb NONE) no toquem l'animació: la mòmia es
// fins que step() li torni a assignar direcció. // quedarà al frame actual fins que step() li torni a assignar direcció.
if (dx != 0.0F) { if (dx > 0.0F) {
anim_ = anim_walk_lateral_; 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) { } else if (dy < 0.0F) {
anim_ = anim_walk_up_; anim_ = anim_walk_north_;
flip_ = SDL_FLIP_NONE;
} else if (dy > 0.0F) { } else if (dy > 0.0F) {
anim_ = anim_walk_down_; anim_ = anim_walk_south_;
flip_ = SDL_FLIP_NONE; }
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();
} }
if (anim_.has_value()) { anim_->reset(); }
} }
auto Mummy::chooseDirection(const std::vector<Direction>& allowed) -> Direction { auto Mummy::chooseDirection(const std::vector<Direction>& allowed) -> Direction {
@@ -234,10 +246,10 @@ auto Mummy::detectLaneCrossing(float dx, float dy) const -> std::optional<float>
} }
auto Mummy::currentDirection() const -> Direction { auto Mummy::currentDirection() const -> Direction {
if (dir_x_ > 0.0F) { return Direction::RIGHT; } if (dir_x_ > 0.0F) { return Direction::EAST; }
if (dir_x_ < 0.0F) { return Direction::LEFT; } if (dir_x_ < 0.0F) { return Direction::WEST; }
if (dir_y_ > 0.0F) { return Direction::DOWN; } if (dir_y_ > 0.0F) { return Direction::SOUTH; }
if (dir_y_ < 0.0F) { return Direction::UP; } if (dir_y_ < 0.0F) { return Direction::NORTH; }
return Direction::NONE; return Direction::NONE;
} }
@@ -268,10 +280,10 @@ auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
Direction dir; Direction dir;
}; };
const std::array<Candidate, 4> ALL{{ const std::array<Candidate, 4> ALL{{
{.dx = step, .dy = 0.0F, .dir = Direction::RIGHT}, {.dx = step, .dy = 0.0F, .dir = Direction::EAST},
{.dx = -step, .dy = 0.0F, .dir = Direction::LEFT}, {.dx = -step, .dy = 0.0F, .dir = Direction::WEST},
{.dx = 0.0F, .dy = -step, .dir = Direction::UP}, {.dx = 0.0F, .dy = -step, .dir = Direction::NORTH},
{.dx = 0.0F, .dy = step, .dir = Direction::DOWN}, {.dx = 0.0F, .dy = step, .dir = Direction::SOUTH},
}}; }};
DirectionPools pools; DirectionPools pools;
for (const auto& c : ALL) { for (const auto& c : ALL) {
@@ -293,16 +305,16 @@ auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
void Mummy::applyChoice(Direction dir) { void Mummy::applyChoice(Direction dir) {
switch (dir) { switch (dir) {
case Direction::UP: case Direction::NORTH:
changeDirection(0.0F, -1.0F); changeDirection(0.0F, -1.0F);
break; break;
case Direction::DOWN: case Direction::SOUTH:
changeDirection(0.0F, 1.0F); changeDirection(0.0F, 1.0F);
break; break;
case Direction::LEFT: case Direction::WEST:
changeDirection(-1.0F, 0.0F); changeDirection(-1.0F, 0.0F);
break; break;
case Direction::RIGHT: case Direction::EAST:
changeDirection(1.0F, 0.0F); changeDirection(1.0F, 0.0F);
break; break;
case Direction::NONE: case Direction::NONE:
@@ -347,16 +359,16 @@ auto Mummy::chooseHurtDirection(Direction preferred) const -> Direction {
float dx = 0.0F; float dx = 0.0F;
float dy = 0.0F; float dy = 0.0F;
switch (d) { switch (d) {
case Direction::UP: case Direction::NORTH:
dy = -STEP; dy = -STEP;
break; break;
case Direction::DOWN: case Direction::SOUTH:
dy = STEP; dy = STEP;
break; break;
case Direction::LEFT: case Direction::WEST:
dx = -STEP; dx = -STEP;
break; break;
case Direction::RIGHT: case Direction::EAST:
dx = STEP; dx = STEP;
break; break;
case Direction::NONE: case Direction::NONE:
@@ -373,21 +385,21 @@ auto Mummy::chooseHurtDirection(Direction preferred) const -> Direction {
Direction left = Direction::NONE; Direction left = Direction::NONE;
Direction right = Direction::NONE; Direction right = Direction::NONE;
switch (preferred) { switch (preferred) {
case Direction::UP: case Direction::NORTH:
left = Direction::LEFT; left = Direction::WEST;
right = Direction::RIGHT; right = Direction::EAST;
break; break;
case Direction::DOWN: case Direction::SOUTH:
left = Direction::RIGHT; left = Direction::EAST;
right = Direction::LEFT; right = Direction::WEST;
break; break;
case Direction::LEFT: case Direction::WEST:
left = Direction::DOWN; left = Direction::SOUTH;
right = Direction::UP; right = Direction::NORTH;
break; break;
case Direction::RIGHT: case Direction::EAST:
left = Direction::UP; left = Direction::NORTH;
right = Direction::DOWN; right = Direction::SOUTH;
break; break;
case Direction::NONE: case Direction::NONE:
return Direction::NONE; return Direction::NONE;
+17 -13
View File
@@ -16,10 +16,10 @@
class Mummy : public DynamicHazard { class Mummy : public DynamicHazard {
public: public:
enum class Direction : std::uint8_t { NONE, enum class Direction : std::uint8_t { NONE,
UP, NORTH,
DOWN, SOUTH,
LEFT, WEST,
RIGHT }; EAST };
// Estat d'alt nivell. ROAMING = IA normal (corridor_wanderer); HURT = // Estat d'alt nivell. ROAMING = IA normal (corridor_wanderer); HURT =
// post-fuetada, fugint en línia recta durant `hurt_config_.duration_s`. // post-fuetada, fugint en línia recta durant `hurt_config_.duration_s`.
@@ -91,10 +91,11 @@ class Mummy : public DynamicHazard {
}; };
[[nodiscard]] auto collectDirectionPools(float step) const -> DirectionPools; [[nodiscard]] auto collectDirectionPools(float step) const -> DirectionPools;
// Canvi de direcció + aplicació de l'animació i flip corresponents. // Canvi de direcció + selecció de l'animació corresponent (un cardinal
// Tots els camins que trien nova direcció (ctor amb `initial_direction`, // per branch). Tots els camins que trien nova direcció (ctor amb
// gir després d'un bloqueig) passen per ací per mantenir anim_ i // `initial_direction`, gir després d'un bloqueig) passen per ací; el
// flip_ en sincronia amb (dir_x_, dir_y_). // 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); void changeDirection(float dx, float dy);
// Aplica la direcció triada (wrapper del switch en una sola crida). // 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). // totes ho estan, retorna `NONE` (la mòmia queda quieta en HURT).
[[nodiscard]] auto chooseHurtDirection(Direction preferred) const -> Direction; [[nodiscard]] auto chooseHurtDirection(Direction preferred) const -> Direction;
// Animacions carregades del YAML, una per cada facing. Per a horitzontal // Animacions carregades del YAML, una per cada cardinal. La variant
// només en cal una (`walk`) i el flip ja diferencia esquerra/dreta. // `walk_west` reaprofita els frames de l'est però amb `flip_h: true`
AnimationComponent anim_walk_down_; // declarat al YAML — el motor d'animació aplica el flip al sprite
AnimationComponent anim_walk_up_; // automàticament; aquesta classe no toca mai `flip_` directament.
AnimationComponent anim_walk_lateral_; 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 // Estat HURT i la seua configuració. `hurt_speed_` és l'efectiva
// (resultat d'aplicar el `speed` del bloc `hurt:` o, si val 0, la // (resultat d'aplicar el `speed` del bloc `hurt:` o, si val 0, la
@@ -255,13 +255,13 @@ namespace {
auto whipDirToMummyDir(WhipAttack::Direction d) -> Mummy::Direction { auto whipDirToMummyDir(WhipAttack::Direction d) -> Mummy::Direction {
switch (d) { switch (d) {
case WhipAttack::Direction::NORTH: case WhipAttack::Direction::NORTH:
return Mummy::Direction::UP; return Mummy::Direction::NORTH;
case WhipAttack::Direction::SOUTH: case WhipAttack::Direction::SOUTH:
return Mummy::Direction::DOWN; return Mummy::Direction::SOUTH;
case WhipAttack::Direction::EAST: case WhipAttack::Direction::EAST:
return Mummy::Direction::RIGHT; return Mummy::Direction::EAST;
case WhipAttack::Direction::WEST: case WhipAttack::Direction::WEST:
return Mummy::Direction::LEFT; return Mummy::Direction::WEST;
} }
return Mummy::Direction::NONE; return Mummy::Direction::NONE;
} }
+5 -4
View File
@@ -155,10 +155,11 @@ class Entity {
// el sprite) amb el tile. Nullopt si l'entitat no en té. // el sprite) amb el tile. Nullopt si l'entitat no en té.
[[nodiscard]] auto getCollisionBox() const -> const std::optional<CollisionBox>& { return collision_box_; } [[nodiscard]] auto getCollisionBox() const -> const std::optional<CollisionBox>& { return collision_box_; }
// Flip del sprite. Les entitats estàtiques mai no el toquen (queda a // Flip del sprite, propagat des del motor d'animació
// SDL_FLIP_NONE); les dinàmiques que miren cap als costats (p. ex. // (`AnimationComponent::flipMode()`): cap entitat el decideix per
// Mummy en direcció horitzontal) el canvien quan gira. El manager // direcció — el YAML declara `flip_h: true` a l'animació simètrica
// el passa a Surface::render. // (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_; } [[nodiscard]] auto getFlip() const -> SDL_FlipMode { return flip_; }
void setFlip(SDL_FlipMode flip) { flip_ = flip; } void setFlip(SDL_FlipMode flip) { flip_ = flip; }
+73 -119
View File
@@ -203,22 +203,21 @@ void Player::readWhipBlock(const fkyaml::node& yaml) {
} }
void Player::applyCardinal(Cardinal c) { 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) { switch (c) {
case Cardinal::NORTH: case Cardinal::NORTH:
facing_ = Facing::UP; facing_ = Facing::NORTH;
setFlip(SDL_FLIP_NONE);
break; break;
case Cardinal::SOUTH: case Cardinal::SOUTH:
facing_ = Facing::DOWN; facing_ = Facing::SOUTH;
setFlip(SDL_FLIP_NONE);
break; break;
case Cardinal::EAST: case Cardinal::EAST:
facing_ = Facing::SIDE; facing_ = Facing::EAST;
setFlip(SDL_FLIP_NONE);
break; break;
case Cardinal::WEST: case Cardinal::WEST:
facing_ = Facing::SIDE; facing_ = Facing::WEST;
setFlip(SDL_FLIP_HORIZONTAL);
break; break;
case Cardinal::NONE: case Cardinal::NONE:
break; break;
@@ -236,10 +235,10 @@ auto Player::cardinalFromInput(float vx, float vy) -> Cardinal {
} }
auto Player::cardinalFromString(const std::string& s) -> Cardinal { auto Player::cardinalFromString(const std::string& s) -> Cardinal {
if (s == "north" || s == "up") { return Cardinal::NORTH; } if (s == "north") { return Cardinal::NORTH; }
if (s == "south" || s == "down") { return Cardinal::SOUTH; } if (s == "south") { return Cardinal::SOUTH; }
if (s == "east" || s == "right") { return Cardinal::EAST; } if (s == "east") { return Cardinal::EAST; }
if (s == "west" || s == "left") { return Cardinal::WEST; } if (s == "west") { return Cardinal::WEST; }
throw std::runtime_error("Player: cardinal desconegut '" + s + "' al bloc turn.transitions"); throw std::runtime_error("Player: cardinal desconegut '" + s + "' al bloc turn.transitions");
} }
@@ -313,17 +312,14 @@ void Player::updateFacingAndTurn(float delta_time, float vx, float vy) {
turning_remaining_ms_ = static_cast<float>(turn_config.duration_ms); turning_remaining_ms_ = static_cast<float>(turn_config.duration_ms);
applyCardinal(intermediate_cardinal_); applyCardinal(intermediate_cardinal_);
} else if (vx != 0.0F) { } else if (vx != 0.0F) {
// Tria de facing original: prevalença lateral, flip segons el // Tria de facing: prevalença lateral, EAST/WEST segons el signe
// signe de vx; verticals purs van a UP/DOWN amb flip NONE; en // de vx; verticals purs van a NORTH/SOUTH. En aturar-se
// aturar-se conservem `facing_` per a l'animació stand. // conservem `facing_` per a l'animació stand.
facing_ = Facing::SIDE; facing_ = (vx < 0.0F) ? Facing::WEST : Facing::EAST;
setFlip(vx < 0.0F ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE);
} else if (vy < 0.0F) { } else if (vy < 0.0F) {
facing_ = Facing::UP; facing_ = Facing::NORTH;
setFlip(SDL_FLIP_NONE);
} else if (vy > 0.0F) { } else if (vy > 0.0F) {
facing_ = Facing::DOWN; facing_ = Facing::SOUTH;
setFlip(SDL_FLIP_NONE);
} }
} }
@@ -342,11 +338,11 @@ Player::Player(const std::string& anim_key, int id, float x, float y, const Cont
paint_tile_index_(paint_tile_index) { paint_tile_index_(paint_tile_index) {
setPos(x, y); setPos(x, y);
// Postura inicial mirant cap al jugador (cara avall), no de costat: // Postura inicial mirant cap al jugador (cara avall), no de costat:
// forcem facing_ a DOWN i l'animació `stand_down` abans del primer // forcem facing_ a SOUTH i l'animació `stand_south` abans del primer
// frame d'input — el primer `update` la preservarà si l'usuari no // frame d'input — el primer `update` la preservarà si l'usuari no
// prem cap tecla. // prem cap tecla.
facing_ = Facing::DOWN; facing_ = Facing::SOUTH;
setCurrentAnimation("stand_down"); setCurrentAnimation("stand_south");
} }
void Player::update(float delta_time) { void Player::update(float delta_time) {
@@ -650,16 +646,16 @@ auto Player::consumeAttackEdge() -> bool {
auto Player::currentFacingCardinal() const -> Cardinal { auto Player::currentFacingCardinal() const -> Cardinal {
switch (facing_) { switch (facing_) {
case Facing::UP: case Facing::NORTH:
return Cardinal::NORTH; return Cardinal::NORTH;
case Facing::DOWN: case Facing::SOUTH:
return Cardinal::SOUTH; return Cardinal::SOUTH;
case Facing::SIDE: 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 return Cardinal::EAST;
// (convenció estable a tots els assets actuals).
return (getFlip() == SDL_FLIP_HORIZONTAL) ? Cardinal::WEST : Cardinal::EAST;
} }
void Player::enterAttacking(Cardinal facing) { void Player::enterAttacking(Cardinal facing) {
@@ -725,7 +721,7 @@ void Player::startJump() {
// Un gir de 180° en curs es compromet immediatament: si l'usuari // Un gir de 180° en curs es compromet immediatament: si l'usuari
// ha disparat un giro (oest→est, p.ex.) i salta abans que s'esgote // ha disparat un giro (oest→est, p.ex.) i salta abans que s'esgote
// el timer, l'animació del salt ha de mostrar la direcció final // el timer, l'animació del salt ha de mostrar la direcció final
// (jump lateral), no la intermèdia (jump_down). Aplem el pending // (jump lateral), no la intermèdia (jump_south). Aplem el pending
// ara mateix i resetem l'estat del gir. Si no hi havia gir en // ara mateix i resetem l'estat del gir. Si no hi havia gir en
// curs, és no-op. // curs, és no-op.
if (turning_remaining_ms_ > 0.0F) { if (turning_remaining_ms_ > 0.0F) {
@@ -1106,91 +1102,50 @@ void Player::renderShadow() const {
Screen::get().blendRect(SX, SY, SHADOW_W, SHADOW_H, SHADOW_ARGB, SHADOW_ALPHA); Screen::get().blendRect(SX, SY, SHADOW_W, SHADOW_H, SHADOW_ARGB, SHADOW_ALPHA);
} }
auto Player::animationName(bool moving) const -> const char* { // Sufix d'animació per cardinal — empat 1:1 amb l'enum `Facing` i el
// En l'aire forcem la variant `jump_*` (per direcció), independent de // sufix declarat als YAMLs de jugador (sam.yaml, morcus.yaml).
// si hi ha velocitat horitzontal: per al jugador, l'estat visual del auto Player::facingSuffix(Facing facing) -> const char* {
// salt és per direcció, no per velocitat. switch (facing) {
if (state_ == State::AIRBORNE) { case Facing::NORTH:
switch (facing_) { return "_north";
case Facing::UP: case Facing::SOUTH:
return "jump_up"; return "_south";
case Facing::DOWN: case Facing::EAST:
return "jump_down"; return "_east";
case Facing::SIDE: case Facing::WEST:
break; return "_west";
}
return "jump";
} }
// En LANDING_STUN, `stun_*` (per direcció): mostrem el frame return "_east";
// 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) { // Prefix d'animació per estat. ATTACKING té dues sub-fases: durant
switch (facing_) { // `swing_ms` és `attack_*` (animació del braç llançant el látigo); durant
case Facing::UP: // `cooldown_ms` posterior, `recover_*`. A AIRBORNE/LANDING_STUN/HURT, la
return "stun_up"; // variant és per direcció, no per velocitat: l'estat bloqueja el `moving`
case Facing::DOWN: // argument i no fa servir walk/stand.
return "stun_down"; auto Player::statePrefix(State state, bool moving, bool in_attack_swing) -> const char* {
case Facing::SIDE: switch (state) {
break; case State::AIRBORNE:
} return "jump";
return "stun"; case State::LANDING_STUN:
} return "stun";
// En HURT, `hurt_*` (per direcció): el jugador llisca sense case State::HURT:
// control en la direcció triada per `enterHurt`. Mateixa estructura return "hurt";
// que walk per facilitat (ara mateix els frames són clons). case State::ATTACKING:
if (state_ == State::HURT) { return in_attack_swing ? "attack" : "recover";
switch (facing_) { case State::GROUNDED:
case Facing::UP: case State::EXITED:
return "hurt_up"; case State::DISABLED:
case Facing::DOWN:
return "hurt_down";
case Facing::SIDE:
break;
}
return "hurt";
}
// 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<float>(whip_config.cooldown_ms));
if (IN_SWING) {
switch (facing_) {
case Facing::UP:
return "attack_up";
case Facing::DOWN:
return "attack_down";
case Facing::SIDE:
break;
}
return "attack";
}
switch (facing_) {
case Facing::UP:
return "recover_up";
case Facing::DOWN:
return "recover_down";
case Facing::SIDE:
break;
}
return "recover";
}
switch (facing_) {
case Facing::UP:
return moving ? "walk_up" : "stand_up";
case Facing::DOWN:
return moving ? "walk_down" : "stand_down";
case Facing::SIDE:
break; break;
} }
return moving ? "walk" : "stand"; return moving ? "walk" : "stand";
} }
auto Player::animationName(bool moving) const -> std::string {
const bool IN_ATTACK_SWING = (attacking_remaining_ms_ > static_cast<float>(whip_config.cooldown_ms));
return std::string(statePrefix(state_, moving, IN_ATTACK_SWING)) + facingSuffix(facing_);
}
auto Player::getCurrentTile() const -> std::pair<int, int> { auto Player::getCurrentTile() const -> std::pair<int, int> {
// El grid del tilemap és de `tile_size` píxels i està 16-alineat // El grid del tilemap és de `tile_size` píxels i està 16-alineat
// respecte a l'origen del tilemap (pot ser negatiu per overscan). El // respecte a l'origen del tilemap (pot ser negatiu per overscan). El
@@ -1249,11 +1204,10 @@ void Player::respawn(float x, float y) {
intermediate_cardinal_ = Cardinal::NONE; intermediate_cardinal_ = Cardinal::NONE;
last_input_cardinal_ = Cardinal::NONE; last_input_cardinal_ = Cardinal::NONE;
// Reseteig del facing al d'spawn original (mirant cap a la càmera): // Reseteig del facing al d'spawn original (mirant cap a la càmera):
// sense això, un EXITED que entrava per la porta amb facing UP // 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. // re-aparix d'esquena al pròxim cicle. Mateixa lògica que el ctor.
facing_ = Facing::DOWN; facing_ = Facing::SOUTH;
setFlip(SDL_FLIP_NONE); setCurrentAnimation("stand_south");
setCurrentAnimation("stand_down");
setPosX(x); setPosX(x);
setPosY(y); setPosY(y);
} }
@@ -1300,10 +1254,10 @@ void Player::spawnFromAir(float x, float y) {
intended_vy_ = 0.0F; intended_vy_ = 0.0F;
// Mirant cap avall durant la caiguda — `animationName(AIRBORNE)` amb // Mirant cap avall durant la caiguda — `animationName(AIRBORNE)` amb
// facing DOWN ja resol a `jump_down`. `respawn` ja ha posat facing a // facing SOUTH ja resol a `jump_south`. `respawn` ja ha posat facing a
// DOWN; ho deixem explícit ací per claredat de la cinemàtica. // SOUTH; ho deixem explícit ací per claredat de la cinemàtica.
facing_ = Facing::DOWN; facing_ = Facing::SOUTH;
setCurrentAnimation("jump_down"); setCurrentAnimation("jump_south");
} }
auto Player::getBaseBox() const -> SDL_FRect { auto Player::getBaseBox() const -> SDL_FRect {
+24 -17
View File
@@ -351,9 +351,12 @@ class Player : public AnimatedSprite {
void snapToHorizontalLane(float delta_time); void snapToHorizontalLane(float delta_time);
void snapToVerticalLane(float delta_time); void snapToVerticalLane(float delta_time);
// Nom d'animació per al `facing_` actual. Si `moving` és cert s'usa // Nom d'animació per al `facing_` actual. Composat com a `prefix +
// la variant `walk*`, altrament la variant `stand*`. // suffix` via els dos helpers estàtics (declarats junt amb `Facing`
[[nodiscard]] auto animationName(bool moving) const -> const char*; // 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( void snapToLane(
float delta_time, float delta_time,
float base, float base,
@@ -428,19 +431,24 @@ class Player : public AnimatedSprite {
// reinstanciar-lo. // reinstanciar-lo.
Map::Info map_; Map::Info map_;
// Direcció "cap on mira" el jugador. Governa quin conjunt d'animacions // Direcció "cap on mira" el jugador. Una entrada per cada cardinal: el
// s'aplica (lateral, amunt, avall) i si el sprite es flipeja o no. Els // codi del joc no toca mai el flip — l'oest reprodueix `*_west`, que al
// moviments purament horitzontals o diagonals prenen `SIDE` (prevalença // YAML declara `flip_h: true`, i és el motor d'animació qui aplica el
// lateral), els purament verticals fan `UP`/`DOWN`. En aturar-se es // flip al sprite. Els moviments diagonals prenen el component lateral
// preserva l'última orientació perquè la versió stand coincideixi. // (EAST/WEST); en aturar-se es preserva l'última orientació perquè la
enum class Facing : std::uint8_t { SIDE, // versió stand coincideixi.
UP, enum class Facing : std::uint8_t { EAST,
DOWN }; WEST,
Facing facing_{Facing::SIDE}; 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°. // 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 // Pública perquè el `WhipAttack` la passa a `enterHurt` quan colpeja
// un altre jugador (friendly fire). // un altre jugador (friendly fire).
public: public:
@@ -552,9 +560,8 @@ class Player : public AnimatedSprite {
// timer `attacking_remaining_ms_` a `swing_ms + cooldown_ms`. // timer `attacking_remaining_ms_` a `swing_ms + cooldown_ms`.
void enterAttacking(Cardinal facing); void enterAttacking(Cardinal facing);
// Cardinal que correspon al `(facing_, flip_)` actual del sprite — // Cardinal corresponent al `facing_` actual del sprite — la direcció
// la direcció cap on mira el jugador. Si `facing_ == SIDE` el // cap on mira el jugador. Mapeig 1:1 amb `Facing` (mai NONE).
// resultat depèn del flip; UP/DOWN són trivials. Mai retorna NONE.
[[nodiscard]] auto currentFacingCardinal() const -> Cardinal; [[nodiscard]] auto currentFacingCardinal() const -> Cardinal;
// Activa l'estat HURT: el jugador queda projectat amb velocitat // Activa l'estat HURT: el jugador queda projectat amb velocitat
+207
View File
@@ -0,0 +1,207 @@
// Smoke test del parser d'animacions amb suport de flip H/V. Stand-alone:
// només enllaça `animated_sprite_parser.cpp` (la part del motor que NO toca
// Resource::Cache ni Surface), perquè la cobertura interessa el contracte
// de càrrega del YAML, no el render real.
//
// Tests coberts:
// 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 }`
// en una animació sense flip → només aquest frame
// té el flag.
// T4 mapping no hereta — animació amb `flip_h: true` però un frame
// `{ index: N }` (mapping sense flip) → flip_h=false
// en aquest frame (el mapping declara el seu propi
// estat, sense heretar).
// T5 combinat V+H — frame `{ index, flip_h: true, flip_v: true }` →
// ambdós flags actius.
#include <cstdio>
#include <iostream>
#include <string>
#include "core/rendering/sprite/animated_sprite.hpp"
#include "external/fkyaml_node.hpp"
namespace {
int g_failures = 0;
void check(bool cond, const char* msg, const char* file, int line) {
if (!cond) {
std::cerr << " FAIL: " << msg << " (" << file << ":" << line << ")\n";
++g_failures;
}
}
#define REQUIRE(cond) check((cond), #cond, __FILE__, __LINE__)
// Paràmetres comuns per a tots els tests: spritesheet hipotètic 64×64
// amb tiles de 16×16 → grid 4×4, max_tiles=16.
auto sheetParams() -> AnimatedSprite::SheetParams {
return AnimatedSprite::SheetParams{
.frame_width = 16.0F,
.frame_height = 16.0F,
.frames_per_row = 4,
.max_tiles = 16,
};
}
auto parse(const std::string& yaml_str) {
return AnimatedSprite::parseAnimationsFromYaml(
fkyaml::node::deserialize(yaml_str), sheetParams());
}
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 }
stand: { frames: [4] }
)";
const auto ANIMS = parse(YAML);
REQUIRE(ANIMS.size() == 2);
const auto WALK_IT = ANIMS.find("walk");
REQUIRE(WALK_IT != ANIMS.end());
if (WALK_IT != ANIMS.end()) {
const auto& WALK = WALK_IT->second;
REQUIRE(WALK.frames.size() == 4);
for (const auto& F : WALK.frames) {
REQUIRE(!F.flip_h);
REQUIRE(!F.flip_v);
}
}
const auto STAND_IT = ANIMS.find("stand");
REQUIRE(STAND_IT != ANIMS.end());
if (STAND_IT != ANIMS.end()) {
const auto& STAND = STAND_IT->second;
REQUIRE(STAND.frames.size() == 1);
REQUIRE(!STAND.frames[0].flip_h);
REQUIRE(!STAND.frames[0].flip_v);
}
}
void testFlipAtAnimationLevel() {
std::cout << "[T2] flip a nivell d'anim hereten frames int\n";
const std::string YAML = R"(
animations:
walk_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
)";
const auto ANIMS = parse(YAML);
const auto IT = ANIMS.find("walk_west");
REQUIRE(IT != ANIMS.end());
if (IT != ANIMS.end()) {
const auto& A = IT->second;
REQUIRE(A.frames.size() == 4);
for (const auto& F : A.frames) {
REQUIRE(F.flip_h);
REQUIRE(!F.flip_v);
}
}
}
void testOverridePerFrame() {
std::cout << "[T3] override per-frame en animació sense flip\n";
const std::string YAML = R"(
animations:
death_spin:
frames:
- 0
- 4
- 8
- { index: 8, flip_h: true }
frame_ms: 100
)";
const auto ANIMS = parse(YAML);
const auto IT = ANIMS.find("death_spin");
REQUIRE(IT != ANIMS.end());
if (IT != ANIMS.end()) {
const auto& A = IT->second;
REQUIRE(A.frames.size() == 4);
// Els tres primers (int, sense flip a l'anim) → tot a false.
REQUIRE(!A.frames[0].flip_h);
REQUIRE(!A.frames[0].flip_v);
REQUIRE(!A.frames[1].flip_h);
REQUIRE(!A.frames[1].flip_v);
REQUIRE(!A.frames[2].flip_h);
REQUIRE(!A.frames[2].flip_v);
// El quart (mapping amb flip_h: true) → només flip_h.
REQUIRE(A.frames[3].flip_h);
REQUIRE(!A.frames[3].flip_v);
}
}
void testMappingDoesNotInherit() {
std::cout << "[T4] mapping NO hereta el flip de l'animació\n";
const std::string YAML = R"(
animations:
mixed:
frames:
- 0
- { index: 4 }
frame_ms: 100
flip_h: true
)";
const auto ANIMS = parse(YAML);
const auto IT = ANIMS.find("mixed");
REQUIRE(IT != ANIMS.end());
if (IT != ANIMS.end()) {
const auto& A = IT->second;
REQUIRE(A.frames.size() == 2);
// Frame int hereta el flip_h true de l'animació.
REQUIRE(A.frames[0].flip_h);
REQUIRE(!A.frames[0].flip_v);
// Frame mapping declara els seus propis flags (cap declarat
// → false) i NO hereta del flip a nivell d'anim.
REQUIRE(!A.frames[1].flip_h);
REQUIRE(!A.frames[1].flip_v);
}
}
void testCombinedVerticalAndHorizontal() {
std::cout << "[T5] flip combinat V+H per-frame\n";
const std::string YAML = R"(
animations:
upside_down:
frames:
- { index: 5, flip_h: true, flip_v: true }
frame_ms: 100
)";
const auto ANIMS = parse(YAML);
const auto IT = ANIMS.find("upside_down");
REQUIRE(IT != ANIMS.end());
if (IT != ANIMS.end()) {
const auto& A = IT->second;
REQUIRE(A.frames.size() == 1);
REQUIRE(A.frames[0].flip_h);
REQUIRE(A.frames[0].flip_v);
}
}
} // namespace
auto main() -> int {
std::cout << "== smoke tests: animation flip parser ==\n";
testDefaultNoFlip();
testFlipAtAnimationLevel();
testOverridePerFrame();
testMappingDoesNotInherit();
testCombinedVerticalAndHorizontal();
if (g_failures == 0) {
std::cout << "ALL OK\n";
return 0;
}
std::cerr << g_failures << " smoke check(s) failed\n";
return 1;
}
+2 -2
View File
@@ -444,7 +444,7 @@ namespace {
// Direcció preferida: dreta. Mòmia ha d'entrar en HURT i avançar // Direcció preferida: dreta. Mòmia ha d'entrar en HURT i avançar
// a la dreta (la velocitat de hurt és més alta que la base). // a la dreta (la velocitat de hurt és més alta que la base).
mummy.enterHurt(Mummy::Direction::RIGHT); mummy.enterHurt(Mummy::Direction::EAST);
REQUIRE(mummy.getState() == Mummy::State::HURT); REQUIRE(mummy.getState() == Mummy::State::HURT);
REQUIRE(mummy.getDirX() > 0.0F); REQUIRE(mummy.getDirX() > 0.0F);
@@ -488,7 +488,7 @@ namespace {
// timer. Aquesta variant del test NO comprova la sortida // timer. Aquesta variant del test NO comprova la sortida
// per col·lisió; només verifica que entrar a HURT a la // per col·lisió; només verifica que entrar a HURT a la
// paret no peta. // paret no peta.
mummy.enterHurt(Mummy::Direction::RIGHT); mummy.enterHurt(Mummy::Direction::EAST);
REQUIRE(mummy.getState() == Mummy::State::HURT); REQUIRE(mummy.getState() == Mummy::State::HURT);
// Update llarg → expira el timer i torna a ROAMING. // Update llarg → expira el timer i torna a ROAMING.
mummy.update(1.0F); mummy.update(1.0F);