From 74dad71cc2009d9b29788c0b5a38e17e4cd27fae Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 24 Apr 2026 13:49:17 +0200 Subject: [PATCH] =?UTF-8?q?feat(player):=20gir=20de=20180=C2=B0=20amb=20po?= =?UTF-8?q?se=20interm=C3=A8dia=20configurable=20al=20YAML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/player/player.gif | Bin 1802 -> 0 bytes data/player/player.yaml | 37 ------- data/player/sam.yaml | 12 +++ source/game/entities/player.cpp | 173 ++++++++++++++++++++++++++++---- source/game/entities/player.hpp | 49 ++++++++- 5 files changed, 215 insertions(+), 56 deletions(-) delete mode 100644 data/player/player.gif delete mode 100644 data/player/player.yaml diff --git a/data/player/player.gif b/data/player/player.gif deleted file mode 100644 index ab20f75034cb44e20614809e9d53770255b35f90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1802 zcmXw&dr;GN62RB`2#Okf0TD_Nkm#X_f*26&4~&pjlwd$X5JD&qOAf+wf(IBPLZl!; zUO@pvOV02xM5T(zd7;WfN=Z23go-2rN`{y8v^Eu|`CfFUGrP0@>>r=~>@NK)UpFdy z4}1xF0ECcUuU}kT)ai6ut#)K&M6Fi$_Vy~3%C@#Pg+d{d$s`hqSS%KaL_(pE&*$@a zJT8|T7Z=B3v4Vqx84L!UPRB5eN~KaL6cUL} z6eL#)z9~M-&QA$F7tc(OI+752C??P&+|MP5=5*B4f#zbR%=Bwbr8jeF^&G7FYY!2N zDhzhv2U2(pCk~y&qS*&xL_ew(MzL^l+T>)9M{`;!H{8CF&XknaLyLe36QW6VoV`?C7b}@ zAx6@#e&4+CWe<9Xc6u^zQvK?jX(hWob9T5U+3$|DaSR~Xcx|@WgU8#h!QoeEP($y_ za{1$&3OOgMG$fjL_(2#>42g&8As0gK|A|?~<-HR+OjWov40jL1(0Mr0n0N?;H{}M(8ApPYG zXIzPzuWsYENmvpgL&(Lr!IWT(h>`H5ZOGK$Cn%8S9pK|k-m`p}@m{yRU$J_PvF`h4DZD~*CF2cj2oyphzz};@9s63agsy#?F3=@WR{?~j3j@y^abpQF@L}+(uDQzaG zIRJNLV9kp&fr?`tuh%-Aj0Jz{Or5P9}f7? zH(GvpH{bHl2~DxX&eB3HIb-p4t)4os>CDw%Ul_}qc@vOE0IikLYU6!6ZTFRIr+XsD|JFY=_RQ@Km1SSSj zqyT@&Eh5+Kz(uq3o1@Ou<-zHc)!R*uPi_-_R*+HPWpm!Gq+rLnjPC?C*W3b%b~lZF zU$jr<7A5qk9JN11{cLA+p!<4hKp;K8!l87%AZ^1DLhbN|ul!TeF8eVf8lN1s{l%e7 z?3&zIvEIF;eXuH~@|U2iv37m2Cu6Vo1=K{}C+M!GEtSUwWzP0puT7afB@0-Y>^fbS zVfRzO>B6QhA@$KS=H_hfy6F*i%Bic3?7VEpD>8AyN!q${gKK=_+1hU#8>KDxpEfix zQDsw;d|)dlPSFz|eCB3p(^kbzRZ?j9gE!sJ*LF9Vhuyl_STUcdT9s*i`^kE}&Dz1$ zxaQ=i55)c6}efzPm8Z*FQoIhIr=x^ zI^~C!UfZ1CH2QJgvI7k)o|*IMiSzisT_kpGO1-zIdsCv@-aJF8Tk_-0BbJlJn{Pj^ zzXT06lZANmKKo+w?3b-Y-l{BT)uEp}UzRvKrA^oPZ9TQ`@P;eDIg}qs-M8JB`9(N$ zIP}>M%&Mr+?6Rl6mG0v;5iar5BdhZUTD)Q<2OUE=W#e~8neBady`oj)cgFqOL++^!X35c}aa%h)77{CeZP&JJntwVY z)x6NS@)IvSnUjyx=Gn;e=gp4Kv%ALWiU*y`$exrx5cB!9uGyh}#fM>!EJL<|snGOT zms%@&?OV~m43-b8iZ#ny3w;_g$G=-~ktwxXHT|=|HU5Rr{14{~gFAdT-XN@si>+Um Nk`(LOaoh{Q(); } + if (t.contains("transitions")) { + const auto& tr = t["transitions"]; + if (tr.contains("west_to_east")) { turn_config.w_to_e = cardinalFromString(tr["west_to_east"].get_value()); } + if (tr.contains("east_to_west")) { turn_config.e_to_w = cardinalFromString(tr["east_to_west"].get_value()); } + if (tr.contains("north_to_south")) { turn_config.n_to_s = cardinalFromString(tr["north_to_south"].get_value()); } + if (tr.contains("south_to_north")) { turn_config.s_to_n = cardinalFromString(tr["south_to_north"].get_value()); } + } + } +} + +void Player::applyCardinal(Cardinal c) { + switch (c) { + case Cardinal::NORTH: + facing_ = Facing::UP; + setFlip(SDL_FLIP_NONE); + break; + case Cardinal::SOUTH: + facing_ = Facing::DOWN; + setFlip(SDL_FLIP_NONE); + break; + case Cardinal::EAST: + facing_ = Facing::SIDE; + setFlip(SDL_FLIP_NONE); + break; + case Cardinal::WEST: + facing_ = Facing::SIDE; + setFlip(SDL_FLIP_HORIZONTAL); + break; + case Cardinal::NONE: + break; + } +} + +auto Player::cardinalFromInput(float vx, float vy) -> Cardinal { + // Mateixa precedència que la lògica original: l'horitzontal preval + // sobre el vertical (els diagonals s'interpreten com a laterals). + if (vx > 0.0F) { return Cardinal::EAST; } + if (vx < 0.0F) { return Cardinal::WEST; } + if (vy < 0.0F) { return Cardinal::NORTH; } + if (vy > 0.0F) { return Cardinal::SOUTH; } + return Cardinal::NONE; +} + +auto Player::cardinalFromString(const std::string& s) -> Cardinal { + if (s == "north" || s == "up") { return Cardinal::NORTH; } + if (s == "south" || s == "down") { return Cardinal::SOUTH; } + if (s == "east" || s == "right") { return Cardinal::EAST; } + if (s == "west" || s == "left") { return Cardinal::WEST; } + throw std::runtime_error("Player: cardinal desconegut '" + s + "' al bloc turn.transitions"); +} + +auto Player::opposite(Cardinal c) -> Cardinal { + switch (c) { + case Cardinal::NORTH: + return Cardinal::SOUTH; + case Cardinal::SOUTH: + return Cardinal::NORTH; + case Cardinal::EAST: + return Cardinal::WEST; + case Cardinal::WEST: + return Cardinal::EAST; + case Cardinal::NONE: + return Cardinal::NONE; + } + return Cardinal::NONE; +} + +auto Player::intermediateFor(Cardinal target) -> Cardinal { + switch (target) { + case Cardinal::EAST: + return turn_config.w_to_e; + case Cardinal::WEST: + return turn_config.e_to_w; + case Cardinal::SOUTH: + return turn_config.n_to_s; + case Cardinal::NORTH: + return turn_config.s_to_n; + case Cardinal::NONE: + return Cardinal::NONE; + } + return Cardinal::NONE; +} + +void Player::updateFacingAndTurn(float delta_time, float vx, float vy) { + // Cardinal d'INPUT cru. Important: la detecció del gir de 180° s'ha + // de basar en l'input (intent del jugador), no en la velocitat + // resultant. Mentre no hi haja inèrcia, vx/vy són directament l'input + // i serveixen com a tal; quan s'introduïsca, cal preservar aquesta + // separació (calcular el cardinal a partir de les tecles/gamepad + // crus, no de la velocitat suavitzada). + const Cardinal INPUT_CARD = cardinalFromInput(vx, vy); + + // Si hi ha un gir en curs, descomptem el timer i mantenim el facing + // forçat al cardinal intermedi. Quan s'esgota, apliquem el cardinal + // pendent. El moviment físic continua sense alterar-se (vx/vy ja + // s'han enviat a setVel al caller). + if (turning_remaining_ms_ > 0.0F) { + turning_remaining_ms_ -= delta_time * 1000.0F; + if (turning_remaining_ms_ <= 0.0F) { + turning_remaining_ms_ = 0.0F; + applyCardinal(pending_cardinal_); + pending_cardinal_ = Cardinal::NONE; + intermediate_cardinal_ = Cardinal::NONE; + } else { + applyCardinal(intermediate_cardinal_); + } + } else { + // Detecció del gir: `last_input_cardinal_` és l'últim cardinal + // d'input no nul (es preserva entre releases). Així, el primer + // input des de l'estat idle no dispara cap gir — només dispara + // quan el jugador "canvia d'opinió" prement la direcció oposada + // a la prèvia. Requereix també que el bloc `turn:` del YAML + // estiga activat (`duration_ms > 0`). + const bool TRIGGER_TURN = + (turn_config.duration_ms > 0) && INPUT_CARD != Cardinal::NONE && last_input_cardinal_ != Cardinal::NONE && INPUT_CARD == opposite(last_input_cardinal_); + if (TRIGGER_TURN) { + pending_cardinal_ = INPUT_CARD; + intermediate_cardinal_ = intermediateFor(INPUT_CARD); + 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 UP/DOWN amb flip NONE; en + // aturar-se conservem `facing_` per a l'animació stand. + facing_ = Facing::SIDE; + setFlip(vx < 0.0F ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE); + } else if (vy < 0.0F) { + facing_ = Facing::UP; + setFlip(SDL_FLIP_NONE); + } else if (vy > 0.0F) { + facing_ = Facing::DOWN; + setFlip(SDL_FLIP_NONE); + } + } + + // Memoritzem l'últim cardinal d'input no nul per a la propera detecció + // de gir (ignorant els zeros: l'estat idle no reinicia la memòria). + if (INPUT_CARD != Cardinal::NONE) { + last_input_cardinal_ = INPUT_CARD; + } } Player::Player(const std::string& anim_key, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index) @@ -106,22 +255,7 @@ void Player::update(float delta_time) { setVelX(vx); setVelY(vy); - // Tria de facing amb prevalença lateral: si hi ha component horitzontal - // (inclòs el diagonal), l'animació és lateral i el flip segueix el signe - // de `vx`. Només en moviment purament vertical passem a UP/DOWN, i en - // aquest cas el flip es força a NONE perquè els sprites frontals no - // s'han d'espillar segons l'últim lateral. En aturar-se conservem - // `facing_` per a l'animació stand corresponent. - if (vx != 0.0F) { - facing_ = Facing::SIDE; - setFlip(vx < 0.0F ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE); - } else if (vy < 0.0F) { - facing_ = Facing::UP; - setFlip(SDL_FLIP_NONE); - } else if (vy > 0.0F) { - facing_ = Facing::DOWN; - setFlip(SDL_FLIP_NONE); - } + updateFacingAndTurn(delta_time, vx, vy); // Snap al passadís: si només hi ha moviment horitzontal (o vertical) i // la caixa està prou prop d'una lane perpendicular (≤ mitja caixa), la @@ -156,9 +290,12 @@ void Player::update(float delta_time) { // Animació basada en moviment real: si la col·lisió (o l'absència // d'input) ha deixat el jugador al mateix lloc que al principi del - // frame, queda en `stand` del facing actual. + // frame, queda en `stand` del facing actual. Durant un gir de 180° + // forcem també `stand` encara que el jugador s'estiga movent — la + // pose intermèdia és sempre estàtica per disseny. const bool MOVED = (getPosX() != FRAME_START_X) || (getPosY() != FRAME_START_Y); - setCurrentAnimation(animationName(MOVED)); + const bool ANIM_AS_MOVING = MOVED && (turning_remaining_ms_ <= 0.0F); + setCurrentAnimation(animationName(ANIM_AS_MOVING)); // Detecció de canvi de tile. Es calcula *després* del clamp i de les // col·lisions perquè la posició aquí ja és la definitiva del frame; diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index 77790f0..89ff0d7 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -23,7 +23,8 @@ class Player : public AnimatedSprite { SDL_Scancode right; }; - // Geometria carregada des de `data/player/player.yaml`. `sprite_*` són + // Geometria carregada des del YAML del player (p.ex. `data/player/sam.yaml`). + // `sprite_*` són // la mida del frame (sprite 12×24 amb perspectiva falsa: visualment, els // 12 píxels superiors són el cap i els 12 inferiors la base). // La caixa de col·lisió és un quadrat `collision_size` concèntric amb el @@ -121,6 +122,52 @@ class Player : public AnimatedSprite { DOWN }; Facing facing_{Facing::SIDE}; + // 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. + enum class Cardinal : std::uint8_t { NONE, + NORTH, + SOUTH, + EAST, + WEST }; + + // Configuració del gir de 180°, llegida del bloc `turn:` del YAML del + // player. Si `duration_ms` és 0 (o el bloc no existeix) la mecànica + // queda desactivada i els canvis de direcció són instantanis. + struct TurnConfig { + int duration_ms{0}; + Cardinal w_to_e{Cardinal::SOUTH}; + Cardinal e_to_w{Cardinal::NORTH}; + Cardinal n_to_s{Cardinal::EAST}; + Cardinal s_to_n{Cardinal::WEST}; + }; + // Definida out-of-class al .cpp: dins de la classe, els initializers + // per defecte de TurnConfig encara no són visibles per al compilador. + static TurnConfig turn_config; + + // Estat del gir en curs. Mentre `turning_remaining_ms_ > 0`, el facing/ + // flip es manté forçat a `intermediate_cardinal_` i l'animació es força + // a `stand` encara que el moviment continuï. En esgotar-se el timer, + // s'aplica `pending_cardinal_`. `last_input_cardinal_` recorda l'últim + // cardinal d'input no nul: serveix per disparar el gir només quan + // l'usuari canvia d'opinió, no la primera vegada que prem una tecla + // des de l'estat idle. + float turning_remaining_ms_{0.0F}; + Cardinal pending_cardinal_{Cardinal::NONE}; + Cardinal intermediate_cardinal_{Cardinal::NONE}; + Cardinal last_input_cardinal_{Cardinal::NONE}; + + // Aplica un cardinal a (`facing_`, flip). NONE no fa res — preserva el + // facing actual (cas idle: l'última orientació es manté). + void applyCardinal(Cardinal c); + // Actualitza facing_/flip i l'estat del gir a partir de l'input del frame. + // Extret de `update` per acotar la complexitat cognitiva (clang-tidy). + void updateFacingAndTurn(float delta_time, float vx, float vy); + static auto cardinalFromInput(float vx, float vy) -> Cardinal; + static auto cardinalFromString(const std::string& s) -> Cardinal; + static auto opposite(Cardinal c) -> Cardinal; + static auto intermediateFor(Cardinal target) -> Cardinal; + // Bus del nivell (opcional). Propietat externa (EntityManager); el // Player només en guarda un punter per publicar events. EventBus* bus_{nullptr};