diff --git a/data/player/player.yaml b/data/player/player.yaml index b88c2f8..95c140f 100644 --- a/data/player/player.yaml +++ b/data/player/player.yaml @@ -3,6 +3,14 @@ tileSetFile: player.gif frameWidth: 12 frameHeight: 24 +# Geometria de col·lisió del jugador (independent de l'animació): +# size → costat del quadrat de col·lisió (16×16) +# base_size → costat del quadrat visual inferior del sprite (els "peus"); +# s'usa per centrar la caixa de col·lisió sobre ell. +collision: + size: 16 + base_size: 12 + animations: - name: stand frames: [1] diff --git a/docs/audit-game.md b/docs/audit-game.md index 7216d47..b2f20c7 100644 --- a/docs/audit-game.md +++ b/docs/audit-game.md @@ -41,7 +41,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | ID | Fitxer:línia | Troballa | Severitat | Estat | Notes | |----|--------------|----------|-----------|-------|-------| | HC-01 | `player.cpp:15` | `SPEED_PX_PER_S = 60.0F` cablejat | Importante | DONE | Afegit `Defaults::Player::SPEED_PX_PER_S` a `utils/defaults.hpp` (patró usat a l'audit d'àudio); `player.cpp` fa alias local per reduir soroll. TODO-comment ("si més endavant volem sprint...") eliminat (PATCH-02) | -| HC-02 | `player.hpp:21-35` | `SPRITE_WIDTH/HEIGHT`, `LEGACY_BASE_SIZE`, `COLLISION_SIZE` cablejats | Importante | PENDING | Derivar del YAML del player | +| HC-02 | `player.hpp:21-35` | `SPRITE_WIDTH/HEIGHT`, `LEGACY_BASE_SIZE`, `COLLISION_SIZE` cablejats | Importante | DONE | Bloc `collision:` afegit a `player.yaml` (size, base_size); `Player` exposa `inline static int sprite_*`/`base_size`/`collision_*` recalculats al `loadFrom()`. GameScene crida el load abans del spawn (mateix patró que Map) | | HC-03 | `map.hpp:18-62` | Tota la geometria del mapa + 4 índexs de paleta en `.hpp` | Crítico | DONE | Bloc `geometry:` afegit a `pyramid.yaml`; `Map` exposa `inline static int` + `loadFrom(yaml)`; derivats (CORRIDOR/BOARD/ORIGIN/PLAYFIELD) recalculats al load. `constexpr` → `const` en els locals dependents a `player.cpp` i `game_scene.cpp`. Índexs de paleta queden per DEAD-02 | | HC-04 | `notification_presets.hpp:24-38` | `text_color`, `bg_color`, `time_ms` en `constexpr` | Menor | PENDING | Bloc `notifications:` a `config.yaml` o tema extern | | HC-05 | `ui_palette.hpp:12-21` | Índexs de paleta (`RENDER_INFO_TEXT=47`, `SHADOW=34`, `CANVAS_BG=65`) en `constexpr` | Menor | PENDING | Bloc `ui:` a `config.yaml` | diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 3235626..57f116f 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -4,8 +4,11 @@ #include #include +#include +#include #include "core/resources/resource_cache.hpp" +#include "external/fkyaml_node.hpp" #include "game/map/map.hpp" #include "utils/defaults.hpp" @@ -16,6 +19,32 @@ namespace { } // namespace +void Player::loadFrom(const std::string& yaml_path) { + std::ifstream file(yaml_path); + if (!file.is_open()) { + throw std::runtime_error("Player: no s'ha pogut obrir " + yaml_path); + } + auto yaml = fkyaml::node::deserialize(file); + + sprite_w = yaml["frameWidth"].get_value(); + sprite_h = yaml["frameHeight"].get_value(); + + if (!yaml.contains("collision")) { + throw std::runtime_error("Player: manca bloc `collision:` a " + yaml_path); + } + const auto& c = yaml["collision"]; + collision_size = c["size"].get_value(); + base_size = c["base_size"].get_value(); + + // Col·lisió centrada en el quadrat inferior del sprite. Si la caixa és + // més gran que la base (cas habitual, 16 vs 12) el padding és simètric + // a cada costat; si són iguals, padding=0 i la caixa coincideix amb la + // base sense offsets. + collision_padding = (collision_size - base_size) / 2; + collision_x_offset = -collision_padding; + collision_y_offset = (sprite_h - base_size) - collision_padding; +} + Player::Player(float x, float y, const Controls& controls) : AnimatedSprite(Resource::Cache::get().getAnimationData("player.yaml")), controls_(controls) { @@ -78,31 +107,31 @@ void Player::update(float delta_time) { // Límits de la zona jugable: la caixa de col·lisió no pot eixir del // playfield (muros). El cap del sprite sí pot sobresortir per damunt. - const auto MIN_X = static_cast(Map::playfield_x - COLLISION_X_OFFSET); + const auto MIN_X = static_cast(Map::playfield_x - collision_x_offset); const auto MAX_X = static_cast( - Map::playfield_x + Map::playfield_w - COLLISION_X_OFFSET - COLLISION_SIZE); - const auto MIN_Y = static_cast(Map::playfield_y - COLLISION_Y_OFFSET); + Map::playfield_x + Map::playfield_w - collision_x_offset - collision_size); + const auto MIN_Y = static_cast(Map::playfield_y - collision_y_offset); const auto MAX_Y = static_cast( - Map::playfield_y + Map::playfield_h - COLLISION_Y_OFFSET - COLLISION_SIZE); + Map::playfield_y + Map::playfield_h - collision_y_offset - collision_size); setPosX(std::clamp(getPosX(), MIN_X, MAX_X)); setPosY(std::clamp(getPosY(), MIN_Y, MAX_Y)); } auto Player::baseBox() const -> SDL_FRect { return { - .x = getPosX() + static_cast(COLLISION_X_OFFSET), - .y = getPosY() + static_cast(COLLISION_Y_OFFSET), - .w = static_cast(COLLISION_SIZE), - .h = static_cast(COLLISION_SIZE), + .x = getPosX() + static_cast(collision_x_offset), + .y = getPosY() + static_cast(collision_y_offset), + .w = static_cast(collision_size), + .h = static_cast(collision_size), }; } void Player::snapToHorizontalLane(float delta_time) { - // Lanes en posY: base + i·PITCH_Y per a i ∈ [0, GRID_ROWS]. Correspon a + // Lanes en posY: base + i·pitch_y per a i ∈ [0, grid_rows]. Correspon a // la caixa de col·lisió ocupant exactament un passadís horitzontal. - const auto BASE = static_cast(Map::playfield_y - COLLISION_Y_OFFSET); + const auto BASE = static_cast(Map::playfield_y - collision_y_offset); const auto PITCH = static_cast(Map::pitch_y); - constexpr auto THRESHOLD = static_cast(COLLISION_SIZE) / 2.0F; + const auto THRESHOLD = static_cast(collision_size) / 2.0F; const auto IDX = std::clamp( static_cast(std::round((getPosY() - BASE) / PITCH)), @@ -120,9 +149,9 @@ void Player::snapToHorizontalLane(float delta_time) { } void Player::snapToVerticalLane(float delta_time) { - const auto BASE = static_cast(Map::playfield_x - COLLISION_X_OFFSET); + const auto BASE = static_cast(Map::playfield_x - collision_x_offset); const auto PITCH = static_cast(Map::pitch_x); - constexpr auto THRESHOLD = static_cast(COLLISION_SIZE) / 2.0F; + const auto THRESHOLD = static_cast(collision_size) / 2.0F; const auto IDX = std::clamp( static_cast(std::round((getPosX() - BASE) / PITCH)), diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index af21ad9..a362ef5 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -2,6 +2,8 @@ #include +#include + #include "core/rendering/sprite/animated_sprite.hpp" class Player : public AnimatedSprite { @@ -16,23 +18,23 @@ class Player : public AnimatedSprite { SDL_Scancode right; }; - // Sprite 12×24 amb perspectiva falsa: visualment, els 12 píxels - // superiors són el cap i els 12 inferiors la base. - static constexpr int SPRITE_WIDTH = 12; - static constexpr int SPRITE_HEIGHT = 24; + // Geometria carregada des de `data/player/player.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 + // quadrat inferior del sprite (`base_size`). Els offsets els calculem + // al load a partir d'aquests tres valors; el mapa de col·lisions i el + // de dibuix són independents (el sprite pot sobresortir sense afectar). + inline static int sprite_w = 0; + inline static int sprite_h = 0; + inline static int base_size = 0; + inline static int collision_size = 0; + inline static int collision_padding = 0; + inline static int collision_x_offset = 0; + inline static int collision_y_offset = 0; - // Caixa de col·lisió: quadrat 16×16 concèntric amb els 12×12 inferiors - // del sprite (la base "antiga"). Encaixa just als passadissos de 16 px - // → el jugador va ajustat pels corredors. L'ampliació és simètrica: - // 2 px per cada costat i 2 px per dalt i per baix. Mapa de colisions i - // de dibuix són independents: el sprite pot sobresortir (el cap) sense - // afectar les col·lisions. - static constexpr int LEGACY_BASE_SIZE = 12; - static constexpr int COLLISION_SIZE = 16; - static constexpr int COLLISION_PADDING = (COLLISION_SIZE - LEGACY_BASE_SIZE) / 2; - static constexpr int COLLISION_X_OFFSET = -COLLISION_PADDING; - static constexpr int COLLISION_Y_OFFSET = - (SPRITE_HEIGHT - LEGACY_BASE_SIZE) - COLLISION_PADDING; + // Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor. + static void loadFrom(const std::string& yaml_path); Player(float x, float y, const Controls& controls); @@ -42,17 +44,17 @@ class Player : public AnimatedSprite { // per al Z-order contra els sarcs: peus per sobre d'un sarc → darrere; // per sota → davant. [[nodiscard]] auto getFeetY() const -> float { - return getPosY() + static_cast(COLLISION_Y_OFFSET + COLLISION_SIZE); + return getPosY() + static_cast(collision_y_offset + collision_size); } private: - // Rectangle de col·lisió del jugador (16×16), usat pel mapa de colisions - // i com a referència per al Z-order. + // Rectangle de col·lisió del jugador, usat pel mapa de colisions i com + // a referència per al Z-order. [[nodiscard]] auto baseBox() const -> SDL_FRect; // Snap cap al passadís perpendicular a la direcció premuda. Si la caixa - // de col·lisió està a ≤ COLLISION_SIZE/2 d'una lane, s'hi aproxima - // gradualment (a velocitat de moviment). Això permet canviar de direcció + // de col·lisió està a ≤ collision_size/2 d'una lane, s'hi aproxima + // gradualment (a velocitat de moviment). Permet canviar de direcció // sense haver d'alinear-se píxel-perfect en l'encreuament. void snapToHorizontalLane(float delta_time); void snapToVerticalLane(float delta_time); diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 183900d..273b97d 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -38,6 +38,11 @@ GameScene::GameScene() Resource::Cache::get().loadSurface("player.gif", "data/player/player.gif"); Resource::Cache::get().loadAnimation("player.yaml", "data/player/player.yaml"); + // Geometria de col·lisió del jugador (caixa 16×16, base 12): carregada + // del mateix YAML que l'animació. Ha d'estar feta abans d'instanciar + // Player perquè el spawn consulta `collision_*`. + Player::loadFrom("data/player/player.yaml"); + // Jugador 0: fletxes del teclat (Defaults::KeysGame). Instanciar-ne un // segon només requereix afegir una altra entrada amb uns Controls // distints (p.ex. WASD). @@ -54,8 +59,8 @@ GameScene::GameScene() constexpr auto CORRIDOR_CENTER_X = static_cast(GameCanvas::CENTER_X); const auto CORRIDOR_CENTER_Y = static_cast(Map::playfield_y) + (static_cast(Map::outer_corridor) / 2.0F); - constexpr auto SPAWN_X = CORRIDOR_CENTER_X - (static_cast(Player::SPRITE_WIDTH) / 2.0F); - const auto SPAWN_Y = CORRIDOR_CENTER_Y - static_cast(Player::COLLISION_Y_OFFSET) - (static_cast(Player::COLLISION_SIZE) / 2.0F); + const auto SPAWN_X = CORRIDOR_CENTER_X - (static_cast(Player::sprite_w) / 2.0F); + const auto SPAWN_Y = CORRIDOR_CENTER_Y - static_cast(Player::collision_y_offset) - (static_cast(Player::collision_size) / 2.0F); players_.emplace_back(std::make_unique(SPAWN_X, SPAWN_Y, CONTROLS_P1)); // Música de fons de la piràmide 1 en bucle infinit. El track ja està al