From 682c27c07c55758e91239ba0bfb7c8a93e33eb39 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 16:37:07 +0200 Subject: [PATCH 01/18] refactor: eliminar ShapeLoader::resolvePath i BASE_PATH (codi mort) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cap caller invocava resolvePath fora de la seua pròpia definició. A més, BASE_PATH apuntava a "data/shapes/" mentre que load() ja construeix el path amb el prefix "shapes/" directament — el helper mai s'hauria activat encara que es cridara. Hallazgo #18 de CODE_REVIEW.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/graphics/shape_loader.cpp | 117 +++++++++++--------------- source/core/graphics/shape_loader.hpp | 14 ++- 2 files changed, 56 insertions(+), 75 deletions(-) diff --git a/source/core/graphics/shape_loader.cpp b/source/core/graphics/shape_loader.cpp index 486ea99..7f662a0 100644 --- a/source/core/graphics/shape_loader.cpp +++ b/source/core/graphics/shape_loader.cpp @@ -9,77 +9,62 @@ namespace Graphics { -// Inicialización de variables estàtiques -std::unordered_map> ShapeLoader::cache; + // Inicialización de variables estàtiques + std::unordered_map> ShapeLoader::cache; -auto ShapeLoader::load(const std::string& filename) -> std::shared_ptr { - // Check cache first - auto it = cache.find(filename); - if (it != cache.end()) { - std::cout << "[ShapeLoader] Cache hit: " << filename << '\n'; - return it->second; // Cache hit + auto ShapeLoader::load(const std::string& filename) -> std::shared_ptr { + // Check cache first + auto it = cache.find(filename); + if (it != cache.end()) { + std::cout << "[ShapeLoader] Cache hit: " << filename << '\n'; + return it->second; // Cache hit + } + + // Normalize path: "ship.shp" → "shapes/ship.shp" + // "logo/letra_j.shp" → "shapes/logo/letra_j.shp" + std::string normalized = filename; + if (!normalized.starts_with("shapes/")) { + // Doesn't start with "shapes/", so add it + normalized = "shapes/" + normalized; + } + + // Load from resource system + std::vector data = Resource::Helper::loadFile(normalized); + if (data.empty()) { + std::cerr << "[ShapeLoader] Error: no s'ha pogut load " << normalized + << '\n'; + return nullptr; + } + + // Convert bytes to string and parse + std::string file_content(data.begin(), data.end()); + auto shape = std::make_shared(); + if (!shape->parseFile(file_content)) { + std::cerr << "[ShapeLoader] Error: no s'ha pogut parsejar " << normalized + << '\n'; + return nullptr; + } + + // Verify shape is valid + if (!shape->isValid()) { + std::cerr << "[ShapeLoader] Error: shape invàlida " << normalized << '\n'; + return nullptr; + } + + // Cache and return + std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->getName() + << ", " << shape->getNumPrimitives() << " primitives)" << '\n'; + + cache[filename] = shape; + return shape; } - // Normalize path: "ship.shp" → "shapes/ship.shp" - // "logo/letra_j.shp" → "shapes/logo/letra_j.shp" - std::string normalized = filename; - if (!normalized.starts_with("shapes/")) { - // Doesn't start with "shapes/", so add it - normalized = "shapes/" + normalized; - } - - // Load from resource system - std::vector data = Resource::Helper::loadFile(normalized); - if (data.empty()) { - std::cerr << "[ShapeLoader] Error: no s'ha pogut load " << normalized + void ShapeLoader::clearCache() { + std::cout << "[ShapeLoader] Netejant caché (" << cache.size() << " formes)" << '\n'; - return nullptr; + cache.clear(); } - // Convert bytes to string and parse - std::string file_content(data.begin(), data.end()); - auto shape = std::make_shared(); - if (!shape->parseFile(file_content)) { - std::cerr << "[ShapeLoader] Error: no s'ha pogut parsejar " << normalized - << '\n'; - return nullptr; - } - - // Verify shape is valid - if (!shape->isValid()) { - std::cerr << "[ShapeLoader] Error: shape invàlida " << normalized << '\n'; - return nullptr; - } - - // Cache and return - std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->getName() - << ", " << shape->getNumPrimitives() << " primitives)" << '\n'; - - cache[filename] = shape; - return shape; -} - -void ShapeLoader::clearCache() { - std::cout << "[ShapeLoader] Netejant caché (" << cache.size() << " formes)" - << '\n'; - cache.clear(); -} - -auto ShapeLoader::getCacheSize() -> size_t { return cache.size(); } - -auto ShapeLoader::resolvePath(const std::string& filename) -> std::string { - // Si es un path absolut (comença con '/'), usar-lo directament - if (!filename.empty() && filename[0] == '/') { - return filename; - } - - // Si ya conté el prefix base_path, usar-lo directament - if (filename.starts_with(BASE_PATH)) { - return filename; - } - - // Altrament, añadir base_path (ara suporta subdirectoris) - return std::string(BASE_PATH) + filename; -} + auto ShapeLoader::getCacheSize() -> size_t { return cache.size(); } } // namespace Graphics diff --git a/source/core/graphics/shape_loader.hpp b/source/core/graphics/shape_loader.hpp index 330f09a..a8a228b 100644 --- a/source/core/graphics/shape_loader.hpp +++ b/source/core/graphics/shape_loader.hpp @@ -11,9 +11,9 @@ namespace Graphics { -// Carregador estàtic de formes con caché -class ShapeLoader { - public: + // Carregador estàtic de formes con caché + class ShapeLoader { + public: // No instanciable (tot estàtic) ShapeLoader() = delete; @@ -28,12 +28,8 @@ class ShapeLoader { // Estadístiques (debug) static auto getCacheSize() -> size_t; - private: + private: static std::unordered_map> cache; - static constexpr const char* BASE_PATH = "data/shapes/"; - - // Helpers privats - static auto resolvePath(const std::string& filename) -> std::string; -}; + }; } // namespace Graphics From 707fd29b97a14cfc441a8533251780b09b6ae4bc Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 16:50:03 +0200 Subject: [PATCH 02/18] =?UTF-8?q?refactor:=20eliminar=20Rotation3D=20i=20e?= =?UTF-8?q?l=20seu=20cam=C3=AD=20de=20codi=20(codi=20mort)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'struct Rotation3D, la funció apply3dRotation i el paràmetre opcional rotation_3d de renderShape mai s'activaven en cap caller: - Ship, Enemy i Bullet passaven explícitament nullptr. - Title scene, logo scene, starfield, vector_text i ship_animator usaven el default nullptr (set els 7 callers). CLAUDE.md descriu un sistema 3D del title screen que ja no està viu — el comentari en ship_animator.cpp aclareix que la perspectiva s'ha bakeat dins de la shape, així que la rotació dinàmica era residu històric. Esborrats: struct Rotation3D + ctors + hasRotation(), apply3dRotation(), la branca rotation_3d a transformPoint() i el seu paràmetre, el paràmetre rotation_3d de renderShape, i els arguments nullptr als 3 callers d'entitats. Hallazgo #16 de CODE_REVIEW.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/rendering/shape_renderer.cpp | 128 ++++++++--------------- source/core/rendering/shape_renderer.hpp | 56 +++------- source/game/entities/bullet.cpp | 12 +-- source/game/entities/enemy.cpp | 88 ++++++++-------- source/game/entities/ship.cpp | 16 ++- 5 files changed, 117 insertions(+), 183 deletions(-) diff --git a/source/core/rendering/shape_renderer.cpp b/source/core/rendering/shape_renderer.cpp index 7ec2d7e..e18ff64 100644 --- a/source/core/rendering/shape_renderer.cpp +++ b/source/core/rendering/shape_renderer.cpp @@ -9,102 +9,58 @@ namespace Rendering { -// Helper: aplicar rotación 3D a un point 2D (assumeix Z=0) -static auto apply3dRotation(float x, float y, const Rotation3D& rot) -> Vec2 { - float z = 0.0F; // Todos los points 2D comencen a Z=0 + // Helper: transformar un point con rotación, scale i traslación + static auto transformPoint(const Vec2& point, const Vec2& shape_centre, const Vec2& position, float angle, float scale) -> Vec2 { + // 1. Centrar el point respecte al centro de la shape + float centered_x = point.x - shape_centre.x; + float centered_y = point.y - shape_centre.y; - // Pitch (rotación eix X): cabeceo arriba/baix - float cos_pitch = std::cos(rot.pitch); - float sin_pitch = std::sin(rot.pitch); - float y1 = (y * cos_pitch) - (z * sin_pitch); - float z1 = (y * sin_pitch) + (z * cos_pitch); + // 2. Aplicar scale al point + float scaled_x = centered_x * scale; + float scaled_y = centered_y * scale; - // Yaw (rotación eix Y): guiñada izquierda/derecha - float cos_yaw = std::cos(rot.yaw); - float sin_yaw = std::sin(rot.yaw); - float x2 = (x * cos_yaw) + (z1 * sin_yaw); - float z2 = (-x * sin_yaw) + (z1 * cos_yaw); + // 3. Aplicar rotación 2D (Z-axis) + float cos_a = std::cos(angle); + float sin_a = std::sin(angle); - // Roll (rotación eix Z): alabeo lateral - float cos_roll = std::cos(rot.roll); - float sin_roll = std::sin(rot.roll); - float x3 = (x2 * cos_roll) - (y1 * sin_roll); - float y3 = (x2 * sin_roll) + (y1 * cos_roll); + float rotated_x = (scaled_x * cos_a) - (scaled_y * sin_a); + float rotated_y = (scaled_x * sin_a) + (scaled_y * cos_a); - // Proyecció perspectiva (Z-divide simple) - // Naves quieren hacia el point de fuga (320, 240) a "infinit" (Z → +∞) - // Z més grande = més lluny = més pequeño a pantalla - constexpr float PERSPECTIVE_FACTOR = 500.0F; - float scale_factor = PERSPECTIVE_FACTOR / (PERSPECTIVE_FACTOR + z2); - - return {.x = x3 * scale_factor, .y = y3 * scale_factor}; -} - -// Helper: transformar un point con rotación, scale i traslación -static auto transformPoint(const Vec2& point, const Vec2& shape_centre, const Vec2& position, float angle, float scale, const Rotation3D* rotation_3d) -> Vec2 { - // 1. Centrar el point respecte al centro de la shape - float centered_x = point.x - shape_centre.x; - float centered_y = point.y - shape_centre.y; - - // 2. Aplicar rotación 3D (si es proporciona) - if ((rotation_3d != nullptr) && rotation_3d->hasRotation()) { - Vec2 rotated_3d = apply3dRotation(centered_x, centered_y, *rotation_3d); - centered_x = rotated_3d.x; - centered_y = rotated_3d.y; + // 4. Aplicar traslación a posición mundial + return {.x = rotated_x + position.x, .y = rotated_y + position.y}; } - // 3. Aplicar scale al point (después de rotación 3D) - float scaled_x = centered_x * scale; - float scaled_y = centered_y * scale; + void renderShape(Rendering::Renderer* renderer, + const std::shared_ptr& shape, + const Vec2& position, + float angle, + float scale, + float progress, + float brightness, + SDL_Color color) { + if (!shape || !shape->isValid()) { + return; + } + if (progress < 1.0F) { + return; + } - // 4. Aplicar rotación 2D (Z-axis, tradicional) - // IMPORTANT: En el sistema original, angle=0 apunta AMUNT (no derecha) - // Per això usem (angle - PI/2) per compensar - // Pero aquí angle ya ve en el sistema correcte del juego - float cos_a = std::cos(angle); - float sin_a = std::sin(angle); + const Vec2& shape_centre = shape->getCenter(); - float rotated_x = (scaled_x * cos_a) - (scaled_y * sin_a); - float rotated_y = (scaled_x * sin_a) + (scaled_y * cos_a); - - // 5. Aplicar traslación a posición mundial - return {.x = rotated_x + position.x, .y = rotated_y + position.y}; -} - -void renderShape(Rendering::Renderer* renderer, - const std::shared_ptr& shape, - const Vec2& position, - float angle, - float scale, - float progress, - float brightness, - const Rotation3D* rotation_3d, - SDL_Color color) { - if (!shape || !shape->isValid()) { - return; - } - if (progress < 1.0F) { - return; - } - - const Vec2& shape_centre = shape->getCenter(); - - for (const auto& primitive : shape->getPrimitives()) { - if (primitive.type == Graphics::PrimitiveType::POLYLINE) { - // POLYLINE: conectar puntos consecutivos. - for (size_t i = 0; i < primitive.points.size() - 1; i++) { - const Vec2 P1 = transformPoint(primitive.points[i], shape_centre, position, angle, scale, rotation_3d); - const Vec2 P2 = transformPoint(primitive.points[i + 1], shape_centre, position, angle, scale, rotation_3d); - linea(renderer, static_cast(P1.x), static_cast(P1.y), - static_cast(P2.x), static_cast(P2.y), brightness, 0.0F, color); + for (const auto& primitive : shape->getPrimitives()) { + if (primitive.type == Graphics::PrimitiveType::POLYLINE) { + // POLYLINE: conectar puntos consecutivos. + for (size_t i = 0; i < primitive.points.size() - 1; i++) { + const Vec2 P1 = transformPoint(primitive.points[i], shape_centre, position, angle, scale); + const Vec2 P2 = transformPoint(primitive.points[i + 1], shape_centre, position, angle, scale); + linea(renderer, static_cast(P1.x), static_cast(P1.y), static_cast(P2.x), static_cast(P2.y), brightness, 0.0F, color); + } + } else if (primitive.points.size() >= 2) { // LINE + const Vec2 P1 = transformPoint(primitive.points[0], shape_centre, position, angle, scale); + const Vec2 P2 = transformPoint(primitive.points[1], shape_centre, position, angle, scale); + linea(renderer, static_cast(P1.x), static_cast(P1.y), static_cast(P2.x), static_cast(P2.y), brightness, 0.0F, color); } - } else if (primitive.points.size() >= 2) { // LINE - const Vec2 P1 = transformPoint(primitive.points[0], shape_centre, position, angle, scale, rotation_3d); - const Vec2 P2 = transformPoint(primitive.points[1], shape_centre, position, angle, scale, rotation_3d); - linea(renderer, static_cast(P1.x), static_cast(P1.y), - static_cast(P2.x), static_cast(P2.y), brightness, 0.0F, color); } } -} } // namespace Rendering diff --git a/source/core/rendering/shape_renderer.hpp b/source/core/rendering/shape_renderer.hpp index 88c156b..43eed6b 100644 --- a/source/core/rendering/shape_renderer.hpp +++ b/source/core/rendering/shape_renderer.hpp @@ -3,53 +3,31 @@ #pragma once -#include "core/rendering/render_context.hpp" - #include #include #include "core/graphics/shape.hpp" +#include "core/rendering/render_context.hpp" #include "core/types.hpp" namespace Rendering { -// Estructura per rotacions 3D (pitch, yaw, roll) -struct Rotation3D { - float pitch; // Rotación eix X (cabeceo arriba/baix) - float yaw; // Rotación eix Y (guiñada izquierda/derecha) - float roll; // Rotación eix Z (alabeo lateral) - - Rotation3D() - : pitch(0.0F), - yaw(0.0F), - roll(0.0F) {} - Rotation3D(float p, float y, float r) - : pitch(p), - yaw(y), - roll(r) {} - - [[nodiscard]] auto hasRotation() const -> bool { - return pitch != 0.0F || yaw != 0.0F || roll != 0.0F; - } -}; - -// Renderizar shape con transformacions -// - renderer: SDL renderer -// - shape: shape vectorial a draw -// - position: posición del centro en coordenades mundials -// - angle: rotación en radians (0 = amunt, sentit horari) -// - scale: factor de scale (1.0 = mida original) -// - progress: progrés de l'animación (0.0-1.0, default 1.0 = tot visible) -// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness) -void renderShape(Rendering::Renderer* renderer, - const std::shared_ptr& shape, - const Vec2& position, - float angle, - float scale = 1.0F, - float progress = 1.0F, - float brightness = 1.0F, - const Rotation3D* rotation_3d = nullptr, - SDL_Color color = {0, 0, 0, 0}); // alpha==0 → usa global oscilador + // Renderizar shape con transformacions + // - renderer: SDL renderer + // - shape: shape vectorial a draw + // - position: posición del centro en coordenades mundials + // - angle: rotación en radians (0 = amunt, sentit horari) + // - scale: factor de scale (1.0 = mida original) + // - progress: progrés de l'animación (0.0-1.0, default 1.0 = tot visible) + // - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness) + void renderShape(Rendering::Renderer* renderer, + const std::shared_ptr& shape, + const Vec2& position, + float angle, + float scale = 1.0F, + float progress = 1.0F, + float brightness = 1.0F, + SDL_Color color = {0, 0, 0, 0}); // alpha==0 → usa global oscilador } // namespace Rendering diff --git a/source/game/entities/bullet.cpp b/source/game/entities/bullet.cpp index 6a838cb..6434263 100644 --- a/source/game/entities/bullet.cpp +++ b/source/game/entities/bullet.cpp @@ -17,14 +17,13 @@ #include "game/constants.hpp" namespace { -// Velocidad escalar de las balas (px/s). Conserva el feel del Pascal original -// (7 px/frame × 20 FPS = 140 px/s). -constexpr float BULLET_SPEED = 140.0F; + // Velocidad escalar de las balas (px/s). Conserva el feel del Pascal original + // (7 px/frame × 20 FPS = 140 px/s). + constexpr float BULLET_SPEED = 140.0F; } // namespace Bullet::Bullet(Rendering::Renderer* renderer) - : Entity(renderer) - { + : Entity(renderer) { // Brightness específico para balas brightness_ = Defaults::Brightness::BALA; @@ -134,7 +133,6 @@ void Bullet::desactivar() { void Bullet::draw() const { if (is_active_ && shape_) { // Les bales roten segons l'angle de trayectòria (estático tras disparo) - Rendering::renderShape(renderer_, shape_, center_, angle_, 1.0F, 1.0F, brightness_, - /*rotation_3d=*/nullptr, Defaults::Palette::BULLET); + Rendering::renderShape(renderer_, shape_, center_, angle_, 1.0F, 1.0F, brightness_, Defaults::Palette::BULLET); } } diff --git a/source/game/entities/enemy.cpp b/source/game/entities/enemy.cpp index 388afe2..c23a846 100644 --- a/source/game/entities/enemy.cpp +++ b/source/game/entities/enemy.cpp @@ -17,41 +17,40 @@ namespace { -// Velocidad inicial vectorial a partir de un ángulo (rad). -// angle=0 apunta hacia arriba (eje Y negativo SDL), como el resto del juego. -auto angleToDirection(float angle) -> Vec2 { - return Vec2{ - .x = std::cos(angle - (Constants::PI / 2.0F)), - .y = std::sin(angle - (Constants::PI / 2.0F)), - }; -} - -// Recupera el "ángulo equivalente" de un body en movimiento (para zigzag). -// Si está parado, devuelve 0. -auto velocityToAngle(const Vec2& velocity) -> float { - if (velocity.lengthSquared() < 0.0001F) { - return 0.0F; + // Velocidad inicial vectorial a partir de un ángulo (rad). + // angle=0 apunta hacia arriba (eje Y negativo SDL), como el resto del juego. + auto angleToDirection(float angle) -> Vec2 { + return Vec2{ + .x = std::cos(angle - (Constants::PI / 2.0F)), + .y = std::sin(angle - (Constants::PI / 2.0F)), + }; + } + + // Recupera el "ángulo equivalente" de un body en movimiento (para zigzag). + // Si está parado, devuelve 0. + auto velocityToAngle(const Vec2& velocity) -> float { + if (velocity.lengthSquared() < 0.0001F) { + return 0.0F; + } + // El movimiento (vx, vy) corresponde a angle - PI/2; invertimos. + return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F); } - // El movimiento (vx, vy) corresponde a angle - PI/2; invertimos. - return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F); -} } // namespace Enemy::Enemy(Rendering::Renderer* renderer) : Entity(renderer), - - tracking_strength_(0.5F) - { + + tracking_strength_(0.5F) { brightness_ = Defaults::Brightness::ENEMIC; // Configuración del cuerpo físico — defaults para enemy genérico. // init() ajusta velocidad y masa según el tipo (Pentagon/Quadrat/Molinillo). - body_.setMass(5.0F); // Más liviano que la nave (10.0) - body_.radius = 0.0F; // 0 hasta spawn (no colisiona inactivo) - body_.restitution = 1.0F; // Rebote elástico perfecto contra paredes - body_.linear_damping = 0.0F; // Sin fricción: mantienen velocidad - body_.angular_damping = 0.0F; // Idem + body_.setMass(5.0F); // Más liviano que la nave (10.0) + body_.radius = 0.0F; // 0 hasta spawn (no colisiona inactivo) + body_.restitution = 1.0F; // Rebote elástico perfecto contra paredes + body_.linear_damping = 0.0F; // Sin fricción: mantienen velocidad + body_.angular_damping = 0.0F; // Idem } void Enemy::init(EnemyType type, const Vec2* ship_pos) { @@ -225,12 +224,17 @@ void Enemy::draw() const { const float SCALE = computeCurrentScale(); SDL_Color color{}; switch (type_) { - case EnemyType::PENTAGON: color = Defaults::Palette::PENTAGON; break; - case EnemyType::QUADRAT: color = Defaults::Palette::QUADRAT; break; - case EnemyType::MOLINILLO: color = Defaults::Palette::MOLINILLO; break; + case EnemyType::PENTAGON: + color = Defaults::Palette::PENTAGON; + break; + case EnemyType::QUADRAT: + color = Defaults::Palette::QUADRAT; + break; + case EnemyType::MOLINILLO: + color = Defaults::Palette::MOLINILLO; + break; } - Rendering::renderShape(renderer_, shape_, center_, rotacio_, SCALE, 1.0F, brightness_, - /*rotation_3d=*/nullptr, color); + Rendering::renderShape(renderer_, shape_, center_, rotacio_, SCALE, 1.0F, brightness_, color); } void Enemy::destruir() { @@ -269,7 +273,7 @@ void Enemy::behaviorPentagon(float delta_time) { if (RAND_VAL < ZIGZAG_PROB_PER_SECOND * delta_time) { const float CURRENT_ANGLE = velocityToAngle(body_.velocity); const float DELTA = (static_cast(std::rand()) / RAND_MAX) * - Defaults::Enemies::Pentagon::CANVI_ANGLE_MAX; + Defaults::Enemies::Pentagon::CANVI_ANGLE_MAX; const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA); const float SPEED = body_.velocity.length(); setVelocityFromAngle(NEW_ANGLE, SPEED); @@ -294,7 +298,7 @@ void Enemy::behaviorQuadrat(float delta_time) { // Mezcla LERP: velocidad actual con la deseada según tracking_strength_. body_.velocity = (body_.velocity * (1.0F - tracking_strength_)) + - (DESIRED_VEL * tracking_strength_); + (DESIRED_VEL * tracking_strength_); // Renormalizar a la velocidad escalar original const float NEW_SPEED = body_.velocity.length(); @@ -342,19 +346,19 @@ void Enemy::updatePalpitation(float delta_time) { animacio_.palpitacio_fase = 0.0F; const float FREQ_RANGE = Defaults::Enemies::Animation::PALPITACIO_FREQ_MAX - - Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN; + Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN; animacio_.palpitacio_frequencia = Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN + - ((static_cast(std::rand()) / RAND_MAX) * FREQ_RANGE); + ((static_cast(std::rand()) / RAND_MAX) * FREQ_RANGE); const float AMP_RANGE = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MAX - - Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN; + Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN; animacio_.palpitacio_amplitud = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN + - ((static_cast(std::rand()) / RAND_MAX) * AMP_RANGE); + ((static_cast(std::rand()) / RAND_MAX) * AMP_RANGE); const float DUR_RANGE = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MAX - - Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN; + Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN; animacio_.palpitacio_temps_restant = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN + - ((static_cast(std::rand()) / RAND_MAX) * DUR_RANGE); + ((static_cast(std::rand()) / RAND_MAX) * DUR_RANGE); } } } @@ -380,15 +384,15 @@ void Enemy::updateRotationAcceleration(float delta_time) { animacio_.drotacio_t = 0.0F; const float MULT_RANGE = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MAX - - Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN; + Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN; const float MULTIPLIER = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN + - ((static_cast(std::rand()) / RAND_MAX) * MULT_RANGE); + ((static_cast(std::rand()) / RAND_MAX) * MULT_RANGE); animacio_.drotacio_objetivo = animacio_.drotacio_base * MULTIPLIER; const float DUR_RANGE = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MAX - - Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN; + Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN; animacio_.drotacio_duracio = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN + - ((static_cast(std::rand()) / RAND_MAX) * DUR_RANGE); + ((static_cast(std::rand()) / RAND_MAX) * DUR_RANGE); } } } diff --git a/source/game/entities/ship.cpp b/source/game/entities/ship.cpp index e954759..f4b129e 100644 --- a/source/game/entities/ship.cpp +++ b/source/game/entities/ship.cpp @@ -20,17 +20,16 @@ #include "game/constants.hpp" Ship::Ship(Rendering::Renderer* renderer, const char* shape_file) - : Entity(renderer) - { + : Entity(renderer) { // Brightness específico para naves brightness_ = Defaults::Brightness::NAU; // Configuración del cuerpo físico - body_.setMass(10.0F); // Masa de referencia para choques - body_.radius = Defaults::Entities::SHIP_RADIUS; // Radio de colisión - body_.restitution = 0.6F; // Rebote moderado contra paredes - body_.linear_damping = 1.5F; // Fricción exponencial (s⁻¹) - body_.angular_damping = 0.0F; // La rotación es 100% por input, no inercial + body_.setMass(10.0F); // Masa de referencia para choques + body_.radius = Defaults::Entities::SHIP_RADIUS; // Radio de colisión + body_.restitution = 0.6F; // Rebote moderado contra paredes + body_.linear_damping = 1.5F; // Fricción exponencial (s⁻¹) + body_.angular_damping = 0.0F; // La rotación es 100% por input, no inercial // Cargar shape compartida desde archivo shape_ = Graphics::ShapeLoader::load(shape_file); @@ -158,6 +157,5 @@ void Ship::draw() const { const float VISUAL_PUSH = SPEED / 33.33F; const float SCALE = 1.0F + (VISUAL_PUSH / 12.0F); - Rendering::renderShape(renderer_, shape_, center_, angle_, SCALE, 1.0F, brightness_, - /*rotation_3d=*/nullptr, Defaults::Palette::SHIP); + Rendering::renderShape(renderer_, shape_, center_, angle_, SCALE, 1.0F, brightness_, Defaults::Palette::SHIP); } From 88bb6afab1b14221dfc51da2d9f51c04e421e5eb Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 17:10:59 +0200 Subject: [PATCH 03/18] refactor: convertir loops del ctor de GameScene a std::ranges::fill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cppcheck (style: useStlAlgorithm) marcava els raw loops d'assignació sobre std::array com a candidats clars per std::fill/std::generate. Amb fill només es construeix la temporal una vegada i es copia a cada element, en lloc de construir N temporals. Preexistent, no introduït per cap commit recent, però el hook ho demanava en tocar el fitxer. Es resolt el root cause en lloc de suprimir. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/game/scenes/game_scene.cpp | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 748f009..d38f4b8 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -28,8 +28,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) context_(context), debris_manager_(sdl.getRenderer()), floating_score_manager_(sdl.getRenderer()), - text_(sdl.getRenderer()) - { + text_(sdl.getRenderer()) { // Recuperar configuración de match des del context match_config_ = context_.getMatchConfig(); @@ -49,14 +48,10 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) ships_[1] = Ship(sdl.getRenderer(), "ship2.shp"); // Jugador 2: interceptor con ales // Inicialitzar balas con renderer - for (auto& bullet : bullets_) { - bullet = Bullet(sdl.getRenderer()); - } + std::ranges::fill(bullets_, Bullet(sdl.getRenderer())); // Inicialitzar enemigos con renderer - for (auto& enemy : enemies_) { - enemy = Enemy(sdl.getRenderer()); - } + std::ranges::fill(enemies_, Enemy(sdl.getRenderer())); // El resto del estado del juego (física, stages, naves, vidas, puntuación) // se inicializa en init(), que se llama al final del constructor para que @@ -138,7 +133,7 @@ void GameScene::init() { // Jugador inactiu: marcar como a mort permanent ships_[i].markHit(); hit_timer_per_player_[i] = 999.0F; // Valor sentinella (permanent inactiu) - lives_per_player_[i] = 0; // Sin vides + lives_per_player_[i] = 0; // Sin vides std::cout << "[GameScene] Jugador " << (i + 1) << " inactiu\n"; } } @@ -235,7 +230,7 @@ void GameScene::stepMidGameJoin() { auto* input = Input::get(); for (uint8_t pid = 0; pid < 2; pid++) { const bool ACTIU = (pid == 0) ? match_config_.jugador1_actiu - : match_config_.jugador2_actiu; + : match_config_.jugador2_actiu; const bool MUERTO_SIN_VIDAS = hit_timer_per_player_[pid] == 999.0F; if (ACTIU && !MUERTO_SIN_VIDAS) { continue; // jugador ya está jugando From e3b0958d105ff6d7ce20f921ebcdce55bab7e26b Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 17:11:47 +0200 Subject: [PATCH 04/18] =?UTF-8?q?refactor:=20eliminar=20doble=20inicialitz?= =?UTF-8?q?aci=C3=B3=20d'enemies=5F=20a=20GameScene?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El ctor de GameScene ja construïa cada Enemy amb el renderer, però init() (cridat des del propi ctor) tornava a assignar enemies_[i] = Enemy(sdl_.getRenderer()) sobre la mateixa instància. Treball perdut, a més d'incoherent amb ships_ i bullets_, que no es reassignen a init() sinó que es limiten a init()/addBody. Eliminem la reassignació i deixem només setShipPosition i addBody, alineat amb la resta d'entitats. Hallazgo #34 de CODE_REVIEW.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/game/scenes/game_scene.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index d38f4b8..0ba0bf9 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -138,11 +138,12 @@ void GameScene::init() { } } - // [MODIFIED] Initialize enemies as inactive (stage system will spawn them). + // Initialize enemies as inactive (stage system will spawn them). // Registramos el body al world incluso inactivo: con radius=0 no colisiona // ni se mueve, y al init() del stage system se activa sin re-registrar. + // La construcció dels Enemy ja s'ha fet al ctor de GameScene; ací només + // configurem la referència a la nau i registrem el body. for (auto& enemy : enemies_) { - enemy = Enemy(sdl_.getRenderer()); enemy.setShipPosition(&ships_[0].getCenter()); // Set ship reference (P1 for now) physics_world_.addBody(&enemy.getBody()); // DON'T call enemy.init() here - stage system handles spawning From e1d6cd1bb9c4357c9986ad70a022223c7198cb8e Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 17:25:02 +0200 Subject: [PATCH 05/18] refactor: fusionar GameScene::init() al ctor (coherent amb Scene) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'interfície Scene només declara handleEvent/update/draw/isFinished. GameScene::init() era un mètode públic addicional que ningú (ni el Director) cridava externament: només el propi ctor el cridava al final. El comentari del header ("llamado por Director tras crear la escena") era fals: el Director mai l'invoca. TitleScene i LogoScene ja inicialitzen tot al ctor sense exposar init(). Aquesta diferència trencava l'expectativa del lifecycle. Movem tot el cos de init() al ctor i esborrem la declaració i la definició. Aprofitem per: - Eliminar el guard "if (!stage_config_)" que pressuposava re-init, cas que mai s'arribava a donar. - Treure el comentari DEPRECATED sobre spawn_position_ (residu). Hallazgo #1 de CODE_REVIEW.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/game/scenes/game_scene.cpp | 57 ++++------ source/game/scenes/game_scene.hpp | 171 +++++++++++++++--------------- 2 files changed, 102 insertions(+), 126 deletions(-) diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 0ba0bf9..1a83e4a 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -53,46 +53,26 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) // Inicialitzar enemigos con renderer std::ranges::fill(enemies_, Enemy(sdl.getRenderer())); - // El resto del estado del juego (física, stages, naves, vidas, puntuación) - // se inicializa en init(), que se llama al final del constructor para que - // la escena esté lista en cuanto el Director la haya construido. - init(); -} - -auto GameScene::isFinished() const -> bool { - return context_.nextScene() != SceneType::GAME; -} - -void GameScene::handleEvent(const SDL_Event& event) { - // GameScene no procesa eventos puntuales SDL: la lógica de input se - // resuelve en update() consultando Input::checkAction. - (void)event; -} - -void GameScene::init() { // Inicialitzar generador de números aleatoris // Basat en el codi Pascal original: line 376 std::srand(static_cast(std::time(nullptr))); // Configurar el mundo físico con los límites de la zona de juego. - // Las entidades se registrarán cada una al inicializarse (Fase 6c-e). physics_world_.clear(); physics_world_.setBounds(Defaults::Zones::PLAYAREA); - // [NEW] Load stage configuration (only once) + // Load stage configuration + stage_config_ = StageSystem::StageLoader::load("data/stages/stages.yaml"); if (!stage_config_) { - stage_config_ = StageSystem::StageLoader::load("data/stages/stages.yaml"); - if (!stage_config_) { - std::cerr << "[GameScene] Error: no s'ha pogut load stages.yaml" << '\n'; - // Continue without stage system (will crash, but helps debugging) - } + std::cerr << "[GameScene] Error: no s'ha pogut load stages.yaml" << '\n'; + // Continue without stage system (will crash, but helps debugging) } - // [NEW] Initialize stage manager + // Initialize stage manager stage_manager_ = std::make_unique(stage_config_.get()); stage_manager_->init(); - // [NEW] Set ship position reference for safe spawn (P1 for now, TODO: dual tracking) + // Set ship position reference for safe spawn (P1 for now, TODO: dual tracking) stage_manager_->getSpawnController().setShipPosition(&ships_[0].getCenter()); // Inicialitzar timers de muerte per player @@ -113,11 +93,6 @@ void GameScene::init() { score_per_player_[1] = 0; floating_score_manager_.reset(); - // DEPRECATED: spawn_position_ ya no s'usa, es calcula dinàmicament con getSpawnPoint(player_id) - // const SDL_FRect& zona = Defaults::Zones::PLAYAREA; - // spawn_position_.x = zona.x + zona.w * 0.5f; - // spawn_position_.y = zona.y + zona.h * Defaults::Game::INIT_HUD_SHIP_START_Y_RATIO; - // Inicialitzar naves segons configuración (solo jugadors active) for (uint8_t i = 0; i < 2; i++) { bool jugador_actiu = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; @@ -126,7 +101,7 @@ void GameScene::init() { // Jugador active: init normalment Vec2 spawn_pos = getSpawnPoint(i); ships_[i].init(&spawn_pos, false); // No invulnerability at start - // Registrar el cuerpo físico de la nave en el mundo (Fase 6c) + // Registrar el cuerpo físico de la nave en el mundo physics_world_.addBody(&ships_[i].getBody()); std::cout << "[GameScene] Jugador " << (i + 1) << " inicialitzat\n"; } else { @@ -141,15 +116,13 @@ void GameScene::init() { // Initialize enemies as inactive (stage system will spawn them). // Registramos el body al world incluso inactivo: con radius=0 no colisiona // ni se mueve, y al init() del stage system se activa sin re-registrar. - // La construcció dels Enemy ja s'ha fet al ctor de GameScene; ací només - // configurem la referència a la nau i registrem el body. for (auto& enemy : enemies_) { enemy.setShipPosition(&ships_[0].getCenter()); // Set ship reference (P1 for now) physics_world_.addBody(&enemy.getBody()); // DON'T call enemy.init() here - stage system handles spawning } - // Inicialitzar balas (now 6 instead of 3). + // Inicialitzar balas. // Se registran en el physics_world para integración cinemática. // Como su body_.radius=0, no colisionan físicamente con nadie (las // colisiones de gameplay se gestionan en detectar_col·lisions_*). @@ -158,14 +131,20 @@ void GameScene::init() { physics_world_.addBody(&bullet.getBody()); } - // [ELIMINAT] Iniciar música de juego (ara es gestiona en stage_manager) - // La música s'inicia cuando es transiciona de INIT_HUD a LEVEL_START - // Audio::get()->playMusic("game.ogg"); - // Reset flag de sons de animación init_hud_rect_sound_played_ = false; } +auto GameScene::isFinished() const -> bool { + return context_.nextScene() != SceneType::GAME; +} + +void GameScene::handleEvent(const SDL_Event& event) { + // GameScene no procesa eventos puntuales SDL: la lógica de input se + // resuelve en update() consultando Input::checkAction. + (void)event; +} + void GameScene::update(float delta_time) { // Orquestador delgado: cada paso vive en su propia función para // mantener update() legible y reducir complejidad cognitiva. diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index 4223b1c..32d9e2a 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -11,9 +11,9 @@ #include "core/graphics/vector_text.hpp" #include "core/physics/physics_world.hpp" #include "core/rendering/sdl_manager.hpp" +#include "core/system/game_config.hpp" #include "core/system/scene.hpp" #include "core/system/scene_context.hpp" -#include "core/system/game_config.hpp" #include "core/types.hpp" #include "game/constants.hpp" #include "game/effects/debris_manager.hpp" @@ -33,105 +33,102 @@ enum class GameOverState : uint8_t { // Clase principal del juego (escena) class GameScene final : public Scene { - public: - explicit GameScene(SDLManager& sdl, SceneManager::SceneContext& context); - ~GameScene() override = default; + public: + explicit GameScene(SDLManager& sdl, SceneManager::SceneContext& context); + ~GameScene() override = default; - // Scene interface - void handleEvent(const SDL_Event& event) override; - void update(float delta_time) override; - void draw() override; - [[nodiscard]] auto isFinished() const -> bool override; + // Scene interface + void handleEvent(const SDL_Event& event) override; + void update(float delta_time) override; + void draw() override; + [[nodiscard]] auto isFinished() const -> bool override; - // Inicialización del estado del juego (llamado por Director tras crear la escena). - void init(); + private: + SDLManager& sdl_; + SceneManager::SceneContext& context_; + GameConfig::MatchConfig match_config_; // Configuración de jugadors active - private: - SDLManager& sdl_; - SceneManager::SceneContext& context_; - GameConfig::MatchConfig match_config_; // Configuración de jugadors active + // Mundo físico (Fase 5) — integración cinemática + colisiones + Physics::PhysicsWorld physics_world_; - // Mundo físico (Fase 5) — integración cinemática + colisiones - Physics::PhysicsWorld physics_world_; + // Efectes visuals + Effects::DebrisManager debris_manager_; + Effects::FloatingScoreManager floating_score_manager_; - // Efectes visuals - Effects::DebrisManager debris_manager_; - Effects::FloatingScoreManager floating_score_manager_; + // Estat del juego + std::array ships_; // [0]=P1, [1]=P2 + std::array enemies_; + // 6 balas: P1=[0,1,2], P2=[3,4,5]. El cast a size_t evita la + // widening conversion implícita que detecta clang-tidy. + std::array(Constants::MAX_BALES) * 2> bullets_; + std::array hit_timer_per_player_; // Death timers per player (seconds) - // Estat del juego - std::array ships_; // [0]=P1, [1]=P2 - std::array enemies_; - // 6 balas: P1=[0,1,2], P2=[3,4,5]. El cast a size_t evita la - // widening conversion implícita que detecta clang-tidy. - std::array(Constants::MAX_BALES) * 2> bullets_; - std::array hit_timer_per_player_; // Death timers per player (seconds) + // Lives and game over system + std::array lives_per_player_; // [0]=P1, [1]=P2 + GameOverState game_over_state_; // Game over state machine (NONE, CONTINUE, GAME_OVER) + int continue_counter_; // Continue countdown (9→0) + float continue_tick_timer_; // Timer for countdown tick (1.0s) + int continues_used_; // Continues used this game (0-3 max) + float game_over_timer_; // Final GAME OVER timer before title screen + Vec2 death_position_; // Death position (for respawn) + std::array score_per_player_; // [0]=P1, [1]=P2 - // Lives and game over system - std::array lives_per_player_; // [0]=P1, [1]=P2 - GameOverState game_over_state_; // Game over state machine (NONE, CONTINUE, GAME_OVER) - int continue_counter_; // Continue countdown (9→0) - float continue_tick_timer_; // Timer for countdown tick (1.0s) - int continues_used_; // Continues used this game (0-3 max) - float game_over_timer_; // Final GAME OVER timer before title screen - Vec2 death_position_; // Death position (for respawn) - std::array score_per_player_; // [0]=P1, [1]=P2 + // Text vectorial + Graphics::VectorText text_; - // Text vectorial - Graphics::VectorText text_; + // [NEW] Stage system + std::unique_ptr stage_config_; + std::unique_ptr stage_manager_; - // [NEW] Stage system - std::unique_ptr stage_config_; - std::unique_ptr stage_manager_; + // Control de sons de animación INIT_HUD + bool init_hud_rect_sound_played_{false}; // Flag para evitar repetir sonido del rectángulo - // Control de sons de animación INIT_HUD - bool init_hud_rect_sound_played_{false}; // Flag para evitar repetir sonido del rectángulo + // Funciones privades + void tocado(uint8_t player_id); + void drawMargins() const; // Dibuixar vores de la zona de juego + void drawScoreboard(); // Dibuixar marcador de puntuación + void fireBullet(uint8_t player_id); // Shoot bullet from player + [[nodiscard]] auto getSpawnPoint(uint8_t player_id) const -> Vec2; // Get spawn position for player - // Funciones privades - void tocado(uint8_t player_id); - void drawMargins() const; // Dibuixar vores de la zona de juego - void drawScoreboard(); // Dibuixar marcador de puntuación - void fireBullet(uint8_t player_id); // Shoot bullet from player - [[nodiscard]] auto getSpawnPoint(uint8_t player_id) const -> Vec2; // Get spawn position for player + // [NEW] Continue & Join system + void joinPlayer(uint8_t player_id); // Join inactive player mid-game + void drawContinue(); // Draw continue screen - // [NEW] Continue & Join system - void joinPlayer(uint8_t player_id); // Join inactive player mid-game - void drawContinue(); // Draw continue screen + // [NEW] Stage system helpers + void drawStageMessage(const std::string& message); - // [NEW] Stage system helpers - void drawStageMessage(const std::string& message); + // Helpers de renderitzat (extracció de draw() per reduir complexitat). + // Cadascun gestiona un bloc concret; draw() només despatxa segons l'estat. + void drawEnemies() const; + void drawBullets() const; + void drawActiveShipsAlive() const; + void drawContinueState(); + void drawGameOverState(); + void drawInitHudState(); + void drawLevelStartState(); + void drawPlayingState(); + void drawLevelCompletedState(); - // Helpers de renderitzat (extracció de draw() per reduir complexitat). - // Cadascun gestiona un bloc concret; draw() només despatxa segons l'estat. - void drawEnemies() const; - void drawBullets() const; - void drawActiveShipsAlive() const; - void drawContinueState(); - void drawGameOverState(); - void drawInitHudState(); - void drawLevelStartState(); - void drawPlayingState(); - void drawLevelCompletedState(); + // [NEW] Función helper del marcador + [[nodiscard]] auto buildScoreboard() const -> std::string; - // [NEW] Función helper del marcador - [[nodiscard]] auto buildScoreboard() const -> std::string; - - // Sub-pasos de update() (descompuestos en Fase 9d para reducir - // complejidad cognitiva; cada uno es responsable de una sección). - void stepPhysics(float delta_time); - void stepShootingInput(); - void stepMidGameJoin(); - // Devuelven true si el frame debe salir tras esta sección. - [[nodiscard]] auto stepContinueScreen(float delta_time) -> bool; - [[nodiscard]] auto stepGameOver(float delta_time) -> bool; - // Avanza el death timer / respawn / transición a CONTINUE. Si algún - // jugador está en secuencia de muerte, también actualiza efectos - // (enemigos, balas, debris) que siguen vivos en el escenario. - void stepDeathSequence(float delta_time); - void stepStageStateMachine(float delta_time); - void runStageInitHud(float delta_time); - void runStageLevelStart(float delta_time); - void runStagePlaying(float delta_time); - void runStageLevelCompleted(float delta_time); - // Helper: ejecuta colisiones de gameplay con el Context preparado. - void runCollisionDetections(); + // Sub-pasos de update() (descompuestos en Fase 9d para reducir + // complejidad cognitiva; cada uno es responsable de una sección). + void stepPhysics(float delta_time); + void stepShootingInput(); + void stepMidGameJoin(); + // Devuelven true si el frame debe salir tras esta sección. + [[nodiscard]] auto stepContinueScreen(float delta_time) -> bool; + [[nodiscard]] auto stepGameOver(float delta_time) -> bool; + // Avanza el death timer / respawn / transición a CONTINUE. Si algún + // jugador está en secuencia de muerte, también actualiza efectos + // (enemigos, balas, debris) que siguen vivos en el escenario. + void stepDeathSequence(float delta_time); + void stepStageStateMachine(float delta_time); + void runStageInitHud(float delta_time); + void runStageLevelStart(float delta_time); + void runStagePlaying(float delta_time); + void runStageLevelCompleted(float delta_time); + // Helper: ejecuta colisiones de gameplay con el Context preparado. + void runCollisionDetections(); }; From 97c98272c9f688b3f489bee28021b5529aee5c0e Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 17:37:37 +0200 Subject: [PATCH 06/18] refactor: consolidar Ship::isAlive/isHit/isActive en isActive() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Els tres mètodes retornaven el mateix bool a partir d'is_hit_: isActive() = !is_hit_ (override de Entity) isAlive() = !is_hit_ isHit() = is_hit_ Eren tres formes diferents de preguntar el mateix, repartides sense criteri pels call-sites (collision_system, game_scene). Conservem isActive() perquè és l'override polimòrfic d'Entity i esborrem els altres dos. Actualitzats els 5 call-sites externs. Hallazgo #11 de CODE_REVIEW.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/game/entities/ship.hpp | 84 ++++---- source/game/scenes/game_scene.cpp | 6 +- source/game/systems/collision_system.cpp | 241 ++++++++++++----------- 3 files changed, 167 insertions(+), 164 deletions(-) diff --git a/source/game/entities/ship.hpp b/source/game/entities/ship.hpp index 33fd307..1a957a6 100644 --- a/source/game/entities/ship.hpp +++ b/source/game/entities/ship.hpp @@ -11,54 +11,52 @@ #include "core/types.hpp" class Ship : public Entities::Entity { - public: - Ship() - : Entity(nullptr) {} - explicit Ship(Rendering::Renderer* renderer, const char* shape_file = "ship.shp"); + public: + Ship() + : Entity(nullptr) {} + explicit Ship(Rendering::Renderer* renderer, const char* shape_file = "ship.shp"); - void init() override { init(nullptr, false); } - void init(const Vec2* spawn_point, bool activar_invulnerabilitat = false); - void processInput(float delta_time, uint8_t player_id); - void update(float delta_time) override; - void postUpdate(float delta_time) override; - void draw() const override; + void init() override { init(nullptr, false); } + void init(const Vec2* spawn_point, bool activar_invulnerabilitat = false); + void processInput(float delta_time, uint8_t player_id); + void update(float delta_time) override; + void postUpdate(float delta_time) override; + void draw() const override; - // Override: Interfaz de Entity - [[nodiscard]] auto isActive() const -> bool override { return !is_hit_; } + // Override: Interfaz de Entity + [[nodiscard]] auto isActive() const -> bool override { return !is_hit_; } - // Override: Interfaz de colisión - [[nodiscard]] auto getCollisionRadius() const -> float override { - return Defaults::Entities::SHIP_RADIUS; - } - [[nodiscard]] auto isCollidable() const -> bool override { - return !is_hit_ && invulnerable_timer_ <= 0.0F; - } + // Override: Interfaz de colisión + [[nodiscard]] auto getCollisionRadius() const -> float override { + return Defaults::Entities::SHIP_RADIUS; + } + [[nodiscard]] auto isCollidable() const -> bool override { + return !is_hit_ && invulnerable_timer_ <= 0.0F; + } - // Getters (API pública sin cambios) - [[nodiscard]] auto isAlive() const -> bool { return !is_hit_; } - [[nodiscard]] auto isHit() const -> bool { return is_hit_; } - [[nodiscard]] auto isInvulnerable() const -> bool { return invulnerable_timer_ > 0.0F; } - // Velocidad como vector cartesiano (ahora viene directa del body_). - [[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; } - // Velocidad escalar (utilidad para draw y debugging). - [[nodiscard]] auto getSpeed() const -> float { return body_.velocity.length(); } + // Getters + [[nodiscard]] auto isInvulnerable() const -> bool { return invulnerable_timer_ > 0.0F; } + // Velocidad como vector cartesiano (ahora viene directa del body_). + [[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; } + // Velocidad escalar (utilidad para draw y debugging). + [[nodiscard]] auto getSpeed() const -> float { return body_.velocity.length(); } - // Setters - void setCenter(const Vec2& nou_centre) { - center_ = nou_centre; - body_.position = nou_centre; - } + // Setters + void setCenter(const Vec2& nou_centre) { + center_ = nou_centre; + body_.position = nou_centre; + } - // Colisiones - void markHit() { - is_hit_ = true; - body_.velocity = Vec2{}; // Detener al morir - } + // Colisiones + void markHit() { + is_hit_ = true; + body_.velocity = Vec2{}; // Detener al morir + } - private: - // Miembros específicos de Ship (heredados: renderer_, shape_, center_, angle_, brightness_, body_). - // Inicializados en la declaración: el ctor por defecto deja la nave "viva y sin invulnerabilidad", - // que es el estado coherente al que llevan tanto init() como el ctor con renderer. - bool is_hit_{false}; - float invulnerable_timer_{0.0F}; // 0.0f = vulnerable, >0.0f = invulnerable + private: + // Miembros específicos de Ship (heredados: renderer_, shape_, center_, angle_, brightness_, body_). + // Inicializados en la declaración: el ctor por defecto deja la nave "viva y sin invulnerabilidad", + // que es el estado coherente al que llevan tanto init() como el ctor con renderer. + bool is_hit_{false}; + float invulnerable_timer_{0.0F}; // 0.0f = vulnerable, >0.0f = invulnerable }; diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 1a83e4a..2c7170c 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -573,11 +573,11 @@ void GameScene::drawInitHudState() { Systems::InitHud::drawScoreboardAnimated(text_, buildScoreboard(), score_progress); } - if (ship1_progress > 0.0F && match_config_.jugador1_actiu && !ships_[0].isHit()) { + if (ship1_progress > 0.0F && match_config_.jugador1_actiu && ships_[0].isActive()) { ships_[0].draw(); } - if (ship2_progress > 0.0F && match_config_.jugador2_actiu && !ships_[1].isHit()) { + if (ship2_progress > 0.0F && match_config_.jugador2_actiu && ships_[1].isActive()) { ships_[1].draw(); } } @@ -820,7 +820,7 @@ void GameScene::fireBullet(uint8_t player_id) { if (hit_timer_per_player_[player_id] > 0.0F) { return; } - if (!ships_[player_id].isAlive()) { + if (!ships_[player_id].isActive()) { return; } diff --git a/source/game/systems/collision_system.cpp b/source/game/systems/collision_system.cpp index 4ee9587..ff1f6f9 100644 --- a/source/game/systems/collision_system.cpp +++ b/source/game/systems/collision_system.cpp @@ -10,141 +10,146 @@ namespace Systems::Collision { -void detectBulletEnemy(Context& ctx) { - constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER; - constexpr float VELOCITAT_EXPLOSIO = 80.0F; // px/s (explosión suau) + void detectBulletEnemy(Context& ctx) { + constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER; + constexpr float VELOCITAT_EXPLOSIO = 80.0F; // px/s (explosión suau) - for (auto& bullet : ctx.bullets) { - for (auto& enemy : ctx.enemies) { - if (!Physics::checkCollision(bullet, enemy, AMPLIFIER)) { - continue; - } + for (auto& bullet : ctx.bullets) { + for (auto& enemy : ctx.enemies) { + if (!Physics::checkCollision(bullet, enemy, AMPLIFIER)) { + continue; + } - // *** COLISIÓN bullet → enemy *** - const Vec2& enemy_pos = enemy.getCenter(); + // *** COLISIÓN bullet → enemy *** + const Vec2& enemy_pos = enemy.getCenter(); - // 1. Puntos según tipo - int points = 0; - switch (enemy.getType()) { - case EnemyType::PENTAGON: - points = Defaults::Enemies::Scoring::PENTAGON_SCORE; - break; - case EnemyType::QUADRAT: - points = Defaults::Enemies::Scoring::QUADRAT_SCORE; - break; - case EnemyType::MOLINILLO: - points = Defaults::Enemies::Scoring::MOLINILLO_SCORE; - break; - } + // 1. Puntos según tipo + int points = 0; + switch (enemy.getType()) { + case EnemyType::PENTAGON: + points = Defaults::Enemies::Scoring::PENTAGON_SCORE; + break; + case EnemyType::QUADRAT: + points = Defaults::Enemies::Scoring::QUADRAT_SCORE; + break; + case EnemyType::MOLINILLO: + points = Defaults::Enemies::Scoring::MOLINILLO_SCORE; + break; + } - uint8_t owner_id = bullet.getOwnerId(); - ctx.score_per_player[owner_id] += points; - ctx.floating_score_manager.crear(points, enemy_pos); + uint8_t owner_id = bullet.getOwnerId(); + ctx.score_per_player[owner_id] += points; + ctx.floating_score_manager.crear(points, enemy_pos); - // 2. Destruir enemy + crear explosión (debris hereda color del enemy) - SDL_Color enemy_color{}; - switch (enemy.getType()) { - case EnemyType::PENTAGON: enemy_color = Defaults::Palette::PENTAGON; break; - case EnemyType::QUADRAT: enemy_color = Defaults::Palette::QUADRAT; break; - case EnemyType::MOLINILLO: enemy_color = Defaults::Palette::MOLINILLO; break; - } - enemy.destruir(); - Vec2 vel_enemic = enemy.getVelocityVector(); - ctx.debris_manager.explode( - enemy.getShape(), - enemy_pos, - 0.0F, // angle (la rotación es interna del enemy) - 1.0F, // escala - VELOCITAT_EXPLOSIO, - enemy.getBrightness(), - vel_enemic, - enemy.getRotationDelta(), - 0.0F, // sin herencia visual - Defaults::Sound::EXPLOSION, - enemy_color - ); + // 2. Destruir enemy + crear explosión (debris hereda color del enemy) + SDL_Color enemy_color{}; + switch (enemy.getType()) { + case EnemyType::PENTAGON: + enemy_color = Defaults::Palette::PENTAGON; + break; + case EnemyType::QUADRAT: + enemy_color = Defaults::Palette::QUADRAT; + break; + case EnemyType::MOLINILLO: + enemy_color = Defaults::Palette::MOLINILLO; + break; + } + enemy.destruir(); + Vec2 vel_enemic = enemy.getVelocityVector(); + ctx.debris_manager.explode( + enemy.getShape(), + enemy_pos, + 0.0F, // angle (la rotación es interna del enemy) + 1.0F, // escala + VELOCITAT_EXPLOSIO, + enemy.getBrightness(), + vel_enemic, + enemy.getRotationDelta(), + 0.0F, // sin herencia visual + Defaults::Sound::EXPLOSION, + enemy_color); - // 3. Desactivar bullet (solo destruye 1 enemy) - bullet.desactivar(); - break; - } - } -} - -void detectShipEnemy(Context& ctx) { - constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER; - - for (uint8_t i = 0; i < 2; i++) { - // Skip si ya tocado / muerto / invulnerable - if (ctx.hit_timer_per_player[i] > 0.0F || - !ctx.ships[i].isAlive() || - ctx.ships[i].isInvulnerable()) { - continue; - } - - for (const auto& enemy : ctx.enemies) { - if (enemy.isInvulnerable()) { - continue; - } - if (Physics::checkCollision(ctx.ships[i], enemy, AMPLIFIER)) { - ctx.on_player_hit(i); - break; // Solo una colisión por player por frame + // 3. Desactivar bullet (solo destruye 1 enemy) + bullet.desactivar(); + break; } } } -} -void detectBulletPlayer(Context& ctx) { - if (!Defaults::Game::FRIENDLY_FIRE_ENABLED) { - return; - } + void detectShipEnemy(Context& ctx) { + constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER; - constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_PLAYER_AMPLIFIER; - - for (auto& bullet : ctx.bullets) { - if (!bullet.isActive() || bullet.getGraceTimer() > 0.0F) { - continue; - } - const uint8_t BULLET_OWNER = bullet.getOwnerId(); - - for (uint8_t player_id = 0; player_id < 2; player_id++) { - if (ctx.hit_timer_per_player[player_id] > 0.0F || - !ctx.ships[player_id].isAlive() || - ctx.ships[player_id].isInvulnerable()) { - continue; - } - const bool JUGADOR_ACTIU = (player_id == 0) - ? ctx.match_config.jugador1_actiu - : ctx.match_config.jugador2_actiu; - if (!JUGADOR_ACTIU) { + for (uint8_t i = 0; i < 2; i++) { + // Skip si ya tocado / muerto / invulnerable + if (ctx.hit_timer_per_player[i] > 0.0F || + !ctx.ships[i].isActive() || + ctx.ships[i].isInvulnerable()) { continue; } - if (!Physics::checkCollision(bullet, ctx.ships[player_id], AMPLIFIER)) { - continue; + for (const auto& enemy : ctx.enemies) { + if (enemy.isInvulnerable()) { + continue; + } + if (Physics::checkCollision(ctx.ships[i], enemy, AMPLIFIER)) { + ctx.on_player_hit(i); + break; // Solo una colisión por player por frame + } } - - // *** FRIENDLY FIRE HIT *** - if (BULLET_OWNER == player_id) { - // Self-hit: víctima pierde 1 vida. - ctx.on_player_hit(player_id); - } else { - // Teammate hit: víctima pierde 1, atacante gana 1. - ctx.on_player_hit(player_id); - ctx.lives_per_player[BULLET_OWNER]++; - } - - Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME); - bullet.desactivar(); - break; // Una bullet solo impacta una vez por frame } } -} -void detectAll(Context& ctx) { - detectBulletEnemy(ctx); - detectShipEnemy(ctx); - detectBulletPlayer(ctx); -} + void detectBulletPlayer(Context& ctx) { + if (!Defaults::Game::FRIENDLY_FIRE_ENABLED) { + return; + } + + constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_PLAYER_AMPLIFIER; + + for (auto& bullet : ctx.bullets) { + if (!bullet.isActive() || bullet.getGraceTimer() > 0.0F) { + continue; + } + const uint8_t BULLET_OWNER = bullet.getOwnerId(); + + for (uint8_t player_id = 0; player_id < 2; player_id++) { + if (ctx.hit_timer_per_player[player_id] > 0.0F || + !ctx.ships[player_id].isActive() || + ctx.ships[player_id].isInvulnerable()) { + continue; + } + const bool JUGADOR_ACTIU = (player_id == 0) + ? ctx.match_config.jugador1_actiu + : ctx.match_config.jugador2_actiu; + if (!JUGADOR_ACTIU) { + continue; + } + + if (!Physics::checkCollision(bullet, ctx.ships[player_id], AMPLIFIER)) { + continue; + } + + // *** FRIENDLY FIRE HIT *** + if (BULLET_OWNER == player_id) { + // Self-hit: víctima pierde 1 vida. + ctx.on_player_hit(player_id); + } else { + // Teammate hit: víctima pierde 1, atacante gana 1. + ctx.on_player_hit(player_id); + ctx.lives_per_player[BULLET_OWNER]++; + } + + Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME); + bullet.desactivar(); + break; // Una bullet solo impacta una vez por frame + } + } + } + + void detectAll(Context& ctx) { + detectBulletEnemy(ctx); + detectShipEnemy(ctx); + detectBulletPlayer(ctx); + } } // namespace Systems::Collision From e4b6d2df6a0e5da1d7b50cb64308882efa483afc Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 18:06:51 +0200 Subject: [PATCH 07/18] build: corregir cppcheck del pre-commit (path relatiu) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Amb -I "\$REPO_ROOT/source" (path absolut), cppcheck no resolia bé el i emetia un syntaxError fals sobre les capçaleres del tipus "enum class X : std::uint8_t {" (afecta scene_context.hpp i d'altres que tenen enums tipats). El bug estava latent des del commit c45e524 ("clang-tidy --fix mecánico (... enum size)"), que va afegir els underlying-types als enums. Cap commit posterior va tocar fitxers que els inclogueren, així que ningú l'havia activat fins ara. Resolt amb path relatiu (els git hooks corren sempre des del repo root, així que "source" és suficient). Co-Authored-By: Claude Opus 4.7 (1M context) --- .githooks/pre-commit | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index fe14911..fa0ea76 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -71,6 +71,10 @@ if [ ${#CPP_STAGED[@]} -eq 0 ]; then fi echo "pre-commit: cppcheck sobre ${#CPP_STAGED[@]} fitxer(s)..." >&2 +# Nota: el path d'inclusió ha d'anar en relatiu. Amb path absolut, cppcheck +# falla a parsejar "enum class X : std::uint8_t" (no resol bé) i +# emet un syntaxError fals. Els hooks de git s'executen sempre des de la +# rel del repo, així que "source" relatiu és prou. if ! cppcheck \ --enable=warning,style,performance,portability \ --std=c++20 \ @@ -85,7 +89,7 @@ if ! cppcheck \ -DLINUX_BUILD \ --quiet \ --error-exitcode=1 \ - -I "$REPO_ROOT/source" \ + -I source \ "${CPP_STAGED[@]}"; then echo "pre-commit: cppcheck ha trobat errors — commit avortat" >&2 exit 1 From 11e9d6569bee8cea07326ade2c86770b03bb7474 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 18:07:27 +0200 Subject: [PATCH 08/18] refactor: eliminar Options::physics/audio/gameplay (codi mort) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aquestes tres seccions s'estaven carregant del YAML, parsejant, validant i escrivint, però cap d'elles tenia consumidor en runtime: - Options::physics: l'únic call-site era un std::cout informatiu a director.cpp:109. Ship/Enemy/Bullet llegeixen Defaults::Physics directament. - Options::audio: explícitament desacoblat (audio.hpp:22-25) — la font de configuració era Defaults::Audio via Audio::Config. - Options::gameplay: zero readers. Els arrays són compile-time. Esborrats: - Structs Physics/Gameplay/Music/Sound/Audio i les globals. - Defaults a init(), helpers loadXxxConfigFromYaml, secció escrita a saveToFile, crides al loadFromFile. - La línia "Física: rotation=..." del console output del Director. Es manté Options::window i Options::rendering (sí utilitzats). Hallazgo #21 de CODE_REVIEW.md (opció a: borrar). Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/system/director.cpp | 18 +- source/game/options.cpp | 1010 ++++++++++++++----------------- source/game/options.hpp | 126 ++-- 3 files changed, 493 insertions(+), 661 deletions(-) diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 69e7701..24decac 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -9,10 +9,6 @@ #include #include -#include "debug_overlay.hpp" -#include "scene.hpp" -#include "scene_context.hpp" -#include "global_events.hpp" #include "core/audio/audio.hpp" #include "core/audio/audio_adapter.hpp" #include "core/defaults.hpp" @@ -22,11 +18,15 @@ #include "core/resources/resource_helper.hpp" #include "core/resources/resource_loader.hpp" #include "core/utils/path_utils.hpp" +#include "debug_overlay.hpp" +#include "game/options.hpp" #include "game/scenes/game_scene.hpp" #include "game/scenes/logo_scene.hpp" #include "game/scenes/title_scene.hpp" -#include "game/options.hpp" +#include "global_events.hpp" #include "project.h" +#include "scene.hpp" +#include "scene_context.hpp" #ifndef _WIN32 #include @@ -106,8 +106,6 @@ Director::Director(std::vector const& args) { std::cout << "Configuración carregada\n"; std::cout << " Finestra: " << Options::window.width << "×" << Options::window.height << '\n'; - std::cout << " Física: rotation=" << Options::physics.rotation_speed - << " rad/s\n"; std::cout << " Input: " << Input::get()->getNumGamepads() << " gamepad(s) detectat(s)\n"; } @@ -291,8 +289,7 @@ auto Director::buildScene(SceneType type, SDLManager& sdl, SceneContext& context } } -void Director::runFrameLoop(Scene& scene, SDLManager& sdl, SceneContext& context, - System::DebugOverlay& debug_overlay) { +void Director::runFrameLoop(Scene& scene, SDLManager& sdl, SceneContext& context, System::DebugOverlay& debug_overlay) { SDL_Event event; Uint64 last_time = SDL_GetTicks(); @@ -315,8 +312,7 @@ void Director::runFrameLoop(Scene& scene, SDLManager& sdl, SceneContext& context if (GlobalEvents::handle(event, sdl, context)) { continue; } - if (event.type == SDL_EVENT_KEY_DOWN - && event.key.scancode == SDL_SCANCODE_F11) { + if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_F11) { debug_overlay.toggle(); continue; } diff --git a/source/game/options.cpp b/source/game/options.cpp index 9b4cf1b..8a383bd 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -11,629 +11,499 @@ namespace Options { -// ========== FUNCIONS AUXILIARS PER CONVERSIÓ DE CONTROLES ========== + // ========== FUNCIONS AUXILIARS PER CONVERSIÓ DE CONTROLES ========== -// Mapa de SDL_Scancode a string -static const std::unordered_map SCANCODE_TO_STRING = { - {SDL_SCANCODE_A, "A"}, - {SDL_SCANCODE_B, "B"}, - {SDL_SCANCODE_C, "C"}, - {SDL_SCANCODE_D, "D"}, - {SDL_SCANCODE_E, "E"}, - {SDL_SCANCODE_F, "F"}, - {SDL_SCANCODE_G, "G"}, - {SDL_SCANCODE_H, "H"}, - {SDL_SCANCODE_I, "I"}, - {SDL_SCANCODE_J, "J"}, - {SDL_SCANCODE_K, "K"}, - {SDL_SCANCODE_L, "L"}, - {SDL_SCANCODE_M, "M"}, - {SDL_SCANCODE_N, "N"}, - {SDL_SCANCODE_O, "O"}, - {SDL_SCANCODE_P, "P"}, - {SDL_SCANCODE_Q, "Q"}, - {SDL_SCANCODE_R, "R"}, - {SDL_SCANCODE_S, "S"}, - {SDL_SCANCODE_T, "T"}, - {SDL_SCANCODE_U, "U"}, - {SDL_SCANCODE_V, "V"}, - {SDL_SCANCODE_W, "W"}, - {SDL_SCANCODE_X, "X"}, - {SDL_SCANCODE_Y, "Y"}, - {SDL_SCANCODE_Z, "Z"}, - {SDL_SCANCODE_1, "1"}, - {SDL_SCANCODE_2, "2"}, - {SDL_SCANCODE_3, "3"}, - {SDL_SCANCODE_4, "4"}, - {SDL_SCANCODE_5, "5"}, - {SDL_SCANCODE_6, "6"}, - {SDL_SCANCODE_7, "7"}, - {SDL_SCANCODE_8, "8"}, - {SDL_SCANCODE_9, "9"}, - {SDL_SCANCODE_0, "0"}, - {SDL_SCANCODE_RETURN, "RETURN"}, - {SDL_SCANCODE_ESCAPE, "ESCAPE"}, - {SDL_SCANCODE_BACKSPACE, "BACKSPACE"}, - {SDL_SCANCODE_TAB, "TAB"}, - {SDL_SCANCODE_SPACE, "SPACE"}, - {SDL_SCANCODE_UP, "UP"}, - {SDL_SCANCODE_DOWN, "DOWN"}, - {SDL_SCANCODE_LEFT, "LEFT"}, - {SDL_SCANCODE_RIGHT, "RIGHT"}, - {SDL_SCANCODE_LSHIFT, "LSHIFT"}, - {SDL_SCANCODE_RSHIFT, "RSHIFT"}, - {SDL_SCANCODE_LCTRL, "LCTRL"}, - {SDL_SCANCODE_RCTRL, "RCTRL"}, - {SDL_SCANCODE_LALT, "LALT"}, - {SDL_SCANCODE_RALT, "RALT"}}; + // Mapa de SDL_Scancode a string + static const std::unordered_map SCANCODE_TO_STRING = { + {SDL_SCANCODE_A, "A"}, + {SDL_SCANCODE_B, "B"}, + {SDL_SCANCODE_C, "C"}, + {SDL_SCANCODE_D, "D"}, + {SDL_SCANCODE_E, "E"}, + {SDL_SCANCODE_F, "F"}, + {SDL_SCANCODE_G, "G"}, + {SDL_SCANCODE_H, "H"}, + {SDL_SCANCODE_I, "I"}, + {SDL_SCANCODE_J, "J"}, + {SDL_SCANCODE_K, "K"}, + {SDL_SCANCODE_L, "L"}, + {SDL_SCANCODE_M, "M"}, + {SDL_SCANCODE_N, "N"}, + {SDL_SCANCODE_O, "O"}, + {SDL_SCANCODE_P, "P"}, + {SDL_SCANCODE_Q, "Q"}, + {SDL_SCANCODE_R, "R"}, + {SDL_SCANCODE_S, "S"}, + {SDL_SCANCODE_T, "T"}, + {SDL_SCANCODE_U, "U"}, + {SDL_SCANCODE_V, "V"}, + {SDL_SCANCODE_W, "W"}, + {SDL_SCANCODE_X, "X"}, + {SDL_SCANCODE_Y, "Y"}, + {SDL_SCANCODE_Z, "Z"}, + {SDL_SCANCODE_1, "1"}, + {SDL_SCANCODE_2, "2"}, + {SDL_SCANCODE_3, "3"}, + {SDL_SCANCODE_4, "4"}, + {SDL_SCANCODE_5, "5"}, + {SDL_SCANCODE_6, "6"}, + {SDL_SCANCODE_7, "7"}, + {SDL_SCANCODE_8, "8"}, + {SDL_SCANCODE_9, "9"}, + {SDL_SCANCODE_0, "0"}, + {SDL_SCANCODE_RETURN, "RETURN"}, + {SDL_SCANCODE_ESCAPE, "ESCAPE"}, + {SDL_SCANCODE_BACKSPACE, "BACKSPACE"}, + {SDL_SCANCODE_TAB, "TAB"}, + {SDL_SCANCODE_SPACE, "SPACE"}, + {SDL_SCANCODE_UP, "UP"}, + {SDL_SCANCODE_DOWN, "DOWN"}, + {SDL_SCANCODE_LEFT, "LEFT"}, + {SDL_SCANCODE_RIGHT, "RIGHT"}, + {SDL_SCANCODE_LSHIFT, "LSHIFT"}, + {SDL_SCANCODE_RSHIFT, "RSHIFT"}, + {SDL_SCANCODE_LCTRL, "LCTRL"}, + {SDL_SCANCODE_RCTRL, "RCTRL"}, + {SDL_SCANCODE_LALT, "LALT"}, + {SDL_SCANCODE_RALT, "RALT"}}; -// Mapa invers: string a SDL_Scancode -static const std::unordered_map STRING_TO_SCANCODE = { - {"A", SDL_SCANCODE_A}, - {"B", SDL_SCANCODE_B}, - {"C", SDL_SCANCODE_C}, - {"D", SDL_SCANCODE_D}, - {"E", SDL_SCANCODE_E}, - {"F", SDL_SCANCODE_F}, - {"G", SDL_SCANCODE_G}, - {"H", SDL_SCANCODE_H}, - {"I", SDL_SCANCODE_I}, - {"J", SDL_SCANCODE_J}, - {"K", SDL_SCANCODE_K}, - {"L", SDL_SCANCODE_L}, - {"M", SDL_SCANCODE_M}, - {"N", SDL_SCANCODE_N}, - {"O", SDL_SCANCODE_O}, - {"P", SDL_SCANCODE_P}, - {"Q", SDL_SCANCODE_Q}, - {"R", SDL_SCANCODE_R}, - {"S", SDL_SCANCODE_S}, - {"T", SDL_SCANCODE_T}, - {"U", SDL_SCANCODE_U}, - {"V", SDL_SCANCODE_V}, - {"W", SDL_SCANCODE_W}, - {"X", SDL_SCANCODE_X}, - {"Y", SDL_SCANCODE_Y}, - {"Z", SDL_SCANCODE_Z}, - {"1", SDL_SCANCODE_1}, - {"2", SDL_SCANCODE_2}, - {"3", SDL_SCANCODE_3}, - {"4", SDL_SCANCODE_4}, - {"5", SDL_SCANCODE_5}, - {"6", SDL_SCANCODE_6}, - {"7", SDL_SCANCODE_7}, - {"8", SDL_SCANCODE_8}, - {"9", SDL_SCANCODE_9}, - {"0", SDL_SCANCODE_0}, - {"RETURN", SDL_SCANCODE_RETURN}, - {"ESCAPE", SDL_SCANCODE_ESCAPE}, - {"BACKSPACE", SDL_SCANCODE_BACKSPACE}, - {"TAB", SDL_SCANCODE_TAB}, - {"SPACE", SDL_SCANCODE_SPACE}, - {"UP", SDL_SCANCODE_UP}, - {"DOWN", SDL_SCANCODE_DOWN}, - {"LEFT", SDL_SCANCODE_LEFT}, - {"RIGHT", SDL_SCANCODE_RIGHT}, - {"LSHIFT", SDL_SCANCODE_LSHIFT}, - {"RSHIFT", SDL_SCANCODE_RSHIFT}, - {"LCTRL", SDL_SCANCODE_LCTRL}, - {"RCTRL", SDL_SCANCODE_RCTRL}, - {"LALT", SDL_SCANCODE_LALT}, - {"RALT", SDL_SCANCODE_RALT}}; + // Mapa invers: string a SDL_Scancode + static const std::unordered_map STRING_TO_SCANCODE = { + {"A", SDL_SCANCODE_A}, + {"B", SDL_SCANCODE_B}, + {"C", SDL_SCANCODE_C}, + {"D", SDL_SCANCODE_D}, + {"E", SDL_SCANCODE_E}, + {"F", SDL_SCANCODE_F}, + {"G", SDL_SCANCODE_G}, + {"H", SDL_SCANCODE_H}, + {"I", SDL_SCANCODE_I}, + {"J", SDL_SCANCODE_J}, + {"K", SDL_SCANCODE_K}, + {"L", SDL_SCANCODE_L}, + {"M", SDL_SCANCODE_M}, + {"N", SDL_SCANCODE_N}, + {"O", SDL_SCANCODE_O}, + {"P", SDL_SCANCODE_P}, + {"Q", SDL_SCANCODE_Q}, + {"R", SDL_SCANCODE_R}, + {"S", SDL_SCANCODE_S}, + {"T", SDL_SCANCODE_T}, + {"U", SDL_SCANCODE_U}, + {"V", SDL_SCANCODE_V}, + {"W", SDL_SCANCODE_W}, + {"X", SDL_SCANCODE_X}, + {"Y", SDL_SCANCODE_Y}, + {"Z", SDL_SCANCODE_Z}, + {"1", SDL_SCANCODE_1}, + {"2", SDL_SCANCODE_2}, + {"3", SDL_SCANCODE_3}, + {"4", SDL_SCANCODE_4}, + {"5", SDL_SCANCODE_5}, + {"6", SDL_SCANCODE_6}, + {"7", SDL_SCANCODE_7}, + {"8", SDL_SCANCODE_8}, + {"9", SDL_SCANCODE_9}, + {"0", SDL_SCANCODE_0}, + {"RETURN", SDL_SCANCODE_RETURN}, + {"ESCAPE", SDL_SCANCODE_ESCAPE}, + {"BACKSPACE", SDL_SCANCODE_BACKSPACE}, + {"TAB", SDL_SCANCODE_TAB}, + {"SPACE", SDL_SCANCODE_SPACE}, + {"UP", SDL_SCANCODE_UP}, + {"DOWN", SDL_SCANCODE_DOWN}, + {"LEFT", SDL_SCANCODE_LEFT}, + {"RIGHT", SDL_SCANCODE_RIGHT}, + {"LSHIFT", SDL_SCANCODE_LSHIFT}, + {"RSHIFT", SDL_SCANCODE_RSHIFT}, + {"LCTRL", SDL_SCANCODE_LCTRL}, + {"RCTRL", SDL_SCANCODE_RCTRL}, + {"LALT", SDL_SCANCODE_LALT}, + {"RALT", SDL_SCANCODE_RALT}}; -// Mapa de botó de gamepad (int) a string -static const std::unordered_map BUTTON_TO_STRING = { - {SDL_GAMEPAD_BUTTON_SOUTH, "SOUTH"}, // A (Xbox), Cross (PS) - {SDL_GAMEPAD_BUTTON_EAST, "EAST"}, // B (Xbox), Circle (PS) - {SDL_GAMEPAD_BUTTON_WEST, "WEST"}, // X (Xbox), Square (PS) - {SDL_GAMEPAD_BUTTON_NORTH, "NORTH"}, // Y (Xbox), Triangle (PS) - {SDL_GAMEPAD_BUTTON_BACK, "BACK"}, - {SDL_GAMEPAD_BUTTON_START, "START"}, - {SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, "LEFT_SHOULDER"}, - {SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, "RIGHT_SHOULDER"}, - {SDL_GAMEPAD_BUTTON_DPAD_UP, "DPAD_UP"}, - {SDL_GAMEPAD_BUTTON_DPAD_DOWN, "DPAD_DOWN"}, - {SDL_GAMEPAD_BUTTON_DPAD_LEFT, "DPAD_LEFT"}, - {SDL_GAMEPAD_BUTTON_DPAD_RIGHT, "DPAD_RIGHT"}, - {100, "L2_AS_BUTTON"}, // Trigger L2 como a botó digital - {101, "R2_AS_BUTTON"} // Trigger R2 como a botó digital -}; + // Mapa de botó de gamepad (int) a string + static const std::unordered_map BUTTON_TO_STRING = { + {SDL_GAMEPAD_BUTTON_SOUTH, "SOUTH"}, // A (Xbox), Cross (PS) + {SDL_GAMEPAD_BUTTON_EAST, "EAST"}, // B (Xbox), Circle (PS) + {SDL_GAMEPAD_BUTTON_WEST, "WEST"}, // X (Xbox), Square (PS) + {SDL_GAMEPAD_BUTTON_NORTH, "NORTH"}, // Y (Xbox), Triangle (PS) + {SDL_GAMEPAD_BUTTON_BACK, "BACK"}, + {SDL_GAMEPAD_BUTTON_START, "START"}, + {SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, "LEFT_SHOULDER"}, + {SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, "RIGHT_SHOULDER"}, + {SDL_GAMEPAD_BUTTON_DPAD_UP, "DPAD_UP"}, + {SDL_GAMEPAD_BUTTON_DPAD_DOWN, "DPAD_DOWN"}, + {SDL_GAMEPAD_BUTTON_DPAD_LEFT, "DPAD_LEFT"}, + {SDL_GAMEPAD_BUTTON_DPAD_RIGHT, "DPAD_RIGHT"}, + {100, "L2_AS_BUTTON"}, // Trigger L2 como a botó digital + {101, "R2_AS_BUTTON"} // Trigger R2 como a botó digital + }; -// Mapa invers: string a botó de gamepad -static const std::unordered_map STRING_TO_BUTTON = { - {"SOUTH", SDL_GAMEPAD_BUTTON_SOUTH}, - {"EAST", SDL_GAMEPAD_BUTTON_EAST}, - {"WEST", SDL_GAMEPAD_BUTTON_WEST}, - {"NORTH", SDL_GAMEPAD_BUTTON_NORTH}, - {"BACK", SDL_GAMEPAD_BUTTON_BACK}, - {"START", SDL_GAMEPAD_BUTTON_START}, - {"LEFT_SHOULDER", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER}, - {"RIGHT_SHOULDER", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER}, - {"DPAD_UP", SDL_GAMEPAD_BUTTON_DPAD_UP}, - {"DPAD_DOWN", SDL_GAMEPAD_BUTTON_DPAD_DOWN}, - {"DPAD_LEFT", SDL_GAMEPAD_BUTTON_DPAD_LEFT}, - {"DPAD_RIGHT", SDL_GAMEPAD_BUTTON_DPAD_RIGHT}, - {"L2_AS_BUTTON", 100}, - {"R2_AS_BUTTON", 101}}; + // Mapa invers: string a botó de gamepad + static const std::unordered_map STRING_TO_BUTTON = { + {"SOUTH", SDL_GAMEPAD_BUTTON_SOUTH}, + {"EAST", SDL_GAMEPAD_BUTTON_EAST}, + {"WEST", SDL_GAMEPAD_BUTTON_WEST}, + {"NORTH", SDL_GAMEPAD_BUTTON_NORTH}, + {"BACK", SDL_GAMEPAD_BUTTON_BACK}, + {"START", SDL_GAMEPAD_BUTTON_START}, + {"LEFT_SHOULDER", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER}, + {"RIGHT_SHOULDER", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER}, + {"DPAD_UP", SDL_GAMEPAD_BUTTON_DPAD_UP}, + {"DPAD_DOWN", SDL_GAMEPAD_BUTTON_DPAD_DOWN}, + {"DPAD_LEFT", SDL_GAMEPAD_BUTTON_DPAD_LEFT}, + {"DPAD_RIGHT", SDL_GAMEPAD_BUTTON_DPAD_RIGHT}, + {"L2_AS_BUTTON", 100}, + {"R2_AS_BUTTON", 101}}; -static auto scancodeToString(SDL_Scancode code) -> std::string { - auto it = SCANCODE_TO_STRING.find(code); - return (it != SCANCODE_TO_STRING.end()) ? it->second : "UNKNOWN"; -} + static auto scancodeToString(SDL_Scancode code) -> std::string { + auto it = SCANCODE_TO_STRING.find(code); + return (it != SCANCODE_TO_STRING.end()) ? it->second : "UNKNOWN"; + } -static auto stringToScancode(const std::string& str) -> SDL_Scancode { - auto it = STRING_TO_SCANCODE.find(str); - return (it != STRING_TO_SCANCODE.end()) ? it->second : SDL_SCANCODE_UNKNOWN; -} + static auto stringToScancode(const std::string& str) -> SDL_Scancode { + auto it = STRING_TO_SCANCODE.find(str); + return (it != STRING_TO_SCANCODE.end()) ? it->second : SDL_SCANCODE_UNKNOWN; + } -static auto buttonToString(int button) -> std::string { - auto it = BUTTON_TO_STRING.find(button); - return (it != BUTTON_TO_STRING.end()) ? it->second : "UNKNOWN"; -} + static auto buttonToString(int button) -> std::string { + auto it = BUTTON_TO_STRING.find(button); + return (it != BUTTON_TO_STRING.end()) ? it->second : "UNKNOWN"; + } -static auto stringToButton(const std::string& str) -> int { - auto it = STRING_TO_BUTTON.find(str); - return (it != STRING_TO_BUTTON.end()) ? it->second : SDL_GAMEPAD_BUTTON_INVALID; -} + static auto stringToButton(const std::string& str) -> int { + auto it = STRING_TO_BUTTON.find(str); + return (it != STRING_TO_BUTTON.end()) ? it->second : SDL_GAMEPAD_BUTTON_INVALID; + } -// ========== FI FUNCIONS AUXILIARS ========== + // ========== FI FUNCIONS AUXILIARS ========== -// Inicialitzar opciones con valors per defecte de Defaults:: -void init() { + // Inicialitzar opciones con valors per defecte de Defaults:: + void init() { #ifdef _DEBUG - console = true; + console = true; #else - console = false; + console = false; #endif - // Window - window.width = Defaults::Window::WIDTH; - window.height = Defaults::Window::HEIGHT; - window.fullscreen = Defaults::Window::FULLSCREEN; - window.zoom_factor = Defaults::Window::BASE_ZOOM; + // Window + window.width = Defaults::Window::WIDTH; + window.height = Defaults::Window::HEIGHT; + window.fullscreen = Defaults::Window::FULLSCREEN; + window.zoom_factor = Defaults::Window::BASE_ZOOM; - // Physics - physics.rotation_speed = Defaults::Physics::ROTATION_SPEED; - physics.acceleration = Defaults::Physics::ACCELERATION; - physics.max_velocity = Defaults::Physics::MAX_VELOCITY; - physics.friction = Defaults::Physics::FRICTION; - physics.enemy_speed = Defaults::Physics::ENEMY_SPEED; - physics.bullet_speed = Defaults::Physics::BULLET_SPEED; + // Rendering + rendering.vsync = Defaults::Rendering::VSYNC_DEFAULT; - // Gameplay - gameplay.max_enemies = Defaults::Entities::MAX_ORNIS; - gameplay.max_bullets = Defaults::Entities::MAX_BALES; - - // Rendering - rendering.vsync = Defaults::Rendering::VSYNC_DEFAULT; - - // Audio - audio.enabled = Defaults::Audio::ENABLED; - audio.volume = Defaults::Audio::VOLUME; - audio.music.enabled = Defaults::Audio::MUSIC_ENABLED; - audio.music.volume = Defaults::Audio::MUSIC_VOLUME; - audio.sound.enabled = Defaults::Audio::SOUND_ENABLED; - audio.sound.volume = Defaults::Audio::SOUND_VOLUME; - - // Version - version = std::string(Project::VERSION); -} - -// Establir la ruta del file de configuración -void setConfigFile(const std::string& path) { config_file_path = path; } - -// Funciones auxiliars per load seccions del YAML - -// Lee un campo escalar del YAML aplicando un validador; si la clau no -// existe, deja `dest` intacto; si la conversió o la validació fallen, -// asigna `fallback`. Estàtic per quedar dins de la unitat de traducció. -template -static void readField(const fkyaml::node& parent, const char* key, T& dest, - T fallback, Validator&& validate) { - if (!parent.contains(key)) { - return; + // Version + version = std::string(Project::VERSION); } - try { - auto val = parent[key].template get_value(); - dest = validate(val) ? val : fallback; - } catch (...) { - dest = fallback; - } -} -// Variant sin validador: només lectura amb fallback en cas d'error. -template -static void readField(const fkyaml::node& parent, const char* key, T& dest, T fallback) { - if (!parent.contains(key)) { - return; - } - try { - dest = parent[key].template get_value(); - } catch (...) { - dest = fallback; - } -} + // Establir la ruta del file de configuración + void setConfigFile(const std::string& path) { config_file_path = path; } -static void loadWindowConfigFromYaml(const fkyaml::node& yaml) { - if (!yaml.contains("window")) { - return; - } - const auto& win = yaml["window"]; + // Funciones auxiliars per load seccions del YAML - readField(win, "width", window.width, Defaults::Window::WIDTH, - [](int v) { return v >= Defaults::Window::MIN_WIDTH; }); - readField(win, "height", window.height, Defaults::Window::HEIGHT, - [](int v) { return v >= Defaults::Window::MIN_HEIGHT; }); - readField(win, "fullscreen", window.fullscreen, false); - - if (win.contains("zoom_factor")) { - readField(win, "zoom_factor", window.zoom_factor, Defaults::Window::BASE_ZOOM, - [](float v) { return v >= Defaults::Window::MIN_ZOOM && v <= 10.0F; }); - } else { - // Legacy config: infer zoom from width - window.zoom_factor = static_cast(window.width) / Defaults::Window::WIDTH; - window.zoom_factor = std::max(Defaults::Window::MIN_ZOOM, window.zoom_factor); - } -} - -static void loadPhysicsConfigFromYaml(const fkyaml::node& yaml) { - if (!yaml.contains("physics")) { - return; - } - const auto& phys = yaml["physics"]; - constexpr auto POSITIVE = [](float v) { return v > 0.0F; }; - - readField(phys, "rotation_speed", physics.rotation_speed, Defaults::Physics::ROTATION_SPEED, POSITIVE); - readField(phys, "acceleration", physics.acceleration, Defaults::Physics::ACCELERATION, POSITIVE); - readField(phys, "max_velocity", physics.max_velocity, Defaults::Physics::MAX_VELOCITY, POSITIVE); - readField(phys, "friction", physics.friction, Defaults::Physics::FRICTION, POSITIVE); - readField(phys, "enemy_speed", physics.enemy_speed, Defaults::Physics::ENEMY_SPEED, POSITIVE); - readField(phys, "bullet_speed", physics.bullet_speed, Defaults::Physics::BULLET_SPEED, POSITIVE); -} - -static void loadGameplayConfigFromYaml(const fkyaml::node& yaml) { - if (yaml.contains("gameplay")) { - const auto& game = yaml["gameplay"]; - - if (game.contains("max_enemies")) { - try { - auto val = game["max_enemies"].get_value(); - gameplay.max_enemies = - (val > 0 && val <= 50) ? val : Defaults::Entities::MAX_ORNIS; - } catch (...) { - gameplay.max_enemies = Defaults::Entities::MAX_ORNIS; - } + // Lee un campo escalar del YAML aplicando un validador; si la clau no + // existe, deja `dest` intacto; si la conversió o la validació fallen, + // asigna `fallback`. Estàtic per quedar dins de la unitat de traducció. + template + static void readField(const fkyaml::node& parent, const char* key, T& dest, T fallback, Validator&& validate) { + if (!parent.contains(key)) { + return; } + try { + auto val = parent[key].template get_value(); + dest = validate(val) ? val : fallback; + } catch (...) { + dest = fallback; + } + } - if (game.contains("max_bullets")) { - try { - auto val = game["max_bullets"].get_value(); - gameplay.max_bullets = - (val > 0 && val <= 10) ? val : Defaults::Entities::MAX_BALES; - } catch (...) { - gameplay.max_bullets = Defaults::Entities::MAX_BALES; + // Variant sin validador: només lectura amb fallback en cas d'error. + template + static void readField(const fkyaml::node& parent, const char* key, T& dest, T fallback) { + if (!parent.contains(key)) { + return; + } + try { + dest = parent[key].template get_value(); + } catch (...) { + dest = fallback; + } + } + + static void loadWindowConfigFromYaml(const fkyaml::node& yaml) { + if (!yaml.contains("window")) { + return; + } + const auto& win = yaml["window"]; + + readField(win, "width", window.width, Defaults::Window::WIDTH, [](int v) { return v >= Defaults::Window::MIN_WIDTH; }); + readField(win, "height", window.height, Defaults::Window::HEIGHT, [](int v) { return v >= Defaults::Window::MIN_HEIGHT; }); + readField(win, "fullscreen", window.fullscreen, false); + + if (win.contains("zoom_factor")) { + readField(win, "zoom_factor", window.zoom_factor, Defaults::Window::BASE_ZOOM, [](float v) { return v >= Defaults::Window::MIN_ZOOM && v <= 10.0F; }); + } else { + // Legacy config: infer zoom from width + window.zoom_factor = static_cast(window.width) / Defaults::Window::WIDTH; + window.zoom_factor = std::max(Defaults::Window::MIN_ZOOM, window.zoom_factor); + } + } + + static void loadRenderingConfigFromYaml(const fkyaml::node& yaml) { + if (yaml.contains("rendering")) { + const auto& rend = yaml["rendering"]; + + if (rend.contains("vsync")) { + try { + int val = rend["vsync"].get_value(); + // Validar: solo 0 o 1 + rendering.vsync = (val == 0 || val == 1) ? val : Defaults::Rendering::VSYNC_DEFAULT; + } catch (...) { + rendering.vsync = Defaults::Rendering::VSYNC_DEFAULT; + } } } } -} -static void loadRenderingConfigFromYaml(const fkyaml::node& yaml) { - if (yaml.contains("rendering")) { - const auto& rend = yaml["rendering"]; + // Carregar controls del player 1 desde YAML + static void loadPlayer1ControlsFromYaml(const fkyaml::node& yaml) { + if (!yaml.contains("player1")) { + return; + } - if (rend.contains("vsync")) { - try { - int val = rend["vsync"].get_value(); - // Validar: solo 0 o 1 - rendering.vsync = (val == 0 || val == 1) ? val : Defaults::Rendering::VSYNC_DEFAULT; - } catch (...) { - rendering.vsync = Defaults::Rendering::VSYNC_DEFAULT; + const auto& p1 = yaml["player1"]; + + // Carregar controls de teclat + if (p1.contains("keyboard")) { + const auto& kb = p1["keyboard"]; + if (kb.contains("key_left")) { + player1.keyboard.key_left = stringToScancode(kb["key_left"].get_value()); + } + if (kb.contains("key_right")) { + player1.keyboard.key_right = stringToScancode(kb["key_right"].get_value()); + } + if (kb.contains("key_thrust")) { + player1.keyboard.key_thrust = stringToScancode(kb["key_thrust"].get_value()); + } + if (kb.contains("key_shoot")) { + player1.keyboard.key_shoot = stringToScancode(kb["key_shoot"].get_value()); } } - } -} -static void loadAudioMusicSection(const fkyaml::node& aud) { - if (!aud.contains("music")) { - return; - } - const auto& mus = aud["music"]; - constexpr auto UNIT_RANGE = [](float v) { return v >= 0.0F && v <= 1.0F; }; - - readField(mus, "enabled", audio.music.enabled, Defaults::Audio::MUSIC_ENABLED); - readField(mus, "volume", audio.music.volume, Defaults::Audio::MUSIC_VOLUME, UNIT_RANGE); -} - -static void loadAudioSoundSection(const fkyaml::node& aud) { - if (!aud.contains("sound")) { - return; - } - const auto& snd = aud["sound"]; - constexpr auto UNIT_RANGE = [](float v) { return v >= 0.0F && v <= 1.0F; }; - - readField(snd, "enabled", audio.sound.enabled, Defaults::Audio::SOUND_ENABLED); - readField(snd, "volume", audio.sound.volume, Defaults::Audio::SOUND_VOLUME, UNIT_RANGE); -} - -static void loadAudioConfigFromYaml(const fkyaml::node& yaml) { - if (!yaml.contains("audio")) { - return; - } - const auto& aud = yaml["audio"]; - constexpr auto UNIT_RANGE = [](float v) { return v >= 0.0F && v <= 1.0F; }; - - readField(aud, "enabled", audio.enabled, Defaults::Audio::ENABLED); - readField(aud, "volume", audio.volume, Defaults::Audio::VOLUME, UNIT_RANGE); - loadAudioMusicSection(aud); - loadAudioSoundSection(aud); -} - -// Carregar controls del player 1 desde YAML -static void loadPlayer1ControlsFromYaml(const fkyaml::node& yaml) { - if (!yaml.contains("player1")) { - return; - } - - const auto& p1 = yaml["player1"]; - - // Carregar controls de teclat - if (p1.contains("keyboard")) { - const auto& kb = p1["keyboard"]; - if (kb.contains("key_left")) { - player1.keyboard.key_left = stringToScancode(kb["key_left"].get_value()); + // Carregar controls de gamepad + if (p1.contains("gamepad")) { + const auto& gp = p1["gamepad"]; + if (gp.contains("button_left")) { + player1.gamepad.button_left = stringToButton(gp["button_left"].get_value()); + } + if (gp.contains("button_right")) { + player1.gamepad.button_right = stringToButton(gp["button_right"].get_value()); + } + if (gp.contains("button_thrust")) { + player1.gamepad.button_thrust = stringToButton(gp["button_thrust"].get_value()); + } + if (gp.contains("button_shoot")) { + player1.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value()); + } } - if (kb.contains("key_right")) { - player1.keyboard.key_right = stringToScancode(kb["key_right"].get_value()); - } - if (kb.contains("key_thrust")) { - player1.keyboard.key_thrust = stringToScancode(kb["key_thrust"].get_value()); - } - if (kb.contains("key_shoot")) { - player1.keyboard.key_shoot = stringToScancode(kb["key_shoot"].get_value()); + + // Carregar nom del gamepad + if (p1.contains("gamepad_name")) { + player1.gamepad_name = p1["gamepad_name"].get_value(); } } - // Carregar controls de gamepad - if (p1.contains("gamepad")) { - const auto& gp = p1["gamepad"]; - if (gp.contains("button_left")) { - player1.gamepad.button_left = stringToButton(gp["button_left"].get_value()); + // Carregar controls del player 2 desde YAML + static void loadPlayer2ControlsFromYaml(const fkyaml::node& yaml) { + if (!yaml.contains("player2")) { + return; } - if (gp.contains("button_right")) { - player1.gamepad.button_right = stringToButton(gp["button_right"].get_value()); + + const auto& p2 = yaml["player2"]; + + // Carregar controls de teclat + if (p2.contains("keyboard")) { + const auto& kb = p2["keyboard"]; + if (kb.contains("key_left")) { + player2.keyboard.key_left = stringToScancode(kb["key_left"].get_value()); + } + if (kb.contains("key_right")) { + player2.keyboard.key_right = stringToScancode(kb["key_right"].get_value()); + } + if (kb.contains("key_thrust")) { + player2.keyboard.key_thrust = stringToScancode(kb["key_thrust"].get_value()); + } + if (kb.contains("key_shoot")) { + player2.keyboard.key_shoot = stringToScancode(kb["key_shoot"].get_value()); + } } - if (gp.contains("button_thrust")) { - player1.gamepad.button_thrust = stringToButton(gp["button_thrust"].get_value()); + + // Carregar controls de gamepad + if (p2.contains("gamepad")) { + const auto& gp = p2["gamepad"]; + if (gp.contains("button_left")) { + player2.gamepad.button_left = stringToButton(gp["button_left"].get_value()); + } + if (gp.contains("button_right")) { + player2.gamepad.button_right = stringToButton(gp["button_right"].get_value()); + } + if (gp.contains("button_thrust")) { + player2.gamepad.button_thrust = stringToButton(gp["button_thrust"].get_value()); + } + if (gp.contains("button_shoot")) { + player2.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value()); + } } - if (gp.contains("button_shoot")) { - player1.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value()); + + // Carregar nom del gamepad + if (p2.contains("gamepad_name")) { + player2.gamepad_name = p2["gamepad_name"].get_value(); } } - // Carregar nom del gamepad - if (p1.contains("gamepad_name")) { - player1.gamepad_name = p1["gamepad_name"].get_value(); - } -} + // Carregar configuración des del file YAML + auto loadFromFile() -> bool { + const std::string CONFIG_VERSION = std::string(Project::VERSION); -// Carregar controls del player 2 desde YAML -static void loadPlayer2ControlsFromYaml(const fkyaml::node& yaml) { - if (!yaml.contains("player2")) { - return; - } - - const auto& p2 = yaml["player2"]; - - // Carregar controls de teclat - if (p2.contains("keyboard")) { - const auto& kb = p2["keyboard"]; - if (kb.contains("key_left")) { - player2.keyboard.key_left = stringToScancode(kb["key_left"].get_value()); - } - if (kb.contains("key_right")) { - player2.keyboard.key_right = stringToScancode(kb["key_right"].get_value()); - } - if (kb.contains("key_thrust")) { - player2.keyboard.key_thrust = stringToScancode(kb["key_thrust"].get_value()); - } - if (kb.contains("key_shoot")) { - player2.keyboard.key_shoot = stringToScancode(kb["key_shoot"].get_value()); - } - } - - // Carregar controls de gamepad - if (p2.contains("gamepad")) { - const auto& gp = p2["gamepad"]; - if (gp.contains("button_left")) { - player2.gamepad.button_left = stringToButton(gp["button_left"].get_value()); - } - if (gp.contains("button_right")) { - player2.gamepad.button_right = stringToButton(gp["button_right"].get_value()); - } - if (gp.contains("button_thrust")) { - player2.gamepad.button_thrust = stringToButton(gp["button_thrust"].get_value()); - } - if (gp.contains("button_shoot")) { - player2.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value()); - } - } - - // Carregar nom del gamepad - if (p2.contains("gamepad_name")) { - player2.gamepad_name = p2["gamepad_name"].get_value(); - } -} - -// Carregar configuración des del file YAML -auto loadFromFile() -> bool { - const std::string CONFIG_VERSION = std::string(Project::VERSION); - - std::ifstream file(config_file_path); - if (!file.good()) { - // El file no existeix → crear-ne un de nuevo con valors per defecte - if (console) { - std::cout << "Archivo de config no trobat, creant-ne un de nuevo: " - << config_file_path << '\n'; - } - saveToFile(); - return true; - } - - // Llegir todo el contingut del file - std::string content((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - file.close(); - - try { - // Parsejar YAML - auto yaml = fkyaml::node::deserialize(content); - - // Validar versión - if (yaml.contains("version")) { - version = yaml["version"].get_value(); - } - - if (CONFIG_VERSION != version) { - // Versión incompatible → regenerar config + std::ifstream file(config_file_path); + if (!file.good()) { + // El file no existeix → crear-ne un de nuevo con valors per defecte if (console) { - std::cout << "Versión de config incompatible (esperada: " - << CONFIG_VERSION << ", trobada: " << version - << "), regenerant config\n"; + std::cout << "Archivo de config no trobat, creant-ne un de nuevo: " + << config_file_path << '\n'; + } + saveToFile(); + return true; + } + + // Llegir todo el contingut del file + std::string content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + file.close(); + + try { + // Parsejar YAML + auto yaml = fkyaml::node::deserialize(content); + + // Validar versión + if (yaml.contains("version")) { + version = yaml["version"].get_value(); + } + + if (CONFIG_VERSION != version) { + // Versión incompatible → regenerar config + if (console) { + std::cout << "Versión de config incompatible (esperada: " + << CONFIG_VERSION << ", trobada: " << version + << "), regenerant config\n"; + } + init(); + saveToFile(); + return true; + } + + // Carregar seccions + loadWindowConfigFromYaml(yaml); + loadRenderingConfigFromYaml(yaml); + loadPlayer1ControlsFromYaml(yaml); + loadPlayer2ControlsFromYaml(yaml); + + if (console) { + std::cout << "Config carregada correctament desde: " << config_file_path + << '\n'; + } + + return true; + + } catch (const fkyaml::exception& e) { + // Error de parsejat YAML → regenerar config + if (console) { + std::cerr << "Error parsejant YAML: " << e.what() << '\n'; + std::cerr << "Creant config nuevo con valors per defecte\n"; } init(); saveToFile(); return true; } + } - // Carregar seccions - loadWindowConfigFromYaml(yaml); - loadPhysicsConfigFromYaml(yaml); - loadGameplayConfigFromYaml(yaml); - loadRenderingConfigFromYaml(yaml); - loadAudioConfigFromYaml(yaml); - loadPlayer1ControlsFromYaml(yaml); - loadPlayer2ControlsFromYaml(yaml); + // Guardar controls del player 1 a YAML + static void savePlayer1ControlsToYaml(std::ofstream& file) { + file << "# CONTROLS JUGADOR 1\n"; + file << "player1:\n"; + file << " keyboard:\n"; + file << " key_left: " << scancodeToString(player1.keyboard.key_left) << "\n"; + file << " key_right: " << scancodeToString(player1.keyboard.key_right) << "\n"; + file << " key_thrust: " << scancodeToString(player1.keyboard.key_thrust) << "\n"; + file << " key_shoot: " << scancodeToString(player1.keyboard.key_shoot) << "\n"; + file << " gamepad:\n"; + file << " button_left: " << buttonToString(player1.gamepad.button_left) << "\n"; + file << " button_right: " << buttonToString(player1.gamepad.button_right) << "\n"; + file << " button_thrust: " << buttonToString(player1.gamepad.button_thrust) << "\n"; + file << " button_shoot: " << buttonToString(player1.gamepad.button_shoot) << "\n"; + file << " gamepad_name: \"" << player1.gamepad_name << "\" # Buit = primer disponible\n\n"; + } + + // Guardar controls del player 2 a YAML + static void savePlayer2ControlsToYaml(std::ofstream& file) { + file << "# CONTROLS JUGADOR 2\n"; + file << "player2:\n"; + file << " keyboard:\n"; + file << " key_left: " << scancodeToString(player2.keyboard.key_left) << "\n"; + file << " key_right: " << scancodeToString(player2.keyboard.key_right) << "\n"; + file << " key_thrust: " << scancodeToString(player2.keyboard.key_thrust) << "\n"; + file << " key_shoot: " << scancodeToString(player2.keyboard.key_shoot) << "\n"; + file << " gamepad:\n"; + file << " button_left: " << buttonToString(player2.gamepad.button_left) << "\n"; + file << " button_right: " << buttonToString(player2.gamepad.button_right) << "\n"; + file << " button_thrust: " << buttonToString(player2.gamepad.button_thrust) << "\n"; + file << " button_shoot: " << buttonToString(player2.gamepad.button_shoot) << "\n"; + file << " gamepad_name: \"" << player2.gamepad_name << "\" # Buit = segon disponible\n\n"; + } + + // Guardar configuración al file YAML + auto saveToFile() -> bool { + std::ofstream file(config_file_path); + if (!file.is_open()) { + if (console) { + std::cerr << "No s'ha pogut obrir el file de config per escriure: " + << config_file_path << '\n'; + } + return false; + } + + // Escriure manualment per controlar format i comentaris + file << "# Orni Attack - Archivo de Configuración\n"; + file << "# Auto-generat. Les edicions manuals es preserven si són " + "vàlides.\n\n"; + + file << "version: \"" << Project::VERSION << "\"\n\n"; + + file << "# FINESTRA\n"; + file << "window:\n"; + file << " width: " << window.width << " # Calculated from zoom_factor\n"; + file << " height: " << window.height << " # Calculated from zoom_factor\n"; + file << " fullscreen: " << (window.fullscreen ? "true" : "false") << "\n"; + file << " zoom_factor: " << window.zoom_factor << " # 0.5x-max (0.1 increments)\n\n"; + + file << "# RENDERITZACIÓ\n"; + file << "rendering:\n"; + file << " vsync: " << rendering.vsync << " # 0=disabled, 1=enabled\n\n"; + + // Guardar controls de jugadors + savePlayer1ControlsToYaml(file); + savePlayer2ControlsToYaml(file); + + file.close(); if (console) { - std::cout << "Config carregada correctament desde: " << config_file_path - << '\n'; + std::cout << "Config guardada a: " << config_file_path << '\n'; } return true; - - } catch (const fkyaml::exception& e) { - // Error de parsejat YAML → regenerar config - if (console) { - std::cerr << "Error parsejant YAML: " << e.what() << '\n'; - std::cerr << "Creant config nuevo con valors per defecte\n"; - } - init(); - saveToFile(); - return true; } -} - -// Guardar controls del player 1 a YAML -static void savePlayer1ControlsToYaml(std::ofstream& file) { - file << "# CONTROLS JUGADOR 1\n"; - file << "player1:\n"; - file << " keyboard:\n"; - file << " key_left: " << scancodeToString(player1.keyboard.key_left) << "\n"; - file << " key_right: " << scancodeToString(player1.keyboard.key_right) << "\n"; - file << " key_thrust: " << scancodeToString(player1.keyboard.key_thrust) << "\n"; - file << " key_shoot: " << scancodeToString(player1.keyboard.key_shoot) << "\n"; - file << " gamepad:\n"; - file << " button_left: " << buttonToString(player1.gamepad.button_left) << "\n"; - file << " button_right: " << buttonToString(player1.gamepad.button_right) << "\n"; - file << " button_thrust: " << buttonToString(player1.gamepad.button_thrust) << "\n"; - file << " button_shoot: " << buttonToString(player1.gamepad.button_shoot) << "\n"; - file << " gamepad_name: \"" << player1.gamepad_name << "\" # Buit = primer disponible\n\n"; -} - -// Guardar controls del player 2 a YAML -static void savePlayer2ControlsToYaml(std::ofstream& file) { - file << "# CONTROLS JUGADOR 2\n"; - file << "player2:\n"; - file << " keyboard:\n"; - file << " key_left: " << scancodeToString(player2.keyboard.key_left) << "\n"; - file << " key_right: " << scancodeToString(player2.keyboard.key_right) << "\n"; - file << " key_thrust: " << scancodeToString(player2.keyboard.key_thrust) << "\n"; - file << " key_shoot: " << scancodeToString(player2.keyboard.key_shoot) << "\n"; - file << " gamepad:\n"; - file << " button_left: " << buttonToString(player2.gamepad.button_left) << "\n"; - file << " button_right: " << buttonToString(player2.gamepad.button_right) << "\n"; - file << " button_thrust: " << buttonToString(player2.gamepad.button_thrust) << "\n"; - file << " button_shoot: " << buttonToString(player2.gamepad.button_shoot) << "\n"; - file << " gamepad_name: \"" << player2.gamepad_name << "\" # Buit = segon disponible\n\n"; -} - -// Guardar configuración al file YAML -auto saveToFile() -> bool { - std::ofstream file(config_file_path); - if (!file.is_open()) { - if (console) { - std::cerr << "No s'ha pogut obrir el file de config per escriure: " - << config_file_path << '\n'; - } - return false; - } - - // Escriure manualment per controlar format i comentaris - file << "# Orni Attack - Archivo de Configuración\n"; - file << "# Auto-generat. Les edicions manuals es preserven si són " - "vàlides.\n\n"; - - file << "version: \"" << Project::VERSION << "\"\n\n"; - - file << "# FINESTRA\n"; - file << "window:\n"; - file << " width: " << window.width << " # Calculated from zoom_factor\n"; - file << " height: " << window.height << " # Calculated from zoom_factor\n"; - file << " fullscreen: " << (window.fullscreen ? "true" : "false") << "\n"; - file << " zoom_factor: " << window.zoom_factor << " # 0.5x-max (0.1 increments)\n\n"; - - file << "# FÍSICA (todos los valors en px/s, rad/s, etc.)\n"; - file << "physics:\n"; - file << " rotation_speed: " << physics.rotation_speed << " # rad/s\n"; - file << " acceleration: " << physics.acceleration << " # px/s²\n"; - file << " max_velocity: " << physics.max_velocity << " # px/s\n"; - file << " friction: " << physics.friction << " # px/s²\n"; - file << " enemy_speed: " << physics.enemy_speed - << " # unitats/frame\n"; - file << " bullet_speed: " << physics.bullet_speed - << " # unitats/frame\n\n"; - - file << "# GAMEPLAY\n"; - file << "gameplay:\n"; - file << " max_enemies: " << gameplay.max_enemies << "\n"; - file << " max_bullets: " << gameplay.max_bullets << "\n\n"; - - file << "# RENDERITZACIÓ\n"; - file << "rendering:\n"; - file << " vsync: " << rendering.vsync << " # 0=disabled, 1=enabled\n\n"; - - file << "# AUDIO\n"; - file << "audio:\n"; - file << " enabled: " << (audio.enabled ? "true" : "false") << "\n"; - file << " volume: " << audio.volume << " # 0.0 to 1.0\n"; - file << " music:\n"; - file << " enabled: " << (audio.music.enabled ? "true" : "false") << "\n"; - file << " volume: " << audio.music.volume << " # 0.0 to 1.0\n"; - file << " sound:\n"; - file << " enabled: " << (audio.sound.enabled ? "true" : "false") << "\n"; - file << " volume: " << audio.sound.volume << " # 0.0 to 1.0\n\n"; - - // Guardar controls de jugadors - savePlayer1ControlsToYaml(file); - savePlayer2ControlsToYaml(file); - - file.close(); - - if (console) { - std::cout << "Config guardada a: " << config_file_path << '\n'; - } - - return true; -} } // namespace Options diff --git a/source/game/options.hpp b/source/game/options.hpp index 4fccd64..e4b0f01 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -6,116 +6,82 @@ namespace Options { -// Estructures de configuración + // Estructures de configuración -struct Window { + struct Window { int width{1280}; int height{720}; bool fullscreen{false}; float zoom_factor{1.0F}; // Zoom level (0.5x to max_zoom) -}; + }; -struct Physics { - float rotation_speed{3.14F}; // rad/s - float acceleration{400.0F}; // px/s² - float max_velocity{120.0F}; // px/s - float friction{20.0F}; // px/s² - float enemy_speed{2.0F}; // unitats/frame - float bullet_speed{6.0F}; // unitats/frame -}; - -struct Gameplay { - int max_enemies{15}; - int max_bullets{3}; -}; - -struct Rendering { + struct Rendering { int vsync{1}; // 0=disabled, 1=enabled -}; + }; -struct Music { - bool enabled{true}; - float volume{0.8F}; -}; + // Controles de jugadors -struct Sound { - bool enabled{true}; - float volume{1.0F}; -}; - -struct Audio { - Music music{}; - Sound sound{}; - bool enabled{true}; - float volume{1.0F}; -}; - -// Controles de jugadors - -struct KeyboardControls { + struct KeyboardControls { SDL_Scancode key_left{SDL_SCANCODE_LEFT}; SDL_Scancode key_right{SDL_SCANCODE_RIGHT}; SDL_Scancode key_thrust{SDL_SCANCODE_UP}; SDL_Scancode key_shoot{SDL_SCANCODE_SPACE}; SDL_Scancode key_start{SDL_SCANCODE_1}; -}; + }; -struct GamepadControls { + struct GamepadControls { int button_left{SDL_GAMEPAD_BUTTON_DPAD_LEFT}; int button_right{SDL_GAMEPAD_BUTTON_DPAD_RIGHT}; int button_thrust{SDL_GAMEPAD_BUTTON_WEST}; // X button int button_shoot{SDL_GAMEPAD_BUTTON_SOUTH}; // A button -}; + }; -struct PlayerControls { + struct PlayerControls { KeyboardControls keyboard{}; GamepadControls gamepad{}; std::string gamepad_name; // Buit = auto-assignar per índex -}; + }; -// Variables globals (inline per evitar ODR violations) + // Variables globals (inline per evitar ODR violations) -inline std::string version{}; // Versión del config per validació -inline bool console{false}; // Eixida de debug -inline Window window{}; -inline Physics physics{}; -inline Gameplay gameplay{}; -inline Rendering rendering{}; -inline Audio audio{}; + inline std::string version{}; // Versión del config per validació + inline bool console{false}; // Eixida de debug + inline Window window{}; + inline Rendering rendering{}; -// Controles per player -inline PlayerControls player1{ - .keyboard = - {.key_left = SDL_SCANCODE_LEFT, - .key_right = SDL_SCANCODE_RIGHT, - .key_thrust = SDL_SCANCODE_UP, - .key_shoot = SDL_SCANCODE_SPACE, - .key_start = SDL_SCANCODE_1}, - .gamepad_name = "" // Primer gamepad disponible -}; + // Controles per player + inline PlayerControls player1{ + .keyboard = + {.key_left = SDL_SCANCODE_LEFT, + .key_right = SDL_SCANCODE_RIGHT, + .key_thrust = SDL_SCANCODE_UP, + .key_shoot = SDL_SCANCODE_SPACE, + .key_start = SDL_SCANCODE_1}, + .gamepad_name = "" // Primer gamepad disponible + }; -inline PlayerControls player2{ - .keyboard = - {.key_left = SDL_SCANCODE_A, - .key_right = SDL_SCANCODE_D, - .key_thrust = SDL_SCANCODE_W, - .key_shoot = SDL_SCANCODE_LSHIFT, - .key_start = SDL_SCANCODE_2}, - .gamepad_name = "" // Segon gamepad disponible -}; + inline PlayerControls player2{ + .keyboard = + {.key_left = SDL_SCANCODE_A, + .key_right = SDL_SCANCODE_D, + .key_thrust = SDL_SCANCODE_W, + .key_shoot = SDL_SCANCODE_LSHIFT, + .key_start = SDL_SCANCODE_2}, + .gamepad_name = "" // Segon gamepad disponible + }; -// Per compatibilitat con pollo (no utilitzat en orni, pero necessari per Input) -inline KeyboardControls keyboard_controls{}; -inline GamepadControls gamepad_controls{}; + // Per compatibilitat con pollo (no utilitzat en orni, pero necessari per Input) + inline KeyboardControls keyboard_controls{}; + inline GamepadControls gamepad_controls{}; -inline std::string config_file_path{}; // Establert per setConfigFile() + inline std::string config_file_path{}; // Establert per setConfigFile() -// Funciones públiques + // Funciones públiques -void init(); // Inicialitzar con valors per defecte -void setConfigFile( - const std::string& path); // Establir ruta del file de config -auto loadFromFile() -> bool; // Carregar config YAML -auto saveToFile() -> bool; // Guardar config YAML + void init(); // Inicialitzar con valors per defecte + void setConfigFile( + const std::string& path); // Establir ruta del file de config + auto loadFromFile() -> bool; // Carregar config YAML + auto saveToFile() -> bool; // Guardar config YAML } // namespace Options From a0c1c8342fbc91068cd2f81eaf74cdb63a02de72 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 18:18:12 +0200 Subject: [PATCH 09/18] refactor: esborrar aliases morts de game/constants.hpp (#24) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Esborrats: - Constants::MARGIN_LEFT/RIGHT/TOP/BOTTOM (zero callers; tots els call-sites llegeixen Defaults::Zones::PLAYAREA directament). - Constants::VELOCITAT i VELOCITAT_MAX (zero callers; eren els últims lectors de Defaults::Physics::ENEMY_SPEED/BULLET_SPEED). Es mantenen MAX_ORNIS, MAX_BALES (sí usats a game_scene.hpp) i PI, més els helpers de zona. Habilita el hallazgo #25 (eliminar Defaults::Physics::ENEMY_SPEED / BULLET_SPEED / VELOCITY_SCALE) — ja sense lectors. Hallazgo #24 de CODE_REVIEW.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/game/constants.hpp | 79 ++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 46 deletions(-) diff --git a/source/game/constants.hpp b/source/game/constants.hpp index 1a52e44..0e7fda0 100644 --- a/source/game/constants.hpp +++ b/source/game/constants.hpp @@ -2,58 +2,45 @@ #include "core/defaults.hpp" -// Aliases para backward compatibility con codi existent -// Permet usar Constants::MARGIN_LEFT en lloc de Defaults::Game::MARGIN_LEFT +// Aliases utilitzats per simplificar lectures freqüents de Defaults:: namespace Constants { -// Márgenes de l'àrea de juego (derivats de Defaults::Zones::GAME) -constexpr int MARGIN_LEFT = static_cast(Defaults::Zones::PLAYAREA.x); -constexpr int MARGIN_RIGHT = - static_cast(Defaults::Zones::PLAYAREA.x + Defaults::Zones::PLAYAREA.w); -constexpr int MARGIN_TOP = static_cast(Defaults::Zones::PLAYAREA.y); -constexpr int MARGIN_BOTTOM = - static_cast(Defaults::Zones::PLAYAREA.y + Defaults::Zones::PLAYAREA.h); + // Límits de objectes + constexpr int MAX_ORNIS = Defaults::Entities::MAX_ORNIS; + constexpr int MAX_BALES = Defaults::Entities::MAX_BALES; -// Límits de objectes -constexpr int MAX_ORNIS = Defaults::Entities::MAX_ORNIS; -constexpr int MAX_BALES = Defaults::Entities::MAX_BALES; + // Matemàtiques + constexpr float PI = Defaults::Math::PI; -// Velocitats (valors legacy del codi Pascal) -constexpr int VELOCITAT = static_cast(Defaults::Physics::ENEMY_SPEED); -constexpr int VELOCITAT_MAX = static_cast(Defaults::Physics::BULLET_SPEED); + // Helpers per comprovar límits de zona + inline auto isInPlayArea(float x, float y) -> bool { + const SDL_FPoint POINT = {x, y}; + return SDL_PointInRectFloat(&POINT, &Defaults::Zones::PLAYAREA); + } -// Matemàtiques -constexpr float PI = Defaults::Math::PI; + inline void getPlayAreaBounds(float& min_x, float& max_x, float& min_y, float& max_y) { + const auto& zona = Defaults::Zones::PLAYAREA; + min_x = zona.x; + max_x = zona.x + zona.w; + min_y = zona.y; + max_y = zona.y + zona.h; + } -// Helpers per comprovar límits de zona -inline auto isInPlayArea(float x, float y) -> bool { - const SDL_FPoint POINT = {x, y}; - return SDL_PointInRectFloat(&POINT, &Defaults::Zones::PLAYAREA); -} + // Obtenir límits segurs (compensant radi de l'entidad) + inline void getSafePlayAreaBounds(float radi, float& min_x, float& max_x, float& min_y, float& max_y) { + const auto& zona = Defaults::Zones::PLAYAREA; + constexpr float MARGE_SEGURETAT = 10.0F; // Safety margin -inline void getPlayAreaBounds(float& min_x, float& max_x, float& min_y, float& max_y) { - const auto& zona = Defaults::Zones::PLAYAREA; - min_x = zona.x; - max_x = zona.x + zona.w; - min_y = zona.y; - max_y = zona.y + zona.h; -} + min_x = zona.x + radi + MARGE_SEGURETAT; + max_x = zona.x + zona.w - radi - MARGE_SEGURETAT; + min_y = zona.y + radi + MARGE_SEGURETAT; + max_y = zona.y + zona.h - radi - MARGE_SEGURETAT; + } -// Obtenir límits segurs (compensant radi de l'entidad) -inline void getSafePlayAreaBounds(float radi, float& min_x, float& max_x, float& min_y, float& max_y) { - const auto& zona = Defaults::Zones::PLAYAREA; - constexpr float MARGE_SEGURETAT = 10.0F; // Safety margin - - min_x = zona.x + radi + MARGE_SEGURETAT; - max_x = zona.x + zona.w - radi - MARGE_SEGURETAT; - min_y = zona.y + radi + MARGE_SEGURETAT; - max_y = zona.y + zona.h - radi - MARGE_SEGURETAT; -} - -// Obtenir centro de l'àrea de juego -inline void getPlayAreaCenter(float& centre_x, float& centre_y) { - const auto& zona = Defaults::Zones::PLAYAREA; - centre_x = zona.x + (zona.w / 2.0F); - centre_y = zona.y + (zona.h / 2.0F); -} + // Obtenir centro de l'àrea de juego + inline void getPlayAreaCenter(float& centre_x, float& centre_y) { + const auto& zona = Defaults::Zones::PLAYAREA; + centre_x = zona.x + (zona.w / 2.0F); + centre_y = zona.y + (zona.h / 2.0F); + } } // namespace Constants From f7770174607b4f3152a09ca5a4cef9b469d27c12 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 18:27:38 +0200 Subject: [PATCH 10/18] refactor: esborrar Defaults::Physics::{ENEMY,BULLET}_SPEED i VELOCITY_SCALE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Constants legacy heretades del Pascal, en unitats/frame, que la migració a SDL3 va deixar sense ús real: - ENEMY_SPEED i BULLET_SPEED només es llegien des d'Options::physics (esborrat al #21) i des de Constants::VELOCITAT/VELOCITAT_MAX (esborrat al #24). Ara amb zero callers. - VELOCITY_SCALE no tenia callers (les velocitats efectives es calculen a Bullet::BULLET_SPEED = 140 px/s i a Defaults::Enemies::{Pentagon,Cuadrado,Molinillo}::VELOCITAT). S'ajusta el comentari del namespace per reflectir que ara conté només la física del control de la nau. Hallazgo #25 de CODE_REVIEW.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/defaults.hpp | 1039 +++++++++++++++++++------------------- 1 file changed, 518 insertions(+), 521 deletions(-) diff --git a/source/core/defaults.hpp b/source/core/defaults.hpp index 1a2be14..9678341 100644 --- a/source/core/defaults.hpp +++ b/source/core/defaults.hpp @@ -5,526 +5,523 @@ #include namespace Defaults { -// Configuración de ventana -namespace Window { -constexpr int WIDTH = 1280; -constexpr int HEIGHT = 720; -constexpr int MIN_WIDTH = 640; // Mínimo: mitad del baseline (16:9) -constexpr int MIN_HEIGHT = 360; -// Zoom system -constexpr float BASE_ZOOM = 1.0F; // 1280x720 baseline (16:9) -constexpr float MIN_ZOOM = 0.5F; // 640x360 minimum -constexpr float ZOOM_INCREMENT = 0.1F; // 10% steps (F1/F2) -constexpr bool FULLSCREEN = true; // Pantalla completa activada por defecto -} // namespace Window - -// Dimensiones base del juego (coordenadas lógicas, 16:9) -namespace Game { -constexpr int WIDTH = 1280; -constexpr int HEIGHT = 720; -} // namespace Game - -// Zones del juego (SDL_FRect con cálculos automáticos basat en porcentajes) -namespace Zones { -// --- CONFIGURACIÓ DE PORCENTATGES --- -// Todas las zones definides como a porcentajes de Game::WIDTH (640) i Game::HEIGHT (480) - -// Percentatges de height (divisió vertical) -constexpr float SCOREBOARD_TOP_HEIGHT_PERCENT = 0.02F; // 10% superior -constexpr float MAIN_PLAYAREA_HEIGHT_PERCENT = 0.88F; // 80% central -constexpr float SCOREBOARD_BOTTOM_HEIGHT_PERCENT = 0.10F; // 10% inferior - -// Padding horizontal para PLAYAREA (dentro de MAIN_PLAYAREA) -constexpr float PLAYAREA_PADDING_HORIZONTAL_PERCENT = 0.015F; // 5% a cada costat - -// --- CÀLCULS AUTOMÀTICS DE PÍXELS --- -// Cálculos automáticos a partir dels porcentajes - -// Alçades -constexpr float SCOREBOARD_TOP_H = Game::HEIGHT * SCOREBOARD_TOP_HEIGHT_PERCENT; -constexpr float MAIN_PLAYAREA_H = Game::HEIGHT * MAIN_PLAYAREA_HEIGHT_PERCENT; -constexpr float SCOREBOARD_BOTTOM_H = Game::HEIGHT * SCOREBOARD_BOTTOM_HEIGHT_PERCENT; - -// Posicions Y -constexpr float SCOREBOARD_TOP_Y = 0.0F; -constexpr float MAIN_PLAYAREA_Y = SCOREBOARD_TOP_H; -constexpr float SCOREBOARD_BOTTOM_Y = MAIN_PLAYAREA_Y + MAIN_PLAYAREA_H; - -// Padding horizontal de PLAYAREA -constexpr float PLAYAREA_PADDING_H = Game::WIDTH * PLAYAREA_PADDING_HORIZONTAL_PERCENT; - -// --- ZONES FINALS (SDL_FRect) --- - -// Marcador superior (reservado para futuro uso) -// Ocupa el 2% superior -constexpr SDL_FRect SCOREBOARD_TOP = { - 0.0F, // x = 0.0 - SCOREBOARD_TOP_Y, // y = 0.0 - static_cast(Game::WIDTH), // ancho completo - SCOREBOARD_TOP_H // alto -}; - -// Área de juego principal (contenedor del 80% central, sin padding) -// Ocupa el 88% central, ancho completo -constexpr SDL_FRect MAIN_PLAYAREA = { - 0.0F, // x = 0.0 - MAIN_PLAYAREA_Y, // debajo del scoreboard superior - static_cast(Game::WIDTH), // ancho completo - MAIN_PLAYAREA_H // alto -}; - -// Zona de juego real (con padding horizontal del 5%) -// Ocupa: dentro de MAIN_PLAYAREA, con márgenes laterales -// Se utiliza para límites del juego, colisiones, spawn -constexpr SDL_FRect PLAYAREA = { - PLAYAREA_PADDING_H, // padding horizontal - MAIN_PLAYAREA_Y, // debajo del scoreboard superior (igual que MAIN_PLAYAREA) - Game::WIDTH - (2.0F * PLAYAREA_PADDING_H), // ancho con padding - MAIN_PLAYAREA_H // alto (igual que MAIN_PLAYAREA) -}; - -// Marcador inferior (marcador actual) -// Ocupa el 10% inferior -constexpr SDL_FRect SCOREBOARD = { - 0.0F, // x = 0.0 - SCOREBOARD_BOTTOM_Y, // fondo - static_cast(Game::WIDTH), // ancho completo - SCOREBOARD_BOTTOM_H // alto -}; - -// Padding horizontal del marcador (para alinear zonas izquierda/derecha con PLAYAREA) -constexpr float SCOREBOARD_PADDING_H = 0.0F; // Game::WIDTH * 0.015f; -} // namespace Zones - -// Objetos del juego -namespace Entities { -constexpr int MAX_ORNIS = 15; -constexpr int MAX_BALES = 3; - -constexpr float SHIP_RADIUS = 12.0F; -constexpr float ENEMY_RADIUS = 20.0F; -constexpr float BULLET_RADIUS = 3.0F; -} // namespace Entities - -// Paleta semántica por tipo de entidad. Si una entity declara color, lo -// pasa al pipeline con alpha=255 (sentinela "color válido"); si no, se -// usa el color global del oscilador (g_current_line_color). -namespace Palette { -constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro -constexpr SDL_Color BULLET = {.r = 120, .g = 255, .b = 140, .a = 255}; // Verde laser -constexpr SDL_Color PENTAGON = {.r = 120, .g = 170, .b = 255, .a = 255}; // Azul "esquivador" -constexpr SDL_Color QUADRAT = {.r = 255, .g = 110, .b = 110, .a = 255}; // Rojo "tank" -constexpr SDL_Color MOLINILLO = {.r = 255, .g = 130, .b = 255, .a = 255}; // Magenta agresivo -} // namespace Palette - -// Ship (nave del player) -namespace Ship { -// Invulnerabilidad post-respawn -constexpr float INVULNERABILITY_DURATION = 3.0F; // Segundos de invulnerabilidad - -// Parpadeo visual durante invulnerabilidad -constexpr float BLINK_VISIBLE_TIME = 0.1F; // Tiempo visible (segundos) -constexpr float BLINK_INVISIBLE_TIME = 0.1F; // Tiempo invisible (segundos) -// Frecuencia total: 0.2s/ciclo = 5 Hz (~15 parpadeos en 3s) -} // namespace Ship - -// Game rules (lives, respawn, game over) -namespace Game { -constexpr int STARTING_LIVES = 3; // Initial lives -constexpr float DEATH_DURATION = 3.0F; // Seconds of death animation -constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over -constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 0.80F; // 80% hitbox (generous) -constexpr float COLLISION_BULLET_ENEMY_AMPLIFIER = 1.15F; // 115% hitbox (generous) - -// Friendly fire system -constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire -constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%) -constexpr float BULLET_GRACE_PERIOD = 0.2F; // Inmunidad post-disparo (s) - -// Transición LEVEL_START (mensajes aleatorios PRE-level) -constexpr float LEVEL_START_DURATION = 3.0F; // Duración total -constexpr float LEVEL_START_TYPING_RATIO = 0.3F; // 30% escribiendo, 70% mostrando - -// Transición LEVEL_COMPLETED (mensaje "GOOD JOB COMMANDER!") -constexpr float LEVEL_COMPLETED_DURATION = 3.0F; // Duración total -constexpr float LEVEL_COMPLETED_TYPING_RATIO = 0.0F; // 0.0 = sin typewriter (directo) - -// Transición INIT_HUD (animación inicial del HUD) -constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado - -// Ratios de animación (inicio y fin como porcentajes del tiempo total) -// RECT (rectángulo de márgenes) -constexpr float INIT_HUD_RECT_RATIO_INIT = 0.30F; -constexpr float INIT_HUD_RECT_RATIO_END = 0.85F; - -// SCORE (marcador de puntuación) -constexpr float INIT_HUD_SCORE_RATIO_INIT = 0.60F; -constexpr float INIT_HUD_SCORE_RATIO_END = 0.90F; - -// SHIP1 (nave player 1) -constexpr float INIT_HUD_SHIP1_RATIO_INIT = 0.0F; -constexpr float INIT_HUD_SHIP1_RATIO_END = 1.0F; - -// SHIP2 (nave player 2) -constexpr float INIT_HUD_SHIP2_RATIO_INIT = 0.20F; -constexpr float INIT_HUD_SHIP2_RATIO_END = 1.0F; - -// Posición inicial de la nave en INIT_HUD (75% de altura de zona de juego) -constexpr float INIT_HUD_SHIP_START_Y_RATIO = 0.75F; // 75% desde el top de PLAYAREA - -// Spawn positions (distribución horizontal para 2 jugadores) -constexpr float P1_SPAWN_X_RATIO = 0.33F; // 33% desde izquierda -constexpr float P2_SPAWN_X_RATIO = 0.67F; // 67% desde izquierda -constexpr float SPAWN_Y_RATIO = 0.75F; // 75% desde arriba - -// Continue system behavior -constexpr int CONTINUE_COUNT_START = 9; // Countdown starts at 9 -constexpr float CONTINUE_TICK_DURATION = 1.0F; // Seconds per countdown tick -constexpr int MAX_CONTINUES = 3; // Maximum continues per game -constexpr bool INFINITE_CONTINUES = false; // If true, unlimited continues - -// Continue screen visual configuration -namespace ContinueScreen { -// "CONTINUE" text -constexpr float CONTINUE_TEXT_SCALE = 2.0F; // Text size -constexpr float CONTINUE_TEXT_Y_RATIO = 0.30F; // 35% from top of PLAYAREA - -// Countdown number (9, 8, 7...) -constexpr float COUNTER_TEXT_SCALE = 4.0F; // Text size (large) -constexpr float COUNTER_TEXT_Y_RATIO = 0.50F; // 50% from top of PLAYAREA - -// "CONTINUES LEFT: X" text -constexpr float INFO_TEXT_SCALE = 0.7F; // Text size (small) -constexpr float INFO_TEXT_Y_RATIO = 0.75F; // 65% from top of PLAYAREA -} // namespace ContinueScreen - -// Game Over screen visual configuration -namespace GameOverScreen { -constexpr float TEXT_SCALE = 2.0F; // "GAME OVER" text size -constexpr float TEXT_SPACING = 4.0F; // Character spacing -} // namespace GameOverScreen - -// Stage message configuration (LEVEL_START, LEVEL_COMPLETED) -constexpr float STAGE_MESSAGE_Y_RATIO = 0.25F; // 25% from top of PLAYAREA -constexpr float STAGE_MESSAGE_MAX_WIDTH_RATIO = 0.9F; // 90% of PLAYAREA width -} // namespace Game - -// Física (valores actuales del juego, sincronizados con joc_asteroides.cpp) -namespace Physics { -constexpr float ROTATION_SPEED = 3.14F; // rad/s (~180°/s) -constexpr float ACCELERATION = 400.0F; // px/s² -constexpr float MAX_VELOCITY = 120.0F; // px/s -constexpr float FRICTION = 20.0F; // px/s² -constexpr float ENEMY_SPEED = 2.0F; // unidades/frame -constexpr float BULLET_SPEED = 6.0F; // unidades/frame -constexpr float VELOCITY_SCALE = 20.0F; // factor conversión frame→tiempo - -// Explosions (debris physics) -namespace Debris { -constexpr float VELOCITAT_BASE = 80.0F; // Velocidad inicial (px/s) -constexpr float VARIACIO_VELOCITAT = 40.0F; // ±variació aleatòria (px/s) -constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²) -constexpr float ROTACIO_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s) -constexpr float ROTACIO_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s) -constexpr float TEMPS_VIDA = 2.0F; // Duració màxima (segons) - enemy/bullet debris -constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris lifetime (matches DEATH_DURATION) -constexpr float SHRINK_RATE = 0.5F; // Reducció de mida (factor/s) - -// Herència de velocity angular (trayectorias curvas) -constexpr float FACTOR_HERENCIA_MIN = 0.7F; // Mínimo 70% del drotacio heredat -constexpr float FACTOR_HERENCIA_MAX = 1.0F; // Màxim 100% del drotacio heredat -constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²) - -// Angular velocity sin for trajectory inheritance -// Excess above this threshold is converted to tangential linear velocity -// Prevents "vortex trap" problem with high-rotation enemies -constexpr float VELOCITAT_ROT_MAX = 1.5F; // rad/s (~86°/s) -} // namespace Debris -} // namespace Physics - -// Matemáticas -namespace Math { -constexpr float PI = std::numbers::pi_v; -} // namespace Math - -// La antigua oscilación CPU (namespace Color) se ha migrado al shader de -// postpro. Los parámetros de flicker / background pulse viven ahora en -// data/config/postfx.yaml y se aplican en shaders/postfx.frag.glsl. - -// Brillantor (control de intensitat per cada type de entidad) -namespace Brightness { -// Brillantor estàtica per entidades de juego (0.0-1.0) -constexpr float NAU = 1.0F; // Màxima visibilitat (player) -constexpr float ENEMIC = 0.7F; // 30% més tènue (destaca menys) -constexpr float BALA = 1.0F; // Brillo a tope (màxima visibilitat) - -// Starfield: gradient segons distancia al centro -// distancia_centre: 0.0 (centro) → 1.0 (vora pantalla) -// brightness = MIN + (MAX - MIN) * distancia_centre -constexpr float STARFIELD_MIN = 0.3F; // Estrelles llunyanes (prop del centro) -constexpr float STARFIELD_MAX = 0.8F; // Estrelles properes (vora pantalla) -} // namespace Brightness - -// Renderització (V-Sync i altres opciones de render) -namespace Rendering { -constexpr int VSYNC_DEFAULT = 1; // 0=disabled, 1=enabled -} // namespace Rendering - -// Audio (sistema de sonido y música) — usado por Audio::Config en init() -namespace Audio { -constexpr bool ENABLED = true; // Audio habilitado por defecto -constexpr float VOLUME = 1.0F; // Volumen maestro (0..1) -constexpr bool MUSIC_ENABLED = true; // Música habilitada -constexpr float MUSIC_VOLUME = 0.8F; // Volumen música (0..1) -constexpr bool SOUND_ENABLED = true; // Efectos habilitados -constexpr float SOUND_VOLUME = 1.0F; // Volumen efectos (0..1) -constexpr float VOLUME_STEP = 0.05F; // Paso UI (5%) -constexpr int FREQUENCY = 48000; // Frecuencia de muestreo (Hz) -constexpr int CROSSFADE_MS = 1500; // Crossfade por defecto entre pistas (ms) -constexpr SDL_AudioFormat FORMAT = SDL_AUDIO_S16; // PCM 16-bit signed nativo -constexpr int CHANNELS = 2; // Estéreo -} // namespace Audio - -// Música (pistas de fondo) -namespace Music { -constexpr const char* GAME_TRACK = "game.ogg"; // Pista de juego -constexpr const char* TITLE_TRACK = "title.ogg"; // Pista de titulo -constexpr int FADE_DURATION_MS = 1000; // Fade out duration -} // namespace Music - -// Efectes de so (sons puntuals) -namespace Sound { -constexpr const char* CONTINUE = "effects/continue.wav"; // Cuenta atras -constexpr const char* EXPLOSION = "effects/explosion.wav"; // Explosión -constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa -constexpr const char* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; // Friendly fire hit -constexpr const char* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD -constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo -constexpr const char* LOGO = "effects/logo.wav"; // Logo -constexpr const char* START = "effects/start.wav"; // El player pulsa START -constexpr const char* GOOD_JOB_COMMANDER = "voices/good_job_commander.wav"; // Voz: "Good job, commander" -} // namespace Sound - -// Controls (mapeo de teclas para los jugadores) -namespace Controls { -namespace P1 { -constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_RIGHT; -constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_LEFT; -constexpr SDL_Scancode THRUST = SDL_SCANCODE_UP; -constexpr SDL_Keycode SHOOT = SDLK_SPACE; -} // namespace P1 - -namespace P2 { -constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_D; -constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_A; -constexpr SDL_Scancode THRUST = SDL_SCANCODE_W; -constexpr SDL_Keycode SHOOT = SDLK_LSHIFT; -} // namespace P2 -} // namespace Controls - -// Enemy type configuration (type de enemigos) -namespace Enemies { -// Pentagon (esquivador - zigzag evasion) -namespace Pentagon { -constexpr float VELOCITAT = 35.0F; // px/s (slightly slower) -constexpr float CANVI_ANGLE_PROB = 0.20F; // 20% per wall hit (frequent zigzag) -constexpr float CANVI_ANGLE_MAX = 1.0F; // Max random angle change (rad) -constexpr float DROTACIO_MIN = 0.75F; // Min visual rotation (rad/s) [+50%] -constexpr float DROTACIO_MAX = 3.75F; // Max visual rotation (rad/s) [+50%] -constexpr const char* SHAPE_FILE = "enemy_pentagon.shp"; -} // namespace Pentagon - -// Cuadrado (perseguidor - tracks player) -namespace Cuadrado { -constexpr float VELOCITAT = 40.0F; // px/s (medium speed) -constexpr float TRACKING_STRENGTH = 0.5F; // Interpolation toward player (0.0-1.0) -constexpr float TRACKING_INTERVAL = 1.0F; // Seconds between angle updates -constexpr float DROTACIO_MIN = 0.3F; // Slow rotation [+50%] -constexpr float DROTACIO_MAX = 1.5F; // [+50%] -constexpr const char* SHAPE_FILE = "enemy_square.shp"; -} // namespace Cuadrado - -// Molinillo (agressiu - fast straight lines, proximity spin-up) -namespace Molinillo { -constexpr float VELOCITAT = 50.0F; // px/s (fastest) -constexpr float CANVI_ANGLE_PROB = 0.05F; // 5% per wall hit (rare direction change) -constexpr float CANVI_ANGLE_MAX = 0.3F; // Small angle adjustments -constexpr float DROTACIO_MIN = 3.0F; // Base rotation (rad/s) [+50%] -constexpr float DROTACIO_MAX = 6.0F; // [+50%] -constexpr float DROTACIO_PROXIMITY_MULTIPLIER = 3.0F; // Spin-up multiplier when near ship -constexpr float PROXIMITY_DISTANCE = 100.0F; // Distance threshold (px) -constexpr const char* SHAPE_FILE = "enemy_pinwheel.shp"; -} // namespace Molinillo - -// Animation parameters (shared) -namespace Animation { -// Palpitation -constexpr float PALPITACIO_TRIGGER_PROB = 0.01F; // 1% chance per second -constexpr float PALPITACIO_DURACIO_MIN = 1.0F; // Min duration (seconds) -constexpr float PALPITACIO_DURACIO_MAX = 3.0F; // Max duration (seconds) -constexpr float PALPITACIO_AMPLITUD_MIN = 0.08F; // Min scale variation -constexpr float PALPITACIO_AMPLITUD_MAX = 0.20F; // Max scale variation -constexpr float PALPITACIO_FREQ_MIN = 1.5F; // Min frequency (Hz) -constexpr float PALPITACIO_FREQ_MAX = 3.0F; // Max frequency (Hz) - -// Rotation acceleration -constexpr float ROTACIO_ACCEL_TRIGGER_PROB = 0.02F; // 2% chance per second [4x more frequent] -constexpr float ROTACIO_ACCEL_DURACIO_MIN = 3.0F; // Min transition time -constexpr float ROTACIO_ACCEL_DURACIO_MAX = 8.0F; // Max transition time -constexpr float ROTACIO_ACCEL_MULTIPLIER_MIN = 0.3F; // Min speed multiplier [more dramatic] -constexpr float ROTACIO_ACCEL_MULTIPLIER_MAX = 4.0F; // Max speed multiplier [more dramatic] -} // namespace Animation - -// Spawn safety and invulnerability system -namespace Spawn { -// Safe spawn distance from player -constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0F; // 3x ship radius -constexpr float SAFETY_DISTANCE = Defaults::Entities::SHIP_RADIUS * SAFETY_DISTANCE_MULTIPLIER; // 36.0f px -constexpr int MAX_SPAWN_ATTEMPTS = 50; // Max attempts to find safe position - -// Invulnerability system -constexpr float INVULNERABILITY_DURATION = 3.0F; // Seconds -constexpr float INVULNERABILITY_BRIGHTNESS_START = 0.3F; // Dim -constexpr float INVULNERABILITY_BRIGHTNESS_END = 0.7F; // Normal (same as Defaults::Brightness::ENEMIC) -constexpr float INVULNERABILITY_SCALE_START = 0.0F; // Invisible -constexpr float INVULNERABILITY_SCALE_END = 1.0F; // Full size -} // namespace Spawn - -// Scoring system (puntuación per type de enemy) -namespace Scoring { -constexpr int PENTAGON_SCORE = 100; // Pentágono (esquivador, 35 px/s) -constexpr int QUADRAT_SCORE = 150; // Cuadrado (perseguidor, 40 px/s) -constexpr int MOLINILLO_SCORE = 200; // Molinillo (agressiu, 50 px/s) -} // namespace Scoring - -} // namespace Enemies - -// Title scene ship animations (naves 3D flotantes a l'escena de título) -namespace Title { -namespace Ships { -// ============================================================ -// PARÀMETRES BASE (ajustar aquí per experimentar) -// ============================================================ - -// 1. Escala global de las naves -constexpr float SHIP_BASE_SCALE = 2.5F; // Multiplicador (1.0 = mida original del .shp) - -// 2. Altura vertical (cercanía al centro) -// Ratio Y desde el centro de la pantalla (0.0 = centro, 1.0 = bottom de pantalla) -constexpr float TARGET_Y_RATIO = 0.15625F; - -// 3. Radio orbital (distance radial desde centro en coordenadas polares) -constexpr float CLOCK_RADIUS = 150.0F; // Distancia des del centro - -// 4. Ángulos de posición (clock positions en coordenadas polares) -// En coordenadas de pantalla: 0° = derecha, 90° = baix, 180° = izquierda, 270° = dalt -constexpr float CLOCK_8_ANGLE = 150.0F * Math::PI / 180.0F; // 8 o'clock (bottom-left) -constexpr float CLOCK_4_ANGLE = 30.0F * Math::PI / 180.0F; // 4 o'clock (bottom-right) - -// 5. Radio máximo de la shape de la nave (para calcular offset automáticamente) -constexpr float SHIP_MAX_RADIUS = 30.0F; // Radi del cercle circumscrit a ship_starfield.shp - -// 6. Margen de seguridad para offset de entrada -constexpr float ENTRY_OFFSET_MARGIN = 227.5F; // Para offset total de ~340px (ajustado) - -// ============================================================ -// VALORS DERIVATS (calculats automáticoament - NO modificar) -// ============================================================ - -// Centro de la pantalla (point de referència) -constexpr float CENTER_X = Game::WIDTH / 2.0F; // auto-derivado de Game::WIDTH -constexpr float CENTER_Y = Game::HEIGHT / 2.0F; // auto-derivado de Game::HEIGHT - -// Posicions target (calculades dinàmicament des dels parámetros base) -// Nota: std::cos/sin no són constexpr en C++20, pero funcionen en runtime -// Les funciones inline són optimitzades por el compilador (zero overhead) -inline auto p1TargetX() -> float { - return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_8_ANGLE)); -} -inline auto p1TargetY() -> float { - return CENTER_Y + ((Game::HEIGHT / 2.0F) * TARGET_Y_RATIO); -} -inline auto p2TargetX() -> float { - return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_4_ANGLE)); -} -inline auto p2TargetY() -> float { - return CENTER_Y + ((Game::HEIGHT / 2.0F) * TARGET_Y_RATIO); -} - -// Escales de animación (relatives a SHIP_BASE_SCALE) -constexpr float ENTRY_SCALE_START = 1.5F * SHIP_BASE_SCALE; // Entrada: 50% més grande -constexpr float FLOATING_SCALE = 1.0F * SHIP_BASE_SCALE; // Flotante: scale base - -// Offset de entrada (ajustat automáticoament a l'scale) -// Fórmula: (radi màxim de la ship * scale de entrada) + margen -constexpr float ENTRY_OFFSET = (SHIP_MAX_RADIUS * ENTRY_SCALE_START) + ENTRY_OFFSET_MARGIN; - -// Vec2 de fuga (centro para l'animación de salida) -constexpr float VANISHING_POINT_X = CENTER_X; // auto-derivado de Game::WIDTH -constexpr float VANISHING_POINT_Y = CENTER_Y; // auto-derivado de Game::HEIGHT - -// ============================================================ -// ANIMACIONS (durades, oscil·lacions, delays) -// ============================================================ - -// Durades de animación -constexpr float ENTRY_DURATION = 2.0F; // Entrada (segons) -constexpr float EXIT_DURATION = 1.0F; // Salida (segons) - -// Flotació (oscil·lació reduïda y diferenciada per ship) -constexpr float FLOAT_AMPLITUDE_X = 4.0F; // Amplitud X (píxels) -constexpr float FLOAT_AMPLITUDE_Y = 2.5F; // Amplitud Y (píxels) - -// Freqüències base -constexpr float FLOAT_FREQUENCY_X_BASE = 0.5F; // Hz -constexpr float FLOAT_FREQUENCY_Y_BASE = 0.7F; // Hz -constexpr float FLOAT_PHASE_OFFSET = 1.57F; // π/2 (90°) - -// Delays de entrada (per a entrada escalonada) -constexpr float P1_ENTRY_DELAY = 0.0F; // P1 entra immediatament -constexpr float P2_ENTRY_DELAY = 0.5F; // P2 entra 0.5s después - -// Delay global antes de start l'animación de entrada al state MAIN -constexpr float ENTRANCE_DELAY = 5.0F; // Temps de espera antes que las naves entrin - -// Multiplicadors de freqüència para cada ship (variació sutil ±12%) -constexpr float P1_FREQUENCY_MULTIPLIER = 0.88F; // 12% més lenta -constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; // 12% més ràpida - -} // namespace Ships - -namespace Layout { -// Posicions verticals (anclatges des del TOP de pantalla lógica, 0.0-1.0) -constexpr float LOGO_POS = 0.20F; // Logo "ORNI" -constexpr float PRESS_START_POS = 0.75F; // "PRESS START TO PLAY" -constexpr float COPYRIGHT1_POS = 0.90F; // Primera línia copyright - -// Separacions relatives (proporció respecte Game::HEIGHT = 480px) -constexpr float LOGO_LINE_SPACING = 0.02F; // Entre "ORNI" i "ATTACK!" (10px) -constexpr float COPYRIGHT_LINE_SPACING = 0.0F; // Entre línies copyright (5px) - -// Factors de scale -constexpr float LOGO_SCALE = 0.6F; // Escala "ORNI ATTACK!" -constexpr float PRESS_START_SCALE = 1.0F; // Escala "PRESS START TO PLAY" -constexpr float COPYRIGHT_SCALE = 0.5F; // Escala copyright -constexpr float JAILGAMES_SCALE = 0.25F; // Escala del logo JAILGAMES pequeño sobre el copyright - -// Separación entre el logo JAILGAMES y la línea de copyright (proporción de Game::HEIGHT). -constexpr float JAILGAMES_COPYRIGHT_GAP = 0.015F; - -// Espaiat entre caràcters (usado per VectorText) -constexpr float TEXT_SPACING = 2.0F; -} // namespace Layout -} // namespace Title - -// Floating score numbers (números flotantes de puntuación) -namespace FloatingScore { -constexpr float LIFETIME = 2.0F; // Duració màxima (segons) -constexpr float VELOCITY_Y = -30.0F; // Velocidad vertical (px/s, negatiu = amunt) -constexpr float VELOCITY_X = 0.0F; // Velocidad horizontal (px/s) -constexpr float SCALE = 0.45F; // Escala del text (0.6 = 60% del marcador) -constexpr float SPACING = 0.0F; // Espaiat entre caràcters -constexpr int MAX_CONCURRENT = 15; // Pool size (= MAX_ORNIS) -} // namespace FloatingScore + // Configuración de ventana + namespace Window { + constexpr int WIDTH = 1280; + constexpr int HEIGHT = 720; + constexpr int MIN_WIDTH = 640; // Mínimo: mitad del baseline (16:9) + constexpr int MIN_HEIGHT = 360; + // Zoom system + constexpr float BASE_ZOOM = 1.0F; // 1280x720 baseline (16:9) + constexpr float MIN_ZOOM = 0.5F; // 640x360 minimum + constexpr float ZOOM_INCREMENT = 0.1F; // 10% steps (F1/F2) + constexpr bool FULLSCREEN = true; // Pantalla completa activada por defecto + } // namespace Window + + // Dimensiones base del juego (coordenadas lógicas, 16:9) + namespace Game { + constexpr int WIDTH = 1280; + constexpr int HEIGHT = 720; + } // namespace Game + + // Zones del juego (SDL_FRect con cálculos automáticos basat en porcentajes) + namespace Zones { + // --- CONFIGURACIÓ DE PORCENTATGES --- + // Todas las zones definides como a porcentajes de Game::WIDTH (640) i Game::HEIGHT (480) + + // Percentatges de height (divisió vertical) + constexpr float SCOREBOARD_TOP_HEIGHT_PERCENT = 0.02F; // 10% superior + constexpr float MAIN_PLAYAREA_HEIGHT_PERCENT = 0.88F; // 80% central + constexpr float SCOREBOARD_BOTTOM_HEIGHT_PERCENT = 0.10F; // 10% inferior + + // Padding horizontal para PLAYAREA (dentro de MAIN_PLAYAREA) + constexpr float PLAYAREA_PADDING_HORIZONTAL_PERCENT = 0.015F; // 5% a cada costat + + // --- CÀLCULS AUTOMÀTICS DE PÍXELS --- + // Cálculos automáticos a partir dels porcentajes + + // Alçades + constexpr float SCOREBOARD_TOP_H = Game::HEIGHT * SCOREBOARD_TOP_HEIGHT_PERCENT; + constexpr float MAIN_PLAYAREA_H = Game::HEIGHT * MAIN_PLAYAREA_HEIGHT_PERCENT; + constexpr float SCOREBOARD_BOTTOM_H = Game::HEIGHT * SCOREBOARD_BOTTOM_HEIGHT_PERCENT; + + // Posicions Y + constexpr float SCOREBOARD_TOP_Y = 0.0F; + constexpr float MAIN_PLAYAREA_Y = SCOREBOARD_TOP_H; + constexpr float SCOREBOARD_BOTTOM_Y = MAIN_PLAYAREA_Y + MAIN_PLAYAREA_H; + + // Padding horizontal de PLAYAREA + constexpr float PLAYAREA_PADDING_H = Game::WIDTH * PLAYAREA_PADDING_HORIZONTAL_PERCENT; + + // --- ZONES FINALS (SDL_FRect) --- + + // Marcador superior (reservado para futuro uso) + // Ocupa el 2% superior + constexpr SDL_FRect SCOREBOARD_TOP = { + 0.0F, // x = 0.0 + SCOREBOARD_TOP_Y, // y = 0.0 + static_cast(Game::WIDTH), // ancho completo + SCOREBOARD_TOP_H // alto + }; + + // Área de juego principal (contenedor del 80% central, sin padding) + // Ocupa el 88% central, ancho completo + constexpr SDL_FRect MAIN_PLAYAREA = { + 0.0F, // x = 0.0 + MAIN_PLAYAREA_Y, // debajo del scoreboard superior + static_cast(Game::WIDTH), // ancho completo + MAIN_PLAYAREA_H // alto + }; + + // Zona de juego real (con padding horizontal del 5%) + // Ocupa: dentro de MAIN_PLAYAREA, con márgenes laterales + // Se utiliza para límites del juego, colisiones, spawn + constexpr SDL_FRect PLAYAREA = { + PLAYAREA_PADDING_H, // padding horizontal + MAIN_PLAYAREA_Y, // debajo del scoreboard superior (igual que MAIN_PLAYAREA) + Game::WIDTH - (2.0F * PLAYAREA_PADDING_H), // ancho con padding + MAIN_PLAYAREA_H // alto (igual que MAIN_PLAYAREA) + }; + + // Marcador inferior (marcador actual) + // Ocupa el 10% inferior + constexpr SDL_FRect SCOREBOARD = { + 0.0F, // x = 0.0 + SCOREBOARD_BOTTOM_Y, // fondo + static_cast(Game::WIDTH), // ancho completo + SCOREBOARD_BOTTOM_H // alto + }; + + // Padding horizontal del marcador (para alinear zonas izquierda/derecha con PLAYAREA) + constexpr float SCOREBOARD_PADDING_H = 0.0F; // Game::WIDTH * 0.015f; + } // namespace Zones + + // Objetos del juego + namespace Entities { + constexpr int MAX_ORNIS = 15; + constexpr int MAX_BALES = 3; + + constexpr float SHIP_RADIUS = 12.0F; + constexpr float ENEMY_RADIUS = 20.0F; + constexpr float BULLET_RADIUS = 3.0F; + } // namespace Entities + + // Paleta semántica por tipo de entidad. Si una entity declara color, lo + // pasa al pipeline con alpha=255 (sentinela "color válido"); si no, se + // usa el color global del oscilador (g_current_line_color). + namespace Palette { + constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro + constexpr SDL_Color BULLET = {.r = 120, .g = 255, .b = 140, .a = 255}; // Verde laser + constexpr SDL_Color PENTAGON = {.r = 120, .g = 170, .b = 255, .a = 255}; // Azul "esquivador" + constexpr SDL_Color QUADRAT = {.r = 255, .g = 110, .b = 110, .a = 255}; // Rojo "tank" + constexpr SDL_Color MOLINILLO = {.r = 255, .g = 130, .b = 255, .a = 255}; // Magenta agresivo + } // namespace Palette + + // Ship (nave del player) + namespace Ship { + // Invulnerabilidad post-respawn + constexpr float INVULNERABILITY_DURATION = 3.0F; // Segundos de invulnerabilidad + + // Parpadeo visual durante invulnerabilidad + constexpr float BLINK_VISIBLE_TIME = 0.1F; // Tiempo visible (segundos) + constexpr float BLINK_INVISIBLE_TIME = 0.1F; // Tiempo invisible (segundos) + // Frecuencia total: 0.2s/ciclo = 5 Hz (~15 parpadeos en 3s) + } // namespace Ship + + // Game rules (lives, respawn, game over) + namespace Game { + constexpr int STARTING_LIVES = 3; // Initial lives + constexpr float DEATH_DURATION = 3.0F; // Seconds of death animation + constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over + constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 0.80F; // 80% hitbox (generous) + constexpr float COLLISION_BULLET_ENEMY_AMPLIFIER = 1.15F; // 115% hitbox (generous) + + // Friendly fire system + constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire + constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%) + constexpr float BULLET_GRACE_PERIOD = 0.2F; // Inmunidad post-disparo (s) + + // Transición LEVEL_START (mensajes aleatorios PRE-level) + constexpr float LEVEL_START_DURATION = 3.0F; // Duración total + constexpr float LEVEL_START_TYPING_RATIO = 0.3F; // 30% escribiendo, 70% mostrando + + // Transición LEVEL_COMPLETED (mensaje "GOOD JOB COMMANDER!") + constexpr float LEVEL_COMPLETED_DURATION = 3.0F; // Duración total + constexpr float LEVEL_COMPLETED_TYPING_RATIO = 0.0F; // 0.0 = sin typewriter (directo) + + // Transición INIT_HUD (animación inicial del HUD) + constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado + + // Ratios de animación (inicio y fin como porcentajes del tiempo total) + // RECT (rectángulo de márgenes) + constexpr float INIT_HUD_RECT_RATIO_INIT = 0.30F; + constexpr float INIT_HUD_RECT_RATIO_END = 0.85F; + + // SCORE (marcador de puntuación) + constexpr float INIT_HUD_SCORE_RATIO_INIT = 0.60F; + constexpr float INIT_HUD_SCORE_RATIO_END = 0.90F; + + // SHIP1 (nave player 1) + constexpr float INIT_HUD_SHIP1_RATIO_INIT = 0.0F; + constexpr float INIT_HUD_SHIP1_RATIO_END = 1.0F; + + // SHIP2 (nave player 2) + constexpr float INIT_HUD_SHIP2_RATIO_INIT = 0.20F; + constexpr float INIT_HUD_SHIP2_RATIO_END = 1.0F; + + // Posición inicial de la nave en INIT_HUD (75% de altura de zona de juego) + constexpr float INIT_HUD_SHIP_START_Y_RATIO = 0.75F; // 75% desde el top de PLAYAREA + + // Spawn positions (distribución horizontal para 2 jugadores) + constexpr float P1_SPAWN_X_RATIO = 0.33F; // 33% desde izquierda + constexpr float P2_SPAWN_X_RATIO = 0.67F; // 67% desde izquierda + constexpr float SPAWN_Y_RATIO = 0.75F; // 75% desde arriba + + // Continue system behavior + constexpr int CONTINUE_COUNT_START = 9; // Countdown starts at 9 + constexpr float CONTINUE_TICK_DURATION = 1.0F; // Seconds per countdown tick + constexpr int MAX_CONTINUES = 3; // Maximum continues per game + constexpr bool INFINITE_CONTINUES = false; // If true, unlimited continues + + // Continue screen visual configuration + namespace ContinueScreen { + // "CONTINUE" text + constexpr float CONTINUE_TEXT_SCALE = 2.0F; // Text size + constexpr float CONTINUE_TEXT_Y_RATIO = 0.30F; // 35% from top of PLAYAREA + + // Countdown number (9, 8, 7...) + constexpr float COUNTER_TEXT_SCALE = 4.0F; // Text size (large) + constexpr float COUNTER_TEXT_Y_RATIO = 0.50F; // 50% from top of PLAYAREA + + // "CONTINUES LEFT: X" text + constexpr float INFO_TEXT_SCALE = 0.7F; // Text size (small) + constexpr float INFO_TEXT_Y_RATIO = 0.75F; // 65% from top of PLAYAREA + } // namespace ContinueScreen + + // Game Over screen visual configuration + namespace GameOverScreen { + constexpr float TEXT_SCALE = 2.0F; // "GAME OVER" text size + constexpr float TEXT_SPACING = 4.0F; // Character spacing + } // namespace GameOverScreen + + // Stage message configuration (LEVEL_START, LEVEL_COMPLETED) + constexpr float STAGE_MESSAGE_Y_RATIO = 0.25F; // 25% from top of PLAYAREA + constexpr float STAGE_MESSAGE_MAX_WIDTH_RATIO = 0.9F; // 90% of PLAYAREA width + } // namespace Game + + // Física del control de la nau (px/s, rad/s) + namespace Physics { + constexpr float ROTATION_SPEED = 3.14F; // rad/s (~180°/s) + constexpr float ACCELERATION = 400.0F; // px/s² + constexpr float MAX_VELOCITY = 120.0F; // px/s + constexpr float FRICTION = 20.0F; // px/s² + + // Explosions (debris physics) + namespace Debris { + constexpr float VELOCITAT_BASE = 80.0F; // Velocidad inicial (px/s) + constexpr float VARIACIO_VELOCITAT = 40.0F; // ±variació aleatòria (px/s) + constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²) + constexpr float ROTACIO_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s) + constexpr float ROTACIO_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s) + constexpr float TEMPS_VIDA = 2.0F; // Duració màxima (segons) - enemy/bullet debris + constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris lifetime (matches DEATH_DURATION) + constexpr float SHRINK_RATE = 0.5F; // Reducció de mida (factor/s) + + // Herència de velocity angular (trayectorias curvas) + constexpr float FACTOR_HERENCIA_MIN = 0.7F; // Mínimo 70% del drotacio heredat + constexpr float FACTOR_HERENCIA_MAX = 1.0F; // Màxim 100% del drotacio heredat + constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²) + + // Angular velocity sin for trajectory inheritance + // Excess above this threshold is converted to tangential linear velocity + // Prevents "vortex trap" problem with high-rotation enemies + constexpr float VELOCITAT_ROT_MAX = 1.5F; // rad/s (~86°/s) + } // namespace Debris + } // namespace Physics + + // Matemáticas + namespace Math { + constexpr float PI = std::numbers::pi_v; + } // namespace Math + + // La antigua oscilación CPU (namespace Color) se ha migrado al shader de + // postpro. Los parámetros de flicker / background pulse viven ahora en + // data/config/postfx.yaml y se aplican en shaders/postfx.frag.glsl. + + // Brillantor (control de intensitat per cada type de entidad) + namespace Brightness { + // Brillantor estàtica per entidades de juego (0.0-1.0) + constexpr float NAU = 1.0F; // Màxima visibilitat (player) + constexpr float ENEMIC = 0.7F; // 30% més tènue (destaca menys) + constexpr float BALA = 1.0F; // Brillo a tope (màxima visibilitat) + + // Starfield: gradient segons distancia al centro + // distancia_centre: 0.0 (centro) → 1.0 (vora pantalla) + // brightness = MIN + (MAX - MIN) * distancia_centre + constexpr float STARFIELD_MIN = 0.3F; // Estrelles llunyanes (prop del centro) + constexpr float STARFIELD_MAX = 0.8F; // Estrelles properes (vora pantalla) + } // namespace Brightness + + // Renderització (V-Sync i altres opciones de render) + namespace Rendering { + constexpr int VSYNC_DEFAULT = 1; // 0=disabled, 1=enabled + } // namespace Rendering + + // Audio (sistema de sonido y música) — usado por Audio::Config en init() + namespace Audio { + constexpr bool ENABLED = true; // Audio habilitado por defecto + constexpr float VOLUME = 1.0F; // Volumen maestro (0..1) + constexpr bool MUSIC_ENABLED = true; // Música habilitada + constexpr float MUSIC_VOLUME = 0.8F; // Volumen música (0..1) + constexpr bool SOUND_ENABLED = true; // Efectos habilitados + constexpr float SOUND_VOLUME = 1.0F; // Volumen efectos (0..1) + constexpr float VOLUME_STEP = 0.05F; // Paso UI (5%) + constexpr int FREQUENCY = 48000; // Frecuencia de muestreo (Hz) + constexpr int CROSSFADE_MS = 1500; // Crossfade por defecto entre pistas (ms) + constexpr SDL_AudioFormat FORMAT = SDL_AUDIO_S16; // PCM 16-bit signed nativo + constexpr int CHANNELS = 2; // Estéreo + } // namespace Audio + + // Música (pistas de fondo) + namespace Music { + constexpr const char* GAME_TRACK = "game.ogg"; // Pista de juego + constexpr const char* TITLE_TRACK = "title.ogg"; // Pista de titulo + constexpr int FADE_DURATION_MS = 1000; // Fade out duration + } // namespace Music + + // Efectes de so (sons puntuals) + namespace Sound { + constexpr const char* CONTINUE = "effects/continue.wav"; // Cuenta atras + constexpr const char* EXPLOSION = "effects/explosion.wav"; // Explosión + constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa + constexpr const char* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; // Friendly fire hit + constexpr const char* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD + constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo + constexpr const char* LOGO = "effects/logo.wav"; // Logo + constexpr const char* START = "effects/start.wav"; // El player pulsa START + constexpr const char* GOOD_JOB_COMMANDER = "voices/good_job_commander.wav"; // Voz: "Good job, commander" + } // namespace Sound + + // Controls (mapeo de teclas para los jugadores) + namespace Controls { + namespace P1 { + constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_RIGHT; + constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_LEFT; + constexpr SDL_Scancode THRUST = SDL_SCANCODE_UP; + constexpr SDL_Keycode SHOOT = SDLK_SPACE; + } // namespace P1 + + namespace P2 { + constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_D; + constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_A; + constexpr SDL_Scancode THRUST = SDL_SCANCODE_W; + constexpr SDL_Keycode SHOOT = SDLK_LSHIFT; + } // namespace P2 + } // namespace Controls + + // Enemy type configuration (type de enemigos) + namespace Enemies { + // Pentagon (esquivador - zigzag evasion) + namespace Pentagon { + constexpr float VELOCITAT = 35.0F; // px/s (slightly slower) + constexpr float CANVI_ANGLE_PROB = 0.20F; // 20% per wall hit (frequent zigzag) + constexpr float CANVI_ANGLE_MAX = 1.0F; // Max random angle change (rad) + constexpr float DROTACIO_MIN = 0.75F; // Min visual rotation (rad/s) [+50%] + constexpr float DROTACIO_MAX = 3.75F; // Max visual rotation (rad/s) [+50%] + constexpr const char* SHAPE_FILE = "enemy_pentagon.shp"; + } // namespace Pentagon + + // Cuadrado (perseguidor - tracks player) + namespace Cuadrado { + constexpr float VELOCITAT = 40.0F; // px/s (medium speed) + constexpr float TRACKING_STRENGTH = 0.5F; // Interpolation toward player (0.0-1.0) + constexpr float TRACKING_INTERVAL = 1.0F; // Seconds between angle updates + constexpr float DROTACIO_MIN = 0.3F; // Slow rotation [+50%] + constexpr float DROTACIO_MAX = 1.5F; // [+50%] + constexpr const char* SHAPE_FILE = "enemy_square.shp"; + } // namespace Cuadrado + + // Molinillo (agressiu - fast straight lines, proximity spin-up) + namespace Molinillo { + constexpr float VELOCITAT = 50.0F; // px/s (fastest) + constexpr float CANVI_ANGLE_PROB = 0.05F; // 5% per wall hit (rare direction change) + constexpr float CANVI_ANGLE_MAX = 0.3F; // Small angle adjustments + constexpr float DROTACIO_MIN = 3.0F; // Base rotation (rad/s) [+50%] + constexpr float DROTACIO_MAX = 6.0F; // [+50%] + constexpr float DROTACIO_PROXIMITY_MULTIPLIER = 3.0F; // Spin-up multiplier when near ship + constexpr float PROXIMITY_DISTANCE = 100.0F; // Distance threshold (px) + constexpr const char* SHAPE_FILE = "enemy_pinwheel.shp"; + } // namespace Molinillo + + // Animation parameters (shared) + namespace Animation { + // Palpitation + constexpr float PALPITACIO_TRIGGER_PROB = 0.01F; // 1% chance per second + constexpr float PALPITACIO_DURACIO_MIN = 1.0F; // Min duration (seconds) + constexpr float PALPITACIO_DURACIO_MAX = 3.0F; // Max duration (seconds) + constexpr float PALPITACIO_AMPLITUD_MIN = 0.08F; // Min scale variation + constexpr float PALPITACIO_AMPLITUD_MAX = 0.20F; // Max scale variation + constexpr float PALPITACIO_FREQ_MIN = 1.5F; // Min frequency (Hz) + constexpr float PALPITACIO_FREQ_MAX = 3.0F; // Max frequency (Hz) + + // Rotation acceleration + constexpr float ROTACIO_ACCEL_TRIGGER_PROB = 0.02F; // 2% chance per second [4x more frequent] + constexpr float ROTACIO_ACCEL_DURACIO_MIN = 3.0F; // Min transition time + constexpr float ROTACIO_ACCEL_DURACIO_MAX = 8.0F; // Max transition time + constexpr float ROTACIO_ACCEL_MULTIPLIER_MIN = 0.3F; // Min speed multiplier [more dramatic] + constexpr float ROTACIO_ACCEL_MULTIPLIER_MAX = 4.0F; // Max speed multiplier [more dramatic] + } // namespace Animation + + // Spawn safety and invulnerability system + namespace Spawn { + // Safe spawn distance from player + constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0F; // 3x ship radius + constexpr float SAFETY_DISTANCE = Defaults::Entities::SHIP_RADIUS * SAFETY_DISTANCE_MULTIPLIER; // 36.0f px + constexpr int MAX_SPAWN_ATTEMPTS = 50; // Max attempts to find safe position + + // Invulnerability system + constexpr float INVULNERABILITY_DURATION = 3.0F; // Seconds + constexpr float INVULNERABILITY_BRIGHTNESS_START = 0.3F; // Dim + constexpr float INVULNERABILITY_BRIGHTNESS_END = 0.7F; // Normal (same as Defaults::Brightness::ENEMIC) + constexpr float INVULNERABILITY_SCALE_START = 0.0F; // Invisible + constexpr float INVULNERABILITY_SCALE_END = 1.0F; // Full size + } // namespace Spawn + + // Scoring system (puntuación per type de enemy) + namespace Scoring { + constexpr int PENTAGON_SCORE = 100; // Pentágono (esquivador, 35 px/s) + constexpr int QUADRAT_SCORE = 150; // Cuadrado (perseguidor, 40 px/s) + constexpr int MOLINILLO_SCORE = 200; // Molinillo (agressiu, 50 px/s) + } // namespace Scoring + + } // namespace Enemies + + // Title scene ship animations (naves 3D flotantes a l'escena de título) + namespace Title { + namespace Ships { + // ============================================================ + // PARÀMETRES BASE (ajustar aquí per experimentar) + // ============================================================ + + // 1. Escala global de las naves + constexpr float SHIP_BASE_SCALE = 2.5F; // Multiplicador (1.0 = mida original del .shp) + + // 2. Altura vertical (cercanía al centro) + // Ratio Y desde el centro de la pantalla (0.0 = centro, 1.0 = bottom de pantalla) + constexpr float TARGET_Y_RATIO = 0.15625F; + + // 3. Radio orbital (distance radial desde centro en coordenadas polares) + constexpr float CLOCK_RADIUS = 150.0F; // Distancia des del centro + + // 4. Ángulos de posición (clock positions en coordenadas polares) + // En coordenadas de pantalla: 0° = derecha, 90° = baix, 180° = izquierda, 270° = dalt + constexpr float CLOCK_8_ANGLE = 150.0F * Math::PI / 180.0F; // 8 o'clock (bottom-left) + constexpr float CLOCK_4_ANGLE = 30.0F * Math::PI / 180.0F; // 4 o'clock (bottom-right) + + // 5. Radio máximo de la shape de la nave (para calcular offset automáticamente) + constexpr float SHIP_MAX_RADIUS = 30.0F; // Radi del cercle circumscrit a ship_starfield.shp + + // 6. Margen de seguridad para offset de entrada + constexpr float ENTRY_OFFSET_MARGIN = 227.5F; // Para offset total de ~340px (ajustado) + + // ============================================================ + // VALORS DERIVATS (calculats automáticoament - NO modificar) + // ============================================================ + + // Centro de la pantalla (point de referència) + constexpr float CENTER_X = Game::WIDTH / 2.0F; // auto-derivado de Game::WIDTH + constexpr float CENTER_Y = Game::HEIGHT / 2.0F; // auto-derivado de Game::HEIGHT + + // Posicions target (calculades dinàmicament des dels parámetros base) + // Nota: std::cos/sin no són constexpr en C++20, pero funcionen en runtime + // Les funciones inline són optimitzades por el compilador (zero overhead) + inline auto p1TargetX() -> float { + return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_8_ANGLE)); + } + inline auto p1TargetY() -> float { + return CENTER_Y + ((Game::HEIGHT / 2.0F) * TARGET_Y_RATIO); + } + inline auto p2TargetX() -> float { + return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_4_ANGLE)); + } + inline auto p2TargetY() -> float { + return CENTER_Y + ((Game::HEIGHT / 2.0F) * TARGET_Y_RATIO); + } + + // Escales de animación (relatives a SHIP_BASE_SCALE) + constexpr float ENTRY_SCALE_START = 1.5F * SHIP_BASE_SCALE; // Entrada: 50% més grande + constexpr float FLOATING_SCALE = 1.0F * SHIP_BASE_SCALE; // Flotante: scale base + + // Offset de entrada (ajustat automáticoament a l'scale) + // Fórmula: (radi màxim de la ship * scale de entrada) + margen + constexpr float ENTRY_OFFSET = (SHIP_MAX_RADIUS * ENTRY_SCALE_START) + ENTRY_OFFSET_MARGIN; + + // Vec2 de fuga (centro para l'animación de salida) + constexpr float VANISHING_POINT_X = CENTER_X; // auto-derivado de Game::WIDTH + constexpr float VANISHING_POINT_Y = CENTER_Y; // auto-derivado de Game::HEIGHT + + // ============================================================ + // ANIMACIONS (durades, oscil·lacions, delays) + // ============================================================ + + // Durades de animación + constexpr float ENTRY_DURATION = 2.0F; // Entrada (segons) + constexpr float EXIT_DURATION = 1.0F; // Salida (segons) + + // Flotació (oscil·lació reduïda y diferenciada per ship) + constexpr float FLOAT_AMPLITUDE_X = 4.0F; // Amplitud X (píxels) + constexpr float FLOAT_AMPLITUDE_Y = 2.5F; // Amplitud Y (píxels) + + // Freqüències base + constexpr float FLOAT_FREQUENCY_X_BASE = 0.5F; // Hz + constexpr float FLOAT_FREQUENCY_Y_BASE = 0.7F; // Hz + constexpr float FLOAT_PHASE_OFFSET = 1.57F; // π/2 (90°) + + // Delays de entrada (per a entrada escalonada) + constexpr float P1_ENTRY_DELAY = 0.0F; // P1 entra immediatament + constexpr float P2_ENTRY_DELAY = 0.5F; // P2 entra 0.5s después + + // Delay global antes de start l'animación de entrada al state MAIN + constexpr float ENTRANCE_DELAY = 5.0F; // Temps de espera antes que las naves entrin + + // Multiplicadors de freqüència para cada ship (variació sutil ±12%) + constexpr float P1_FREQUENCY_MULTIPLIER = 0.88F; // 12% més lenta + constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; // 12% més ràpida + + } // namespace Ships + + namespace Layout { + // Posicions verticals (anclatges des del TOP de pantalla lógica, 0.0-1.0) + constexpr float LOGO_POS = 0.20F; // Logo "ORNI" + constexpr float PRESS_START_POS = 0.75F; // "PRESS START TO PLAY" + constexpr float COPYRIGHT1_POS = 0.90F; // Primera línia copyright + + // Separacions relatives (proporció respecte Game::HEIGHT = 480px) + constexpr float LOGO_LINE_SPACING = 0.02F; // Entre "ORNI" i "ATTACK!" (10px) + constexpr float COPYRIGHT_LINE_SPACING = 0.0F; // Entre línies copyright (5px) + + // Factors de scale + constexpr float LOGO_SCALE = 0.6F; // Escala "ORNI ATTACK!" + constexpr float PRESS_START_SCALE = 1.0F; // Escala "PRESS START TO PLAY" + constexpr float COPYRIGHT_SCALE = 0.5F; // Escala copyright + constexpr float JAILGAMES_SCALE = 0.25F; // Escala del logo JAILGAMES pequeño sobre el copyright + + // Separación entre el logo JAILGAMES y la línea de copyright (proporción de Game::HEIGHT). + constexpr float JAILGAMES_COPYRIGHT_GAP = 0.015F; + + // Espaiat entre caràcters (usado per VectorText) + constexpr float TEXT_SPACING = 2.0F; + } // namespace Layout + } // namespace Title + + // Floating score numbers (números flotantes de puntuación) + namespace FloatingScore { + constexpr float LIFETIME = 2.0F; // Duració màxima (segons) + constexpr float VELOCITY_Y = -30.0F; // Velocidad vertical (px/s, negatiu = amunt) + constexpr float VELOCITY_X = 0.0F; // Velocidad horizontal (px/s) + constexpr float SCALE = 0.45F; // Escala del text (0.6 = 60% del marcador) + constexpr float SPACING = 0.0F; // Espaiat entre caràcters + constexpr int MAX_CONCURRENT = 15; // Pool size (= MAX_ORNIS) + } // namespace FloatingScore } // namespace Defaults From aa0abd9ae17e970a4af000104d07e169f46201a0 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 18:45:10 +0200 Subject: [PATCH 11/18] refactor: partir defaults.hpp en source/core/defaults/*.hpp (umbrella) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit defaults.hpp tenia 527 línies amb 17 namespaces de dominis distints (Window, Game, Zones, Entities, Palette, Ship, Physics, Math, Brightness, Rendering, Audio, Music, Sound, Controls, Enemies, Title, FloatingScore). 22 .cpp/.hpp l'incloïen, així que tocar una constant forçava recompilar pràcticament tot. Es divideix en 15 subfitxers (un per namespace, fusionant Music/Sound a audio.hpp i unificant els dos blocs Game duplicats en un sol): defaults/window.hpp defaults/audio.hpp defaults/game.hpp defaults/controls.hpp defaults/zones.hpp defaults/enemies.hpp defaults/entities.hpp defaults/title.hpp defaults/palette.hpp defaults/floating_score.hpp defaults/ship.hpp defaults/math.hpp defaults/physics.hpp defaults/brightness.hpp defaults/rendering.hpp Cross-deps explícites (#include en lloc d'order-of-declaration): zones.hpp -> game.hpp (per Game::WIDTH/HEIGHT) enemies.hpp -> entities.hpp (per SHIP_RADIUS) title.hpp -> game.hpp, math.hpp + defaults.hpp queda com a umbrella que inclou els 15 subfitxers. Els 22 includers existents no requereixen cap canvi. Codi nou pot incloure el subfitxer concret per millorar la compilació incremental. Hallazgos #22 i #30 de CODE_REVIEW.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/defaults.hpp | 548 +----------------------- source/core/defaults/audio.hpp | 47 ++ source/core/defaults/brightness.hpp | 23 + source/core/defaults/controls.hpp | 24 ++ source/core/defaults/enemies.hpp | 83 ++++ source/core/defaults/entities.hpp | 15 + source/core/defaults/floating_score.hpp | 15 + source/core/defaults/game.hpp | 91 ++++ source/core/defaults/math.hpp | 12 + source/core/defaults/palette.hpp | 19 + source/core/defaults/physics.hpp | 35 ++ source/core/defaults/rendering.hpp | 10 + source/core/defaults/ship.hpp | 16 + source/core/defaults/title.hpp | 129 ++++++ source/core/defaults/window.hpp | 18 + source/core/defaults/zones.hpp | 81 ++++ 16 files changed, 641 insertions(+), 525 deletions(-) create mode 100644 source/core/defaults/audio.hpp create mode 100644 source/core/defaults/brightness.hpp create mode 100644 source/core/defaults/controls.hpp create mode 100644 source/core/defaults/enemies.hpp create mode 100644 source/core/defaults/entities.hpp create mode 100644 source/core/defaults/floating_score.hpp create mode 100644 source/core/defaults/game.hpp create mode 100644 source/core/defaults/math.hpp create mode 100644 source/core/defaults/palette.hpp create mode 100644 source/core/defaults/physics.hpp create mode 100644 source/core/defaults/rendering.hpp create mode 100644 source/core/defaults/ship.hpp create mode 100644 source/core/defaults/title.hpp create mode 100644 source/core/defaults/window.hpp create mode 100644 source/core/defaults/zones.hpp diff --git a/source/core/defaults.hpp b/source/core/defaults.hpp index 9678341..528a61b 100644 --- a/source/core/defaults.hpp +++ b/source/core/defaults.hpp @@ -1,527 +1,25 @@ +// defaults.hpp - Umbrella header que reuneix totes les constants del joc. +// © 2026 JailDesigner +// +// El contingut viu ara a source/core/defaults/*.hpp (un fitxer per +// namespace). Es manté aquest umbrella per no haver de tocar els 22 +// includers existents. Codi nou pot incloure directament el subfitxer +// concret per millorar el temps de compilació incremental. + #pragma once -#include -#include -#include - -namespace Defaults { - // Configuración de ventana - namespace Window { - constexpr int WIDTH = 1280; - constexpr int HEIGHT = 720; - constexpr int MIN_WIDTH = 640; // Mínimo: mitad del baseline (16:9) - constexpr int MIN_HEIGHT = 360; - // Zoom system - constexpr float BASE_ZOOM = 1.0F; // 1280x720 baseline (16:9) - constexpr float MIN_ZOOM = 0.5F; // 640x360 minimum - constexpr float ZOOM_INCREMENT = 0.1F; // 10% steps (F1/F2) - constexpr bool FULLSCREEN = true; // Pantalla completa activada por defecto - } // namespace Window - - // Dimensiones base del juego (coordenadas lógicas, 16:9) - namespace Game { - constexpr int WIDTH = 1280; - constexpr int HEIGHT = 720; - } // namespace Game - - // Zones del juego (SDL_FRect con cálculos automáticos basat en porcentajes) - namespace Zones { - // --- CONFIGURACIÓ DE PORCENTATGES --- - // Todas las zones definides como a porcentajes de Game::WIDTH (640) i Game::HEIGHT (480) - - // Percentatges de height (divisió vertical) - constexpr float SCOREBOARD_TOP_HEIGHT_PERCENT = 0.02F; // 10% superior - constexpr float MAIN_PLAYAREA_HEIGHT_PERCENT = 0.88F; // 80% central - constexpr float SCOREBOARD_BOTTOM_HEIGHT_PERCENT = 0.10F; // 10% inferior - - // Padding horizontal para PLAYAREA (dentro de MAIN_PLAYAREA) - constexpr float PLAYAREA_PADDING_HORIZONTAL_PERCENT = 0.015F; // 5% a cada costat - - // --- CÀLCULS AUTOMÀTICS DE PÍXELS --- - // Cálculos automáticos a partir dels porcentajes - - // Alçades - constexpr float SCOREBOARD_TOP_H = Game::HEIGHT * SCOREBOARD_TOP_HEIGHT_PERCENT; - constexpr float MAIN_PLAYAREA_H = Game::HEIGHT * MAIN_PLAYAREA_HEIGHT_PERCENT; - constexpr float SCOREBOARD_BOTTOM_H = Game::HEIGHT * SCOREBOARD_BOTTOM_HEIGHT_PERCENT; - - // Posicions Y - constexpr float SCOREBOARD_TOP_Y = 0.0F; - constexpr float MAIN_PLAYAREA_Y = SCOREBOARD_TOP_H; - constexpr float SCOREBOARD_BOTTOM_Y = MAIN_PLAYAREA_Y + MAIN_PLAYAREA_H; - - // Padding horizontal de PLAYAREA - constexpr float PLAYAREA_PADDING_H = Game::WIDTH * PLAYAREA_PADDING_HORIZONTAL_PERCENT; - - // --- ZONES FINALS (SDL_FRect) --- - - // Marcador superior (reservado para futuro uso) - // Ocupa el 2% superior - constexpr SDL_FRect SCOREBOARD_TOP = { - 0.0F, // x = 0.0 - SCOREBOARD_TOP_Y, // y = 0.0 - static_cast(Game::WIDTH), // ancho completo - SCOREBOARD_TOP_H // alto - }; - - // Área de juego principal (contenedor del 80% central, sin padding) - // Ocupa el 88% central, ancho completo - constexpr SDL_FRect MAIN_PLAYAREA = { - 0.0F, // x = 0.0 - MAIN_PLAYAREA_Y, // debajo del scoreboard superior - static_cast(Game::WIDTH), // ancho completo - MAIN_PLAYAREA_H // alto - }; - - // Zona de juego real (con padding horizontal del 5%) - // Ocupa: dentro de MAIN_PLAYAREA, con márgenes laterales - // Se utiliza para límites del juego, colisiones, spawn - constexpr SDL_FRect PLAYAREA = { - PLAYAREA_PADDING_H, // padding horizontal - MAIN_PLAYAREA_Y, // debajo del scoreboard superior (igual que MAIN_PLAYAREA) - Game::WIDTH - (2.0F * PLAYAREA_PADDING_H), // ancho con padding - MAIN_PLAYAREA_H // alto (igual que MAIN_PLAYAREA) - }; - - // Marcador inferior (marcador actual) - // Ocupa el 10% inferior - constexpr SDL_FRect SCOREBOARD = { - 0.0F, // x = 0.0 - SCOREBOARD_BOTTOM_Y, // fondo - static_cast(Game::WIDTH), // ancho completo - SCOREBOARD_BOTTOM_H // alto - }; - - // Padding horizontal del marcador (para alinear zonas izquierda/derecha con PLAYAREA) - constexpr float SCOREBOARD_PADDING_H = 0.0F; // Game::WIDTH * 0.015f; - } // namespace Zones - - // Objetos del juego - namespace Entities { - constexpr int MAX_ORNIS = 15; - constexpr int MAX_BALES = 3; - - constexpr float SHIP_RADIUS = 12.0F; - constexpr float ENEMY_RADIUS = 20.0F; - constexpr float BULLET_RADIUS = 3.0F; - } // namespace Entities - - // Paleta semántica por tipo de entidad. Si una entity declara color, lo - // pasa al pipeline con alpha=255 (sentinela "color válido"); si no, se - // usa el color global del oscilador (g_current_line_color). - namespace Palette { - constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro - constexpr SDL_Color BULLET = {.r = 120, .g = 255, .b = 140, .a = 255}; // Verde laser - constexpr SDL_Color PENTAGON = {.r = 120, .g = 170, .b = 255, .a = 255}; // Azul "esquivador" - constexpr SDL_Color QUADRAT = {.r = 255, .g = 110, .b = 110, .a = 255}; // Rojo "tank" - constexpr SDL_Color MOLINILLO = {.r = 255, .g = 130, .b = 255, .a = 255}; // Magenta agresivo - } // namespace Palette - - // Ship (nave del player) - namespace Ship { - // Invulnerabilidad post-respawn - constexpr float INVULNERABILITY_DURATION = 3.0F; // Segundos de invulnerabilidad - - // Parpadeo visual durante invulnerabilidad - constexpr float BLINK_VISIBLE_TIME = 0.1F; // Tiempo visible (segundos) - constexpr float BLINK_INVISIBLE_TIME = 0.1F; // Tiempo invisible (segundos) - // Frecuencia total: 0.2s/ciclo = 5 Hz (~15 parpadeos en 3s) - } // namespace Ship - - // Game rules (lives, respawn, game over) - namespace Game { - constexpr int STARTING_LIVES = 3; // Initial lives - constexpr float DEATH_DURATION = 3.0F; // Seconds of death animation - constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over - constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 0.80F; // 80% hitbox (generous) - constexpr float COLLISION_BULLET_ENEMY_AMPLIFIER = 1.15F; // 115% hitbox (generous) - - // Friendly fire system - constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire - constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%) - constexpr float BULLET_GRACE_PERIOD = 0.2F; // Inmunidad post-disparo (s) - - // Transición LEVEL_START (mensajes aleatorios PRE-level) - constexpr float LEVEL_START_DURATION = 3.0F; // Duración total - constexpr float LEVEL_START_TYPING_RATIO = 0.3F; // 30% escribiendo, 70% mostrando - - // Transición LEVEL_COMPLETED (mensaje "GOOD JOB COMMANDER!") - constexpr float LEVEL_COMPLETED_DURATION = 3.0F; // Duración total - constexpr float LEVEL_COMPLETED_TYPING_RATIO = 0.0F; // 0.0 = sin typewriter (directo) - - // Transición INIT_HUD (animación inicial del HUD) - constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado - - // Ratios de animación (inicio y fin como porcentajes del tiempo total) - // RECT (rectángulo de márgenes) - constexpr float INIT_HUD_RECT_RATIO_INIT = 0.30F; - constexpr float INIT_HUD_RECT_RATIO_END = 0.85F; - - // SCORE (marcador de puntuación) - constexpr float INIT_HUD_SCORE_RATIO_INIT = 0.60F; - constexpr float INIT_HUD_SCORE_RATIO_END = 0.90F; - - // SHIP1 (nave player 1) - constexpr float INIT_HUD_SHIP1_RATIO_INIT = 0.0F; - constexpr float INIT_HUD_SHIP1_RATIO_END = 1.0F; - - // SHIP2 (nave player 2) - constexpr float INIT_HUD_SHIP2_RATIO_INIT = 0.20F; - constexpr float INIT_HUD_SHIP2_RATIO_END = 1.0F; - - // Posición inicial de la nave en INIT_HUD (75% de altura de zona de juego) - constexpr float INIT_HUD_SHIP_START_Y_RATIO = 0.75F; // 75% desde el top de PLAYAREA - - // Spawn positions (distribución horizontal para 2 jugadores) - constexpr float P1_SPAWN_X_RATIO = 0.33F; // 33% desde izquierda - constexpr float P2_SPAWN_X_RATIO = 0.67F; // 67% desde izquierda - constexpr float SPAWN_Y_RATIO = 0.75F; // 75% desde arriba - - // Continue system behavior - constexpr int CONTINUE_COUNT_START = 9; // Countdown starts at 9 - constexpr float CONTINUE_TICK_DURATION = 1.0F; // Seconds per countdown tick - constexpr int MAX_CONTINUES = 3; // Maximum continues per game - constexpr bool INFINITE_CONTINUES = false; // If true, unlimited continues - - // Continue screen visual configuration - namespace ContinueScreen { - // "CONTINUE" text - constexpr float CONTINUE_TEXT_SCALE = 2.0F; // Text size - constexpr float CONTINUE_TEXT_Y_RATIO = 0.30F; // 35% from top of PLAYAREA - - // Countdown number (9, 8, 7...) - constexpr float COUNTER_TEXT_SCALE = 4.0F; // Text size (large) - constexpr float COUNTER_TEXT_Y_RATIO = 0.50F; // 50% from top of PLAYAREA - - // "CONTINUES LEFT: X" text - constexpr float INFO_TEXT_SCALE = 0.7F; // Text size (small) - constexpr float INFO_TEXT_Y_RATIO = 0.75F; // 65% from top of PLAYAREA - } // namespace ContinueScreen - - // Game Over screen visual configuration - namespace GameOverScreen { - constexpr float TEXT_SCALE = 2.0F; // "GAME OVER" text size - constexpr float TEXT_SPACING = 4.0F; // Character spacing - } // namespace GameOverScreen - - // Stage message configuration (LEVEL_START, LEVEL_COMPLETED) - constexpr float STAGE_MESSAGE_Y_RATIO = 0.25F; // 25% from top of PLAYAREA - constexpr float STAGE_MESSAGE_MAX_WIDTH_RATIO = 0.9F; // 90% of PLAYAREA width - } // namespace Game - - // Física del control de la nau (px/s, rad/s) - namespace Physics { - constexpr float ROTATION_SPEED = 3.14F; // rad/s (~180°/s) - constexpr float ACCELERATION = 400.0F; // px/s² - constexpr float MAX_VELOCITY = 120.0F; // px/s - constexpr float FRICTION = 20.0F; // px/s² - - // Explosions (debris physics) - namespace Debris { - constexpr float VELOCITAT_BASE = 80.0F; // Velocidad inicial (px/s) - constexpr float VARIACIO_VELOCITAT = 40.0F; // ±variació aleatòria (px/s) - constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²) - constexpr float ROTACIO_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s) - constexpr float ROTACIO_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s) - constexpr float TEMPS_VIDA = 2.0F; // Duració màxima (segons) - enemy/bullet debris - constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris lifetime (matches DEATH_DURATION) - constexpr float SHRINK_RATE = 0.5F; // Reducció de mida (factor/s) - - // Herència de velocity angular (trayectorias curvas) - constexpr float FACTOR_HERENCIA_MIN = 0.7F; // Mínimo 70% del drotacio heredat - constexpr float FACTOR_HERENCIA_MAX = 1.0F; // Màxim 100% del drotacio heredat - constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²) - - // Angular velocity sin for trajectory inheritance - // Excess above this threshold is converted to tangential linear velocity - // Prevents "vortex trap" problem with high-rotation enemies - constexpr float VELOCITAT_ROT_MAX = 1.5F; // rad/s (~86°/s) - } // namespace Debris - } // namespace Physics - - // Matemáticas - namespace Math { - constexpr float PI = std::numbers::pi_v; - } // namespace Math - - // La antigua oscilación CPU (namespace Color) se ha migrado al shader de - // postpro. Los parámetros de flicker / background pulse viven ahora en - // data/config/postfx.yaml y se aplican en shaders/postfx.frag.glsl. - - // Brillantor (control de intensitat per cada type de entidad) - namespace Brightness { - // Brillantor estàtica per entidades de juego (0.0-1.0) - constexpr float NAU = 1.0F; // Màxima visibilitat (player) - constexpr float ENEMIC = 0.7F; // 30% més tènue (destaca menys) - constexpr float BALA = 1.0F; // Brillo a tope (màxima visibilitat) - - // Starfield: gradient segons distancia al centro - // distancia_centre: 0.0 (centro) → 1.0 (vora pantalla) - // brightness = MIN + (MAX - MIN) * distancia_centre - constexpr float STARFIELD_MIN = 0.3F; // Estrelles llunyanes (prop del centro) - constexpr float STARFIELD_MAX = 0.8F; // Estrelles properes (vora pantalla) - } // namespace Brightness - - // Renderització (V-Sync i altres opciones de render) - namespace Rendering { - constexpr int VSYNC_DEFAULT = 1; // 0=disabled, 1=enabled - } // namespace Rendering - - // Audio (sistema de sonido y música) — usado por Audio::Config en init() - namespace Audio { - constexpr bool ENABLED = true; // Audio habilitado por defecto - constexpr float VOLUME = 1.0F; // Volumen maestro (0..1) - constexpr bool MUSIC_ENABLED = true; // Música habilitada - constexpr float MUSIC_VOLUME = 0.8F; // Volumen música (0..1) - constexpr bool SOUND_ENABLED = true; // Efectos habilitados - constexpr float SOUND_VOLUME = 1.0F; // Volumen efectos (0..1) - constexpr float VOLUME_STEP = 0.05F; // Paso UI (5%) - constexpr int FREQUENCY = 48000; // Frecuencia de muestreo (Hz) - constexpr int CROSSFADE_MS = 1500; // Crossfade por defecto entre pistas (ms) - constexpr SDL_AudioFormat FORMAT = SDL_AUDIO_S16; // PCM 16-bit signed nativo - constexpr int CHANNELS = 2; // Estéreo - } // namespace Audio - - // Música (pistas de fondo) - namespace Music { - constexpr const char* GAME_TRACK = "game.ogg"; // Pista de juego - constexpr const char* TITLE_TRACK = "title.ogg"; // Pista de titulo - constexpr int FADE_DURATION_MS = 1000; // Fade out duration - } // namespace Music - - // Efectes de so (sons puntuals) - namespace Sound { - constexpr const char* CONTINUE = "effects/continue.wav"; // Cuenta atras - constexpr const char* EXPLOSION = "effects/explosion.wav"; // Explosión - constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa - constexpr const char* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; // Friendly fire hit - constexpr const char* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD - constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo - constexpr const char* LOGO = "effects/logo.wav"; // Logo - constexpr const char* START = "effects/start.wav"; // El player pulsa START - constexpr const char* GOOD_JOB_COMMANDER = "voices/good_job_commander.wav"; // Voz: "Good job, commander" - } // namespace Sound - - // Controls (mapeo de teclas para los jugadores) - namespace Controls { - namespace P1 { - constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_RIGHT; - constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_LEFT; - constexpr SDL_Scancode THRUST = SDL_SCANCODE_UP; - constexpr SDL_Keycode SHOOT = SDLK_SPACE; - } // namespace P1 - - namespace P2 { - constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_D; - constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_A; - constexpr SDL_Scancode THRUST = SDL_SCANCODE_W; - constexpr SDL_Keycode SHOOT = SDLK_LSHIFT; - } // namespace P2 - } // namespace Controls - - // Enemy type configuration (type de enemigos) - namespace Enemies { - // Pentagon (esquivador - zigzag evasion) - namespace Pentagon { - constexpr float VELOCITAT = 35.0F; // px/s (slightly slower) - constexpr float CANVI_ANGLE_PROB = 0.20F; // 20% per wall hit (frequent zigzag) - constexpr float CANVI_ANGLE_MAX = 1.0F; // Max random angle change (rad) - constexpr float DROTACIO_MIN = 0.75F; // Min visual rotation (rad/s) [+50%] - constexpr float DROTACIO_MAX = 3.75F; // Max visual rotation (rad/s) [+50%] - constexpr const char* SHAPE_FILE = "enemy_pentagon.shp"; - } // namespace Pentagon - - // Cuadrado (perseguidor - tracks player) - namespace Cuadrado { - constexpr float VELOCITAT = 40.0F; // px/s (medium speed) - constexpr float TRACKING_STRENGTH = 0.5F; // Interpolation toward player (0.0-1.0) - constexpr float TRACKING_INTERVAL = 1.0F; // Seconds between angle updates - constexpr float DROTACIO_MIN = 0.3F; // Slow rotation [+50%] - constexpr float DROTACIO_MAX = 1.5F; // [+50%] - constexpr const char* SHAPE_FILE = "enemy_square.shp"; - } // namespace Cuadrado - - // Molinillo (agressiu - fast straight lines, proximity spin-up) - namespace Molinillo { - constexpr float VELOCITAT = 50.0F; // px/s (fastest) - constexpr float CANVI_ANGLE_PROB = 0.05F; // 5% per wall hit (rare direction change) - constexpr float CANVI_ANGLE_MAX = 0.3F; // Small angle adjustments - constexpr float DROTACIO_MIN = 3.0F; // Base rotation (rad/s) [+50%] - constexpr float DROTACIO_MAX = 6.0F; // [+50%] - constexpr float DROTACIO_PROXIMITY_MULTIPLIER = 3.0F; // Spin-up multiplier when near ship - constexpr float PROXIMITY_DISTANCE = 100.0F; // Distance threshold (px) - constexpr const char* SHAPE_FILE = "enemy_pinwheel.shp"; - } // namespace Molinillo - - // Animation parameters (shared) - namespace Animation { - // Palpitation - constexpr float PALPITACIO_TRIGGER_PROB = 0.01F; // 1% chance per second - constexpr float PALPITACIO_DURACIO_MIN = 1.0F; // Min duration (seconds) - constexpr float PALPITACIO_DURACIO_MAX = 3.0F; // Max duration (seconds) - constexpr float PALPITACIO_AMPLITUD_MIN = 0.08F; // Min scale variation - constexpr float PALPITACIO_AMPLITUD_MAX = 0.20F; // Max scale variation - constexpr float PALPITACIO_FREQ_MIN = 1.5F; // Min frequency (Hz) - constexpr float PALPITACIO_FREQ_MAX = 3.0F; // Max frequency (Hz) - - // Rotation acceleration - constexpr float ROTACIO_ACCEL_TRIGGER_PROB = 0.02F; // 2% chance per second [4x more frequent] - constexpr float ROTACIO_ACCEL_DURACIO_MIN = 3.0F; // Min transition time - constexpr float ROTACIO_ACCEL_DURACIO_MAX = 8.0F; // Max transition time - constexpr float ROTACIO_ACCEL_MULTIPLIER_MIN = 0.3F; // Min speed multiplier [more dramatic] - constexpr float ROTACIO_ACCEL_MULTIPLIER_MAX = 4.0F; // Max speed multiplier [more dramatic] - } // namespace Animation - - // Spawn safety and invulnerability system - namespace Spawn { - // Safe spawn distance from player - constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0F; // 3x ship radius - constexpr float SAFETY_DISTANCE = Defaults::Entities::SHIP_RADIUS * SAFETY_DISTANCE_MULTIPLIER; // 36.0f px - constexpr int MAX_SPAWN_ATTEMPTS = 50; // Max attempts to find safe position - - // Invulnerability system - constexpr float INVULNERABILITY_DURATION = 3.0F; // Seconds - constexpr float INVULNERABILITY_BRIGHTNESS_START = 0.3F; // Dim - constexpr float INVULNERABILITY_BRIGHTNESS_END = 0.7F; // Normal (same as Defaults::Brightness::ENEMIC) - constexpr float INVULNERABILITY_SCALE_START = 0.0F; // Invisible - constexpr float INVULNERABILITY_SCALE_END = 1.0F; // Full size - } // namespace Spawn - - // Scoring system (puntuación per type de enemy) - namespace Scoring { - constexpr int PENTAGON_SCORE = 100; // Pentágono (esquivador, 35 px/s) - constexpr int QUADRAT_SCORE = 150; // Cuadrado (perseguidor, 40 px/s) - constexpr int MOLINILLO_SCORE = 200; // Molinillo (agressiu, 50 px/s) - } // namespace Scoring - - } // namespace Enemies - - // Title scene ship animations (naves 3D flotantes a l'escena de título) - namespace Title { - namespace Ships { - // ============================================================ - // PARÀMETRES BASE (ajustar aquí per experimentar) - // ============================================================ - - // 1. Escala global de las naves - constexpr float SHIP_BASE_SCALE = 2.5F; // Multiplicador (1.0 = mida original del .shp) - - // 2. Altura vertical (cercanía al centro) - // Ratio Y desde el centro de la pantalla (0.0 = centro, 1.0 = bottom de pantalla) - constexpr float TARGET_Y_RATIO = 0.15625F; - - // 3. Radio orbital (distance radial desde centro en coordenadas polares) - constexpr float CLOCK_RADIUS = 150.0F; // Distancia des del centro - - // 4. Ángulos de posición (clock positions en coordenadas polares) - // En coordenadas de pantalla: 0° = derecha, 90° = baix, 180° = izquierda, 270° = dalt - constexpr float CLOCK_8_ANGLE = 150.0F * Math::PI / 180.0F; // 8 o'clock (bottom-left) - constexpr float CLOCK_4_ANGLE = 30.0F * Math::PI / 180.0F; // 4 o'clock (bottom-right) - - // 5. Radio máximo de la shape de la nave (para calcular offset automáticamente) - constexpr float SHIP_MAX_RADIUS = 30.0F; // Radi del cercle circumscrit a ship_starfield.shp - - // 6. Margen de seguridad para offset de entrada - constexpr float ENTRY_OFFSET_MARGIN = 227.5F; // Para offset total de ~340px (ajustado) - - // ============================================================ - // VALORS DERIVATS (calculats automáticoament - NO modificar) - // ============================================================ - - // Centro de la pantalla (point de referència) - constexpr float CENTER_X = Game::WIDTH / 2.0F; // auto-derivado de Game::WIDTH - constexpr float CENTER_Y = Game::HEIGHT / 2.0F; // auto-derivado de Game::HEIGHT - - // Posicions target (calculades dinàmicament des dels parámetros base) - // Nota: std::cos/sin no són constexpr en C++20, pero funcionen en runtime - // Les funciones inline són optimitzades por el compilador (zero overhead) - inline auto p1TargetX() -> float { - return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_8_ANGLE)); - } - inline auto p1TargetY() -> float { - return CENTER_Y + ((Game::HEIGHT / 2.0F) * TARGET_Y_RATIO); - } - inline auto p2TargetX() -> float { - return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_4_ANGLE)); - } - inline auto p2TargetY() -> float { - return CENTER_Y + ((Game::HEIGHT / 2.0F) * TARGET_Y_RATIO); - } - - // Escales de animación (relatives a SHIP_BASE_SCALE) - constexpr float ENTRY_SCALE_START = 1.5F * SHIP_BASE_SCALE; // Entrada: 50% més grande - constexpr float FLOATING_SCALE = 1.0F * SHIP_BASE_SCALE; // Flotante: scale base - - // Offset de entrada (ajustat automáticoament a l'scale) - // Fórmula: (radi màxim de la ship * scale de entrada) + margen - constexpr float ENTRY_OFFSET = (SHIP_MAX_RADIUS * ENTRY_SCALE_START) + ENTRY_OFFSET_MARGIN; - - // Vec2 de fuga (centro para l'animación de salida) - constexpr float VANISHING_POINT_X = CENTER_X; // auto-derivado de Game::WIDTH - constexpr float VANISHING_POINT_Y = CENTER_Y; // auto-derivado de Game::HEIGHT - - // ============================================================ - // ANIMACIONS (durades, oscil·lacions, delays) - // ============================================================ - - // Durades de animación - constexpr float ENTRY_DURATION = 2.0F; // Entrada (segons) - constexpr float EXIT_DURATION = 1.0F; // Salida (segons) - - // Flotació (oscil·lació reduïda y diferenciada per ship) - constexpr float FLOAT_AMPLITUDE_X = 4.0F; // Amplitud X (píxels) - constexpr float FLOAT_AMPLITUDE_Y = 2.5F; // Amplitud Y (píxels) - - // Freqüències base - constexpr float FLOAT_FREQUENCY_X_BASE = 0.5F; // Hz - constexpr float FLOAT_FREQUENCY_Y_BASE = 0.7F; // Hz - constexpr float FLOAT_PHASE_OFFSET = 1.57F; // π/2 (90°) - - // Delays de entrada (per a entrada escalonada) - constexpr float P1_ENTRY_DELAY = 0.0F; // P1 entra immediatament - constexpr float P2_ENTRY_DELAY = 0.5F; // P2 entra 0.5s después - - // Delay global antes de start l'animación de entrada al state MAIN - constexpr float ENTRANCE_DELAY = 5.0F; // Temps de espera antes que las naves entrin - - // Multiplicadors de freqüència para cada ship (variació sutil ±12%) - constexpr float P1_FREQUENCY_MULTIPLIER = 0.88F; // 12% més lenta - constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; // 12% més ràpida - - } // namespace Ships - - namespace Layout { - // Posicions verticals (anclatges des del TOP de pantalla lógica, 0.0-1.0) - constexpr float LOGO_POS = 0.20F; // Logo "ORNI" - constexpr float PRESS_START_POS = 0.75F; // "PRESS START TO PLAY" - constexpr float COPYRIGHT1_POS = 0.90F; // Primera línia copyright - - // Separacions relatives (proporció respecte Game::HEIGHT = 480px) - constexpr float LOGO_LINE_SPACING = 0.02F; // Entre "ORNI" i "ATTACK!" (10px) - constexpr float COPYRIGHT_LINE_SPACING = 0.0F; // Entre línies copyright (5px) - - // Factors de scale - constexpr float LOGO_SCALE = 0.6F; // Escala "ORNI ATTACK!" - constexpr float PRESS_START_SCALE = 1.0F; // Escala "PRESS START TO PLAY" - constexpr float COPYRIGHT_SCALE = 0.5F; // Escala copyright - constexpr float JAILGAMES_SCALE = 0.25F; // Escala del logo JAILGAMES pequeño sobre el copyright - - // Separación entre el logo JAILGAMES y la línea de copyright (proporción de Game::HEIGHT). - constexpr float JAILGAMES_COPYRIGHT_GAP = 0.015F; - - // Espaiat entre caràcters (usado per VectorText) - constexpr float TEXT_SPACING = 2.0F; - } // namespace Layout - } // namespace Title - - // Floating score numbers (números flotantes de puntuación) - namespace FloatingScore { - constexpr float LIFETIME = 2.0F; // Duració màxima (segons) - constexpr float VELOCITY_Y = -30.0F; // Velocidad vertical (px/s, negatiu = amunt) - constexpr float VELOCITY_X = 0.0F; // Velocidad horizontal (px/s) - constexpr float SCALE = 0.45F; // Escala del text (0.6 = 60% del marcador) - constexpr float SPACING = 0.0F; // Espaiat entre caràcters - constexpr int MAX_CONCURRENT = 15; // Pool size (= MAX_ORNIS) - } // namespace FloatingScore - -} // namespace Defaults +#include "core/defaults/audio.hpp" +#include "core/defaults/brightness.hpp" +#include "core/defaults/controls.hpp" +#include "core/defaults/enemies.hpp" +#include "core/defaults/entities.hpp" +#include "core/defaults/floating_score.hpp" +#include "core/defaults/game.hpp" +#include "core/defaults/math.hpp" +#include "core/defaults/palette.hpp" +#include "core/defaults/physics.hpp" +#include "core/defaults/rendering.hpp" +#include "core/defaults/ship.hpp" +#include "core/defaults/title.hpp" +#include "core/defaults/window.hpp" +#include "core/defaults/zones.hpp" diff --git a/source/core/defaults/audio.hpp b/source/core/defaults/audio.hpp new file mode 100644 index 0000000..6000c0f --- /dev/null +++ b/source/core/defaults/audio.hpp @@ -0,0 +1,47 @@ +// audio.hpp - Configuració d'audio (sistema), pistes de música i efectes +// © 2026 JailDesigner + +#pragma once + +#include + +// Audio (sistema de sonido y música) — usado por Audio::Config en init() +namespace Defaults::Audio { + + constexpr bool ENABLED = true; // Audio habilitado por defecto + constexpr float VOLUME = 1.0F; // Volumen maestro (0..1) + constexpr bool MUSIC_ENABLED = true; // Música habilitada + constexpr float MUSIC_VOLUME = 0.8F; // Volumen música (0..1) + constexpr bool SOUND_ENABLED = true; // Efectos habilitados + constexpr float SOUND_VOLUME = 1.0F; // Volumen efectos (0..1) + constexpr float VOLUME_STEP = 0.05F; // Paso UI (5%) + constexpr int FREQUENCY = 48000; // Frecuencia de muestreo (Hz) + constexpr int CROSSFADE_MS = 1500; // Crossfade por defecto entre pistas (ms) + constexpr SDL_AudioFormat FORMAT = SDL_AUDIO_S16; // PCM 16-bit signed nativo + constexpr int CHANNELS = 2; // Estéreo + +} // namespace Defaults::Audio + +// Música (pistas de fondo) +namespace Defaults::Music { + + constexpr const char* GAME_TRACK = "game.ogg"; // Pista de juego + constexpr const char* TITLE_TRACK = "title.ogg"; // Pista de titulo + constexpr int FADE_DURATION_MS = 1000; // Fade out duration + +} // namespace Defaults::Music + +// Efectes de so (sons puntuals) +namespace Defaults::Sound { + + constexpr const char* CONTINUE = "effects/continue.wav"; // Cuenta atras + constexpr const char* EXPLOSION = "effects/explosion.wav"; // Explosión + constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa + constexpr const char* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; // Friendly fire hit + constexpr const char* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD + constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo + constexpr const char* LOGO = "effects/logo.wav"; // Logo + constexpr const char* START = "effects/start.wav"; // El player pulsa START + constexpr const char* GOOD_JOB_COMMANDER = "voices/good_job_commander.wav"; // Voz: "Good job, commander" + +} // namespace Defaults::Sound diff --git a/source/core/defaults/brightness.hpp b/source/core/defaults/brightness.hpp new file mode 100644 index 0000000..446e311 --- /dev/null +++ b/source/core/defaults/brightness.hpp @@ -0,0 +1,23 @@ +// brightness.hpp - Control d'intensitat per tipus d'entitat i starfield +// © 2026 JailDesigner + +#pragma once + +// La antigua oscilación CPU (namespace Color) se ha migrado al shader de +// postpro. Los parámetros de flicker / background pulse viven ahora en +// data/config/postfx.yaml y se aplican en shaders/postfx.frag.glsl. + +namespace Defaults::Brightness { + + // Brillantor estàtica per entidades de juego (0.0-1.0) + constexpr float NAU = 1.0F; // Màxima visibilitat (player) + constexpr float ENEMIC = 0.7F; // 30% més tènue (destaca menys) + constexpr float BALA = 1.0F; // Brillo a tope (màxima visibilitat) + + // Starfield: gradient segons distancia al centro + // distancia_centre: 0.0 (centro) → 1.0 (vora pantalla) + // brightness = MIN + (MAX - MIN) * distancia_centre + constexpr float STARFIELD_MIN = 0.3F; // Estrelles llunyanes (prop del centro) + constexpr float STARFIELD_MAX = 0.8F; // Estrelles properes (vora pantalla) + +} // namespace Defaults::Brightness diff --git a/source/core/defaults/controls.hpp b/source/core/defaults/controls.hpp new file mode 100644 index 0000000..e512fbf --- /dev/null +++ b/source/core/defaults/controls.hpp @@ -0,0 +1,24 @@ +// controls.hpp - Mapeig de tecles per defecte dels jugadors +// © 2026 JailDesigner + +#pragma once + +#include + +namespace Defaults::Controls { + + namespace P1 { + constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_RIGHT; + constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_LEFT; + constexpr SDL_Scancode THRUST = SDL_SCANCODE_UP; + constexpr SDL_Keycode SHOOT = SDLK_SPACE; + } // namespace P1 + + namespace P2 { + constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_D; + constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_A; + constexpr SDL_Scancode THRUST = SDL_SCANCODE_W; + constexpr SDL_Keycode SHOOT = SDLK_LSHIFT; + } // namespace P2 + +} // namespace Defaults::Controls diff --git a/source/core/defaults/enemies.hpp b/source/core/defaults/enemies.hpp new file mode 100644 index 0000000..b4c81ee --- /dev/null +++ b/source/core/defaults/enemies.hpp @@ -0,0 +1,83 @@ +// enemies.hpp - Configuració per tipus d'enemic (Pentagon/Cuadrado/Molinillo), spawn i scoring +// © 2026 JailDesigner + +#pragma once + +#include "core/defaults/entities.hpp" + +namespace Defaults::Enemies { + + // Pentagon (esquivador - zigzag evasion) + namespace Pentagon { + constexpr float VELOCITAT = 35.0F; // px/s (slightly slower) + constexpr float CANVI_ANGLE_PROB = 0.20F; // 20% per wall hit (frequent zigzag) + constexpr float CANVI_ANGLE_MAX = 1.0F; // Max random angle change (rad) + constexpr float DROTACIO_MIN = 0.75F; // Min visual rotation (rad/s) [+50%] + constexpr float DROTACIO_MAX = 3.75F; // Max visual rotation (rad/s) [+50%] + constexpr const char* SHAPE_FILE = "enemy_pentagon.shp"; + } // namespace Pentagon + + // Cuadrado (perseguidor - tracks player) + namespace Cuadrado { + constexpr float VELOCITAT = 40.0F; // px/s (medium speed) + constexpr float TRACKING_STRENGTH = 0.5F; // Interpolation toward player (0.0-1.0) + constexpr float TRACKING_INTERVAL = 1.0F; // Seconds between angle updates + constexpr float DROTACIO_MIN = 0.3F; // Slow rotation [+50%] + constexpr float DROTACIO_MAX = 1.5F; // [+50%] + constexpr const char* SHAPE_FILE = "enemy_square.shp"; + } // namespace Cuadrado + + // Molinillo (agressiu - fast straight lines, proximity spin-up) + namespace Molinillo { + constexpr float VELOCITAT = 50.0F; // px/s (fastest) + constexpr float CANVI_ANGLE_PROB = 0.05F; // 5% per wall hit (rare direction change) + constexpr float CANVI_ANGLE_MAX = 0.3F; // Small angle adjustments + constexpr float DROTACIO_MIN = 3.0F; // Base rotation (rad/s) [+50%] + constexpr float DROTACIO_MAX = 6.0F; // [+50%] + constexpr float DROTACIO_PROXIMITY_MULTIPLIER = 3.0F; // Spin-up multiplier when near ship + constexpr float PROXIMITY_DISTANCE = 100.0F; // Distance threshold (px) + constexpr const char* SHAPE_FILE = "enemy_pinwheel.shp"; + } // namespace Molinillo + + // Animation parameters (shared) + namespace Animation { + // Palpitation + constexpr float PALPITACIO_TRIGGER_PROB = 0.01F; // 1% chance per second + constexpr float PALPITACIO_DURACIO_MIN = 1.0F; // Min duration (seconds) + constexpr float PALPITACIO_DURACIO_MAX = 3.0F; // Max duration (seconds) + constexpr float PALPITACIO_AMPLITUD_MIN = 0.08F; // Min scale variation + constexpr float PALPITACIO_AMPLITUD_MAX = 0.20F; // Max scale variation + constexpr float PALPITACIO_FREQ_MIN = 1.5F; // Min frequency (Hz) + constexpr float PALPITACIO_FREQ_MAX = 3.0F; // Max frequency (Hz) + + // Rotation acceleration + constexpr float ROTACIO_ACCEL_TRIGGER_PROB = 0.02F; // 2% chance per second [4x more frequent] + constexpr float ROTACIO_ACCEL_DURACIO_MIN = 3.0F; // Min transition time + constexpr float ROTACIO_ACCEL_DURACIO_MAX = 8.0F; // Max transition time + constexpr float ROTACIO_ACCEL_MULTIPLIER_MIN = 0.3F; // Min speed multiplier [more dramatic] + constexpr float ROTACIO_ACCEL_MULTIPLIER_MAX = 4.0F; // Max speed multiplier [more dramatic] + } // namespace Animation + + // Spawn safety and invulnerability system + namespace Spawn { + // Safe spawn distance from player + constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0F; // 3x ship radius + constexpr float SAFETY_DISTANCE = Defaults::Entities::SHIP_RADIUS * SAFETY_DISTANCE_MULTIPLIER; // 36.0f px + constexpr int MAX_SPAWN_ATTEMPTS = 50; // Max attempts to find safe position + + // Invulnerability system + constexpr float INVULNERABILITY_DURATION = 3.0F; // Seconds + constexpr float INVULNERABILITY_BRIGHTNESS_START = 0.3F; // Dim + constexpr float INVULNERABILITY_BRIGHTNESS_END = 0.7F; // Normal (same as Defaults::Brightness::ENEMIC) + constexpr float INVULNERABILITY_SCALE_START = 0.0F; // Invisible + constexpr float INVULNERABILITY_SCALE_END = 1.0F; // Full size + } // namespace Spawn + + // Scoring system (puntuación per type de enemy) + namespace Scoring { + constexpr int PENTAGON_SCORE = 100; // Pentágono (esquivador, 35 px/s) + constexpr int QUADRAT_SCORE = 150; // Cuadrado (perseguidor, 40 px/s) + constexpr int MOLINILLO_SCORE = 200; // Molinillo (agressiu, 50 px/s) + } // namespace Scoring + +} // namespace Defaults::Enemies diff --git a/source/core/defaults/entities.hpp b/source/core/defaults/entities.hpp new file mode 100644 index 0000000..4bd5de4 --- /dev/null +++ b/source/core/defaults/entities.hpp @@ -0,0 +1,15 @@ +// entities.hpp - Configuració d'objectes del joc (límits i radis de col·lisió) +// © 2026 JailDesigner + +#pragma once + +namespace Defaults::Entities { + + constexpr int MAX_ORNIS = 15; + constexpr int MAX_BALES = 3; + + constexpr float SHIP_RADIUS = 12.0F; + constexpr float ENEMY_RADIUS = 20.0F; + constexpr float BULLET_RADIUS = 3.0F; + +} // namespace Defaults::Entities diff --git a/source/core/defaults/floating_score.hpp b/source/core/defaults/floating_score.hpp new file mode 100644 index 0000000..e69c1dd --- /dev/null +++ b/source/core/defaults/floating_score.hpp @@ -0,0 +1,15 @@ +// floating_score.hpp - Números flotants de puntuació +// © 2026 JailDesigner + +#pragma once + +namespace Defaults::FloatingScore { + + constexpr float LIFETIME = 2.0F; // Duració màxima (segons) + constexpr float VELOCITY_Y = -30.0F; // Velocidad vertical (px/s, negatiu = amunt) + constexpr float VELOCITY_X = 0.0F; // Velocidad horizontal (px/s) + constexpr float SCALE = 0.45F; // Escala del text (0.6 = 60% del marcador) + constexpr float SPACING = 0.0F; // Espaiat entre caràcters + constexpr int MAX_CONCURRENT = 15; // Pool size (= MAX_ORNIS) + +} // namespace Defaults::FloatingScore diff --git a/source/core/defaults/game.hpp b/source/core/defaults/game.hpp new file mode 100644 index 0000000..e5f4c9d --- /dev/null +++ b/source/core/defaults/game.hpp @@ -0,0 +1,91 @@ +// game.hpp - Dimensions del joc i regles de partida (vides, durades, colisions) +// © 2026 JailDesigner + +#pragma once + +namespace Defaults::Game { + + // Dimensiones base del juego (coordenadas lógicas, 16:9) + constexpr int WIDTH = 1280; + constexpr int HEIGHT = 720; + + // Regles de partida + constexpr int STARTING_LIVES = 3; // Initial lives + constexpr float DEATH_DURATION = 3.0F; // Seconds of death animation + constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over + constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 0.80F; // 80% hitbox (generous) + constexpr float COLLISION_BULLET_ENEMY_AMPLIFIER = 1.15F; // 115% hitbox (generous) + + // Friendly fire system + constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire + constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%) + constexpr float BULLET_GRACE_PERIOD = 0.2F; // Inmunidad post-disparo (s) + + // Transición LEVEL_START (mensajes aleatorios PRE-level) + constexpr float LEVEL_START_DURATION = 3.0F; // Duración total + constexpr float LEVEL_START_TYPING_RATIO = 0.3F; // 30% escribiendo, 70% mostrando + + // Transición LEVEL_COMPLETED (mensaje "GOOD JOB COMMANDER!") + constexpr float LEVEL_COMPLETED_DURATION = 3.0F; // Duración total + constexpr float LEVEL_COMPLETED_TYPING_RATIO = 0.0F; // 0.0 = sin typewriter (directo) + + // Transición INIT_HUD (animación inicial del HUD) + constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado + + // Ratios de animación (inicio y fin como porcentajes del tiempo total) + // RECT (rectángulo de márgenes) + constexpr float INIT_HUD_RECT_RATIO_INIT = 0.30F; + constexpr float INIT_HUD_RECT_RATIO_END = 0.85F; + + // SCORE (marcador de puntuación) + constexpr float INIT_HUD_SCORE_RATIO_INIT = 0.60F; + constexpr float INIT_HUD_SCORE_RATIO_END = 0.90F; + + // SHIP1 (nave player 1) + constexpr float INIT_HUD_SHIP1_RATIO_INIT = 0.0F; + constexpr float INIT_HUD_SHIP1_RATIO_END = 1.0F; + + // SHIP2 (nave player 2) + constexpr float INIT_HUD_SHIP2_RATIO_INIT = 0.20F; + constexpr float INIT_HUD_SHIP2_RATIO_END = 1.0F; + + // Posición inicial de la nave en INIT_HUD (75% de altura de zona de juego) + constexpr float INIT_HUD_SHIP_START_Y_RATIO = 0.75F; // 75% desde el top de PLAYAREA + + // Spawn positions (distribución horizontal para 2 jugadores) + constexpr float P1_SPAWN_X_RATIO = 0.33F; // 33% desde izquierda + constexpr float P2_SPAWN_X_RATIO = 0.67F; // 67% desde izquierda + constexpr float SPAWN_Y_RATIO = 0.75F; // 75% desde arriba + + // Continue system behavior + constexpr int CONTINUE_COUNT_START = 9; // Countdown starts at 9 + constexpr float CONTINUE_TICK_DURATION = 1.0F; // Seconds per countdown tick + constexpr int MAX_CONTINUES = 3; // Maximum continues per game + constexpr bool INFINITE_CONTINUES = false; // If true, unlimited continues + + // Continue screen visual configuration + namespace ContinueScreen { + // "CONTINUE" text + constexpr float CONTINUE_TEXT_SCALE = 2.0F; // Text size + constexpr float CONTINUE_TEXT_Y_RATIO = 0.30F; // 35% from top of PLAYAREA + + // Countdown number (9, 8, 7...) + constexpr float COUNTER_TEXT_SCALE = 4.0F; // Text size (large) + constexpr float COUNTER_TEXT_Y_RATIO = 0.50F; // 50% from top of PLAYAREA + + // "CONTINUES LEFT: X" text + constexpr float INFO_TEXT_SCALE = 0.7F; // Text size (small) + constexpr float INFO_TEXT_Y_RATIO = 0.75F; // 65% from top of PLAYAREA + } // namespace ContinueScreen + + // Game Over screen visual configuration + namespace GameOverScreen { + constexpr float TEXT_SCALE = 2.0F; // "GAME OVER" text size + constexpr float TEXT_SPACING = 4.0F; // Character spacing + } // namespace GameOverScreen + + // Stage message configuration (LEVEL_START, LEVEL_COMPLETED) + constexpr float STAGE_MESSAGE_Y_RATIO = 0.25F; // 25% from top of PLAYAREA + constexpr float STAGE_MESSAGE_MAX_WIDTH_RATIO = 0.9F; // 90% of PLAYAREA width + +} // namespace Defaults::Game diff --git a/source/core/defaults/math.hpp b/source/core/defaults/math.hpp new file mode 100644 index 0000000..7cdf1db --- /dev/null +++ b/source/core/defaults/math.hpp @@ -0,0 +1,12 @@ +// math.hpp - Constants matemàtiques +// © 2026 JailDesigner + +#pragma once + +#include + +namespace Defaults::Math { + + constexpr float PI = std::numbers::pi_v; + +} // namespace Defaults::Math diff --git a/source/core/defaults/palette.hpp b/source/core/defaults/palette.hpp new file mode 100644 index 0000000..2c3eae5 --- /dev/null +++ b/source/core/defaults/palette.hpp @@ -0,0 +1,19 @@ +// palette.hpp - Paleta semàntica per tipus d'entitat +// © 2026 JailDesigner + +#pragma once + +#include + +// Paleta semántica por tipo de entidad. Si una entity declara color, lo +// pasa al pipeline con alpha=255 (sentinela "color válido"); si no, se +// usa el color global del oscilador (g_current_line_color). +namespace Defaults::Palette { + + constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro + constexpr SDL_Color BULLET = {.r = 120, .g = 255, .b = 140, .a = 255}; // Verde laser + constexpr SDL_Color PENTAGON = {.r = 120, .g = 170, .b = 255, .a = 255}; // Azul "esquivador" + constexpr SDL_Color QUADRAT = {.r = 255, .g = 110, .b = 110, .a = 255}; // Rojo "tank" + constexpr SDL_Color MOLINILLO = {.r = 255, .g = 130, .b = 255, .a = 255}; // Magenta agresivo + +} // namespace Defaults::Palette diff --git a/source/core/defaults/physics.hpp b/source/core/defaults/physics.hpp new file mode 100644 index 0000000..77704f5 --- /dev/null +++ b/source/core/defaults/physics.hpp @@ -0,0 +1,35 @@ +// physics.hpp - Constants de física del control de la nau i debris d'explosió +// © 2026 JailDesigner + +#pragma once + +namespace Defaults::Physics { + + constexpr float ROTATION_SPEED = 3.14F; // rad/s (~180°/s) + constexpr float ACCELERATION = 400.0F; // px/s² + constexpr float MAX_VELOCITY = 120.0F; // px/s + constexpr float FRICTION = 20.0F; // px/s² + + // Explosions (debris physics) + namespace Debris { + constexpr float VELOCITAT_BASE = 80.0F; // Velocidad inicial (px/s) + constexpr float VARIACIO_VELOCITAT = 40.0F; // ±variació aleatòria (px/s) + constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²) + constexpr float ROTACIO_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s) + constexpr float ROTACIO_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s) + constexpr float TEMPS_VIDA = 2.0F; // Duració màxima (segons) - enemy/bullet debris + constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris lifetime (matches DEATH_DURATION) + constexpr float SHRINK_RATE = 0.5F; // Reducció de mida (factor/s) + + // Herència de velocity angular (trayectorias curvas) + constexpr float FACTOR_HERENCIA_MIN = 0.7F; // Mínimo 70% del drotacio heredat + constexpr float FACTOR_HERENCIA_MAX = 1.0F; // Màxim 100% del drotacio heredat + constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²) + + // Angular velocity sin for trajectory inheritance + // Excess above this threshold is converted to tangential linear velocity + // Prevents "vortex trap" problem with high-rotation enemies + constexpr float VELOCITAT_ROT_MAX = 1.5F; // rad/s (~86°/s) + } // namespace Debris + +} // namespace Defaults::Physics diff --git a/source/core/defaults/rendering.hpp b/source/core/defaults/rendering.hpp new file mode 100644 index 0000000..b6a636f --- /dev/null +++ b/source/core/defaults/rendering.hpp @@ -0,0 +1,10 @@ +// rendering.hpp - Opcions de renderització +// © 2026 JailDesigner + +#pragma once + +namespace Defaults::Rendering { + + constexpr int VSYNC_DEFAULT = 1; // 0=disabled, 1=enabled + +} // namespace Defaults::Rendering diff --git a/source/core/defaults/ship.hpp b/source/core/defaults/ship.hpp new file mode 100644 index 0000000..6156a3d --- /dev/null +++ b/source/core/defaults/ship.hpp @@ -0,0 +1,16 @@ +// ship.hpp - Configuració de la nau (invulnerabilitat, parpelleig) +// © 2026 JailDesigner + +#pragma once + +namespace Defaults::Ship { + + // Invulnerabilidad post-respawn + constexpr float INVULNERABILITY_DURATION = 3.0F; // Segundos de invulnerabilidad + + // Parpadeo visual durante invulnerabilidad + constexpr float BLINK_VISIBLE_TIME = 0.1F; // Tiempo visible (segundos) + constexpr float BLINK_INVISIBLE_TIME = 0.1F; // Tiempo invisible (segundos) + // Frecuencia total: 0.2s/ciclo = 5 Hz (~15 parpadeos en 3s) + +} // namespace Defaults::Ship diff --git a/source/core/defaults/title.hpp b/source/core/defaults/title.hpp new file mode 100644 index 0000000..cf2922b --- /dev/null +++ b/source/core/defaults/title.hpp @@ -0,0 +1,129 @@ +// title.hpp - Animacions de naves i layout de l'escena de títol +// © 2026 JailDesigner + +#pragma once + +#include + +#include "core/defaults/game.hpp" +#include "core/defaults/math.hpp" + +// Title scene ship animations (naves 3D flotantes a l'escena de título) +namespace Defaults::Title { + + namespace Ships { + // ============================================================ + // PARÀMETRES BASE (ajustar aquí per experimentar) + // ============================================================ + + // 1. Escala global de las naves + constexpr float SHIP_BASE_SCALE = 2.5F; // Multiplicador (1.0 = mida original del .shp) + + // 2. Altura vertical (cercanía al centro) + // Ratio Y desde el centro de la pantalla (0.0 = centro, 1.0 = bottom de pantalla) + constexpr float TARGET_Y_RATIO = 0.15625F; + + // 3. Radio orbital (distance radial desde centro en coordenadas polares) + constexpr float CLOCK_RADIUS = 150.0F; // Distancia des del centro + + // 4. Ángulos de posición (clock positions en coordenadas polares) + // En coordenadas de pantalla: 0° = derecha, 90° = baix, 180° = izquierda, 270° = dalt + constexpr float CLOCK_8_ANGLE = 150.0F * Math::PI / 180.0F; // 8 o'clock (bottom-left) + constexpr float CLOCK_4_ANGLE = 30.0F * Math::PI / 180.0F; // 4 o'clock (bottom-right) + + // 5. Radio máximo de la shape de la nave (para calcular offset automáticamente) + constexpr float SHIP_MAX_RADIUS = 30.0F; // Radi del cercle circumscrit a ship_starfield.shp + + // 6. Margen de seguridad para offset de entrada + constexpr float ENTRY_OFFSET_MARGIN = 227.5F; // Para offset total de ~340px (ajustado) + + // ============================================================ + // VALORS DERIVATS (calculats automáticoament - NO modificar) + // ============================================================ + + // Centro de la pantalla (point de referència) + constexpr float CENTER_X = Game::WIDTH / 2.0F; // auto-derivado de Game::WIDTH + constexpr float CENTER_Y = Game::HEIGHT / 2.0F; // auto-derivado de Game::HEIGHT + + // Posicions target (calculades dinàmicament des dels parámetros base) + // Nota: std::cos/sin no són constexpr en C++20, pero funcionen en runtime + // Les funciones inline són optimitzades por el compilador (zero overhead) + inline auto p1TargetX() -> float { + return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_8_ANGLE)); + } + inline auto p1TargetY() -> float { + return CENTER_Y + ((Game::HEIGHT / 2.0F) * TARGET_Y_RATIO); + } + inline auto p2TargetX() -> float { + return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_4_ANGLE)); + } + inline auto p2TargetY() -> float { + return CENTER_Y + ((Game::HEIGHT / 2.0F) * TARGET_Y_RATIO); + } + + // Escales de animación (relatives a SHIP_BASE_SCALE) + constexpr float ENTRY_SCALE_START = 1.5F * SHIP_BASE_SCALE; // Entrada: 50% més grande + constexpr float FLOATING_SCALE = 1.0F * SHIP_BASE_SCALE; // Flotante: scale base + + // Offset de entrada (ajustat automáticoament a l'scale) + // Fórmula: (radi màxim de la ship * scale de entrada) + margen + constexpr float ENTRY_OFFSET = (SHIP_MAX_RADIUS * ENTRY_SCALE_START) + ENTRY_OFFSET_MARGIN; + + // Vec2 de fuga (centro para l'animación de salida) + constexpr float VANISHING_POINT_X = CENTER_X; // auto-derivado de Game::WIDTH + constexpr float VANISHING_POINT_Y = CENTER_Y; // auto-derivado de Game::HEIGHT + + // ============================================================ + // ANIMACIONS (durades, oscil·lacions, delays) + // ============================================================ + + // Durades de animación + constexpr float ENTRY_DURATION = 2.0F; // Entrada (segons) + constexpr float EXIT_DURATION = 1.0F; // Salida (segons) + + // Flotació (oscil·lació reduïda y diferenciada per ship) + constexpr float FLOAT_AMPLITUDE_X = 4.0F; // Amplitud X (píxels) + constexpr float FLOAT_AMPLITUDE_Y = 2.5F; // Amplitud Y (píxels) + + // Freqüències base + constexpr float FLOAT_FREQUENCY_X_BASE = 0.5F; // Hz + constexpr float FLOAT_FREQUENCY_Y_BASE = 0.7F; // Hz + constexpr float FLOAT_PHASE_OFFSET = 1.57F; // π/2 (90°) + + // Delays de entrada (per a entrada escalonada) + constexpr float P1_ENTRY_DELAY = 0.0F; // P1 entra immediatament + constexpr float P2_ENTRY_DELAY = 0.5F; // P2 entra 0.5s después + + // Delay global antes de start l'animación de entrada al state MAIN + constexpr float ENTRANCE_DELAY = 5.0F; // Temps de espera antes que las naves entrin + + // Multiplicadors de freqüència para cada ship (variació sutil ±12%) + constexpr float P1_FREQUENCY_MULTIPLIER = 0.88F; // 12% més lenta + constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; // 12% més ràpida + + } // namespace Ships + + namespace Layout { + // Posicions verticals (anclatges des del TOP de pantalla lógica, 0.0-1.0) + constexpr float LOGO_POS = 0.20F; // Logo "ORNI" + constexpr float PRESS_START_POS = 0.75F; // "PRESS START TO PLAY" + constexpr float COPYRIGHT1_POS = 0.90F; // Primera línia copyright + + // Separacions relatives (proporció respecte Game::HEIGHT = 480px) + constexpr float LOGO_LINE_SPACING = 0.02F; // Entre "ORNI" i "ATTACK!" (10px) + constexpr float COPYRIGHT_LINE_SPACING = 0.0F; // Entre línies copyright (5px) + + // Factors de scale + constexpr float LOGO_SCALE = 0.6F; // Escala "ORNI ATTACK!" + constexpr float PRESS_START_SCALE = 1.0F; // Escala "PRESS START TO PLAY" + constexpr float COPYRIGHT_SCALE = 0.5F; // Escala copyright + constexpr float JAILGAMES_SCALE = 0.25F; // Escala del logo JAILGAMES pequeño sobre el copyright + + // Separación entre el logo JAILGAMES y la línea de copyright (proporción de Game::HEIGHT). + constexpr float JAILGAMES_COPYRIGHT_GAP = 0.015F; + + // Espaiat entre caràcters (usado per VectorText) + constexpr float TEXT_SPACING = 2.0F; + } // namespace Layout + +} // namespace Defaults::Title diff --git a/source/core/defaults/window.hpp b/source/core/defaults/window.hpp new file mode 100644 index 0000000..8d1d665 --- /dev/null +++ b/source/core/defaults/window.hpp @@ -0,0 +1,18 @@ +// window.hpp - Configuració de la finestra (mida, fullscreen, zoom) +// © 2026 JailDesigner + +#pragma once + +namespace Defaults::Window { + + constexpr int WIDTH = 1280; + constexpr int HEIGHT = 720; + constexpr int MIN_WIDTH = 640; // Mínimo: mitad del baseline (16:9) + constexpr int MIN_HEIGHT = 360; + // Zoom system + constexpr float BASE_ZOOM = 1.0F; // 1280x720 baseline (16:9) + constexpr float MIN_ZOOM = 0.5F; // 640x360 minimum + constexpr float ZOOM_INCREMENT = 0.1F; // 10% steps (F1/F2) + constexpr bool FULLSCREEN = true; // Pantalla completa activada por defecto + +} // namespace Defaults::Window diff --git a/source/core/defaults/zones.hpp b/source/core/defaults/zones.hpp new file mode 100644 index 0000000..d643d82 --- /dev/null +++ b/source/core/defaults/zones.hpp @@ -0,0 +1,81 @@ +// zones.hpp - Zones de l'àrea de joc (SDL_FRect amb càlculs automàtics per percentatges) +// © 2026 JailDesigner + +#pragma once + +#include + +#include "core/defaults/game.hpp" + +namespace Defaults::Zones { + + // --- CONFIGURACIÓ DE PORCENTATGES --- + // Todas las zones definides como a porcentajes de Game::WIDTH (640) i Game::HEIGHT (480) + + // Percentatges de height (divisió vertical) + constexpr float SCOREBOARD_TOP_HEIGHT_PERCENT = 0.02F; // 10% superior + constexpr float MAIN_PLAYAREA_HEIGHT_PERCENT = 0.88F; // 80% central + constexpr float SCOREBOARD_BOTTOM_HEIGHT_PERCENT = 0.10F; // 10% inferior + + // Padding horizontal para PLAYAREA (dentro de MAIN_PLAYAREA) + constexpr float PLAYAREA_PADDING_HORIZONTAL_PERCENT = 0.015F; // 5% a cada costat + + // --- CÀLCULS AUTOMÀTICS DE PÍXELS --- + // Cálculos automáticos a partir dels porcentajes + + // Alçades + constexpr float SCOREBOARD_TOP_H = Game::HEIGHT * SCOREBOARD_TOP_HEIGHT_PERCENT; + constexpr float MAIN_PLAYAREA_H = Game::HEIGHT * MAIN_PLAYAREA_HEIGHT_PERCENT; + constexpr float SCOREBOARD_BOTTOM_H = Game::HEIGHT * SCOREBOARD_BOTTOM_HEIGHT_PERCENT; + + // Posicions Y + constexpr float SCOREBOARD_TOP_Y = 0.0F; + constexpr float MAIN_PLAYAREA_Y = SCOREBOARD_TOP_H; + constexpr float SCOREBOARD_BOTTOM_Y = MAIN_PLAYAREA_Y + MAIN_PLAYAREA_H; + + // Padding horizontal de PLAYAREA + constexpr float PLAYAREA_PADDING_H = Game::WIDTH * PLAYAREA_PADDING_HORIZONTAL_PERCENT; + + // --- ZONES FINALS (SDL_FRect) --- + + // Marcador superior (reservado para futuro uso) + // Ocupa el 2% superior + constexpr SDL_FRect SCOREBOARD_TOP = { + 0.0F, // x = 0.0 + SCOREBOARD_TOP_Y, // y = 0.0 + static_cast(Game::WIDTH), // ancho completo + SCOREBOARD_TOP_H // alto + }; + + // Área de juego principal (contenedor del 80% central, sin padding) + // Ocupa el 88% central, ancho completo + constexpr SDL_FRect MAIN_PLAYAREA = { + 0.0F, // x = 0.0 + MAIN_PLAYAREA_Y, // debajo del scoreboard superior + static_cast(Game::WIDTH), // ancho completo + MAIN_PLAYAREA_H // alto + }; + + // Zona de juego real (con padding horizontal del 5%) + // Ocupa: dentro de MAIN_PLAYAREA, con márgenes laterales + // Se utiliza para límites del juego, colisiones, spawn + constexpr SDL_FRect PLAYAREA = { + PLAYAREA_PADDING_H, // padding horizontal + MAIN_PLAYAREA_Y, // debajo del scoreboard superior (igual que MAIN_PLAYAREA) + Game::WIDTH - (2.0F * PLAYAREA_PADDING_H), // ancho con padding + MAIN_PLAYAREA_H // alto (igual que MAIN_PLAYAREA) + }; + + // Marcador inferior (marcador actual) + // Ocupa el 10% inferior + constexpr SDL_FRect SCOREBOARD = { + 0.0F, // x = 0.0 + SCOREBOARD_BOTTOM_Y, // fondo + static_cast(Game::WIDTH), // ancho completo + SCOREBOARD_BOTTOM_H // alto + }; + + // Padding horizontal del marcador (para alinear zonas izquierda/derecha con PLAYAREA) + constexpr float SCOREBOARD_PADDING_H = 0.0F; // Game::WIDTH * 0.015f; + +} // namespace Defaults::Zones From 5f6d51b6cb404f92542e3ec22cca5fefe2310568 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 19:24:06 +0200 Subject: [PATCH 12/18] refactor: introduir Config::EngineConfig com a struct POD a core/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pas 1/N del hallazgo #28. Sense canvi de comportament: Nou: source/core/config/engine_config.hpp - Defineix Config::EngineConfig (POD) amb les sub-structs WindowConfig, RenderingConfig, KeyboardBindings, GamepadBindings, PlayerBindings i el flag console. - Sense singletons ni virtuals; la inversió real es fa en commits posteriors injectant Config::EngineConfig& per constructor. Modificat: source/game/options.hpp - Elimina les struct definitions locals (Window, Rendering, ...). - Afegeix Options::engine_config (única font de veritat). - Conserva Options::window, Options::rendering, player1, player2, keyboard_controls, gamepad_controls i console com a referències inline a camps d'engine_config. Cost runtime zero, callsites existents no requereixen cap canvi. Hallazgo #28 de CODE_REVIEW.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/config/engine_config.hpp | 61 +++++++++++++++++ source/game/options.hpp | 97 ++++++++++------------------ 2 files changed, 94 insertions(+), 64 deletions(-) create mode 100644 source/core/config/engine_config.hpp diff --git a/source/core/config/engine_config.hpp b/source/core/config/engine_config.hpp new file mode 100644 index 0000000..47fd099 --- /dev/null +++ b/source/core/config/engine_config.hpp @@ -0,0 +1,61 @@ +// engine_config.hpp - Configuració runtime del motor (window, render, input) +// © 2026 JailDesigner +// +// Struct POD que conté la configuració runtime que els sistemes de `core/` +// llegeixen i muten. La capa de persistència (YAML) viu a `game/options.cpp`, +// que omple aquesta struct a init() i loadFromFile(). +// +// Es passa per referència (mutable quan cal) al constructor dels sistemes +// que la necessiten, mantenint `core/` agnòstic a `game/`. + +#pragma once + +#include + +#include + +namespace Config { + + struct WindowConfig { + int width{1280}; + int height{720}; + bool fullscreen{false}; + float zoom_factor{1.0F}; // Zoom level (0.5x to max_zoom) + }; + + struct RenderingConfig { + int vsync{1}; // 0=disabled, 1=enabled + }; + + struct KeyboardBindings { + SDL_Scancode key_left{SDL_SCANCODE_LEFT}; + SDL_Scancode key_right{SDL_SCANCODE_RIGHT}; + SDL_Scancode key_thrust{SDL_SCANCODE_UP}; + SDL_Scancode key_shoot{SDL_SCANCODE_SPACE}; + SDL_Scancode key_start{SDL_SCANCODE_1}; + }; + + struct GamepadBindings { + int button_left{SDL_GAMEPAD_BUTTON_DPAD_LEFT}; + int button_right{SDL_GAMEPAD_BUTTON_DPAD_RIGHT}; + int button_thrust{SDL_GAMEPAD_BUTTON_WEST}; // X button + int button_shoot{SDL_GAMEPAD_BUTTON_SOUTH}; // A button + }; + + struct PlayerBindings { + KeyboardBindings keyboard{}; + GamepadBindings gamepad{}; + std::string gamepad_name; // Empty = auto-assign by index + }; + + struct EngineConfig { + WindowConfig window{}; + RenderingConfig rendering{}; + PlayerBindings player1{}; + PlayerBindings player2{}; + KeyboardBindings keyboard_controls{}; // Defaults globals per Input + GamepadBindings gamepad_controls{}; + bool console{false}; + }; + +} // namespace Config diff --git a/source/game/options.hpp b/source/game/options.hpp index e4b0f01..ef8b3dd 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -1,79 +1,48 @@ -#pragma once +// options.hpp - Persistència YAML i globals d'opcions (capa game) +// © 2026 JailDesigner +// +// La configuració runtime viu en una struct única (Config::EngineConfig, +// definida a `core/config/engine_config.hpp`). Aquest fitxer hi posa al +// damunt la capa de persistència YAML i les compatibilitats històriques +// (Options::window, Options::rendering, ...) com a referències inline a +// camps d'aquesta struct. Cost runtime zero, callsites existents no +// requereixen cap canvi. -#include // Para SDL_Scancode +#pragma once #include +#include "core/config/engine_config.hpp" + namespace Options { - // Estructures de configuración - - struct Window { - int width{1280}; - int height{720}; - bool fullscreen{false}; - float zoom_factor{1.0F}; // Zoom level (0.5x to max_zoom) - }; - - struct Rendering { - int vsync{1}; // 0=disabled, 1=enabled - }; - - // Controles de jugadors - - struct KeyboardControls { - SDL_Scancode key_left{SDL_SCANCODE_LEFT}; - SDL_Scancode key_right{SDL_SCANCODE_RIGHT}; - SDL_Scancode key_thrust{SDL_SCANCODE_UP}; - SDL_Scancode key_shoot{SDL_SCANCODE_SPACE}; - SDL_Scancode key_start{SDL_SCANCODE_1}; - }; - - struct GamepadControls { - int button_left{SDL_GAMEPAD_BUTTON_DPAD_LEFT}; - int button_right{SDL_GAMEPAD_BUTTON_DPAD_RIGHT}; - int button_thrust{SDL_GAMEPAD_BUTTON_WEST}; // X button - int button_shoot{SDL_GAMEPAD_BUTTON_SOUTH}; // A button - }; - - struct PlayerControls { - KeyboardControls keyboard{}; - GamepadControls gamepad{}; - std::string gamepad_name; // Buit = auto-assignar per índex - }; - - // Variables globals (inline per evitar ODR violations) - - inline std::string version{}; // Versión del config per validació - inline bool console{false}; // Eixida de debug - inline Window window{}; - inline Rendering rendering{}; - - // Controles per player - inline PlayerControls player1{ - .keyboard = - {.key_left = SDL_SCANCODE_LEFT, - .key_right = SDL_SCANCODE_RIGHT, - .key_thrust = SDL_SCANCODE_UP, - .key_shoot = SDL_SCANCODE_SPACE, - .key_start = SDL_SCANCODE_1}, - .gamepad_name = "" // Primer gamepad disponible - }; - - inline PlayerControls player2{ - .keyboard = - {.key_left = SDL_SCANCODE_A, + // Única font de veritat de la configuració runtime. Tota la resta de + // globals d'aquest namespace són referències inline a camps d'aquesta + // struct. + inline Config::EngineConfig engine_config{ + .player2 = { + .keyboard = { + .key_left = SDL_SCANCODE_A, .key_right = SDL_SCANCODE_D, .key_thrust = SDL_SCANCODE_W, .key_shoot = SDL_SCANCODE_LSHIFT, - .key_start = SDL_SCANCODE_2}, - .gamepad_name = "" // Segon gamepad disponible + .key_start = SDL_SCANCODE_2, + }, + .gamepad_name = "", + }, }; - // Per compatibilitat con pollo (no utilitzat en orni, pero necessari per Input) - inline KeyboardControls keyboard_controls{}; - inline GamepadControls gamepad_controls{}; + // Aliases (referències) per al codi existent. + inline Config::WindowConfig& window = engine_config.window; + inline Config::RenderingConfig& rendering = engine_config.rendering; + inline Config::PlayerBindings& player1 = engine_config.player1; + inline Config::PlayerBindings& player2 = engine_config.player2; + inline Config::KeyboardBindings& keyboard_controls = engine_config.keyboard_controls; + inline Config::GamepadBindings& gamepad_controls = engine_config.gamepad_controls; + inline bool& console = engine_config.console; + // Persistència YAML (es queda en game/, no en core/) + inline std::string version{}; // Versión del config per validació inline std::string config_file_path{}; // Establert per setConfigFile() // Funciones públiques From ecb41cbc3a60407c980e43b5016d914bea898814 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 19:25:01 +0200 Subject: [PATCH 13/18] =?UTF-8?q?refactor(#28):=20DebugOverlay=20rep=20Con?= =?UTF-8?q?fig::RenderingConfig=20per=20refer=C3=A8ncia?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pas 2/N del hallazgo #28. DebugOverlay deixa d'incloure game/options.hpp i passa a rebre un const Config::RenderingConfig& en el seu constructor. El Director li passa Options::rendering (que ja és un alias d'engine_config.rendering). Eliminat: include "game/options.hpp" des de core/system/debug_overlay.cpp. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/system/debug_overlay.cpp | 79 +++++++++++++++------------- source/core/system/debug_overlay.hpp | 15 ++++-- source/core/system/director.cpp | 2 +- 3 files changed, 52 insertions(+), 44 deletions(-) diff --git a/source/core/system/debug_overlay.cpp b/source/core/system/debug_overlay.cpp index 1997031..5e81d09 100644 --- a/source/core/system/debug_overlay.cpp +++ b/source/core/system/debug_overlay.cpp @@ -5,53 +5,56 @@ #include #include "core/types.hpp" -#include "game/options.hpp" namespace System { -namespace { -// Posición y tamaño del overlay en coordenadas lógicas (1280×720). -constexpr float OVERLAY_X = 12.0F; -constexpr float OVERLAY_Y_FPS = 12.0F; -constexpr float OVERLAY_LINE_HEIGHT = 18.0F; // separación entre líneas (scale 0.4 → ~16 px alto) -constexpr float OVERLAY_SCALE = 0.4F; -constexpr float OVERLAY_SPACING = 2.0F; -constexpr float OVERLAY_BRIGHTNESS = 1.0F; + namespace { + // Posición y tamaño del overlay en coordenadas lógicas (1280×720). + constexpr float OVERLAY_X = 12.0F; + constexpr float OVERLAY_Y_FPS = 12.0F; + constexpr float OVERLAY_LINE_HEIGHT = 18.0F; // separación entre líneas (scale 0.4 → ~16 px alto) + constexpr float OVERLAY_SCALE = 0.4F; + constexpr float OVERLAY_SPACING = 2.0F; + constexpr float OVERLAY_BRIGHTNESS = 1.0F; -// Cadencia de actualización del valor de FPS mostrado. -constexpr float FPS_UPDATE_INTERVAL = 0.5F; -} // namespace + // Cadencia de actualización del valor de FPS mostrado. + constexpr float FPS_UPDATE_INTERVAL = 0.5F; + } // namespace -DebugOverlay::DebugOverlay(Rendering::Renderer* renderer) - : text_(renderer) - {} + DebugOverlay::DebugOverlay(Rendering::Renderer* renderer, + const Config::RenderingConfig& rendering_cfg) + : text_(renderer), + rendering_cfg_(&rendering_cfg) {} -void DebugOverlay::update(float delta_time) { - fps_accumulator_ += delta_time; - fps_frame_count_++; + void DebugOverlay::update(float delta_time) { + fps_accumulator_ += delta_time; + fps_frame_count_++; - if (fps_accumulator_ >= FPS_UPDATE_INTERVAL) { - fps_display_ = static_cast(fps_frame_count_ / fps_accumulator_); - fps_frame_count_ = 0; - fps_accumulator_ = 0.0F; - } -} - -void DebugOverlay::draw() const { - if (!visible_) { - return; + if (fps_accumulator_ >= FPS_UPDATE_INTERVAL) { + fps_display_ = static_cast(fps_frame_count_ / fps_accumulator_); + fps_frame_count_ = 0; + fps_accumulator_ = 0.0F; + } } - const std::string FPS_TEXT = "FPS: " + std::to_string(fps_display_); - const std::string VSYNC_TEXT = std::string("VSYNC: ") - + (Options::rendering.vsync == 1 ? "ON" : "OFF"); + void DebugOverlay::draw() const { + if (!visible_) { + return; + } - text_.render(FPS_TEXT, - Vec2{.x = OVERLAY_X, .y = OVERLAY_Y_FPS}, - OVERLAY_SCALE, OVERLAY_SPACING, OVERLAY_BRIGHTNESS); - text_.render(VSYNC_TEXT, - Vec2{.x = OVERLAY_X, .y = OVERLAY_Y_FPS + OVERLAY_LINE_HEIGHT}, - OVERLAY_SCALE, OVERLAY_SPACING, OVERLAY_BRIGHTNESS); -} + const std::string FPS_TEXT = "FPS: " + std::to_string(fps_display_); + const std::string VSYNC_TEXT = std::string("VSYNC: ") + (rendering_cfg_->vsync == 1 ? "ON" : "OFF"); + + text_.render(FPS_TEXT, + Vec2{.x = OVERLAY_X, .y = OVERLAY_Y_FPS}, + OVERLAY_SCALE, + OVERLAY_SPACING, + OVERLAY_BRIGHTNESS); + text_.render(VSYNC_TEXT, + Vec2{.x = OVERLAY_X, .y = OVERLAY_Y_FPS + OVERLAY_LINE_HEIGHT}, + OVERLAY_SCALE, + OVERLAY_SPACING, + OVERLAY_BRIGHTNESS); + } } // namespace System diff --git a/source/core/system/debug_overlay.hpp b/source/core/system/debug_overlay.hpp index 89b87a7..3e0237a 100644 --- a/source/core/system/debug_overlay.hpp +++ b/source/core/system/debug_overlay.hpp @@ -7,14 +7,18 @@ #pragma once +#include "core/config/engine_config.hpp" #include "core/graphics/vector_text.hpp" #include "core/rendering/render_context.hpp" namespace System { -class DebugOverlay { - public: - explicit DebugOverlay(Rendering::Renderer* renderer); + class DebugOverlay { + public: + // El rendering_cfg ha de viure tant com l'overlay (el posseeix + // el Director, que sobreviu a tots els sistemes). + DebugOverlay(Rendering::Renderer* renderer, + const Config::RenderingConfig& rendering_cfg); // Acumula FPS. Llamar una vez por frame con el delta del Director. void update(float delta_time); @@ -25,14 +29,15 @@ class DebugOverlay { void toggle() { visible_ = !visible_; } [[nodiscard]] auto isVisible() const -> bool { return visible_; } - private: + private: Graphics::VectorText text_; + const Config::RenderingConfig* rendering_cfg_; bool visible_{true}; // FPS counter — se actualiza cada FPS_UPDATE_INTERVAL segundos. float fps_accumulator_{0.0F}; int fps_frame_count_{0}; int fps_display_{0}; -}; + }; } // namespace System diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 24decac..b55627e 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -258,7 +258,7 @@ auto Director::run() -> int { // Overlay de debug (FPS + VSync). Vive en el Director porque es global // a todas las escenas. Toggle con F11 (visible por defecto en _DEBUG). - System::DebugOverlay debug_overlay(sdl.getRenderer()); + System::DebugOverlay debug_overlay(sdl.getRenderer(), Options::rendering); // Bucle principal: construir escena → frame loop → destruir → siguiente. while (context.nextScene() != SceneType::EXIT) { From 2f0b148380bb219b38bae2a1c843833d4ce667fd Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 19:30:38 +0200 Subject: [PATCH 14/18] build: alinear cppcheck del hook amb el de make cppcheck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Afegit --suppress=useStlAlgorithm al hook, que ja estava al target cppcheck de CMakeLists.txt:297. La regla és sorollosa (suggereix std::find_if/std::any_of/std::transform a qualsevol raw loop que es podria fer més curt) i el projecte ja va decidir desactivar-la a nivell de pasada completa. Segon desencontre entre el hook i el target CMake (el primer va ser el -I absolut vs relatiu). Mantenim el hook i make cppcheck coherents per no fer veure problemes que no n'hi ha. Co-Authored-By: Claude Opus 4.7 (1M context) --- .githooks/pre-commit | 1 + 1 file changed, 1 insertion(+) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index fa0ea76..9da501c 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -85,6 +85,7 @@ if ! cppcheck \ --suppress='*:*source/external/*' \ --suppress='*:*source/legacy/*' \ --suppress=normalCheckLevelMaxBranches \ + --suppress=useStlAlgorithm \ -D_DEBUG \ -DLINUX_BUILD \ --quiet \ From d11821866238b2dd0f35f781dc2eb040d3b437ac Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 19:30:55 +0200 Subject: [PATCH 15/18] =?UTF-8?q?refactor(#28):=20Input=20rep=20Config::Pl?= =?UTF-8?q?ayerBindings=20per=20par=C3=A0metre?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pas 3/N del hallazgo #28. Input deixa d'incloure game/options.hpp. Els antics applyPlayerXFromOptions es renombren a applyPlayerXBindings(const Config::PlayerBindings&) i reben els bindings per paràmetre en lloc de llegir-los del global Options::*. El Director hi passa Options::player1/2 als call-sites. Esborrats applyKeyboardBindingsFromOptions i applyGamepadBindingsFromOptions que no eren cridats per ningú (dead code aprofitat). Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/input/input.cpp | 78 ++++------- source/core/input/input.hpp | 237 ++++++++++++++++---------------- source/core/system/director.cpp | 4 +- 3 files changed, 144 insertions(+), 175 deletions(-) diff --git a/source/core/input/input.cpp b/source/core/input/input.cpp index 5a718d3..5009840 100644 --- a/source/core/input/input.cpp +++ b/source/core/input/input.cpp @@ -8,8 +8,6 @@ #include // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator #include // Para move -#include "game/options.hpp" // Para Options::controls - // Singleton Input* Input::instance = nullptr; @@ -51,34 +49,6 @@ void Input::bindKey(Action action, SDL_Scancode code) { keyboard_.bindings[action].scancode = code; } -// Aplica las teclas configuradas desde Options -void Input::applyKeyboardBindingsFromOptions() { - bindKey(Action::LEFT, Options::keyboard_controls.key_left); - bindKey(Action::RIGHT, Options::keyboard_controls.key_right); - bindKey(Action::THRUST, Options::keyboard_controls.key_thrust); -} - -// Aplica configuración de botones del gamepad desde Options al primer gamepad conectado -void Input::applyGamepadBindingsFromOptions() { - // Si no hay gamepads conectados, no hay nada que hacer - if (gamepads_.empty()) { - return; - } - - // Obtener el primer gamepad conectado - const auto& gamepad = gamepads_[0]; - - // Aplicar bindings desde Options - // Los valores pueden ser: - // - 0-20+: Botones SDL_GamepadButton (DPAD, face buttons, shoulders) - // - 100: L2 trigger - // - 101: R2 trigger - // - 200+: Ejes del stick analógico - gamepad->bindings[Action::LEFT].button = Options::gamepad_controls.button_left; - gamepad->bindings[Action::RIGHT].button = Options::gamepad_controls.button_right; - gamepad->bindings[Action::THRUST].button = Options::gamepad_controls.button_thrust; -} - // Asigna inputs a botones del mando void Input::bindGameControllerButton(const std::shared_ptr& gamepad, Action action, SDL_GamepadButton button) { if (gamepad != nullptr) { @@ -494,22 +464,22 @@ auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std:: // ========== MÉTODOS ESPECÍFICOS POR JUGADOR (ORNI) ========== // Aplica configuración de controles del player 1 -void Input::applyPlayer1BindingsFromOptions() { +void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) { // 1. Aplicar bindings de teclado (NO usar bindKey, llenar mapa específico) - player1_keyboard_bindings_[Action::LEFT].scancode = Options::player1.keyboard.key_left; - player1_keyboard_bindings_[Action::RIGHT].scancode = Options::player1.keyboard.key_right; - player1_keyboard_bindings_[Action::THRUST].scancode = Options::player1.keyboard.key_thrust; - player1_keyboard_bindings_[Action::SHOOT].scancode = Options::player1.keyboard.key_shoot; - player1_keyboard_bindings_[Action::START].scancode = Options::player1.keyboard.key_start; + player1_keyboard_bindings_[Action::LEFT].scancode = bindings.keyboard.key_left; + player1_keyboard_bindings_[Action::RIGHT].scancode = bindings.keyboard.key_right; + player1_keyboard_bindings_[Action::THRUST].scancode = bindings.keyboard.key_thrust; + player1_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot; + player1_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start; // 2. Encontrar gamepad por nombre (o usar primer gamepad como fallback) std::shared_ptr gamepad = nullptr; - if (Options::player1.gamepad_name.empty()) { + if (bindings.gamepad_name.empty()) { // Fallback: usar primer gamepad disponible gamepad = (!gamepads_.empty()) ? gamepads_[0] : nullptr; } else { // Buscar por nombre - gamepad = findAvailableGamepadByName(Options::player1.gamepad_name); + gamepad = findAvailableGamepadByName(bindings.gamepad_name); } if (!gamepad) { @@ -518,32 +488,32 @@ void Input::applyPlayer1BindingsFromOptions() { } // 3. Aplicar bindings de gamepad - gamepad->bindings[Action::LEFT].button = Options::player1.gamepad.button_left; - gamepad->bindings[Action::RIGHT].button = Options::player1.gamepad.button_right; - gamepad->bindings[Action::THRUST].button = Options::player1.gamepad.button_thrust; - gamepad->bindings[Action::SHOOT].button = Options::player1.gamepad.button_shoot; + gamepad->bindings[Action::LEFT].button = bindings.gamepad.button_left; + gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right; + gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust; + gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot; // 4. Cachear referencia player1_gamepad_ = gamepad; } // Aplica configuración de controles del player 2 -void Input::applyPlayer2BindingsFromOptions() { +void Input::applyPlayer2Bindings(const Config::PlayerBindings& bindings) { // 1. Aplicar bindings de teclado (mapa específico de P2, no sobrescribe P1) - player2_keyboard_bindings_[Action::LEFT].scancode = Options::player2.keyboard.key_left; - player2_keyboard_bindings_[Action::RIGHT].scancode = Options::player2.keyboard.key_right; - player2_keyboard_bindings_[Action::THRUST].scancode = Options::player2.keyboard.key_thrust; - player2_keyboard_bindings_[Action::SHOOT].scancode = Options::player2.keyboard.key_shoot; - player2_keyboard_bindings_[Action::START].scancode = Options::player2.keyboard.key_start; + player2_keyboard_bindings_[Action::LEFT].scancode = bindings.keyboard.key_left; + player2_keyboard_bindings_[Action::RIGHT].scancode = bindings.keyboard.key_right; + player2_keyboard_bindings_[Action::THRUST].scancode = bindings.keyboard.key_thrust; + player2_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot; + player2_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start; // 2. Encontrar gamepad por nombre (o usar segundo gamepad como fallback) std::shared_ptr gamepad = nullptr; - if (Options::player2.gamepad_name.empty()) { + if (bindings.gamepad_name.empty()) { // Fallback: usar segundo gamepad disponible gamepad = (gamepads_.size() > 1) ? gamepads_[1] : nullptr; } else { // Buscar por nombre - gamepad = findAvailableGamepadByName(Options::player2.gamepad_name); + gamepad = findAvailableGamepadByName(bindings.gamepad_name); } if (!gamepad) { @@ -552,10 +522,10 @@ void Input::applyPlayer2BindingsFromOptions() { } // 3. Aplicar bindings de gamepad - gamepad->bindings[Action::LEFT].button = Options::player2.gamepad.button_left; - gamepad->bindings[Action::RIGHT].button = Options::player2.gamepad.button_right; - gamepad->bindings[Action::THRUST].button = Options::player2.gamepad.button_thrust; - gamepad->bindings[Action::SHOOT].button = Options::player2.gamepad.button_shoot; + gamepad->bindings[Action::LEFT].button = bindings.gamepad.button_left; + gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right; + gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust; + gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot; // 4. Cachear referencia player2_gamepad_ = gamepad; diff --git a/source/core/input/input.hpp b/source/core/input/input.hpp index 30eff20..541c3b4 100644 --- a/source/core/input/input.hpp +++ b/source/core/input/input.hpp @@ -9,153 +9,152 @@ #include // Para unordered_map #include // Para vector +#include "core/config/engine_config.hpp" #include "core/input/input_types.hpp" // for InputAction // --- Clase Input: gestiona la entrada de teclado y mandos (singleton) --- class Input { - public: - // --- Constantes --- - static constexpr bool ALLOW_REPEAT = true; // Permite repetición - static constexpr bool DO_NOT_ALLOW_REPEAT = false; // No permite repetición - static constexpr bool CHECK_KEYBOARD = true; // Comprueba teclado - static constexpr bool DO_NOT_CHECK_KEYBOARD = false; // No comprueba teclado - static constexpr int TRIGGER_L2_AS_BUTTON = 100; // L2 como botón - static constexpr int TRIGGER_R2_AS_BUTTON = 101; // R2 como botón + public: + // --- Constantes --- + static constexpr bool ALLOW_REPEAT = true; // Permite repetición + static constexpr bool DO_NOT_ALLOW_REPEAT = false; // No permite repetición + static constexpr bool CHECK_KEYBOARD = true; // Comprueba teclado + static constexpr bool DO_NOT_CHECK_KEYBOARD = false; // No comprueba teclado + static constexpr int TRIGGER_L2_AS_BUTTON = 100; // L2 como botón + static constexpr int TRIGGER_R2_AS_BUTTON = 101; // R2 como botón - // --- Tipos --- - using Action = InputAction; // Alias para mantener compatibilidad + // --- Tipos --- + using Action = InputAction; // Alias para mantener compatibilidad - // --- Estructuras --- - struct KeyState { - Uint8 scancode{0}; // Scancode asociado - bool is_held{false}; // Está pulsada ahora mismo - bool just_pressed{false}; // Se acaba de pulsar en este fotograma - }; + // --- Estructuras --- + struct KeyState { + Uint8 scancode{0}; // Scancode asociado + bool is_held{false}; // Está pulsada ahora mismo + bool just_pressed{false}; // Se acaba de pulsar en este fotograma + }; - struct ButtonState { - int button{static_cast(SDL_GAMEPAD_BUTTON_INVALID)}; // GameControllerButton asociado - bool is_held{false}; // Está pulsada ahora mismo - bool just_pressed{false}; // Se acaba de pulsar en este fotograma - bool axis_active{false}; // Estado del eje - bool trigger_active{false}; // Estado del trigger como botón digital - }; + struct ButtonState { + int button{static_cast(SDL_GAMEPAD_BUTTON_INVALID)}; // GameControllerButton asociado + bool is_held{false}; // Está pulsada ahora mismo + bool just_pressed{false}; // Se acaba de pulsar en este fotograma + bool axis_active{false}; // Estado del eje + bool trigger_active{false}; // Estado del trigger como botón digital + }; - struct Keyboard { - std::unordered_map bindings; // Mapa de acciones a estados de tecla - }; + struct Keyboard { + std::unordered_map bindings; // Mapa de acciones a estados de tecla + }; - struct Gamepad { - SDL_Gamepad* pad{nullptr}; // Puntero al gamepad SDL - SDL_JoystickID instance_id{0}; // ID de instancia del joystick - std::string name; // Nombre del gamepad - std::string path; // Ruta del dispositivo - std::unordered_map bindings; // Mapa de acciones a estados de botón + struct Gamepad { + SDL_Gamepad* pad{nullptr}; // Puntero al gamepad SDL + SDL_JoystickID instance_id{0}; // ID de instancia del joystick + std::string name; // Nombre del gamepad + std::string path; // Ruta del dispositivo + std::unordered_map bindings; // Mapa de acciones a estados de botón - explicit Gamepad(SDL_Gamepad* gamepad) - : pad(gamepad), - instance_id(SDL_GetJoystickID(SDL_GetGamepadJoystick(gamepad))), - name(std::string(SDL_GetGamepadName(gamepad))), - path(std::string(SDL_GetGamepadPath(pad))), - bindings{ - // Movimiento y acciones del player - {Action::LEFT, ButtonState{.button = static_cast(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}}, - {Action::RIGHT, ButtonState{.button = static_cast(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}}, - {Action::THRUST, ButtonState{.button = static_cast(SDL_GAMEPAD_BUTTON_WEST)}}, - {Action::SHOOT, ButtonState{.button = static_cast(SDL_GAMEPAD_BUTTON_SOUTH)}}} {} + explicit Gamepad(SDL_Gamepad* gamepad) + : pad(gamepad), + instance_id(SDL_GetJoystickID(SDL_GetGamepadJoystick(gamepad))), + name(std::string(SDL_GetGamepadName(gamepad))), + path(std::string(SDL_GetGamepadPath(pad))), + bindings{ + // Movimiento y acciones del player + {Action::LEFT, ButtonState{.button = static_cast(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}}, + {Action::RIGHT, ButtonState{.button = static_cast(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}}, + {Action::THRUST, ButtonState{.button = static_cast(SDL_GAMEPAD_BUTTON_WEST)}}, + {Action::SHOOT, ButtonState{.button = static_cast(SDL_GAMEPAD_BUTTON_SOUTH)}}} {} - ~Gamepad() { - if (pad != nullptr) { - SDL_CloseGamepad(pad); - } - } + ~Gamepad() { + if (pad != nullptr) { + SDL_CloseGamepad(pad); + } + } - // Reasigna un botón a una acción - void rebindAction(Action action, SDL_GamepadButton new_button) { - bindings[action].button = static_cast(new_button); - } - }; + // Reasigna un botón a una acción + void rebindAction(Action action, SDL_GamepadButton new_button) { + bindings[action].button = static_cast(new_button); + } + }; - // --- Tipos --- - using Gamepads = std::vector>; // Vector de gamepads + // --- Tipos --- + using Gamepads = std::vector>; // Vector de gamepads - // --- Singleton --- - static void init(const std::string& game_controller_db_path); - static void destroy(); - static auto get() -> Input*; + // --- Singleton --- + static void init(const std::string& game_controller_db_path); + static void destroy(); + static auto get() -> Input*; - // --- Actualización del sistema --- - void update(); // Actualiza estados de entrada + // --- Actualización del sistema --- + void update(); // Actualiza estados de entrada - // --- Configuración de controles --- - void bindKey(Action action, SDL_Scancode code); - void applyKeyboardBindingsFromOptions(); - void applyGamepadBindingsFromOptions(); + // --- Configuración de controles --- + void bindKey(Action action, SDL_Scancode code); - // Configuración por player (Orni - dos jugadores) - void applyPlayer1BindingsFromOptions(); - void applyPlayer2BindingsFromOptions(); + // Configuración por player (Orni - dos jugadores) + void applyPlayer1Bindings(const Config::PlayerBindings& bindings); + void applyPlayer2Bindings(const Config::PlayerBindings& bindings); - static void bindGameControllerButton(const std::shared_ptr& gamepad, Action action, SDL_GamepadButton button); - static void bindGameControllerButton(const std::shared_ptr& gamepad, Action action_target, Action action_source); + static void bindGameControllerButton(const std::shared_ptr& gamepad, Action action, SDL_GamepadButton button); + static void bindGameControllerButton(const std::shared_ptr& gamepad, Action action_target, Action action_source); - // --- Consulta de entrada --- - auto checkAction(Action action, bool repeat = true, bool check_keyboard = true, const std::shared_ptr& gamepad = nullptr) -> bool; - auto checkAnyInput(bool check_keyboard = true, const std::shared_ptr& gamepad = nullptr) -> bool; - auto checkAnyButton(bool repeat = DO_NOT_ALLOW_REPEAT) -> bool; - void resetInputStates(); + // --- Consulta de entrada --- + auto checkAction(Action action, bool repeat = true, bool check_keyboard = true, const std::shared_ptr& gamepad = nullptr) -> bool; + auto checkAnyInput(bool check_keyboard = true, const std::shared_ptr& gamepad = nullptr) -> bool; + auto checkAnyButton(bool repeat = DO_NOT_ALLOW_REPEAT) -> bool; + void resetInputStates(); - // Consulta por player (Orni - dos jugadores) - auto checkActionPlayer1(Action action, bool repeat = true) -> bool; - auto checkActionPlayer2(Action action, bool repeat = true) -> bool; + // Consulta por player (Orni - dos jugadores) + auto checkActionPlayer1(Action action, bool repeat = true) -> bool; + auto checkActionPlayer2(Action action, bool repeat = true) -> bool; - // Check if any player pressed any action from a list - auto checkAnyPlayerAction(const std::span& actions, bool repeat = DO_NOT_ALLOW_REPEAT) -> bool; + // Check if any player pressed any action from a list + auto checkAnyPlayerAction(const std::span& actions, bool repeat = DO_NOT_ALLOW_REPEAT) -> bool; - // --- Gestión de gamepads --- - [[nodiscard]] auto gameControllerFound() const -> bool; - [[nodiscard]] auto getNumGamepads() const -> int; - [[nodiscard]] auto getGamepad(SDL_JoystickID id) const -> std::shared_ptr; - [[nodiscard]] auto getGamepadByName(const std::string& name) const -> std::shared_ptr; - [[nodiscard]] auto getGamepads() const -> const Gamepads& { return gamepads_; } - auto findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr; - static auto getControllerName(const std::shared_ptr& gamepad) -> std::string; - [[nodiscard]] auto getControllerNames() const -> std::vector; - [[nodiscard]] static auto getControllerBinding(const std::shared_ptr& gamepad, Action action) -> SDL_GamepadButton; - void printConnectedGamepads() const; + // --- Gestión de gamepads --- + [[nodiscard]] auto gameControllerFound() const -> bool; + [[nodiscard]] auto getNumGamepads() const -> int; + [[nodiscard]] auto getGamepad(SDL_JoystickID id) const -> std::shared_ptr; + [[nodiscard]] auto getGamepadByName(const std::string& name) const -> std::shared_ptr; + [[nodiscard]] auto getGamepads() const -> const Gamepads& { return gamepads_; } + auto findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr; + static auto getControllerName(const std::shared_ptr& gamepad) -> std::string; + [[nodiscard]] auto getControllerNames() const -> std::vector; + [[nodiscard]] static auto getControllerBinding(const std::shared_ptr& gamepad, Action action) -> SDL_GamepadButton; + void printConnectedGamepads() const; - // --- Eventos --- - auto handleEvent(const SDL_Event& event) -> std::string; + // --- Eventos --- + auto handleEvent(const SDL_Event& event) -> std::string; - private: - // --- Constantes --- - static constexpr Sint16 AXIS_THRESHOLD = 30000; // Umbral para ejes analógicos - static constexpr Sint16 TRIGGER_THRESHOLD = 16384; // Umbral para triggers (50% del rango) - static constexpr std::array BUTTON_INPUTS = {Action::SHOOT}; // Inputs que usan botones + private: + // --- Constantes --- + static constexpr Sint16 AXIS_THRESHOLD = 30000; // Umbral para ejes analógicos + static constexpr Sint16 TRIGGER_THRESHOLD = 16384; // Umbral para triggers (50% del rango) + static constexpr std::array BUTTON_INPUTS = {Action::SHOOT}; // Inputs que usan botones - // --- Métodos --- - explicit Input(std::string game_controller_db_path); - ~Input() = default; + // --- Métodos --- + explicit Input(std::string game_controller_db_path); + ~Input() = default; - void initSDLGamePad(); - static auto checkAxisInput(Action action, const std::shared_ptr& gamepad, bool repeat) -> bool; - static auto checkTriggerInput(Action action, const std::shared_ptr& gamepad, bool repeat) -> bool; - auto addGamepad(int device_index) -> std::string; - auto removeGamepad(SDL_JoystickID id) -> std::string; - void addGamepadMappingsFromFile(); - void discoverGamepads(); + void initSDLGamePad(); + static auto checkAxisInput(Action action, const std::shared_ptr& gamepad, bool repeat) -> bool; + static auto checkTriggerInput(Action action, const std::shared_ptr& gamepad, bool repeat) -> bool; + auto addGamepad(int device_index) -> std::string; + auto removeGamepad(SDL_JoystickID id) -> std::string; + void addGamepadMappingsFromFile(); + void discoverGamepads(); - // --- Variables miembro --- - static Input* instance; // Instancia única del singleton + // --- Variables miembro --- + static Input* instance; // Instancia única del singleton - Gamepads gamepads_; // Lista de gamepads conectados - Keyboard keyboard_{}; // Estado del teclado (solo acciones globales) - std::string gamepad_mappings_file_; // Ruta al archivo de mappings + Gamepads gamepads_; // Lista de gamepads conectados + Keyboard keyboard_{}; // Estado del teclado (solo acciones globales) + std::string gamepad_mappings_file_; // Ruta al archivo de mappings - // Referencias cacheadas a gamepads por player (Orni) - std::shared_ptr player1_gamepad_; - std::shared_ptr player2_gamepad_; + // Referencias cacheadas a gamepads por player (Orni) + std::shared_ptr player1_gamepad_; + std::shared_ptr player2_gamepad_; - // Mapas de bindings separados por player (Orni - dos jugadores) - std::unordered_map player1_keyboard_bindings_; - std::unordered_map player2_keyboard_bindings_; + // Mapas de bindings separados por player (Orni - dos jugadores) + std::unordered_map player1_keyboard_bindings_; + std::unordered_map player2_keyboard_bindings_; }; \ No newline at end of file diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index b55627e..2296db9 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -99,8 +99,8 @@ Director::Director(std::vector const& args) { Input::init("data/gamecontrollerdb.txt"); // Aplicar configuración de controls dels jugadors - Input::get()->applyPlayer1BindingsFromOptions(); - Input::get()->applyPlayer2BindingsFromOptions(); + Input::get()->applyPlayer1Bindings(Options::player1); + Input::get()->applyPlayer2Bindings(Options::player2); if (Options::console) { std::cout << "Configuración carregada\n"; From fdd34eb94348fa895e11e557e6062b2f01189254 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 19:35:35 +0200 Subject: [PATCH 16/18] refactor(#28): SDLManager rep Config::EngineConfig + on_persist callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pas 4/N del hallazgo #28. SDLManager deixa d'incloure game/options.hpp. El ctor accepta ara una Config::EngineConfig& (per llegir/mutar window i rendering) i un opcional std::function on_persist callback. Canvis funcionals: - Es mantenen les mutacions de window.{width,height,zoom_factor,fullscreen} però ara sobre cfg_->window en lloc d'Options::window. Comportament idèntic perquè Options::window és un alias a engine_config.window. - toggleVSync deixa de cridar Options::saveToFile() directament i invoca on_persist_ si està connectat. El Director li passa una lambda que fa la persistència (mantenint sdl_manager agnòstic). - initWindowAndGpu (free function) rep el vsync inicial per paràmetre. - Eliminat el ctor per defecte (SDLManager()) que no era cridat des de cap call-site del projecte. Cleanup preexistent surfat per clang-tidy en treure el ctor default: - finestra_, max_width_, max_height_, max_zoom_ passen a tindre default member initializers; eliminat el seu ctor mem-init redundant. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/rendering/sdl_manager.cpp | 148 +++++++++++--------------- source/core/rendering/sdl_manager.hpp | 94 ++++++++-------- source/core/system/director.cpp | 6 +- 3 files changed, 114 insertions(+), 134 deletions(-) diff --git a/source/core/rendering/sdl_manager.cpp b/source/core/rendering/sdl_manager.cpp index 22d2b86..47c5416 100644 --- a/source/core/rendering/sdl_manager.cpp +++ b/source/core/rendering/sdl_manager.cpp @@ -13,95 +13,65 @@ #include "core/defaults.hpp" #include "core/input/mouse.hpp" #include "core/rendering/coordinate_transform.hpp" -#include "game/options.hpp" #include "project.h" namespace { -auto initWindowAndGpu(SDL_Window** out_window, - Rendering::Renderer& gpu_renderer, - int width, int height, bool fullscreen) -> bool { - // Título estático estilo CCAE. El FPS y el estado de VSync los muestra - // el DebugOverlay (toggle F11), no la barra de título. - const std::string TITLE = std::format("© 2026 {} — JailDesigner", - Project::LONG_NAME); + auto initWindowAndGpu(SDL_Window** out_window, + Rendering::Renderer& gpu_renderer, + int width, + int height, + bool fullscreen, + int initial_vsync) -> bool { + // Título estático estilo CCAE. El FPS y el estado de VSync los muestra + // el DebugOverlay (toggle F11), no la barra de título. + const std::string TITLE = std::format("© 2026 {} — JailDesigner", + Project::LONG_NAME); - SDL_WindowFlags flags = SDL_WINDOW_RESIZABLE; - if (fullscreen) { - flags = static_cast(flags | SDL_WINDOW_FULLSCREEN); + SDL_WindowFlags flags = SDL_WINDOW_RESIZABLE; + if (fullscreen) { + flags = static_cast(flags | SDL_WINDOW_FULLSCREEN); + } + + SDL_Window* window = SDL_CreateWindow(TITLE.c_str(), width, height, flags); + if (window == nullptr) { + std::cerr << "Error creant finestra: " << SDL_GetError() << '\n'; + return false; + } + + if (!fullscreen) { + SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); + } + + // Inicializar el FrameRenderer (claim del window + pipeline de líneas). + if (!gpu_renderer.init(window, + static_cast(Defaults::Game::WIDTH), + static_cast(Defaults::Game::HEIGHT))) { + std::cerr << "Error inicialitzant GpuFrameRenderer\n"; + SDL_DestroyWindow(window); + return false; + } + + gpu_renderer.setVSync(initial_vsync != 0); + + // Cargar parámetros del postpro desde el resource pack. Si el YAML falta + // o falla, el loader devuelve los defaults built-in (bloom suave + flicker + // sutil + background verde tenue). + gpu_renderer.setPostFx(Config::PostFx::load("config/postfx.yaml")); + + *out_window = window; + return true; } - - SDL_Window* window = SDL_CreateWindow(TITLE.c_str(), width, height, flags); - if (window == nullptr) { - std::cerr << "Error creant finestra: " << SDL_GetError() << '\n'; - return false; - } - - if (!fullscreen) { - SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); - } - - // Inicializar el FrameRenderer (claim del window + pipeline de líneas). - if (!gpu_renderer.init(window, - static_cast(Defaults::Game::WIDTH), - static_cast(Defaults::Game::HEIGHT))) { - std::cerr << "Error inicialitzant GpuFrameRenderer\n"; - SDL_DestroyWindow(window); - return false; - } - - gpu_renderer.setVSync(Options::rendering.vsync != 0); - - // Cargar parámetros del postpro desde el resource pack. Si el YAML falta - // o falla, el loader devuelve los defaults built-in (bloom suave + flicker - // sutil + background verde tenue). - gpu_renderer.setPostFx(Config::PostFx::load("config/postfx.yaml")); - - *out_window = window; - return true; -} } // namespace -SDLManager::SDLManager() - : finestra_(nullptr), - current_width_(Defaults::Window::WIDTH), - current_height_(Defaults::Window::HEIGHT), - is_fullscreen_(false), - max_width_(1920), - max_height_(1080), - zoom_factor_(Defaults::Window::BASE_ZOOM), - windowed_width_(Defaults::Window::WIDTH), - windowed_height_(Defaults::Window::HEIGHT), - max_zoom_(1.0F) { - if (!SDL_Init(SDL_INIT_VIDEO)) { - std::cerr << "Error inicialitzant SDL3: " << SDL_GetError() << '\n'; - return; - } - - calculateMaxWindowSize(); - - if (!initWindowAndGpu(&finestra_, gpu_renderer_, current_width_, current_height_, false)) { - SDL_Quit(); - return; - } - - updateViewport(); - - std::cout << "SDL3 inicialitzat: " << current_width_ << "x" << current_height_ - << " (logic: " << Defaults::Game::WIDTH << "x" - << Defaults::Game::HEIGHT << ")" << '\n'; -} - -SDLManager::SDLManager(int width, int height, bool fullscreen) - : finestra_(nullptr), +SDLManager::SDLManager(int width, int height, bool fullscreen, Config::EngineConfig& cfg, std::function on_persist) + : cfg_(&cfg), + on_persist_(std::move(on_persist)), current_width_(width), current_height_(height), is_fullscreen_(fullscreen), - max_width_(1920), - max_height_(1080), zoom_factor_(static_cast(width) / Defaults::Window::WIDTH), windowed_width_(width), - windowed_height_(height), - max_zoom_(1.0F) { + windowed_height_(height) { if (!SDL_Init(SDL_INIT_VIDEO)) { std::cerr << "Error inicialitzant SDL3: " << SDL_GetError() << '\n'; return; @@ -109,7 +79,7 @@ SDLManager::SDLManager(int width, int height, bool fullscreen) calculateMaxWindowSize(); - if (!initWindowAndGpu(&finestra_, gpu_renderer_, current_width_, current_height_, is_fullscreen_)) { + if (!initWindowAndGpu(&finestra_, gpu_renderer_, current_width_, current_height_, is_fullscreen_, cfg_->rendering.vsync)) { SDL_Quit(); return; } @@ -194,9 +164,9 @@ void SDLManager::applyZoom(float new_zoom) { windowed_width_ = new_width; windowed_height_ = new_height; - Options::window.width = new_width; - Options::window.height = new_height; - Options::window.zoom_factor = zoom_factor_; + cfg_->window.width = new_width; + cfg_->window.height = new_height; + cfg_->window.zoom_factor = zoom_factor_; std::cout << "Zoom: " << zoom_factor_ << "x (" << new_width << "x" << new_height << ")" << '\n'; @@ -216,9 +186,9 @@ void SDLManager::updateViewport() { offset_y = std::max(offset_y, 0); gpu_renderer_.setViewport(static_cast(offset_x), - static_cast(offset_y), - static_cast(scaled_width), - static_cast(scaled_height)); + static_cast(offset_y), + static_cast(scaled_width), + static_cast(scaled_height)); std::cout << "Viewport: " << scaled_width << "x" << scaled_height << " @ (" << offset_x << "," << offset_y << ") [scale=" << scale << "]" @@ -288,7 +258,7 @@ void SDLManager::toggleFullscreen() { << windowed_width_ << "x" << windowed_height_ << ")" << '\n'; } - Options::window.fullscreen = is_fullscreen_; + cfg_->window.fullscreen = is_fullscreen_; Mouse::setForceHidden(is_fullscreen_); } @@ -334,7 +304,9 @@ void SDLManager::present() { } void SDLManager::toggleVSync() { - Options::rendering.vsync = (Options::rendering.vsync == 1) ? 0 : 1; - gpu_renderer_.setVSync(Options::rendering.vsync != 0); - Options::saveToFile(); + cfg_->rendering.vsync = (cfg_->rendering.vsync == 1) ? 0 : 1; + gpu_renderer_.setVSync(cfg_->rendering.vsync != 0); + if (on_persist_) { + on_persist_(); + } } diff --git a/source/core/rendering/sdl_manager.hpp b/source/core/rendering/sdl_manager.hpp index 86052e7..a1273c7 100644 --- a/source/core/rendering/sdl_manager.hpp +++ b/source/core/rendering/sdl_manager.hpp @@ -11,61 +11,67 @@ #include #include +#include +#include "core/config/engine_config.hpp" #include "core/rendering/render_context.hpp" class SDLManager { - public: - SDLManager(); // Constructor per defecte (usa Defaults::) - SDLManager(int width, int height, bool fullscreen); // Constructor con configuración - ~SDLManager(); + public: + // `cfg` ha de viure tant com el manager (el posseeix el Director). + // `on_persist` es crida després de mutar la config (per exemple a + // toggleVSync) per delegar la persistència en una capa externa + // (game/Options::saveToFile), mantenint sdl_manager agnòstic. + SDLManager(int width, int height, bool fullscreen, Config::EngineConfig& cfg, std::function on_persist = {}); + ~SDLManager(); - // No permetre còpia ni assignació - SDLManager(const SDLManager&) = delete; - auto operator=(const SDLManager&) -> SDLManager& = delete; + // No permetre còpia ni assignació + SDLManager(const SDLManager&) = delete; + auto operator=(const SDLManager&) -> SDLManager& = delete; - // [NUEVO] Gestió de finestra dinàmica - void increaseWindowSize(); // F2: +100px - void decreaseWindowSize(); // F1: -100px - void toggleFullscreen(); // F3 - void toggleVSync(); // F4 - auto handleWindowEvent(const SDL_Event& event) -> bool; // Per a SDL_EVENT_WINDOW_RESIZED + // [NUEVO] Gestió de finestra dinàmica + void increaseWindowSize(); // F2: +100px + void decreaseWindowSize(); // F1: -100px + void toggleFullscreen(); // F3 + void toggleVSync(); // F4 + auto handleWindowEvent(const SDL_Event& event) -> bool; // Per a SDL_EVENT_WINDOW_RESIZED - // Funciones principals (renderizado). - // clear() devuelve false si la swapchain no está disponible (p.ej. - // ventana minimizada). El caller debe saltarse draw+present ese frame. - [[nodiscard]] auto clear(uint8_t r = 0, uint8_t g = 0, uint8_t b = 0) -> bool; - void present(); + // Funciones principals (renderizado). + // clear() devuelve false si la swapchain no está disponible (p.ej. + // ventana minimizada). El caller debe saltarse draw+present ese frame. + [[nodiscard]] auto clear(uint8_t r = 0, uint8_t g = 0, uint8_t b = 0) -> bool; + void present(); - // Getters - auto getRenderer() -> Rendering::Renderer* { return &gpu_renderer_; } - [[nodiscard]] auto getScaleFactor() const -> float { return zoom_factor_; } + // Getters + auto getRenderer() -> Rendering::Renderer* { return &gpu_renderer_; } + [[nodiscard]] auto getScaleFactor() const -> float { return zoom_factor_; } - // [NUEVO] Actualitzar context de renderizado (factor de scale global) - void updateRenderingContext() const; + // [NUEVO] Actualitzar context de renderizado (factor de scale global) + void updateRenderingContext() const; - private: - SDL_Window* finestra_; - Rendering::Renderer gpu_renderer_; // GpuFrameRenderer (SDL3 GPU) + private: + SDL_Window* finestra_{nullptr}; + Rendering::Renderer gpu_renderer_; // GpuFrameRenderer (SDL3 GPU) + Config::EngineConfig* cfg_; // Propietat del Director, sobreviu al manager + std::function on_persist_; // Opcional: persistència delegada - // [NUEVO] Estat de la finestra - int current_width_; // Mida física actual - int current_height_; - bool is_fullscreen_; - int max_width_; // Calculat des del display - int max_height_; + // [NUEVO] Estat de la finestra + int current_width_; // Mida física actual + int current_height_; + bool is_fullscreen_; + int max_width_{1920}; // Fallback si no es pot llegir del display + int max_height_{1080}; - // [ZOOM SYSTEM] - float zoom_factor_; // Current zoom (0.5x to max_zoom_) - int windowed_width_; // Saved size before fullscreen - int windowed_height_; // Saved size before fullscreen - float max_zoom_; // Maximum zoom (calculated from display) - - // [NUEVO] Funciones internes - void calculateMaxWindowSize(); // Llegir resolució del display - void calculateMaxZoom(); // Calculate max zoom from display - void applyZoom(float new_zoom); // Apply zoom and resize window - void applyWindowSize(int width, int height); // Canviar mida + centrar - void updateViewport(); // Configurar viewport con letterbox + // [ZOOM SYSTEM] + float zoom_factor_; // Current zoom (0.5x to max_zoom_) + int windowed_width_; // Saved size before fullscreen + int windowed_height_; // Saved size before fullscreen + float max_zoom_{1.0F}; // Maximum zoom (calculated from display) + // [NUEVO] Funciones internes + void calculateMaxWindowSize(); // Llegir resolució del display + void calculateMaxZoom(); // Calculate max zoom from display + void applyZoom(float new_zoom); // Apply zoom and resize window + void applyWindowSize(int width, int height); // Canviar mida + centrar + void updateViewport(); // Configurar viewport con letterbox }; diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 2296db9..29d1a18 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -220,8 +220,10 @@ auto Director::run() -> int { int initial_height = static_cast(std::round( Defaults::Window::HEIGHT * Options::window.zoom_factor)); - // Crear gestor SDL con configuración de Options - SDLManager sdl(initial_width, initial_height, Options::window.fullscreen); + // Crear gestor SDL con configuración de Options. + // Inyectamos la engine_config + un callback per persistir-la quan + // toggleVSync (F4) muti vsync, sense que sdl_manager conegui Options. + SDLManager sdl(initial_width, initial_height, Options::window.fullscreen, Options::engine_config, [] { Options::saveToFile(); }); // CRÍTIC: Forçar ocultació del cursor DESPRÉS de toda la inicialización SDL // Això evita que SDL mostre el cursor automàticament durante la creació de la finestra From 41ce3fece59f7444ac437d50ec3c8aed769675dc Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 19:40:52 +0200 Subject: [PATCH 17/18] refactor(#28): Director rep EngineConfig + ConfigPersistence, main orquestra MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pas 5/N del hallazgo #28. Director deixa d'incloure game/options.hpp i les seves crides a Options::*. El seu ctor accepta ara: - Config::EngineConfig& cfg → la struct runtime (window, console, ...). - Config::ConfigPersistence → 4 lambdes (init/set_path/load/save) que delegen la persistència a la capa concreta (game/Options::*). Cap més referència a Options:: ni a "game/..." dins del Director: - cfg_->* substitueix tot Options::* (window, console, player1/2, rendering, engine_config). - persistence_.{init,save,load,set_path} substitueix les funcions d'I/O de YAML. run() i checkProgramArguments deixen de ser estàtics (necessiten accés a cfg_ i persistence_). Això també desfà el smell del hallazgo #37 (Director::run estàtic que llegia estat d'instància). main.cpp queda com a orquestrador: construeix la struct ConfigPersistence amb lambdes que enllacen amb Options::* i la injecta al Director. Afegit: Config::ConfigPersistence a engine_config.hpp. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/config/engine_config.hpp | 11 ++++++ source/core/system/director.cpp | 50 ++++++++++++------------ source/core/system/director.hpp | 58 +++++++++++++++------------- source/main.cpp | 25 +++++++++--- 4 files changed, 89 insertions(+), 55 deletions(-) diff --git a/source/core/config/engine_config.hpp b/source/core/config/engine_config.hpp index 47fd099..1d0b9bb 100644 --- a/source/core/config/engine_config.hpp +++ b/source/core/config/engine_config.hpp @@ -12,6 +12,7 @@ #include +#include #include namespace Config { @@ -58,4 +59,14 @@ namespace Config { bool console{false}; }; + // Capa de persistència delegada cap a l'EngineConfig. Permet al Director + // orquestrar init/load/save sense conèixer cap esquema concret (YAML, + // SQLite, ...) ni la capa que el conté (`game/options.cpp`). + struct ConfigPersistence { + std::function init_defaults; // Restaura valors per defecte + std::function set_path; // Indica on guardar + std::function load; // Llegeix path → EngineConfig + std::function save; // Escriu EngineConfig → path + }; + } // namespace Config diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 29d1a18..2416d85 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -19,7 +19,6 @@ #include "core/resources/resource_loader.hpp" #include "core/utils/path_utils.hpp" #include "debug_overlay.hpp" -#include "game/options.hpp" #include "game/scenes/game_scene.hpp" #include "game/scenes/logo_scene.hpp" #include "game/scenes/title_scene.hpp" @@ -38,11 +37,15 @@ using SceneManager::SceneContext; using SceneType = SceneContext::SceneType; // Constructor -Director::Director(std::vector const& args) { +Director::Director(std::vector const& args, + Config::EngineConfig& cfg, + Config::ConfigPersistence persistence) + : cfg_(&cfg), + persistence_(std::move(persistence)) { std::cout << "Orni Attack - Inici\n"; // Inicialitzar opciones con valors per defecte - Options::init(); + persistence_.init_defaults(); // Comprovar arguments del programa executable_path_ = checkProgramArguments(args); @@ -90,22 +93,22 @@ Director::Director(std::vector const& args) { createSystemFolder(std::string("jailgames/") + Project::NAME); // Establir ruta del file de configuración - Options::setConfigFile(system_folder_ + "/config.yaml"); + persistence_.set_path(system_folder_ + "/config.yaml"); // Carregar o crear configuración - Options::loadFromFile(); + persistence_.load(); // Inicialitzar sistema de input Input::init("data/gamecontrollerdb.txt"); // Aplicar configuración de controls dels jugadors - Input::get()->applyPlayer1Bindings(Options::player1); - Input::get()->applyPlayer2Bindings(Options::player2); + Input::get()->applyPlayer1Bindings(cfg_->player1); + Input::get()->applyPlayer2Bindings(cfg_->player2); - if (Options::console) { + if (cfg_->console) { std::cout << "Configuración carregada\n"; - std::cout << " Finestra: " << Options::window.width << "×" - << Options::window.height << '\n'; + std::cout << " Finestra: " << cfg_->window.width << "×" + << cfg_->window.height << '\n'; std::cout << " Input: " << Input::get()->getNumGamepads() << " gamepad(s) detectat(s)\n"; } @@ -115,7 +118,7 @@ Director::Director(std::vector const& args) { Director::~Director() { // Guardar opciones - Options::saveToFile(); + persistence_.save(); // Cleanup input Input::destroy(); @@ -136,11 +139,11 @@ auto Director::checkProgramArguments(std::vector const& args) const std::string& argument = args[i]; if (argument == "--console") { - Options::console = true; + cfg_->console = true; std::cout << "Mode consola activat\n"; } else if (argument == "--reset-config") { - Options::init(); - Options::saveToFile(); + persistence_.init_defaults(); + persistence_.save(); std::cout << "Configuración restablida als valors per defecte\n"; } } @@ -207,7 +210,7 @@ void Director::createSystemFolder(const std::string& folder) { } } - if (Options::console) { + if (cfg_->console) { std::cout << "Carpeta del sistema: " << system_folder_ << '\n'; } } @@ -216,18 +219,17 @@ void Director::createSystemFolder(const std::string& folder) { auto Director::run() -> int { // Calculate initial size from saved zoom_factor int initial_width = static_cast(std::round( - Defaults::Window::WIDTH * Options::window.zoom_factor)); + Defaults::Window::WIDTH * cfg_->window.zoom_factor)); int initial_height = static_cast(std::round( - Defaults::Window::HEIGHT * Options::window.zoom_factor)); + Defaults::Window::HEIGHT * cfg_->window.zoom_factor)); - // Crear gestor SDL con configuración de Options. - // Inyectamos la engine_config + un callback per persistir-la quan - // toggleVSync (F4) muti vsync, sense que sdl_manager conegui Options. - SDLManager sdl(initial_width, initial_height, Options::window.fullscreen, Options::engine_config, [] { Options::saveToFile(); }); + // Crear gestor SDL amb la engine_config + callback de persistència + // per a quan toggleVSync (F4) muti vsync. Mantenim sdl_manager agnòstic. + SDLManager sdl(initial_width, initial_height, cfg_->window.fullscreen, *cfg_, [this] { persistence_.save(); }); // CRÍTIC: Forçar ocultació del cursor DESPRÉS de toda la inicialización SDL // Això evita que SDL mostre el cursor automàticament durante la creació de la finestra - if (!Options::window.fullscreen) { + if (!cfg_->window.fullscreen) { Mouse::forceHide(); } @@ -246,7 +248,7 @@ auto Director::run() -> int { // Precachear música para evitar lag al empezar AudioResource::getMusic("title.ogg"); AudioResource::getMusic("game.ogg"); - if (Options::console) { + if (cfg_->console) { std::cout << "Música precacheada\n"; } @@ -260,7 +262,7 @@ auto Director::run() -> int { // Overlay de debug (FPS + VSync). Vive en el Director porque es global // a todas las escenas. Toggle con F11 (visible por defecto en _DEBUG). - System::DebugOverlay debug_overlay(sdl.getRenderer(), Options::rendering); + System::DebugOverlay debug_overlay(sdl.getRenderer(), cfg_->rendering); // Bucle principal: construir escena → frame loop → destruir → siguiente. while (context.nextScene() != SceneType::EXIT) { diff --git a/source/core/system/director.hpp b/source/core/system/director.hpp index d4d16d7..c6ce16e 100644 --- a/source/core/system/director.hpp +++ b/source/core/system/director.hpp @@ -4,41 +4,47 @@ #include #include +#include "core/config/engine_config.hpp" #include "scene_context.hpp" class Scene; class SDLManager; -namespace System { class DebugOverlay; } +namespace System { + class DebugOverlay; +} class Director { - public: - explicit Director(std::vector const& args); - ~Director(); + public: + // `cfg` ha de viure tant com el Director (típicament owned per main). + // `persistence` encapsula init/load/save delegats a la capa concreta + // (game/Options::*). + Director(std::vector const& args, + Config::EngineConfig& cfg, + Config::ConfigPersistence persistence); + ~Director(); - // Main game loop. Estático: los miembros del Director (executable_path_, - // system_folder_) se establecen en el ctor y no se vuelven a leer aquí; - // el bucle solo orquesta sistemas globales (SDLManager, Options, Audio). - static auto run() -> int; + // Bucle principal del juego. + auto run() -> int; - private: - std::string executable_path_; - std::string system_folder_; + private: + std::string executable_path_; + std::string system_folder_; + Config::EngineConfig* cfg_; + Config::ConfigPersistence persistence_; - static auto checkProgramArguments(std::vector const& args) - -> std::string; - void createSystemFolder(const std::string& folder); + auto checkProgramArguments(std::vector const& args) + -> std::string; + void createSystemFolder(const std::string& folder); - // Construye la escena correspondiente al tipo solicitado. Retorna - // nullptr para EXIT u otros valores no constructibles. - static auto buildScene(SceneManager::SceneContext::SceneType type, - SDLManager& sdl, - SceneManager::SceneContext& context) - -> std::unique_ptr; + // Construye la escena correspondiente al tipo solicitado. Retorna + // nullptr para EXIT u otros valores no constructibles. + static auto buildScene(SceneManager::SceneContext::SceneType type, + SDLManager& sdl, + SceneManager::SceneContext& context) + -> std::unique_ptr; - // Ejecuta el bucle de frames de UNA escena hasta que scene.isFinished() - // sea true. Maneja delta_time, eventos (globales + escena), update y draw. - // El debug_overlay es global a todas las escenas; el Director lo posee. - static void runFrameLoop(Scene& scene, SDLManager& sdl, - SceneManager::SceneContext& context, - System::DebugOverlay& debug_overlay); + // Ejecuta el bucle de frames de UNA escena hasta que scene.isFinished() + // sea true. Maneja delta_time, eventos (globales + escena), update y draw. + // El debug_overlay es global a todas las escenas; el Director lo posee. + static void runFrameLoop(Scene& scene, SDLManager& sdl, SceneManager::SceneContext& context, System::DebugOverlay& debug_overlay); }; diff --git a/source/main.cpp b/source/main.cpp index 831bc43..68c7663 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -1,18 +1,33 @@ -// main.cpp - Vec2 de entrada del juego Asteroides +// main.cpp - Punt d'entrada de l'aplicació // © 2026 JailDesigner +// +// Aquí orquestrem la capa de persistència (YAML via game/Options::*) i +// injectem el resultat al Director. El Director queda independent de +// game/options.hpp i pot operar només amb Config::EngineConfig. #include #include +#include "core/config/engine_config.hpp" #include "core/system/director.hpp" +#include "game/options.hpp" auto main(int argc, char* argv[]) -> int { // Convertir arguments a std::vector std::vector args(argv, argv + argc); - // Crear director (inicialitza sistema, opciones, configuración) - Director director(args); + // Capa de persistència delegada: lambdes prim que enllacen el contracte + // de Config::ConfigPersistence amb la implementació YAML de Options::*. + const Config::ConfigPersistence PERSISTENCE{ + .init_defaults = [] { Options::init(); }, + .set_path = [](const std::string& path) { Options::setConfigFile(path); }, + .load = [] { return Options::loadFromFile(); }, + .save = [] { return Options::saveToFile(); }, + }; - // Executar bucle principal del juego - return Director::run(); + // El Director rep la struct d'engine_config + la capa de persistència. + // No coneix Options::* directament. + Director director(args, Options::engine_config, PERSISTENCE); + + return director.run(); } From 329ae7a38e674feb4c69d2c2cefaa6d6be77bdad Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 19:47:18 +0200 Subject: [PATCH 18/18] =?UTF-8?q?refactor(#28):=20renombrar=20Options=20?= =?UTF-8?q?=E2=86=92=20ConfigYaml=20+=20netejar=20aliases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pas 7 final del hallazgo #28. La capa de game/options havia esdevingut exclusivament una capa de persistència YAML que llegia i escrivia Config::EngineConfig. El nom "Options" no reflectia bé aquest rol. Canvis: - Renomenats fitxers: game/options.{hpp,cpp} → game/config_yaml.{hpp,cpp} (preservant la història de git via mv). - Renomenat namespace: Options → ConfigYaml. - Esborrades del .hpp les referències-alias inline (window, rendering, player1, player2, keyboard_controls, gamepad_controls, console) que ja no tenien call-sites externs (només existien per a la transició). El .hpp ara només exposa engine_config + version + path + funcions. - A config_yaml.cpp s'introdueixen aliases internes (anonymous namespace) per mantenir llegible el codi de la implementació, sense exposar-les. - Actualitzat main.cpp per a usar ConfigYaml::*. - Actualitzats els comentaris stale a sdl_manager.hpp, director.hpp, engine_config.hpp i audio.hpp. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/audio/audio.hpp | 2 +- source/core/config/engine_config.hpp | 4 +- source/core/rendering/sdl_manager.hpp | 2 +- source/core/system/director.hpp | 2 +- source/game/{options.cpp => config_yaml.cpp} | 16 ++++-- source/game/config_yaml.hpp | 43 +++++++++++++++ source/game/options.hpp | 56 -------------------- source/main.cpp | 20 +++---- 8 files changed, 71 insertions(+), 74 deletions(-) rename source/game/{options.cpp => config_yaml.cpp} (97%) create mode 100644 source/game/config_yaml.hpp delete mode 100644 source/game/options.hpp diff --git a/source/core/audio/audio.hpp b/source/core/audio/audio.hpp index e37e141..e9f522f 100644 --- a/source/core/audio/audio.hpp +++ b/source/core/audio/audio.hpp @@ -20,7 +20,7 @@ namespace Ja { // --- Clase Audio: gestor d'àudio (singleton) --- // Port del subsistema d'àudio del projecte ../aee, desacoblat d'Options: // la configuración entra per la struct Audio::Config a init()/applySettings(), -// en lloc de llegir directament Options::audio. Això deixa audio.cpp independent +// en lloc de llegir directament ConfigYaml::*. Això deixa audio.cpp independent // del layout d'Options i permet substituir la font de configuración. // // Els volums es manegen internament como a float 0.0–1.0; la capa de diff --git a/source/core/config/engine_config.hpp b/source/core/config/engine_config.hpp index 1d0b9bb..32f91c8 100644 --- a/source/core/config/engine_config.hpp +++ b/source/core/config/engine_config.hpp @@ -2,7 +2,7 @@ // © 2026 JailDesigner // // Struct POD que conté la configuració runtime que els sistemes de `core/` -// llegeixen i muten. La capa de persistència (YAML) viu a `game/options.cpp`, +// llegeixen i muten. La capa de persistència (YAML) viu a `game/config_yaml.cpp`, // que omple aquesta struct a init() i loadFromFile(). // // Es passa per referència (mutable quan cal) al constructor dels sistemes @@ -61,7 +61,7 @@ namespace Config { // Capa de persistència delegada cap a l'EngineConfig. Permet al Director // orquestrar init/load/save sense conèixer cap esquema concret (YAML, - // SQLite, ...) ni la capa que el conté (`game/options.cpp`). + // SQLite, ...) ni la capa que el conté (`game/config_yaml.cpp`). struct ConfigPersistence { std::function init_defaults; // Restaura valors per defecte std::function set_path; // Indica on guardar diff --git a/source/core/rendering/sdl_manager.hpp b/source/core/rendering/sdl_manager.hpp index a1273c7..f5b0dfe 100644 --- a/source/core/rendering/sdl_manager.hpp +++ b/source/core/rendering/sdl_manager.hpp @@ -21,7 +21,7 @@ class SDLManager { // `cfg` ha de viure tant com el manager (el posseeix el Director). // `on_persist` es crida després de mutar la config (per exemple a // toggleVSync) per delegar la persistència en una capa externa - // (game/Options::saveToFile), mantenint sdl_manager agnòstic. + // (game/ConfigYaml::saveToFile), mantenint sdl_manager agnòstic. SDLManager(int width, int height, bool fullscreen, Config::EngineConfig& cfg, std::function on_persist = {}); ~SDLManager(); diff --git a/source/core/system/director.hpp b/source/core/system/director.hpp index c6ce16e..712202a 100644 --- a/source/core/system/director.hpp +++ b/source/core/system/director.hpp @@ -17,7 +17,7 @@ class Director { public: // `cfg` ha de viure tant com el Director (típicament owned per main). // `persistence` encapsula init/load/save delegats a la capa concreta - // (game/Options::*). + // (game/ConfigYaml::*). Director(std::vector const& args, Config::EngineConfig& cfg, Config::ConfigPersistence persistence); diff --git a/source/game/options.cpp b/source/game/config_yaml.cpp similarity index 97% rename from source/game/options.cpp rename to source/game/config_yaml.cpp index 8a383bd..18d9f6b 100644 --- a/source/game/options.cpp +++ b/source/game/config_yaml.cpp @@ -1,4 +1,4 @@ -#include "options.hpp" +#include "config_yaml.hpp" #include #include @@ -9,7 +9,17 @@ #include "external/fkyaml_node.hpp" #include "project.h" -namespace Options { +namespace ConfigYaml { + + namespace { + // Aliases internes per a la implementació, no exposades al .hpp. + // Permeten escriure window.width en lloc d'engine_config.window.width. + Config::WindowConfig& window = engine_config.window; + Config::RenderingConfig& rendering = engine_config.rendering; + Config::PlayerBindings& player1 = engine_config.player1; + Config::PlayerBindings& player2 = engine_config.player2; + bool& console = engine_config.console; + } // namespace // ========== FUNCIONS AUXILIARS PER CONVERSIÓ DE CONTROLES ========== @@ -506,4 +516,4 @@ namespace Options { return true; } -} // namespace Options +} // namespace ConfigYaml diff --git a/source/game/config_yaml.hpp b/source/game/config_yaml.hpp new file mode 100644 index 0000000..95322b3 --- /dev/null +++ b/source/game/config_yaml.hpp @@ -0,0 +1,43 @@ +// config_yaml.hpp - Capa de persistència YAML de Config::EngineConfig +// © 2026 JailDesigner +// +// La configuració runtime viu en Config::EngineConfig (core/config/). +// Aquest fitxer afegeix una capa de persistència YAML que llegeix i +// escriu aquesta struct a disc. La connexió amb el Director es fa via +// Config::ConfigPersistence (lambdes a `main.cpp`), mantenint `core/` +// agnòstic respecte d'aquesta capa. + +#pragma once + +#include + +#include "core/config/engine_config.hpp" + +namespace ConfigYaml { + + // Única font de veritat de la configuració runtime. La capa YAML llegeix + // i escriu aquí; els consumidors (Director i sistemes de core/) reben + // referència a aquesta struct via injecció. + inline Config::EngineConfig engine_config{ + .player2 = { + .keyboard = { + .key_left = SDL_SCANCODE_A, + .key_right = SDL_SCANCODE_D, + .key_thrust = SDL_SCANCODE_W, + .key_shoot = SDL_SCANCODE_LSHIFT, + .key_start = SDL_SCANCODE_2, + }, + .gamepad_name = "", + }, + }; + + // Persistència YAML (no exposada a `core/`). + inline std::string version{}; // Versión del config per validació + inline std::string config_file_path{}; // Establert per setConfigFile() + + void init(); // Inicialitzar engine_config amb els valors per defecte + void setConfigFile(const std::string& path); + auto loadFromFile() -> bool; + auto saveToFile() -> bool; + +} // namespace ConfigYaml diff --git a/source/game/options.hpp b/source/game/options.hpp deleted file mode 100644 index ef8b3dd..0000000 --- a/source/game/options.hpp +++ /dev/null @@ -1,56 +0,0 @@ -// options.hpp - Persistència YAML i globals d'opcions (capa game) -// © 2026 JailDesigner -// -// La configuració runtime viu en una struct única (Config::EngineConfig, -// definida a `core/config/engine_config.hpp`). Aquest fitxer hi posa al -// damunt la capa de persistència YAML i les compatibilitats històriques -// (Options::window, Options::rendering, ...) com a referències inline a -// camps d'aquesta struct. Cost runtime zero, callsites existents no -// requereixen cap canvi. - -#pragma once - -#include - -#include "core/config/engine_config.hpp" - -namespace Options { - - // Única font de veritat de la configuració runtime. Tota la resta de - // globals d'aquest namespace són referències inline a camps d'aquesta - // struct. - inline Config::EngineConfig engine_config{ - .player2 = { - .keyboard = { - .key_left = SDL_SCANCODE_A, - .key_right = SDL_SCANCODE_D, - .key_thrust = SDL_SCANCODE_W, - .key_shoot = SDL_SCANCODE_LSHIFT, - .key_start = SDL_SCANCODE_2, - }, - .gamepad_name = "", - }, - }; - - // Aliases (referències) per al codi existent. - inline Config::WindowConfig& window = engine_config.window; - inline Config::RenderingConfig& rendering = engine_config.rendering; - inline Config::PlayerBindings& player1 = engine_config.player1; - inline Config::PlayerBindings& player2 = engine_config.player2; - inline Config::KeyboardBindings& keyboard_controls = engine_config.keyboard_controls; - inline Config::GamepadBindings& gamepad_controls = engine_config.gamepad_controls; - inline bool& console = engine_config.console; - - // Persistència YAML (es queda en game/, no en core/) - inline std::string version{}; // Versión del config per validació - inline std::string config_file_path{}; // Establert per setConfigFile() - - // Funciones públiques - - void init(); // Inicialitzar con valors per defecte - void setConfigFile( - const std::string& path); // Establir ruta del file de config - auto loadFromFile() -> bool; // Carregar config YAML - auto saveToFile() -> bool; // Guardar config YAML - -} // namespace Options diff --git a/source/main.cpp b/source/main.cpp index 68c7663..58a7458 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -1,33 +1,33 @@ // main.cpp - Punt d'entrada de l'aplicació // © 2026 JailDesigner // -// Aquí orquestrem la capa de persistència (YAML via game/Options::*) i +// Aquí orquestrem la capa de persistència (YAML via game/ConfigYaml::*) i // injectem el resultat al Director. El Director queda independent de -// game/options.hpp i pot operar només amb Config::EngineConfig. +// game/config_yaml.hpp i pot operar només amb Config::EngineConfig. #include #include #include "core/config/engine_config.hpp" #include "core/system/director.hpp" -#include "game/options.hpp" +#include "game/config_yaml.hpp" auto main(int argc, char* argv[]) -> int { // Convertir arguments a std::vector std::vector args(argv, argv + argc); // Capa de persistència delegada: lambdes prim que enllacen el contracte - // de Config::ConfigPersistence amb la implementació YAML de Options::*. + // de Config::ConfigPersistence amb la implementació YAML de ConfigYaml::*. const Config::ConfigPersistence PERSISTENCE{ - .init_defaults = [] { Options::init(); }, - .set_path = [](const std::string& path) { Options::setConfigFile(path); }, - .load = [] { return Options::loadFromFile(); }, - .save = [] { return Options::saveToFile(); }, + .init_defaults = [] { ConfigYaml::init(); }, + .set_path = [](const std::string& path) { ConfigYaml::setConfigFile(path); }, + .load = [] { return ConfigYaml::loadFromFile(); }, + .save = [] { return ConfigYaml::saveToFile(); }, }; // El Director rep la struct d'engine_config + la capa de persistència. - // No coneix Options::* directament. - Director director(args, Options::engine_config, PERSISTENCE); + // No coneix ConfigYaml:: directament. + Director director(args, ConfigYaml::engine_config, PERSISTENCE); return director.run(); }