Merge branch 'refactor/options-class'

Tanca els 4 ítems DEFERRED de l'auditoria audit/game (COUP-02, COUP-03,
CONV-02 part Options, PATCH-01). Veure docs/audit-game.md → "Registro
de commits — refactor/options-class" per al desglossament dels commits
i les decisions preses durant el refactor.
This commit is contained in:
JailDesigner committed 2026-04-22 09:09:20 +02:00
commit a7e681a347
14 files changed
+375 -196

No files matched your search

+14 -4
View File
@@ -3,7 +3,7 @@
Rama: `audit/game`. Rama: `audit/game`.
Cobertura: `source/game/{entities,map,scenes,ui}` + `source/game/options.{hpp,cpp}` (12 fitxers, ~1.486 línies). Cobertura: `source/game/{entities,map,scenes,ui}` + `source/game/options.{hpp,cpp}` (12 fitxers, ~1.486 línies).
Estado global: **28 DONE, 2 DONE (parcial), 5 WONTFIX, 4 DEFERRED** sobre 39 ítems (el top 5 compta dins dels seus IDs individuals). 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). Els 4 DEFERRED (COUP-02, COUP-03, CONV-02 resta, PATCH-01) van a la futura branca `refactor/options-class`. Estado global: **28 DONE, 2 DONE (parcial), 5 WONTFIX, 4 DEFERRED** sobre 39 ítems (el top 5 compta dins dels seus IDs individuals). 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). Els 4 DEFERRED (COUP-02, COUP-03, CONV-02 resta, PATCH-01) es tanquen a la branca `refactor/options-class` (registre de commits al final del fitxer).
Estados posibles: Estados posibles:
- `PENDING` — identificat, sense tocar - `PENDING` — identificat, sense tocar
@@ -69,14 +69,14 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer
|----|--------------|----------|-----------|-------|-------| |----|--------------|----------|-----------|-------|-------|
| 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-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-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-03 | `options.hpp:96-105` | Globals `inline` mutats lliurement des de `core/input/global_inputs.cpp` | Importante | DONE | Estat mutable mogut a anonymous namespace dins `options.cpp`; API pública són getters const-ref (`window()`, `video()`, `audio()`, `locale()`, `postfxPresets()`, `crtpiPresets()`) + setters específics (`setWindowZoom`, `setShaderEnabled`, `setActiveShaderName`, ...) que muten i persisteixen via `saveToFile()`. Els setters NO notifiquen els subsistemes: el caller (`global_inputs.cpp`) crida després `Screen::setWindowZoom`, `reapplyShader()`, etc. La part "observers" del audit es deixa WONTFIX (veure CONV-02): notificar des d'Options invertiria la direcció `Director→Screen/Audio` i Options aprenria del layout dels subsistemes |
| 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 | | 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 ## 5. Inconsistències internes
| ID | Fitxer:línia | Troballa | Severitat | Estat | Notes | | 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-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. NOTA (post `refactor/options-class`): "encapsular" en COUP-03 vol dir ocultar l'estat mutable darrere de l'API del namespace (anonymous namespace + getters/setters), no convertir Options a classe — el patró `namespace per a estat compartit` queda preservat |
| 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-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-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-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 |
@@ -97,6 +97,16 @@ Els IDs es mantenen estables una vegada assignats. Cada entrada apunta al fitxer
| ID | Fitxer:línia | Troballa | Severitat | Estat | Notes | | 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-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-02 | `map.*`, `options.*` | Interfaces+callbacks: `Map` simulated class, `Options` globals sense observers | Importante | DONE | Map resolt amb PATCH-07 (namespace). Options resolt amb COUP-03: estat mutable encapsulat darrere de l'API del namespace (anonymous ns + getters/setters). La part "observers" (notificacions automàtiques a Screen/Audio) queda WONTFIX justificat: invertiria la direcció de dependència `Director→Screen/Audio` que el projecte manté intencionadament |
| CONV-03 | `options.cpp` | `std::cerr` viola la regla "std::cout only" | Importante | DONE | Resolt amb 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 | 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 | | 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 |
---
## Registro de commits — `refactor/options-class`
Branca per a tancar els 4 ítems DEFERRED de `audit/game`. Base: `main` (32dcf26).
- `a3b03b8` — COUP-03 + CONV-02: estat mutable d'`Options` mogut a anonymous ns + API de getters/setters; `global_inputs.cpp` i `director.cpp` adaptats. CONV-02 (part Options) tanca al pas. La part "observers" del audit es deixa WONTFIX (invertiria `Director→Screen/Audio`)
- `64e0178` — COUP-02: `Map::Info` plana injectada al `Player` (només Player; momia/roca/loadlevel resten amb `Map::*` global, com diu la nota original del audit)
- `d9f6fb4` — PATCH-01: `tryMove(dx, dy, CollisionFn)` a `MovingSprite` + hook virtual `collisionBox()` a `Sprite`; `Player::update` deixa de fer `update + revertir + reaplicar` i passa a `animate(dt) + tryMove`
+3 -1
View File
@@ -70,7 +70,9 @@ mirant amunt.
- DONE: sis animacions al YAML, `facing_` persistent, flip condicionat, - DONE: sis animacions al YAML, `facing_` persistent, flip condicionat,
`stand` automàtic a la col·lisió. `stand` automàtic a la col·lisió.
- Build i smoke tests (14/14) verdes. - Build i smoke tests (13/13) verdes. (Aquesta nota deia "14/14" abans;
el comptatge real al `tests/test_entities_smoke.cpp` són 13 funcions
`test*` invocades des de `main()` — corregit a `refactor/options-class`.)
- Sense smoke test dedicat: la lògica està entrellaçada amb teclat i - Sense smoke test dedicat: la lògica està entrellaçada amb teclat i
`Map::collides`; l'smoke actual de `Player` no cobreix teclat simulat. `Map::collides`; l'smoke actual de `Player` no cobreix teclat simulat.
- Pendent (ajornat): si algun dia es vol feedback auditiu de wall-bump - Pendent (ajornat): si algun dia es vol feedback auditiu de wall-bump
+52 -51
View File
@@ -42,83 +42,86 @@ namespace GlobalInputs {
} }
void onDecZoom() { void onDecZoom() {
if (Options::window.fullscreen) { return; } if (Options::window().fullscreen) { return; }
if (Options::window.zoom <= MIN_ZOOM) { return; } if (Options::window().zoom <= MIN_ZOOM) { return; }
Options::window.zoom--; const int NEW_ZOOM = Options::window().zoom - 1;
Screen::get().setWindowZoom(Options::window.zoom); Options::setWindowZoom(NEW_ZOOM);
Options::saveToFile(); Screen::get().setWindowZoom(NEW_ZOOM);
Notification::notify(Locale::get().text("input.zoom") + " x" + std::to_string(Options::window.zoom)); Notification::notify(Locale::get().text("input.zoom") + " x" + std::to_string(NEW_ZOOM));
} }
void onIncZoom() { void onIncZoom() {
if (Options::window.fullscreen) { return; } if (Options::window().fullscreen) { return; }
if (Options::window.zoom >= MAX_ZOOM) { return; } if (Options::window().zoom >= MAX_ZOOM) { return; }
Options::window.zoom++; const int NEW_ZOOM = Options::window().zoom + 1;
Screen::get().setWindowZoom(Options::window.zoom); Options::setWindowZoom(NEW_ZOOM);
Options::saveToFile(); Screen::get().setWindowZoom(NEW_ZOOM);
Notification::notify(Locale::get().text("input.zoom") + " x" + std::to_string(Options::window.zoom)); Notification::notify(Locale::get().text("input.zoom") + " x" + std::to_string(NEW_ZOOM));
} }
void onFullscreen() { void onFullscreen() {
Options::window.fullscreen = !Options::window.fullscreen; const bool NEW_FS = !Options::window().fullscreen;
Screen::get().setFullscreen(Options::window.fullscreen); Options::setWindowFullscreen(NEW_FS);
Options::saveToFile(); Screen::get().setFullscreen(NEW_FS);
notifyLocale(Options::window.fullscreen ? "input.fullscreen_on" : "input.fullscreen_off"); notifyLocale(NEW_FS ? "input.fullscreen_on" : "input.fullscreen_off");
} }
void onToggleVSync() { void onToggleVSync() {
Options::video.vsync = !Options::video.vsync; const bool NEW_VSYNC = !Options::video().vsync;
Screen::get().setVSync(Options::video.vsync); Options::setVideoVSync(NEW_VSYNC);
Options::saveToFile(); Screen::get().setVSync(NEW_VSYNC);
notifyLocale(Options::video.vsync ? "input.vsync_on" : "input.vsync_off"); notifyLocale(NEW_VSYNC ? "input.vsync_on" : "input.vsync_off");
} }
void onToggleShader() { void onToggleShader() {
Options::video.shader.enabled = !Options::video.shader.enabled; const bool NEW_ENABLED = !Options::video().shader.enabled;
Options::setShaderEnabled(NEW_ENABLED);
reapplyShader(); reapplyShader();
Options::saveToFile(); notifyLocale(NEW_ENABLED ? "input.shader_on" : "input.shader_off");
notifyLocale(Options::video.shader.enabled ? "input.shader_on" : "input.shader_off");
} }
void onNextShader() { void onNextShader() {
Options::video.shader.current_shader = (Options::video.shader.current_shader == "crtpi") ? "postfx" : "crtpi"; const std::string NEW_NAME = (Options::video().shader.current_shader == "crtpi") ? "postfx" : "crtpi";
Options::setActiveShaderName(NEW_NAME);
reapplyShader(); reapplyShader();
Options::saveToFile(); Notification::notify(Locale::get().text("input.shader") + ": " + NEW_NAME);
Notification::notify(Locale::get().text("input.shader") + ": " + Options::video.shader.current_shader);
} }
// Busca l'entrada actual al vector de presets i passa a la següent // Busca l'entrada actual al vector de presets i passa a la següent
// (wrap-around). Si no en troba cap s'evita el canvi silenciosament. // (wrap-around). Si no en troba cap s'evita el canvi silenciosament.
void onNextPreset() { void onNextPreset() {
const bool IS_CRTPI = Options::video.shader.current_shader == "crtpi"; const bool IS_CRTPI = Options::video().shader.current_shader == "crtpi";
std::string new_preset;
if (IS_CRTPI) { if (IS_CRTPI) {
if (Options::crtpi_presets.empty()) { return; } const auto& presets = Options::crtpiPresets();
auto it = std::ranges::find_if(Options::crtpi_presets, if (presets.empty()) { return; }
[](const auto& preset) { return preset.name == Options::video.shader.current_crtpi_preset; }); auto it = std::ranges::find_if(presets,
std::size_t idx = (it == Options::crtpi_presets.end()) ? 0 : std::distance(Options::crtpi_presets.begin(), it); [](const auto& preset) { return preset.name == Options::video().shader.current_crtpi_preset; });
idx = (idx + 1) % Options::crtpi_presets.size(); std::size_t idx = (it == presets.end()) ? 0 : std::distance(presets.begin(), it);
Options::video.shader.current_crtpi_preset = Options::crtpi_presets[idx].name; idx = (idx + 1) % presets.size();
new_preset = presets[idx].name;
Options::setCrtPiPreset(new_preset);
} else { } else {
if (Options::postfx_presets.empty()) { return; } const auto& presets = Options::postfxPresets();
auto it = std::ranges::find_if(Options::postfx_presets, if (presets.empty()) { return; }
[](const auto& preset) { return preset.name == Options::video.shader.current_postfx_preset; }); auto it = std::ranges::find_if(presets,
std::size_t idx = (it == Options::postfx_presets.end()) ? 0 : std::distance(Options::postfx_presets.begin(), it); [](const auto& preset) { return preset.name == Options::video().shader.current_postfx_preset; });
idx = (idx + 1) % Options::postfx_presets.size(); std::size_t idx = (it == presets.end()) ? 0 : std::distance(presets.begin(), it);
Options::video.shader.current_postfx_preset = Options::postfx_presets[idx].name; idx = (idx + 1) % presets.size();
new_preset = presets[idx].name;
Options::setPostFXPreset(new_preset);
} }
reapplyShader(); reapplyShader();
Options::saveToFile(); Notification::notify(Locale::get().text("input.preset") + ": " + new_preset);
const std::string& name = IS_CRTPI ? Options::video.shader.current_crtpi_preset : Options::video.shader.current_postfx_preset;
Notification::notify(Locale::get().text("input.preset") + ": " + name);
} }
void onNextScalingMode() { void onNextScalingMode() {
using Mode = Defaults::ScalingMode; using Mode = Defaults::ScalingMode;
Options::video.scaling_mode = static_cast<Mode>((static_cast<int>(Options::video.scaling_mode) + 1) % 5); const auto NEW_MODE = static_cast<Mode>((static_cast<int>(Options::video().scaling_mode) + 1) % 5);
Screen::get().setScalingMode(Options::video.scaling_mode); Options::setVideoScalingMode(NEW_MODE);
Options::saveToFile(); Screen::get().setScalingMode(NEW_MODE);
const char* mode_key = nullptr; const char* mode_key = nullptr;
switch (Options::video.scaling_mode) { switch (NEW_MODE) {
case Mode::DISABLED: case Mode::DISABLED:
mode_key = "input.scaling_disabled"; mode_key = "input.scaling_disabled";
break; break;
@@ -139,14 +142,12 @@ namespace GlobalInputs {
} }
void onCycleRenderInfo() { void onCycleRenderInfo() {
const int NEXT = (static_cast<int>(Options::video.render_info_position) + 1) % Defaults::RENDER_INFO_POSITION_COUNT; const int NEXT = (static_cast<int>(Options::video().render_info_position) + 1) % Defaults::RENDER_INFO_POSITION_COUNT;
Options::video.render_info_position = static_cast<Defaults::RenderInfoPosition>(NEXT); Options::setVideoRenderInfoPosition(static_cast<Defaults::RenderInfoPosition>(NEXT));
Options::saveToFile();
} }
void onToggleRenderTime() { void onToggleRenderTime() {
Options::video.render_info_show_time = !Options::video.render_info_show_time; Options::setVideoRenderInfoShowTime(!Options::video().render_info_show_time);
Options::saveToFile();
} }
void onExit() { void onExit() {
@@ -72,3 +72,12 @@ void MovingSprite::setPosY(float value) {
y_ = value; y_ = value;
pos_.y = static_cast<int>(y_); pos_.y = static_cast<int>(y_);
} }
void MovingSprite::tryMove(float dx, float dy, const CollisionFn& collides) {
const float OLD_X = getPosX();
setPosX(OLD_X + dx);
if (collides(collisionBox())) { setPosX(OLD_X); }
const float OLD_Y = getPosY();
setPosY(OLD_Y + dy);
if (collides(collisionBox())) { setPosY(OLD_Y); }
}
@@ -2,6 +2,7 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <functional>
#include <memory> #include <memory>
#include "core/rendering/sprite/sprite.hpp" #include "core/rendering/sprite/sprite.hpp"
@@ -40,6 +41,15 @@ class MovingSprite : public Sprite {
auto getFlip() -> SDL_FlipMode { return flip_; } auto getFlip() -> SDL_FlipMode { return flip_; }
void flip() { flip_ = (flip_ == SDL_FLIP_HORIZONTAL) ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL; } void flip() { flip_ = (flip_ == SDL_FLIP_HORIZONTAL) ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL; }
// Moviment eix-separat amb col·lisió: aplica `dx` (i si la nova
// `collisionBox()` col·lisiona, el reverteix); després aplica `dy`
// (idem). El caller calcula els deltes (típicament `vx_*dt`,
// `vy_*dt`) i passa la funció de detecció. Aquesta separació deixa
// que el sprite "llisqui" contra obstacles en compte de quedar-se
// bloquejat en sec quan només un eix col·lisiona.
using CollisionFn = std::function<bool(const SDL_FRect&)>;
void tryMove(float dx, float dy, const CollisionFn& collides);
protected: protected:
void move(float delta_time); void move(float delta_time);
+9
View File
@@ -19,6 +19,15 @@ class Sprite {
// Hook d'actualització per frame. Base buida (no hi ha Sprite pur a // Hook d'actualització per frame. Base buida (no hi ha Sprite pur a
// actualitzar); les subclasses mòbils/animades fan override. // actualitzar); les subclasses mòbils/animades fan override.
virtual void update(float /*delta_time*/) {} virtual void update(float /*delta_time*/) {}
// Caixa que `MovingSprite::tryMove` envia al callback de col·lisió per
// decidir si el moviment és vàlid. Per defecte coincideix amb `pos_`
// (el sprite sencer); les subclasses que usen falsa perspectiva (p.ex.
// `Player`) la sobreescriuen amb una sub-caixa més petita (només els
// peus) per a que el cap puga sobresortir sense bloquejar el
// moviment.
[[nodiscard]] virtual auto collisionBox() const -> SDL_FRect { return pos_; }
virtual void render(); virtual void render();
// Renderitza substituint l'índex source_color per target_color (ús típic: // Renderitza substituint l'índex source_color per target_color (ús típic:
// la classe Text, que recolora glifs monocroms per paràmetre). // la classe Text, que recolora glifs monocroms per paràmetre).
+1 -1
View File
@@ -61,7 +61,7 @@ Director::Director() {
// Traduccions per a tots els textos visibles. Si el fitxer no existeix // Traduccions per a tots els textos visibles. Si el fitxer no existeix
// o hi ha un error de parse, Locale::text() caurà a retornar la clau. // o hi ha un error de parse, Locale::text() caurà a retornar la clau.
Locale::get().load(std::string(Defaults::I18n::LOCALE_DIR) + Options::locale + ".yaml"); Locale::get().load(std::string(Defaults::I18n::LOCALE_DIR) + Options::locale() + ".yaml");
// Hotkeys globals. GlobalInputs::update() les consumirà a cada frame. // Hotkeys globals. GlobalInputs::update() les consumirà a cada frame.
KeyConfig::get().load("data/input/keys.yaml"); KeyConfig::get().load("data/input/keys.yaml");
+24 -33
View File
@@ -11,7 +11,6 @@
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "game/entities/event_bus.hpp" #include "game/entities/event_bus.hpp"
#include "game/map/map.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
namespace { namespace {
@@ -47,9 +46,10 @@ void Player::loadFrom(const std::string& yaml_path) {
collision_y_offset = (sprite_h - base_size) - 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) Player::Player(const std::string& anim_key, float x, float y, const Controls& controls, const Map::Info& map_info)
: AnimatedSprite(Resource::Cache::get().getAnimationData(anim_key)), : AnimatedSprite(Resource::Cache::get().getAnimationData(anim_key)),
controls_(controls) { controls_(controls),
map_(map_info) {
setPos(x, y); setPos(x, y);
setCurrentAnimation("stand"); setCurrentAnimation("stand");
} }
@@ -104,22 +104,13 @@ void Player::update(float delta_time) {
snapToVerticalLane(delta_time); snapToVerticalLane(delta_time);
} }
// Guardem la posició prèvia i deixem que AnimatedSprite::update aplique // Animació del frame + moviment col·lisió-aware. `tryMove` aplica els
// la velocitat com sempre; així no dupliquem la integració temporal ni // deltes per eix i reverteix l'eix que col·lisiona, així el jugador
// l'animació. Després, axis-separated collision: revertim i re-apliquem // llisca contra sarcòfags i murs en compte de quedar-se bloquejat en
// cada eix comprovant el mapa de colisions, de manera que el jugador // sec. La caixa que s'envia al callback és `collisionBox()` (override
// llisca contra els sarcòfags en compte de quedar-se bloquejat en sec. // a Player retornant `getBaseBox()`), no el sprite sencer.
const float OLD_X = getPosX(); animate(delta_time);
const float OLD_Y = getPosY(); tryMove(vx * delta_time, vy * delta_time, [this](const SDL_FRect& box) { return map_.collides(box); });
AnimatedSprite::update(delta_time);
const float NEW_X = getPosX();
const float NEW_Y = getPosY();
setPos(OLD_X, OLD_Y);
setPosX(NEW_X);
if (Map::collides(baseBox())) { setPosX(OLD_X); }
setPosY(NEW_Y);
if (Map::collides(baseBox())) { setPosY(OLD_Y); }
clampToPlayfield(); clampToPlayfield();
@@ -157,28 +148,28 @@ auto Player::animationName(bool moving) const -> const char* {
} }
auto Player::getCurrentTile() const -> std::pair<int, int> { auto Player::getCurrentTile() const -> std::pair<int, int> {
// El grid del tilemap és de `Map::tile_size` píxels i està 16-alineat // El grid del tilemap és de `tile_size` píxels i està 16-alineat
// respecte a l'origen del tilemap (pot ser negatiu per overscan). El // respecte a l'origen del tilemap (pot ser negatiu per overscan). El
// centre de la caixa de col·lisió ens dóna el tile on "està" el Player. // centre de la caixa de col·lisió ens dóna el tile on "està" el Player.
const auto BOX = baseBox(); const auto BOX = baseBox();
const float CX = BOX.x + (BOX.w * 0.5F); const float CX = BOX.x + (BOX.w * 0.5F);
const float CY = BOX.y + (BOX.h * 0.5F); const float CY = BOX.y + (BOX.h * 0.5F);
const int COL = static_cast<int>(std::floor( const int COL = static_cast<int>(std::floor(
(CX - static_cast<float>(Map::tilemap_origin_x)) / static_cast<float>(Map::tile_size))); (CX - static_cast<float>(map_.tilemap_origin_x)) / static_cast<float>(map_.tile_size)));
const int ROW = static_cast<int>(std::floor( const int ROW = static_cast<int>(std::floor(
(CY - static_cast<float>(Map::tilemap_origin_y)) / static_cast<float>(Map::tile_size))); (CY - static_cast<float>(map_.tilemap_origin_y)) / static_cast<float>(map_.tile_size)));
return {COL, ROW}; return {COL, ROW};
} }
void Player::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.
const 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);
const 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);
const 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);
const 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));
} }
@@ -214,24 +205,24 @@ void Player::snapToLane(
const float STEP = std::min(std::abs(DELTA), SPEED_PX_PER_S * delta_time); const float STEP = std::min(std::abs(DELTA), SPEED_PX_PER_S * delta_time);
const float SAVED = (this->*get_pos)(); const float SAVED = (this->*get_pos)();
(this->*set_pos)(SAVED + std::copysign(STEP, DELTA)); (this->*set_pos)(SAVED + std::copysign(STEP, DELTA));
if (Map::collides(baseBox())) { (this->*set_pos)(SAVED); } if (map_.collides(baseBox())) { (this->*set_pos)(SAVED); }
} }
} }
void Player::snapToHorizontalLane(float delta_time) { void Player::snapToHorizontalLane(float delta_time) {
snapToLane(delta_time, snapToLane(delta_time,
static_cast<float>(Map::playfield_y - collision_y_offset), static_cast<float>(map_.playfield_y - collision_y_offset),
static_cast<float>(Map::pitch_y), static_cast<float>(map_.pitch_y),
Map::grid_rows, map_.grid_rows,
&Player::getPosY, &Player::getPosY,
&Player::setPosY); &Player::setPosY);
} }
void Player::snapToVerticalLane(float delta_time) { void Player::snapToVerticalLane(float delta_time) {
snapToLane(delta_time, snapToLane(delta_time,
static_cast<float>(Map::playfield_x - collision_x_offset), static_cast<float>(map_.playfield_x - collision_x_offset),
static_cast<float>(Map::pitch_x), static_cast<float>(map_.pitch_x),
Map::grid_cols, map_.grid_cols,
&Player::getPosX, &Player::getPosX,
&Player::setPosX); &Player::setPosX);
} }
+14 -1
View File
@@ -7,6 +7,7 @@
#include <utility> #include <utility>
#include "core/rendering/sprite/animated_sprite.hpp" #include "core/rendering/sprite/animated_sprite.hpp"
#include "game/map/map.hpp"
class EventBus; class EventBus;
@@ -40,7 +41,7 @@ class Player : public AnimatedSprite {
// Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor. // Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor.
static void loadFrom(const std::string& yaml_path); static void loadFrom(const std::string& yaml_path);
Player(const std::string& anim_key, float x, float y, const Controls& controls); Player(const std::string& anim_key, float x, float y, const Controls& controls, const Map::Info& map_info);
void update(float delta_time) override; void update(float delta_time) override;
@@ -66,6 +67,12 @@ class Player : public AnimatedSprite {
// amb el jugador (momies, trampes, porta). // amb el jugador (momies, trampes, porta).
[[nodiscard]] auto getBaseBox() const -> SDL_FRect; [[nodiscard]] auto getBaseBox() const -> SDL_FRect;
// Override del hook de Sprite: `MovingSprite::tryMove` envia aquesta
// caixa al callback de col·lisió. La base del Player és més petita
// que el sprite (el cap pot sobresortir per murs/sarcòfags sense
// bloquejar el moviment).
[[nodiscard]] auto collisionBox() const -> SDL_FRect override { return getBaseBox(); }
private: private:
// Alias privat històric. Mantingut perquè viu dins de l'implementació // Alias privat històric. Mantingut perquè viu dins de l'implementació
// (snapToLane, clamp). Els consumidors externs usen `getBaseBox()`. // (snapToLane, clamp). Els consumidors externs usen `getBaseBox()`.
@@ -95,6 +102,12 @@ class Player : public AnimatedSprite {
void (Player::*set_pos)(float)); void (Player::*set_pos)(float));
Controls controls_; Controls controls_;
// Còpia plana de la geometria del mapa + punters a `collides`/
// `inside_playfield`. POD trivial; copiada al ctor i no muta durant
// la vida del Player. Vol dir que aquest Player només és vàlid per
// a un mapa concret — si el mapa canvia, la Scene ha de
// reinstanciar-lo.
Map::Info map_;
// Direcció "cap on mira" el jugador. Governa quin conjunt d'animacions // Direcció "cap on mira" el jugador. Governa quin conjunt d'animacions
// s'aplica (lateral, amunt, avall) i si el sprite es flipeja o no. Els // s'aplica (lateral, amunt, avall) i si el sprite es flipeja o no. Els
+18
View File
@@ -104,4 +104,22 @@ namespace Map {
return box.x >= static_cast<float>(playfield_x) && box.y >= static_cast<float>(playfield_y) && box.x + box.w <= static_cast<float>(playfield_x + playfield_w) && box.y + box.h <= static_cast<float>(playfield_y + playfield_h); return box.x >= static_cast<float>(playfield_x) && box.y >= static_cast<float>(playfield_y) && box.x + box.w <= static_cast<float>(playfield_x + playfield_w) && box.y + box.h <= static_cast<float>(playfield_y + playfield_h);
} }
auto makeInfo() -> Info {
return {
.playfield_x = playfield_x,
.playfield_y = playfield_y,
.playfield_w = playfield_w,
.playfield_h = playfield_h,
.pitch_x = pitch_x,
.pitch_y = pitch_y,
.grid_cols = grid_cols,
.grid_rows = grid_rows,
.tile_size = tile_size,
.tilemap_origin_x = tilemap_origin_x,
.tilemap_origin_y = tilemap_origin_y,
.collides = &collides,
.inside_playfield = &insidePlayfield,
};
}
} // namespace Map } // namespace Map
+31
View File
@@ -14,6 +14,14 @@
// els membres viuen com a globals del namespace i els callers hi accedeixen // els membres viuen com a globals del namespace i els callers hi accedeixen
// amb `Map::grid_cols`, `Map::collides(...)`, etc. El canvi respecte a // amb `Map::grid_cols`, `Map::collides(...)`, etc. El canvi respecte a
// l'anterior `class Map { static ... };` és purament sintàctic. // l'anterior `class Map { static ... };` és purament sintàctic.
//
// `MapInfo` (POD trivialment copiable) empaqueta la geometria + dos
// punters a funció de col·lisió per injectar-la a entitats que han
// d'evitar conèixer el namespace `Map::` directament. Construïda via
// `Map::makeInfo()` després de `loadFrom`. Avui només l'usa el Player
// (COUP-02 a docs/audit-game.md); momies, roques i `loadFromLevel`
// continuen llegint `Map::*` directament — la nota original justifica
// que mentre hi hagi un Map global actiu, l'acoblament és acceptable.
namespace Map { namespace Map {
// Geometria de la graella (carregada de pyramid.yaml > geometry). // Geometria de la graella (carregada de pyramid.yaml > geometry).
@@ -73,4 +81,27 @@ namespace Map {
// entitats dinàmiques per validar posicions candidates del seu step. // entitats dinàmiques per validar posicions candidates del seu step.
[[nodiscard]] auto insidePlayfield(const SDL_FRect& box) -> bool; [[nodiscard]] auto insidePlayfield(const SDL_FRect& box) -> bool;
// Snapshot plà de la geometria del mapa + punters a `collides` /
// `insidePlayfield`. Pensat per ser passat per valor (POD petit,
// trivialment copiable). Un cop construïda, llegeix els valors
// congelats — si `Map::loadFrom` es torna a cridar amb un mapa
// diferent, cal demanar un MapInfo nou.
struct Info {
int playfield_x{0};
int playfield_y{0};
int playfield_w{0};
int playfield_h{0};
int pitch_x{0};
int pitch_y{0};
int grid_cols{0};
int grid_rows{0};
int tile_size{0};
int tilemap_origin_x{0};
int tilemap_origin_y{0};
bool (*collides)(const SDL_FRect&){nullptr};
bool (*inside_playfield)(const SDL_FRect&){nullptr};
};
[[nodiscard]] auto makeInfo() -> Info;
} // namespace Map } // namespace Map
+157 -91
View File
@@ -14,6 +14,21 @@
namespace { namespace {
// --- Estat mutable (privat al TU) ---
// Encapsulat ací per impedir mutacions directes des de fora del namespace
// Options:: (decisió COUP-03 / docs/audit-game.md). Els callers llegeixen
// via getters const-ref i muten via setters específics que persisteixen.
std::string g_config_version;
std::string g_config_file_path;
std::string g_postfx_file_path;
std::string g_crtpi_file_path;
std::string g_locale{Defaults::I18n::LOCALE};
Options::Window g_window{};
Options::Video g_video{};
Options::Audio g_audio{};
std::vector<Options::PostFXPreset> g_postfx_presets{};
std::vector<Options::CrtPiPreset> g_crtpi_presets{};
// Taules enum ↔ string per a la serialització a YAML. La primera entrada // 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 // é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. // cap entrada coneguda — per això `LETTERBOX`/`NEAREST`/`OFF` encapçalen.
@@ -93,40 +108,40 @@ namespace {
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"];
readField(node, "zoom", Options::window.zoom); readField(node, "zoom", g_window.zoom);
readField(node, "fullscreen", Options::window.fullscreen); readField(node, "fullscreen", g_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"];
readField(node, "acceleration", Options::video.gpu.acceleration); readField(node, "acceleration", g_video.gpu.acceleration);
readField(node, "preferred_driver", Options::video.gpu.preferred_driver); readField(node, "preferred_driver", g_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"];
readField(node, "enabled", Options::video.shader.enabled); readField(node, "enabled", g_video.shader.enabled);
readField(node, "current_shader", Options::video.shader.current_shader); readField(node, "current_shader", g_video.shader.current_shader);
readField(node, "current_postfx_preset", Options::video.shader.current_postfx_preset); readField(node, "current_postfx_preset", g_video.shader.current_postfx_preset);
readField(node, "current_crtpi_preset", Options::video.shader.current_crtpi_preset); readField(node, "current_crtpi_preset", g_video.shader.current_crtpi_preset);
} }
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"];
readField(node, "vsync", Options::video.vsync); readField(node, "vsync", g_video.vsync);
if (node.contains("scaling_mode")) { if (node.contains("scaling_mode")) {
Options::video.scaling_mode = enumFromString(SCALING_MODE_NAMES, node["scaling_mode"].get_value<std::string>()); g_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 = enumFromString(TEXTURE_FILTER_NAMES, node["texture_filter"].get_value<std::string>()); g_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 = enumFromString(RENDER_INFO_POSITION_NAMES, node["render_info_position"].get_value<std::string>()); g_video.render_info_position = enumFromString(RENDER_INFO_POSITION_NAMES, node["render_info_position"].get_value<std::string>());
} }
readField(node, "render_info_show_time", Options::video.render_info_show_time); readField(node, "render_info_show_time", g_video.render_info_show_time);
parseGpu(node); parseGpu(node);
parseShader(node); parseShader(node);
} }
@@ -134,22 +149,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"];
readField(node, "enabled", Options::audio.music.enabled); readField(node, "enabled", g_audio.music.enabled);
readField(node, "volume", Options::audio.music.volume); readField(node, "volume", g_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"];
readField(node, "enabled", Options::audio.sound.enabled); readField(node, "enabled", g_audio.sound.enabled);
readField(node, "volume", Options::audio.sound.volume); readField(node, "volume", g_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"];
readField(node, "enabled", Options::audio.enabled); readField(node, "enabled", g_audio.enabled);
readField(node, "volume", Options::audio.volume); readField(node, "volume", g_audio.volume);
parseMusic(node); parseMusic(node);
parseSound(node); parseSound(node);
} }
@@ -423,14 +438,65 @@ namespace {
namespace Options { namespace Options {
// --- Getters de l'estat ---
auto window() -> const Window& { return g_window; }
auto video() -> const Video& { return g_video; }
auto audio() -> const Audio& { return g_audio; }
auto locale() -> const std::string& { return g_locale; }
auto configVersion() -> const std::string& { return g_config_version; }
auto postfxPresets() -> const std::vector<PostFXPreset>& { return g_postfx_presets; }
auto crtpiPresets() -> const std::vector<CrtPiPreset>& { return g_crtpi_presets; }
// --- Setters: muten + persisteixen ---
void setWindowZoom(int value) {
g_window.zoom = value;
saveToFile();
}
void setWindowFullscreen(bool value) {
g_window.fullscreen = value;
saveToFile();
}
void setVideoVSync(bool value) {
g_video.vsync = value;
saveToFile();
}
void setVideoScalingMode(Defaults::ScalingMode value) {
g_video.scaling_mode = value;
saveToFile();
}
void setVideoRenderInfoPosition(Defaults::RenderInfoPosition value) {
g_video.render_info_position = value;
saveToFile();
}
void setVideoRenderInfoShowTime(bool value) {
g_video.render_info_show_time = value;
saveToFile();
}
void setShaderEnabled(bool value) {
g_video.shader.enabled = value;
saveToFile();
}
void setActiveShaderName(const std::string& value) {
g_video.shader.current_shader = value;
saveToFile();
}
void setPostFXPreset(const std::string& value) {
g_video.shader.current_postfx_preset = value;
saveToFile();
}
void setCrtPiPreset(const std::string& value) {
g_video.shader.current_crtpi_preset = value;
saveToFile();
}
void setConfigFile(const std::string& path) { void setConfigFile(const std::string& path) {
config_file_path = path; g_config_file_path = path;
} }
auto loadFromFile() -> bool { auto loadFromFile() -> bool {
if (config_file_path.empty()) { return false; } if (g_config_file_path.empty()) { return false; }
const std::string CONTENT = readFileOrEmpty(config_file_path); const std::string CONTENT = readFileOrEmpty(g_config_file_path);
if (CONTENT.empty()) { if (CONTENT.empty()) {
// Primera execució: creem el fitxer amb els valors per defecte. // Primera execució: creem el fitxer amb els valors per defecte.
return saveToFile(); return saveToFile();
@@ -449,12 +515,12 @@ namespace Options {
if (file_version != Texts::VERSION) { if (file_version != Texts::VERSION) {
std::cout << "Options: config.yaml versió \"" << file_version std::cout << "Options: config.yaml versió \"" << file_version
<< "\" != \"" << Texts::VERSION << "\"; reescrivint amb defaults.\n"; << "\" != \"" << Texts::VERSION << "\"; reescrivint amb defaults.\n";
backupFile(config_file_path); backupFile(g_config_file_path);
return saveToFile(); return saveToFile();
} }
config_version = file_version; g_config_version = file_version;
if (yaml.contains("locale")) { Options::locale = yaml["locale"].get_value<std::string>(); } if (yaml.contains("locale")) { g_locale = yaml["locale"].get_value<std::string>(); }
parseWindow(yaml); parseWindow(yaml);
parseVideo(yaml); parseVideo(yaml);
parseAudio(yaml); parseAudio(yaml);
@@ -462,84 +528,84 @@ namespace Options {
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cout << "Options: error parsejant config.yaml: " << e.what() << '\n'; std::cout << "Options: error parsejant config.yaml: " << e.what() << '\n';
std::cout << "Options: reescrivint el fitxer amb defaults.\n"; std::cout << "Options: reescrivint el fitxer amb defaults.\n";
backupFile(config_file_path); backupFile(g_config_file_path);
return saveToFile(); return saveToFile();
} }
} }
auto saveToFile() -> bool { auto saveToFile() -> bool {
if (config_file_path.empty()) { return false; } if (g_config_file_path.empty()) { return false; }
config_version = Texts::VERSION; g_config_version = Texts::VERSION;
std::ostringstream out; std::ostringstream out;
out << "# Configuració d'AEE 2026. Generat automàticament; editable a mà.\n"; out << "# Configuració d'AEE 2026. Generat automàticament; editable a mà.\n";
out << "version: \"" << config_version << "\"\n"; out << "version: \"" << g_config_version << "\"\n";
out << "locale: \"" << locale << "\"\n\n"; out << "locale: \"" << g_locale << "\"\n\n";
out << "window:\n"; out << "window:\n";
out << " zoom: " << window.zoom << "\n"; out << " zoom: " << g_window.zoom << "\n";
out << " fullscreen: " << (window.fullscreen ? "true" : "false") << "\n\n"; out << " fullscreen: " << (g_window.fullscreen ? "true" : "false") << "\n\n";
out << "video:\n"; out << "video:\n";
out << " vsync: " << (video.vsync ? "true" : "false") << "\n"; out << " vsync: " << (g_video.vsync ? "true" : "false") << "\n";
out << " scaling_mode: \"" << enumToString(SCALING_MODE_NAMES, video.scaling_mode) << "\"\n"; out << " scaling_mode: \"" << enumToString(SCALING_MODE_NAMES, g_video.scaling_mode) << "\"\n";
out << " texture_filter: \"" << enumToString(TEXTURE_FILTER_NAMES, video.texture_filter) << "\"\n"; out << " texture_filter: \"" << enumToString(TEXTURE_FILTER_NAMES, g_video.texture_filter) << "\"\n";
out << " render_info_position: \"" << enumToString(RENDER_INFO_POSITION_NAMES, video.render_info_position) << "\"\n"; out << " render_info_position: \"" << enumToString(RENDER_INFO_POSITION_NAMES, g_video.render_info_position) << "\"\n";
out << " render_info_show_time: " << (video.render_info_show_time ? "true" : "false") << "\n"; out << " render_info_show_time: " << (g_video.render_info_show_time ? "true" : "false") << "\n";
out << " gpu:\n"; out << " gpu:\n";
out << " acceleration: " << (video.gpu.acceleration ? "true" : "false") << "\n"; out << " acceleration: " << (g_video.gpu.acceleration ? "true" : "false") << "\n";
out << " preferred_driver: \"" << video.gpu.preferred_driver << "\"\n"; out << " preferred_driver: \"" << g_video.gpu.preferred_driver << "\"\n";
out << " shader:\n"; out << " shader:\n";
out << " enabled: " << (video.shader.enabled ? "true" : "false") << "\n"; out << " enabled: " << (g_video.shader.enabled ? "true" : "false") << "\n";
out << " current_shader: \"" << video.shader.current_shader << "\"\n"; out << " current_shader: \"" << g_video.shader.current_shader << "\"\n";
out << " current_postfx_preset: \"" << video.shader.current_postfx_preset << "\"\n"; out << " current_postfx_preset: \"" << g_video.shader.current_postfx_preset << "\"\n";
out << " current_crtpi_preset: \"" << video.shader.current_crtpi_preset << "\"\n\n"; out << " current_crtpi_preset: \"" << g_video.shader.current_crtpi_preset << "\"\n\n";
out << "audio:\n"; out << "audio:\n";
out << " enabled: " << (audio.enabled ? "true" : "false") << "\n"; out << " enabled: " << (g_audio.enabled ? "true" : "false") << "\n";
out << " volume: " << audio.volume << "\n"; out << " volume: " << g_audio.volume << "\n";
out << " music:\n"; out << " music:\n";
out << " enabled: " << (audio.music.enabled ? "true" : "false") << "\n"; out << " enabled: " << (g_audio.music.enabled ? "true" : "false") << "\n";
out << " volume: " << audio.music.volume << "\n"; out << " volume: " << g_audio.music.volume << "\n";
out << " sound:\n"; out << " sound:\n";
out << " enabled: " << (audio.sound.enabled ? "true" : "false") << "\n"; out << " enabled: " << (g_audio.sound.enabled ? "true" : "false") << "\n";
out << " volume: " << audio.sound.volume << "\n"; out << " volume: " << g_audio.sound.volume << "\n";
return writeFile(config_file_path, out.str()); return writeFile(g_config_file_path, out.str());
} }
auto buildScreenConfig() -> Screen::Config { auto buildScreenConfig() -> Screen::Config {
return { return {
.zoom = window.zoom, .zoom = g_window.zoom,
.fullscreen = window.fullscreen, .fullscreen = g_window.fullscreen,
.vsync = video.vsync, .vsync = g_video.vsync,
.scaling_mode = video.scaling_mode, .scaling_mode = g_video.scaling_mode,
.texture_filter = video.texture_filter, .texture_filter = g_video.texture_filter,
.gpu_acceleration = video.gpu.acceleration, .gpu_acceleration = g_video.gpu.acceleration,
.preferred_driver = video.gpu.preferred_driver, .preferred_driver = g_video.gpu.preferred_driver,
}; };
} }
auto buildAudioConfig() -> ::Audio::Config { auto buildAudioConfig() -> ::Audio::Config {
return { return {
.enabled = audio.enabled, .enabled = g_audio.enabled,
.volume = audio.volume, .volume = g_audio.volume,
.music_enabled = audio.music.enabled, .music_enabled = g_audio.music.enabled,
.music_volume = audio.music.volume, .music_volume = g_audio.music.volume,
.sound_enabled = audio.sound.enabled, .sound_enabled = g_audio.sound.enabled,
.sound_volume = audio.sound.volume, .sound_volume = g_audio.sound.volume,
}; };
} }
void setPostFXFile(const std::string& path) { void setPostFXFile(const std::string& path) {
postfx_file_path = path; g_postfx_file_path = path;
} }
auto loadPostFXFromFile() -> bool { auto loadPostFXFromFile() -> bool {
return loadPresetsFromFile<PostFXPreset>( return loadPresetsFromFile<PostFXPreset>(
postfx_file_path, g_postfx_file_path,
postfx_presets, g_postfx_presets,
&defaultPostFXPresets, &defaultPostFXPresets,
&parsePostFXPreset, &parsePostFXPreset,
&savePostFXToFile, &savePostFXToFile,
@@ -548,21 +614,21 @@ namespace Options {
auto savePostFXToFile() -> bool { auto savePostFXToFile() -> bool {
return savePresetsToFile<PostFXPreset>( return savePresetsToFile<PostFXPreset>(
postfx_file_path, g_postfx_file_path,
postfx_presets, g_postfx_presets,
&writePostFXPreset, &writePostFXPreset,
"Presets PostFX per a AEE 2026. Editable a mà.", "Presets PostFX per a AEE 2026. Editable a mà.",
"Els noms han de coincidir amb config.yaml > video.shader.current_postfx_preset."); "Els noms han de coincidir amb config.yaml > video.shader.current_postfx_preset.");
} }
void setCrtPiFile(const std::string& path) { void setCrtPiFile(const std::string& path) {
crtpi_file_path = path; g_crtpi_file_path = path;
} }
auto loadCrtPiFromFile() -> bool { auto loadCrtPiFromFile() -> bool {
return loadPresetsFromFile<CrtPiPreset>( return loadPresetsFromFile<CrtPiPreset>(
crtpi_file_path, g_crtpi_file_path,
crtpi_presets, g_crtpi_presets,
&defaultCrtPiPresets, &defaultCrtPiPresets,
&parseCrtPiPreset, &parseCrtPiPreset,
&saveCrtPiToFile, &saveCrtPiToFile,
@@ -571,29 +637,29 @@ namespace Options {
auto saveCrtPiToFile() -> bool { auto saveCrtPiToFile() -> bool {
return savePresetsToFile<CrtPiPreset>( return savePresetsToFile<CrtPiPreset>(
crtpi_file_path, g_crtpi_file_path,
crtpi_presets, g_crtpi_presets,
&writeCrtPiPreset, &writeCrtPiPreset,
"Presets CRT-Pi per a AEE 2026. Editable a mà.", "Presets CRT-Pi per a AEE 2026. Editable a mà.",
"Els noms han de coincidir amb config.yaml > video.shader.current_crtpi_preset."); "Els noms han de coincidir amb config.yaml > video.shader.current_crtpi_preset.");
} }
auto buildActiveShader() -> Rendering::ShaderType { auto buildActiveShader() -> Rendering::ShaderType {
if (video.shader.current_shader == "crtpi") { if (g_video.shader.current_shader == "crtpi") {
return Rendering::ShaderType::CRTPI; return Rendering::ShaderType::CRTPI;
} }
return Rendering::ShaderType::POSTFX; return Rendering::ShaderType::POSTFX;
} }
auto buildRenderInfoFrame(Uint8 text_color, Uint8 shadow_color) -> RenderInfo::Frame { auto buildRenderInfoFrame(Uint8 text_color, Uint8 shadow_color) -> RenderInfo::Frame {
const std::string& active_preset = (video.shader.current_shader == "crtpi") const std::string& active_preset = (g_video.shader.current_shader == "crtpi")
? video.shader.current_crtpi_preset ? g_video.shader.current_crtpi_preset
: video.shader.current_postfx_preset; : g_video.shader.current_postfx_preset;
return { return {
.target_position = video.render_info_position, .target_position = g_video.render_info_position,
.show_time = video.render_info_show_time, .show_time = g_video.render_info_show_time,
.shader_enabled = video.shader.enabled, .shader_enabled = g_video.shader.enabled,
.shader_name = video.shader.current_shader, .shader_name = g_video.shader.current_shader,
.shader_preset = active_preset, .shader_preset = active_preset,
.text_color = text_color, .text_color = text_color,
.shadow_color = shadow_color, .shadow_color = shadow_color,
@@ -603,12 +669,12 @@ namespace Options {
auto buildCrtPiParams() -> Rendering::CrtPiParams { auto buildCrtPiParams() -> Rendering::CrtPiParams {
// Si el shader està deshabilitat, retornem defaults (el dispatch anirà per // Si el shader està deshabilitat, retornem defaults (el dispatch anirà per
// PostFX passthrough, però sent nets per si algú consulta aquests params). // PostFX passthrough, però sent nets per si algú consulta aquests params).
if (!video.shader.enabled) { return {}; } if (!g_video.shader.enabled) { return {}; }
const auto IT = std::ranges::find_if(crtpi_presets, const auto IT = std::ranges::find_if(g_crtpi_presets,
[](const CrtPiPreset& preset) { return preset.name == video.shader.current_crtpi_preset; }); [](const CrtPiPreset& preset) { return preset.name == g_video.shader.current_crtpi_preset; });
if (IT == crtpi_presets.end()) { if (IT == g_crtpi_presets.end()) {
std::cout << "Options: preset CrtPi \"" << video.shader.current_crtpi_preset << "\" no trobat; aplicant defaults.\n"; std::cout << "Options: preset CrtPi \"" << g_video.shader.current_crtpi_preset << "\" no trobat; aplicant defaults.\n";
return {}; return {};
} }
return { return {
@@ -631,12 +697,12 @@ namespace Options {
auto buildPostFXParams() -> Rendering::PostFXParams { auto buildPostFXParams() -> Rendering::PostFXParams {
// Si el shader està deshabilitat, retornem params a zero → passthrough. // Si el shader està deshabilitat, retornem params a zero → passthrough.
if (!video.shader.enabled) { return {}; } if (!g_video.shader.enabled) { return {}; }
const auto IT = std::ranges::find_if(postfx_presets, const auto IT = std::ranges::find_if(g_postfx_presets,
[](const PostFXPreset& preset) { return preset.name == video.shader.current_postfx_preset; }); [](const PostFXPreset& preset) { return preset.name == g_video.shader.current_postfx_preset; });
if (IT == postfx_presets.end()) { if (IT == g_postfx_presets.end()) {
std::cout << "Options: preset PostFX \"" << video.shader.current_postfx_preset << "\" no trobat; aplicant passthrough.\n"; std::cout << "Options: preset PostFX \"" << g_video.shader.current_postfx_preset << "\" no trobat; aplicant passthrough.\n";
return {}; return {};
} }
return { return {
+32 -13
View File
@@ -13,6 +13,15 @@
// del config.yaml. Els subsistemes (Screen, Audio, ...) reben structs tipats // del config.yaml. Els subsistemes (Screen, Audio, ...) reben structs tipats
// a través dels build*Config(), de manera que no queden acoblats a aquest // a través dels build*Config(), de manera que no queden acoblats a aquest
// namespace ni al format concret del fitxer. // namespace ni al format concret del fitxer.
//
// L'estat mutable viu en un anonymous namespace dins d'`options.cpp`: els
// callers el llegeixen via getters const-ref (`window()`, `video()`, ...) i
// el muten via setters específics que persisteixen a config.yaml. Els
// setters NO notifiquen els subsistemes (Screen/Audio): el caller (típic.
// `core/input/global_inputs.cpp`) crida després el setter del subsistema.
// Aquesta asimetria preserva la direcció `Director → Screen/Audio` i evita
// que Options aprenga del layout dels subsistemes (decisió WONTFIX d'aquest
// refactor: veure docs/audit-game.md → COUP-03).
namespace Options { namespace Options {
struct Window { struct Window {
@@ -95,20 +104,30 @@ namespace Options {
Sound sound{}; Sound sound{};
}; };
// --- Estat global (inline vars per a que una sola definició viva a tot // --- Getters de l'estat (read-only) ---
// el programa sense necessitar un .cpp extra per a cada una) --- [[nodiscard]] auto window() -> const Window&;
inline std::string config_version{}; [[nodiscard]] auto video() -> const Video&;
inline std::string config_file_path{}; [[nodiscard]] auto audio() -> const Audio&;
inline std::string postfx_file_path{}; [[nodiscard]] auto locale() -> const std::string&;
inline std::string crtpi_file_path{}; [[nodiscard]] auto configVersion() -> const std::string&;
inline std::string locale{Defaults::I18n::LOCALE}; [[nodiscard]] auto postfxPresets() -> const std::vector<PostFXPreset>&;
inline Window window{}; [[nodiscard]] auto crtpiPresets() -> const std::vector<CrtPiPreset>&;
inline Video video{};
inline Audio audio{};
inline std::vector<PostFXPreset> postfx_presets{};
inline std::vector<CrtPiPreset> crtpi_presets{};
// --- API pública --- // --- Setters: muten + persisteixen a config.yaml ---
// No notifiquen els subsistemes; el caller ha de propagar el canvi al
// subsistema corresponent (p.ex. Screen::setWindowZoom) per separat.
void setWindowZoom(int value);
void setWindowFullscreen(bool value);
void setVideoVSync(bool value);
void setVideoScalingMode(Defaults::ScalingMode value);
void setVideoRenderInfoPosition(Defaults::RenderInfoPosition value);
void setVideoRenderInfoShowTime(bool value);
void setShaderEnabled(bool value);
void setActiveShaderName(const std::string& value); // "postfx" | "crtpi"
void setPostFXPreset(const std::string& value);
void setCrtPiPreset(const std::string& value);
// --- API pública: càrrega/desament de fitxers ---
void setConfigFile(const std::string& path); void setConfigFile(const std::string& path);
auto loadFromFile() -> bool; auto loadFromFile() -> bool;
auto saveToFile() -> bool; auto saveToFile() -> bool;
+1 -1
View File
@@ -98,7 +98,7 @@ GameScene::GameScene(Config config)
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);
const auto SPAWN_X = CORRIDOR_CENTER_X - (static_cast<float>(Player::sprite_w) / 2.0F); const auto SPAWN_X = CORRIDOR_CENTER_X - (static_cast<float>(Player::sprite_w) / 2.0F);
const 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>(ANIM_KEY, SPAWN_X, SPAWN_Y, CONTROLS_P1)); players_.emplace_back(std::make_unique<Player>(ANIM_KEY, SPAWN_X, SPAWN_Y, CONTROLS_P1, Map::makeInfo()));
// Connectem el Player al bus perquè emeti `on_tile_entered` i els // Connectem el Player al bus perquè emeti `on_tile_entered` i els
// sarcòfags rebin les notificacions de pas per tile. // sarcòfags rebin les notificacions de pas per tile.