From a3592d6c73667923063e514836801365bc0d84d5 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Tue, 21 Apr 2026 20:41:49 +0200 Subject: [PATCH] =?UTF-8?q?feat(player):=20animacions=20direccionals=20i?= =?UTF-8?q?=20stand=20autom=C3=A0tic=20en=20col=C2=B7lisi=C3=B3;=20v0.18.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/player-animations.md | 78 +++++++++++++++++++++++++++++++++ source/game/entities/player.cpp | 50 +++++++++++++++++---- source/game/entities/player.hpp | 15 +++++++ source/utils/defines.hpp | 2 +- 4 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 docs/player-animations.md diff --git a/docs/player-animations.md b/docs/player-animations.md new file mode 100644 index 0000000..4ae4581 --- /dev/null +++ b/docs/player-animations.md @@ -0,0 +1,78 @@ +# Animacions i direccionalitat del jugador + +Documenta els afegits d'aquesta sessió a `source/game/entities/player.*`: sis +animacions direccionals amb `facing_` persistent, regla de flip només per al +lateral i transició automàtica a `stand` quan la col·lisió bloqueja el +moviment. Tot el canvi és local al `Player`; no s'ha tocat l'EntityManager +ni el bus. + +## 1. YAML — sis animacions + +El fitxer `data/player/player.yaml` defineix ara sis animacions, agrupades +en tres files de 4 frames sobre `player.gif` (convenció habitual: d'esquerra +a dreta i de dalt a baix): + +| Nom | Frames | Descripció | +|---------------|------------|----------------------------| +| `stand_down` | `[0]` | parat mirant cap avall | +| `walk_down` | `[0..3]` | caminant cap avall | +| `stand_up` | `[4]` | parat mirant cap amunt | +| `walk_up` | `[4..7]` | caminant cap amunt | +| `stand` | `[8]` | parat lateral | +| `walk` | `[8..11]` | caminant lateral | + +Les laterals (`stand` i `walk`) ja existien; les variants `_up` i `_down` +són noves i permeten distingir visualment la direcció de moviment. + +## 2. `Facing` persistent + +S'ha afegit l'enum privat `Facing { SIDE, UP, DOWN }` amb el membre +`facing_{SIDE}`. La tria es fa a cada `update`, amb prevalença lateral +per si un dia es permetessin diagonals: + +- `vx != 0` → `SIDE` (inclou diagonals). +- `vx == 0 && vy < 0` → `UP`. +- `vx == 0 && vy > 0` → `DOWN`. +- Cap input → es conserva l'últim `facing_` perquè l'animació `stand_*` + coincideixi amb l'última direcció caminada. + +## 3. Regla de flip + +- `SIDE`: el sprite es flipeja horitzontalment si `vx < 0`, i es restaura + a `FLIP_NONE` si `vx > 0`. El sprite original mira a la dreta. +- `UP` / `DOWN`: el flip es força explícitament a `FLIP_NONE`. Els sprites + frontals/dorsals no s'han d'espillar només perquè l'últim lateral havia + estat cap a l'esquerra. + +## 4. `stand` automàtic a la col·lisió + +Quan el jugador prem una tecla però la col·lisió (mapa o sarcòfag) li +impedeix avançar, l'animació `walk*` s'atura i es força la variant +`stand_*` corresponent al facing. L'algorisme: + +1. Al principi del frame es captura `FRAME_START_X` / `FRAME_START_Y`. +2. S'aplica snap al passadís perpendicular si hi ha pur moviment H o V. +3. `AnimatedSprite::update` integra la velocitat; la col·lisió + eix-separada reverteix cada eix si `Map::collides(baseBox())`. +4. Després de `clampToPlayfield`, es compara la posició final amb el + valor capturat al pas 1. Si és idèntica, `MOVED = false` i es tria + `stand` / `stand_up` / `stand_down` segons `facing_`. +5. Si hi ha hagut moviment real (incloent-hi snap), s'aplica la variant + `walk*` del mateix facing. + +D'aquesta manera, xocar contra un mur o un sarcòfag fa que el jugador es +quede immòbil en compte d'animar el caminat "en el lloc". El facing +segueix reflectint la intenció (on vol anar), no el resultat, perquè +l'`stand_up` d'empentar-se contra un mur ha de mostrar el personatge +mirant amunt. + +## 5. Estat + +- DONE: sis animacions al YAML, `facing_` persistent, flip condicionat, + `stand` automàtic a la col·lisió. +- Build i smoke tests (14/14) verdes. +- Sense smoke test dedicat: la lògica està entrellaçada amb teclat i + `Map::collides`; l'smoke actual de `Player` no cobreix teclat simulat. +- Pendent (ajornat): si algun dia es vol feedback auditiu de wall-bump + (so en xocar), la condició natural és `!MOVED && (vx != 0 || vy != 0)` + al final d'`update`. diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index d80c09f..dd254db 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -66,24 +66,38 @@ void Player::update(float delta_time) { setVelX(vx); setVelY(vy); - // El sprite original mira a la dreta; flipem només si es mou a l'esquerra - // i restaurem si torna a la dreta. Moviments purament verticals conserven - // l'últim flip perquè el jugador no "gire" sense motiu. - if (vx < 0.0F) { - setFlip(SDL_FLIP_HORIZONTAL); - } else if (vx > 0.0F) { + // 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); } - const bool MOVING = vx != 0.0F || vy != 0.0F; - setCurrentAnimation(MOVING ? "walk" : "stand"); - // 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 // Y (o X) es corregeix gradualment per enfilar el passadís. Amb // moviment diagonal no es fa snap — l'usuari pilota manualment. const bool ONLY_H = (vx != 0.0F) && (vy == 0.0F); const bool ONLY_V = (vy != 0.0F) && (vx == 0.0F); + + // Pos d'inici de frame (abans de snap i integració): si en acabar el + // frame coincideix amb la posició final, el jugador no s'ha mogut + // realment (ni per input, ni per snap, ni ha superat la col·lisió) i cal + // forçar l'animació `stand` corresponent al facing — així s'atura el + // caminat quan xoca contra un mur o un sarcòfag. + const float FRAME_START_X = getPosX(); + const float FRAME_START_Y = getPosY(); + if (ONLY_H) { snapToHorizontalLane(delta_time); } else if (ONLY_V) { @@ -109,6 +123,12 @@ void Player::update(float delta_time) { clampToPlayfield(); + // 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. + const bool MOVED = (getPosX() != FRAME_START_X) || (getPosY() != FRAME_START_Y); + setCurrentAnimation(animationName(MOVED)); + // 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; // així els sarcòfags no reben falsos positius mentre el Player rellisca @@ -124,6 +144,18 @@ void Player::update(float delta_time) { } } +auto Player::animationName(bool moving) const -> const char* { + switch (facing_) { + case Facing::UP: + return moving ? "walk_up" : "stand_up"; + case Facing::DOWN: + return moving ? "walk_down" : "stand_down"; + case Facing::SIDE: + break; + } + return moving ? "walk" : "stand"; +} + auto Player::getCurrentTile() const -> std::pair { // El grid del tilemap és de `Map::tile_size` píxels i està 16-alineat // respecte a l'origen del tilemap (pot ser negatiu per overscan). El diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index d5c2aeb..50d53d4 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -2,6 +2,7 @@ #include +#include #include #include @@ -81,6 +82,10 @@ class Player : public AnimatedSprite { // wrappers deleguen a `snapToLane` només canviant l'eix actiu. 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*; void snapToLane( float delta_time, float base, @@ -91,6 +96,16 @@ class Player : public AnimatedSprite { Controls controls_; + // 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 `SIDE` (prevalença + // lateral), els purament verticals fan `UP`/`DOWN`. En aturar-se es + // preserva l'última orientació perquè la versió stand coincideixi. + enum class Facing : std::uint8_t { SIDE, + UP, + DOWN }; + Facing facing_{Facing::SIDE}; + // Bus del nivell (opcional). Propietat externa (EntityManager); el // Player només en guarda un punter per publicar events. EventBus* bus_{nullptr}; diff --git a/source/utils/defines.hpp b/source/utils/defines.hpp index 9c82a6b..62e2fcf 100644 --- a/source/utils/defines.hpp +++ b/source/utils/defines.hpp @@ -3,7 +3,7 @@ namespace Texts { constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner"; constexpr const char* COPYRIGHT = "© 2026 JailDesigner"; - constexpr const char* VERSION = "0.15.0"; // Single source of truth — Makefile el grep + constexpr const char* VERSION = "0.18.0"; // Single source of truth — Makefile el grep } // namespace Texts namespace GameCanvas {