From e6c7f0dcfb9d6683b7786bba1ab7647365ae42e9 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 17:32:56 +0200 Subject: [PATCH] =?UTF-8?q?HC-03:=20geometria=20del=20mapa=20traslladada?= =?UTF-8?q?=20de=20map.hpp=20al=20YAML=20de=20la=20pir=C3=A0mide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El bloc `geometry:` de pyramid.yaml porta les 9 constants base (grid, sarc, pitch, HUD, murs); els derivats (corridor, board, origin, playfield) es recalculen al `Map::loadFrom()`. `constexpr` passa a `const` on calia i els membres estàtics adopten snake_case. Desbloqueja la iteració "pirámide a pirámide" sense recompilar. --- data/maps/pyramid.yaml | 16 ++++++ docs/audit-game.md | 4 +- source/game/entities/player.cpp | 24 ++++----- source/game/map/map.cpp | 85 +++++++++++++++++++++++------- source/game/map/map.hpp | 86 ++++++++++++++----------------- source/game/scenes/game_scene.cpp | 21 ++++++-- 6 files changed, 153 insertions(+), 83 deletions(-) diff --git a/data/maps/pyramid.yaml b/data/maps/pyramid.yaml index 6fc0a90..dc7fd10 100644 --- a/data/maps/pyramid.yaml +++ b/data/maps/pyramid.yaml @@ -15,6 +15,22 @@ tileset: data/tilesets/tomb.gif tile_size: 16 origin: [-8, 0] +# Geometria lògica de la piràmide (separada del render visual del tileset). +# La graella de sarcòfags, el pitch entre ells i les amplades dels murs +# decideixen els límits del jugador i les seues caixes de col·lisió. Abans +# vivien com a `static constexpr` a map.hpp; ara cada piràmide pot definir +# la seua pròpia forma sense recompilar. +geometry: + grid_cols: 4 + grid_rows: 5 + sarc_w: 48 + sarc_h: 16 + pitch_x: 64 + pitch_y: 32 + hud_h: 24 + back_wall_h: 24 + side_wall_w: 24 + legend: W: 292 X: 169 diff --git a/docs/audit-game.md b/docs/audit-game.md index 420af35..942ac8c 100644 --- a/docs/audit-game.md +++ b/docs/audit-game.md @@ -18,7 +18,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | ID | Títol | Estat | |----|-------|-------| -| HC-03 | Geometria del mapa cablejada a `map.hpp` (bloqueja "pirámide a pirámide") | PENDING | +| HC-03 | Geometria del mapa cablejada a `map.hpp` (bloqueja "pirámide a pirámide") | DONE | | COUP-01 | `core/` inclou `game/ui/notification_presets.hpp` (dependència invertida) | PENDING | | DUP-01 | PostFX i CrtPi són dos pipelines duplicats a `options.cpp` | PENDING | | PATCH-04 | Reescriure silenciosament `config.yaml` al fallar parseig o versió | PENDING | @@ -42,7 +42,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer |----|--------------|----------|-----------|-------|-------| | HC-01 | `player.cpp:15` | `SPEED_PX_PER_S = 60.0F` cablejat | Importante | PENDING | Mourea `Defaults::Player` o `player.yaml` | | HC-02 | `player.hpp:21-35` | `SPRITE_WIDTH/HEIGHT`, `LEGACY_BASE_SIZE`, `COLLISION_SIZE` cablejats | Importante | PENDING | Derivar del YAML del player | -| HC-03 | `map.hpp:18-62` | Tota la geometria del mapa + 4 índexs de paleta en `.hpp` | Crítico | PENDING | YAML per piràmide (ja tenim `pyramid.yaml`) | +| 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` | | HC-06 | `game_scene.cpp:24-26,52` | Rutes `data/maps/pyramid.yaml`, `data/player/player.*`, `pyramid_1.ogg` cablejades | Importante | PENDING | Paràmetre del ctor o `scenes.yaml` | diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index fec2434..2855ef1 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -78,12 +78,12 @@ 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. - constexpr auto MIN_X = static_cast(Map::PLAYFIELD_X - COLLISION_X_OFFSET); - constexpr auto MAX_X = static_cast( - Map::PLAYFIELD_X + Map::PLAYFIELD_W - COLLISION_X_OFFSET - COLLISION_SIZE); - constexpr auto MIN_Y = static_cast(Map::PLAYFIELD_Y - COLLISION_Y_OFFSET); - constexpr auto MAX_Y = static_cast( - Map::PLAYFIELD_Y + Map::PLAYFIELD_H - COLLISION_Y_OFFSET - COLLISION_SIZE); + 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); + const auto MAX_Y = static_cast( + 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)); } @@ -100,14 +100,14 @@ auto Player::baseBox() const -> SDL_FRect { void Player::snapToHorizontalLane(float delta_time) { // 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. - constexpr auto BASE = static_cast(Map::PLAYFIELD_Y - COLLISION_Y_OFFSET); - constexpr auto PITCH = static_cast(Map::PITCH_Y); + 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 IDX = std::clamp( static_cast(std::round((getPosY() - BASE) / PITCH)), 0, - Map::GRID_ROWS); + Map::grid_rows); const float LANE = BASE + (static_cast(IDX) * PITCH); const float DY = LANE - getPosY(); @@ -120,14 +120,14 @@ void Player::snapToHorizontalLane(float delta_time) { } void Player::snapToVerticalLane(float delta_time) { - constexpr auto BASE = static_cast(Map::PLAYFIELD_X - COLLISION_X_OFFSET); - constexpr auto PITCH = static_cast(Map::PITCH_X); + 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 IDX = std::clamp( static_cast(std::round((getPosX() - BASE) / PITCH)), 0, - Map::GRID_COLS); + Map::grid_cols); const float LANE = BASE + (static_cast(IDX) * PITCH); const float DX = LANE - getPosX(); diff --git a/source/game/map/map.cpp b/source/game/map/map.cpp index 896b114..f4401c1 100644 --- a/source/game/map/map.cpp +++ b/source/game/map/map.cpp @@ -1,7 +1,12 @@ #include "game/map/map.hpp" +#include +#include +#include + #include "core/rendering/screen.hpp" #include "core/resources/resource_cache.hpp" +#include "external/fkyaml_node.hpp" #include "utils/defines.hpp" namespace { @@ -16,18 +21,60 @@ namespace { } // namespace -Map::Map() = default; +void Map::loadFrom(const std::string& yaml_path) { + std::ifstream file(yaml_path); + if (!file.is_open()) { + throw std::runtime_error("Map: no s'ha pogut obrir " + yaml_path); + } + auto yaml = fkyaml::node::deserialize(file); + if (!yaml.contains("geometry")) { + throw std::runtime_error("Map: manca bloc `geometry:` a " + yaml_path); + } + const auto& g = yaml["geometry"]; + + grid_cols = g["grid_cols"].get_value(); + grid_rows = g["grid_rows"].get_value(); + sarc_w = g["sarc_w"].get_value(); + sarc_h = g["sarc_h"].get_value(); + pitch_x = g["pitch_x"].get_value(); + pitch_y = g["pitch_y"].get_value(); + hud_h = g["hud_h"].get_value(); + back_wall_h = g["back_wall_h"].get_value(); + side_wall_w = g["side_wall_w"].get_value(); + + // Derivats: el passadís és el sobrant del pitch sobre el sarcòfag, i + // l'exterior reutilitza la mateixa amplada per mantenir el pas lliure. + corridor_w = pitch_x - sarc_w; + corridor_h = pitch_y - sarc_h; + outer_corridor = corridor_w; + + board_w = ((grid_cols - 1) * pitch_x) + sarc_w; + board_h = ((grid_rows - 1) * pitch_y) + sarc_h; + + origin_x = side_wall_w + outer_corridor; + origin_y = hud_h + back_wall_h + outer_corridor; + + playfield_x = side_wall_w; + playfield_y = hud_h + back_wall_h; + playfield_w = board_w + (2 * outer_corridor); + playfield_h = board_h + (2 * outer_corridor); + + std::cout << "Map: geometria " << grid_cols << "x" << grid_rows + << " sarcs " << sarc_w << "x" << sarc_h + << " pitch " << pitch_x << "x" << pitch_y + << " des de " << yaml_path << '\n'; +} auto Map::getSarcRowBottomY(int row) -> int { - return ORIGIN_Y + (row * PITCH_Y) + SARC_H; + return origin_y + (row * pitch_y) + sarc_h; } auto Map::getSarcRect(int col, int row) -> SDL_FRect { return { - .x = static_cast(ORIGIN_X + (col * PITCH_X)), - .y = static_cast(ORIGIN_Y + (row * PITCH_Y)), - .w = static_cast(SARC_W), - .h = static_cast(SARC_H), + .x = static_cast(origin_x + (col * pitch_x)), + .y = static_cast(origin_y + (row * pitch_y)), + .w = static_cast(sarc_w), + .h = static_cast(sarc_h), }; } @@ -41,37 +88,37 @@ void Map::renderBackground() { screen.fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, palette[FLOOR]); // Marcador (dalt). - screen.fillRect(0, 0, GameCanvas::WIDTH, HUD_H, palette[HUD_BG]); + screen.fillRect(0, 0, GameCanvas::WIDTH, hud_h, palette[HUD_BG]); // Mur posterior: banda horitzontal sota el marcador. - screen.fillRect(0, HUD_H, GameCanvas::WIDTH, BACK_WALL_H, palette[WALL]); + screen.fillRect(0, hud_h, GameCanvas::WIDTH, back_wall_h, palette[WALL]); // Murs laterals (des del marcador fins al final). - screen.fillRect(0, HUD_H, SIDE_WALL_W, GameCanvas::HEIGHT - HUD_H, palette[WALL]); - screen.fillRect(GameCanvas::WIDTH - SIDE_WALL_W, HUD_H, SIDE_WALL_W, GameCanvas::HEIGHT - HUD_H, palette[WALL]); + screen.fillRect(0, hud_h, side_wall_w, GameCanvas::HEIGHT - hud_h, palette[WALL]); + screen.fillRect(GameCanvas::WIDTH - side_wall_w, hud_h, side_wall_w, GameCanvas::HEIGHT - hud_h, palette[WALL]); // Mur frontal: de la vora inferior del passadís de baix fins al final del // canvas. Assumeix els 40 px extra de 320×240 i fa de primer pla. - const int FRONT_WALL_Y = ORIGIN_Y + BOARD_H + OUTER_CORRIDOR; + const int FRONT_WALL_Y = origin_y + board_h + outer_corridor; screen.fillRect(0, FRONT_WALL_Y, GameCanvas::WIDTH, GameCanvas::HEIGHT - FRONT_WALL_Y, palette[WALL]); } void Map::renderSarcsRow(int row) { auto& screen = Screen::get(); const auto& palette = Resource::Cache::get().getPalette("ui"); - const int Y = ORIGIN_Y + (row * PITCH_Y); - for (int col = 0; col < GRID_COLS; ++col) { - const int X = ORIGIN_X + (col * PITCH_X); - screen.fillRect(X, Y, SARC_W, SARC_H, palette[SARC]); + const int Y = origin_y + (row * pitch_y); + for (int col = 0; col < grid_cols; ++col) { + const int X = origin_x + (col * pitch_x); + screen.fillRect(X, Y, sarc_w, sarc_h, palette[SARC]); } } auto Map::collides(const SDL_FRect& base_box) -> bool { - // Recorregut trivial dels 20 sarcòfags. Amb una graella tan petita no - // val la pena indexar per col/row prèviament; si algun dia creix, es pot + // Recorregut trivial dels sarcòfags. Amb una graella petita no val la + // pena indexar per col/row prèviament; si algun dia creix, es pot // calcular el rang de columnes/files candidates a partir de base_box. - for (int row = 0; row < GRID_ROWS; ++row) { - for (int col = 0; col < GRID_COLS; ++col) { + for (int row = 0; row < grid_rows; ++row) { + for (int col = 0; col < grid_cols; ++col) { if (overlapStrict(base_box, getSarcRect(col, row))) { return true; } } } diff --git a/source/game/map/map.hpp b/source/game/map/map.hpp index d0e00fe..0c56e43 100644 --- a/source/game/map/map.hpp +++ b/source/game/map/map.hpp @@ -2,57 +2,48 @@ #include +#include + // Mapa d'una piràmide: sala rectangular amb marcador superior, muros pels -// quatre costats i graella 4×4 de sarcòfags envoltada per un passadís a cada -// cantó. Mesures portades de l'AEE original (mapa.cpp: sarcòfag 50×20, pitch -// 65×35, passadís 15 px) i adaptades a 320×240 repartint els 40 px extra -// sobretot al mur frontal, que fa de primer pla per a la falsa perspectiva. +// quatre costats i graella de sarcòfags envoltada per un passadís a cada +// cantó. La geometria (graella, pitch, amplada de murs) es carrega des del +// bloc `geometry:` del YAML de la piràmide — així cada sala pot tenir la +// seua forma sense recompilar. // // Z-order: el render es divideix en fons (marcador + muros + sòl) i files // de sarcòfags. El caller (GameScene) alternarà jugadors ordenats per la Y -// dels peus amb cada fila per a aconseguir la perspectiva falsa (el sarcòfag -// tapa el cos del jugador si aquest està "darrere"; el cap del jugador tapa -// el sarcòfag de la fila superior si està "davant"). +// dels peus amb cada fila per a aconseguir la perspectiva falsa. class Map { public: - // Graella portada tal qual de l'AEE (4×4 sarcòfags, pitch 65×35). - static constexpr int GRID_COLS = 4; - static constexpr int GRID_ROWS = 5; - static constexpr int SARC_W = 48; - static constexpr int SARC_H = 16; - static constexpr int PITCH_X = 64; - static constexpr int PITCH_Y = 32; + // Geometria de la graella (carregada de pyramid.yaml > geometry). + // Membres estàtics no-constexpr: el nom segueix snake_case per tidy; + // callers (Player, GameScene) els consulten com `Map::playfield_x`. + inline static int grid_cols = 0; + inline static int grid_rows = 0; + inline static int sarc_w = 0; + inline static int sarc_h = 0; + inline static int pitch_x = 0; + inline static int pitch_y = 0; - // El pas entre sarcòfags (15×15) és exactament el que ha de cabre el - // jugador; el reutilitzem també com a passadís exterior (perímetre). - static constexpr int CORRIDOR_W = PITCH_X - SARC_W; - static constexpr int CORRIDOR_H = PITCH_Y - SARC_H; - static constexpr int OUTER_CORRIDOR = CORRIDOR_W; + // Cromat de la sala: marcador (HUD), mur posterior (dalt) i murs + // laterals. El mur frontal es deriva del passadís exterior inferior. + inline static int hud_h = 0; + inline static int back_wall_h = 0; + inline static int side_wall_w = 0; - // Dimensions del tauler complet (des del primer sarcòfag fins al cantó - // inferior dret de l'últim). Derivades: no cal tocar-les a mà. - static constexpr int BOARD_W = ((GRID_COLS - 1) * PITCH_X) + SARC_W; - static constexpr int BOARD_H = ((GRID_ROWS - 1) * PITCH_Y) + SARC_H; - - // Cromat de la sala: marcador, mur posterior (dalt), murs laterals i mur - // frontal (baix). El frontal és més gros per absorbir els 40 px extra del - // pas de 320×200 → 320×240 i reforçar la falsa perspectiva. - static constexpr int HUD_H = 24; - static constexpr int BACK_WALL_H = 24; - static constexpr int SIDE_WALL_W = 24; - - // Origen del tauler dins del canvas (cantó superior-esquerra del primer - // sarcòfag). Derivat: mur + passadís en cada eix. - static constexpr int ORIGIN_X = SIDE_WALL_W + OUTER_CORRIDOR; - static constexpr int ORIGIN_Y = HUD_H + BACK_WALL_H + OUTER_CORRIDOR; - - // Límits de la zona jugable (on el jugador pot moure's lliurement, sense - // comptar encara les colisions contra sarcòfags). Serviran per al pas - // següent de colisions. - static constexpr int PLAYFIELD_X = SIDE_WALL_W; - static constexpr int PLAYFIELD_Y = HUD_H + BACK_WALL_H; - static constexpr int PLAYFIELD_W = BOARD_W + (2 * OUTER_CORRIDOR); - static constexpr int PLAYFIELD_H = BOARD_H + (2 * OUTER_CORRIDOR); + // Derivats de la geometria base: el pas entre sarcòfags ha de cabre + // exactament el jugador, i el mateix passadís s'usa a l'exterior. + inline static int corridor_w = 0; + inline static int corridor_h = 0; + inline static int outer_corridor = 0; + inline static int board_w = 0; + inline static int board_h = 0; + inline static int origin_x = 0; + inline static int origin_y = 0; + inline static int playfield_x = 0; + inline static int playfield_y = 0; + inline static int playfield_w = 0; + inline static int playfield_h = 0; // Índexs a la paleta UI (data/palettes/sega-cube.pal). Centralitzats ací // per a poder ajustar l'estètica sense buscar magic numbers pel codi. @@ -61,15 +52,18 @@ class Map { static constexpr Uint8 FLOOR = 40; static constexpr Uint8 SARC = 160; - Map(); + // Carrega el bloc `geometry:` del YAML i recalcula tots els derivats. + // Ha de cridar-se abans d'instanciar `Player` o consultar `collides()`: + // GameScene ho fa al ctor. + static void loadFrom(const std::string& yaml_path); // Fons estàtic: marcador, muros (back/side/front) i sòl interior. No // pinta cap sarcòfag; això queda per a renderSarcsRow() perquè el caller // puga interleaveing amb els jugadors. static void renderBackground(); - // Pinta la fila `row` de sarcòfags (4 rectangles a pitch 65). El caller - // decideix quan cridar-la respecte als jugadors segons la seua Y. + // Pinta la fila `row` de sarcòfags. El caller decideix quan cridar-la + // respecte als jugadors segons la seua Y. static void renderSarcsRow(int row); // Y de la cara inferior dels sarcòfags de la fila `row` (la "baseline" diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 615e061..183900d 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -20,8 +20,21 @@ #include "utils/defaults.hpp" #include "utils/defines.hpp" +namespace { + + // La geometria ha de quedar inicialitzada abans que Tilemap parsejar el + // mateix YAML i abans que el ctor de Player consulte Map::PLAYFIELD_*. + // Cridar-la des d'una expressió que retorne la ruta permet encadenar-ho + // a l'init list sense fer Tilemap default-constructible. + auto loadMapGeometry(const char* yaml_path) -> const char* { + Map::loadFrom(yaml_path); + return yaml_path; + } + +} // namespace + GameScene::GameScene() - : tilemap_("data/maps/pyramid.yaml") { + : tilemap_(loadMapGeometry("data/maps/pyramid.yaml")) { Resource::Cache::get().loadSurface("player.gif", "data/player/player.gif"); Resource::Cache::get().loadAnimation("player.yaml", "data/player/player.yaml"); @@ -39,10 +52,10 @@ GameScene::GameScene() // centrada en Y al passadís exterior superior. Així no naix dins de // cap sarcòfag. constexpr auto CORRIDOR_CENTER_X = static_cast(GameCanvas::CENTER_X); - constexpr auto CORRIDOR_CENTER_Y = - static_cast(Map::PLAYFIELD_Y) + (static_cast(Map::OUTER_CORRIDOR) / 2.0F); + 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); - constexpr auto SPAWN_Y = CORRIDOR_CENTER_Y - static_cast(Player::COLLISION_Y_OFFSET) - (static_cast(Player::COLLISION_SIZE) / 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