Merge branch 'refactor/code-review-cleanup'
Atac sistemàtic al CODE_REVIEW.md generat tras tancar el cicle de lint. 10 hallazgos del audit + 3 side-roads del hook, en 18 commits atòmics. Hallazgos del audit cerrats: - #1 Scene::init() lifecycle (fusionat al ctor de GameScene) - #11 Ship::isAlive/isHit/isActive consolidats en isActive() - #16 Rotation3D mort (eliminat struct + paràmetre + apply3dRotation) - #18 ShapeLoader::resolvePath + BASE_PATH morts - #21 Options::physics/audio/gameplay morts (cap reader en runtime) - #22+#30 defaults.hpp partit en 15 subfitxers + umbrella - #24 aliases morts de game/constants.hpp (MARGIN_*, VELOCITAT*) - #25 Defaults::Physics::*_SPEED legacy del Pascal - #28 inversió de dependència core→game per a Options: Config::EngineConfig (POD) viu a core/config/, els sistemes (Director, SDLManager, DebugOverlay, Input) reben referència injectada. La capa YAML viu a game/config_yaml.{hpp,cpp} (renomenat des d'Options). - #34 doble inicialització d'enemies_ a GameScene - #37 Director::run() ja no estàtica (lateral de #28) Out of scope (per un hallazgo separat): - core/system/director.cpp encara inclou game/scenes/*.hpp; cal factory pattern per a escenes. Side-roads del hook: - relative path a cppcheck del pre-commit - alineació amb --suppress=useStlAlgorithm de make cppcheck - std::ranges::fill surfat per cppcheck a GameScene ctor Verificat: compila clean, clang-tidy + cppcheck zero hits nous (32 NOLINTs preexistents igual que abans).
This commit is contained in:
@@ -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 <cstdint> 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <SDL3/SDL.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
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<void()> init_defaults; // Restaura valors per defecte
|
||||
std::function<void(const std::string& path)> set_path; // Indica on guardar
|
||||
std::function<bool()> load; // Llegeix path → EngineConfig
|
||||
std::function<bool()> save; // Escriu EngineConfig → path
|
||||
};
|
||||
|
||||
} // namespace Config
|
||||
+23
-528
@@ -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 <SDL3/SDL.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <numbers>
|
||||
|
||||
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<float>(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<float>(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<float>(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<float>;
|
||||
} // 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"
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// audio.hpp - Configuració d'audio (sistema), pistes de música i efectes
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
// 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
|
||||
@@ -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
|
||||
@@ -0,0 +1,24 @@
|
||||
// controls.hpp - Mapeig de tecles per defecte dels jugadors
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,12 @@
|
||||
// math.hpp - Constants matemàtiques
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <numbers>
|
||||
|
||||
namespace Defaults::Math {
|
||||
|
||||
constexpr float PI = std::numbers::pi_v<float>;
|
||||
|
||||
} // namespace Defaults::Math
|
||||
@@ -0,0 +1,19 @@
|
||||
// palette.hpp - Paleta semàntica per tipus d'entitat
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
// 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,129 @@
|
||||
// title.hpp - Animacions de naves i layout de l'escena de títol
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#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
|
||||
@@ -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
|
||||
@@ -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 <SDL3/SDL.h>
|
||||
|
||||
#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<float>(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<float>(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<float>(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
|
||||
@@ -9,77 +9,62 @@
|
||||
|
||||
namespace Graphics {
|
||||
|
||||
// Inicialización de variables estàtiques
|
||||
std::unordered_map<std::string, std::shared_ptr<Shape>> ShapeLoader::cache;
|
||||
// Inicialización de variables estàtiques
|
||||
std::unordered_map<std::string, std::shared_ptr<Shape>> ShapeLoader::cache;
|
||||
|
||||
auto ShapeLoader::load(const std::string& filename) -> std::shared_ptr<Shape> {
|
||||
// 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<Shape> {
|
||||
// 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<uint8_t> 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<Shape>();
|
||||
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<uint8_t> 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<Shape>();
|
||||
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
|
||||
|
||||
@@ -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<std::string, std::shared_ptr<Shape>> cache;
|
||||
static constexpr const char* BASE_PATH = "data/shapes/";
|
||||
|
||||
// Helpers privats
|
||||
static auto resolvePath(const std::string& filename) -> std::string;
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace Graphics
|
||||
|
||||
+24
-54
@@ -8,8 +8,6 @@
|
||||
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
|
||||
#include <utility> // 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>& 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> 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> 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;
|
||||
|
||||
+118
-119
@@ -9,153 +9,152 @@
|
||||
#include <unordered_map> // Para unordered_map
|
||||
#include <vector> // 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<int>(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<int>(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<Action, KeyState> bindings; // Mapa de acciones a estados de tecla
|
||||
};
|
||||
struct Keyboard {
|
||||
std::unordered_map<Action, KeyState> 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<Action, ButtonState> 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<Action, ButtonState> 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<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}},
|
||||
{Action::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}},
|
||||
{Action::THRUST, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}},
|
||||
{Action::SHOOT, ButtonState{.button = static_cast<int>(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<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}},
|
||||
{Action::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}},
|
||||
{Action::THRUST, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}},
|
||||
{Action::SHOOT, ButtonState{.button = static_cast<int>(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<int>(new_button);
|
||||
}
|
||||
};
|
||||
// Reasigna un botón a una acción
|
||||
void rebindAction(Action action, SDL_GamepadButton new_button) {
|
||||
bindings[action].button = static_cast<int>(new_button);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Tipos ---
|
||||
using Gamepads = std::vector<std::shared_ptr<Gamepad>>; // Vector de gamepads
|
||||
// --- Tipos ---
|
||||
using Gamepads = std::vector<std::shared_ptr<Gamepad>>; // 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>& gamepad, Action action, SDL_GamepadButton button);
|
||||
static void bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action_target, Action action_source);
|
||||
static void bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action, SDL_GamepadButton button);
|
||||
static void bindGameControllerButton(const std::shared_ptr<Gamepad>& 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>& gamepad = nullptr) -> bool;
|
||||
auto checkAnyInput(bool check_keyboard = true, const std::shared_ptr<Gamepad>& 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>& gamepad = nullptr) -> bool;
|
||||
auto checkAnyInput(bool check_keyboard = true, const std::shared_ptr<Gamepad>& 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<const InputAction>& actions, bool repeat = DO_NOT_ALLOW_REPEAT) -> bool;
|
||||
// Check if any player pressed any action from a list
|
||||
auto checkAnyPlayerAction(const std::span<const InputAction>& 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<Gamepad>;
|
||||
[[nodiscard]] auto getGamepadByName(const std::string& name) const -> std::shared_ptr<Input::Gamepad>;
|
||||
[[nodiscard]] auto getGamepads() const -> const Gamepads& { return gamepads_; }
|
||||
auto findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr<Gamepad>;
|
||||
static auto getControllerName(const std::shared_ptr<Gamepad>& gamepad) -> std::string;
|
||||
[[nodiscard]] auto getControllerNames() const -> std::vector<std::string>;
|
||||
[[nodiscard]] static auto getControllerBinding(const std::shared_ptr<Gamepad>& 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<Gamepad>;
|
||||
[[nodiscard]] auto getGamepadByName(const std::string& name) const -> std::shared_ptr<Input::Gamepad>;
|
||||
[[nodiscard]] auto getGamepads() const -> const Gamepads& { return gamepads_; }
|
||||
auto findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr<Gamepad>;
|
||||
static auto getControllerName(const std::shared_ptr<Gamepad>& gamepad) -> std::string;
|
||||
[[nodiscard]] auto getControllerNames() const -> std::vector<std::string>;
|
||||
[[nodiscard]] static auto getControllerBinding(const std::shared_ptr<Gamepad>& 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<Action, 1> 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<Action, 1> 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>& gamepad, bool repeat) -> bool;
|
||||
static auto checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& 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>& gamepad, bool repeat) -> bool;
|
||||
static auto checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& 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<Gamepad> player1_gamepad_;
|
||||
std::shared_ptr<Gamepad> player2_gamepad_;
|
||||
// Referencias cacheadas a gamepads por player (Orni)
|
||||
std::shared_ptr<Gamepad> player1_gamepad_;
|
||||
std::shared_ptr<Gamepad> player2_gamepad_;
|
||||
|
||||
// Mapas de bindings separados por player (Orni - dos jugadores)
|
||||
std::unordered_map<Action, KeyState> player1_keyboard_bindings_;
|
||||
std::unordered_map<Action, KeyState> player2_keyboard_bindings_;
|
||||
// Mapas de bindings separados por player (Orni - dos jugadores)
|
||||
std::unordered_map<Action, KeyState> player1_keyboard_bindings_;
|
||||
std::unordered_map<Action, KeyState> player2_keyboard_bindings_;
|
||||
};
|
||||
@@ -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<SDL_WindowFlags>(flags | SDL_WINDOW_FULLSCREEN);
|
||||
SDL_WindowFlags flags = SDL_WINDOW_RESIZABLE;
|
||||
if (fullscreen) {
|
||||
flags = static_cast<SDL_WindowFlags>(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<float>(Defaults::Game::WIDTH),
|
||||
static_cast<float>(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<float>(Defaults::Game::WIDTH),
|
||||
static_cast<float>(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<void()> 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<float>(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<float>(offset_x),
|
||||
static_cast<float>(offset_y),
|
||||
static_cast<float>(scaled_width),
|
||||
static_cast<float>(scaled_height));
|
||||
static_cast<float>(offset_y),
|
||||
static_cast<float>(scaled_width),
|
||||
static_cast<float>(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_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,61 +11,67 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
#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<void()> 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<void()> 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
|
||||
};
|
||||
|
||||
@@ -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<Graphics::Shape>& 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<Graphics::Shape>& 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<int>(P1.x), static_cast<int>(P1.y),
|
||||
static_cast<int>(P2.x), static_cast<int>(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<int>(P1.x), static_cast<int>(P1.y), static_cast<int>(P2.x), static_cast<int>(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<int>(P1.x), static_cast<int>(P1.y), static_cast<int>(P2.x), static_cast<int>(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<int>(P1.x), static_cast<int>(P1.y),
|
||||
static_cast<int>(P2.x), static_cast<int>(P2.y), brightness, 0.0F, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Rendering
|
||||
|
||||
@@ -3,53 +3,31 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/rendering/render_context.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#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<Graphics::Shape>& 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<Graphics::Shape>& 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
|
||||
|
||||
@@ -5,53 +5,56 @@
|
||||
#include <string>
|
||||
|
||||
#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<int>(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<int>(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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,10 +9,6 @@
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
|
||||
#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 <pwd.h>
|
||||
@@ -38,11 +37,15 @@ using SceneManager::SceneContext;
|
||||
using SceneType = SceneContext::SceneType;
|
||||
|
||||
// Constructor
|
||||
Director::Director(std::vector<std::string> const& args) {
|
||||
Director::Director(std::vector<std::string> 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<std::string> 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<std::string> const& args) {
|
||||
|
||||
Director::~Director() {
|
||||
// Guardar opciones
|
||||
Options::saveToFile();
|
||||
persistence_.save();
|
||||
|
||||
// Cleanup input
|
||||
Input::destroy();
|
||||
@@ -138,11 +139,11 @@ auto Director::checkProgramArguments(std::vector<std::string> 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<int>(std::round(
|
||||
Defaults::Window::WIDTH * Options::window.zoom_factor));
|
||||
Defaults::Window::WIDTH * cfg_->window.zoom_factor));
|
||||
int initial_height = static_cast<int>(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;
|
||||
}
|
||||
|
||||
@@ -4,41 +4,47 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<std::string> 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<std::string> 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<std::string> const& args)
|
||||
-> std::string;
|
||||
void createSystemFolder(const std::string& folder);
|
||||
auto checkProgramArguments(std::vector<std::string> 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<Scene>;
|
||||
// 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<Scene>;
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,519 @@
|
||||
#include "config_yaml.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#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<SDL_Scancode, std::string> 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<std::string, SDL_Scancode> 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<int, std::string> 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<std::string, int> 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 <typename T, typename Validator>
|
||||
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<T>();
|
||||
dest = validate(val) ? val : fallback;
|
||||
} catch (...) {
|
||||
dest = fallback;
|
||||
}
|
||||
}
|
||||
|
||||
// Variant sin validador: només lectura amb fallback en cas d'error.
|
||||
template <typename T>
|
||||
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<T>();
|
||||
} 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<float>(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<int>();
|
||||
// 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<std::string>());
|
||||
}
|
||||
if (kb.contains("key_right")) {
|
||||
player1.keyboard.key_right = stringToScancode(kb["key_right"].get_value<std::string>());
|
||||
}
|
||||
if (kb.contains("key_thrust")) {
|
||||
player1.keyboard.key_thrust = stringToScancode(kb["key_thrust"].get_value<std::string>());
|
||||
}
|
||||
if (kb.contains("key_shoot")) {
|
||||
player1.keyboard.key_shoot = stringToScancode(kb["key_shoot"].get_value<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
// 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<std::string>());
|
||||
}
|
||||
if (gp.contains("button_right")) {
|
||||
player1.gamepad.button_right = stringToButton(gp["button_right"].get_value<std::string>());
|
||||
}
|
||||
if (gp.contains("button_thrust")) {
|
||||
player1.gamepad.button_thrust = stringToButton(gp["button_thrust"].get_value<std::string>());
|
||||
}
|
||||
if (gp.contains("button_shoot")) {
|
||||
player1.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
// Carregar nom del gamepad
|
||||
if (p1.contains("gamepad_name")) {
|
||||
player1.gamepad_name = p1["gamepad_name"].get_value<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
// 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<std::string>());
|
||||
}
|
||||
if (kb.contains("key_right")) {
|
||||
player2.keyboard.key_right = stringToScancode(kb["key_right"].get_value<std::string>());
|
||||
}
|
||||
if (kb.contains("key_thrust")) {
|
||||
player2.keyboard.key_thrust = stringToScancode(kb["key_thrust"].get_value<std::string>());
|
||||
}
|
||||
if (kb.contains("key_shoot")) {
|
||||
player2.keyboard.key_shoot = stringToScancode(kb["key_shoot"].get_value<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
// 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<std::string>());
|
||||
}
|
||||
if (gp.contains("button_right")) {
|
||||
player2.gamepad.button_right = stringToButton(gp["button_right"].get_value<std::string>());
|
||||
}
|
||||
if (gp.contains("button_thrust")) {
|
||||
player2.gamepad.button_thrust = stringToButton(gp["button_thrust"].get_value<std::string>());
|
||||
}
|
||||
if (gp.contains("button_shoot")) {
|
||||
player2.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
// Carregar nom del gamepad
|
||||
if (p2.contains("gamepad_name")) {
|
||||
player2.gamepad_name = p2["gamepad_name"].get_value<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
// 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<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
file.close();
|
||||
|
||||
try {
|
||||
// Parsejar YAML
|
||||
auto yaml = fkyaml::node::deserialize(content);
|
||||
|
||||
// Validar versión
|
||||
if (yaml.contains("version")) {
|
||||
version = yaml["version"].get_value<std::string>();
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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 <string>
|
||||
|
||||
#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
|
||||
+33
-46
@@ -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<int>(Defaults::Zones::PLAYAREA.x);
|
||||
constexpr int MARGIN_RIGHT =
|
||||
static_cast<int>(Defaults::Zones::PLAYAREA.x + Defaults::Zones::PLAYAREA.w);
|
||||
constexpr int MARGIN_TOP = static_cast<int>(Defaults::Zones::PLAYAREA.y);
|
||||
constexpr int MARGIN_BOTTOM =
|
||||
static_cast<int>(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<int>(Defaults::Physics::ENEMY_SPEED);
|
||||
constexpr int VELOCITAT_MAX = static_cast<int>(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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<float>(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<float>(std::rand()) / RAND_MAX) * FREQ_RANGE);
|
||||
((static_cast<float>(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<float>(std::rand()) / RAND_MAX) * AMP_RANGE);
|
||||
((static_cast<float>(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<float>(std::rand()) / RAND_MAX) * DUR_RANGE);
|
||||
((static_cast<float>(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<float>(std::rand()) / RAND_MAX) * MULT_RANGE);
|
||||
((static_cast<float>(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<float>(std::rand()) / RAND_MAX) * DUR_RANGE);
|
||||
((static_cast<float>(std::rand()) / RAND_MAX) * DUR_RANGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -1,639 +0,0 @@
|
||||
#include "options.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#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<SDL_Scancode, std::string> 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<std::string, SDL_Scancode> 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<int, std::string> 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<std::string, int> 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 <typename T, typename Validator>
|
||||
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<T>();
|
||||
dest = validate(val) ? val : fallback;
|
||||
} catch (...) {
|
||||
dest = fallback;
|
||||
}
|
||||
}
|
||||
|
||||
// Variant sin validador: només lectura amb fallback en cas d'error.
|
||||
template <typename T>
|
||||
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<T>();
|
||||
} 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<float>(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<int>();
|
||||
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<int>();
|
||||
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<int>();
|
||||
// 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<std::string>());
|
||||
}
|
||||
if (kb.contains("key_right")) {
|
||||
player1.keyboard.key_right = stringToScancode(kb["key_right"].get_value<std::string>());
|
||||
}
|
||||
if (kb.contains("key_thrust")) {
|
||||
player1.keyboard.key_thrust = stringToScancode(kb["key_thrust"].get_value<std::string>());
|
||||
}
|
||||
if (kb.contains("key_shoot")) {
|
||||
player1.keyboard.key_shoot = stringToScancode(kb["key_shoot"].get_value<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
// 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<std::string>());
|
||||
}
|
||||
if (gp.contains("button_right")) {
|
||||
player1.gamepad.button_right = stringToButton(gp["button_right"].get_value<std::string>());
|
||||
}
|
||||
if (gp.contains("button_thrust")) {
|
||||
player1.gamepad.button_thrust = stringToButton(gp["button_thrust"].get_value<std::string>());
|
||||
}
|
||||
if (gp.contains("button_shoot")) {
|
||||
player1.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
// Carregar nom del gamepad
|
||||
if (p1.contains("gamepad_name")) {
|
||||
player1.gamepad_name = p1["gamepad_name"].get_value<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
// 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<std::string>());
|
||||
}
|
||||
if (kb.contains("key_right")) {
|
||||
player2.keyboard.key_right = stringToScancode(kb["key_right"].get_value<std::string>());
|
||||
}
|
||||
if (kb.contains("key_thrust")) {
|
||||
player2.keyboard.key_thrust = stringToScancode(kb["key_thrust"].get_value<std::string>());
|
||||
}
|
||||
if (kb.contains("key_shoot")) {
|
||||
player2.keyboard.key_shoot = stringToScancode(kb["key_shoot"].get_value<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
// 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<std::string>());
|
||||
}
|
||||
if (gp.contains("button_right")) {
|
||||
player2.gamepad.button_right = stringToButton(gp["button_right"].get_value<std::string>());
|
||||
}
|
||||
if (gp.contains("button_thrust")) {
|
||||
player2.gamepad.button_thrust = stringToButton(gp["button_thrust"].get_value<std::string>());
|
||||
}
|
||||
if (gp.contains("button_shoot")) {
|
||||
player2.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
// Carregar nom del gamepad
|
||||
if (p2.contains("gamepad_name")) {
|
||||
player2.gamepad_name = p2["gamepad_name"].get_value<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
// 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<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
file.close();
|
||||
|
||||
try {
|
||||
// Parsejar YAML
|
||||
auto yaml = fkyaml::node::deserialize(content);
|
||||
|
||||
// Validar versión
|
||||
if (yaml.contains("version")) {
|
||||
version = yaml["version"].get_value<std::string>();
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1,121 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h> // Para SDL_Scancode
|
||||
|
||||
#include <string>
|
||||
|
||||
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
|
||||
@@ -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<unsigned>(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<StageSystem::StageManager>(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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Ship, 2> ships_; // [0]=P1, [1]=P2
|
||||
std::array<Enemy, Constants::MAX_ORNIS> 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<Bullet, static_cast<std::size_t>(Constants::MAX_BALES) * 2> bullets_;
|
||||
std::array<float, 2> hit_timer_per_player_; // Death timers per player (seconds)
|
||||
|
||||
// Estat del juego
|
||||
std::array<Ship, 2> ships_; // [0]=P1, [1]=P2
|
||||
std::array<Enemy, Constants::MAX_ORNIS> 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<Bullet, static_cast<std::size_t>(Constants::MAX_BALES) * 2> bullets_;
|
||||
std::array<float, 2> hit_timer_per_player_; // Death timers per player (seconds)
|
||||
// Lives and game over system
|
||||
std::array<int, 2> 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<int, 2> score_per_player_; // [0]=P1, [1]=P2
|
||||
|
||||
// Lives and game over system
|
||||
std::array<int, 2> 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<int, 2> score_per_player_; // [0]=P1, [1]=P2
|
||||
// Text vectorial
|
||||
Graphics::VectorText text_;
|
||||
|
||||
// Text vectorial
|
||||
Graphics::VectorText text_;
|
||||
// [NEW] Stage system
|
||||
std::unique_ptr<StageSystem::StageSystemConfig> stage_config_;
|
||||
std::unique_ptr<StageSystem::StageManager> stage_manager_;
|
||||
|
||||
// [NEW] Stage system
|
||||
std::unique_ptr<StageSystem::StageSystemConfig> stage_config_;
|
||||
std::unique_ptr<StageSystem::StageManager> 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();
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
+20
-5
@@ -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 <string>
|
||||
#include <vector>
|
||||
|
||||
#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::string>
|
||||
std::vector<std::string> 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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user