Auditoria de game/: neteja general aplicada (rama audit/game)

This commit is contained in:
JailDesigner committed 2026-04-20 18:19:59 +02:00
commit 07a6e4d5d8
16 files changed
+714 -450

No files matched your search

+16
View File
@@ -15,6 +15,22 @@ tileset: data/tilesets/tomb.gif
tile_size: 16 tile_size: 16
origin: [-8, 0] 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: legend:
W: 292 W: 292
X: 169 X: 169
+8
View File
@@ -3,6 +3,14 @@ tileSetFile: player.gif
frameWidth: 12 frameWidth: 12
frameHeight: 24 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: animations:
- name: stand - name: stand
frames: [1] frames: [1]
+102
View File
@@ -0,0 +1,102 @@
# 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).
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
- `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") | 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ó | DONE |
| HC-01+HC-02+HC-06 | `SPEED_PX_PER_S`, caixa de col·lisió i rutes d'assets cablejades | DONE |
---
## 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 | DONE | Afegits `loadPresetsFromFile<T>` i `savePresetsToFile<T>` 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<std::pair<Enum, string_view>, 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<T> }` | Menor | DONE | Afegit `readField<T>(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 | DONE | Extret `Player::clampToPlayfield()`; `update()` el crida després de la col·lisió per eix |
## 2. Hardcoding
| 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 | 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 | 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 | 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
| 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 | 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 | 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 `<path>.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 | DONE | `Map` passa a ser `namespace Map`; callers no canvien (ja usen `Map::grid_cols`/`Map::collides`). Pattern alineat amb `Options` |
## 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 | 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 | 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 | 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 | 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
| ID | Fitxer:línia | Troballa | Severitat | Estat | Notes |
|----|--------------|----------|-----------|-------|-------|
| 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 | DONE | Resolt amb 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 | 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 | 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 |
+1 -1
View File
@@ -7,12 +7,12 @@
#include "core/input/key_config.hpp" #include "core/input/key_config.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/rendering/notification_presets.hpp"
#include "core/rendering/notifier.hpp" #include "core/rendering/notifier.hpp"
#include "core/rendering/render_info.hpp" #include "core/rendering/render_info.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "game/ui/notification_presets.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
namespace GlobalInputs { namespace GlobalInputs {
@@ -5,6 +5,7 @@
#include <string> #include <string>
#include "core/rendering/notifier.hpp" #include "core/rendering/notifier.hpp"
#include "utils/defaults.hpp"
// Temes per a les notificacions de pantalla. Un "preset" captura els dos // 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 // colors (text i fons, índexs sobre la paleta "ui") i el temps de visibilitat
@@ -18,13 +19,12 @@ namespace Notification {
int time_ms; int time_ms;
}; };
// Tema per defecte: text clar sobre fons fosc, 2 segons de visibilitat. // Tema per defecte: text clar sobre fons fosc. Els valors concrets
// Per a ajustar l'aparença global de totes les notificacions sense // viuen a `Defaults::UI` — tocar l'aparença global es fa allà.
// presetejar-les manualment, toca només aquest bloc.
constexpr Preset DEFAULT{ constexpr Preset DEFAULT{
.text_color = 197, .text_color = Defaults::UI::NOTIFICATION_TEXT,
.bg_color = 96, .bg_color = Defaults::UI::NOTIFICATION_BG,
.time_ms = 4000, .time_ms = Defaults::UI::NOTIFICATION_TIME_MS,
}; };
// Preset per al missatge de confirmació d'eixida (ESC doble). Color // 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 // coincideix amb la finestra durant la qual la segona pulsació d'ESC
// confirma el tancament — veure GlobalInputs::onExit. // confirma el tancament — veure GlobalInputs::onExit.
constexpr Preset EXIT{ constexpr Preset EXIT{
.text_color = 88, .text_color = Defaults::UI::EXIT_TEXT,
.bg_color = 40, .bg_color = Defaults::UI::EXIT_BG,
.time_ms = 4000, .time_ms = Defaults::UI::EXIT_TIME_MS,
}; };
// Helper de conveniència: aplica un preset (per defecte DEFAULT) cridant // Helper de conveniència: aplica un preset (per defecte DEFAULT) cridant
+2 -2
View File
@@ -17,8 +17,8 @@ class Text;
// a l'arrencada; si Notifier::init() es crida primer, les carrega ell). // a l'arrencada; si Notifier::init() es crida primer, les carrega ell).
// //
// API crua: Notifier::get().notify(text, text_color, bg_color, time_ms). // API crua: Notifier::get().notify(text, text_color, bg_color, time_ms).
// Per a treballar amb presets del joc, vegeu Notification::notify() a // Per a treballar amb presets pre-definits, vegeu Notification::notify() a
// game/ui/notification_presets.hpp. // core/rendering/notification_presets.hpp.
class Notifier { class Notifier {
public: public:
static auto get() -> Notifier&; static auto get() -> Notifier&;
+82 -47
View File
@@ -4,20 +4,49 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <fstream>
#include <stdexcept>
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "external/fkyaml_node.hpp"
#include "game/map/map.hpp" #include "game/map/map.hpp"
#include "utils/defaults.hpp"
namespace { namespace {
// Velocitat constant. Si més endavant volem sprint, slow zones, etc., // Alias local per reduir soroll; la constant viu a Defaults::Player.
// es pot convertir en un membre modificable. constexpr float SPEED_PX_PER_S = Defaults::Player::SPEED_PX_PER_S;
constexpr float SPEED_PX_PER_S = 60.0F;
} // namespace } // namespace
Player::Player(float x, float y, const Controls& controls) void Player::loadFrom(const std::string& yaml_path) {
: AnimatedSprite(Resource::Cache::get().getAnimationData("player.yaml")), 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<int>();
sprite_h = yaml["frameHeight"].get_value<int>();
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<int>();
base_size = c["base_size"].get_value<int>();
// 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(const std::string& anim_key, float x, float y, const Controls& controls)
: AnimatedSprite(Resource::Cache::get().getAnimationData(anim_key)),
controls_(controls) { controls_(controls) {
setPos(x, y); setPos(x, y);
setCurrentAnimation("stand"); setCurrentAnimation("stand");
@@ -76,65 +105,71 @@ void Player::update(float delta_time) {
setPosY(NEW_Y); setPosY(NEW_Y);
if (Map::collides(baseBox())) { setPosY(OLD_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 // 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. // playfield (muros). El cap del sprite sí pot sobresortir per damunt.
constexpr auto MIN_X = static_cast<float>(Map::PLAYFIELD_X - COLLISION_X_OFFSET); const auto MIN_X = static_cast<float>(Map::playfield_x - collision_x_offset);
constexpr auto MAX_X = static_cast<float>( const auto MAX_X = static_cast<float>(
Map::PLAYFIELD_X + Map::PLAYFIELD_W - COLLISION_X_OFFSET - COLLISION_SIZE); Map::playfield_x + Map::playfield_w - collision_x_offset - collision_size);
constexpr auto MIN_Y = static_cast<float>(Map::PLAYFIELD_Y - COLLISION_Y_OFFSET); const auto MIN_Y = static_cast<float>(Map::playfield_y - collision_y_offset);
constexpr auto MAX_Y = static_cast<float>( const auto MAX_Y = static_cast<float>(
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)); setPosX(std::clamp(getPosX(), MIN_X, MAX_X));
setPosY(std::clamp(getPosY(), MIN_Y, MAX_Y)); setPosY(std::clamp(getPosY(), MIN_Y, MAX_Y));
} }
auto Player::baseBox() const -> SDL_FRect { auto Player::baseBox() const -> SDL_FRect {
return { return {
.x = getPosX() + static_cast<float>(COLLISION_X_OFFSET), .x = getPosX() + static_cast<float>(collision_x_offset),
.y = getPosY() + static_cast<float>(COLLISION_Y_OFFSET), .y = getPosY() + static_cast<float>(collision_y_offset),
.w = static_cast<float>(COLLISION_SIZE), .w = static_cast<float>(collision_size),
.h = static_cast<float>(COLLISION_SIZE), .h = static_cast<float>(collision_size),
}; };
} }
void Player::snapToHorizontalLane(float delta_time) { void Player::snapToLane(
// Lanes en posY: base + i·PITCH_Y per a i ∈ [0, GRID_ROWS]. Correspon a float delta_time,
// la caixa de col·lisió ocupant exactament un passadís horitzontal. float base,
constexpr auto BASE = static_cast<float>(Map::PLAYFIELD_Y - COLLISION_Y_OFFSET); float pitch,
constexpr auto PITCH = static_cast<float>(Map::PITCH_Y); int max_idx,
constexpr auto THRESHOLD = static_cast<float>(COLLISION_SIZE) / 2.0F; 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<float>(collision_size) / 2.0F;
const auto IDX = std::clamp( const auto IDX = std::clamp(
static_cast<int>(std::round((getPosY() - BASE) / PITCH)), static_cast<int>(std::round(((this->*get_pos)() - base) / pitch)),
0, 0,
Map::GRID_ROWS); max_idx);
const float LANE = BASE + (static_cast<float>(IDX) * PITCH); const float LANE = base + (static_cast<float>(IDX) * pitch);
const float DY = LANE - getPosY(); const float DELTA = LANE - (this->*get_pos)();
if (std::abs(DY) > 0.0F && std::abs(DY) <= THRESHOLD) { if (std::abs(DELTA) > 0.0F && std::abs(DELTA) <= THRESHOLD) {
const float STEP = std::min(std::abs(DY), SPEED_PX_PER_S * delta_time); const float STEP = std::min(std::abs(DELTA), SPEED_PX_PER_S * delta_time);
const float SAVED = getPosY(); const float SAVED = (this->*get_pos)();
setPosY(getPosY() + std::copysign(STEP, DY)); (this->*set_pos)(SAVED + std::copysign(STEP, DELTA));
if (Map::collides(baseBox())) { setPosY(SAVED); } if (Map::collides(baseBox())) { (this->*set_pos)(SAVED); }
} }
} }
void Player::snapToHorizontalLane(float delta_time) {
snapToLane(delta_time,
static_cast<float>(Map::playfield_y - collision_y_offset),
static_cast<float>(Map::pitch_y),
Map::grid_rows,
&Player::getPosY,
&Player::setPosY);
}
void Player::snapToVerticalLane(float delta_time) { void Player::snapToVerticalLane(float delta_time) {
constexpr auto BASE = static_cast<float>(Map::PLAYFIELD_X - COLLISION_X_OFFSET); snapToLane(delta_time,
constexpr auto PITCH = static_cast<float>(Map::PITCH_X); static_cast<float>(Map::playfield_x - collision_x_offset),
constexpr auto THRESHOLD = static_cast<float>(COLLISION_SIZE) / 2.0F; static_cast<float>(Map::pitch_x),
Map::grid_cols,
const auto IDX = std::clamp( &Player::getPosX,
static_cast<int>(std::round((getPosX() - BASE) / PITCH)), &Player::setPosX);
0,
Map::GRID_COLS);
const float LANE = BASE + (static_cast<float>(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); }
}
} }
+37 -23
View File
@@ -2,6 +2,8 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <string>
#include "core/rendering/sprite/animated_sprite.hpp" #include "core/rendering/sprite/animated_sprite.hpp"
class Player : public AnimatedSprite { class Player : public AnimatedSprite {
@@ -16,25 +18,25 @@ class Player : public AnimatedSprite {
SDL_Scancode right; SDL_Scancode right;
}; };
// Sprite 12×24 amb perspectiva falsa: visualment, els 12 píxels // Geometria carregada des de `data/player/player.yaml`. `sprite_*` són
// superiors són el cap i els 12 inferiors la base. // la mida del frame (sprite 12×24 amb perspectiva falsa: visualment, els
static constexpr int SPRITE_WIDTH = 12; // 12 píxels superiors són el cap i els 12 inferiors la base).
static constexpr int SPRITE_HEIGHT = 24; // 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 // Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor.
// del sprite (la base "antiga"). Encaixa just als passadissos de 16 px static void loadFrom(const std::string& yaml_path);
// → 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;
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; void update(float delta_time) override;
@@ -42,20 +44,32 @@ class Player : public AnimatedSprite {
// per al Z-order contra els sarcs: peus per sobre d'un sarc → darrere; // per al Z-order contra els sarcs: peus per sobre d'un sarc → darrere;
// per sota → davant. // per sota → davant.
[[nodiscard]] auto getFeetY() const -> float { [[nodiscard]] auto getFeetY() const -> float {
return getPosY() + static_cast<float>(COLLISION_Y_OFFSET + COLLISION_SIZE); return getPosY() + static_cast<float>(collision_y_offset + collision_size);
} }
private: private:
// Rectangle de col·lisió del jugador (16×16), usat pel mapa de colisions // Rectangle de col·lisió del jugador, usat pel mapa de colisions i com
// i com a referència per al Z-order. // a referència per al Z-order.
[[nodiscard]] auto baseBox() const -> SDL_FRect; [[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 // 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 // de col·lisió està a ≤ collision_size/2 d'una lane, s'hi aproxima
// gradualment (a velocitat de moviment). Això permet canviar de direcció // 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 snapToHorizontalLane(float delta_time);
void snapToVerticalLane(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_; Controls controls_;
}; };
+65 -56
View File
@@ -1,8 +1,10 @@
#include "game/map/map.hpp" #include "game/map/map.hpp"
#include "core/rendering/screen.hpp" #include <fstream>
#include "core/resources/resource_cache.hpp" #include <iostream>
#include "utils/defines.hpp" #include <stdexcept>
#include "external/fkyaml_node.hpp"
namespace { namespace {
@@ -16,64 +18,71 @@ namespace {
} // namespace } // namespace
Map::Map() = default; namespace Map {
auto Map::getSarcRowBottomY(int row) -> int { void loadFrom(const std::string& yaml_path) {
return ORIGIN_Y + (row * PITCH_Y) + SARC_H; std::ifstream file(yaml_path);
} if (!file.is_open()) {
throw std::runtime_error("Map: no s'ha pogut obrir " + yaml_path);
auto Map::getSarcRect(int col, int row) -> SDL_FRect {
return {
.x = static_cast<float>(ORIGIN_X + (col * PITCH_X)),
.y = static_cast<float>(ORIGIN_Y + (row * PITCH_Y)),
.w = static_cast<float>(SARC_W),
.h = static_cast<float>(SARC_H),
};
}
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 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"];
auto Map::collides(const SDL_FRect& base_box) -> bool { grid_cols = g["grid_cols"].get_value<int>();
// Recorregut trivial dels 20 sarcòfags. Amb una graella tan petita no grid_rows = g["grid_rows"].get_value<int>();
// val la pena indexar per col/row prèviament; si algun dia creix, es pot sarc_w = g["sarc_w"].get_value<int>();
sarc_h = g["sarc_h"].get_value<int>();
pitch_x = g["pitch_x"].get_value<int>();
pitch_y = g["pitch_y"].get_value<int>();
hud_h = g["hud_h"].get_value<int>();
back_wall_h = g["back_wall_h"].get_value<int>();
side_wall_w = g["side_wall_w"].get_value<int>();
// 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 getSarcRect(int col, int row) -> SDL_FRect {
return {
.x = static_cast<float>(origin_x + (col * pitch_x)),
.y = static_cast<float>(origin_y + (row * pitch_y)),
.w = static_cast<float>(sarc_w),
.h = static_cast<float>(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. // calcular el rang de columnes/files candidates a partir de base_box.
for (int row = 0; row < GRID_ROWS; ++row) { for (int row = 0; row < grid_rows; ++row) {
for (int col = 0; col < GRID_COLS; ++col) { for (int col = 0; col < grid_cols; ++col) {
if (overlapStrict(base_box, getSarcRect(col, row))) { return true; } if (overlapStrict(base_box, getSarcRect(col, row))) { return true; }
} }
} }
return false; return false;
} }
} // namespace Map
+45 -73
View File
@@ -2,87 +2,59 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <string>
// Mapa d'una piràmide: sala rectangular amb marcador superior, muros pels // 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 // quatre costats i graella de sarcòfags envoltada per un passadís a cada
// cantó. Mesures portades de l'AEE original (mapa.cpp: sarcòfag 50×20, pitch // cantó. La geometria (graella, pitch, amplada de murs) es carrega des del
// 65×35, passadís 15 px) i adaptades a 320×240 repartint els 40 px extra // bloc `geometry:` del YAML de la piràmide — així cada sala pot tenir la
// sobretot al mur frontal, que fa de primer pla per a la falsa perspectiva. // seua forma sense recompilar.
// //
// Z-order: el render es divideix en fons (marcador + muros + sòl) i files // `Map` és un namespace (no una classe) perquè no té estat d'instància:
// de sarcòfags. El caller (GameScene) alternarà jugadors ordenats per la Y // els membres viuen com a globals del namespace i els callers hi accedeixen
// dels peus amb cada fila per a aconseguir la perspectiva falsa (el sarcòfag // amb `Map::grid_cols`, `Map::collides(...)`, etc. El canvi respecte a
// tapa el cos del jugador si aquest està "darrere"; el cap del jugador tapa // l'anterior `class Map { static ... };` és purament sintàctic.
// el sarcòfag de la fila superior si està "davant"). namespace Map {
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;
// El pas entre sarcòfags (15×15) és exactament el que ha de cabre el // Geometria de la graella (carregada de pyramid.yaml > geometry).
// jugador; el reutilitzem també com a passadís exterior (perímetre). inline int grid_cols = 0;
static constexpr int CORRIDOR_W = PITCH_X - SARC_W; inline int grid_rows = 0;
static constexpr int CORRIDOR_H = PITCH_Y - SARC_H; inline int sarc_w = 0;
static constexpr int OUTER_CORRIDOR = CORRIDOR_W; inline int sarc_h = 0;
inline int pitch_x = 0;
inline int pitch_y = 0;
// Dimensions del tauler complet (des del primer sarcòfag fins al cantó // Cromat de la sala: marcador (HUD), mur posterior (dalt) i murs
// inferior dret de l'últim). Derivades: no cal tocar-les a mà. // laterals. El mur frontal es deriva del passadís exterior inferior.
static constexpr int BOARD_W = ((GRID_COLS - 1) * PITCH_X) + SARC_W; inline int hud_h = 0;
static constexpr int BOARD_H = ((GRID_ROWS - 1) * PITCH_Y) + SARC_H; inline int back_wall_h = 0;
inline int side_wall_w = 0;
// Cromat de la sala: marcador, mur posterior (dalt), murs laterals i mur // Derivats de la geometria base: el pas entre sarcòfags ha de cabre
// frontal (baix). El frontal és més gros per absorbir els 40 px extra del // exactament el jugador, i el mateix passadís s'usa a l'exterior.
// pas de 320×200 → 320×240 i reforçar la falsa perspectiva. inline int corridor_w = 0;
static constexpr int HUD_H = 24; inline int corridor_h = 0;
static constexpr int BACK_WALL_H = 24; inline int outer_corridor = 0;
static constexpr int SIDE_WALL_W = 24; 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;
// Origen del tauler dins del canvas (cantó superior-esquerra del primer // Carrega el bloc `geometry:` del YAML i recalcula tots els derivats.
// sarcòfag). Derivat: mur + passadís en cada eix. // Ha de cridar-se abans d'instanciar `Player` o consultar `collides()`:
static constexpr int ORIGIN_X = SIDE_WALL_W + OUTER_CORRIDOR; // GameScene ho fa al ctor.
static constexpr int ORIGIN_Y = HUD_H + BACK_WALL_H + OUTER_CORRIDOR; void loadFrom(const std::string& yaml_path);
// 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);
// Í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;
Map();
// 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.
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. // 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 // Mapa de colisions (separat del mapa de dibuix): retorna true si el
// rectangle `base_box` (la base del jugador, per exemple) s'encavalca // rectangle `base_box` (la base del jugador, per exemple) s'encavalca
// amb cap sarcòfag. Així Player mai camine sobre un sarcòfag, // amb cap sarcòfag.
// independentment de com els pinte renderSarcsRow. [[nodiscard]] auto collides(const SDL_FRect& base_box) -> bool;
[[nodiscard]] static auto collides(const SDL_FRect& base_box) -> bool;
}; } // namespace Map
+4 -1
View File
@@ -126,13 +126,16 @@ void Tilemap::render() const {
const int X_START = std::max(0, -origin_x_); const int X_START = std::max(0, -origin_x_);
const int X_END = std::min(width_px_, CANVAS_W - 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) { for (int y = Y_START; y < Y_END; ++y) {
const auto* src_row = &pixels_[static_cast<std::size_t>(y) * static_cast<std::size_t>(width_px_)]; const auto* src_row = &pixels_[static_cast<std::size_t>(y) * static_cast<std::size_t>(width_px_)];
auto* dst_row = auto* dst_row =
&canvas[(static_cast<std::size_t>(origin_y_ + y) * static_cast<std::size_t>(CANVAS_W)) + static_cast<std::size_t>(origin_x_)]; &canvas[(static_cast<std::size_t>(origin_y_ + y) * static_cast<std::size_t>(CANVAS_W)) + static_cast<std::size_t>(origin_x_)];
for (int x = X_START; x < X_END; ++x) { for (int x = X_START; x < X_END; ++x) {
const auto PIXEL = src_row[x]; const auto PIXEL = src_row[x];
if ((PIXEL & 0xFF000000U) != 0U) { dst_row[x] = PIXEL; } if ((PIXEL & ALPHA_MASK) != 0U) { dst_row[x] = PIXEL; }
} }
} }
} }
+225 -199
View File
@@ -1,64 +1,56 @@
#include "game/options.hpp" #include "game/options.hpp"
#include <algorithm> #include <algorithm>
#include <array>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <string_view>
#include <utility>
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
namespace { namespace {
auto scalingModeToString(Defaults::ScalingMode mode) -> const char* { // Taules enum ↔ string per a la serialització a YAML. La primera entrada
switch (mode) { // és el fallback quan l'enum no té nom conegut o el text no casa amb
case Defaults::ScalingMode::DISABLED: // cap entrada coneguda — per això `LETTERBOX`/`NEAREST`/`OFF` encapçalen.
return "disabled"; constexpr std::array SCALING_MODE_NAMES = {
case Defaults::ScalingMode::STRETCH: std::pair{Defaults::ScalingMode::LETTERBOX, std::string_view{"letterbox"}},
return "stretch"; std::pair{Defaults::ScalingMode::DISABLED, std::string_view{"disabled"}},
case Defaults::ScalingMode::LETTERBOX: std::pair{Defaults::ScalingMode::STRETCH, std::string_view{"stretch"}},
return "letterbox"; std::pair{Defaults::ScalingMode::OVERSCAN, std::string_view{"overscan"}},
case Defaults::ScalingMode::OVERSCAN: std::pair{Defaults::ScalingMode::INTEGER, std::string_view{"integer"}},
return "overscan"; };
case Defaults::ScalingMode::INTEGER:
return "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 <typename Enum, std::size_t N>
auto enumToString(const std::array<std::pair<Enum, std::string_view>, 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 { template <typename Enum, std::size_t N>
if (text == "disabled") { return Defaults::ScalingMode::DISABLED; } auto enumFromString(const std::array<std::pair<Enum, std::string_view>, N>& table, const std::string& text) -> Enum {
if (text == "stretch") { return Defaults::ScalingMode::STRETCH; } for (const auto& [e, name] : table) {
if (text == "overscan") { return Defaults::ScalingMode::OVERSCAN; } if (name == text) { return e; }
if (text == "integer") { return Defaults::ScalingMode::INTEGER; }
return Defaults::ScalingMode::LETTERBOX;
} }
return table.front().first;
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";
}
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;
} }
auto readFileOrEmpty(const std::string& path) -> std::string { auto readFileOrEmpty(const std::string& path) -> std::string {
@@ -76,51 +68,65 @@ namespace {
return file.good(); 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<T>(); }`.
template <typename T>
void readField(const fkyaml::node& node, const char* key, T& dst) {
if (node.contains(key)) { dst = node[key].get_value<T>(); }
}
// Còpia `<path>` a `<path>.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::cout << "Options: còpia de seguretat a " << path << ".bak\n";
}
void parseWindow(const fkyaml::node& yaml) { void parseWindow(const fkyaml::node& yaml) {
if (!yaml.contains("window")) { return; } if (!yaml.contains("window")) { return; }
const auto& node = yaml["window"]; const auto& node = yaml["window"];
if (node.contains("zoom")) { Options::window.zoom = node["zoom"].get_value<int>(); } readField(node, "zoom", Options::window.zoom);
if (node.contains("fullscreen")) { Options::window.fullscreen = node["fullscreen"].get_value<bool>(); } readField(node, "fullscreen", Options::window.fullscreen);
} }
void parseGpu(const fkyaml::node& video_node) { void parseGpu(const fkyaml::node& video_node) {
if (!video_node.contains("gpu")) { return; } if (!video_node.contains("gpu")) { return; }
const auto& node = video_node["gpu"]; const auto& node = video_node["gpu"];
if (node.contains("acceleration")) { Options::video.gpu.acceleration = node["acceleration"].get_value<bool>(); } readField(node, "acceleration", Options::video.gpu.acceleration);
if (node.contains("preferred_driver")) { Options::video.gpu.preferred_driver = node["preferred_driver"].get_value<std::string>(); } readField(node, "preferred_driver", Options::video.gpu.preferred_driver);
} }
void parseShader(const fkyaml::node& video_node) { void parseShader(const fkyaml::node& video_node) {
if (!video_node.contains("shader")) { return; } if (!video_node.contains("shader")) { return; }
const auto& node = video_node["shader"]; const auto& node = video_node["shader"];
if (node.contains("enabled")) { Options::video.shader.enabled = node["enabled"].get_value<bool>(); } readField(node, "enabled", Options::video.shader.enabled);
if (node.contains("current_shader")) { readField(node, "current_shader", Options::video.shader.current_shader);
Options::video.shader.current_shader = node["current_shader"].get_value<std::string>(); readField(node, "current_postfx_preset", Options::video.shader.current_postfx_preset);
} readField(node, "current_crtpi_preset", Options::video.shader.current_crtpi_preset);
if (node.contains("current_postfx_preset")) {
Options::video.shader.current_postfx_preset = node["current_postfx_preset"].get_value<std::string>();
}
if (node.contains("current_crtpi_preset")) {
Options::video.shader.current_crtpi_preset = node["current_crtpi_preset"].get_value<std::string>();
}
} }
void parseVideo(const fkyaml::node& yaml) { void parseVideo(const fkyaml::node& yaml) {
if (!yaml.contains("video")) { return; } if (!yaml.contains("video")) { return; }
const auto& node = yaml["video"]; const auto& node = yaml["video"];
if (node.contains("vsync")) { Options::video.vsync = node["vsync"].get_value<bool>(); } readField(node, "vsync", Options::video.vsync);
if (node.contains("scaling_mode")) { if (node.contains("scaling_mode")) {
Options::video.scaling_mode = scalingModeFromString(node["scaling_mode"].get_value<std::string>()); Options::video.scaling_mode = enumFromString(SCALING_MODE_NAMES, node["scaling_mode"].get_value<std::string>());
} }
if (node.contains("texture_filter")) { if (node.contains("texture_filter")) {
Options::video.texture_filter = filterFromString(node["texture_filter"].get_value<std::string>()); Options::video.texture_filter = enumFromString(TEXTURE_FILTER_NAMES, node["texture_filter"].get_value<std::string>());
} }
if (node.contains("render_info_position")) { if (node.contains("render_info_position")) {
Options::video.render_info_position = renderInfoPositionFromString(node["render_info_position"].get_value<std::string>()); Options::video.render_info_position = enumFromString(RENDER_INFO_POSITION_NAMES, node["render_info_position"].get_value<std::string>());
}
if (node.contains("render_info_show_time")) {
Options::video.render_info_show_time = node["render_info_show_time"].get_value<bool>();
} }
readField(node, "render_info_show_time", Options::video.render_info_show_time);
parseGpu(node); parseGpu(node);
parseShader(node); parseShader(node);
} }
@@ -128,22 +134,22 @@ namespace {
void parseMusic(const fkyaml::node& audio_node) { void parseMusic(const fkyaml::node& audio_node) {
if (!audio_node.contains("music")) { return; } if (!audio_node.contains("music")) { return; }
const auto& node = audio_node["music"]; const auto& node = audio_node["music"];
if (node.contains("enabled")) { Options::audio.music.enabled = node["enabled"].get_value<bool>(); } readField(node, "enabled", Options::audio.music.enabled);
if (node.contains("volume")) { Options::audio.music.volume = node["volume"].get_value<float>(); } readField(node, "volume", Options::audio.music.volume);
} }
void parseSound(const fkyaml::node& audio_node) { void parseSound(const fkyaml::node& audio_node) {
if (!audio_node.contains("sound")) { return; } if (!audio_node.contains("sound")) { return; }
const auto& node = audio_node["sound"]; const auto& node = audio_node["sound"];
if (node.contains("enabled")) { Options::audio.sound.enabled = node["enabled"].get_value<bool>(); } readField(node, "enabled", Options::audio.sound.enabled);
if (node.contains("volume")) { Options::audio.sound.volume = node["volume"].get_value<float>(); } readField(node, "volume", Options::audio.sound.volume);
} }
void parseAudio(const fkyaml::node& yaml) { void parseAudio(const fkyaml::node& yaml) {
if (!yaml.contains("audio")) { return; } if (!yaml.contains("audio")) { return; }
const auto& node = yaml["audio"]; const auto& node = yaml["audio"];
if (node.contains("enabled")) { Options::audio.enabled = node["enabled"].get_value<bool>(); } readField(node, "enabled", Options::audio.enabled);
if (node.contains("volume")) { Options::audio.volume = node["volume"].get_value<float>(); } readField(node, "volume", Options::audio.volume);
parseMusic(node); parseMusic(node);
parseSound(node); parseSound(node);
} }
@@ -209,15 +215,15 @@ namespace {
} }
void parsePostFXPreset(const fkyaml::node& node, Options::PostFXPreset& preset) { void parsePostFXPreset(const fkyaml::node& node, Options::PostFXPreset& preset) {
if (node.contains("name")) { preset.name = node["name"].get_value<std::string>(); } readField(node, "name", preset.name);
if (node.contains("vignette")) { preset.vignette = node["vignette"].get_value<float>(); } readField(node, "vignette", preset.vignette);
if (node.contains("scanlines")) { preset.scanlines = node["scanlines"].get_value<float>(); } readField(node, "scanlines", preset.scanlines);
if (node.contains("chroma")) { preset.chroma = node["chroma"].get_value<float>(); } readField(node, "chroma", preset.chroma);
if (node.contains("mask")) { preset.mask = node["mask"].get_value<float>(); } readField(node, "mask", preset.mask);
if (node.contains("gamma")) { preset.gamma = node["gamma"].get_value<float>(); } readField(node, "gamma", preset.gamma);
if (node.contains("curvature")) { preset.curvature = node["curvature"].get_value<float>(); } readField(node, "curvature", preset.curvature);
if (node.contains("bleeding")) { preset.bleeding = node["bleeding"].get_value<float>(); } readField(node, "bleeding", preset.bleeding);
if (node.contains("flicker")) { preset.flicker = node["flicker"].get_value<float>(); } readField(node, "flicker", preset.flicker);
} }
void writePostFXPreset(std::ostringstream& out, const Options::PostFXPreset& preset) { void writePostFXPreset(std::ostringstream& out, const Options::PostFXPreset& preset) {
@@ -299,21 +305,21 @@ namespace {
} }
void parseCrtPiPreset(const fkyaml::node& node, Options::CrtPiPreset& preset) { void parseCrtPiPreset(const fkyaml::node& node, Options::CrtPiPreset& preset) {
if (node.contains("name")) { preset.name = node["name"].get_value<std::string>(); } readField(node, "name", preset.name);
if (node.contains("scanline_weight")) { preset.scanline_weight = node["scanline_weight"].get_value<float>(); } readField(node, "scanline_weight", preset.scanline_weight);
if (node.contains("scanline_gap_brightness")) { preset.scanline_gap_brightness = node["scanline_gap_brightness"].get_value<float>(); } readField(node, "scanline_gap_brightness", preset.scanline_gap_brightness);
if (node.contains("bloom_factor")) { preset.bloom_factor = node["bloom_factor"].get_value<float>(); } readField(node, "bloom_factor", preset.bloom_factor);
if (node.contains("input_gamma")) { preset.input_gamma = node["input_gamma"].get_value<float>(); } readField(node, "input_gamma", preset.input_gamma);
if (node.contains("output_gamma")) { preset.output_gamma = node["output_gamma"].get_value<float>(); } readField(node, "output_gamma", preset.output_gamma);
if (node.contains("mask_brightness")) { preset.mask_brightness = node["mask_brightness"].get_value<float>(); } readField(node, "mask_brightness", preset.mask_brightness);
if (node.contains("curvature_x")) { preset.curvature_x = node["curvature_x"].get_value<float>(); } readField(node, "curvature_x", preset.curvature_x);
if (node.contains("curvature_y")) { preset.curvature_y = node["curvature_y"].get_value<float>(); } readField(node, "curvature_y", preset.curvature_y);
if (node.contains("mask_type")) { preset.mask_type = node["mask_type"].get_value<int>(); } readField(node, "mask_type", preset.mask_type);
if (node.contains("enable_scanlines")) { preset.enable_scanlines = node["enable_scanlines"].get_value<bool>(); } readField(node, "enable_scanlines", preset.enable_scanlines);
if (node.contains("enable_multisample")) { preset.enable_multisample = node["enable_multisample"].get_value<bool>(); } readField(node, "enable_multisample", preset.enable_multisample);
if (node.contains("enable_gamma")) { preset.enable_gamma = node["enable_gamma"].get_value<bool>(); } readField(node, "enable_gamma", preset.enable_gamma);
if (node.contains("enable_curvature")) { preset.enable_curvature = node["enable_curvature"].get_value<bool>(); } readField(node, "enable_curvature", preset.enable_curvature);
if (node.contains("enable_sharper")) { preset.enable_sharper = node["enable_sharper"].get_value<bool>(); } readField(node, "enable_sharper", preset.enable_sharper);
} }
void writeCrtPiPreset(std::ostringstream& out, const Options::CrtPiPreset& preset) { void writeCrtPiPreset(std::ostringstream& out, const Options::CrtPiPreset& preset) {
@@ -334,6 +340,79 @@ namespace {
out << " enable_sharper: " << (preset.enable_sharper ? "true" : "false") << "\n"; 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 <typename T>
auto loadPresetsFromFile(
const std::string& path,
std::vector<T>& presets,
std::vector<T> (*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()) {
// 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::cout << "Options: " << kind_label << " manca `presets:`; reescrivint amb defaults.\n";
backupFile(path);
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()) {
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::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();
}
}
// 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 <typename T>
auto savePresetsToFile(
const std::string& path,
const std::vector<T>& 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
namespace Options { namespace Options {
@@ -359,8 +438,14 @@ namespace Options {
file_version = yaml["version"].get_value<std::string>(); file_version = yaml["version"].get_value<std::string>();
} }
// Sense migració per ara: si la versió no coincideix, reescrivim // Sense migració per ara: si la versió no coincideix, reescrivim
// amb els defaults de la versió nova. // amb els defaults de la versió nova. El fitxer original queda
if (file_version != Texts::VERSION) { return saveToFile(); } // a `<path>.bak` per si l'usuari volia preservar tuning manual.
if (file_version != Texts::VERSION) {
std::cout << "Options: config.yaml versió \"" << file_version
<< "\" != \"" << Texts::VERSION << "\"; reescrivint amb defaults.\n";
backupFile(config_file_path);
return saveToFile();
}
config_version = file_version; config_version = file_version;
if (yaml.contains("locale")) { Options::locale = yaml["locale"].get_value<std::string>(); } if (yaml.contains("locale")) { Options::locale = yaml["locale"].get_value<std::string>(); }
@@ -369,8 +454,9 @@ namespace Options {
parseAudio(yaml); parseAudio(yaml);
return true; return true;
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "Options: error parsejant config.yaml: " << e.what() << '\n'; std::cout << "Options: error parsejant config.yaml: " << e.what() << '\n';
std::cerr << "Options: reescrivint el fitxer amb defaults.\n"; std::cout << "Options: reescrivint el fitxer amb defaults.\n";
backupFile(config_file_path);
return saveToFile(); return saveToFile();
} }
} }
@@ -391,9 +477,9 @@ namespace Options {
out << "video:\n"; out << "video:\n";
out << " vsync: " << (video.vsync ? "true" : "false") << "\n"; out << " vsync: " << (video.vsync ? "true" : "false") << "\n";
out << " scaling_mode: \"" << scalingModeToString(video.scaling_mode) << "\"\n"; out << " scaling_mode: \"" << enumToString(SCALING_MODE_NAMES, video.scaling_mode) << "\"\n";
out << " texture_filter: \"" << filterToString(video.texture_filter) << "\"\n"; out << " texture_filter: \"" << enumToString(TEXTURE_FILTER_NAMES, video.texture_filter) << "\"\n";
out << " render_info_position: \"" << renderInfoPositionToString(video.render_info_position) << "\"\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 << " render_info_show_time: " << (video.render_info_show_time ? "true" : "false") << "\n";
out << " gpu:\n"; out << " gpu:\n";
out << " acceleration: " << (video.gpu.acceleration ? "true" : "false") << "\n"; out << " acceleration: " << (video.gpu.acceleration ? "true" : "false") << "\n";
@@ -418,15 +504,15 @@ namespace Options {
} }
auto buildScreenConfig() -> Screen::Config { auto buildScreenConfig() -> Screen::Config {
Screen::Config config; return {
config.zoom = window.zoom; .zoom = window.zoom,
config.fullscreen = window.fullscreen; .fullscreen = window.fullscreen,
config.vsync = video.vsync; .vsync = video.vsync,
config.scaling_mode = video.scaling_mode; .scaling_mode = video.scaling_mode,
config.texture_filter = video.texture_filter; .texture_filter = video.texture_filter,
config.gpu_acceleration = video.gpu.acceleration; .gpu_acceleration = video.gpu.acceleration,
config.preferred_driver = video.gpu.preferred_driver; .preferred_driver = video.gpu.preferred_driver,
return config; };
} }
auto buildAudioConfig() -> ::Audio::Config { auto buildAudioConfig() -> ::Audio::Config {
@@ -445,52 +531,22 @@ namespace Options {
} }
auto loadPostFXFromFile() -> bool { auto loadPostFXFromFile() -> bool {
if (postfx_file_path.empty()) { return false; } return loadPresetsFromFile<PostFXPreset>(
postfx_file_path,
const std::string CONTENT = readFileOrEmpty(postfx_file_path); postfx_presets,
if (CONTENT.empty()) { &defaultPostFXPresets,
postfx_presets = defaultPostFXPresets(); &parsePostFXPreset,
return savePostFXToFile(); &savePostFXToFile,
} "postfx.yaml");
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();
}
} }
auto savePostFXToFile() -> bool { auto savePostFXToFile() -> bool {
if (postfx_file_path.empty()) { return false; } return savePresetsToFile<PostFXPreset>(
postfx_file_path,
std::ostringstream out; postfx_presets,
out << "# Presets PostFX per a AEE 2026. Editable a mà.\n"; &writePostFXPreset,
out << "# Els noms han de coincidir amb config.yaml > video.shader.current_postfx_preset.\n"; "Presets PostFX per a AEE 2026. Editable a mà.",
out << "presets:\n"; "Els noms han de coincidir amb config.yaml > video.shader.current_postfx_preset.");
for (const auto& preset : postfx_presets) {
writePostFXPreset(out, preset);
}
return writeFile(postfx_file_path, out.str());
} }
void setCrtPiFile(const std::string& path) { void setCrtPiFile(const std::string& path) {
@@ -498,52 +554,22 @@ namespace Options {
} }
auto loadCrtPiFromFile() -> bool { auto loadCrtPiFromFile() -> bool {
if (crtpi_file_path.empty()) { return false; } return loadPresetsFromFile<CrtPiPreset>(
crtpi_file_path,
const std::string CONTENT = readFileOrEmpty(crtpi_file_path); crtpi_presets,
if (CONTENT.empty()) { &defaultCrtPiPresets,
crtpi_presets = defaultCrtPiPresets(); &parseCrtPiPreset,
return saveCrtPiToFile(); &saveCrtPiToFile,
} "crtpi.yaml");
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();
}
} }
auto saveCrtPiToFile() -> bool { auto saveCrtPiToFile() -> bool {
if (crtpi_file_path.empty()) { return false; } return savePresetsToFile<CrtPiPreset>(
crtpi_file_path,
std::ostringstream out; crtpi_presets,
out << "# Presets CRT-Pi per a AEE 2026. Editable a mà.\n"; &writeCrtPiPreset,
out << "# Els noms han de coincidir amb config.yaml > video.shader.current_crtpi_preset.\n"; "Presets CRT-Pi per a AEE 2026. Editable a mà.",
out << "presets:\n"; "Els noms han de coincidir amb config.yaml > video.shader.current_crtpi_preset.");
for (const auto& preset : crtpi_presets) {
writeCrtPiPreset(out, preset);
}
return writeFile(crtpi_file_path, out.str());
} }
auto buildActiveShader() -> Rendering::ShaderType { auto buildActiveShader() -> Rendering::ShaderType {
@@ -576,7 +602,7 @@ namespace Options {
const auto IT = std::ranges::find_if(crtpi_presets, const auto IT = std::ranges::find_if(crtpi_presets,
[](const CrtPiPreset& preset) { return preset.name == video.shader.current_crtpi_preset; }); [](const CrtPiPreset& preset) { return preset.name == video.shader.current_crtpi_preset; });
if (IT == crtpi_presets.end()) { 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 {};
} }
return { return {
@@ -604,7 +630,7 @@ namespace Options {
const auto IT = std::ranges::find_if(postfx_presets, const auto IT = std::ranges::find_if(postfx_presets,
[](const PostFXPreset& preset) { return preset.name == video.shader.current_postfx_preset; }); [](const PostFXPreset& preset) { return preset.name == video.shader.current_postfx_preset; });
if (IT == postfx_presets.end()) { 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 {};
} }
return { return {
+56 -22
View File
@@ -4,7 +4,9 @@
#include <algorithm> #include <algorithm>
#include <cstddef> #include <cstddef>
#include <filesystem>
#include <iterator> #include <iterator>
#include <utility>
#include <vector> #include <vector>
#include "core/audio/audio.hpp" #include "core/audio/audio.hpp"
@@ -20,10 +22,42 @@
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
GameScene::GameScene() namespace {
: tilemap_("data/maps/pyramid.yaml") {
Resource::Cache::get().loadSurface("player.gif", "data/player/player.gif"); // La geometria ha de quedar inicialitzada abans que Tilemap parsejar el
Resource::Cache::get().loadAnimation("player.yaml", "data/player/player.yaml"); // 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 Tilemap(yaml_path);
}
} // namespace
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(config_.player_yaml);
// Jugador 0: fletxes del teclat (Defaults::KeysGame). Instanciar-ne un // Jugador 0: fletxes del teclat (Defaults::KeysGame). Instanciar-ne un
// segon només requereix afegir una altra entrada amb uns Controls // segon només requereix afegir una altra entrada amb uns Controls
@@ -39,18 +73,17 @@ GameScene::GameScene()
// centrada en Y al passadís exterior superior. Així no naix dins de // centrada en Y al passadís exterior superior. Així no naix dins de
// cap sarcòfag. // cap sarcòfag.
constexpr auto CORRIDOR_CENTER_X = static_cast<float>(GameCanvas::CENTER_X); constexpr auto CORRIDOR_CENTER_X = static_cast<float>(GameCanvas::CENTER_X);
constexpr auto CORRIDOR_CENTER_Y = const auto CORRIDOR_CENTER_Y =
static_cast<float>(Map::PLAYFIELD_Y) + (static_cast<float>(Map::OUTER_CORRIDOR) / 2.0F); static_cast<float>(Map::playfield_y) + (static_cast<float>(Map::outer_corridor) / 2.0F);
constexpr auto SPAWN_X = CORRIDOR_CENTER_X - (static_cast<float>(Player::SPRITE_WIDTH) / 2.0F); const auto SPAWN_X = CORRIDOR_CENTER_X - (static_cast<float>(Player::sprite_w) / 2.0F);
constexpr auto SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(Player::COLLISION_Y_OFFSET) - (static_cast<float>(Player::COLLISION_SIZE) / 2.0F); const auto SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(Player::collision_y_offset) - (static_cast<float>(Player::collision_size) / 2.0F);
players_.emplace_back(std::make_unique<Player>(SPAWN_X, SPAWN_Y, CONTROLS_P1)); players_.emplace_back(std::make_unique<Player>(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 // Música de fons en bucle infinit. El track ja està al Resource::Cache
// Resource::Cache (preload al Director), així que playMusic només mou el // (preload al Director), així que playMusic només mou el punter cap a
// punter cap a jail_audio i obri l'stream. // jail_audio i obri l'stream. Audio::get() està garantit no-null:
if (Audio::get() != nullptr) { // Director::init crida Audio::init abans d'instanciar GameScene.
Audio::get()->playMusic("pyramid_1.ogg", -1); Audio::get()->playMusic(config_.music_track, -1);
}
} }
GameScene::~GameScene() = default; GameScene::~GameScene() = default;
@@ -75,20 +108,21 @@ void GameScene::render() {
// col·lisions del jugador. // col·lisions del jugador.
tilemap_.render(); tilemap_.render();
// Ordenem punters a jugadors per Y dels peus (ascendent = més al fons // Amb un sol jugador, l'ordre és irrellevant i ens estalviem el vector
// primer). Usem un vector auxiliar per no tocar l'ordre d'entrada de // temporal. Quan n'hi haja més d'un (o quan els sarcs siguen sprites)
// players_, que és el d'assignació de controls. // 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<Player*> sorted; std::vector<Player*> sorted;
sorted.reserve(players_.size()); sorted.reserve(players_.size());
std::ranges::transform(players_, std::back_inserter(sorted), [](const auto& p) { return p.get(); }); std::ranges::transform(players_, std::back_inserter(sorted), [](const auto& p) { return p.get(); });
std::ranges::sort(sorted, [](const Player* a, const Player* b) { std::ranges::sort(sorted, [](const Player* a, const Player* b) {
return a->getFeetY() < b->getFeetY(); 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(); } for (auto* player : sorted) { player->render(); }
}
Notifier::get().render(); Notifier::get().render();
RenderInfo::get().render(); RenderInfo::get().render();
+15 -1
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <memory> #include <memory>
#include <string>
#include <vector> #include <vector>
#include "core/scene/scene.hpp" #include "core/scene/scene.hpp"
@@ -10,13 +11,26 @@ class Player;
class GameScene : public Scene { class GameScene : public Scene {
public: 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; ~GameScene() override;
void update(int delta_ms) override; void update(int delta_ms) override;
void render() override; void render() override;
private: private:
Config config_;
Tilemap tilemap_; Tilemap tilemap_;
// Vector des del principi: AEE suportarà multijugador local; cada Player // Vector des del principi: AEE suportarà multijugador local; cada Player
+7 -16
View File
@@ -1,23 +1,14 @@
#pragma once #pragma once
#include <SDL3/SDL.h> #include "utils/defaults.hpp"
// Constants semàntiques que mapegen cada element de la UI al seu color dins // Alias semàntics als índexs de paleta UI (viuen a `Defaults::UI`). Canviar
// de la paleta compartida (registre "ui" a Resource::Cache, carregada des de // l'aparença d'un component només requereix tocar-los allà; ací només
// data/palettes/sega-cube.pal). Canviar l'aparença d'un component només // reexportem amb els noms que consumeix el codi del joc.
// requereix canviar la seua constant ací — no cal buscar l'índex pel codi.
namespace UiColor { namespace UiColor {
// Color per al text de l'overlay de debug RenderInfo (F12). constexpr auto RENDER_INFO_TEXT = Defaults::UI::RENDER_INFO_TEXT;
constexpr Uint8 RENDER_INFO_TEXT = 47; constexpr auto RENDER_INFO_SHADOW = Defaults::UI::RENDER_INFO_SHADOW;
constexpr auto CANVAS_BG = Defaults::UI::CANVAS_BG;
// 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;
} // namespace UiColor } // namespace UiColor
+40
View File
@@ -80,4 +80,44 @@ namespace Defaults {
constexpr SDL_Scancode EXIT = SDL_SCANCODE_ESCAPE; constexpr SDL_Scancode EXIT = SDL_SCANCODE_ESCAPE;
} // namespace KeysGame } // namespace KeysGame
namespace Player {
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.
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 } // namespace Defaults