diff --git a/.githooks/pre-commit b/.githooks/pre-commit index fe14911..9da501c 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 \ @@ -81,11 +85,12 @@ if ! cppcheck \ --suppress='*:*source/external/*' \ --suppress='*:*source/legacy/*' \ --suppress=normalCheckLevelMaxBranches \ + --suppress=useStlAlgorithm \ -D_DEBUG \ -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 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 new file mode 100644 index 0000000..32f91c8 --- /dev/null +++ b/source/core/config/engine_config.hpp @@ -0,0 +1,72 @@ +// 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/config_yaml.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 +#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}; + }; + + // 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/config_yaml.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/defaults.hpp b/source/core/defaults.hpp index 1a2be14..528a61b 100644 --- a/source/core/defaults.hpp +++ b/source/core/defaults.hpp @@ -1,530 +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 (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 - -} // 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 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 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/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..f5b0dfe 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/ConfigYaml::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/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/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 69e7701..2416d85 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,14 @@ #include "core/resources/resource_helper.hpp" #include "core/resources/resource_loader.hpp" #include "core/utils/path_utils.hpp" +#include "debug_overlay.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 @@ -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,24 +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()->applyPlayer1BindingsFromOptions(); - Input::get()->applyPlayer2BindingsFromOptions(); + 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 << " Física: rotation=" << Options::physics.rotation_speed - << " rad/s\n"; + std::cout << " Finestra: " << cfg_->window.width << "×" + << cfg_->window.height << '\n'; std::cout << " Input: " << Input::get()->getNumGamepads() << " gamepad(s) detectat(s)\n"; } @@ -117,7 +118,7 @@ Director::Director(std::vector const& args) { Director::~Director() { // Guardar opciones - Options::saveToFile(); + persistence_.save(); // Cleanup input Input::destroy(); @@ -138,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"; } } @@ -209,7 +210,7 @@ void Director::createSystemFolder(const std::string& folder) { } } - if (Options::console) { + if (cfg_->console) { std::cout << "Carpeta del sistema: " << system_folder_ << '\n'; } } @@ -218,16 +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 - SDLManager sdl(initial_width, initial_height, Options::window.fullscreen); + // 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()); + System::DebugOverlay debug_overlay(sdl.getRenderer(), cfg_->rendering); // Bucle principal: construir escena → frame loop → destruir → siguiente. while (context.nextScene() != SceneType::EXIT) { @@ -291,8 +293,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 +316,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/core/system/director.hpp b/source/core/system/director.hpp index d4d16d7..712202a 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/ConfigYaml::*). + 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/game/config_yaml.cpp b/source/game/config_yaml.cpp new file mode 100644 index 0000000..18d9f6b --- /dev/null +++ b/source/game/config_yaml.cpp @@ -0,0 +1,519 @@ +#include "config_yaml.hpp" + +#include +#include +#include +#include + +#include "core/defaults.hpp" +#include "external/fkyaml_node.hpp" +#include "project.h" + +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 ========== + + // 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 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}}; + + 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 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; + } + + // ========== FI FUNCIONS AUXILIARS ========== + + // Inicialitzar opciones con valors per defecte de Defaults:: + void init() { +#ifdef _DEBUG + console = true; +#else + 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; + + // Rendering + rendering.vsync = Defaults::Rendering::VSYNC_DEFAULT; + + // 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; + } + 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; + } + } + + 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; + } + } + } + } + + // 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()); + } + 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 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()); + } + } + + // Carregar nom del gamepad + if (p1.contains("gamepad_name")) { + player1.gamepad_name = p1["gamepad_name"].get_value(); + } + } + + // 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 + 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; + } + } + + // 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 guardada a: " << config_file_path << '\n'; + } + + return true; + } + +} // 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/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 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); } 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/options.cpp b/source/game/options.cpp deleted file mode 100644 index 9b4cf1b..0000000 --- a/source/game/options.cpp +++ /dev/null @@ -1,639 +0,0 @@ -#include "options.hpp" - -#include -#include -#include -#include - -#include "core/defaults.hpp" -#include "external/fkyaml_node.hpp" -#include "project.h" - -namespace Options { - -// ========== 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 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 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 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 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 ========== - -// Inicialitzar opciones con valors per defecte de Defaults:: -void init() { -#ifdef _DEBUG - console = true; -#else - 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; - - // 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; - - // 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; - } - 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; - } -} - -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 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; - } - } - - 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; - } - } - } -} - -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 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()); - } - 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 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()); - } - } - - // Carregar nom del gamepad - if (p1.contains("gamepad_name")) { - player1.gamepad_name = p1["gamepad_name"].get_value(); - } -} - -// 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 - 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); - loadPhysicsConfigFromYaml(yaml); - loadGameplayConfigFromYaml(yaml); - loadRenderingConfigFromYaml(yaml); - loadAudioConfigFromYaml(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; - } -} - -// 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 deleted file mode 100644 index 4fccd64..0000000 --- a/source/game/options.hpp +++ /dev/null @@ -1,121 +0,0 @@ -#pragma once - -#include // Para SDL_Scancode - -#include - -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 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 { - int vsync{1}; // 0=disabled, 1=enabled -}; - -struct Music { - bool enabled{true}; - float volume{0.8F}; -}; - -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 { - 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 Physics physics{}; -inline Gameplay gameplay{}; -inline Rendering rendering{}; -inline Audio audio{}; - -// 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 -}; - -// 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() - -// 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/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 748f009..2c7170c 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,55 +48,31 @@ 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 - // 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 @@ -118,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; @@ -131,29 +101,28 @@ 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 { // 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"; } } - // [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. 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 } - // 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_*). @@ -162,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. @@ -235,7 +210,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 @@ -598,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(); } } @@ -845,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/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(); }; 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 diff --git a/source/main.cpp b/source/main.cpp index 831bc43..58a7458 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/ConfigYaml::*) i +// injectem el resultat al Director. El Director queda independent de +// 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/config_yaml.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 ConfigYaml::*. + const Config::ConfigPersistence PERSISTENCE{ + .init_defaults = [] { ConfigYaml::init(); }, + .set_path = [](const std::string& path) { ConfigYaml::setConfigFile(path); }, + .load = [] { return ConfigYaml::loadFromFile(); }, + .save = [] { return ConfigYaml::saveToFile(); }, + }; - // Executar bucle principal del juego - return Director::run(); + // El Director rep la struct d'engine_config + la capa de persistència. + // No coneix ConfigYaml:: directament. + Director director(args, ConfigYaml::engine_config, PERSISTENCE); + + return director.run(); }