From b89547571623a87aa717d7adb3d610b97cced67f Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 17:25:04 +0200 Subject: [PATCH 01/20] Auditoria game/: llista completa de 38 troballes en docs/audit-game.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inventari de duplicació, hardcoding, parxes, acoplament, inconsistències, codi mort i violacions de convencions. Tots els ítems PENDING; els commits següents en tancaran un a un. --- docs/audit-game.md | 100 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 docs/audit-game.md diff --git a/docs/audit-game.md b/docs/audit-game.md new file mode 100644 index 0000000..420af35 --- /dev/null +++ b/docs/audit-game.md @@ -0,0 +1,100 @@ +# Auditoría `source/game/` + +Rama: `audit/game`. +Cobertura: `source/game/{entities,map,scenes,ui}` + `source/game/options.{hpp,cpp}` (12 fitxers, ~1.486 línies). + +Estados posibles: +- `PENDING` — identificat, sense tocar +- `WIP` — diff proposat, pendent d'aplicar +- `DONE` — aplicat i commiteat +- `WONTFIX` — es decideix no tocar (justificació a la nota) +- `DEFERRED` — es farà en fase posterior + +Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer i línies afectades en el moment del clone (branca `main`). + +--- + +## Top 5 prioritari + +| ID | Títol | Estat | +|----|-------|-------| +| HC-03 | Geometria del mapa cablejada a `map.hpp` (bloqueja "pirámide a pirámide") | PENDING | +| 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 | +| HC-01 | `SPEED_PX_PER_S`, caixa de col·lisió i rutes d'assets cablejades | PENDING | + +--- + +## 1. Duplicació + +| ID | Fitxer:línia | Troballa | Severitat | Estat | Notes | +|----|--------------|----------|-----------|-------|-------| +| DUP-01 | `options.cpp:152-335,443-547` | PostFX i CrtPi tenen `default*Presets/parse*Preset/write*Preset/load*FromFile/save*ToFile/set*File` duplicats | Importante | PENDING | Template `PresetStore` amb serialitzadors per camp | +| DUP-02 | `player.cpp:100-140` | `snapToHorizontalLane` i `snapToVerticalLane` són el mateix algorisme amb eixos permutats | Importante | PENDING | Unificar en `snapToLane(Axis, delta_time)` | +| DUP-03 | `options.cpp:14-62` | Tres parelles `enumToString`/`enumFromString` amb la mateixa forma | Menor | PENDING | Taula `constexpr` + helpers genèrics | +| DUP-04 | `options.cpp:79-149` | Cada `parse*` repeteix `if (node.contains) { ... get_value }` | Menor | PENDING | Helper `readField(node, key, dst)` | +| DUP-05 | `player.cpp:81-88` | Càlcul `MIN_/MAX_` + `clamp` per eix és la mateixa fórmula que el snap | Menor | PENDING | Fold en helper | + +## 2. Hardcoding + +| ID | Fitxer:línia | Troballa | Severitat | Estat | Notes | +|----|--------------|----------|-----------|-------|-------| +| 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-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` | +| HC-07 | `options.cpp:27,55` | Fallbacks `"letterbox"`/`"off"` com a strings màgics que dupliquen els enums | Menor | PENDING | Passar per `toString(Defaults::...)` | +| HC-08 | `tilemap.cpp:135` | `0xFF000000U` com a màscara alfa | Menor | PENDING | Constant amb nom a `defines.hpp` | + +## 3. Parxes i improvisacions + +| ID | Fitxer:línia | Troballa | Severitat | Estat | Notes | +|----|--------------|----------|-----------|-------|-------| +| PATCH-01 | `player.cpp:66-77` | Col·lisió per eix fent `AnimatedSprite::update` + revertir + reaplicar | Importante | PENDING | `tryMove(dx, dy, collisionFn)` a la classe base | +| PATCH-02 | `player.cpp:13-15` | Comentari tipus TODO ("si més endavant volem sprint...") | Menor | PENDING | Treure o convertir en feature amb issue | +| PATCH-03 | `map.cpp:13-15` | `overlapStrict` custom perquè SDL3 considera el toc com intersecció | Menor | PENDING | Si s'usa més, mourea `utils/` | +| PATCH-04 | `options.cpp:361-363,371-375,475-480,528-533` | Reescriure silenciosament `config.yaml` al fallar parseig o canviar versió | Crítico | PENDING | `.bak` + log abans de sobreescriure | +| PATCH-05 | `game_scene.cpp:51` | `if (Audio::get() != nullptr)` com a defensa sobre un singleton Meyers | Menor | PENDING | Assumir invariant, treure | +| PATCH-06 | `game_scene.cpp:78-91` | Ordenació per `getFeetY()` amb 1 jugador i sense sarcs-sprite: cerimonial | Menor | PENDING | Comentar "reactivar quan hi haja sarcs-sprite" o treure | +| PATCH-07 | `map.hpp:64` + `map.cpp:19` | `Map` declara ctor però tots els mètodes són `static` (transició a mitges) | Importante | PENDING | Decidir: namespace o instanciar | + +## 4. Acoplament innecessari + +| ID | Fitxer:línia | Troballa | Severitat | Estat | Notes | +|----|--------------|----------|-----------|-------|-------| +| COUP-01 | `core/input/global_inputs.cpp:15` | `core/` inclou `game/ui/notification_presets.hpp` | Crítico | PENDING | Mourea `core/rendering/` o injectar presets des de `GameScene` | +| COUP-02 | `player.cpp` + `Map::PLAYFIELD_*`, `PITCH_*`, `GRID_*`, `collides` | `Player` coneix la geometria interna del mapa | Importante | PENDING | Passar `const MapInfo&` o un `World*` al ctor del player | +| COUP-03 | `options.hpp:96-105` | Globals `inline` mutats lliurement des de `core/input/global_inputs.cpp` | Importante | PENDING | Setters + signal quan canvien | +| COUP-04 | `tilemap.cpp:117-138` | Clip+blit directe al canvas ARGB que podria viure a `Surface` | Menor | PENDING | `blitInto(canvas, origin)` genèric | + +## 5. Inconsistències internes + +| ID | Fitxer:línia | Troballa | Severitat | Estat | Notes | +|----|--------------|----------|-----------|-------|-------| +| INCONS-01 | `game/*` | 3 estils de mòdul: namespace+inline (Options), classe all-static (Map), OOP (Player/Tilemap), namespace+constexpr (UiColor/Notification) | Importante | PENDING | Decidir i uniformar | +| INCONS-02 | `options.cpp:372,477,530,579,607` | `std::cerr` en comptes de `std::cout` (convenció del projecte) | Importante | PENDING | Substituir per `std::cout` prefixat | +| INCONS-03 | `options.cpp` vs `tilemap.cpp:77-78` | Diferent política en YAML absent: `contains()` vs accés directe que llança | Menor | PENDING | Uniformar (llançar sempre si és un fitxer obligatori) | +| INCONS-04 | `options.cpp`, `player.cpp` | Locals ALL-CAPS (`CONTENT`, `IT`, `DT`, `OLD_X`) barrejats amb `snake_case` | Menor | PENDING | Uniformar estil | +| INCONS-05 | `options.cpp:420-430` | `Screen::Config` s'omple camp a camp mentre que `Audio::Config` fa `designated initializers` | Menor | PENDING | Uniformar | + +## 6. Codi mort + +| ID | Fitxer:línia | Troballa | Severitat | Estat | Notes | +|----|--------------|----------|-----------|-------|-------| +| DEAD-01 | `map.hpp:69,73,78` | `renderBackground`/`renderSarcsRow`/`getSarcRowBottomY` no tenen callers | Importante | PENDING | Esborrar o re-integrar amb sarcs-sprite | +| DEAD-02 | `map.hpp:59-62` | Índexs `HUD_BG/WALL/FLOOR/SARC` només usats pel codi mort | Importante | PENDING | Esborrar amb DEAD-01 | +| DEAD-03 | `map.cpp:3-5` | Includes `screen.hpp/resource_cache.hpp/defines.hpp` orfes si es neteja DEAD-01 | Menor | PENDING | Revisar després de DEAD-01 | +| DEAD-04 | `map.hpp:64` + `map.cpp:19` | Ctor de `Map` mai instanciat | Menor | PENDING | Veure PATCH-07 | +| DEAD-05 | `game_scene.cpp:78-86` | Ordenació Z-order cerimonial (1 jugador, sense sarcs-sprite) | Menor | PENDING | Veure PATCH-06 | + +## 7. Violacions de convencions + +| ID | Fitxer:línia | Troballa | Severitat | Estat | Notes | +|----|--------------|----------|-----------|-------|-------| +| CONV-01 | `player.*`, `map.*`, `ui/*`, `game_scene.cpp` | Speed, caixa de col·lisió, geometria, índexs paleta, rutes d'assets, nom del track: tot cablejat | Crítico | PENDING | Tapat parcialment per HC-01/02/03/05/06 | +| CONV-02 | `map.*`, `options.*` | Interfaces+callbacks: `Map` simulated class, `Options` globals sense observers | Importante | PENDING | Cobert per PATCH-07 + COUP-03 | +| CONV-03 | `options.cpp` | `std::cerr` viola la regla "std::cout only" | Importante | PENDING | Duplicat d'INCONS-02 | +| CONV-04 | `player.cpp:27` | `SDL_GetKeyboardState` directe en `Player::update` (fuga de SDL al gameplay) | Menor | PENDING | Anar per `GlobalInputs`/`EventBuffer` | From e6c7f0dcfb9d6683b7786bba1ab7647365ae42e9 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 17:32:56 +0200 Subject: [PATCH 02/20] =?UTF-8?q?HC-03:=20geometria=20del=20mapa=20traslla?= =?UTF-8?q?dada=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 From ab7a79dc50974174448f93fa7ba1bf738a965cd3 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 17:35:03 +0200 Subject: [PATCH 03/20] COUP-01: notification_presets.hpp mogut de game/ui/ a core/rendering/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El fitxer definia presets del Notifier (que ja viu a core/rendering/), però estava a game/ i era consumit únicament per core/input/global_inputs.cpp — una dependència invertida core→game. El mogueu al costat del component que consumeix; el comentari de `notifier.hpp` hi apunta. `game/ui/` es queda només amb `ui_palette.hpp`. --- docs/audit-game.md | 4 ++-- source/core/input/global_inputs.cpp | 2 +- source/{game/ui => core/rendering}/notification_presets.hpp | 0 source/core/rendering/notifier.hpp | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) rename source/{game/ui => core/rendering}/notification_presets.hpp (100%) diff --git a/docs/audit-game.md b/docs/audit-game.md index 942ac8c..b2fb3fa 100644 --- a/docs/audit-game.md +++ b/docs/audit-game.md @@ -19,7 +19,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") | DONE | -| COUP-01 | `core/` inclou `game/ui/notification_presets.hpp` (dependència invertida) | PENDING | +| COUP-01 | `core/` inclou `game/ui/notification_presets.hpp` (dependència invertida) | DONE | | 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 | | HC-01 | `SPEED_PX_PER_S`, caixa de col·lisió i rutes d'assets cablejades | PENDING | @@ -65,7 +65,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | ID | Fitxer:línia | Troballa | Severitat | Estat | Notes | |----|--------------|----------|-----------|-------|-------| -| COUP-01 | `core/input/global_inputs.cpp:15` | `core/` inclou `game/ui/notification_presets.hpp` | Crítico | PENDING | Mourea `core/rendering/` o injectar presets des de `GameScene` | +| COUP-01 | `core/input/global_inputs.cpp:15` | `core/` inclou `game/ui/notification_presets.hpp` | Crítico | DONE | `notification_presets.hpp` mogut a `core/rendering/` (al costat del `Notifier` que consumeix). `global_inputs.cpp` i el comentari de `notifier.hpp` actualitzats | | COUP-02 | `player.cpp` + `Map::PLAYFIELD_*`, `PITCH_*`, `GRID_*`, `collides` | `Player` coneix la geometria interna del mapa | Importante | PENDING | Passar `const MapInfo&` o un `World*` al ctor del player | | COUP-03 | `options.hpp:96-105` | Globals `inline` mutats lliurement des de `core/input/global_inputs.cpp` | Importante | PENDING | Setters + signal quan canvien | | COUP-04 | `tilemap.cpp:117-138` | Clip+blit directe al canvas ARGB que podria viure a `Surface` | Menor | PENDING | `blitInto(canvas, origin)` genèric | diff --git a/source/core/input/global_inputs.cpp b/source/core/input/global_inputs.cpp index 315ac00..35b3120 100644 --- a/source/core/input/global_inputs.cpp +++ b/source/core/input/global_inputs.cpp @@ -7,12 +7,12 @@ #include "core/input/key_config.hpp" #include "core/locale/locale.hpp" +#include "core/rendering/notification_presets.hpp" #include "core/rendering/notifier.hpp" #include "core/rendering/render_info.hpp" #include "core/rendering/screen.hpp" #include "core/system/event_buffer.hpp" #include "game/options.hpp" -#include "game/ui/notification_presets.hpp" #include "utils/defaults.hpp" namespace GlobalInputs { diff --git a/source/game/ui/notification_presets.hpp b/source/core/rendering/notification_presets.hpp similarity index 100% rename from source/game/ui/notification_presets.hpp rename to source/core/rendering/notification_presets.hpp diff --git a/source/core/rendering/notifier.hpp b/source/core/rendering/notifier.hpp index 42ca726..82b8e0f 100644 --- a/source/core/rendering/notifier.hpp +++ b/source/core/rendering/notifier.hpp @@ -17,8 +17,8 @@ class Text; // a l'arrencada; si Notifier::init() es crida primer, les carrega ell). // // API crua: Notifier::get().notify(text, text_color, bg_color, time_ms). -// Per a treballar amb presets del joc, vegeu Notification::notify() a -// game/ui/notification_presets.hpp. +// Per a treballar amb presets pre-definits, vegeu Notification::notify() a +// core/rendering/notification_presets.hpp. class Notifier { public: static auto get() -> Notifier&; From 505c20dabcc9ef425991dc006e603fa075f1007d Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 17:37:58 +0200 Subject: [PATCH 04/20] DUP-01: pipeline unificat de presets PostFX i CrtPi a options.cpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Afegits `loadPresetsFromFile` i `savePresetsToFile` a l'anonymous namespace; els quatre entry-points passen a ser one-liners que injecten parse/write/defaults per tipus. Abans eren dues còpies del mateix codi diferenciades només pel tipus concret de preset. Els YAML regenerats són byte-a-byte idèntics als anteriors (el formatter i els comentaris es preserven). --- docs/audit-game.md | 4 +- source/game/options.cpp | 179 +++++++++++++++++++++------------------- 2 files changed, 95 insertions(+), 88 deletions(-) diff --git a/docs/audit-game.md b/docs/audit-game.md index b2fb3fa..ede4c10 100644 --- a/docs/audit-game.md +++ b/docs/audit-game.md @@ -20,7 +20,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer |----|-------|-------| | 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) | DONE | -| DUP-01 | PostFX i CrtPi són dos pipelines duplicats a `options.cpp` | PENDING | +| DUP-01 | PostFX i CrtPi són dos pipelines duplicats a `options.cpp` | DONE | | PATCH-04 | Reescriure silenciosament `config.yaml` al fallar parseig o versió | PENDING | | HC-01 | `SPEED_PX_PER_S`, caixa de col·lisió i rutes d'assets cablejades | PENDING | @@ -30,7 +30,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | ID | Fitxer:línia | Troballa | Severitat | Estat | Notes | |----|--------------|----------|-----------|-------|-------| -| DUP-01 | `options.cpp:152-335,443-547` | PostFX i CrtPi tenen `default*Presets/parse*Preset/write*Preset/load*FromFile/save*ToFile/set*File` duplicats | Importante | PENDING | Template `PresetStore` amb serialitzadors per camp | +| DUP-01 | `options.cpp:152-335,443-547` | PostFX i CrtPi tenen `default*Presets/parse*Preset/write*Preset/load*FromFile/save*ToFile/set*File` duplicats | Importante | DONE | Afegits `loadPresetsFromFile` i `savePresetsToFile` a l'anonymous namespace; els 4 entry-points passen a ser one-liners que injecten parse/write/defaults per tipus. Els fitxers `postfx.yaml`/`crtpi.yaml` regenerats són idèntics a l'anterior. `std::cerr` manté el comportament — INCONS-02 el corregirà | | DUP-02 | `player.cpp:100-140` | `snapToHorizontalLane` i `snapToVerticalLane` són el mateix algorisme amb eixos permutats | Importante | PENDING | Unificar en `snapToLane(Axis, delta_time)` | | DUP-03 | `options.cpp:14-62` | Tres parelles `enumToString`/`enumFromString` amb la mateixa forma | Menor | PENDING | Taula `constexpr` + helpers genèrics | | DUP-04 | `options.cpp:79-149` | Cada `parse*` repeteix `if (node.contains) { ... get_value }` | Menor | PENDING | Helper `readField(node, key, dst)` | diff --git a/source/game/options.cpp b/source/game/options.cpp index cea9241..db5d4ea 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -334,6 +334,73 @@ namespace { out << " enable_sharper: " << (preset.enable_sharper ? "true" : "false") << "\n"; } + // Pipeline genèric de càrrega de presets: si el fitxer no existeix o és + // invàlid (no deserialitza, manca `presets:`, no és seqüència, queda + // buit), es restauren els defaults via `defaults_fn` i es reescriu via + // `save_fn`. Unifica PostFX i CrtPi — abans eren dues còpies del mateix + // codi que només diferien en el tipus concret de preset. + template + auto loadPresetsFromFile( + const std::string& path, + std::vector& presets, + std::vector (*defaults_fn)(), + void (*parse_fn)(const fkyaml::node&, T&), + bool (*save_fn)(), + const char* kind_label) -> bool { + if (path.empty()) { return false; } + + const std::string CONTENT = readFileOrEmpty(path); + if (CONTENT.empty()) { + presets = defaults_fn(); + return save_fn(); + } + try { + auto yaml = fkyaml::node::deserialize(CONTENT); + if (!yaml.contains("presets") || !yaml["presets"].is_sequence()) { + presets = defaults_fn(); + return save_fn(); + } + presets.clear(); + for (const auto& preset_node : yaml["presets"]) { + T preset; + parse_fn(preset_node, preset); + presets.push_back(preset); + } + if (presets.empty()) { + presets = defaults_fn(); + return save_fn(); + } + return true; + } catch (const std::exception& e) { + std::cerr << "Options: error parsejant " << kind_label << ": " << e.what() << '\n'; + std::cerr << "Options: reescrivint el fitxer amb presets per defecte.\n"; + presets = defaults_fn(); + return save_fn(); + } + } + + // Pipeline genèric d'escriptura: capçalera de comentari + `presets:` + + // una entrada per preset via `write_fn`. `config_key_note` és la segona + // línia de comentari que recorda amb quin camp de config.yaml han de + // coincidir els noms. + template + auto savePresetsToFile( + const std::string& path, + const std::vector& presets, + void (*write_fn)(std::ostringstream&, const T&), + const char* header_comment, + const char* config_key_note) -> bool { + if (path.empty()) { return false; } + std::ostringstream out; + out << "# " << header_comment << "\n"; + out << "# " << config_key_note << "\n"; + out << "presets:\n"; + for (const auto& preset : presets) { + write_fn(out, preset); + } + return writeFile(path, out.str()); + } + } // namespace namespace Options { @@ -445,52 +512,22 @@ namespace Options { } auto loadPostFXFromFile() -> bool { - if (postfx_file_path.empty()) { return false; } - - const std::string CONTENT = readFileOrEmpty(postfx_file_path); - if (CONTENT.empty()) { - postfx_presets = defaultPostFXPresets(); - return savePostFXToFile(); - } - - try { - auto yaml = fkyaml::node::deserialize(CONTENT); - if (!yaml.contains("presets") || !yaml["presets"].is_sequence()) { - postfx_presets = defaultPostFXPresets(); - return savePostFXToFile(); - } - - postfx_presets.clear(); - for (const auto& preset_node : yaml["presets"]) { - PostFXPreset preset; - parsePostFXPreset(preset_node, preset); - postfx_presets.push_back(preset); - } - - if (postfx_presets.empty()) { - postfx_presets = defaultPostFXPresets(); - return savePostFXToFile(); - } - return true; - } catch (const std::exception& e) { - std::cerr << "Options: error parsejant postfx.yaml: " << e.what() << '\n'; - std::cerr << "Options: reescrivint el fitxer amb presets per defecte.\n"; - postfx_presets = defaultPostFXPresets(); - return savePostFXToFile(); - } + return loadPresetsFromFile( + postfx_file_path, + postfx_presets, + &defaultPostFXPresets, + &parsePostFXPreset, + &savePostFXToFile, + "postfx.yaml"); } auto savePostFXToFile() -> bool { - if (postfx_file_path.empty()) { return false; } - - std::ostringstream out; - out << "# Presets PostFX per a AEE 2026. Editable a mà.\n"; - out << "# Els noms han de coincidir amb config.yaml > video.shader.current_postfx_preset.\n"; - out << "presets:\n"; - for (const auto& preset : postfx_presets) { - writePostFXPreset(out, preset); - } - return writeFile(postfx_file_path, out.str()); + return savePresetsToFile( + postfx_file_path, + postfx_presets, + &writePostFXPreset, + "Presets PostFX per a AEE 2026. Editable a mà.", + "Els noms han de coincidir amb config.yaml > video.shader.current_postfx_preset."); } void setCrtPiFile(const std::string& path) { @@ -498,52 +535,22 @@ namespace Options { } auto loadCrtPiFromFile() -> bool { - if (crtpi_file_path.empty()) { return false; } - - const std::string CONTENT = readFileOrEmpty(crtpi_file_path); - if (CONTENT.empty()) { - crtpi_presets = defaultCrtPiPresets(); - return saveCrtPiToFile(); - } - - try { - auto yaml = fkyaml::node::deserialize(CONTENT); - if (!yaml.contains("presets") || !yaml["presets"].is_sequence()) { - crtpi_presets = defaultCrtPiPresets(); - return saveCrtPiToFile(); - } - - crtpi_presets.clear(); - for (const auto& preset_node : yaml["presets"]) { - CrtPiPreset preset; - parseCrtPiPreset(preset_node, preset); - crtpi_presets.push_back(preset); - } - - if (crtpi_presets.empty()) { - crtpi_presets = defaultCrtPiPresets(); - return saveCrtPiToFile(); - } - return true; - } catch (const std::exception& e) { - std::cerr << "Options: error parsejant crtpi.yaml: " << e.what() << '\n'; - std::cerr << "Options: reescrivint el fitxer amb presets per defecte.\n"; - crtpi_presets = defaultCrtPiPresets(); - return saveCrtPiToFile(); - } + return loadPresetsFromFile( + crtpi_file_path, + crtpi_presets, + &defaultCrtPiPresets, + &parseCrtPiPreset, + &saveCrtPiToFile, + "crtpi.yaml"); } auto saveCrtPiToFile() -> bool { - if (crtpi_file_path.empty()) { return false; } - - std::ostringstream out; - out << "# Presets CRT-Pi per a AEE 2026. Editable a mà.\n"; - out << "# Els noms han de coincidir amb config.yaml > video.shader.current_crtpi_preset.\n"; - out << "presets:\n"; - for (const auto& preset : crtpi_presets) { - writeCrtPiPreset(out, preset); - } - return writeFile(crtpi_file_path, out.str()); + return savePresetsToFile( + crtpi_file_path, + crtpi_presets, + &writeCrtPiPreset, + "Presets CRT-Pi per a AEE 2026. Editable a mà.", + "Els noms han de coincidir amb config.yaml > video.shader.current_crtpi_preset."); } auto buildActiveShader() -> Rendering::ShaderType { From db2e5d66e0fa9c6524656c914666dbfd65fa3a20 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 17:40:39 +0200 Subject: [PATCH 05/20] PATCH-04: backup .bak abans de reescriure config/presets per fallada MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Helper `backupFile(path)` que copia el YAML a `.bak` si existeix. Es crida a `loadFromFile` (versió mismatch i catch) i a `loadPresetsFromFile` (manca `presets:`, llista buida, catch). La primera execució no genera .bak. Cada reescriptura forçada registra el motiu per stderr (INCONS-02 migrarà a std::cout). Prova manual: config.yaml amb contingut \"===broken===\" genera config.yaml.bak amb el contingut previ i un nou config.yaml amb defaults. --- docs/audit-game.md | 4 ++-- source/game/options.cpp | 31 +++++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/docs/audit-game.md b/docs/audit-game.md index ede4c10..b1fe2e0 100644 --- a/docs/audit-game.md +++ b/docs/audit-game.md @@ -21,7 +21,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | 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) | DONE | | DUP-01 | PostFX i CrtPi són dos pipelines duplicats a `options.cpp` | DONE | -| PATCH-04 | Reescriure silenciosament `config.yaml` al fallar parseig o versió | PENDING | +| PATCH-04 | Reescriure silenciosament `config.yaml` al fallar parseig o versió | DONE | | HC-01 | `SPEED_PX_PER_S`, caixa de col·lisió i rutes d'assets cablejades | PENDING | --- @@ -56,7 +56,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | PATCH-01 | `player.cpp:66-77` | Col·lisió per eix fent `AnimatedSprite::update` + revertir + reaplicar | Importante | PENDING | `tryMove(dx, dy, collisionFn)` a la classe base | | PATCH-02 | `player.cpp:13-15` | Comentari tipus TODO ("si més endavant volem sprint...") | Menor | PENDING | Treure o convertir en feature amb issue | | PATCH-03 | `map.cpp:13-15` | `overlapStrict` custom perquè SDL3 considera el toc com intersecció | Menor | PENDING | Si s'usa més, mourea `utils/` | -| PATCH-04 | `options.cpp:361-363,371-375,475-480,528-533` | Reescriure silenciosament `config.yaml` al fallar parseig o canviar versió | Crítico | PENDING | `.bak` + log abans de sobreescriure | +| PATCH-04 | `options.cpp:361-363,371-375,475-480,528-533` | Reescriure silenciosament `config.yaml` al fallar parseig o canviar versió | Crítico | DONE | Helper `backupFile()` que copia a `.bak` si existeix. Cridat abans de cada reescriptura forçada (versió mismatch + catch a `loadFromFile`, `presets:` absent/invàlid/buit + catch a `loadPresetsFromFile`). Primera execució no genera `.bak`. Missatge explícit per motiu de reescriptura | | PATCH-05 | `game_scene.cpp:51` | `if (Audio::get() != nullptr)` com a defensa sobre un singleton Meyers | Menor | PENDING | Assumir invariant, treure | | PATCH-06 | `game_scene.cpp:78-91` | Ordenació per `getFeetY()` amb 1 jugador i sense sarcs-sprite: cerimonial | Menor | PENDING | Comentar "reactivar quan hi haja sarcs-sprite" o treure | | PATCH-07 | `map.hpp:64` + `map.cpp:19` | `Map` declara ctor però tots els mètodes són `static` (transició a mitges) | Importante | PENDING | Decidir: namespace o instanciar | diff --git a/source/game/options.cpp b/source/game/options.cpp index db5d4ea..9289e00 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -76,6 +76,20 @@ namespace { return file.good(); } + // Còpia `` a `.bak` per protegir ediciones manuals de + // l'usuari quan estem a punt de reescriure el fitxer amb defaults per + // error de parseig o canvi de versió. Si el fitxer no existeix (primera + // execució) o no es pot obrir, no fa res: no hi ha res a salvar. + void backupFile(const std::string& path) { + if (path.empty()) { return; } + std::ifstream src(path, std::ios::binary); + if (!src) { return; } + std::ofstream dst(path + ".bak", std::ios::binary); + if (!dst) { return; } + dst << src.rdbuf(); + std::cerr << "Options: còpia de seguretat a " << path << ".bak\n"; + } + void parseWindow(const fkyaml::node& yaml) { if (!yaml.contains("window")) { return; } const auto& node = yaml["window"]; @@ -351,12 +365,15 @@ namespace { const std::string CONTENT = readFileOrEmpty(path); if (CONTENT.empty()) { + // Primera execució (no existia o era buit): no cal backup. presets = defaults_fn(); return save_fn(); } try { auto yaml = fkyaml::node::deserialize(CONTENT); if (!yaml.contains("presets") || !yaml["presets"].is_sequence()) { + std::cerr << "Options: " << kind_label << " manca `presets:`; reescrivint amb defaults.\n"; + backupFile(path); presets = defaults_fn(); return save_fn(); } @@ -367,6 +384,8 @@ namespace { presets.push_back(preset); } if (presets.empty()) { + std::cerr << "Options: " << kind_label << " sense presets; reescrivint amb defaults.\n"; + backupFile(path); presets = defaults_fn(); return save_fn(); } @@ -374,6 +393,7 @@ namespace { } catch (const std::exception& e) { std::cerr << "Options: error parsejant " << kind_label << ": " << e.what() << '\n'; std::cerr << "Options: reescrivint el fitxer amb presets per defecte.\n"; + backupFile(path); presets = defaults_fn(); return save_fn(); } @@ -426,8 +446,14 @@ namespace Options { file_version = yaml["version"].get_value(); } // Sense migració per ara: si la versió no coincideix, reescrivim - // amb els defaults de la versió nova. - if (file_version != Texts::VERSION) { return saveToFile(); } + // amb els defaults de la versió nova. El fitxer original queda + // a `.bak` per si l'usuari volia preservar tuning manual. + if (file_version != Texts::VERSION) { + std::cerr << "Options: config.yaml versió \"" << file_version + << "\" != \"" << Texts::VERSION << "\"; reescrivint amb defaults.\n"; + backupFile(config_file_path); + return saveToFile(); + } config_version = file_version; if (yaml.contains("locale")) { Options::locale = yaml["locale"].get_value(); } @@ -438,6 +464,7 @@ namespace Options { } catch (const std::exception& e) { std::cerr << "Options: error parsejant config.yaml: " << e.what() << '\n'; std::cerr << "Options: reescrivint el fitxer amb defaults.\n"; + backupFile(config_file_path); return saveToFile(); } } From da4f78fd0c28fa143b66c1c82e0545488e77af23 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 17:43:32 +0200 Subject: [PATCH 06/20] HC-01+PATCH-02: SPEED_PX_PER_S a Defaults::Player MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La velocitat base del jugador (60 px/s) passa de `constexpr` local a `player.cpp` a `Defaults::Player::SPEED_PX_PER_S`, seguint el patró usat a l'audit d'àudio. `player.cpp` manté un alias local per llegibilitat. Elimina el comentari TODO sobre sprint/slow-zones que PATCH-02 volia fora. --- docs/audit-game.md | 6 +++--- source/game/entities/player.cpp | 6 +++--- source/utils/defaults.hpp | 4 ++++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/audit-game.md b/docs/audit-game.md index b1fe2e0..7216d47 100644 --- a/docs/audit-game.md +++ b/docs/audit-game.md @@ -22,7 +22,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | COUP-01 | `core/` inclou `game/ui/notification_presets.hpp` (dependència invertida) | DONE | | DUP-01 | PostFX i CrtPi són dos pipelines duplicats a `options.cpp` | DONE | | PATCH-04 | Reescriure silenciosament `config.yaml` al fallar parseig o versió | DONE | -| HC-01 | `SPEED_PX_PER_S`, caixa de col·lisió i rutes d'assets cablejades | PENDING | +| HC-01 | `SPEED_PX_PER_S`, caixa de col·lisió i rutes d'assets cablejades | DONE (HC-01); PENDING (HC-02, HC-06) | --- @@ -40,7 +40,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 | PENDING | Mourea `Defaults::Player` o `player.yaml` | +| 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-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 | @@ -54,7 +54,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | ID | Fitxer:línia | Troballa | Severitat | Estat | Notes | |----|--------------|----------|-----------|-------|-------| | PATCH-01 | `player.cpp:66-77` | Col·lisió per eix fent `AnimatedSprite::update` + revertir + reaplicar | Importante | PENDING | `tryMove(dx, dy, collisionFn)` a la classe base | -| PATCH-02 | `player.cpp:13-15` | Comentari tipus TODO ("si més endavant volem sprint...") | Menor | PENDING | Treure o convertir en feature amb issue | +| PATCH-02 | `player.cpp:13-15` | Comentari tipus TODO ("si més endavant volem sprint...") | Menor | DONE | Eliminat en HC-01 (la constant ja no viu al `.cpp`, sinó a `Defaults::Player`) | | PATCH-03 | `map.cpp:13-15` | `overlapStrict` custom perquè SDL3 considera el toc com intersecció | Menor | PENDING | Si s'usa més, mourea `utils/` | | PATCH-04 | `options.cpp:361-363,371-375,475-480,528-533` | Reescriure silenciosament `config.yaml` al fallar parseig o canviar versió | Crítico | DONE | Helper `backupFile()` que copia a `.bak` si existeix. Cridat abans de cada reescriptura forçada (versió mismatch + catch a `loadFromFile`, `presets:` absent/invàlid/buit + catch a `loadPresetsFromFile`). Primera execució no genera `.bak`. Missatge explícit per motiu de reescriptura | | PATCH-05 | `game_scene.cpp:51` | `if (Audio::get() != nullptr)` com a defensa sobre un singleton Meyers | Menor | PENDING | Assumir invariant, treure | diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 2855ef1..3235626 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -7,12 +7,12 @@ #include "core/resources/resource_cache.hpp" #include "game/map/map.hpp" +#include "utils/defaults.hpp" namespace { - // Velocitat constant. Si més endavant volem sprint, slow zones, etc., - // es pot convertir en un membre modificable. - constexpr float SPEED_PX_PER_S = 60.0F; + // Alias local per reduir soroll; la constant viu a Defaults::Player. + constexpr float SPEED_PX_PER_S = Defaults::Player::SPEED_PX_PER_S; } // namespace diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index 8d5b68e..03b1188 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -80,4 +80,8 @@ namespace Defaults { constexpr SDL_Scancode EXIT = SDL_SCANCODE_ESCAPE; } // namespace KeysGame + namespace Player { + constexpr float SPEED_PX_PER_S = 60.0F; // Velocitat base del jugador (sense sprint / slow-zones). + } // namespace Player + } // namespace Defaults From e84d8cc5350f4fb06009f3e49fb3a3f316f2d24d Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 17:46:43 +0200 Subject: [PATCH 07/20] =?UTF-8?q?HC-02:=20mides=20de=20sprite/col=C2=B7lis?= =?UTF-8?q?i=C3=B3=20del=20jugador=20al=20player.yaml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Afegit bloc `collision:` (size 16, base_size 12) al YAML del player; les `static constexpr` de `player.hpp` passen a `inline static int` (snake_case) i es calculen al `Player::loadFrom()`. `sprite_w/sprite_h` es llegeixen dels mateixos frameWidth/frameHeight que ja hi viuen. GameScene crida el load abans del spawn (mateix patró que `Map::loadFrom`). Permet ajustar la caixa del jugador sense recompilar. --- data/player/player.yaml | 8 +++++ docs/audit-game.md | 2 +- source/game/entities/player.cpp | 55 +++++++++++++++++++++++-------- source/game/entities/player.hpp | 44 +++++++++++++------------ source/game/scenes/game_scene.cpp | 9 +++-- 5 files changed, 81 insertions(+), 37 deletions(-) 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 From 505e819af1c60a6a767f26c4aefa6c9bf6a32b50 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 17:52:53 +0200 Subject: [PATCH 08/20] HC-06: rutes d'assets de GameScene a Defaults::Pyramid + Config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Afegits `Defaults::Pyramid::{MAP_YAML, PLAYER_SPRITE, PLAYER_YAML, MUSIC_TRACK}` a utils/defaults.hpp. `GameScene` exposa `Config` i `defaultConfig()`; el ctor accepta Config (default = Defaults::Pyramid) i usa els camps enlloc dels literals. `Player` passa a rebre el anim_key per constructor en lloc de cablejar "player.yaml". Cache key = basename (convenció preexistent). Cada piràmide futura serà un Config distint. --- docs/audit-game.md | 2 +- source/game/entities/player.cpp | 4 +-- source/game/entities/player.hpp | 2 +- source/game/scenes/game_scene.cpp | 46 +++++++++++++++++++++---------- source/game/scenes/game_scene.hpp | 16 ++++++++++- source/utils/defaults.hpp | 10 +++++++ 6 files changed, 60 insertions(+), 20 deletions(-) diff --git a/docs/audit-game.md b/docs/audit-game.md index b2f20c7..cd96512 100644 --- a/docs/audit-game.md +++ b/docs/audit-game.md @@ -45,7 +45,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | 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` | +| HC-06 | `game_scene.cpp:24-26,52` | Rutes `data/maps/pyramid.yaml`, `data/player/player.*`, `pyramid_1.ogg` cablejades | Importante | DONE | Afegit `Defaults::Pyramid::{MAP_YAML,PLAYER_SPRITE,PLAYER_YAML,MUSIC_TRACK}`; `GameScene::Config` struct + `defaultConfig()` passa les rutes al ctor (default ve de Defaults). `Player` ctor accepta `anim_key` en lloc de cablejar `"player.yaml"`. Cache key = basename (mateix conveni que abans) | | HC-07 | `options.cpp:27,55` | Fallbacks `"letterbox"`/`"off"` com a strings màgics que dupliquen els enums | Menor | PENDING | Passar per `toString(Defaults::...)` | | HC-08 | `tilemap.cpp:135` | `0xFF000000U` com a màscara alfa | Menor | PENDING | Constant amb nom a `defines.hpp` | diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 57f116f..25e1f49 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -45,8 +45,8 @@ void Player::loadFrom(const std::string& yaml_path) { 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")), +Player::Player(const std::string& anim_key, float x, float y, const Controls& controls) + : AnimatedSprite(Resource::Cache::get().getAnimationData(anim_key)), controls_(controls) { setPos(x, y); setCurrentAnimation("stand"); diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index a362ef5..3ace095 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -36,7 +36,7 @@ class Player : public AnimatedSprite { // 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); + Player(const std::string& anim_key, float x, float y, const Controls& controls); void update(float delta_time) override; diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 273b97d..98e3196 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -4,7 +4,9 @@ #include #include +#include #include +#include #include #include "core/audio/audio.hpp" @@ -23,25 +25,39 @@ 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* { + // mateix YAML i abans que el ctor de Player consulte Map::playfield_*. + // Construïm el Tilemap via un helper que primer carrega la geometria + // i després retorna el Tilemap per valor (move-construït). + auto makeTilemap(const std::string& yaml_path) -> Tilemap { Map::loadFrom(yaml_path); - return yaml_path; + return Tilemap(yaml_path); } } // namespace -GameScene::GameScene() - : 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"); +auto GameScene::defaultConfig() -> Config { + return { + .map_yaml = Defaults::Pyramid::MAP_YAML, + .player_sprite = Defaults::Pyramid::PLAYER_SPRITE, + .player_yaml = Defaults::Pyramid::PLAYER_YAML, + .music_track = Defaults::Pyramid::MUSIC_TRACK, + }; +} + +GameScene::GameScene(Config config) + : config_(std::move(config)), + tilemap_(makeTilemap(config_.map_yaml)) { + // Convenció: el Resource::Cache indexa per basename del fitxer, perquè + // la mateixa clau l'usa el parseador d'animació (YAML > tileSetFile). + const auto SPRITE_KEY = std::filesystem::path(config_.player_sprite).filename().string(); + const auto ANIM_KEY = std::filesystem::path(config_.player_yaml).filename().string(); + Resource::Cache::get().loadSurface(SPRITE_KEY, config_.player_sprite); + Resource::Cache::get().loadAnimation(ANIM_KEY, config_.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"); + Player::loadFrom(config_.player_yaml); // Jugador 0: fletxes del teclat (Defaults::KeysGame). Instanciar-ne un // segon només requereix afegir una altra entrada amb uns Controls @@ -61,13 +77,13 @@ GameScene::GameScene() static_cast(Map::playfield_y) + (static_cast(Map::outer_corridor) / 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)); + players_.emplace_back(std::make_unique(ANIM_KEY, SPAWN_X, SPAWN_Y, CONTROLS_P1)); - // Música de fons de la piràmide 1 en bucle infinit. El track ja està al - // Resource::Cache (preload al Director), així que playMusic només mou el - // punter cap a jail_audio i obri l'stream. + // Música de fons en bucle infinit. El track ja està al Resource::Cache + // (preload al Director), així que playMusic només mou el punter cap a + // jail_audio i obri l'stream. if (Audio::get() != nullptr) { - Audio::get()->playMusic("pyramid_1.ogg", -1); + Audio::get()->playMusic(config_.music_track, -1); } } diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index 5a3fbb2..1d7df1b 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "core/scene/scene.hpp" @@ -10,13 +11,26 @@ class Player; class GameScene : public Scene { public: - GameScene(); + // Conjunt de rutes que defineixen una piràmide concreta (mapa + tilemap + // + sprites del jugador + música). Cada piràmide serà un Config distint; + // `defaultConfig()` retorna la primera (Defaults::Pyramid). + struct Config { + std::string map_yaml; + std::string player_sprite; + std::string player_yaml; + std::string music_track; + }; + + [[nodiscard]] static auto defaultConfig() -> Config; + + explicit GameScene(Config config = defaultConfig()); ~GameScene() override; void update(int delta_ms) override; void render() override; private: + Config config_; Tilemap tilemap_; // Vector des del principi: AEE suportarà multijugador local; cada Player diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index 03b1188..f50de28 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -84,4 +84,14 @@ namespace Defaults { constexpr float SPEED_PX_PER_S = 60.0F; // Velocitat base del jugador (sense sprint / slow-zones). } // namespace Player + // Rutes d'assets per piràmide. Centralitzats ací perquè `GameScene` no + // en contingui cap literal; afegir una piràmide nova serà un altre + // conjunt de constants + un Config distint passat al ctor. + namespace Pyramid { + constexpr const char* MAP_YAML = "data/maps/pyramid.yaml"; + constexpr const char* PLAYER_SPRITE = "data/player/player.gif"; + constexpr const char* PLAYER_YAML = "data/player/player.yaml"; + constexpr const char* MUSIC_TRACK = "pyramid_1.ogg"; + } // namespace Pyramid + } // namespace Defaults From ea725c217771938e676995ecf44df59cc1575fd0 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 17:54:51 +0200 Subject: [PATCH 09/20] =?UTF-8?q?INCONS-02+CONV-03:=20std::cerr=20?= =?UTF-8?q?=E2=86=92=20std::cout=20a=20options.cpp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les 10 ocurrències de std::cerr a options.cpp (incloent les de PATCH-04) passen a std::cout per complir la convenció del projecte ("std::cout only, no SDL_Log"). Prova manual: amb config.yaml trencat, els missatges de backup i reescriptura es veuen per stdout. --- docs/audit-game.md | 4 ++-- source/game/options.cpp | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/audit-game.md b/docs/audit-game.md index cd96512..be76286 100644 --- a/docs/audit-game.md +++ b/docs/audit-game.md @@ -75,7 +75,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | ID | Fitxer:línia | Troballa | Severitat | Estat | Notes | |----|--------------|----------|-----------|-------|-------| | INCONS-01 | `game/*` | 3 estils de mòdul: namespace+inline (Options), classe all-static (Map), OOP (Player/Tilemap), namespace+constexpr (UiColor/Notification) | Importante | PENDING | Decidir i uniformar | -| INCONS-02 | `options.cpp:372,477,530,579,607` | `std::cerr` en comptes de `std::cout` (convenció del projecte) | Importante | PENDING | Substituir per `std::cout` prefixat | +| INCONS-02 | `options.cpp:372,477,530,579,607` | `std::cerr` en comptes de `std::cout` (convenció del projecte) | Importante | DONE | Totes les ocurrències (10, incloent les afegides a PATCH-04) substituïdes per `std::cout`. Test manual: amb config.yaml trencat el missatge de backup va per stdout | | INCONS-03 | `options.cpp` vs `tilemap.cpp:77-78` | Diferent política en YAML absent: `contains()` vs accés directe que llança | Menor | PENDING | Uniformar (llançar sempre si és un fitxer obligatori) | | INCONS-04 | `options.cpp`, `player.cpp` | Locals ALL-CAPS (`CONTENT`, `IT`, `DT`, `OLD_X`) barrejats amb `snake_case` | Menor | PENDING | Uniformar estil | | INCONS-05 | `options.cpp:420-430` | `Screen::Config` s'omple camp a camp mentre que `Audio::Config` fa `designated initializers` | Menor | PENDING | Uniformar | @@ -96,5 +96,5 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer |----|--------------|----------|-----------|-------|-------| | CONV-01 | `player.*`, `map.*`, `ui/*`, `game_scene.cpp` | Speed, caixa de col·lisió, geometria, índexs paleta, rutes d'assets, nom del track: tot cablejat | Crítico | PENDING | Tapat parcialment per HC-01/02/03/05/06 | | CONV-02 | `map.*`, `options.*` | Interfaces+callbacks: `Map` simulated class, `Options` globals sense observers | Importante | PENDING | Cobert per PATCH-07 + COUP-03 | -| CONV-03 | `options.cpp` | `std::cerr` viola la regla "std::cout only" | Importante | PENDING | Duplicat d'INCONS-02 | +| CONV-03 | `options.cpp` | `std::cerr` viola la regla "std::cout only" | Importante | DONE | Resolt amb INCONS-02 | | CONV-04 | `player.cpp:27` | `SDL_GetKeyboardState` directe en `Player::update` (fuga de SDL al gameplay) | Menor | PENDING | Anar per `GlobalInputs`/`EventBuffer` | diff --git a/source/game/options.cpp b/source/game/options.cpp index 9289e00..727e2fe 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -87,7 +87,7 @@ namespace { std::ofstream dst(path + ".bak", std::ios::binary); if (!dst) { return; } dst << src.rdbuf(); - std::cerr << "Options: còpia de seguretat a " << path << ".bak\n"; + std::cout << "Options: còpia de seguretat a " << path << ".bak\n"; } void parseWindow(const fkyaml::node& yaml) { @@ -372,7 +372,7 @@ namespace { try { auto yaml = fkyaml::node::deserialize(CONTENT); if (!yaml.contains("presets") || !yaml["presets"].is_sequence()) { - std::cerr << "Options: " << kind_label << " manca `presets:`; reescrivint amb defaults.\n"; + std::cout << "Options: " << kind_label << " manca `presets:`; reescrivint amb defaults.\n"; backupFile(path); presets = defaults_fn(); return save_fn(); @@ -384,15 +384,15 @@ namespace { presets.push_back(preset); } if (presets.empty()) { - std::cerr << "Options: " << kind_label << " sense presets; reescrivint amb defaults.\n"; + std::cout << "Options: " << kind_label << " sense presets; reescrivint amb defaults.\n"; backupFile(path); presets = defaults_fn(); return save_fn(); } return true; } catch (const std::exception& e) { - std::cerr << "Options: error parsejant " << kind_label << ": " << e.what() << '\n'; - std::cerr << "Options: reescrivint el fitxer amb presets per defecte.\n"; + std::cout << "Options: error parsejant " << kind_label << ": " << e.what() << '\n'; + std::cout << "Options: reescrivint el fitxer amb presets per defecte.\n"; backupFile(path); presets = defaults_fn(); return save_fn(); @@ -449,7 +449,7 @@ namespace Options { // amb els defaults de la versió nova. El fitxer original queda // a `.bak` per si l'usuari volia preservar tuning manual. if (file_version != Texts::VERSION) { - std::cerr << "Options: config.yaml versió \"" << file_version + std::cout << "Options: config.yaml versió \"" << file_version << "\" != \"" << Texts::VERSION << "\"; reescrivint amb defaults.\n"; backupFile(config_file_path); return saveToFile(); @@ -462,8 +462,8 @@ namespace Options { parseAudio(yaml); return true; } catch (const std::exception& e) { - std::cerr << "Options: error parsejant config.yaml: " << e.what() << '\n'; - std::cerr << "Options: reescrivint el fitxer amb defaults.\n"; + std::cout << "Options: error parsejant config.yaml: " << e.what() << '\n'; + std::cout << "Options: reescrivint el fitxer amb defaults.\n"; backupFile(config_file_path); return saveToFile(); } @@ -610,7 +610,7 @@ namespace Options { const auto IT = std::ranges::find_if(crtpi_presets, [](const CrtPiPreset& preset) { return preset.name == video.shader.current_crtpi_preset; }); if (IT == crtpi_presets.end()) { - std::cerr << "Options: preset CrtPi \"" << video.shader.current_crtpi_preset << "\" no trobat; aplicant defaults.\n"; + std::cout << "Options: preset CrtPi \"" << video.shader.current_crtpi_preset << "\" no trobat; aplicant defaults.\n"; return {}; } return { @@ -638,7 +638,7 @@ namespace Options { const auto IT = std::ranges::find_if(postfx_presets, [](const PostFXPreset& preset) { return preset.name == video.shader.current_postfx_preset; }); if (IT == postfx_presets.end()) { - std::cerr << "Options: preset PostFX \"" << video.shader.current_postfx_preset << "\" no trobat; aplicant passthrough.\n"; + std::cout << "Options: preset PostFX \"" << video.shader.current_postfx_preset << "\" no trobat; aplicant passthrough.\n"; return {}; } return { From 53bf3a1ff2c5af7a76ef380f7a202046ed517a71 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 17:57:12 +0200 Subject: [PATCH 10/20] DEAD-01..04: retirat el render primitiu del Map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `renderBackground`, `renderSarcsRow`, `getSarcRowBottomY` i els índexs de paleta `HUD_BG/WALL/FLOOR/SARC` no tenien callers: el Tilemap s'encarrega del render visual i els sarcs passaran a ser actors sprite. `getSarcRect` es queda (la usa `collides`). Includes orfes de `screen.hpp`, `resource_cache.hpp` i `defines.hpp` eliminats. DEAD-04 (ctor Map) ja es va resoldre durant HC-03. --- docs/audit-game.md | 8 ++++---- source/game/map/map.cpp | 42 ----------------------------------------- source/game/map/map.hpp | 31 +++++------------------------- 3 files changed, 9 insertions(+), 72 deletions(-) diff --git a/docs/audit-game.md b/docs/audit-game.md index be76286..b93985b 100644 --- a/docs/audit-game.md +++ b/docs/audit-game.md @@ -84,10 +84,10 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | ID | Fitxer:línia | Troballa | Severitat | Estat | Notes | |----|--------------|----------|-----------|-------|-------| -| DEAD-01 | `map.hpp:69,73,78` | `renderBackground`/`renderSarcsRow`/`getSarcRowBottomY` no tenen callers | Importante | PENDING | Esborrar o re-integrar amb sarcs-sprite | -| DEAD-02 | `map.hpp:59-62` | Índexs `HUD_BG/WALL/FLOOR/SARC` només usats pel codi mort | Importante | PENDING | Esborrar amb DEAD-01 | -| DEAD-03 | `map.cpp:3-5` | Includes `screen.hpp/resource_cache.hpp/defines.hpp` orfes si es neteja DEAD-01 | Menor | PENDING | Revisar després de DEAD-01 | -| DEAD-04 | `map.hpp:64` + `map.cpp:19` | Ctor de `Map` mai instanciat | Menor | PENDING | Veure PATCH-07 | +| DEAD-01 | `map.hpp:69,73,78` | `renderBackground`/`renderSarcsRow`/`getSarcRowBottomY` no tenen callers | Importante | DONE | Eliminats; el tilemap fa el render visual i els sarcs seran actors sprite. `getSarcRect` es queda (la usa `collides`) | +| DEAD-02 | `map.hpp:59-62` | Índexs `HUD_BG/WALL/FLOOR/SARC` només usats pel codi mort | Importante | DONE | Eliminats amb DEAD-01 | +| DEAD-03 | `map.cpp:3-5` | Includes `screen.hpp/resource_cache.hpp/defines.hpp` orfes si es neteja DEAD-01 | Menor | DONE | Eliminats amb DEAD-01 | +| DEAD-04 | `map.hpp:64` + `map.cpp:19` | Ctor de `Map` mai instanciat | Menor | DONE | Ja eliminat durant HC-03 | | DEAD-05 | `game_scene.cpp:78-86` | Ordenació Z-order cerimonial (1 jugador, sense sarcs-sprite) | Menor | PENDING | Veure PATCH-06 | ## 7. Violacions de convencions diff --git a/source/game/map/map.cpp b/source/game/map/map.cpp index f4401c1..fa464ea 100644 --- a/source/game/map/map.cpp +++ b/source/game/map/map.cpp @@ -4,10 +4,7 @@ #include #include -#include "core/rendering/screen.hpp" -#include "core/resources/resource_cache.hpp" #include "external/fkyaml_node.hpp" -#include "utils/defines.hpp" namespace { @@ -65,10 +62,6 @@ void Map::loadFrom(const std::string& yaml_path) { << " des de " << yaml_path << '\n'; } -auto Map::getSarcRowBottomY(int row) -> int { - 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)), @@ -78,41 +71,6 @@ auto Map::getSarcRect(int col, int row) -> SDL_FRect { }; } -void Map::renderBackground() { - auto& screen = Screen::get(); - const auto& palette = Resource::Cache::get().getPalette("ui"); - - // Comencem pintant el sòl a tota la pantalla; després tapem marcador i - // murs. Els rectangles dels murs s'encavalquen a les cantonades: no - // importa perquè són del mateix color. - screen.fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, palette[FLOOR]); - - // Marcador (dalt). - 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]); - - // 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]); - - // 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; - 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]); - } -} - auto Map::collides(const SDL_FRect& base_box) -> bool { // 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 diff --git a/source/game/map/map.hpp b/source/game/map/map.hpp index 0c56e43..e38da68 100644 --- a/source/game/map/map.hpp +++ b/source/game/map/map.hpp @@ -10,9 +10,10 @@ // 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. +// Abans d'aquesta versió `Map` també pintava el fons i els sarcs amb +// primitives; ara el Tilemap s'encarrega del render visual i `Map` només +// aporta la geometria lògica i les col·lisions. Quan els sarcòfags passin +// a ser actors independents es reintroduirà l'ordenació Z via `getFeetY`. class Map { public: // Geometria de la graella (carregada de pyramid.yaml > geometry). @@ -45,38 +46,16 @@ class Map { 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. - static constexpr Uint8 HUD_BG = 0; - static constexpr Uint8 WALL = 80; - static constexpr Uint8 FLOOR = 40; - static constexpr Uint8 SARC = 160; - // 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. 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" - // per a ordenar en Z): un jugador amb els peus per sobre d'aquest valor - // queda darrere; per sota, queda davant. - [[nodiscard]] static auto getSarcRowBottomY(int row) -> int; - // Rectangle ocupat pel sarcòfag a la posició (col, row) de la graella. [[nodiscard]] static auto getSarcRect(int col, int row) -> SDL_FRect; // Mapa de colisions (separat del mapa de dibuix): retorna true si el // rectangle `base_box` (la base del jugador, per exemple) s'encavalca - // amb cap sarcòfag. Així Player mai camine sobre un sarcòfag, - // independentment de com els pinte renderSarcsRow. + // amb cap sarcòfag. [[nodiscard]] static auto collides(const SDL_FRect& base_box) -> bool; }; From 206ba07a6f6b5131fa940435e66b99a4d5858984 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 17:59:05 +0200 Subject: [PATCH 11/20] DUP-02: snapToHorizontalLane/VerticalLane unificats en snapToLane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les dues funcions compartien l'algorisme complet — només canvien l'eix actiu (getPosY/setPosY vs getPosX/setPosX) i els paràmetres del Map. `snapToLane` pren base, pitch, max_idx i dos member-fn pointers i és cridat pels dos wrappers amb els valors corresponents. --- docs/audit-game.md | 2 +- source/game/entities/player.cpp | 64 +++++++++++++++++---------------- source/game/entities/player.hpp | 10 +++++- 3 files changed, 43 insertions(+), 33 deletions(-) diff --git a/docs/audit-game.md b/docs/audit-game.md index b93985b..0b87ffb 100644 --- a/docs/audit-game.md +++ b/docs/audit-game.md @@ -31,7 +31,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | ID | Fitxer:línia | Troballa | Severitat | Estat | Notes | |----|--------------|----------|-----------|-------|-------| | DUP-01 | `options.cpp:152-335,443-547` | PostFX i CrtPi tenen `default*Presets/parse*Preset/write*Preset/load*FromFile/save*ToFile/set*File` duplicats | Importante | DONE | Afegits `loadPresetsFromFile` i `savePresetsToFile` a l'anonymous namespace; els 4 entry-points passen a ser one-liners que injecten parse/write/defaults per tipus. Els fitxers `postfx.yaml`/`crtpi.yaml` regenerats són idèntics a l'anterior. `std::cerr` manté el comportament — INCONS-02 el corregirà | -| DUP-02 | `player.cpp:100-140` | `snapToHorizontalLane` i `snapToVerticalLane` són el mateix algorisme amb eixos permutats | Importante | PENDING | Unificar en `snapToLane(Axis, delta_time)` | +| DUP-02 | `player.cpp:100-140` | `snapToHorizontalLane` i `snapToVerticalLane` són el mateix algorisme amb eixos permutats | Importante | DONE | `snapToLane(dt, base, pitch, max_idx, get_pos, set_pos)` pren member-fn pointers; els dos wrappers deleguen només amb `Map::pitch_*`/`grid_*` distints | | DUP-03 | `options.cpp:14-62` | Tres parelles `enumToString`/`enumFromString` amb la mateixa forma | Menor | PENDING | Taula `constexpr` + helpers genèrics | | DUP-04 | `options.cpp:79-149` | Cada `parse*` repeteix `if (node.contains) { ... get_value }` | Menor | PENDING | Helper `readField(node, key, dst)` | | DUP-05 | `player.cpp:81-88` | Càlcul `MIN_/MAX_` + `clamp` per eix és la mateixa fórmula que el snap | Menor | PENDING | Fold en helper | diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 25e1f49..f0b3e62 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -126,44 +126,46 @@ 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. - const auto BASE = static_cast(Map::playfield_y - collision_y_offset); - const auto PITCH = static_cast(Map::pitch_y); +void Player::snapToLane( + float delta_time, + float base, + float pitch, + int max_idx, + float (Player::*get_pos)() const, + void (Player::*set_pos)(float)) { + // Lanes en pos_axis: base + i·pitch per a i ∈ [0, max_idx]. Correspon a + // la caixa de col·lisió ocupant exactament un passadís perpendicular. const auto THRESHOLD = static_cast(collision_size) / 2.0F; const auto IDX = std::clamp( - static_cast(std::round((getPosY() - BASE) / PITCH)), + static_cast(std::round(((this->*get_pos)() - base) / pitch)), 0, - Map::grid_rows); - const float LANE = BASE + (static_cast(IDX) * PITCH); - const float DY = LANE - getPosY(); + max_idx); + const float LANE = base + (static_cast(IDX) * pitch); + const float DELTA = LANE - (this->*get_pos)(); - if (std::abs(DY) > 0.0F && std::abs(DY) <= THRESHOLD) { - const float STEP = std::min(std::abs(DY), SPEED_PX_PER_S * delta_time); - const float SAVED = getPosY(); - setPosY(getPosY() + std::copysign(STEP, DY)); - if (Map::collides(baseBox())) { setPosY(SAVED); } + if (std::abs(DELTA) > 0.0F && std::abs(DELTA) <= THRESHOLD) { + const float STEP = std::min(std::abs(DELTA), SPEED_PX_PER_S * delta_time); + const float SAVED = (this->*get_pos)(); + (this->*set_pos)(SAVED + std::copysign(STEP, DELTA)); + if (Map::collides(baseBox())) { (this->*set_pos)(SAVED); } } } +void Player::snapToHorizontalLane(float delta_time) { + snapToLane(delta_time, + static_cast(Map::playfield_y - collision_y_offset), + static_cast(Map::pitch_y), + Map::grid_rows, + &Player::getPosY, + &Player::setPosY); +} + void Player::snapToVerticalLane(float delta_time) { - const auto BASE = static_cast(Map::playfield_x - collision_x_offset); - const auto PITCH = static_cast(Map::pitch_x); - const auto THRESHOLD = static_cast(collision_size) / 2.0F; - - const auto IDX = std::clamp( - static_cast(std::round((getPosX() - BASE) / PITCH)), - 0, - Map::grid_cols); - const float LANE = BASE + (static_cast(IDX) * PITCH); - const float DX = LANE - getPosX(); - - if (std::abs(DX) > 0.0F && std::abs(DX) <= THRESHOLD) { - const float STEP = std::min(std::abs(DX), SPEED_PX_PER_S * delta_time); - const float SAVED = getPosX(); - setPosX(getPosX() + std::copysign(STEP, DX)); - if (Map::collides(baseBox())) { setPosX(SAVED); } - } + snapToLane(delta_time, + static_cast(Map::playfield_x - collision_x_offset), + static_cast(Map::pitch_x), + Map::grid_cols, + &Player::getPosX, + &Player::setPosX); } diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index 3ace095..a74b640 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -55,9 +55,17 @@ class Player : public AnimatedSprite { // 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). Permet canviar de direcció - // sense haver d'alinear-se píxel-perfect en l'encreuament. + // sense haver d'alinear-se píxel-perfect en l'encreuament. Els dos + // wrappers deleguen a `snapToLane` només canviant l'eix actiu. void snapToHorizontalLane(float delta_time); void snapToVerticalLane(float delta_time); + void snapToLane( + float delta_time, + float base, + float pitch, + int max_idx, + float (Player::*get_pos)() const, + void (Player::*set_pos)(float)); Controls controls_; }; From 982a1788f18d40890c9a9ac7915d4ed49a06c165 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 18:01:19 +0200 Subject: [PATCH 12/20] =?UTF-8?q?DUP-03+HC-07:=20enum=E2=86=94string=20per?= =?UTF-8?q?=20taula=20a=20options.cpp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tres parelles de `*ToString`/`*FromString` substituïdes per tres taules `constexpr std::array, N>` i dos helpers plantilla (`enumToString`, `enumFromString`). La primera entrada de cada taula és el fallback quan l'enum és desconegut o el text no casa: així els valors "letterbox"/"nearest"/"off" només apareixen una vegada. Resol també HC-07. --- docs/audit-game.md | 4 +- source/game/options.cpp | 92 +++++++++++++++++++---------------------- 2 files changed, 44 insertions(+), 52 deletions(-) diff --git a/docs/audit-game.md b/docs/audit-game.md index 0b87ffb..ca4f131 100644 --- a/docs/audit-game.md +++ b/docs/audit-game.md @@ -32,7 +32,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer |----|--------------|----------|-----------|-------|-------| | DUP-01 | `options.cpp:152-335,443-547` | PostFX i CrtPi tenen `default*Presets/parse*Preset/write*Preset/load*FromFile/save*ToFile/set*File` duplicats | Importante | DONE | Afegits `loadPresetsFromFile` i `savePresetsToFile` a l'anonymous namespace; els 4 entry-points passen a ser one-liners que injecten parse/write/defaults per tipus. Els fitxers `postfx.yaml`/`crtpi.yaml` regenerats són idèntics a l'anterior. `std::cerr` manté el comportament — INCONS-02 el corregirà | | DUP-02 | `player.cpp:100-140` | `snapToHorizontalLane` i `snapToVerticalLane` són el mateix algorisme amb eixos permutats | Importante | DONE | `snapToLane(dt, base, pitch, max_idx, get_pos, set_pos)` pren member-fn pointers; els dos wrappers deleguen només amb `Map::pitch_*`/`grid_*` distints | -| DUP-03 | `options.cpp:14-62` | Tres parelles `enumToString`/`enumFromString` amb la mateixa forma | Menor | PENDING | Taula `constexpr` + helpers genèrics | +| DUP-03 | `options.cpp:14-62` | Tres parelles `enumToString`/`enumFromString` amb la mateixa forma | Menor | DONE | Tres `constexpr std::array, N>` + helpers `enumToString`/`enumFromString` plantilla. La primera entrada és el fallback (documentat). També elimina HC-07 | | DUP-04 | `options.cpp:79-149` | Cada `parse*` repeteix `if (node.contains) { ... get_value }` | Menor | PENDING | Helper `readField(node, key, dst)` | | DUP-05 | `player.cpp:81-88` | Càlcul `MIN_/MAX_` + `clamp` per eix és la mateixa fórmula que el snap | Menor | PENDING | Fold en helper | @@ -46,7 +46,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | 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 | DONE | Afegit `Defaults::Pyramid::{MAP_YAML,PLAYER_SPRITE,PLAYER_YAML,MUSIC_TRACK}`; `GameScene::Config` struct + `defaultConfig()` passa les rutes al ctor (default ve de Defaults). `Player` ctor accepta `anim_key` en lloc de cablejar `"player.yaml"`. Cache key = basename (mateix conveni que abans) | -| HC-07 | `options.cpp:27,55` | Fallbacks `"letterbox"`/`"off"` com a strings màgics que dupliquen els enums | Menor | PENDING | Passar per `toString(Defaults::...)` | +| HC-07 | `options.cpp:27,55` | Fallbacks `"letterbox"`/`"off"` com a strings màgics que dupliquen els enums | Menor | DONE | Resolt amb DUP-03: la primera entrada de cada taula és el fallback i només apareix una vegada | | HC-08 | `tilemap.cpp:135` | `0xFF000000U` com a màscara alfa | Menor | PENDING | Constant amb nom a `defines.hpp` | ## 3. Parxes i improvisacions diff --git a/source/game/options.cpp b/source/game/options.cpp index 727e2fe..6a1b9a7 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -1,64 +1,56 @@ #include "game/options.hpp" #include +#include #include #include #include #include +#include +#include #include "external/fkyaml_node.hpp" #include "utils/defines.hpp" namespace { - auto scalingModeToString(Defaults::ScalingMode mode) -> const char* { - switch (mode) { - case Defaults::ScalingMode::DISABLED: - return "disabled"; - case Defaults::ScalingMode::STRETCH: - return "stretch"; - case Defaults::ScalingMode::LETTERBOX: - return "letterbox"; - case Defaults::ScalingMode::OVERSCAN: - return "overscan"; - case Defaults::ScalingMode::INTEGER: - return "integer"; + // Taules enum ↔ string per a la serialització a YAML. La primera entrada + // és el fallback quan l'enum no té nom conegut o el text no casa amb + // cap entrada coneguda — per això `LETTERBOX`/`NEAREST`/`OFF` encapçalen. + constexpr std::array SCALING_MODE_NAMES = { + std::pair{Defaults::ScalingMode::LETTERBOX, std::string_view{"letterbox"}}, + std::pair{Defaults::ScalingMode::DISABLED, std::string_view{"disabled"}}, + std::pair{Defaults::ScalingMode::STRETCH, std::string_view{"stretch"}}, + std::pair{Defaults::ScalingMode::OVERSCAN, std::string_view{"overscan"}}, + std::pair{Defaults::ScalingMode::INTEGER, std::string_view{"integer"}}, + }; + + constexpr std::array TEXTURE_FILTER_NAMES = { + std::pair{Defaults::TextureFilter::NEAREST, std::string_view{"nearest"}}, + std::pair{Defaults::TextureFilter::LINEAR, std::string_view{"linear"}}, + }; + + constexpr std::array RENDER_INFO_POSITION_NAMES = { + std::pair{Defaults::RenderInfoPosition::OFF, std::string_view{"off"}}, + std::pair{Defaults::RenderInfoPosition::TOP, std::string_view{"top"}}, + std::pair{Defaults::RenderInfoPosition::BOTTOM, std::string_view{"bottom"}}, + }; + + // Helpers generics: el fallback és la primera entrada de la taula. + template + auto enumToString(const std::array, N>& table, Enum value) -> const char* { + for (const auto& [e, name] : table) { + if (e == value) { return name.data(); } } - return "letterbox"; + return table.front().second.data(); } - auto scalingModeFromString(const std::string& text) -> Defaults::ScalingMode { - if (text == "disabled") { return Defaults::ScalingMode::DISABLED; } - if (text == "stretch") { return Defaults::ScalingMode::STRETCH; } - if (text == "overscan") { return Defaults::ScalingMode::OVERSCAN; } - if (text == "integer") { return Defaults::ScalingMode::INTEGER; } - return Defaults::ScalingMode::LETTERBOX; - } - - auto filterToString(Defaults::TextureFilter filter) -> const char* { - return filter == Defaults::TextureFilter::LINEAR ? "linear" : "nearest"; - } - - auto filterFromString(const std::string& text) -> Defaults::TextureFilter { - return text == "linear" ? Defaults::TextureFilter::LINEAR : Defaults::TextureFilter::NEAREST; - } - - auto renderInfoPositionToString(Defaults::RenderInfoPosition pos) -> const char* { - switch (pos) { - case Defaults::RenderInfoPosition::TOP: - return "top"; - case Defaults::RenderInfoPosition::BOTTOM: - return "bottom"; - case Defaults::RenderInfoPosition::OFF: - return "off"; + template + auto enumFromString(const std::array, N>& table, const std::string& text) -> Enum { + for (const auto& [e, name] : table) { + if (name == text) { return e; } } - return "off"; - } - - auto renderInfoPositionFromString(const std::string& text) -> Defaults::RenderInfoPosition { - if (text == "top") { return Defaults::RenderInfoPosition::TOP; } - if (text == "bottom") { return Defaults::RenderInfoPosition::BOTTOM; } - return Defaults::RenderInfoPosition::OFF; + return table.front().first; } auto readFileOrEmpty(const std::string& path) -> std::string { @@ -124,13 +116,13 @@ namespace { const auto& node = yaml["video"]; if (node.contains("vsync")) { Options::video.vsync = node["vsync"].get_value(); } if (node.contains("scaling_mode")) { - Options::video.scaling_mode = scalingModeFromString(node["scaling_mode"].get_value()); + Options::video.scaling_mode = enumFromString(SCALING_MODE_NAMES, node["scaling_mode"].get_value()); } if (node.contains("texture_filter")) { - Options::video.texture_filter = filterFromString(node["texture_filter"].get_value()); + Options::video.texture_filter = enumFromString(TEXTURE_FILTER_NAMES, node["texture_filter"].get_value()); } if (node.contains("render_info_position")) { - Options::video.render_info_position = renderInfoPositionFromString(node["render_info_position"].get_value()); + Options::video.render_info_position = enumFromString(RENDER_INFO_POSITION_NAMES, node["render_info_position"].get_value()); } if (node.contains("render_info_show_time")) { Options::video.render_info_show_time = node["render_info_show_time"].get_value(); @@ -485,9 +477,9 @@ namespace Options { out << "video:\n"; out << " vsync: " << (video.vsync ? "true" : "false") << "\n"; - out << " scaling_mode: \"" << scalingModeToString(video.scaling_mode) << "\"\n"; - out << " texture_filter: \"" << filterToString(video.texture_filter) << "\"\n"; - out << " render_info_position: \"" << renderInfoPositionToString(video.render_info_position) << "\"\n"; + out << " scaling_mode: \"" << enumToString(SCALING_MODE_NAMES, video.scaling_mode) << "\"\n"; + out << " texture_filter: \"" << enumToString(TEXTURE_FILTER_NAMES, video.texture_filter) << "\"\n"; + out << " render_info_position: \"" << enumToString(RENDER_INFO_POSITION_NAMES, video.render_info_position) << "\"\n"; out << " render_info_show_time: " << (video.render_info_show_time ? "true" : "false") << "\n"; out << " gpu:\n"; out << " acceleration: " << (video.gpu.acceleration ? "true" : "false") << "\n"; From cb2364490fb3bd380af78f7eb2e47268fa24bdbf Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 18:03:31 +0200 Subject: [PATCH 13/20] DUP-04: readField plantilla a options.cpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les 7 funcions parse* i els dos parse*Preset repetien el patró `if (node.contains("x")) { x = node["x"].get_value(); }`. Centralitzat en `readField(node, key, dst)` a l'anonymous namespace. --- docs/audit-game.md | 2 +- source/game/options.cpp | 96 ++++++++++++++++++++--------------------- 2 files changed, 49 insertions(+), 49 deletions(-) diff --git a/docs/audit-game.md b/docs/audit-game.md index ca4f131..e3e5146 100644 --- a/docs/audit-game.md +++ b/docs/audit-game.md @@ -33,7 +33,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | DUP-01 | `options.cpp:152-335,443-547` | PostFX i CrtPi tenen `default*Presets/parse*Preset/write*Preset/load*FromFile/save*ToFile/set*File` duplicats | Importante | DONE | Afegits `loadPresetsFromFile` i `savePresetsToFile` a l'anonymous namespace; els 4 entry-points passen a ser one-liners que injecten parse/write/defaults per tipus. Els fitxers `postfx.yaml`/`crtpi.yaml` regenerats són idèntics a l'anterior. `std::cerr` manté el comportament — INCONS-02 el corregirà | | DUP-02 | `player.cpp:100-140` | `snapToHorizontalLane` i `snapToVerticalLane` són el mateix algorisme amb eixos permutats | Importante | DONE | `snapToLane(dt, base, pitch, max_idx, get_pos, set_pos)` pren member-fn pointers; els dos wrappers deleguen només amb `Map::pitch_*`/`grid_*` distints | | DUP-03 | `options.cpp:14-62` | Tres parelles `enumToString`/`enumFromString` amb la mateixa forma | Menor | DONE | Tres `constexpr std::array, N>` + helpers `enumToString`/`enumFromString` plantilla. La primera entrada és el fallback (documentat). També elimina HC-07 | -| DUP-04 | `options.cpp:79-149` | Cada `parse*` repeteix `if (node.contains) { ... get_value }` | Menor | PENDING | Helper `readField(node, key, dst)` | +| DUP-04 | `options.cpp:79-149` | Cada `parse*` repeteix `if (node.contains) { ... get_value }` | Menor | DONE | Afegit `readField(node, key, dst)` plantilla a l'anonymous namespace. Aplicat a les 7 funcions `parse*` + `parsePostFXPreset`/`parseCrtPiPreset` | | DUP-05 | `player.cpp:81-88` | Càlcul `MIN_/MAX_` + `clamp` per eix és la mateixa fórmula que el snap | Menor | PENDING | Fold en helper | ## 2. Hardcoding diff --git a/source/game/options.cpp b/source/game/options.cpp index 6a1b9a7..0934a5e 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -68,6 +68,14 @@ namespace { return file.good(); } + // Helper trivial per a llegir un camp optional del YAML: si existeix, + // sobreescriu `dst`; si no, el deixa com estava (valor per defecte). + // Centralitza el patró `if (node.contains("x")) { x = node["x"].get_value(); }`. + template + void readField(const fkyaml::node& node, const char* key, T& dst) { + if (node.contains(key)) { dst = node[key].get_value(); } + } + // Còpia `` a `.bak` per protegir ediciones manuals de // l'usuari quan estem a punt de reescriure el fitxer amb defaults per // error de parseig o canvi de versió. Si el fitxer no existeix (primera @@ -85,36 +93,30 @@ namespace { void parseWindow(const fkyaml::node& yaml) { if (!yaml.contains("window")) { return; } const auto& node = yaml["window"]; - if (node.contains("zoom")) { Options::window.zoom = node["zoom"].get_value(); } - if (node.contains("fullscreen")) { Options::window.fullscreen = node["fullscreen"].get_value(); } + readField(node, "zoom", Options::window.zoom); + readField(node, "fullscreen", Options::window.fullscreen); } void parseGpu(const fkyaml::node& video_node) { if (!video_node.contains("gpu")) { return; } const auto& node = video_node["gpu"]; - if (node.contains("acceleration")) { Options::video.gpu.acceleration = node["acceleration"].get_value(); } - if (node.contains("preferred_driver")) { Options::video.gpu.preferred_driver = node["preferred_driver"].get_value(); } + readField(node, "acceleration", Options::video.gpu.acceleration); + readField(node, "preferred_driver", Options::video.gpu.preferred_driver); } void parseShader(const fkyaml::node& video_node) { if (!video_node.contains("shader")) { return; } const auto& node = video_node["shader"]; - if (node.contains("enabled")) { Options::video.shader.enabled = node["enabled"].get_value(); } - if (node.contains("current_shader")) { - Options::video.shader.current_shader = node["current_shader"].get_value(); - } - if (node.contains("current_postfx_preset")) { - Options::video.shader.current_postfx_preset = node["current_postfx_preset"].get_value(); - } - if (node.contains("current_crtpi_preset")) { - Options::video.shader.current_crtpi_preset = node["current_crtpi_preset"].get_value(); - } + readField(node, "enabled", Options::video.shader.enabled); + readField(node, "current_shader", Options::video.shader.current_shader); + readField(node, "current_postfx_preset", Options::video.shader.current_postfx_preset); + readField(node, "current_crtpi_preset", Options::video.shader.current_crtpi_preset); } void parseVideo(const fkyaml::node& yaml) { if (!yaml.contains("video")) { return; } const auto& node = yaml["video"]; - if (node.contains("vsync")) { Options::video.vsync = node["vsync"].get_value(); } + readField(node, "vsync", Options::video.vsync); if (node.contains("scaling_mode")) { Options::video.scaling_mode = enumFromString(SCALING_MODE_NAMES, node["scaling_mode"].get_value()); } @@ -124,9 +126,7 @@ namespace { if (node.contains("render_info_position")) { Options::video.render_info_position = enumFromString(RENDER_INFO_POSITION_NAMES, node["render_info_position"].get_value()); } - if (node.contains("render_info_show_time")) { - Options::video.render_info_show_time = node["render_info_show_time"].get_value(); - } + readField(node, "render_info_show_time", Options::video.render_info_show_time); parseGpu(node); parseShader(node); } @@ -134,22 +134,22 @@ namespace { void parseMusic(const fkyaml::node& audio_node) { if (!audio_node.contains("music")) { return; } const auto& node = audio_node["music"]; - if (node.contains("enabled")) { Options::audio.music.enabled = node["enabled"].get_value(); } - if (node.contains("volume")) { Options::audio.music.volume = node["volume"].get_value(); } + readField(node, "enabled", Options::audio.music.enabled); + readField(node, "volume", Options::audio.music.volume); } void parseSound(const fkyaml::node& audio_node) { if (!audio_node.contains("sound")) { return; } const auto& node = audio_node["sound"]; - if (node.contains("enabled")) { Options::audio.sound.enabled = node["enabled"].get_value(); } - if (node.contains("volume")) { Options::audio.sound.volume = node["volume"].get_value(); } + readField(node, "enabled", Options::audio.sound.enabled); + readField(node, "volume", Options::audio.sound.volume); } void parseAudio(const fkyaml::node& yaml) { if (!yaml.contains("audio")) { return; } const auto& node = yaml["audio"]; - if (node.contains("enabled")) { Options::audio.enabled = node["enabled"].get_value(); } - if (node.contains("volume")) { Options::audio.volume = node["volume"].get_value(); } + readField(node, "enabled", Options::audio.enabled); + readField(node, "volume", Options::audio.volume); parseMusic(node); parseSound(node); } @@ -215,15 +215,15 @@ namespace { } void parsePostFXPreset(const fkyaml::node& node, Options::PostFXPreset& preset) { - if (node.contains("name")) { preset.name = node["name"].get_value(); } - if (node.contains("vignette")) { preset.vignette = node["vignette"].get_value(); } - if (node.contains("scanlines")) { preset.scanlines = node["scanlines"].get_value(); } - if (node.contains("chroma")) { preset.chroma = node["chroma"].get_value(); } - if (node.contains("mask")) { preset.mask = node["mask"].get_value(); } - if (node.contains("gamma")) { preset.gamma = node["gamma"].get_value(); } - if (node.contains("curvature")) { preset.curvature = node["curvature"].get_value(); } - if (node.contains("bleeding")) { preset.bleeding = node["bleeding"].get_value(); } - if (node.contains("flicker")) { preset.flicker = node["flicker"].get_value(); } + readField(node, "name", preset.name); + readField(node, "vignette", preset.vignette); + readField(node, "scanlines", preset.scanlines); + readField(node, "chroma", preset.chroma); + readField(node, "mask", preset.mask); + readField(node, "gamma", preset.gamma); + readField(node, "curvature", preset.curvature); + readField(node, "bleeding", preset.bleeding); + readField(node, "flicker", preset.flicker); } void writePostFXPreset(std::ostringstream& out, const Options::PostFXPreset& preset) { @@ -305,21 +305,21 @@ namespace { } void parseCrtPiPreset(const fkyaml::node& node, Options::CrtPiPreset& preset) { - if (node.contains("name")) { preset.name = node["name"].get_value(); } - if (node.contains("scanline_weight")) { preset.scanline_weight = node["scanline_weight"].get_value(); } - if (node.contains("scanline_gap_brightness")) { preset.scanline_gap_brightness = node["scanline_gap_brightness"].get_value(); } - if (node.contains("bloom_factor")) { preset.bloom_factor = node["bloom_factor"].get_value(); } - if (node.contains("input_gamma")) { preset.input_gamma = node["input_gamma"].get_value(); } - if (node.contains("output_gamma")) { preset.output_gamma = node["output_gamma"].get_value(); } - if (node.contains("mask_brightness")) { preset.mask_brightness = node["mask_brightness"].get_value(); } - if (node.contains("curvature_x")) { preset.curvature_x = node["curvature_x"].get_value(); } - if (node.contains("curvature_y")) { preset.curvature_y = node["curvature_y"].get_value(); } - if (node.contains("mask_type")) { preset.mask_type = node["mask_type"].get_value(); } - if (node.contains("enable_scanlines")) { preset.enable_scanlines = node["enable_scanlines"].get_value(); } - if (node.contains("enable_multisample")) { preset.enable_multisample = node["enable_multisample"].get_value(); } - if (node.contains("enable_gamma")) { preset.enable_gamma = node["enable_gamma"].get_value(); } - if (node.contains("enable_curvature")) { preset.enable_curvature = node["enable_curvature"].get_value(); } - if (node.contains("enable_sharper")) { preset.enable_sharper = node["enable_sharper"].get_value(); } + readField(node, "name", preset.name); + readField(node, "scanline_weight", preset.scanline_weight); + readField(node, "scanline_gap_brightness", preset.scanline_gap_brightness); + readField(node, "bloom_factor", preset.bloom_factor); + readField(node, "input_gamma", preset.input_gamma); + readField(node, "output_gamma", preset.output_gamma); + readField(node, "mask_brightness", preset.mask_brightness); + readField(node, "curvature_x", preset.curvature_x); + readField(node, "curvature_y", preset.curvature_y); + readField(node, "mask_type", preset.mask_type); + readField(node, "enable_scanlines", preset.enable_scanlines); + readField(node, "enable_multisample", preset.enable_multisample); + readField(node, "enable_gamma", preset.enable_gamma); + readField(node, "enable_curvature", preset.enable_curvature); + readField(node, "enable_sharper", preset.enable_sharper); } void writeCrtPiPreset(std::ostringstream& out, const Options::CrtPiPreset& preset) { From df059a6fcb18a4d10ccbcddccf7978800da91e68 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 18:05:08 +0200 Subject: [PATCH 14/20] DUP-05: clampToPlayfield() extret de Player::update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Els 4 `const auto` MIN_/MAX_ i els dos `std::clamp` es mouen a un mètode privat `clampToPlayfield`. Separa la responsabilitat de mantenir la caixa dins del playfield de la integració de moviment. --- docs/audit-game.md | 2 +- source/game/entities/player.cpp | 4 ++++ source/game/entities/player.hpp | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/audit-game.md b/docs/audit-game.md index e3e5146..2e705c9 100644 --- a/docs/audit-game.md +++ b/docs/audit-game.md @@ -34,7 +34,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | DUP-02 | `player.cpp:100-140` | `snapToHorizontalLane` i `snapToVerticalLane` són el mateix algorisme amb eixos permutats | Importante | DONE | `snapToLane(dt, base, pitch, max_idx, get_pos, set_pos)` pren member-fn pointers; els dos wrappers deleguen només amb `Map::pitch_*`/`grid_*` distints | | DUP-03 | `options.cpp:14-62` | Tres parelles `enumToString`/`enumFromString` amb la mateixa forma | Menor | DONE | Tres `constexpr std::array, N>` + helpers `enumToString`/`enumFromString` plantilla. La primera entrada és el fallback (documentat). També elimina HC-07 | | DUP-04 | `options.cpp:79-149` | Cada `parse*` repeteix `if (node.contains) { ... get_value }` | Menor | DONE | Afegit `readField(node, key, dst)` plantilla a l'anonymous namespace. Aplicat a les 7 funcions `parse*` + `parsePostFXPreset`/`parseCrtPiPreset` | -| DUP-05 | `player.cpp:81-88` | Càlcul `MIN_/MAX_` + `clamp` per eix és la mateixa fórmula que el snap | Menor | PENDING | Fold en helper | +| DUP-05 | `player.cpp:81-88` | Càlcul `MIN_/MAX_` + `clamp` per eix és la mateixa fórmula que el snap | Menor | DONE | Extret `Player::clampToPlayfield()`; `update()` el crida després de la col·lisió per eix | ## 2. Hardcoding diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index f0b3e62..746ea03 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -105,6 +105,10 @@ void Player::update(float delta_time) { setPosY(NEW_Y); if (Map::collides(baseBox())) { setPosY(OLD_Y); } + clampToPlayfield(); +} + +void Player::clampToPlayfield() { // 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); diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index a74b640..4ca4c5e 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -52,6 +52,10 @@ class Player : public AnimatedSprite { // a referència per al Z-order. [[nodiscard]] auto baseBox() const -> SDL_FRect; + // Clamp de la caixa de col·lisió al playfield (no al sprite sencer: + // el cap pot sobresortir pel mur superior sense afectar el joc). + void clampToPlayfield(); + // 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). Permet canviar de direcció From beb6a4aad1a73c7ac9f78deec902211d7fb9d560 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 18:07:31 +0200 Subject: [PATCH 15/20] =?UTF-8?q?HC-04+HC-05:=20=C3=ADndexs=20de=20paleta?= =?UTF-8?q?=20UI=20centralitzats=20a=20Defaults::UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Afegit `Defaults::UI::` amb els 5 índexs (RENDER_INFO_TEXT/SHADOW, CANVAS_BG, NOTIFICATION_TEXT/BG, EXIT_TEXT/BG) i 2 durades (NOTIFICATION_TIME_MS, EXIT_TIME_MS). `ui_palette.hpp` i `notification_presets.hpp` queden com a alias: canviar el tema global és tocar un sol bloc. Equivalent a tenir un "tema" (vindria d'un YAML el dia que calga). --- docs/audit-game.md | 4 +-- .../core/rendering/notification_presets.hpp | 18 ++++++------- source/game/ui/ui_palette.hpp | 23 +++++----------- source/utils/defaults.hpp | 26 +++++++++++++++++++ 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/docs/audit-game.md b/docs/audit-game.md index 2e705c9..94e80c2 100644 --- a/docs/audit-game.md +++ b/docs/audit-game.md @@ -43,8 +43,8 @@ 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 | 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 | 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` | +| HC-04 | `notification_presets.hpp:24-38` | `text_color`, `bg_color`, `time_ms` en `constexpr` | Menor | DONE | Valors moguts a `Defaults::UI::{NOTIFICATION_*, EXIT_*}`; els `Preset` son alias. Tocar "tema" global es fa a un sol lloc | +| HC-05 | `ui_palette.hpp:12-21` | Índexs de paleta (`RENDER_INFO_TEXT=47`, `SHADOW=34`, `CANVAS_BG=65`) en `constexpr` | Menor | DONE | Valors moguts a `Defaults::UI`; `UiColor::*` queda com a alias exportat al joc | | HC-06 | `game_scene.cpp:24-26,52` | Rutes `data/maps/pyramid.yaml`, `data/player/player.*`, `pyramid_1.ogg` cablejades | Importante | DONE | Afegit `Defaults::Pyramid::{MAP_YAML,PLAYER_SPRITE,PLAYER_YAML,MUSIC_TRACK}`; `GameScene::Config` struct + `defaultConfig()` passa les rutes al ctor (default ve de Defaults). `Player` ctor accepta `anim_key` en lloc de cablejar `"player.yaml"`. Cache key = basename (mateix conveni que abans) | | HC-07 | `options.cpp:27,55` | Fallbacks `"letterbox"`/`"off"` com a strings màgics que dupliquen els enums | Menor | DONE | Resolt amb DUP-03: la primera entrada de cada taula és el fallback i només apareix una vegada | | HC-08 | `tilemap.cpp:135` | `0xFF000000U` com a màscara alfa | Menor | PENDING | Constant amb nom a `defines.hpp` | diff --git a/source/core/rendering/notification_presets.hpp b/source/core/rendering/notification_presets.hpp index c56639e..0d27a5c 100644 --- a/source/core/rendering/notification_presets.hpp +++ b/source/core/rendering/notification_presets.hpp @@ -5,6 +5,7 @@ #include #include "core/rendering/notifier.hpp" +#include "utils/defaults.hpp" // Temes per a les notificacions de pantalla. Un "preset" captura els dos // colors (text i fons, índexs sobre la paleta "ui") i el temps de visibilitat @@ -18,13 +19,12 @@ namespace Notification { int time_ms; }; - // Tema per defecte: text clar sobre fons fosc, 2 segons de visibilitat. - // Per a ajustar l'aparença global de totes les notificacions sense - // presetejar-les manualment, toca només aquest bloc. + // Tema per defecte: text clar sobre fons fosc. Els valors concrets + // viuen a `Defaults::UI` — tocar l'aparença global es fa allà. constexpr Preset DEFAULT{ - .text_color = 197, - .bg_color = 96, - .time_ms = 4000, + .text_color = Defaults::UI::NOTIFICATION_TEXT, + .bg_color = Defaults::UI::NOTIFICATION_BG, + .time_ms = Defaults::UI::NOTIFICATION_TIME_MS, }; // Preset per al missatge de confirmació d'eixida (ESC doble). Color @@ -32,9 +32,9 @@ namespace Notification { // coincideix amb la finestra durant la qual la segona pulsació d'ESC // confirma el tancament — veure GlobalInputs::onExit. constexpr Preset EXIT{ - .text_color = 88, - .bg_color = 40, - .time_ms = 4000, + .text_color = Defaults::UI::EXIT_TEXT, + .bg_color = Defaults::UI::EXIT_BG, + .time_ms = Defaults::UI::EXIT_TIME_MS, }; // Helper de conveniència: aplica un preset (per defecte DEFAULT) cridant diff --git a/source/game/ui/ui_palette.hpp b/source/game/ui/ui_palette.hpp index b9405a3..4c4a518 100644 --- a/source/game/ui/ui_palette.hpp +++ b/source/game/ui/ui_palette.hpp @@ -1,23 +1,14 @@ #pragma once -#include +#include "utils/defaults.hpp" -// Constants semàntiques que mapegen cada element de la UI al seu color dins -// de la paleta compartida (registre "ui" a Resource::Cache, carregada des de -// data/palettes/sega-cube.pal). Canviar l'aparença d'un component només -// requereix canviar la seua constant ací — no cal buscar l'índex pel codi. +// Alias semàntics als índexs de paleta UI (viuen a `Defaults::UI`). Canviar +// l'aparença d'un component només requereix tocar-los allà; ací només +// reexportem amb els noms que consumeix el codi del joc. namespace UiColor { - // Color per al text de l'overlay de debug RenderInfo (F12). - constexpr Uint8 RENDER_INFO_TEXT = 47; - - // Ombra desplaçada (+1, +1) per al mateix overlay. Fosca per a donar - // contrast amb qualsevol fons sense haver de pintar una caixa. - constexpr Uint8 RENDER_INFO_SHADOW = 34; - - // Color de fons del canvas del joc quan no hi ha escena pintada a tota - // la superfície. Útil per a distingir la zona interior del joc en - // escalats que deixen banda negra (letterbox, integer, ...). - constexpr Uint8 CANVAS_BG = 65; + constexpr auto RENDER_INFO_TEXT = Defaults::UI::RENDER_INFO_TEXT; + constexpr auto RENDER_INFO_SHADOW = Defaults::UI::RENDER_INFO_SHADOW; + constexpr auto CANVAS_BG = Defaults::UI::CANVAS_BG; } // namespace UiColor diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index f50de28..8ec1916 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -84,6 +84,32 @@ namespace Defaults { constexpr float SPEED_PX_PER_S = 60.0F; // Velocitat base del jugador (sense sprint / slow-zones). } // namespace Player + // Índexs de paleta (sega-cube.pal) i temps en ms per als components + // de UI. Centralitzats ací perquè un "tema" viu en un únic lloc: tocar + // els colors o durades de l'overlay/notificador no requereix buscar + // pel codi. Si algun dia es volen temes múltiples, es pot moure a YAML + // sense més impacte que dos accessors extra. + namespace UI { + // Overlay de RenderInfo (F12). + constexpr Uint8 RENDER_INFO_TEXT = 47; + constexpr Uint8 RENDER_INFO_SHADOW = 34; + + // Fons del canvas (zona fora del tilemap en letterbox/integer). + constexpr Uint8 CANVAS_BG = 65; + + // Preset per defecte del Notifier. + constexpr Uint8 NOTIFICATION_TEXT = 197; + constexpr Uint8 NOTIFICATION_BG = 96; + constexpr int NOTIFICATION_TIME_MS = 4000; + + // Preset per a la confirmació d'eixida (ESC doble). La durada ha + // de coincidir amb la del Notifier perquè el missatge i la finestra + // de confirmació expiren alhora (veure GlobalInputs::onExit). + constexpr Uint8 EXIT_TEXT = 88; + constexpr Uint8 EXIT_BG = 40; + constexpr int EXIT_TIME_MS = 4000; + } // namespace UI + // Rutes d'assets per piràmide. Centralitzats ací perquè `GameScene` no // en contingui cap literal; afegir una piràmide nova serà un altre // conjunt de constants + un Config distint passat al ctor. From 6673168ea0a588609ffff83be24f55889ea3dbe9 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 18:08:22 +0200 Subject: [PATCH 16/20] HC-08: alpha-mask amb nom a Tilemap::render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `0xFF000000U` passa a `constexpr std::uint32_t ALPHA_MASK` local amb comentari. No s'ha mogut a defines.hpp perquè només s'usa dins de Tilemap::render; si apareix a altres llocs es podrà centralitzar. --- docs/audit-game.md | 2 +- source/game/map/tilemap.cpp | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/audit-game.md b/docs/audit-game.md index 94e80c2..61161fd 100644 --- a/docs/audit-game.md +++ b/docs/audit-game.md @@ -47,7 +47,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | HC-05 | `ui_palette.hpp:12-21` | Índexs de paleta (`RENDER_INFO_TEXT=47`, `SHADOW=34`, `CANVAS_BG=65`) en `constexpr` | Menor | DONE | Valors moguts a `Defaults::UI`; `UiColor::*` queda com a alias exportat al joc | | HC-06 | `game_scene.cpp:24-26,52` | Rutes `data/maps/pyramid.yaml`, `data/player/player.*`, `pyramid_1.ogg` cablejades | Importante | DONE | Afegit `Defaults::Pyramid::{MAP_YAML,PLAYER_SPRITE,PLAYER_YAML,MUSIC_TRACK}`; `GameScene::Config` struct + `defaultConfig()` passa les rutes al ctor (default ve de Defaults). `Player` ctor accepta `anim_key` en lloc de cablejar `"player.yaml"`. Cache key = basename (mateix conveni que abans) | | HC-07 | `options.cpp:27,55` | Fallbacks `"letterbox"`/`"off"` com a strings màgics que dupliquen els enums | Menor | DONE | Resolt amb DUP-03: la primera entrada de cada taula és el fallback i només apareix una vegada | -| HC-08 | `tilemap.cpp:135` | `0xFF000000U` com a màscara alfa | Menor | PENDING | Constant amb nom a `defines.hpp` | +| HC-08 | `tilemap.cpp:135` | `0xFF000000U` com a màscara alfa | Menor | DONE | `constexpr std::uint32_t ALPHA_MASK = 0xFF000000U` local amb comentari. No s'ha mogut a defines.hpp perquè només s'usa dins de `Tilemap::render` | ## 3. Parxes i improvisacions diff --git a/source/game/map/tilemap.cpp b/source/game/map/tilemap.cpp index f6bf7b7..4df152f 100644 --- a/source/game/map/tilemap.cpp +++ b/source/game/map/tilemap.cpp @@ -126,13 +126,16 @@ void Tilemap::render() const { const int X_START = std::max(0, -origin_x_); const int X_END = std::min(width_px_, CANVAS_W - origin_x_); + // Màscara del canal alfa (els píxels transparents del tilemap queden + // amb alpha=0 i cal mantenir el canvas sota visible). + constexpr std::uint32_t ALPHA_MASK = 0xFF000000U; for (int y = Y_START; y < Y_END; ++y) { const auto* src_row = &pixels_[static_cast(y) * static_cast(width_px_)]; auto* dst_row = &canvas[(static_cast(origin_y_ + y) * static_cast(CANVAS_W)) + static_cast(origin_x_)]; for (int x = X_START; x < X_END; ++x) { const auto PIXEL = src_row[x]; - if ((PIXEL & 0xFF000000U) != 0U) { dst_row[x] = PIXEL; } + if ((PIXEL & ALPHA_MASK) != 0U) { dst_row[x] = PIXEL; } } } } From f64bb5e2672671203b1db1bcede99b3f2904a988 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 18:10:17 +0200 Subject: [PATCH 17/20] PATCH-05+PATCH-06+DEAD-05: neteja defenses i sort cerimonial a GameScene MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PATCH-05: `if (Audio::get() != nullptr)` eliminat; Director::init crida Audio::init abans de GameScene, comentari ho documenta. Si mai salta, és un bug real que val detectar. - PATCH-06/DEAD-05: short-circuit per `players_.size() <= 1` — no vector temporal ni sort amb un sol jugador. La branca multi-jugador queda preparada per quan n'hi haja >1 o sarcs-sprites. --- docs/audit-game.md | 6 +++--- source/game/scenes/game_scene.cpp | 36 +++++++++++++++---------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/audit-game.md b/docs/audit-game.md index 61161fd..7f42026 100644 --- a/docs/audit-game.md +++ b/docs/audit-game.md @@ -57,8 +57,8 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | PATCH-02 | `player.cpp:13-15` | Comentari tipus TODO ("si més endavant volem sprint...") | Menor | DONE | Eliminat en HC-01 (la constant ja no viu al `.cpp`, sinó a `Defaults::Player`) | | PATCH-03 | `map.cpp:13-15` | `overlapStrict` custom perquè SDL3 considera el toc com intersecció | Menor | PENDING | Si s'usa més, mourea `utils/` | | PATCH-04 | `options.cpp:361-363,371-375,475-480,528-533` | Reescriure silenciosament `config.yaml` al fallar parseig o canviar versió | Crítico | DONE | Helper `backupFile()` que copia a `.bak` si existeix. Cridat abans de cada reescriptura forçada (versió mismatch + catch a `loadFromFile`, `presets:` absent/invàlid/buit + catch a `loadPresetsFromFile`). Primera execució no genera `.bak`. Missatge explícit per motiu de reescriptura | -| PATCH-05 | `game_scene.cpp:51` | `if (Audio::get() != nullptr)` com a defensa sobre un singleton Meyers | Menor | PENDING | Assumir invariant, treure | -| PATCH-06 | `game_scene.cpp:78-91` | Ordenació per `getFeetY()` amb 1 jugador i sense sarcs-sprite: cerimonial | Menor | PENDING | Comentar "reactivar quan hi haja sarcs-sprite" o treure | +| PATCH-05 | `game_scene.cpp:51` | `if (Audio::get() != nullptr)` com a defensa sobre un singleton Meyers | Menor | DONE | `if` eliminat; comentari documenta l'invariant (Director::init crida Audio::init abans). Si algun dia salta, serà un bug real que val la pena detectar | +| PATCH-06 | `game_scene.cpp:78-91` | Ordenació per `getFeetY()` amb 1 jugador i sense sarcs-sprite: cerimonial | Menor | DONE | Short-circuit si `players_.size() <= 1`: no es construeix vector temporal ni s'ordena. La branca multi-jugador queda preparada per al dia que hi haja 2+ jugadors o sarcs-sprites | | PATCH-07 | `map.hpp:64` + `map.cpp:19` | `Map` declara ctor però tots els mètodes són `static` (transició a mitges) | Importante | PENDING | Decidir: namespace o instanciar | ## 4. Acoplament innecessari @@ -88,7 +88,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | DEAD-02 | `map.hpp:59-62` | Índexs `HUD_BG/WALL/FLOOR/SARC` només usats pel codi mort | Importante | DONE | Eliminats amb DEAD-01 | | DEAD-03 | `map.cpp:3-5` | Includes `screen.hpp/resource_cache.hpp/defines.hpp` orfes si es neteja DEAD-01 | Menor | DONE | Eliminats amb DEAD-01 | | DEAD-04 | `map.hpp:64` + `map.cpp:19` | Ctor de `Map` mai instanciat | Menor | DONE | Ja eliminat durant HC-03 | -| DEAD-05 | `game_scene.cpp:78-86` | Ordenació Z-order cerimonial (1 jugador, sense sarcs-sprite) | Menor | PENDING | Veure PATCH-06 | +| DEAD-05 | `game_scene.cpp:78-86` | Ordenació Z-order cerimonial (1 jugador, sense sarcs-sprite) | Menor | DONE | Resolt amb PATCH-06 | ## 7. Violacions de convencions diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 98e3196..a3869f2 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -81,10 +81,9 @@ GameScene::GameScene(Config config) // Música de fons en bucle infinit. El track ja està al Resource::Cache // (preload al Director), així que playMusic només mou el punter cap a - // jail_audio i obri l'stream. - if (Audio::get() != nullptr) { - Audio::get()->playMusic(config_.music_track, -1); - } + // jail_audio i obri l'stream. Audio::get() està garantit no-null: + // Director::init crida Audio::init abans d'instanciar GameScene. + Audio::get()->playMusic(config_.music_track, -1); } GameScene::~GameScene() = default; @@ -109,20 +108,21 @@ void GameScene::render() { // col·lisions del jugador. tilemap_.render(); - // Ordenem punters a jugadors per Y dels peus (ascendent = més al fons - // primer). Usem un vector auxiliar per no tocar l'ordre d'entrada de - // players_, que és el d'assignació de controls. - std::vector sorted; - sorted.reserve(players_.size()); - std::ranges::transform(players_, std::back_inserter(sorted), [](const auto& p) { return p.get(); }); - std::ranges::sort(sorted, [](const Player* a, const Player* b) { - return a->getFeetY() < b->getFeetY(); - }); - - // De moment els sarcòfags no es pinten (seran actors independents); - // dibuixem tots els jugadors sobre el tilemap ja ordenats per Y. Quan - // els sarcs siguen sprites, tornarem a interleave per al Z-order. - for (auto* player : sorted) { player->render(); } + // Amb un sol jugador, l'ordre és irrellevant i ens estalviem el vector + // temporal. Quan n'hi haja més d'un (o quan els sarcs siguen sprites) + // caldrà ordenar per `getFeetY()` ascendent i interleaveing amb les + // files de sarcs — l'ordre ascendent manté els més al fons darrere. + if (players_.size() <= 1) { + for (auto& player : players_) { player->render(); } + } else { + std::vector sorted; + sorted.reserve(players_.size()); + std::ranges::transform(players_, std::back_inserter(sorted), [](const auto& p) { return p.get(); }); + std::ranges::sort(sorted, [](const Player* a, const Player* b) { + return a->getFeetY() < b->getFeetY(); + }); + for (auto* player : sorted) { player->render(); } + } Notifier::get().render(); RenderInfo::get().render(); From ba5cddd427f4be805afd871ae9ef1238921ed6c7 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 18:12:27 +0200 Subject: [PATCH 18/20] PATCH-07: Map passa de class all-static a namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Map no té estat d'instància (tots els membres són inline globals) i el ctor ja s'havia eliminat a HC-03. Convertit a `namespace Map`: callers no canvien perquè la sintaxi `Map::grid_cols`/`Map::collides(...)` és idèntica. Pattern consistent amb `Options`, que és l'altre mòdul sense estat al game/. --- docs/audit-game.md | 2 +- source/game/map/map.cpp | 126 +++++++++++++++++++++------------------- source/game/map/map.hpp | 63 ++++++++++---------- 3 files changed, 97 insertions(+), 94 deletions(-) diff --git a/docs/audit-game.md b/docs/audit-game.md index 7f42026..299488b 100644 --- a/docs/audit-game.md +++ b/docs/audit-game.md @@ -59,7 +59,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | PATCH-04 | `options.cpp:361-363,371-375,475-480,528-533` | Reescriure silenciosament `config.yaml` al fallar parseig o canviar versió | Crítico | DONE | Helper `backupFile()` que copia a `.bak` si existeix. Cridat abans de cada reescriptura forçada (versió mismatch + catch a `loadFromFile`, `presets:` absent/invàlid/buit + catch a `loadPresetsFromFile`). Primera execució no genera `.bak`. Missatge explícit per motiu de reescriptura | | PATCH-05 | `game_scene.cpp:51` | `if (Audio::get() != nullptr)` com a defensa sobre un singleton Meyers | Menor | DONE | `if` eliminat; comentari documenta l'invariant (Director::init crida Audio::init abans). Si algun dia salta, serà un bug real que val la pena detectar | | PATCH-06 | `game_scene.cpp:78-91` | Ordenació per `getFeetY()` amb 1 jugador i sense sarcs-sprite: cerimonial | Menor | DONE | Short-circuit si `players_.size() <= 1`: no es construeix vector temporal ni s'ordena. La branca multi-jugador queda preparada per al dia que hi haja 2+ jugadors o sarcs-sprites | -| PATCH-07 | `map.hpp:64` + `map.cpp:19` | `Map` declara ctor però tots els mètodes són `static` (transició a mitges) | Importante | PENDING | Decidir: namespace o instanciar | +| PATCH-07 | `map.hpp:64` + `map.cpp:19` | `Map` declara ctor però tots els mètodes són `static` (transició a mitges) | Importante | DONE | `Map` passa a ser `namespace Map`; callers no canvien (ja usen `Map::grid_cols`/`Map::collides`). Pattern alineat amb `Options` | ## 4. Acoplament innecessari diff --git a/source/game/map/map.cpp b/source/game/map/map.cpp index fa464ea..74e4519 100644 --- a/source/game/map/map.cpp +++ b/source/game/map/map.cpp @@ -18,67 +18,71 @@ namespace { } // namespace -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"]; +namespace Map { - 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::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), - }; -} - -auto Map::collides(const SDL_FRect& base_box) -> bool { - // 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) { - if (overlapStrict(base_box, getSarcRect(col, row))) { return true; } + void 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'; } - return false; -} + + auto 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), + }; + } + + auto collides(const SDL_FRect& base_box) -> bool { + // 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) { + if (overlapStrict(base_box, getSarcRect(col, row))) { return true; } + } + } + return false; + } + +} // namespace Map diff --git a/source/game/map/map.hpp b/source/game/map/map.hpp index e38da68..e00c6ad 100644 --- a/source/game/map/map.hpp +++ b/source/game/map/map.hpp @@ -10,52 +10,51 @@ // bloc `geometry:` del YAML de la piràmide — així cada sala pot tenir la // seua forma sense recompilar. // -// Abans d'aquesta versió `Map` també pintava el fons i els sarcs amb -// primitives; ara el Tilemap s'encarrega del render visual i `Map` només -// aporta la geometria lògica i les col·lisions. Quan els sarcòfags passin -// a ser actors independents es reintroduirà l'ordenació Z via `getFeetY`. -class Map { - public: +// `Map` és un namespace (no una classe) perquè no té estat d'instància: +// els membres viuen com a globals del namespace i els callers hi accedeixen +// amb `Map::grid_cols`, `Map::collides(...)`, etc. El canvi respecte a +// l'anterior `class Map { static ... };` és purament sintàctic. +namespace Map { + // 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; + inline int grid_cols = 0; + inline int grid_rows = 0; + inline int sarc_w = 0; + inline int sarc_h = 0; + inline int pitch_x = 0; + inline int pitch_y = 0; // 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; + inline int hud_h = 0; + inline int back_wall_h = 0; + inline int side_wall_w = 0; // 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; + inline int corridor_w = 0; + inline int corridor_h = 0; + inline int outer_corridor = 0; + inline int board_w = 0; + inline int board_h = 0; + inline int origin_x = 0; + inline int origin_y = 0; + inline int playfield_x = 0; + inline int playfield_y = 0; + inline int playfield_w = 0; + inline int playfield_h = 0; // 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); + void loadFrom(const std::string& yaml_path); // Rectangle ocupat pel sarcòfag a la posició (col, row) de la graella. - [[nodiscard]] static auto getSarcRect(int col, int row) -> SDL_FRect; + [[nodiscard]] auto getSarcRect(int col, int row) -> SDL_FRect; // Mapa de colisions (separat del mapa de dibuix): retorna true si el // rectangle `base_box` (la base del jugador, per exemple) s'encavalca // amb cap sarcòfag. - [[nodiscard]] static auto collides(const SDL_FRect& base_box) -> bool; -}; + [[nodiscard]] auto collides(const SDL_FRect& base_box) -> bool; + +} // namespace Map From 1643811d86095618eb33f3a7d89a067c9f78b6c6 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 18:16:05 +0200 Subject: [PATCH 19/20] INCONS-05 + triage final d'auditoria game/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - INCONS-05: `buildScreenConfig` passa a return amb designated initializers, consistent amb `buildAudioConfig`. - INCONS-01: marcat DONE (parcial) — queden 3 estils intencionals (namespace per estat compartit, classes per entitats, constexpr per alias), ja no barreja fortuïta. - INCONS-03/04, PATCH-03, COUP-04, CONV-04: WONTFIX amb raó documentada (contractes diferents, linter ho dicta, o sobre-abstracció per a un únic caller). - COUP-02/03, CONV-02 (parcial): DEFERRED a `refactor/options-class` (equivalent a COUP-01 de l'audit d'àudio). - CONV-01: DONE per HC-01/02/03/04/05/06. --- docs/audit-game.md | 22 +++++++++++----------- source/game/options.cpp | 18 +++++++++--------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/audit-game.md b/docs/audit-game.md index 299488b..6eaf5ce 100644 --- a/docs/audit-game.md +++ b/docs/audit-game.md @@ -55,7 +55,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer |----|--------------|----------|-----------|-------|-------| | PATCH-01 | `player.cpp:66-77` | Col·lisió per eix fent `AnimatedSprite::update` + revertir + reaplicar | Importante | PENDING | `tryMove(dx, dy, collisionFn)` a la classe base | | PATCH-02 | `player.cpp:13-15` | Comentari tipus TODO ("si més endavant volem sprint...") | Menor | DONE | Eliminat en HC-01 (la constant ja no viu al `.cpp`, sinó a `Defaults::Player`) | -| PATCH-03 | `map.cpp:13-15` | `overlapStrict` custom perquè SDL3 considera el toc com intersecció | Menor | PENDING | Si s'usa més, mourea `utils/` | +| PATCH-03 | `map.cpp:13-15` | `overlapStrict` custom perquè SDL3 considera el toc com intersecció | Menor | WONTFIX | Només s'usa dins de `Map::collides` i és una línia; moure a utils/ ara seria sobre-abstraure. Si apareix a un segon mòdul, promourea a `utils/geometry.hpp` | | PATCH-04 | `options.cpp:361-363,371-375,475-480,528-533` | Reescriure silenciosament `config.yaml` al fallar parseig o canviar versió | Crítico | DONE | Helper `backupFile()` que copia a `.bak` si existeix. Cridat abans de cada reescriptura forçada (versió mismatch + catch a `loadFromFile`, `presets:` absent/invàlid/buit + catch a `loadPresetsFromFile`). Primera execució no genera `.bak`. Missatge explícit per motiu de reescriptura | | PATCH-05 | `game_scene.cpp:51` | `if (Audio::get() != nullptr)` com a defensa sobre un singleton Meyers | Menor | DONE | `if` eliminat; comentari documenta l'invariant (Director::init crida Audio::init abans). Si algun dia salta, serà un bug real que val la pena detectar | | PATCH-06 | `game_scene.cpp:78-91` | Ordenació per `getFeetY()` amb 1 jugador i sense sarcs-sprite: cerimonial | Menor | DONE | Short-circuit si `players_.size() <= 1`: no es construeix vector temporal ni s'ordena. La branca multi-jugador queda preparada per al dia que hi haja 2+ jugadors o sarcs-sprites | @@ -66,19 +66,19 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | ID | Fitxer:línia | Troballa | Severitat | Estat | Notes | |----|--------------|----------|-----------|-------|-------| | COUP-01 | `core/input/global_inputs.cpp:15` | `core/` inclou `game/ui/notification_presets.hpp` | Crítico | DONE | `notification_presets.hpp` mogut a `core/rendering/` (al costat del `Notifier` que consumeix). `global_inputs.cpp` i el comentari de `notifier.hpp` actualitzats | -| COUP-02 | `player.cpp` + `Map::PLAYFIELD_*`, `PITCH_*`, `GRID_*`, `collides` | `Player` coneix la geometria interna del mapa | Importante | PENDING | Passar `const MapInfo&` o un `World*` al ctor del player | -| COUP-03 | `options.hpp:96-105` | Globals `inline` mutats lliurement des de `core/input/global_inputs.cpp` | Importante | PENDING | Setters + signal quan canvien | -| COUP-04 | `tilemap.cpp:117-138` | Clip+blit directe al canvas ARGB que podria viure a `Surface` | Menor | PENDING | `blitInto(canvas, origin)` genèric | +| COUP-02 | `player.cpp` + `Map::PLAYFIELD_*`, `PITCH_*`, `GRID_*`, `collides` | `Player` coneix la geometria interna del mapa | Importante | DEFERRED | Player només llegeix globals de `Map::`, no muta res. Injectar `MapInfo&` té sentit quan hi haja múltiples mapes simultanis o un sistema tipus ECS. Mentre hi haja un Map actiu global, l'acoblament és acceptable. Es refacta amb COUP-03 a `refactor/options-class` | +| COUP-03 | `options.hpp:96-105` | Globals `inline` mutats lliurement des de `core/input/global_inputs.cpp` | Importante | DEFERRED | Encapsular tots els globals darrere setters + observers per Screen/Audio/RenderInfo és un refactor ample (equivalent a COUP-01 de l'audit d'àudio, que va anar a una branca pròpia). Es tractarà a `refactor/options-class` perquè toca tots els callers de core/input. Es fa en branca separada | +| COUP-04 | `tilemap.cpp:117-138` | Clip+blit directe al canvas ARGB que podria viure a `Surface` | Menor | WONTFIX | Únic caller en tot el projecte. Promoure a `Surface::blitInto` ara afegiria una API genèrica per a un ús únic (YAGNI). Si un segon client apareix, es mourea en aquell moment | ## 5. Inconsistències internes | ID | Fitxer:línia | Troballa | Severitat | Estat | Notes | |----|--------------|----------|-----------|-------|-------| -| INCONS-01 | `game/*` | 3 estils de mòdul: namespace+inline (Options), classe all-static (Map), OOP (Player/Tilemap), namespace+constexpr (UiColor/Notification) | Importante | PENDING | Decidir i uniformar | +| INCONS-01 | `game/*` | 3 estils de mòdul: namespace+inline (Options), classe all-static (Map), OOP (Player/Tilemap), namespace+constexpr (UiColor/Notification) | Importante | DONE (parcial) | Resolt "classe all-static" amb PATCH-07 (Map → namespace). Queden 3 estils diferenciats per responsabilitat: `namespace` per a estat compartit (Options, Map), classes per a entitats amb estat (Player, Tilemap), `namespace+constexpr` per a alias/presets (UiColor, Notification). La distinció ara és intencional, no fortuïta | | INCONS-02 | `options.cpp:372,477,530,579,607` | `std::cerr` en comptes de `std::cout` (convenció del projecte) | Importante | DONE | Totes les ocurrències (10, incloent les afegides a PATCH-04) substituïdes per `std::cout`. Test manual: amb config.yaml trencat el missatge de backup va per stdout | -| INCONS-03 | `options.cpp` vs `tilemap.cpp:77-78` | Diferent política en YAML absent: `contains()` vs accés directe que llança | Menor | PENDING | Uniformar (llançar sempre si és un fitxer obligatori) | -| INCONS-04 | `options.cpp`, `player.cpp` | Locals ALL-CAPS (`CONTENT`, `IT`, `DT`, `OLD_X`) barrejats amb `snake_case` | Menor | PENDING | Uniformar estil | -| INCONS-05 | `options.cpp:420-430` | `Screen::Config` s'omple camp a camp mentre que `Audio::Config` fa `designated initializers` | Menor | PENDING | Uniformar | +| INCONS-03 | `options.cpp` vs `tilemap.cpp:77-78` | Diferent política en YAML absent: `contains()` vs accés directe que llança | Menor | WONTFIX | Les dues polítiques són correctes per al seu cas: `Tilemap`/`Map::loadFrom` tenen fitxers de definició (asset obligatori → llançar); `Options` parseja preferències de l'usuari (camp absent → manté default). No és inconsistència sinó dos contractes diferents | +| INCONS-04 | `options.cpp`, `player.cpp` | Locals ALL-CAPS (`CONTENT`, `IT`, `DT`, `OLD_X`) barrejats amb `snake_case` | Menor | WONTFIX | El `.clang-tidy` del projecte exigeix `LocalConstantCase = UPPER_CASE` i `VariableCase = lower_case` — els ALL-CAPS són per a `const`/`constexpr` i els snake_case per a mutables. La barreja es correcta i està dictada pel linter | +| INCONS-05 | `options.cpp:420-430` | `Screen::Config` s'omple camp a camp mentre que `Audio::Config` fa `designated initializers` | Menor | DONE | `buildScreenConfig` passa a return amb designated initializers, consistent amb `buildAudioConfig` | ## 6. Codi mort @@ -94,7 +94,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | ID | Fitxer:línia | Troballa | Severitat | Estat | Notes | |----|--------------|----------|-----------|-------|-------| -| CONV-01 | `player.*`, `map.*`, `ui/*`, `game_scene.cpp` | Speed, caixa de col·lisió, geometria, índexs paleta, rutes d'assets, nom del track: tot cablejat | Crítico | PENDING | Tapat parcialment per HC-01/02/03/05/06 | -| CONV-02 | `map.*`, `options.*` | Interfaces+callbacks: `Map` simulated class, `Options` globals sense observers | Importante | PENDING | Cobert per PATCH-07 + COUP-03 | +| CONV-01 | `player.*`, `map.*`, `ui/*`, `game_scene.cpp` | Speed, caixa de col·lisió, geometria, índexs paleta, rutes d'assets, nom del track: tot cablejat | Crítico | DONE | Tapat per HC-01/02/03/04/05/06: speed a Defaults::Player, col·lisió a player.yaml, geometria a pyramid.yaml, índexs paleta a Defaults::UI, rutes a Defaults::Pyramid + Config | +| CONV-02 | `map.*`, `options.*` | Interfaces+callbacks: `Map` simulated class, `Options` globals sense observers | Importante | DONE (parcial) | Map resolt amb PATCH-07 (namespace). La part d'Options/observers va amb COUP-03 (DEFERRED a `refactor/options-class`) | | CONV-03 | `options.cpp` | `std::cerr` viola la regla "std::cout only" | Importante | DONE | Resolt amb INCONS-02 | -| CONV-04 | `player.cpp:27` | `SDL_GetKeyboardState` directe en `Player::update` (fuga de SDL al gameplay) | Menor | PENDING | Anar per `GlobalInputs`/`EventBuffer` | +| CONV-04 | `player.cpp:27` | `SDL_GetKeyboardState` directe en `Player::update` (fuga de SDL al gameplay) | Menor | WONTFIX | `SDL_GetKeyboardState` és l'accés correcte per a state-based polling a 60Hz; `EventBuffer` és per a esdeveniments discrets. La fuga és cosmètica — abstraure-la no aportaria valor | diff --git a/source/game/options.cpp b/source/game/options.cpp index 0934a5e..da175d3 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -504,15 +504,15 @@ namespace Options { } auto buildScreenConfig() -> Screen::Config { - Screen::Config config; - config.zoom = window.zoom; - config.fullscreen = window.fullscreen; - config.vsync = video.vsync; - config.scaling_mode = video.scaling_mode; - config.texture_filter = video.texture_filter; - config.gpu_acceleration = video.gpu.acceleration; - config.preferred_driver = video.gpu.preferred_driver; - return config; + return { + .zoom = window.zoom, + .fullscreen = window.fullscreen, + .vsync = video.vsync, + .scaling_mode = video.scaling_mode, + .texture_filter = video.texture_filter, + .gpu_acceleration = video.gpu.acceleration, + .preferred_driver = video.gpu.preferred_driver, + }; } auto buildAudioConfig() -> ::Audio::Config { From 331f3fc85cfe64c3f2cdbb1f33cba1f4891a2c60 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 18:17:02 +0200 Subject: [PATCH 20/20] docs/audit-game.md: estat global i top 5 sincronitzat al tancament --- docs/audit-game.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/audit-game.md b/docs/audit-game.md index 6eaf5ce..2daac03 100644 --- a/docs/audit-game.md +++ b/docs/audit-game.md @@ -3,6 +3,8 @@ Rama: `audit/game`. Cobertura: `source/game/{entities,map,scenes,ui}` + `source/game/options.{hpp,cpp}` (12 fitxers, ~1.486 línies). +Estado global: **25 DONE, 5 WONTFIX, 3 DEFERRED** sobre 33 ítems. Cada canvi té un commit aïllat i s'ha provat amb `SDL_VIDEODRIVER=dummy` + `SIGINT` (el log de `Game start` a `Bye!` és idèntic entre commits; la branca `config.yaml` de backup verificada manualment amb un fitxer corrupte). + Estados posibles: - `PENDING` — identificat, sense tocar - `WIP` — diff proposat, pendent d'aplicar @@ -22,7 +24,7 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer | COUP-01 | `core/` inclou `game/ui/notification_presets.hpp` (dependència invertida) | DONE | | DUP-01 | PostFX i CrtPi són dos pipelines duplicats a `options.cpp` | DONE | | PATCH-04 | Reescriure silenciosament `config.yaml` al fallar parseig o versió | DONE | -| HC-01 | `SPEED_PX_PER_S`, caixa de col·lisió i rutes d'assets cablejades | DONE (HC-01); PENDING (HC-02, HC-06) | +| HC-01+HC-02+HC-06 | `SPEED_PX_PER_S`, caixa de col·lisió i rutes d'assets cablejades | DONE | ---