Merge branch 'refactor/english-identifiers': identificadors valencians/castellans a anglès
This commit is contained in:
@@ -9,8 +9,8 @@ namespace Defaults::FX::Glow {
|
||||
|
||||
// Neon glow per outline gruixut, aplicat automàticament per renderShape.
|
||||
// Els gruixos d'halo són RÀTIOS del bounding_radius de la shape (escalat
|
||||
// per scale), de manera que un pentàgon (radi 20) té halo gros i una bala
|
||||
// (radi 3) té halo subtil. El core (últim pass) usa el gruix de línia
|
||||
// per scale), de manera que un pentàgon (radius 20) té halo gros i una bala
|
||||
// (radius 3) té halo subtil. El core (últim pass) usa el gruix de línia
|
||||
// global (1.5px) — no escala amb la shape.
|
||||
//
|
||||
// Cap superior: si la shape és molt gran (logos del títol, intro), el
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// enemies.hpp - Configuració per tipus d'enemic (Pentagon/Cuadrado/Molinillo), spawn i scoring
|
||||
// enemies.hpp - Configuració per tipus d'enemic (Pentagon/Square/Molinillo), spawn i scoring
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#pragma once
|
||||
@@ -17,57 +17,57 @@ namespace Defaults::Enemies {
|
||||
|
||||
// Pentagon (esquivador - zigzag evasion)
|
||||
namespace Pentagon {
|
||||
constexpr float VELOCITAT = 35.0F; // px/s (slightly slower)
|
||||
constexpr float SPEED = 35.0F; // px/s (slightly slower)
|
||||
constexpr float MASS = 5.0F; // Masa estándar
|
||||
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 ANGLE_CHANGE_PROB = 0.20F; // 20% per wall hit (frequent zigzag)
|
||||
constexpr float ANGLE_CHANGE_MAX = 1.0F; // Max random angle change (rad)
|
||||
constexpr float ZIGZAG_PROB_PER_SECOND = 0.8F; // Probabilidad de zigzag por segundo
|
||||
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 float ROTATION_DELTA_MIN = 0.75F; // Min visual rotation (rad/s) [+50%]
|
||||
constexpr float ROTATION_DELTA_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 MASS = 8.0F; // Más pesado, "tanque"
|
||||
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%]
|
||||
// Square (perseguidor - tracks player)
|
||||
namespace Square {
|
||||
constexpr float SPEED = 40.0F; // px/s (medium speed)
|
||||
constexpr float MASS = 8.0F; // Más pesado, "tanque"
|
||||
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 ROTATION_DELTA_MIN = 0.3F; // Slow rotation [+50%]
|
||||
constexpr float ROTATION_DELTA_MAX = 1.5F; // [+50%]
|
||||
constexpr const char* SHAPE_FILE = "enemy_square.shp";
|
||||
} // namespace Cuadrado
|
||||
} // namespace Square
|
||||
|
||||
// Molinillo (agressiu - fast straight lines, proximity spin-up)
|
||||
namespace Molinillo {
|
||||
constexpr float VELOCITAT = 50.0F; // px/s (fastest)
|
||||
constexpr float MASS = 4.0F; // Más liviano, ágil
|
||||
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)
|
||||
namespace Pinwheel {
|
||||
constexpr float SPEED = 50.0F; // px/s (fastest)
|
||||
constexpr float MASS = 4.0F; // Más liviano, ágil
|
||||
constexpr float ANGLE_CHANGE_PROB = 0.05F; // 5% per wall hit (rare direction change)
|
||||
constexpr float ANGLE_CHANGE_MAX = 0.3F; // Small angle adjustments
|
||||
constexpr float ROTATION_DELTA_MIN = 3.0F; // Base rotation (rad/s) [+50%]
|
||||
constexpr float ROTATION_DELTA_MAX = 6.0F; // [+50%]
|
||||
constexpr float ROTATION_DELTA_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
|
||||
} // namespace Pinwheel
|
||||
|
||||
// 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)
|
||||
constexpr float PULSE_TRIGGER_PROB = 0.01F; // 1% chance per second
|
||||
constexpr float PULSE_DURATION_MIN = 1.0F; // Min duration (seconds)
|
||||
constexpr float PULSE_DURATION_MAX = 3.0F; // Max duration (seconds)
|
||||
constexpr float PULSE_AMPLITUD_MIN = 0.08F; // Min scale variation
|
||||
constexpr float PULSE_AMPLITUD_MAX = 0.20F; // Max scale variation
|
||||
constexpr float PULSE_FREQ_MIN = 1.5F; // Min frequency (Hz)
|
||||
constexpr float PULSE_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]
|
||||
constexpr float ROTATION_ACCEL_TRIGGER_PROB = 0.02F; // 2% chance per second [4x more frequent]
|
||||
constexpr float ROTATION_ACCEL_DURATION_MIN = 3.0F; // Min transition time
|
||||
constexpr float ROTATION_ACCEL_DURATION_MAX = 8.0F; // Max transition time
|
||||
constexpr float ROTATION_ACCEL_MULTIPLIER_MIN = 0.3F; // Min speed multiplier [more dramatic]
|
||||
constexpr float ROTATION_ACCEL_MULTIPLIER_MAX = 4.0F; // Max speed multiplier [more dramatic]
|
||||
} // namespace Animation
|
||||
|
||||
// Wounded state (entre primer impacto y explosión)
|
||||
@@ -93,9 +93,9 @@ namespace Defaults::Enemies {
|
||||
|
||||
// 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)
|
||||
constexpr int PENTAGON_SCORE = 100; // Pentágono (esquivador, 35 px/s)
|
||||
constexpr int SQUARE_SCORE = 150; // Square (perseguidor, 40 px/s)
|
||||
constexpr int PINWHEEL_SCORE = 200; // Molinillo (agressiu, 50 px/s)
|
||||
} // namespace Scoring
|
||||
|
||||
} // namespace Defaults::Enemies
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
namespace Defaults::Entities {
|
||||
|
||||
constexpr int MAX_ORNIS = 15;
|
||||
constexpr int MAX_BALES = 50;
|
||||
constexpr int MAX_BULLETS = 50;
|
||||
|
||||
constexpr float SHIP_RADIUS = 12.0F;
|
||||
constexpr float ENEMY_RADIUS = 20.0F;
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace Defaults::Game {
|
||||
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)
|
||||
// Posición inicial de la nave en INIT_HUD (75% de altura de zone 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)
|
||||
|
||||
@@ -14,11 +14,11 @@ namespace Defaults::Palette {
|
||||
// brillantor perceptual sota el bloom (sense alterar la identitat de color).
|
||||
// El canal dominant es manté a 255 a cada color per maximitzar la saturació
|
||||
// visible quan el halo s'expandeix.
|
||||
constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro
|
||||
constexpr SDL_Color BULLET = {.r = 155, .g = 255, .b = 175, .a = 255}; // Verde laser
|
||||
constexpr SDL_Color PENTAGON = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cyan pur "esquivador"
|
||||
constexpr SDL_Color QUADRAT = {.r = 255, .g = 0, .b = 0, .a = 255}; // Roig pur "tank"
|
||||
constexpr SDL_Color MOLINILLO = {.r = 255, .g = 0, .b = 255, .a = 255}; // Magenta pur "agressiu"
|
||||
constexpr SDL_Color WOUNDED = {.r = 255, .g = 220, .b = 60, .a = 255}; // Dorado: enemigo herido
|
||||
constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro
|
||||
constexpr SDL_Color BULLET = {.r = 155, .g = 255, .b = 175, .a = 255}; // Verde laser
|
||||
constexpr SDL_Color PENTAGON = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cyan pur "esquivador"
|
||||
constexpr SDL_Color SQUARE = {.r = 255, .g = 0, .b = 0, .a = 255}; // Roig pur "tank"
|
||||
constexpr SDL_Color PINWHEEL = {.r = 255, .g = 0, .b = 255, .a = 255}; // Magenta pur "agressiu"
|
||||
constexpr SDL_Color WOUNDED = {.r = 255, .g = 220, .b = 60, .a = 255}; // Dorado: enemigo herido
|
||||
|
||||
} // namespace Defaults::Palette
|
||||
|
||||
@@ -28,14 +28,14 @@ namespace Defaults::Physics {
|
||||
|
||||
// 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; // Vida mínima garantida (s) — després pot morir per velocitat baixa
|
||||
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris min lifetime (matches DEATH_DURATION)
|
||||
constexpr float SHRINK_RATE = 1.0F; // Reducció de mida (1.0 = encoge a 0 al final del min_lifetime)
|
||||
constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s)
|
||||
constexpr float VARIACIO_SPEED = 40.0F; // ±variació aleatòria (px/s)
|
||||
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
|
||||
constexpr float ROTATION_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
|
||||
constexpr float ROTATION_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s)
|
||||
constexpr float TEMPS_VIDA = 2.0F; // Vida mínima garantida (s) — després pot morir per velocitat baixa
|
||||
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris min lifetime (matches DEATH_DURATION)
|
||||
constexpr float SHRINK_RATE = 1.0F; // Reducció de mida (1.0 = encoge a 0 al final del min_lifetime)
|
||||
|
||||
// Política de mort: passat el min_lifetime, el fragment mor quan la
|
||||
// seva velocity cau per sota d'aquest llindar. Així els fragments
|
||||
@@ -48,9 +48,9 @@ namespace Defaults::Physics {
|
||||
constexpr float RESTITUTION_BOUNDS = 0.7F;
|
||||
|
||||
// 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²)
|
||||
constexpr float INHERITANCE_FACTOR_MIN = 0.7F; // Mínimo 70% del drotacio heredat
|
||||
constexpr float INHERITANCE_FACTOR_MAX = 1.0F; // Màxim 100% del drotacio heredat
|
||||
constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²)
|
||||
|
||||
// Velocity heredada de la nau a l'explosió (80% del feel original).
|
||||
constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F;
|
||||
@@ -68,7 +68,7 @@ namespace Defaults::Physics {
|
||||
// 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)
|
||||
constexpr float SPEED_ROT_MAX = 1.5F; // rad/s (~86°/s)
|
||||
} // namespace Debris
|
||||
|
||||
} // namespace Defaults::Physics
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace Defaults::Title {
|
||||
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
|
||||
// Fórmula: (radius 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)
|
||||
|
||||
@@ -23,12 +23,12 @@ namespace Graphics {
|
||||
}
|
||||
|
||||
void Border::bumpAt(Vec2 contact_point, float strength) {
|
||||
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
|
||||
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||||
const std::array<float, SIDE_COUNT> DISTANCES = {
|
||||
/* TOP */ std::abs(contact_point.y - zona.y),
|
||||
/* RIGHT */ std::abs((zona.x + zona.w) - contact_point.x),
|
||||
/* BOTTOM */ std::abs((zona.y + zona.h) - contact_point.y),
|
||||
/* LEFT */ std::abs(contact_point.x - zona.x)};
|
||||
/* TOP */ std::abs(contact_point.y - zone.y),
|
||||
/* RIGHT */ std::abs((zone.x + zone.w) - contact_point.x),
|
||||
/* BOTTOM */ std::abs((zone.y + zone.h) - contact_point.y),
|
||||
/* LEFT */ std::abs(contact_point.x - zone.x)};
|
||||
|
||||
int closest_idx = 0;
|
||||
float closest_dist = DISTANCES[0];
|
||||
@@ -71,11 +71,11 @@ namespace Graphics {
|
||||
} // namespace
|
||||
|
||||
void Border::draw() const {
|
||||
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
|
||||
const int X1 = static_cast<int>(zona.x);
|
||||
const int Y1 = static_cast<int>(zona.y);
|
||||
const int X2 = static_cast<int>(zona.x + zona.w);
|
||||
const int Y2 = static_cast<int>(zona.y + zona.h);
|
||||
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||||
const int X1 = static_cast<int>(zone.x);
|
||||
const int Y1 = static_cast<int>(zone.y);
|
||||
const int X2 = static_cast<int>(zone.x + zone.w);
|
||||
const int Y2 = static_cast<int>(zone.y + zone.h);
|
||||
|
||||
const int OFF_TOP = static_cast<int>(sides_[SIDE_TOP].displacement_px);
|
||||
const int OFF_RIGHT = static_cast<int>(sides_[SIDE_RIGHT].displacement_px);
|
||||
|
||||
@@ -141,9 +141,9 @@ namespace Graphics {
|
||||
}
|
||||
|
||||
void Playfield::buildLines() {
|
||||
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
|
||||
const float CELL_W = zona.w / static_cast<float>(Defaults::Playfield::COLUMNS);
|
||||
const float CELL_H = zona.h / static_cast<float>(Defaults::Playfield::ROWS);
|
||||
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||||
const float CELL_W = zone.w / static_cast<float>(Defaults::Playfield::COLUMNS);
|
||||
const float CELL_H = zone.h / static_cast<float>(Defaults::Playfield::ROWS);
|
||||
const float SUB_W = CELL_W / static_cast<float>(Defaults::Playfield::SUBDIVISIONS);
|
||||
const float SUB_H = CELL_H / static_cast<float>(Defaults::Playfield::SUBDIVISIONS);
|
||||
const int SUB_VERTS = Defaults::Playfield::COLUMNS * Defaults::Playfield::SUBDIVISIONS;
|
||||
@@ -154,14 +154,14 @@ namespace Graphics {
|
||||
|
||||
// Verticals: posicions i ∈ [1, SUB_VERTS-1].
|
||||
for (int i = 1; i < SUB_VERTS; i++) {
|
||||
const float X = zona.x + (static_cast<float>(i) * SUB_W);
|
||||
const float X = zone.x + (static_cast<float>(i) * SUB_W);
|
||||
const bool IS_MAIN = (i % Defaults::Playfield::SUBDIVISIONS) == 0;
|
||||
const float BRIGHTNESS = IS_MAIN
|
||||
? Defaults::Playfield::GRID_BRIGHTNESS
|
||||
: Defaults::Playfield::SUBGRID_BRIGHTNESS;
|
||||
verticals.push_back(Line{
|
||||
.start = {.x = X, .y = zona.y},
|
||||
.end = {.x = X, .y = zona.y + zona.h},
|
||||
.start = {.x = X, .y = zone.y},
|
||||
.end = {.x = X, .y = zone.y + zone.h},
|
||||
.brightness = BRIGHTNESS,
|
||||
.spawn_time_s = 0.0F,
|
||||
.is_vertical = true});
|
||||
@@ -169,14 +169,14 @@ namespace Graphics {
|
||||
|
||||
// Horitzontals: posicions j ∈ [1, SUB_HORIZ-1].
|
||||
for (int j = 1; j < SUB_HORIZ; j++) {
|
||||
const float Y = zona.y + (static_cast<float>(j) * SUB_H);
|
||||
const float Y = zone.y + (static_cast<float>(j) * SUB_H);
|
||||
const bool IS_MAIN = (j % Defaults::Playfield::SUBDIVISIONS) == 0;
|
||||
const float BRIGHTNESS = IS_MAIN
|
||||
? Defaults::Playfield::GRID_BRIGHTNESS
|
||||
: Defaults::Playfield::SUBGRID_BRIGHTNESS;
|
||||
horizontals.push_back(Line{
|
||||
.start = {.x = zona.x, .y = Y},
|
||||
.end = {.x = zona.x + zona.w, .y = Y},
|
||||
.start = {.x = zone.x, .y = Y},
|
||||
.end = {.x = zone.x + zone.w, .y = Y},
|
||||
.brightness = BRIGHTNESS,
|
||||
.spawn_time_s = 0.0F,
|
||||
.is_vertical = false});
|
||||
|
||||
@@ -25,11 +25,11 @@ namespace Graphics {
|
||||
}
|
||||
|
||||
void StarfieldParallax::buildStars() {
|
||||
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
|
||||
const float MIN_X = zona.x;
|
||||
const float MAX_X = zona.x + zona.w;
|
||||
const float MIN_Y = zona.y;
|
||||
const float MAX_Y = zona.y + zona.h;
|
||||
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||||
const float MIN_X = zone.x;
|
||||
const float MAX_X = zone.x + zone.w;
|
||||
const float MIN_Y = zone.y;
|
||||
const float MAX_Y = zone.y + zone.h;
|
||||
|
||||
// Color únic per a totes les estrelles: el mateix blanc-blau gel
|
||||
// del starfield del títol (Defaults::Title::Colors::STARFIELD).
|
||||
@@ -89,13 +89,13 @@ namespace Graphics {
|
||||
}
|
||||
|
||||
void StarfieldParallax::update(float delta_time, Vec2 world_velocity) {
|
||||
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
|
||||
const float MIN_X = zona.x;
|
||||
const float MAX_X = zona.x + zona.w;
|
||||
const float MIN_Y = zona.y;
|
||||
const float MAX_Y = zona.y + zona.h;
|
||||
const float W = zona.w;
|
||||
const float H = zona.h;
|
||||
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||||
const float MIN_X = zone.x;
|
||||
const float MAX_X = zone.x + zone.w;
|
||||
const float MIN_Y = zone.y;
|
||||
const float MAX_Y = zone.y + zone.h;
|
||||
const float W = zone.w;
|
||||
const float H = zone.h;
|
||||
|
||||
for (auto& star : stars_) {
|
||||
const float FACTOR = layerParallax(star.layer);
|
||||
|
||||
@@ -235,19 +235,19 @@ namespace Graphics {
|
||||
}
|
||||
}
|
||||
|
||||
void VectorText::renderCentered(const std::string& text, const Vec2& centre_punt, float scale, float spacing, float brightness, SDL_Color color) const {
|
||||
void VectorText::renderCentered(const std::string& text, const Vec2& centre_point, float scale, float spacing, float brightness, SDL_Color color) const {
|
||||
// Calcular dimensions del text
|
||||
float text_width = getTextWidth(text, scale, spacing);
|
||||
float text_height = getTextHeight(scale);
|
||||
|
||||
// Calcular posición de l'esquina superior izquierda
|
||||
// restant la meitat de las dimensions del point central
|
||||
Vec2 posicio_esquerra = {
|
||||
.x = centre_punt.x - (text_width / 2.0F),
|
||||
.y = centre_punt.y - (text_height / 2.0F)};
|
||||
Vec2 top_left_position = {
|
||||
.x = centre_point.x - (text_width / 2.0F),
|
||||
.y = centre_point.y - (text_height / 2.0F)};
|
||||
|
||||
// Delegar al método render() existent
|
||||
render(text, posicio_esquerra, scale, spacing, brightness, color);
|
||||
render(text, top_left_position, scale, spacing, brightness, color);
|
||||
}
|
||||
|
||||
auto VectorText::getTextWidth(const std::string& text, float scale, float spacing) -> float {
|
||||
|
||||
@@ -31,12 +31,12 @@ namespace Graphics {
|
||||
|
||||
// Renderizar string centrado en un punto
|
||||
// - text: cadena a renderizar
|
||||
// - centre_punt: punto central del texto (no esquina superior izquierda)
|
||||
// - centre_point: punto central del texto (no esquina superior izquierda)
|
||||
// - scale: factor de scale (1.0 = 20×40 px por carácter)
|
||||
// - spacing: espacio entre caracteres en píxeles (a scale 1.0)
|
||||
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
|
||||
// - color: color RGBA explícit; si alpha==0 (default) s'usa l'oscil·lador global
|
||||
void renderCentered(const std::string& text, const Vec2& centre_punt, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const;
|
||||
void renderCentered(const std::string& text, const Vec2& centre_point, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const;
|
||||
|
||||
// Calcular ancho total de un string (útil para centrado).
|
||||
// Es estático: no depende del estado del VectorText (el ancho viene de
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Physics {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calcular radi combinat (con amplificador per hitbox generós)
|
||||
// Calcular radius combinat (con amplificador per hitbox generós)
|
||||
float suma_radis = (a.getCollisionRadius() + b.getCollisionRadius()) * amplifier;
|
||||
float suma_radis_sq = suma_radis * suma_radis;
|
||||
|
||||
@@ -31,8 +31,8 @@ namespace Physics {
|
||||
return dist_sq <= suma_radis_sq;
|
||||
}
|
||||
|
||||
// Swept collision: una entitat mòbil (radi r_a) s'ha desplaçat de p0 a p1 aquest
|
||||
// frame. Comprova si el segment expandit pel radi conjunt (r_a + radi de b, amb
|
||||
// Swept collision: una entitat mòbil (radius r_a) s'ha desplaçat de p0 a p1 aquest
|
||||
// frame. Comprova si el segment expandit pel radius conjunt (r_a + radius de b, amb
|
||||
// amplificador) toca el cercle de l'entity b. Equival al check discrete quan
|
||||
// p0 == p1 (sense moviment). Evita tunneling a velocitats altes.
|
||||
inline auto checkCollisionSwept(const Vec2& p0, const Vec2& p1, float r_a, const Entities::Entity& b, float amplifier = 1.0F) -> bool {
|
||||
|
||||
@@ -4,52 +4,52 @@
|
||||
|
||||
namespace GameConfig {
|
||||
|
||||
// Mode de juego
|
||||
enum class Mode : std::uint8_t {
|
||||
NORMAL, // Partida normal
|
||||
DEMO // Mode demostració (futur)
|
||||
};
|
||||
// Mode de juego
|
||||
enum class Mode : std::uint8_t {
|
||||
NORMAL, // Partida normal
|
||||
DEMO // Mode demostració (futur)
|
||||
};
|
||||
|
||||
// Configuración de una match
|
||||
struct MatchConfig {
|
||||
bool jugador1_actiu{false}; // Es active el player 1?
|
||||
bool jugador2_actiu{false}; // Es active el player 2?
|
||||
// Configuración de una match
|
||||
struct MatchConfig {
|
||||
bool player1_active{false}; // Es active el player 1?
|
||||
bool player2_active{false}; // Es active el player 2?
|
||||
Mode mode{Mode::NORMAL}; // Mode de juego
|
||||
|
||||
// Métodos auxiliars
|
||||
|
||||
// Retorna true si solo hay un player active
|
||||
[[nodiscard]] auto isSinglePlayer() const -> bool {
|
||||
return (jugador1_actiu && !jugador2_actiu) ||
|
||||
(!jugador1_actiu && jugador2_actiu);
|
||||
return (player1_active && !player2_active) ||
|
||||
(!player1_active && player2_active);
|
||||
}
|
||||
|
||||
// Retorna true si hay dos jugadors active
|
||||
[[nodiscard]] auto isCoop() const -> bool {
|
||||
return jugador1_actiu && jugador2_actiu;
|
||||
return player1_active && player2_active;
|
||||
}
|
||||
|
||||
// Retorna true si no hay sin player active
|
||||
[[nodiscard]] auto hasNoPlayers() const -> bool {
|
||||
return !jugador1_actiu && !jugador2_actiu;
|
||||
return !player1_active && !player2_active;
|
||||
}
|
||||
|
||||
// Compte de jugadors active (0, 1 o 2)
|
||||
[[nodiscard]] auto getPlayerCount() const -> uint8_t {
|
||||
return (jugador1_actiu ? 1 : 0) + (jugador2_actiu ? 1 : 0);
|
||||
return (player1_active ? 1 : 0) + (player2_active ? 1 : 0);
|
||||
}
|
||||
|
||||
// Retorna l'ID de l'únic player active (0 o 1)
|
||||
// Solo vàlid si es_un_jugador() retorna true
|
||||
[[nodiscard]] auto getSinglePlayerId() const -> uint8_t {
|
||||
if (jugador1_actiu && !jugador2_actiu) {
|
||||
if (player1_active && !player2_active) {
|
||||
return 0;
|
||||
}
|
||||
if (!jugador1_actiu && jugador2_actiu) {
|
||||
if (!player1_active && player2_active) {
|
||||
return 1;
|
||||
}
|
||||
return 0; // Fallback (necesario comprovar es_un_jugador() primer)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace GameConfig
|
||||
|
||||
+19
-19
@@ -7,40 +7,40 @@
|
||||
namespace Constants {
|
||||
// Límits de objectes
|
||||
constexpr int MAX_ORNIS = Defaults::Entities::MAX_ORNIS;
|
||||
constexpr int MAX_BALES = Defaults::Entities::MAX_BALES;
|
||||
constexpr int MAX_BULLETS = Defaults::Entities::MAX_BULLETS;
|
||||
|
||||
// Matemàtiques
|
||||
constexpr float PI = Defaults::Math::PI;
|
||||
|
||||
// Helpers per comprovar límits de zona
|
||||
// Helpers per comprovar límits de zone
|
||||
inline auto isInPlayArea(float x, float y) -> bool {
|
||||
const SDL_FPoint POINT = {x, y};
|
||||
return SDL_PointInRectFloat(&POINT, &Defaults::Zones::PLAYAREA);
|
||||
}
|
||||
|
||||
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;
|
||||
const auto& zone = Defaults::Zones::PLAYAREA;
|
||||
min_x = zone.x;
|
||||
max_x = zone.x + zone.w;
|
||||
min_y = zone.y;
|
||||
max_y = zone.y + zone.h;
|
||||
}
|
||||
|
||||
// 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
|
||||
// Obtenir límits segurs (compensant radius de l'entidad)
|
||||
inline void getSafePlayAreaBounds(float radius, float& min_x, float& max_x, float& min_y, float& max_y) {
|
||||
const auto& zone = Defaults::Zones::PLAYAREA;
|
||||
constexpr float SAFETY_MARGIN = 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;
|
||||
min_x = zone.x + radius + SAFETY_MARGIN;
|
||||
max_x = zone.x + zone.w - radius - SAFETY_MARGIN;
|
||||
min_y = zone.y + radius + SAFETY_MARGIN;
|
||||
max_y = zone.y + zone.h - radius - SAFETY_MARGIN;
|
||||
}
|
||||
|
||||
// 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);
|
||||
inline void getPlayAreaCenter(float& center_x, float& center_y) {
|
||||
const auto& zone = Defaults::Zones::PLAYAREA;
|
||||
center_x = zone.x + (zone.w / 2.0F);
|
||||
center_y = zone.y + (zone.h / 2.0F);
|
||||
}
|
||||
} // namespace Constants
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace Effects {
|
||||
// Política: viu sempre durant min_lifetime, després mor quan
|
||||
// |velocity| < MIN_SPEED_TO_DIE (definit en Defaults). Així els
|
||||
// fragments ràpids no "popen" en moviment.
|
||||
float temps_vida; // Temps transcorregut (segons)
|
||||
float elapsed_time; // Temps transcorregut (segons)
|
||||
float min_lifetime; // Temps mínim garantit (segons)
|
||||
bool active; // Està actiu?
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ namespace Effects {
|
||||
float speed =
|
||||
velocitat_base +
|
||||
(((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) *
|
||||
Defaults::Physics::Debris::VARIACIO_VELOCITAT);
|
||||
Defaults::Physics::Debris::VARIACIO_SPEED);
|
||||
debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x;
|
||||
debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y;
|
||||
debris->acceleration = friction;
|
||||
@@ -150,7 +150,7 @@ namespace Effects {
|
||||
|
||||
// Vida i shrinking — min_lifetime és el temps mínim garantit; després
|
||||
// el fragment mor quan |velocity| < MIN_SPEED_TO_DIE.
|
||||
debris->temps_vida = 0.0F;
|
||||
debris->elapsed_time = 0.0F;
|
||||
debris->min_lifetime = lifetime;
|
||||
debris->factor_shrink = Defaults::Physics::Debris::SHRINK_RATE;
|
||||
|
||||
@@ -170,16 +170,16 @@ namespace Effects {
|
||||
|
||||
// FASE 1: Aplicar herència i variació
|
||||
float factor_herencia =
|
||||
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN +
|
||||
Defaults::Physics::Debris::INHERITANCE_FACTOR_MIN +
|
||||
((std::rand() / static_cast<float>(RAND_MAX)) *
|
||||
(Defaults::Physics::Debris::FACTOR_HERENCIA_MAX -
|
||||
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN));
|
||||
(Defaults::Physics::Debris::INHERITANCE_FACTOR_MAX -
|
||||
Defaults::Physics::Debris::INHERITANCE_FACTOR_MIN));
|
||||
float velocitat_ang_heretada = velocitat_angular * factor_herencia;
|
||||
float variacio = ((std::rand() / static_cast<float>(RAND_MAX)) * 0.2F) - 0.1F;
|
||||
velocitat_ang_heretada *= (1.0F + variacio);
|
||||
|
||||
// FASE 2: Cap a la velocity màxima; l'excés es converteix en tangencial
|
||||
constexpr float CAP = Defaults::Physics::Debris::VELOCITAT_ROT_MAX;
|
||||
constexpr float CAP = Defaults::Physics::Debris::SPEED_ROT_MAX;
|
||||
float abs_ang = std::abs(velocitat_ang_heretada);
|
||||
float sign_ang = (velocitat_ang_heretada >= 0.0F) ? 1.0F : -1.0F;
|
||||
|
||||
@@ -213,10 +213,10 @@ namespace Effects {
|
||||
|
||||
// Rotación visual aleatòria (factor = 0.0 o sin velocidad angular)
|
||||
debris.velocitat_rot_visual =
|
||||
Defaults::Physics::Debris::ROTACIO_MIN +
|
||||
Defaults::Physics::Debris::ROTATION_MIN +
|
||||
((std::rand() / static_cast<float>(RAND_MAX)) *
|
||||
(Defaults::Physics::Debris::ROTACIO_MAX -
|
||||
Defaults::Physics::Debris::ROTACIO_MIN));
|
||||
(Defaults::Physics::Debris::ROTATION_MAX -
|
||||
Defaults::Physics::Debris::ROTATION_MIN));
|
||||
|
||||
// 50% probabilitat de rotación en sentit contrari
|
||||
if (std::rand() % 2 == 0) {
|
||||
@@ -266,12 +266,12 @@ namespace Effects {
|
||||
}
|
||||
|
||||
// 1. Actualitzar time de vida
|
||||
debris.temps_vida += delta_time;
|
||||
debris.elapsed_time += delta_time;
|
||||
|
||||
// Política de mort: viu sí o sí durant min_lifetime; després mor
|
||||
// quan la velocity cau per sota d'un llindar. Així els fragments
|
||||
// ràpids no desapareixen en moviment.
|
||||
if (debris.temps_vida >= debris.min_lifetime) {
|
||||
if (debris.elapsed_time >= debris.min_lifetime) {
|
||||
const float SPEED_SQ = (debris.velocity.x * debris.velocity.x) +
|
||||
(debris.velocity.y * debris.velocity.y);
|
||||
if (SPEED_SQ < Defaults::Physics::Debris::MIN_SPEED_TO_DIE_SQ) {
|
||||
@@ -344,7 +344,7 @@ namespace Effects {
|
||||
// 6. Shrink lineal sobre la longitud ORIGINAL (no iteratiu).
|
||||
// SHRINK_T va de 0 a 1 al llarg de min_lifetime; després queda
|
||||
// a 1 i el shrink_factor manté el valor mínim (1 - factor_shrink).
|
||||
const float SHRINK_T = std::min(debris.temps_vida / debris.min_lifetime, 1.0F);
|
||||
const float SHRINK_T = std::min(debris.elapsed_time / debris.min_lifetime, 1.0F);
|
||||
const float SHRINK_FACTOR = std::max(0.0F, 1.0F - (debris.factor_shrink * SHRINK_T));
|
||||
|
||||
// 7. Reconstruir p1/p2 des de la geometria autoritaritzada:
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Effects {
|
||||
// tail = head − velocity_normalitzada × current_length.
|
||||
//
|
||||
// Cicle de vida:
|
||||
// Fase 1 (temps_vida < grow_duration): current_length creix linealment
|
||||
// Fase 1 (elapsed_time < grow_duration): current_length creix linealment
|
||||
// de 0 a max_length. Brillor al màxim.
|
||||
// Fase 2: current_length = max_length × (speed/initial_speed) i brillor
|
||||
// amb la mateixa proporció. Mor quan length o brightness cauen sota
|
||||
@@ -30,7 +30,7 @@ namespace Effects {
|
||||
float max_length; // Longitud màxima (final de la fase de creixement)
|
||||
float grow_duration; // Temps de creixement de 0 a max_length (s)
|
||||
|
||||
float temps_vida; // Acumulador (s)
|
||||
float elapsed_time; // Acumulador (s)
|
||||
float initial_speed; // Speed inicial per a la proporció de fase 2
|
||||
|
||||
float brightness; // 0..1
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace Effects {
|
||||
}
|
||||
}
|
||||
|
||||
void FireworkManager::spawn(const Vec2& origen,
|
||||
void FireworkManager::spawn(const Vec2& origin,
|
||||
SDL_Color color,
|
||||
float initial_speed,
|
||||
int n_points,
|
||||
@@ -74,7 +74,7 @@ namespace Effects {
|
||||
|
||||
// Notificar als subscriptors (playfield pulses, etc.).
|
||||
if (spawn_callback_) {
|
||||
spawn_callback_(origen);
|
||||
spawn_callback_(origin);
|
||||
}
|
||||
|
||||
const float ANGLE_STEP = 2.0F * Defaults::Math::PI / static_cast<float>(n_points);
|
||||
@@ -94,7 +94,7 @@ namespace Effects {
|
||||
const float SPEED =
|
||||
initial_speed + (randSigned() * Defaults::FX::Firework::SPEED_VARIATION);
|
||||
|
||||
fw->head = origen;
|
||||
fw->head = origin;
|
||||
fw->velocity = {.x = std::cos(ANGLE) * SPEED, .y = std::sin(ANGLE) * SPEED};
|
||||
fw->acceleration = Defaults::FX::Firework::FRICTION;
|
||||
|
||||
@@ -102,7 +102,7 @@ namespace Effects {
|
||||
fw->max_length = Defaults::FX::Firework::MAX_LENGTH;
|
||||
fw->grow_duration = Defaults::FX::Firework::GROW_DURATION;
|
||||
|
||||
fw->temps_vida = 0.0F;
|
||||
fw->elapsed_time = 0.0F;
|
||||
fw->initial_speed = SPEED;
|
||||
|
||||
fw->brightness = initial_brightness;
|
||||
@@ -119,7 +119,7 @@ namespace Effects {
|
||||
continue;
|
||||
}
|
||||
|
||||
fw.temps_vida += delta_time;
|
||||
fw.elapsed_time += delta_time;
|
||||
|
||||
// 1. Fricció lineal (aplicar en la direcció del movement).
|
||||
const float SPEED = std::sqrt(
|
||||
@@ -144,9 +144,9 @@ namespace Effects {
|
||||
bounceOffPlayArea(fw.head, fw.velocity);
|
||||
|
||||
// 4. Calcular longitud i brillor segons fase.
|
||||
if (fw.temps_vida < fw.grow_duration) {
|
||||
if (fw.elapsed_time < fw.grow_duration) {
|
||||
// Fase 1: creixement lineal de 0 a max_length.
|
||||
const float T = fw.temps_vida / fw.grow_duration;
|
||||
const float T = fw.elapsed_time / fw.grow_duration;
|
||||
fw.current_length = fw.max_length * T;
|
||||
fw.brightness = Defaults::FX::Firework::INITIAL_BRIGHTNESS;
|
||||
} else {
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Effects {
|
||||
class FireworkManager {
|
||||
public:
|
||||
// Notificació opcional cada vegada que es genera un burst.
|
||||
using SpawnCallback = std::function<void(Vec2 origen)>;
|
||||
using SpawnCallback = std::function<void(Vec2 origin)>;
|
||||
|
||||
explicit FireworkManager(Rendering::Renderer* renderer);
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Effects {
|
||||
// initial_brightness: 0..1.
|
||||
// glow: si true, cada partícula es renderitza amb halo neon.
|
||||
// glow_color: color del halo. Si alpha==0, agafa el color de la línia.
|
||||
void spawn(const Vec2& origen,
|
||||
void spawn(const Vec2& origin,
|
||||
SDL_Color color = Defaults::FX::Firework::DEFAULT_COLOR,
|
||||
float initial_speed = Defaults::FX::Firework::SPEED,
|
||||
int n_points = Defaults::FX::Firework::N_POINTS,
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
|
||||
namespace Effects {
|
||||
|
||||
// FloatingScore: text animat que muestra points guanyats
|
||||
// S'activa cuando es destrueix un enemy i s'esvaeix después de un time
|
||||
struct FloatingScore {
|
||||
// FloatingScore: text animat que muestra points guanyats
|
||||
// S'activa cuando es destrueix un enemy i s'esvaeix después de un time
|
||||
struct FloatingScore {
|
||||
// Text a mostrar (e.g., "100", "150", "200")
|
||||
std::string text;
|
||||
|
||||
@@ -22,12 +22,12 @@ struct FloatingScore {
|
||||
Vec2 velocity; // px/s (normalment sin amunt: {0.0f, -30.0f})
|
||||
|
||||
// Animación de fade
|
||||
float temps_vida; // Temps transcorregut (segons)
|
||||
float temps_max; // Temps de vida màxim (segons)
|
||||
float brightness; // Brillantor calculada (0.0-1.0)
|
||||
float elapsed_time; // Temps transcorregut (segons)
|
||||
float max_lifetime; // Temps de vida màxim (segons)
|
||||
float brightness; // Brillantor calculada (0.0-1.0)
|
||||
|
||||
// Estat
|
||||
bool active;
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace Effects
|
||||
|
||||
@@ -7,93 +7,93 @@
|
||||
|
||||
namespace Effects {
|
||||
|
||||
FloatingScoreManager::FloatingScoreManager(Rendering::Renderer* renderer)
|
||||
: text_(renderer) {
|
||||
// Inicialitzar todos los slots como inactius
|
||||
for (auto& pf : pool_) {
|
||||
pf.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
void FloatingScoreManager::crear(int points, const Vec2& position) {
|
||||
// 1. Trobar slot lliure
|
||||
FloatingScore* pf = findFreeSlot();
|
||||
if (pf == nullptr) {
|
||||
return; // Pool ple (improbable)
|
||||
}
|
||||
|
||||
// 2. Inicialitzar puntuación flotante
|
||||
pf->text = std::to_string(points);
|
||||
pf->position = position;
|
||||
pf->velocity = {.x = Defaults::FloatingScore::VELOCITY_X,
|
||||
.y = Defaults::FloatingScore::VELOCITY_Y};
|
||||
pf->temps_vida = 0.0F;
|
||||
pf->temps_max = Defaults::FloatingScore::LIFETIME;
|
||||
pf->brightness = 1.0F;
|
||||
pf->active = true;
|
||||
}
|
||||
|
||||
void FloatingScoreManager::update(float delta_time) {
|
||||
for (auto& pf : pool_) {
|
||||
if (!pf.active) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 1. Actualitzar posición (deriva sin amunt)
|
||||
pf.position.x += pf.velocity.x * delta_time;
|
||||
pf.position.y += pf.velocity.y * delta_time;
|
||||
|
||||
// 2. Actualitzar time de vida
|
||||
pf.temps_vida += delta_time;
|
||||
|
||||
// 3. Calcular brightness (fade lineal)
|
||||
float progress = pf.temps_vida / pf.temps_max; // 0.0 → 1.0
|
||||
pf.brightness = 1.0F - progress; // 1.0 → 0.0
|
||||
|
||||
// 4. Desactivar cuando acaba el time
|
||||
if (pf.temps_vida >= pf.temps_max) {
|
||||
FloatingScoreManager::FloatingScoreManager(Rendering::Renderer* renderer)
|
||||
: text_(renderer) {
|
||||
// Inicialitzar todos los slots como inactius
|
||||
for (auto& pf : pool_) {
|
||||
pf.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FloatingScoreManager::draw() {
|
||||
for (const auto& pf : pool_) {
|
||||
if (!pf.active) {
|
||||
continue;
|
||||
void FloatingScoreManager::crear(int points, const Vec2& position) {
|
||||
// 1. Trobar slot lliure
|
||||
FloatingScore* pf = findFreeSlot();
|
||||
if (pf == nullptr) {
|
||||
return; // Pool ple (improbable)
|
||||
}
|
||||
|
||||
// Renderizar centrat con brightness (fade)
|
||||
constexpr float SCALE = Defaults::FloatingScore::SCALE;
|
||||
constexpr float SPACING = Defaults::FloatingScore::SPACING;
|
||||
|
||||
text_.renderCentered(pf.text, pf.position, SCALE, SPACING, pf.brightness);
|
||||
// 2. Inicialitzar puntuación flotante
|
||||
pf->text = std::to_string(points);
|
||||
pf->position = position;
|
||||
pf->velocity = {.x = Defaults::FloatingScore::VELOCITY_X,
|
||||
.y = Defaults::FloatingScore::VELOCITY_Y};
|
||||
pf->elapsed_time = 0.0F;
|
||||
pf->max_lifetime = Defaults::FloatingScore::LIFETIME;
|
||||
pf->brightness = 1.0F;
|
||||
pf->active = true;
|
||||
}
|
||||
}
|
||||
|
||||
void FloatingScoreManager::reset() {
|
||||
for (auto& pf : pool_) {
|
||||
pf.active = false;
|
||||
}
|
||||
}
|
||||
void FloatingScoreManager::update(float delta_time) {
|
||||
for (auto& pf : pool_) {
|
||||
if (!pf.active) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto FloatingScoreManager::getActiveCount() const -> int {
|
||||
int count = 0;
|
||||
for (const auto& pf : pool_) {
|
||||
if (pf.active) {
|
||||
count++;
|
||||
// 1. Actualitzar posición (deriva sin amunt)
|
||||
pf.position.x += pf.velocity.x * delta_time;
|
||||
pf.position.y += pf.velocity.y * delta_time;
|
||||
|
||||
// 2. Actualitzar time de vida
|
||||
pf.elapsed_time += delta_time;
|
||||
|
||||
// 3. Calcular brightness (fade lineal)
|
||||
float progress = pf.elapsed_time / pf.max_lifetime; // 0.0 → 1.0
|
||||
pf.brightness = 1.0F - progress; // 1.0 → 0.0
|
||||
|
||||
// 4. Desactivar cuando acaba el time
|
||||
if (pf.elapsed_time >= pf.max_lifetime) {
|
||||
pf.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
auto FloatingScoreManager::findFreeSlot() -> FloatingScore* {
|
||||
for (auto& pf : pool_) {
|
||||
if (!pf.active) {
|
||||
return &pf;
|
||||
void FloatingScoreManager::draw() {
|
||||
for (const auto& pf : pool_) {
|
||||
if (!pf.active) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Renderizar centrat con brightness (fade)
|
||||
constexpr float SCALE = Defaults::FloatingScore::SCALE;
|
||||
constexpr float SPACING = Defaults::FloatingScore::SPACING;
|
||||
|
||||
text_.renderCentered(pf.text, pf.position, SCALE, SPACING, pf.brightness);
|
||||
}
|
||||
}
|
||||
return nullptr; // Pool ple
|
||||
}
|
||||
|
||||
void FloatingScoreManager::reset() {
|
||||
for (auto& pf : pool_) {
|
||||
pf.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
auto FloatingScoreManager::getActiveCount() const -> int {
|
||||
int count = 0;
|
||||
for (const auto& pf : pool_) {
|
||||
if (pf.active) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
auto FloatingScoreManager::findFreeSlot() -> FloatingScore* {
|
||||
for (auto& pf : pool_) {
|
||||
if (!pf.active) {
|
||||
return &pf;
|
||||
}
|
||||
}
|
||||
return nullptr; // Pool ple
|
||||
}
|
||||
|
||||
} // namespace Effects
|
||||
|
||||
@@ -53,7 +53,7 @@ void Bullet::init() {
|
||||
body_.clearAccumulators();
|
||||
}
|
||||
|
||||
void Bullet::disparar(const Vec2& position, float angle, uint8_t owner_id) {
|
||||
void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id) {
|
||||
// Activar bullet
|
||||
is_active_ = true;
|
||||
|
||||
@@ -80,7 +80,7 @@ void Bullet::disparar(const Vec2& position, float angle, uint8_t owner_id) {
|
||||
}
|
||||
|
||||
void Bullet::update(float /*delta_time*/) {
|
||||
// No-op: la desactivació per fora-de-zona viu a
|
||||
// No-op: la desactivació per fora-de-zone viu a
|
||||
// Systems::Collision::desactivateOutOfBoundsBullets() perquè així té accés
|
||||
// al DebrisManager i pot generar el "trencament" visual de la bala alhora.
|
||||
// El moviment l'integra PhysicsWorld; postUpdate sincronitza center_ i prev_position_.
|
||||
|
||||
@@ -17,7 +17,7 @@ class Bullet : public Entities::Entity {
|
||||
explicit Bullet(Rendering::Renderer* renderer);
|
||||
|
||||
void init() override;
|
||||
void disparar(const Vec2& position, float angle, uint8_t owner_id);
|
||||
void fire(const Vec2& position, float angle, uint8_t owner_id);
|
||||
void update(float delta_time) override;
|
||||
void postUpdate(float delta_time) override;
|
||||
void draw() const override;
|
||||
|
||||
+110
-110
@@ -41,7 +41,7 @@ namespace {
|
||||
Enemy::Enemy(Rendering::Renderer* renderer)
|
||||
: Entity(renderer),
|
||||
|
||||
tracking_strength_(Defaults::Enemies::Cuadrado::TRACKING_STRENGTH) {
|
||||
tracking_strength_(Defaults::Enemies::Square::TRACKING_STRENGTH) {
|
||||
brightness_ = Defaults::Brightness::ENEMIC;
|
||||
|
||||
// Configuración del cuerpo físico — defaults para enemy genérico.
|
||||
@@ -58,43 +58,43 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
||||
|
||||
const char* shape_file = nullptr;
|
||||
float base_speed = 0.0F;
|
||||
float drotacio_min = 0.0F;
|
||||
float drotacio_max = 0.0F;
|
||||
float rotation_delta_min = 0.0F;
|
||||
float rotation_delta_max = 0.0F;
|
||||
float type_mass = Defaults::Enemies::Body::DEFAULT_MASS;
|
||||
|
||||
switch (type_) {
|
||||
case EnemyType::PENTAGON:
|
||||
shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE;
|
||||
base_speed = Defaults::Enemies::Pentagon::VELOCITAT;
|
||||
drotacio_min = Defaults::Enemies::Pentagon::DROTACIO_MIN;
|
||||
drotacio_max = Defaults::Enemies::Pentagon::DROTACIO_MAX;
|
||||
base_speed = Defaults::Enemies::Pentagon::SPEED;
|
||||
rotation_delta_min = Defaults::Enemies::Pentagon::ROTATION_DELTA_MIN;
|
||||
rotation_delta_max = Defaults::Enemies::Pentagon::ROTATION_DELTA_MAX;
|
||||
type_mass = Defaults::Enemies::Pentagon::MASS;
|
||||
break;
|
||||
|
||||
case EnemyType::QUADRAT:
|
||||
shape_file = Defaults::Enemies::Cuadrado::SHAPE_FILE;
|
||||
base_speed = Defaults::Enemies::Cuadrado::VELOCITAT;
|
||||
drotacio_min = Defaults::Enemies::Cuadrado::DROTACIO_MIN;
|
||||
drotacio_max = Defaults::Enemies::Cuadrado::DROTACIO_MAX;
|
||||
type_mass = Defaults::Enemies::Cuadrado::MASS;
|
||||
case EnemyType::SQUARE:
|
||||
shape_file = Defaults::Enemies::Square::SHAPE_FILE;
|
||||
base_speed = Defaults::Enemies::Square::SPEED;
|
||||
rotation_delta_min = Defaults::Enemies::Square::ROTATION_DELTA_MIN;
|
||||
rotation_delta_max = Defaults::Enemies::Square::ROTATION_DELTA_MAX;
|
||||
type_mass = Defaults::Enemies::Square::MASS;
|
||||
tracking_timer_ = 0.0F;
|
||||
break;
|
||||
|
||||
case EnemyType::MOLINILLO:
|
||||
shape_file = Defaults::Enemies::Molinillo::SHAPE_FILE;
|
||||
base_speed = Defaults::Enemies::Molinillo::VELOCITAT;
|
||||
drotacio_min = Defaults::Enemies::Molinillo::DROTACIO_MIN;
|
||||
drotacio_max = Defaults::Enemies::Molinillo::DROTACIO_MAX;
|
||||
type_mass = Defaults::Enemies::Molinillo::MASS;
|
||||
case EnemyType::PINWHEEL:
|
||||
shape_file = Defaults::Enemies::Pinwheel::SHAPE_FILE;
|
||||
base_speed = Defaults::Enemies::Pinwheel::SPEED;
|
||||
rotation_delta_min = Defaults::Enemies::Pinwheel::ROTATION_DELTA_MIN;
|
||||
rotation_delta_max = Defaults::Enemies::Pinwheel::ROTATION_DELTA_MAX;
|
||||
type_mass = Defaults::Enemies::Pinwheel::MASS;
|
||||
break;
|
||||
|
||||
default:
|
||||
std::cerr << "[Enemy] Error: tipo desconocido ("
|
||||
<< static_cast<int>(type_) << "), usando PENTAGON\n";
|
||||
shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE;
|
||||
base_speed = Defaults::Enemies::Pentagon::VELOCITAT;
|
||||
drotacio_min = Defaults::Enemies::Pentagon::DROTACIO_MIN;
|
||||
drotacio_max = Defaults::Enemies::Pentagon::DROTACIO_MAX;
|
||||
base_speed = Defaults::Enemies::Pentagon::SPEED;
|
||||
rotation_delta_min = Defaults::Enemies::Pentagon::ROTATION_DELTA_MIN;
|
||||
rotation_delta_max = Defaults::Enemies::Pentagon::ROTATION_DELTA_MAX;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
||||
const int RANGE_Y = static_cast<int>(max_y - min_y);
|
||||
center_.x = static_cast<float>((std::rand() % RANGE_X) + static_cast<int>(min_x));
|
||||
center_.y = static_cast<float>((std::rand() % RANGE_Y) + static_cast<int>(min_y));
|
||||
std::cout << "[Enemy] Advertencia: spawn sin zona segura tras "
|
||||
std::cout << "[Enemy] Advertencia: spawn sin zone segura tras "
|
||||
<< Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS << " intentos\n";
|
||||
}
|
||||
} else {
|
||||
@@ -152,28 +152,28 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
||||
body_.clearAccumulators();
|
||||
|
||||
// Rotación visual aleatoria (independiente del body)
|
||||
const float DROTACIO_RANGE = drotacio_max - drotacio_min;
|
||||
drotacio_ = drotacio_min + ((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DROTACIO_RANGE);
|
||||
rotacio_ = 0.0F;
|
||||
const float ROTATION_DELTA_RANGE = rotation_delta_max - rotation_delta_min;
|
||||
rotation_delta_ = rotation_delta_min + ((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * ROTATION_DELTA_RANGE);
|
||||
rotation_ = 0.0F;
|
||||
|
||||
// Estado de animación
|
||||
animacio_ = EnemyAnimation();
|
||||
animacio_.drotacio_base = drotacio_;
|
||||
animacio_.drotacio_objetivo = drotacio_;
|
||||
animacio_.drotacio_t = 1.0F;
|
||||
animation_ = EnemyAnimation();
|
||||
animation_.rotation_delta_base = rotation_delta_;
|
||||
animation_.rotation_delta_target = rotation_delta_;
|
||||
animation_.rotation_delta_t = 1.0F;
|
||||
|
||||
// Invulnerabilidad post-spawn
|
||||
timer_invulnerabilitat_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
|
||||
invulnerability_timer_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
|
||||
brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START;
|
||||
|
||||
// Timer para próximo cambio de dirección (Pentagon)
|
||||
direction_change_timer_ = 0.0F;
|
||||
|
||||
esta_ = true;
|
||||
is_active_ = true;
|
||||
}
|
||||
|
||||
void Enemy::update(float delta_time) {
|
||||
if (!esta_) {
|
||||
if (!is_active_) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -189,11 +189,11 @@ void Enemy::update(float delta_time) {
|
||||
}
|
||||
|
||||
// Decremento de invulnerabilidad + LERP de brightness
|
||||
if (timer_invulnerabilitat_ > 0.0F) {
|
||||
timer_invulnerabilitat_ -= delta_time;
|
||||
timer_invulnerabilitat_ = std::max(timer_invulnerabilitat_, 0.0F);
|
||||
if (invulnerability_timer_ > 0.0F) {
|
||||
invulnerability_timer_ -= delta_time;
|
||||
invulnerability_timer_ = std::max(invulnerability_timer_, 0.0F);
|
||||
|
||||
const float T_INV = timer_invulnerabilitat_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
|
||||
const float T_INV = invulnerability_timer_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
|
||||
const float T = 1.0F - T_INV;
|
||||
const float SMOOTH_T = T * T * (3.0F - (2.0F * T));
|
||||
constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START;
|
||||
@@ -209,11 +209,11 @@ void Enemy::update(float delta_time) {
|
||||
case EnemyType::PENTAGON:
|
||||
behaviorPentagon(delta_time);
|
||||
break;
|
||||
case EnemyType::QUADRAT:
|
||||
behaviorQuadrat(delta_time);
|
||||
case EnemyType::SQUARE:
|
||||
behaviorSquare(delta_time);
|
||||
break;
|
||||
case EnemyType::MOLINILLO:
|
||||
behaviorMolinillo(delta_time);
|
||||
case EnemyType::PINWHEEL:
|
||||
behaviorPinwheel(delta_time);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -222,18 +222,18 @@ void Enemy::update(float delta_time) {
|
||||
updateAnimation(delta_time);
|
||||
|
||||
// Rotación visual (decoración, no afecta movimiento)
|
||||
rotacio_ += drotacio_ * delta_time;
|
||||
rotation_ += rotation_delta_ * delta_time;
|
||||
}
|
||||
|
||||
void Enemy::postUpdate(float /*delta_time*/) {
|
||||
// Sincronizar mirror tras la integración del world.
|
||||
if (esta_) {
|
||||
if (is_active_) {
|
||||
center_ = body_.position;
|
||||
}
|
||||
}
|
||||
|
||||
void Enemy::draw() const {
|
||||
if (!esta_ || !shape_) {
|
||||
if (!is_active_ || !shape_) {
|
||||
return;
|
||||
}
|
||||
const float SCALE = computeCurrentScale();
|
||||
@@ -242,11 +242,11 @@ void Enemy::draw() const {
|
||||
case EnemyType::PENTAGON:
|
||||
color = Defaults::Palette::PENTAGON;
|
||||
break;
|
||||
case EnemyType::QUADRAT:
|
||||
color = Defaults::Palette::QUADRAT;
|
||||
case EnemyType::SQUARE:
|
||||
color = Defaults::Palette::SQUARE;
|
||||
break;
|
||||
case EnemyType::MOLINILLO:
|
||||
color = Defaults::Palette::MOLINILLO;
|
||||
case EnemyType::PINWHEEL:
|
||||
color = Defaults::Palette::PINWHEEL;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -260,11 +260,11 @@ void Enemy::draw() const {
|
||||
}
|
||||
}
|
||||
|
||||
Rendering::renderShape(renderer_, shape_, center_, rotacio_, SCALE, 1.0F, brightness_, color);
|
||||
Rendering::renderShape(renderer_, shape_, center_, rotation_, SCALE, 1.0F, brightness_, color);
|
||||
}
|
||||
|
||||
void Enemy::destruir() {
|
||||
esta_ = false;
|
||||
void Enemy::destroy() {
|
||||
is_active_ = false;
|
||||
body_.velocity = Vec2{};
|
||||
body_.angular_velocity = 0.0F;
|
||||
body_.radius = 0.0F; // No colisiona mientras está inactivo
|
||||
@@ -273,7 +273,7 @@ void Enemy::destruir() {
|
||||
last_hit_by_ = 0xFF;
|
||||
}
|
||||
|
||||
void Enemy::herir(uint8_t shooter_id) {
|
||||
void Enemy::hurt(uint8_t shooter_id) {
|
||||
wounded_timer_ = Defaults::Enemies::Wounded::DURATION;
|
||||
last_hit_by_ = shooter_id;
|
||||
// El so HIT ara el reprodueix la bala quan es trenca en debris
|
||||
@@ -312,7 +312,7 @@ void Enemy::behaviorPentagon(float delta_time) {
|
||||
if (RAND_VAL < Defaults::Enemies::Pentagon::ZIGZAG_PROB_PER_SECOND * delta_time) {
|
||||
const float CURRENT_ANGLE = velocityToAngle(body_.velocity);
|
||||
const float DELTA = (static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) *
|
||||
Defaults::Enemies::Pentagon::CANVI_ANGLE_MAX;
|
||||
Defaults::Enemies::Pentagon::ANGLE_CHANGE_MAX;
|
||||
const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA);
|
||||
const float SPEED = body_.velocity.length();
|
||||
setVelocityFromAngle(NEW_ANGLE, SPEED);
|
||||
@@ -320,12 +320,12 @@ void Enemy::behaviorPentagon(float delta_time) {
|
||||
}
|
||||
}
|
||||
|
||||
// QUADRAT: tracking discreto cada TRACKING_INTERVAL. Ajusta dirección
|
||||
// SQUARE: tracking discreto cada TRACKING_INTERVAL. Ajusta dirección
|
||||
// hacia el ship mezclando con tracking_strength_.
|
||||
void Enemy::behaviorQuadrat(float delta_time) {
|
||||
void Enemy::behaviorSquare(float delta_time) {
|
||||
tracking_timer_ += delta_time;
|
||||
|
||||
if (tracking_timer_ >= Defaults::Enemies::Cuadrado::TRACKING_INTERVAL && ship_position_ != nullptr) {
|
||||
if (tracking_timer_ >= Defaults::Enemies::Square::TRACKING_INTERVAL && ship_position_ != nullptr) {
|
||||
tracking_timer_ = 0.0F;
|
||||
|
||||
const Vec2 TO_SHIP = *ship_position_ - center_;
|
||||
@@ -348,89 +348,89 @@ void Enemy::behaviorQuadrat(float delta_time) {
|
||||
}
|
||||
}
|
||||
|
||||
// MOLINILLO: movimiento recto + boost de rotación visual cerca del ship.
|
||||
// PINWHEEL: movimiento recto + boost de rotación visual cerca del ship.
|
||||
// Sin tracking — solo cambios de dirección raros (igual que Pentagon pero
|
||||
// con probabilidad mucho menor).
|
||||
void Enemy::behaviorMolinillo(float /*delta_time*/) {
|
||||
void Enemy::behaviorPinwheel(float /*delta_time*/) {
|
||||
// Boost de rotación visual por proximidad al ship
|
||||
if (ship_position_ != nullptr) {
|
||||
const Vec2 TO_SHIP = *ship_position_ - center_;
|
||||
const float DIST = TO_SHIP.length();
|
||||
if (DIST < Defaults::Enemies::Molinillo::PROXIMITY_DISTANCE) {
|
||||
drotacio_ = animacio_.drotacio_base * Defaults::Enemies::Molinillo::DROTACIO_PROXIMITY_MULTIPLIER;
|
||||
if (DIST < Defaults::Enemies::Pinwheel::PROXIMITY_DISTANCE) {
|
||||
rotation_delta_ = animation_.rotation_delta_base * Defaults::Enemies::Pinwheel::ROTATION_DELTA_PROXIMITY_MULTIPLIER;
|
||||
} else {
|
||||
drotacio_ = animacio_.drotacio_base;
|
||||
rotation_delta_ = animation_.rotation_delta_base;
|
||||
}
|
||||
}
|
||||
// Movimiento lineal puro: el world se encarga de integrar y rebotar.
|
||||
}
|
||||
|
||||
void Enemy::updateAnimation(float delta_time) {
|
||||
updatePalpitation(delta_time);
|
||||
updatePulse(delta_time);
|
||||
updateRotationAcceleration(delta_time);
|
||||
}
|
||||
|
||||
void Enemy::updatePalpitation(float delta_time) {
|
||||
if (animacio_.palpitacio_activa) {
|
||||
animacio_.palpitacio_fase += 2.0F * Constants::PI * animacio_.palpitacio_frequencia * delta_time;
|
||||
animacio_.palpitacio_temps_restant -= delta_time;
|
||||
if (animacio_.palpitacio_temps_restant <= 0.0F) {
|
||||
animacio_.palpitacio_activa = false;
|
||||
void Enemy::updatePulse(float delta_time) {
|
||||
if (animation_.pulse_active) {
|
||||
animation_.pulse_phase += 2.0F * Constants::PI * animation_.pulse_frequency * delta_time;
|
||||
animation_.pulse_time_remaining -= delta_time;
|
||||
if (animation_.pulse_time_remaining <= 0.0F) {
|
||||
animation_.pulse_active = false;
|
||||
}
|
||||
} else {
|
||||
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
||||
const float TRIGGER_PROB = Defaults::Enemies::Animation::PALPITACIO_TRIGGER_PROB * delta_time;
|
||||
const float TRIGGER_PROB = Defaults::Enemies::Animation::PULSE_TRIGGER_PROB * delta_time;
|
||||
if (RAND_VAL < TRIGGER_PROB) {
|
||||
animacio_.palpitacio_activa = true;
|
||||
animacio_.palpitacio_fase = 0.0F;
|
||||
animation_.pulse_active = true;
|
||||
animation_.pulse_phase = 0.0F;
|
||||
|
||||
const float FREQ_RANGE = Defaults::Enemies::Animation::PALPITACIO_FREQ_MAX -
|
||||
Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN;
|
||||
animacio_.palpitacio_frequencia = Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN +
|
||||
const float FREQ_RANGE = Defaults::Enemies::Animation::PULSE_FREQ_MAX -
|
||||
Defaults::Enemies::Animation::PULSE_FREQ_MIN;
|
||||
animation_.pulse_frequency = Defaults::Enemies::Animation::PULSE_FREQ_MIN +
|
||||
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * FREQ_RANGE);
|
||||
|
||||
const float AMP_RANGE = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MAX -
|
||||
Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN;
|
||||
animacio_.palpitacio_amplitud = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN +
|
||||
const float AMP_RANGE = Defaults::Enemies::Animation::PULSE_AMPLITUD_MAX -
|
||||
Defaults::Enemies::Animation::PULSE_AMPLITUD_MIN;
|
||||
animation_.pulse_amplitude = Defaults::Enemies::Animation::PULSE_AMPLITUD_MIN +
|
||||
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * AMP_RANGE);
|
||||
|
||||
const float DUR_RANGE = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MAX -
|
||||
Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN;
|
||||
animacio_.palpitacio_temps_restant = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN +
|
||||
const float DUR_RANGE = Defaults::Enemies::Animation::PULSE_DURATION_MAX -
|
||||
Defaults::Enemies::Animation::PULSE_DURATION_MIN;
|
||||
animation_.pulse_time_remaining = Defaults::Enemies::Animation::PULSE_DURATION_MIN +
|
||||
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DUR_RANGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Enemy::updateRotationAcceleration(float delta_time) {
|
||||
if (animacio_.drotacio_t < 1.0F) {
|
||||
animacio_.drotacio_t += delta_time / animacio_.drotacio_duracio;
|
||||
if (animacio_.drotacio_t >= 1.0F) {
|
||||
animacio_.drotacio_t = 1.0F;
|
||||
animacio_.drotacio_base = animacio_.drotacio_objetivo;
|
||||
drotacio_ = animacio_.drotacio_base;
|
||||
if (animation_.rotation_delta_t < 1.0F) {
|
||||
animation_.rotation_delta_t += delta_time / animation_.rotation_delta_duration;
|
||||
if (animation_.rotation_delta_t >= 1.0F) {
|
||||
animation_.rotation_delta_t = 1.0F;
|
||||
animation_.rotation_delta_base = animation_.rotation_delta_target;
|
||||
rotation_delta_ = animation_.rotation_delta_base;
|
||||
} else {
|
||||
const float T = animacio_.drotacio_t;
|
||||
const float T = animation_.rotation_delta_t;
|
||||
const float SMOOTH_T = T * T * (3.0F - (2.0F * T));
|
||||
const float INITIAL = animacio_.drotacio_base;
|
||||
const float TARGET = animacio_.drotacio_objetivo;
|
||||
drotacio_ = INITIAL + ((TARGET - INITIAL) * SMOOTH_T);
|
||||
const float INITIAL = animation_.rotation_delta_base;
|
||||
const float TARGET = animation_.rotation_delta_target;
|
||||
rotation_delta_ = INITIAL + ((TARGET - INITIAL) * SMOOTH_T);
|
||||
}
|
||||
} else {
|
||||
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
||||
const float TRIGGER_PROB = Defaults::Enemies::Animation::ROTACIO_ACCEL_TRIGGER_PROB * delta_time;
|
||||
const float TRIGGER_PROB = Defaults::Enemies::Animation::ROTATION_ACCEL_TRIGGER_PROB * delta_time;
|
||||
if (RAND_VAL < TRIGGER_PROB) {
|
||||
animacio_.drotacio_t = 0.0F;
|
||||
animation_.rotation_delta_t = 0.0F;
|
||||
|
||||
const float MULT_RANGE = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MAX -
|
||||
Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN;
|
||||
const float MULTIPLIER = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN +
|
||||
const float MULT_RANGE = Defaults::Enemies::Animation::ROTATION_ACCEL_MULTIPLIER_MAX -
|
||||
Defaults::Enemies::Animation::ROTATION_ACCEL_MULTIPLIER_MIN;
|
||||
const float MULTIPLIER = Defaults::Enemies::Animation::ROTATION_ACCEL_MULTIPLIER_MIN +
|
||||
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * MULT_RANGE);
|
||||
animacio_.drotacio_objetivo = animacio_.drotacio_base * MULTIPLIER;
|
||||
animation_.rotation_delta_target = animation_.rotation_delta_base * MULTIPLIER;
|
||||
|
||||
const float DUR_RANGE = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MAX -
|
||||
Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN;
|
||||
animacio_.drotacio_duracio = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN +
|
||||
const float DUR_RANGE = Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MAX -
|
||||
Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MIN;
|
||||
animation_.rotation_delta_duration = Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MIN +
|
||||
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DUR_RANGE);
|
||||
}
|
||||
}
|
||||
@@ -438,15 +438,15 @@ void Enemy::updateRotationAcceleration(float delta_time) {
|
||||
|
||||
auto Enemy::computeCurrentScale() const -> float {
|
||||
float scale = 1.0F;
|
||||
if (timer_invulnerabilitat_ > 0.0F) {
|
||||
const float T_INV = timer_invulnerabilitat_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
|
||||
if (invulnerability_timer_ > 0.0F) {
|
||||
const float T_INV = invulnerability_timer_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
|
||||
const float T = 1.0F - T_INV;
|
||||
const float SMOOTH_T = T * T * (3.0F - (2.0F * T));
|
||||
constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_START;
|
||||
constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_END;
|
||||
scale = START + ((END - START) * SMOOTH_T);
|
||||
} else if (animacio_.palpitacio_activa) {
|
||||
scale += animacio_.palpitacio_amplitud * std::sin(animacio_.palpitacio_fase);
|
||||
} else if (animation_.pulse_active) {
|
||||
scale += animation_.pulse_amplitude * std::sin(animation_.pulse_phase);
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
@@ -454,22 +454,22 @@ auto Enemy::computeCurrentScale() const -> float {
|
||||
auto Enemy::getBaseVelocity() const -> float {
|
||||
switch (type_) {
|
||||
case EnemyType::PENTAGON:
|
||||
return Defaults::Enemies::Pentagon::VELOCITAT;
|
||||
case EnemyType::QUADRAT:
|
||||
return Defaults::Enemies::Cuadrado::VELOCITAT;
|
||||
case EnemyType::MOLINILLO:
|
||||
return Defaults::Enemies::Molinillo::VELOCITAT;
|
||||
return Defaults::Enemies::Pentagon::SPEED;
|
||||
case EnemyType::SQUARE:
|
||||
return Defaults::Enemies::Square::SPEED;
|
||||
case EnemyType::PINWHEEL:
|
||||
return Defaults::Enemies::Pinwheel::SPEED;
|
||||
default:
|
||||
return Defaults::Enemies::Pentagon::VELOCITAT;
|
||||
return Defaults::Enemies::Pentagon::SPEED;
|
||||
}
|
||||
}
|
||||
|
||||
auto Enemy::getBaseRotation() const -> float {
|
||||
return animacio_.drotacio_base != 0.0F ? animacio_.drotacio_base : drotacio_;
|
||||
return animation_.rotation_delta_base != 0.0F ? animation_.rotation_delta_base : rotation_delta_;
|
||||
}
|
||||
|
||||
void Enemy::setTrackingStrength(float strength) {
|
||||
if (type_ == EnemyType::QUADRAT) {
|
||||
if (type_ == EnemyType::SQUARE) {
|
||||
tracking_strength_ = strength;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,24 +13,24 @@
|
||||
// Tipo de enemy
|
||||
enum class EnemyType : uint8_t {
|
||||
PENTAGON = 0, // Pentágono esquivador (zigzag)
|
||||
QUADRAT = 1, // Cuadrado perseguidor (tracks ship)
|
||||
MOLINILLO = 2 // Molinillo agresivo (rápido, girando)
|
||||
SQUARE = 1, // Square perseguidor (tracks ship)
|
||||
PINWHEEL = 2 // Molinillo agresivo (rápido, girando)
|
||||
};
|
||||
|
||||
// Estado de animación (palpitación + rotación acelerada)
|
||||
struct EnemyAnimation {
|
||||
// Palpitación (efecto respiración)
|
||||
bool palpitacio_activa = false;
|
||||
float palpitacio_fase = 0.0F;
|
||||
float palpitacio_frequencia = 2.0F;
|
||||
float palpitacio_amplitud = 0.15F;
|
||||
float palpitacio_temps_restant = 0.0F;
|
||||
bool pulse_active = false;
|
||||
float pulse_phase = 0.0F;
|
||||
float pulse_frequency = 2.0F;
|
||||
float pulse_amplitude = 0.15F;
|
||||
float pulse_time_remaining = 0.0F;
|
||||
|
||||
// Aceleración de rotación visual (modulación a largo plazo)
|
||||
float drotacio_base = 0.0F;
|
||||
float drotacio_objetivo = 0.0F;
|
||||
float drotacio_t = 0.0F;
|
||||
float drotacio_duracio = 0.0F;
|
||||
float rotation_delta_base = 0.0F;
|
||||
float rotation_delta_target = 0.0F;
|
||||
float rotation_delta_t = 0.0F;
|
||||
float rotation_delta_duration = 0.0F;
|
||||
};
|
||||
|
||||
class Enemy : public Entities::Entity {
|
||||
@@ -46,7 +46,7 @@ class Enemy : public Entities::Entity {
|
||||
void draw() const override;
|
||||
|
||||
// Override: Interfaz de Entity
|
||||
[[nodiscard]] auto isActive() const -> bool override { return esta_; }
|
||||
[[nodiscard]] auto isActive() const -> bool override { return is_active_; }
|
||||
|
||||
// Override: Interfaz de colisión
|
||||
[[nodiscard]] auto getCollisionRadius() const -> float override {
|
||||
@@ -56,14 +56,14 @@ class Enemy : public Entities::Entity {
|
||||
// poden abatre i el cos físic rebota amb la nau. El damage a la nau
|
||||
// segueix filtrat per `isInvulnerable()` al detectShipEnemy.
|
||||
[[nodiscard]] auto isCollidable() const -> bool override {
|
||||
return esta_;
|
||||
return is_active_;
|
||||
}
|
||||
|
||||
// Marcar destruido (desactiva el cuerpo físicamente: radius=0)
|
||||
void destruir();
|
||||
void destroy();
|
||||
|
||||
// Getters
|
||||
[[nodiscard]] auto getRotationDelta() const -> float { return drotacio_; }
|
||||
[[nodiscard]] auto getRotationDelta() const -> float { return rotation_delta_; }
|
||||
[[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; }
|
||||
|
||||
// Set ship position reference for tracking behavior
|
||||
@@ -79,18 +79,18 @@ class Enemy : public Entities::Entity {
|
||||
// actual del body_.velocity.
|
||||
void setVelocity(float speed);
|
||||
void setRotation(float rot) {
|
||||
drotacio_ = rot;
|
||||
animacio_.drotacio_base = rot;
|
||||
rotation_delta_ = rot;
|
||||
animation_.rotation_delta_base = rot;
|
||||
}
|
||||
void setTrackingStrength(float strength);
|
||||
|
||||
// Invulnerabilidad
|
||||
[[nodiscard]] auto isInvulnerable() const -> bool { return timer_invulnerabilitat_ > 0.0F; }
|
||||
[[nodiscard]] auto getInvulnerabilityTime() const -> float { return timer_invulnerabilitat_; }
|
||||
[[nodiscard]] auto isInvulnerable() const -> bool { return invulnerability_timer_ > 0.0F; }
|
||||
[[nodiscard]] auto getInvulnerabilityTime() const -> float { return invulnerability_timer_; }
|
||||
|
||||
// Estado "herido": entre primer impacto de bala y explosión diferida.
|
||||
// shooter_id: id del jugador que herí; 0xFF = sin atribución (cadena, etc.).
|
||||
void herir(uint8_t shooter_id = 0xFF);
|
||||
void hurt(uint8_t shooter_id = 0xFF);
|
||||
[[nodiscard]] auto isWounded() const -> bool { return wounded_timer_ > 0.0F; }
|
||||
[[nodiscard]] auto getWoundedTimer() const -> float { return wounded_timer_; }
|
||||
[[nodiscard]] auto woundExpiredThisFrame() const -> bool { return wound_expired_this_frame_; }
|
||||
@@ -104,12 +104,12 @@ class Enemy : public Entities::Entity {
|
||||
// Miembros específicos (heredados: renderer_, shape_, center_, angle_, brightness_, body_).
|
||||
// Inicializados en la declaración: el ctor por defecto deja al enemy en estado "inactivo
|
||||
// como pentágono", coherente con lo que harán init() o el ctor con renderer al activarlo.
|
||||
float drotacio_{0.0F}; // Velocidad angular visual (rad/s) — solo decoración, separada de body_.angular_velocity
|
||||
float rotacio_{0.0F}; // Rotación visual acumulada (no afecta movimiento)
|
||||
bool esta_{false};
|
||||
float rotation_delta_{0.0F}; // Velocidad angular visual (rad/s) — solo decoración, separada de body_.angular_velocity
|
||||
float rotation_{0.0F}; // Rotación visual acumulada (no afecta movimiento)
|
||||
bool is_active_{false};
|
||||
|
||||
EnemyType type_{EnemyType::PENTAGON};
|
||||
EnemyAnimation animacio_;
|
||||
EnemyAnimation animation_;
|
||||
|
||||
// Comportamiento type-specific
|
||||
float tracking_timer_{0.0F}; // Quadrat: tiempo desde último update de dirección
|
||||
@@ -118,7 +118,7 @@ class Enemy : public Entities::Entity {
|
||||
float direction_change_timer_{0.0F}; // Pentagon: tiempo para próximo cambio de dirección
|
||||
|
||||
// Invulnerabilidad post-spawn
|
||||
float timer_invulnerabilitat_{0.0F};
|
||||
float invulnerability_timer_{0.0F};
|
||||
|
||||
// Estado "herido": timer cuenta atrás; al cruzar 0 se marca expiración.
|
||||
float wounded_timer_{0.0F};
|
||||
@@ -127,11 +127,11 @@ class Enemy : public Entities::Entity {
|
||||
|
||||
// Métodos privados
|
||||
void updateAnimation(float delta_time);
|
||||
void updatePalpitation(float delta_time);
|
||||
void updatePulse(float delta_time);
|
||||
void updateRotationAcceleration(float delta_time);
|
||||
void behaviorPentagon(float delta_time);
|
||||
void behaviorQuadrat(float delta_time);
|
||||
void behaviorMolinillo(float delta_time);
|
||||
void behaviorSquare(float delta_time);
|
||||
void behaviorPinwheel(float delta_time);
|
||||
[[nodiscard]] auto computeCurrentScale() const -> float;
|
||||
// Estático: solo opera sobre ship_pos pasado; no consulta estado del enemy.
|
||||
static auto attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool;
|
||||
|
||||
@@ -44,10 +44,10 @@ void Ship::init(const Vec2* spawn_point, bool activar_invulnerabilitat) {
|
||||
if (spawn_point != nullptr) {
|
||||
center_ = *spawn_point;
|
||||
} else {
|
||||
float centre_x;
|
||||
float centre_y;
|
||||
Constants::getPlayAreaCenter(centre_x, centre_y);
|
||||
center_ = {.x = centre_x, .y = centre_y};
|
||||
float center_x;
|
||||
float center_y;
|
||||
Constants::getPlayAreaCenter(center_x, center_y);
|
||||
center_ = {.x = center_x, .y = center_y};
|
||||
}
|
||||
|
||||
// Reset orientación
|
||||
@@ -180,7 +180,7 @@ void Ship::draw() const {
|
||||
Rendering::renderShape(renderer_, shape_, center_, angle_, SCALE, 1.0F, brightness_, color);
|
||||
}
|
||||
|
||||
void Ship::herir() {
|
||||
void Ship::hurt() {
|
||||
hurt_timer_ = Defaults::Ship::Hurt::DURATION;
|
||||
Audio::get()->playSound(Defaults::Sound::HURT, Audio::Group::GAME);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class Ship : public Entities::Entity {
|
||||
}
|
||||
|
||||
// Estat "ferit": primera col·lisió amb enemic dispara HURT; segona durant HURT mata.
|
||||
void herir();
|
||||
void hurt();
|
||||
[[nodiscard]] auto isHurt() const -> bool { return hurt_timer_ > 0.0F; }
|
||||
[[nodiscard]] auto getHurtTimer() const -> float { return hurt_timer_; }
|
||||
|
||||
|
||||
@@ -38,9 +38,9 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
||||
|
||||
// Debug output de la configuración
|
||||
std::cout << "[GameScene] Configuración de match - P1: "
|
||||
<< (match_config_.jugador1_actiu ? "ACTIU" : "INACTIU")
|
||||
<< (match_config_.player1_active ? "ACTIU" : "INACTIU")
|
||||
<< ", P2: "
|
||||
<< (match_config_.jugador2_actiu ? "ACTIU" : "INACTIU")
|
||||
<< (match_config_.player2_active ? "ACTIU" : "INACTIU")
|
||||
<< '\n';
|
||||
|
||||
// Consumir opciones (preparació per MODE_DEMO futur)
|
||||
@@ -61,7 +61,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
||||
// 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.
|
||||
// Configurar el mundo físico con los límites de la zone de juego.
|
||||
physics_world_.clear();
|
||||
physics_world_.setBounds(Defaults::Zones::PLAYAREA);
|
||||
|
||||
@@ -77,17 +77,17 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
||||
});
|
||||
|
||||
// Fireworks generen una ripple gran al playfield (ona d'aigua centrada al burst).
|
||||
firework_manager_.setSpawnCallback([this](Vec2 origen) {
|
||||
playfield_.notifyExplosion(origen);
|
||||
firework_manager_.setSpawnCallback([this](Vec2 origin) {
|
||||
playfield_.notifyExplosion(origin);
|
||||
});
|
||||
|
||||
// Explosions properes a una paret també generen bump (falloff lineal amb la distància).
|
||||
debris_manager_.setExplosionCallback([this](Vec2 center) {
|
||||
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
|
||||
const float DIST_LEFT = std::abs(center.x - zona.x);
|
||||
const float DIST_RIGHT = std::abs((zona.x + zona.w) - center.x);
|
||||
const float DIST_TOP = std::abs(center.y - zona.y);
|
||||
const float DIST_BOTTOM = std::abs((zona.y + zona.h) - center.y);
|
||||
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||||
const float DIST_LEFT = std::abs(center.x - zone.x);
|
||||
const float DIST_RIGHT = std::abs((zone.x + zone.w) - center.x);
|
||||
const float DIST_TOP = std::abs(center.y - zone.y);
|
||||
const float DIST_BOTTOM = std::abs((zone.y + zone.h) - center.y);
|
||||
const float MIN_DIST = std::min({DIST_LEFT, DIST_RIGHT, DIST_TOP, DIST_BOTTOM});
|
||||
if (MIN_DIST > Defaults::Border::EXPLOSION_FALLOFF_PX) {
|
||||
return;
|
||||
@@ -130,9 +130,9 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
||||
|
||||
// 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;
|
||||
bool player_active = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
|
||||
|
||||
if (jugador_actiu) {
|
||||
if (player_active) {
|
||||
// Jugador active: init normalment
|
||||
Vec2 spawn_pos = getSpawnPoint(i);
|
||||
ships_[i].init(&spawn_pos, false); // No invulnerability at start
|
||||
@@ -248,11 +248,11 @@ void GameScene::stepPhysics(float delta_time) {
|
||||
|
||||
void GameScene::stepShootingInput() {
|
||||
auto* input = Input::get();
|
||||
if (match_config_.jugador1_actiu &&
|
||||
if (match_config_.player1_active &&
|
||||
input->checkActionPlayer1(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
fireBullet(0);
|
||||
}
|
||||
if (match_config_.jugador2_actiu &&
|
||||
if (match_config_.player2_active &&
|
||||
input->checkActionPlayer2(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
fireBullet(1);
|
||||
}
|
||||
@@ -267,16 +267,16 @@ void GameScene::stepMidGameJoin() {
|
||||
// Solo se permite join si hay al menos un jugador vivo (no se puede
|
||||
// hacer join en pantalla vacía).
|
||||
const bool ALGU_VIU =
|
||||
(match_config_.jugador1_actiu && hit_timer_per_player_[0] != Defaults::Game::HIT_TIMER_INACTIVE_PLAYER) ||
|
||||
(match_config_.jugador2_actiu && hit_timer_per_player_[1] != Defaults::Game::HIT_TIMER_INACTIVE_PLAYER);
|
||||
(match_config_.player1_active && hit_timer_per_player_[0] != Defaults::Game::HIT_TIMER_INACTIVE_PLAYER) ||
|
||||
(match_config_.player2_active && hit_timer_per_player_[1] != Defaults::Game::HIT_TIMER_INACTIVE_PLAYER);
|
||||
if (!ALGU_VIU) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
const bool ACTIU = (pid == 0) ? match_config_.player1_active
|
||||
: match_config_.player2_active;
|
||||
const bool MUERTO_SIN_VIDAS = hit_timer_per_player_[pid] == Defaults::Game::HIT_TIMER_INACTIVE_PLAYER;
|
||||
if (ACTIU && !MUERTO_SIN_VIDAS) {
|
||||
continue; // jugador ya está jugando
|
||||
@@ -375,8 +375,8 @@ void GameScene::stepDeathSequence(float delta_time) {
|
||||
|
||||
// Sin vidas: marcar definitivamente muerto y comprobar transición a CONTINUE.
|
||||
hit_timer_per_player_[i] = Defaults::Game::HIT_TIMER_INACTIVE_PLAYER;
|
||||
const bool P1_DEAD = !match_config_.jugador1_actiu || lives_per_player_[0] <= 0;
|
||||
const bool P2_DEAD = !match_config_.jugador2_actiu || lives_per_player_[1] <= 0;
|
||||
const bool P1_DEAD = !match_config_.player1_active || lives_per_player_[0] <= 0;
|
||||
const bool P2_DEAD = !match_config_.player2_active || lives_per_player_[1] <= 0;
|
||||
if (P1_DEAD && P2_DEAD) {
|
||||
game_over_state_ = GameOverState::CONTINUE;
|
||||
continue_counter_ = Defaults::Game::CONTINUE_COUNT_START;
|
||||
@@ -440,10 +440,10 @@ void GameScene::runStageInitHud(float delta_time) {
|
||||
Defaults::Game::INIT_HUD_SHIP2_RATIO_INIT,
|
||||
Defaults::Game::INIT_HUD_SHIP2_RATIO_END);
|
||||
|
||||
if (match_config_.jugador1_actiu && SHIP1_P < 1.0F) {
|
||||
if (match_config_.player1_active && SHIP1_P < 1.0F) {
|
||||
ships_[0].setCenter(Systems::InitHud::computeShipPosition(SHIP1_P, getSpawnPoint(0)));
|
||||
}
|
||||
if (match_config_.jugador2_actiu && SHIP2_P < 1.0F) {
|
||||
if (match_config_.player2_active && SHIP2_P < 1.0F) {
|
||||
ships_[1].setCenter(Systems::InitHud::computeShipPosition(SHIP2_P, getSpawnPoint(1)));
|
||||
}
|
||||
}
|
||||
@@ -453,7 +453,7 @@ void GameScene::runStageLevelStart(float delta_time) {
|
||||
|
||||
// Ambas naves pueden moverse y disparar durante el intro.
|
||||
for (uint8_t i = 0; i < 2; i++) {
|
||||
const bool ACTIU = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu;
|
||||
const bool ACTIU = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
|
||||
if (ACTIU && hit_timer_per_player_[i] == 0.0F) {
|
||||
ships_[i].processInput(delta_time, i);
|
||||
ships_[i].update(delta_time);
|
||||
@@ -481,7 +481,7 @@ void GameScene::runStagePlaying(float delta_time) {
|
||||
|
||||
// Gameplay normal: ships activos + entidades + colisiones + efectos.
|
||||
for (uint8_t i = 0; i < 2; i++) {
|
||||
const bool ACTIU = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu;
|
||||
const bool ACTIU = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
|
||||
if (ACTIU && hit_timer_per_player_[i] == 0.0F) {
|
||||
ships_[i].processInput(delta_time, i);
|
||||
ships_[i].update(delta_time);
|
||||
@@ -491,7 +491,7 @@ void GameScene::runStagePlaying(float delta_time) {
|
||||
enemy.update(delta_time);
|
||||
}
|
||||
|
||||
// Col·lisions primer, després desactivació per fora-de-zona: així una bala que
|
||||
// Col·lisions primer, després desactivació per fora-de-zone: així una bala que
|
||||
// el mateix frame xoca amb un enemic i alhora surt del PLAYAREA es compta com a
|
||||
// impacte abans no se la trenqui per sortir.
|
||||
runCollisionDetections();
|
||||
@@ -507,7 +507,7 @@ void GameScene::runStagePlaying(float delta_time) {
|
||||
void GameScene::runStageLevelCompleted(float delta_time) {
|
||||
stage_manager_->update(delta_time);
|
||||
for (uint8_t i = 0; i < 2; i++) {
|
||||
const bool ACTIU = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu;
|
||||
const bool ACTIU = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
|
||||
if (ACTIU && hit_timer_per_player_[i] == 0.0F) {
|
||||
ships_[i].processInput(delta_time, i);
|
||||
ships_[i].update(delta_time);
|
||||
@@ -580,8 +580,8 @@ void GameScene::drawBullets() const {
|
||||
|
||||
void GameScene::drawActiveShipsAlive() const {
|
||||
for (uint8_t i = 0; i < 2; i++) {
|
||||
bool jugador_actiu = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu;
|
||||
if (jugador_actiu && hit_timer_per_player_[i] == 0.0F) {
|
||||
bool player_active = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
|
||||
if (player_active && hit_timer_per_player_[i] == 0.0F) {
|
||||
ships_[i].draw();
|
||||
}
|
||||
}
|
||||
@@ -613,10 +613,10 @@ void GameScene::drawGameOverState() {
|
||||
constexpr float SPACING = Defaults::Game::GameOverScreen::TEXT_SPACING;
|
||||
|
||||
const SDL_FRect& play_area = Defaults::Zones::PLAYAREA;
|
||||
float centre_x = play_area.x + (play_area.w / 2.0F);
|
||||
float centre_y = play_area.y + (play_area.h / 2.0F);
|
||||
float center_x = play_area.x + (play_area.w / 2.0F);
|
||||
float center_y = play_area.y + (play_area.h / 2.0F);
|
||||
|
||||
text_.renderCentered(GAME_OVER_TEXT, {.x = centre_x, .y = centre_y}, SCALE, SPACING);
|
||||
text_.renderCentered(GAME_OVER_TEXT, {.x = center_x, .y = center_y}, SCALE, SPACING);
|
||||
|
||||
drawScoreboard();
|
||||
}
|
||||
@@ -663,11 +663,11 @@ void GameScene::drawInitHudState() {
|
||||
Systems::InitHud::drawScoreboardAnimated(text_, buildScoreboard(), score_progress);
|
||||
}
|
||||
|
||||
if (ship1_progress > 0.0F && match_config_.jugador1_actiu && ships_[0].isActive()) {
|
||||
if (ship1_progress > 0.0F && match_config_.player1_active && ships_[0].isActive()) {
|
||||
ships_[0].draw();
|
||||
}
|
||||
|
||||
if (ship2_progress > 0.0F && match_config_.jugador2_actiu && ships_[1].isActive()) {
|
||||
if (ship2_progress > 0.0F && match_config_.player2_active && ships_[1].isActive()) {
|
||||
ships_[1].draw();
|
||||
}
|
||||
}
|
||||
@@ -743,7 +743,7 @@ void GameScene::tocado(uint8_t player_id) {
|
||||
SHIP_POS,
|
||||
SHIP_ANGLE,
|
||||
1.0F,
|
||||
Defaults::Physics::Debris::VELOCITAT_BASE,
|
||||
Defaults::Physics::Debris::SPEED_BASE,
|
||||
SHIP_BRIGHT,
|
||||
INHERITED_VEL,
|
||||
0.0F, // sense herència angular
|
||||
@@ -769,20 +769,20 @@ void GameScene::drawScoreboard() {
|
||||
const float SCALE = Defaults::Hud::SCOREBOARD_TEXT_SCALE;
|
||||
const float SPACING = Defaults::Hud::SCOREBOARD_TEXT_SPACING;
|
||||
|
||||
// Calcular centro de la zona del marcador
|
||||
// Calcular centro de la zone del marcador
|
||||
const SDL_FRect& scoreboard_zone = Defaults::Zones::SCOREBOARD;
|
||||
float centre_x = scoreboard_zone.w / 2.0F;
|
||||
float centre_y = scoreboard_zone.y + (scoreboard_zone.h / 2.0F);
|
||||
float center_x = scoreboard_zone.w / 2.0F;
|
||||
float center_y = scoreboard_zone.y + (scoreboard_zone.h / 2.0F);
|
||||
|
||||
// Renderizar centrat
|
||||
text_.renderCentered(text, {.x = centre_x, .y = centre_y}, SCALE, SPACING);
|
||||
text_.renderCentered(text, {.x = center_x, .y = center_y}, SCALE, SPACING);
|
||||
}
|
||||
|
||||
auto GameScene::buildScoreboard() const -> std::string {
|
||||
// Puntuación P1 (6 dígits) - mostrar zeros si inactiu
|
||||
std::string score_p1;
|
||||
std::string vides_p1;
|
||||
if (match_config_.jugador1_actiu) {
|
||||
if (match_config_.player1_active) {
|
||||
score_p1 = std::to_string(score_per_player_[0]);
|
||||
score_p1 = std::string(6 - std::min(6, static_cast<int>(score_p1.length())), '0') + score_p1;
|
||||
vides_p1 = (lives_per_player_[0] < 10)
|
||||
@@ -801,7 +801,7 @@ auto GameScene::buildScoreboard() const -> std::string {
|
||||
// Puntuación P2 (6 dígits) - mostrar zeros si inactiu
|
||||
std::string score_p2;
|
||||
std::string vides_p2;
|
||||
if (match_config_.jugador2_actiu) {
|
||||
if (match_config_.player2_active) {
|
||||
score_p2 = std::to_string(score_per_player_[1]);
|
||||
score_p2 = std::string(6 - std::min(6, static_cast<int>(score_p2.length())), '0') + score_p2;
|
||||
vides_p2 = (lives_per_player_[1] < 10)
|
||||
@@ -890,7 +890,7 @@ void GameScene::drawStageMessage(const std::string& message) {
|
||||
// ========================================
|
||||
|
||||
auto GameScene::getSpawnPoint(uint8_t player_id) const -> Vec2 {
|
||||
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
|
||||
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||||
|
||||
float x_ratio;
|
||||
if (match_config_.isSinglePlayer()) {
|
||||
@@ -904,8 +904,8 @@ auto GameScene::getSpawnPoint(uint8_t player_id) const -> Vec2 {
|
||||
}
|
||||
|
||||
return {
|
||||
.x = zona.x + (zona.w * x_ratio),
|
||||
.y = zona.y + (zona.h * Defaults::Game::SPAWN_Y_RATIO)};
|
||||
.x = zone.x + (zone.w * x_ratio),
|
||||
.y = zone.y + (zone.h * Defaults::Game::SPAWN_Y_RATIO)};
|
||||
}
|
||||
|
||||
void GameScene::fireBullet(uint8_t player_id) {
|
||||
@@ -927,15 +927,15 @@ void GameScene::fireBullet(uint8_t player_id) {
|
||||
float sin_a = std::sin(ship_angle);
|
||||
float tip_x = (LOCAL_TIP_X * cos_a) - (LOCAL_TIP_Y * sin_a) + ship_centre.x;
|
||||
float tip_y = (LOCAL_TIP_X * sin_a) + (LOCAL_TIP_Y * cos_a) + ship_centre.y;
|
||||
Vec2 posicio_dispar = {.x = tip_x, .y = tip_y};
|
||||
Vec2 fire_position = {.x = tip_x, .y = tip_y};
|
||||
|
||||
// Buscar primera bullet inactiva en el pool del player.
|
||||
// El pool global té MAX_BALES slots per jugador (P1=[0..MAX-1], P2=[MAX..2*MAX-1]).
|
||||
constexpr int SLOTS_PER_PLAYER = Defaults::Entities::MAX_BALES;
|
||||
// El pool global té MAX_BULLETS slots per jugador (P1=[0..MAX-1], P2=[MAX..2*MAX-1]).
|
||||
constexpr int SLOTS_PER_PLAYER = Defaults::Entities::MAX_BULLETS;
|
||||
const int START_IDX = player_id * SLOTS_PER_PLAYER;
|
||||
for (int i = START_IDX; i < START_IDX + SLOTS_PER_PLAYER; i++) {
|
||||
if (!bullets_[i].isActive()) {
|
||||
bullets_[i].disparar(posicio_dispar, ship_angle, player_id);
|
||||
bullets_[i].fire(fire_position, ship_angle, player_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -950,10 +950,10 @@ void GameScene::drawContinue() {
|
||||
float escala_continue = Defaults::Game::ContinueScreen::CONTINUE_TEXT_SCALE;
|
||||
float y_ratio_continue = Defaults::Game::ContinueScreen::CONTINUE_TEXT_Y_RATIO;
|
||||
|
||||
float centre_x = play_area.x + (play_area.w / 2.0F);
|
||||
float center_x = play_area.x + (play_area.w / 2.0F);
|
||||
float centre_y_continue = play_area.y + (play_area.h * y_ratio_continue);
|
||||
|
||||
text_.renderCentered(CONTINUE_TEXT, {.x = centre_x, .y = centre_y_continue}, escala_continue, SPACING);
|
||||
text_.renderCentered(CONTINUE_TEXT, {.x = center_x, .y = centre_y_continue}, escala_continue, SPACING);
|
||||
|
||||
// Countdown number (using constants)
|
||||
const std::string COUNTER_STR = std::to_string(continue_counter_);
|
||||
@@ -962,7 +962,7 @@ void GameScene::drawContinue() {
|
||||
|
||||
float centre_y_counter = play_area.y + (play_area.h * y_ratio_counter);
|
||||
|
||||
text_.renderCentered(COUNTER_STR, {.x = centre_x, .y = centre_y_counter}, escala_counter, SPACING);
|
||||
text_.renderCentered(COUNTER_STR, {.x = center_x, .y = centre_y_counter}, escala_counter, SPACING);
|
||||
|
||||
// "CONTINUES LEFT" (conditional + using constants)
|
||||
if (!Defaults::Game::INFINITE_CONTINUES) {
|
||||
@@ -972,16 +972,16 @@ void GameScene::drawContinue() {
|
||||
|
||||
float centre_y_info = play_area.y + (play_area.h * y_ratio_info);
|
||||
|
||||
text_.renderCentered(CONTINUES_TEXT, {.x = centre_x, .y = centre_y_info}, escala_info, SPACING);
|
||||
text_.renderCentered(CONTINUES_TEXT, {.x = center_x, .y = centre_y_info}, escala_info, SPACING);
|
||||
}
|
||||
}
|
||||
|
||||
void GameScene::joinPlayer(uint8_t player_id) {
|
||||
// Activate player
|
||||
if (player_id == 0) {
|
||||
match_config_.jugador1_actiu = true;
|
||||
match_config_.player1_active = true;
|
||||
} else {
|
||||
match_config_.jugador2_actiu = true;
|
||||
match_config_.player2_active = true;
|
||||
}
|
||||
|
||||
// Reset stats
|
||||
|
||||
@@ -67,7 +67,7 @@ class GameScene final : public Scene {
|
||||
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<Bullet, static_cast<std::size_t>(Constants::MAX_BULLETS) * 2> bullets_;
|
||||
std::array<float, 2> hit_timer_per_player_; // Death timers per player (seconds)
|
||||
|
||||
// Lives and game over system
|
||||
|
||||
@@ -20,18 +20,18 @@ using SceneManager::SceneContext;
|
||||
using SceneType = SceneContext::SceneType;
|
||||
using Option = SceneContext::Option;
|
||||
|
||||
// Helper: calcular el progrés individual de una lletra
|
||||
// Helper: calcular el progrés individual de una letter
|
||||
// en función del progrés global (efecte seqüencial)
|
||||
static auto computeLetterProgress(size_t letra_index, size_t num_letras, float global_progress, float threshold) -> float {
|
||||
if (num_letras == 0) {
|
||||
static auto computeLetterProgress(size_t letter_index, size_t num_letters, float global_progress, float threshold) -> float {
|
||||
if (num_letters == 0) {
|
||||
return 1.0F;
|
||||
}
|
||||
|
||||
// Calcular time per lletra
|
||||
float duration_per_letra = 1.0F / static_cast<float>(num_letras);
|
||||
float step = threshold * duration_per_letra;
|
||||
float start = static_cast<float>(letra_index) * step;
|
||||
float end = start + duration_per_letra;
|
||||
// Calcular time per letter
|
||||
float duration_per_letter = 1.0F / static_cast<float>(num_letters);
|
||||
float step = threshold * duration_per_letter;
|
||||
float start = static_cast<float>(letter_index) * step;
|
||||
float end = start + duration_per_letter;
|
||||
|
||||
// Interpolar progrés
|
||||
if (global_progress < start) {
|
||||
@@ -47,15 +47,14 @@ LogoScene::LogoScene(SDLManager& sdl, SceneContext& context)
|
||||
: sdl_(sdl),
|
||||
context_(context),
|
||||
|
||||
debris_manager_(std::make_unique<Effects::DebrisManager>(sdl.getRenderer()))
|
||||
{
|
||||
debris_manager_(std::make_unique<Effects::DebrisManager>(sdl.getRenderer())) {
|
||||
std::cout << "SceneType Logo: Inicialitzant...\n";
|
||||
|
||||
// Consumir opciones (LOGO no processa opciones actualment)
|
||||
auto option = context_.consumeOption();
|
||||
(void)option; // Suprimir warning
|
||||
|
||||
so_reproduit_.fill(false); // Inicialitzar seguiment de sons
|
||||
sound_played_.fill(false); // Inicialitzar seguiment de sons
|
||||
initLetters();
|
||||
}
|
||||
|
||||
@@ -91,7 +90,7 @@ void LogoScene::initLetters() {
|
||||
"logo/letra_s.shp"};
|
||||
|
||||
// Pas 1: Carregar todas las formes i calcular amplades
|
||||
float ancho_total = 0.0F;
|
||||
float total_width = 0.0F;
|
||||
|
||||
for (const auto& file : archivos) {
|
||||
auto shape = ShapeLoader::load(file);
|
||||
@@ -111,66 +110,66 @@ void LogoScene::initLetters() {
|
||||
}
|
||||
}
|
||||
|
||||
float ancho_sin_escalar = max_x - min_x;
|
||||
float width_unscaled = max_x - min_x;
|
||||
|
||||
// IMPORTANT: Escalar ancho i offset con ESCALA_FINAL
|
||||
// IMPORTANT: Escalar ancho i offset con FINAL_SCALE
|
||||
// per que las posicions finals coincideixin con la mida real de las lletres
|
||||
float ancho = ancho_sin_escalar * ESCALA_FINAL;
|
||||
float offset_centre = (shape->getCenter().x - min_x) * ESCALA_FINAL;
|
||||
float width = width_unscaled * FINAL_SCALE;
|
||||
float center_offset = (shape->getCenter().x - min_x) * FINAL_SCALE;
|
||||
|
||||
lletres_.push_back({shape,
|
||||
letters_.push_back({shape,
|
||||
{.x = 0.0F, .y = 0.0F}, // Posición es calcularà después
|
||||
ancho,
|
||||
offset_centre});
|
||||
width,
|
||||
center_offset});
|
||||
|
||||
ancho_total += ancho;
|
||||
total_width += width;
|
||||
}
|
||||
|
||||
// Pas 2: Añadir espaiat entre lletres
|
||||
ancho_total += ESPAI_ENTRE_LLETRES * (lletres_.size() - 1);
|
||||
total_width += LETTER_SPACING * (letters_.size() - 1);
|
||||
|
||||
// Pas 3: Calcular posición inicial (centrat horitzontal)
|
||||
constexpr auto PANTALLA_ANCHO = static_cast<float>(Defaults::Game::WIDTH);
|
||||
constexpr auto SCREEN_WIDTH = static_cast<float>(Defaults::Game::WIDTH);
|
||||
constexpr auto PANTALLA_ALTO = static_cast<float>(Defaults::Game::HEIGHT);
|
||||
|
||||
float x_inicial = (PANTALLA_ANCHO - ancho_total) / 2.0F;
|
||||
float x_inicial = (SCREEN_WIDTH - total_width) / 2.0F;
|
||||
float y_centre = PANTALLA_ALTO / 2.0F;
|
||||
|
||||
// Pas 4: Assignar posicions a cada lletra
|
||||
// Pas 4: Assignar posicions a cada letter
|
||||
float x_actual = x_inicial;
|
||||
|
||||
for (auto& lletra : lletres_) {
|
||||
for (auto& letter : letters_) {
|
||||
// Posicionar el centro de la shape (shape_centre) en pantalla
|
||||
// Usar offset_centre en lloc de ancho/2 perquè shape_centre
|
||||
// Usar center_offset en lloc de ancho/2 perquè shape_centre
|
||||
// pot no estar exactament al mig del bounding box
|
||||
lletra.position.x = x_actual + lletra.offset_centre;
|
||||
lletra.position.y = y_centre;
|
||||
letter.position.x = x_actual + letter.center_offset;
|
||||
letter.position.y = y_centre;
|
||||
|
||||
// Avançar para següent lletra
|
||||
x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES;
|
||||
// Avançar para següent letter
|
||||
x_actual += letter.width + LETTER_SPACING;
|
||||
}
|
||||
|
||||
std::cout << "[LogoScene] " << lletres_.size()
|
||||
<< " lletres carregades, ancho total: " << ancho_total << " px\n";
|
||||
std::cout << "[LogoScene] " << letters_.size()
|
||||
<< " lletres carregades, ancho total: " << total_width << " px\n";
|
||||
}
|
||||
|
||||
void LogoScene::changeState(AnimationState nou_estat) {
|
||||
estat_actual_ = nou_estat;
|
||||
temps_estat_actual_ = 0.0F; // Reset time
|
||||
current_state_ = nou_estat;
|
||||
temps_current_state_ = 0.0F; // Reset time
|
||||
|
||||
// Inicialitzar state de explosión
|
||||
if (nou_estat == AnimationState::EXPLOSION) {
|
||||
lletra_explosio_index_ = 0;
|
||||
temps_des_ultima_explosio_ = 0.0F;
|
||||
letter_explosion_index_ = 0;
|
||||
time_since_last_explosion_ = 0.0F;
|
||||
|
||||
// Generar ordre aleatori de explosions
|
||||
ordre_explosio_.clear();
|
||||
for (size_t i = 0; i < lletres_.size(); i++) {
|
||||
ordre_explosio_.push_back(i);
|
||||
explosion_order_.clear();
|
||||
for (size_t i = 0; i < letters_.size(); i++) {
|
||||
explosion_order_.push_back(i);
|
||||
}
|
||||
std::random_device rd;
|
||||
std::mt19937 g(rd());
|
||||
std::shuffle(ordre_explosio_.begin(), ordre_explosio_.end(), g);
|
||||
std::shuffle(explosion_order_.begin(), explosion_order_.end(), g);
|
||||
} else if (nou_estat == AnimationState::POST_EXPLOSION) {
|
||||
Audio::get()->playMusic("title.ogg");
|
||||
}
|
||||
@@ -180,35 +179,35 @@ void LogoScene::changeState(AnimationState nou_estat) {
|
||||
}
|
||||
|
||||
auto LogoScene::allLettersComplete() const -> bool {
|
||||
// Cuando global_progress = 1.0, todas las lletres tenen letra_progress = 1.0
|
||||
return temps_estat_actual_ >= DURACIO_ZOOM;
|
||||
// Cuando global_progress = 1.0, todas las lletres tenen letter_progress = 1.0
|
||||
return temps_current_state_ >= DURATION_ZOOM;
|
||||
}
|
||||
|
||||
void LogoScene::updateExplosions(float delta_time) {
|
||||
temps_des_ultima_explosio_ += delta_time;
|
||||
time_since_last_explosion_ += delta_time;
|
||||
|
||||
// Comprovar si es el moment de explode la següent lletra
|
||||
if (temps_des_ultima_explosio_ >= DELAY_ENTRE_EXPLOSIONS) {
|
||||
if (lletra_explosio_index_ < lletres_.size()) {
|
||||
// Explotar lletra actual (en ordre aleatori)
|
||||
size_t index_actual = ordre_explosio_[lletra_explosio_index_];
|
||||
const auto& lletra = lletres_[index_actual];
|
||||
// Comprovar si es el moment de explode la següent letter
|
||||
if (time_since_last_explosion_ >= DELAY_ENTRE_EXPLOSIONS) {
|
||||
if (letter_explosion_index_ < letters_.size()) {
|
||||
// Explotar letter actual (en ordre aleatori)
|
||||
size_t index_actual = explosion_order_[letter_explosion_index_];
|
||||
const auto& letter = letters_[index_actual];
|
||||
|
||||
debris_manager_->explode(
|
||||
lletra.shape, // Forma a explode
|
||||
lletra.position, // Posición
|
||||
letter.shape, // Forma a explode
|
||||
letter.position, // Posición
|
||||
0.0F, // Angle (sin rotación)
|
||||
ESCALA_FINAL, // Escala (lletres a scale final)
|
||||
VELOCITAT_EXPLOSIO, // Velocidad base
|
||||
FINAL_SCALE, // Escala (lletres a scale final)
|
||||
SPEED_EXPLOSIO, // Velocidad base
|
||||
1.0F, // Brightness màxim (per defecte)
|
||||
{.x = 0.0F, .y = 0.0F} // Sin velocity (per defecte)
|
||||
);
|
||||
|
||||
std::cout << "[LogoScene] Explota lletra " << lletra_explosio_index_ << "\n";
|
||||
std::cout << "[LogoScene] Explota letter " << letter_explosion_index_ << "\n";
|
||||
|
||||
// Passar a la següent lletra
|
||||
lletra_explosio_index_++;
|
||||
temps_des_ultima_explosio_ = 0.0F;
|
||||
// Passar a la següent letter
|
||||
letter_explosion_index_++;
|
||||
time_since_last_explosion_ = 0.0F;
|
||||
} else {
|
||||
// Todas las lletres han explotat, transición a POST_EXPLOSION
|
||||
changeState(AnimationState::POST_EXPLOSION);
|
||||
@@ -217,31 +216,31 @@ void LogoScene::updateExplosions(float delta_time) {
|
||||
}
|
||||
|
||||
void LogoScene::update(float delta_time) {
|
||||
temps_estat_actual_ += delta_time;
|
||||
temps_current_state_ += delta_time;
|
||||
|
||||
switch (estat_actual_) {
|
||||
switch (current_state_) {
|
||||
case AnimationState::PRE_ANIMATION:
|
||||
if (temps_estat_actual_ >= DURACIO_PRE) {
|
||||
if (temps_current_state_ >= DURATION_PRE) {
|
||||
changeState(AnimationState::ANIMATION);
|
||||
}
|
||||
break;
|
||||
|
||||
case AnimationState::ANIMATION: {
|
||||
// Reproduir so per cada lletra cuando comença a aparèixer
|
||||
float global_progress = std::min(temps_estat_actual_ / DURACIO_ZOOM, 1.0F);
|
||||
// Reproduir so per cada letter cuando comença a aparèixer
|
||||
float global_progress = std::min(temps_current_state_ / DURATION_ZOOM, 1.0F);
|
||||
|
||||
for (size_t i = 0; i < lletres_.size() && i < so_reproduit_.size(); i++) {
|
||||
if (!so_reproduit_[i]) {
|
||||
float letra_progress = computeLetterProgress(
|
||||
for (size_t i = 0; i < letters_.size() && i < sound_played_.size(); i++) {
|
||||
if (!sound_played_[i]) {
|
||||
float letter_progress = computeLetterProgress(
|
||||
i,
|
||||
lletres_.size(),
|
||||
letters_.size(),
|
||||
global_progress,
|
||||
THRESHOLD_LETRA);
|
||||
LETTER_THRESHOLD);
|
||||
|
||||
// Reproduir so cuando la lletra comença a aparèixer (progress > 0)
|
||||
if (letra_progress > 0.0F) {
|
||||
// Reproduir so cuando la letter comença a aparèixer (progress > 0)
|
||||
if (letter_progress > 0.0F) {
|
||||
Audio::get()->playSound(Defaults::Sound::LOGO, Audio::Group::GAME);
|
||||
so_reproduit_[i] = true;
|
||||
sound_played_[i] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,7 +252,7 @@ void LogoScene::update(float delta_time) {
|
||||
}
|
||||
|
||||
case AnimationState::POST_ANIMATION:
|
||||
if (temps_estat_actual_ >= DURACIO_POST_ANIMATION) {
|
||||
if (temps_current_state_ >= DURATION_POST_ANIMATION) {
|
||||
changeState(AnimationState::EXPLOSION);
|
||||
}
|
||||
break;
|
||||
@@ -263,7 +262,7 @@ void LogoScene::update(float delta_time) {
|
||||
break;
|
||||
|
||||
case AnimationState::POST_EXPLOSION:
|
||||
if (temps_estat_actual_ >= DURACIO_POST_EXPLOSION) {
|
||||
if (temps_current_state_ >= DURATION_POST_EXPLOSION) {
|
||||
// Transición a pantalla de título
|
||||
context_.setNextScene(SceneType::TITLE);
|
||||
}
|
||||
@@ -283,47 +282,47 @@ void LogoScene::draw() {
|
||||
// Director ha hecho el clear; aquí solo pintamos lo de la escena.
|
||||
|
||||
// PRE_ANIMATION: Solo pantalla negra (no se pinta nada).
|
||||
if (estat_actual_ == AnimationState::PRE_ANIMATION) {
|
||||
if (current_state_ == AnimationState::PRE_ANIMATION) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ANIMATION o POST_ANIMATION: Dibuixar lletres con animación
|
||||
if (estat_actual_ == AnimationState::ANIMATION ||
|
||||
estat_actual_ == AnimationState::POST_ANIMATION) {
|
||||
if (current_state_ == AnimationState::ANIMATION ||
|
||||
current_state_ == AnimationState::POST_ANIMATION) {
|
||||
float global_progress =
|
||||
(estat_actual_ == AnimationState::ANIMATION)
|
||||
? std::min(temps_estat_actual_ / DURACIO_ZOOM, 1.0F)
|
||||
(current_state_ == AnimationState::ANIMATION)
|
||||
? std::min(temps_current_state_ / DURATION_ZOOM, 1.0F)
|
||||
: 1.0F; // POST: mantenir al 100%
|
||||
|
||||
const Vec2 ORIGEN_ZOOM = {.x = ORIGEN_ZOOM_X, .y = ORIGEN_ZOOM_Y};
|
||||
const Vec2 ZOOM_ORIGIN = {.x = ZOOM_ORIGIN_X, .y = ZOOM_ORIGIN_Y};
|
||||
|
||||
for (size_t i = 0; i < lletres_.size(); i++) {
|
||||
const auto& lletra = lletres_[i];
|
||||
for (size_t i = 0; i < letters_.size(); i++) {
|
||||
const auto& letter = letters_[i];
|
||||
|
||||
float letra_progress = computeLetterProgress(
|
||||
float letter_progress = computeLetterProgress(
|
||||
i,
|
||||
lletres_.size(),
|
||||
letters_.size(),
|
||||
global_progress,
|
||||
THRESHOLD_LETRA);
|
||||
LETTER_THRESHOLD);
|
||||
|
||||
if (letra_progress <= 0.0F) {
|
||||
if (letter_progress <= 0.0F) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Vec2 pos_actual;
|
||||
pos_actual.x =
|
||||
ORIGEN_ZOOM.x + ((lletra.position.x - ORIGEN_ZOOM.x) * letra_progress);
|
||||
ZOOM_ORIGIN.x + ((letter.position.x - ZOOM_ORIGIN.x) * letter_progress);
|
||||
pos_actual.y =
|
||||
ORIGEN_ZOOM.y + ((lletra.position.y - ORIGEN_ZOOM.y) * letra_progress);
|
||||
ZOOM_ORIGIN.y + ((letter.position.y - ZOOM_ORIGIN.y) * letter_progress);
|
||||
|
||||
float t = letra_progress;
|
||||
float t = letter_progress;
|
||||
float ease_factor = 1.0F - ((1.0F - t) * (1.0F - t));
|
||||
float current_scale =
|
||||
ESCALA_INICIAL + ((ESCALA_FINAL - ESCALA_INICIAL) * ease_factor);
|
||||
INITIAL_SCALE + ((FINAL_SCALE - INITIAL_SCALE) * ease_factor);
|
||||
|
||||
Rendering::renderShape(
|
||||
sdl_.getRenderer(),
|
||||
lletra.shape,
|
||||
letter.shape,
|
||||
pos_actual,
|
||||
0.0F,
|
||||
current_scale,
|
||||
@@ -332,24 +331,24 @@ void LogoScene::draw() {
|
||||
}
|
||||
|
||||
// EXPLOSION: Dibuixar solo lletres que aún no han explotat
|
||||
if (estat_actual_ == AnimationState::EXPLOSION) {
|
||||
if (current_state_ == AnimationState::EXPLOSION) {
|
||||
// Crear conjunt de lletres ya explotades
|
||||
std::set<size_t> explotades;
|
||||
for (size_t i = 0; i < lletra_explosio_index_; i++) {
|
||||
explotades.insert(ordre_explosio_[i]);
|
||||
for (size_t i = 0; i < letter_explosion_index_; i++) {
|
||||
explotades.insert(explosion_order_[i]);
|
||||
}
|
||||
|
||||
// Dibuixar solo lletres que NO han explotat
|
||||
for (size_t i = 0; i < lletres_.size(); i++) {
|
||||
for (size_t i = 0; i < letters_.size(); i++) {
|
||||
if (!explotades.contains(i)) {
|
||||
const auto& lletra = lletres_[i];
|
||||
const auto& letter = letters_[i];
|
||||
|
||||
Rendering::renderShape(
|
||||
sdl_.getRenderer(),
|
||||
lletra.shape,
|
||||
lletra.position,
|
||||
letter.shape,
|
||||
letter.position,
|
||||
0.0F,
|
||||
ESCALA_FINAL,
|
||||
FINAL_SCALE,
|
||||
1.0F);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,76 +20,76 @@
|
||||
#include "game/effects/debris_manager.hpp"
|
||||
|
||||
class LogoScene final : public Scene {
|
||||
public:
|
||||
explicit LogoScene(SDLManager& sdl, SceneManager::SceneContext& context);
|
||||
~LogoScene() override; // Destructor per aturar sons
|
||||
public:
|
||||
explicit LogoScene(SDLManager& sdl, SceneManager::SceneContext& context);
|
||||
~LogoScene() override; // Destructor per aturar sons
|
||||
|
||||
// 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;
|
||||
|
||||
private:
|
||||
// Màquina de estats per l'animación
|
||||
enum class AnimationState : std::uint8_t {
|
||||
PRE_ANIMATION, // Pantalla negra inicial
|
||||
ANIMATION, // Animación de zoom de lletres
|
||||
POST_ANIMATION, // Logo complet visible
|
||||
EXPLOSION, // Explosión seqüencial de lletres
|
||||
POST_EXPLOSION // Espera después de l'última explosión
|
||||
};
|
||||
private:
|
||||
// Màquina de estats per l'animación
|
||||
enum class AnimationState : std::uint8_t {
|
||||
PRE_ANIMATION, // Pantalla negra inicial
|
||||
ANIMATION, // Animación de zoom de lletres
|
||||
POST_ANIMATION, // Logo complet visible
|
||||
EXPLOSION, // Explosión seqüencial de lletres
|
||||
POST_EXPLOSION // Espera después de l'última explosión
|
||||
};
|
||||
|
||||
SDLManager& sdl_;
|
||||
SceneManager::SceneContext& context_;
|
||||
AnimationState estat_actual_{AnimationState::PRE_ANIMATION}; // Estat actual de la màquina
|
||||
float
|
||||
temps_estat_actual_{0.0F}; // Temps en l'state actual (reset en cada transición)
|
||||
SDLManager& sdl_;
|
||||
SceneManager::SceneContext& context_;
|
||||
AnimationState current_state_{AnimationState::PRE_ANIMATION}; // Estat actual de la màquina
|
||||
float
|
||||
temps_current_state_{0.0F}; // Temps en l'state actual (reset en cada transición)
|
||||
|
||||
// Gestor de fragments de explosions
|
||||
std::unique_ptr<Effects::DebrisManager> debris_manager_;
|
||||
// Gestor de fragments de explosions
|
||||
std::unique_ptr<Effects::DebrisManager> debris_manager_;
|
||||
|
||||
// Seguiment de explosions seqüencials
|
||||
size_t lletra_explosio_index_{0}; // Índex de la següent lletra a explode
|
||||
float temps_des_ultima_explosio_{0.0F}; // Temps desde l'última explosión
|
||||
std::vector<size_t> ordre_explosio_; // Ordre aleatori de índexs de lletres
|
||||
// Seguiment de explosions seqüencials
|
||||
size_t letter_explosion_index_{0}; // Índex de la següent letter a explode
|
||||
float time_since_last_explosion_{0.0F}; // Temps desde l'última explosión
|
||||
std::vector<size_t> explosion_order_; // Ordre aleatori de índexs de lletres
|
||||
|
||||
// Estructura para cada lletra del logo
|
||||
struct LetraLogo {
|
||||
std::shared_ptr<Graphics::Shape> shape;
|
||||
Vec2 position; // Posición final en pantalla
|
||||
float ancho; // Ancho del bounding box
|
||||
float offset_centre; // Distancia de min_x a shape_centre.x
|
||||
};
|
||||
// Estructura para cada letter del logo
|
||||
struct LogoLetter {
|
||||
std::shared_ptr<Graphics::Shape> shape;
|
||||
Vec2 position; // Posición final en pantalla
|
||||
float width; // Ancho del bounding box
|
||||
float center_offset; // Distancia de min_x a shape_centre.x
|
||||
};
|
||||
|
||||
std::vector<LetraLogo> lletres_; // 9 lletres: J-A-I-L-G-A-M-E-S
|
||||
std::vector<LogoLetter> letters_; // 9 lletres: J-A-I-L-G-A-M-E-S
|
||||
|
||||
// Seguiment de sons de lletres (evitar reproduccions repetides)
|
||||
std::array<bool, 9> so_reproduit_; // Track si cada lletra ya ha reproduit el so
|
||||
// Seguiment de sons de lletres (evitar reproduccions repetides)
|
||||
std::array<bool, 9> sound_played_; // Track si cada letter ya ha reproduit el so
|
||||
|
||||
// Constants de animación
|
||||
static constexpr float DURACIO_PRE = 1.5F; // Duració PRE_ANIMATION (pantalla negra)
|
||||
static constexpr float DURACIO_ZOOM = 4.0F; // Duració del zoom (segons)
|
||||
static constexpr float DURACIO_POST_ANIMATION = 3.0F; // Duració POST_ANIMATION (logo complet)
|
||||
static constexpr float DURACIO_POST_EXPLOSION = 3.0F; // Duració POST_EXPLOSION (espera final)
|
||||
static constexpr float DELAY_ENTRE_EXPLOSIONS = 0.1F; // Temps entre explosions de lletres
|
||||
static constexpr float VELOCITAT_EXPLOSIO = 240.0F; // Velocidad base fragments (px/s)
|
||||
static constexpr float ESCALA_INICIAL = 0.1F; // Escala inicial (10%)
|
||||
static constexpr float ESCALA_FINAL = 0.8F; // Escala final (80%)
|
||||
static constexpr float ESPAI_ENTRE_LLETRES = 10.0F; // Espaiat entre lletres
|
||||
// Constants de animación
|
||||
static constexpr float DURATION_PRE = 1.5F; // Duració PRE_ANIMATION (pantalla negra)
|
||||
static constexpr float DURATION_ZOOM = 4.0F; // Duració del zoom (segons)
|
||||
static constexpr float DURATION_POST_ANIMATION = 3.0F; // Duració POST_ANIMATION (logo complet)
|
||||
static constexpr float DURATION_POST_EXPLOSION = 3.0F; // Duració POST_EXPLOSION (espera final)
|
||||
static constexpr float DELAY_ENTRE_EXPLOSIONS = 0.1F; // Temps entre explosions de lletres
|
||||
static constexpr float SPEED_EXPLOSIO = 240.0F; // Velocidad base fragments (px/s)
|
||||
static constexpr float INITIAL_SCALE = 0.1F; // Escala inicial (10%)
|
||||
static constexpr float FINAL_SCALE = 0.8F; // Escala final (80%)
|
||||
static constexpr float LETTER_SPACING = 10.0F; // Espaiat entre lletres
|
||||
|
||||
// Constants de animación seqüencial
|
||||
static constexpr float THRESHOLD_LETRA = 0.6F; // Umbral per activar següent lletra (0.0-1.0)
|
||||
static constexpr float ORIGEN_ZOOM_X = Defaults::Game::WIDTH * 0.5F; // Vec2 inicial X del zoom
|
||||
static constexpr float ORIGEN_ZOOM_Y = Defaults::Game::HEIGHT * 0.4F; // Vec2 inicial Y del zoom
|
||||
// Constants de animación seqüencial
|
||||
static constexpr float LETTER_THRESHOLD = 0.6F; // Umbral per activar següent letter (0.0-1.0)
|
||||
static constexpr float ZOOM_ORIGIN_X = Defaults::Game::WIDTH * 0.5F; // Vec2 inicial X del zoom
|
||||
static constexpr float ZOOM_ORIGIN_Y = Defaults::Game::HEIGHT * 0.4F; // Vec2 inicial Y del zoom
|
||||
|
||||
// Métodos privats
|
||||
void initLetters();
|
||||
void updateExplosions(float delta_time);
|
||||
// Estático: solo consulta Input (singleton), no estado de la escena.
|
||||
static auto checkSkipButtonPressed() -> bool;
|
||||
// Métodos privats
|
||||
void initLetters();
|
||||
void updateExplosions(float delta_time);
|
||||
// Estático: solo consulta Input (singleton), no estado de la escena.
|
||||
static auto checkSkipButtonPressed() -> bool;
|
||||
|
||||
// Métodos de gestió de estats
|
||||
void changeState(AnimationState nou_estat);
|
||||
[[nodiscard]] auto allLettersComplete() const -> bool;
|
||||
// Métodos de gestió de estats
|
||||
void changeState(AnimationState nou_estat);
|
||||
[[nodiscard]] auto allLettersComplete() const -> bool;
|
||||
};
|
||||
|
||||
+139
-139
@@ -36,15 +36,15 @@ TitleScene::TitleScene(SDLManager& sdl, SceneContext& context)
|
||||
text_(sdl.getRenderer()) {
|
||||
std::cout << "SceneType Titol: Inicialitzant...\n";
|
||||
|
||||
match_config_.jugador1_actiu = false;
|
||||
match_config_.jugador2_actiu = false;
|
||||
match_config_.player1_active = false;
|
||||
match_config_.player2_active = false;
|
||||
match_config_.mode = GameConfig::Mode::NORMAL;
|
||||
|
||||
auto option = context_.consumeOption();
|
||||
if (option == Option::JUMP_TO_TITLE_MAIN) {
|
||||
std::cout << "SceneType Titol: Opció JUMP_TO_TITLE_MAIN activada\n";
|
||||
estat_actual_ = TitleState::MAIN;
|
||||
temps_estat_main_ = 0.0F;
|
||||
current_state_ = TitleState::MAIN;
|
||||
state_time_main_ = 0.0F;
|
||||
}
|
||||
|
||||
// Càmera 3D: posicionada a l'origen, mirant cap a +Z, amb Y cap amunt.
|
||||
@@ -61,7 +61,7 @@ TitleScene::TitleScene(SDLManager& sdl, SceneContext& context)
|
||||
camera_.get(),
|
||||
200);
|
||||
starfield_->setColor(Defaults::Title::Colors::STARFIELD);
|
||||
if (estat_actual_ == TitleState::MAIN) {
|
||||
if (current_state_ == TitleState::MAIN) {
|
||||
starfield_->setBrightness(BRIGHTNESS_STARFIELD);
|
||||
} else {
|
||||
starfield_->setBrightness(0.0F);
|
||||
@@ -103,7 +103,7 @@ void TitleScene::initTitle() {
|
||||
"title/letra_n.shp",
|
||||
"title/letra_i.shp"};
|
||||
|
||||
float ancho_total_orni = 0.0F;
|
||||
float total_width_orni = 0.0F;
|
||||
for (const auto& file : FITXERS_ORNI) {
|
||||
auto shape = ShapeLoader::load(file);
|
||||
if (!shape || !shape->isValid()) {
|
||||
@@ -122,25 +122,25 @@ void TitleScene::initTitle() {
|
||||
max_y = std::max(max_y, point.y);
|
||||
}
|
||||
}
|
||||
const float ANCHO = (max_x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
const float ALTURA = (max_y - min_y) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
const float OFFSET_CENTRE = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
lletres_orni_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ANCHO, ALTURA, OFFSET_CENTRE});
|
||||
ancho_total_orni += ANCHO;
|
||||
const float WIDTH = (max_x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
const float HEIGHT = (max_y - min_y) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
const float CENTER_OFFSET = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
letters_orni_.push_back({shape, {.x = 0.0F, .y = 0.0F}, WIDTH, HEIGHT, CENTER_OFFSET});
|
||||
total_width_orni += WIDTH;
|
||||
}
|
||||
ancho_total_orni += ESPAI_ENTRE_LLETRES * static_cast<float>(lletres_orni_.size() - 1);
|
||||
total_width_orni += LETTER_SPACING * static_cast<float>(letters_orni_.size() - 1);
|
||||
|
||||
float x_actual = (Defaults::Game::WIDTH - ancho_total_orni) / 2.0F;
|
||||
for (auto& lletra : lletres_orni_) {
|
||||
lletra.position.x = x_actual + lletra.offset_centre;
|
||||
lletra.position.y = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS;
|
||||
x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES;
|
||||
float x_actual = (Defaults::Game::WIDTH - total_width_orni) / 2.0F;
|
||||
for (auto& letter : letters_orni_) {
|
||||
letter.position.x = x_actual + letter.center_offset;
|
||||
letter.position.y = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS;
|
||||
x_actual += letter.width + LETTER_SPACING;
|
||||
}
|
||||
|
||||
const float ALTURA_ORNI = lletres_orni_.empty() ? 50.0F : lletres_orni_[0].altura;
|
||||
const float ORNI_HEIGHT = letters_orni_.empty() ? 50.0F : letters_orni_[0].height;
|
||||
const float Y_ORNI = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS;
|
||||
const float SEPARACION = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_LINE_SPACING;
|
||||
y_attack_dinamica_ = Y_ORNI + ALTURA_ORNI + SEPARACION;
|
||||
dynamic_attack_y_ = Y_ORNI + ORNI_HEIGHT + SEPARACION;
|
||||
|
||||
const std::vector<std::string> FITXERS_ATTACK = {
|
||||
"title/letra_a.shp",
|
||||
@@ -151,7 +151,7 @@ void TitleScene::initTitle() {
|
||||
"title/letra_k.shp",
|
||||
"title/letra_exclamacion.shp"};
|
||||
|
||||
float ancho_total_attack = 0.0F;
|
||||
float total_width_attack = 0.0F;
|
||||
for (const auto& file : FITXERS_ATTACK) {
|
||||
auto shape = ShapeLoader::load(file);
|
||||
if (!shape || !shape->isValid()) {
|
||||
@@ -170,28 +170,28 @@ void TitleScene::initTitle() {
|
||||
max_y = std::max(max_y, point.y);
|
||||
}
|
||||
}
|
||||
const float ANCHO = (max_x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
const float ALTURA = (max_y - min_y) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
const float OFFSET_CENTRE = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
lletres_attack_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ANCHO, ALTURA, OFFSET_CENTRE});
|
||||
ancho_total_attack += ANCHO;
|
||||
const float WIDTH = (max_x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
const float HEIGHT = (max_y - min_y) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
const float CENTER_OFFSET = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
letters_attack_.push_back({shape, {.x = 0.0F, .y = 0.0F}, WIDTH, HEIGHT, CENTER_OFFSET});
|
||||
total_width_attack += WIDTH;
|
||||
}
|
||||
ancho_total_attack += ESPAI_ENTRE_LLETRES * static_cast<float>(lletres_attack_.size() - 1);
|
||||
total_width_attack += LETTER_SPACING * static_cast<float>(letters_attack_.size() - 1);
|
||||
|
||||
x_actual = (Defaults::Game::WIDTH - ancho_total_attack) / 2.0F;
|
||||
for (auto& lletra : lletres_attack_) {
|
||||
lletra.position.x = x_actual + lletra.offset_centre;
|
||||
lletra.position.y = y_attack_dinamica_;
|
||||
x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES;
|
||||
x_actual = (Defaults::Game::WIDTH - total_width_attack) / 2.0F;
|
||||
for (auto& letter : letters_attack_) {
|
||||
letter.position.x = x_actual + letter.center_offset;
|
||||
letter.position.y = dynamic_attack_y_;
|
||||
x_actual += letter.width + LETTER_SPACING;
|
||||
}
|
||||
|
||||
posicions_originals_orni_.clear();
|
||||
for (const auto& lletra : lletres_orni_) {
|
||||
posicions_originals_orni_.push_back(lletra.position);
|
||||
original_positions_orni_.clear();
|
||||
for (const auto& letter : letters_orni_) {
|
||||
original_positions_orni_.push_back(letter.position);
|
||||
}
|
||||
posicions_originals_attack_.clear();
|
||||
for (const auto& lletra : lletres_attack_) {
|
||||
posicions_originals_attack_.push_back(lletra.position);
|
||||
original_positions_attack_.clear();
|
||||
for (const auto& letter : letters_attack_) {
|
||||
original_positions_attack_.push_back(letter.position);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,8 +211,8 @@ void TitleScene::inicialitzarJailgames() {
|
||||
|
||||
constexpr float SCALE = Defaults::Title::Layout::JAILGAMES_SCALE;
|
||||
|
||||
float ancho_total = 0.0F;
|
||||
float altura_max = 0.0F;
|
||||
float total_width = 0.0F;
|
||||
float max_height = 0.0F;
|
||||
for (const auto& file : FITXERS) {
|
||||
auto shape = ShapeLoader::load(file);
|
||||
if (!shape || !shape->isValid()) {
|
||||
@@ -231,28 +231,28 @@ void TitleScene::inicialitzarJailgames() {
|
||||
max_y = std::max(max_y, point.y);
|
||||
}
|
||||
}
|
||||
const float ANCHO = (max_x - min_x) * SCALE;
|
||||
const float ALTURA = (max_y - min_y) * SCALE;
|
||||
const float OFFSET_CENTRE = (shape->getCenter().x - min_x) * SCALE;
|
||||
lletres_jailgames_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ANCHO, ALTURA, OFFSET_CENTRE});
|
||||
ancho_total += ANCHO;
|
||||
altura_max = std::max(altura_max, ALTURA);
|
||||
const float WIDTH = (max_x - min_x) * SCALE;
|
||||
const float HEIGHT = (max_y - min_y) * SCALE;
|
||||
const float CENTER_OFFSET = (shape->getCenter().x - min_x) * SCALE;
|
||||
letters_jailgames_.push_back({shape, {.x = 0.0F, .y = 0.0F}, WIDTH, HEIGHT, CENTER_OFFSET});
|
||||
total_width += WIDTH;
|
||||
max_height = std::max(max_height, HEIGHT);
|
||||
}
|
||||
constexpr float ESPAI_JAILGAMES = ESPAI_ENTRE_LLETRES * SCALE;
|
||||
if (!lletres_jailgames_.empty()) {
|
||||
ancho_total += ESPAI_JAILGAMES * static_cast<float>(lletres_jailgames_.size() - 1);
|
||||
constexpr float JAILGAMES_SPACING = LETTER_SPACING * SCALE;
|
||||
if (!letters_jailgames_.empty()) {
|
||||
total_width += JAILGAMES_SPACING * static_cast<float>(letters_jailgames_.size() - 1);
|
||||
}
|
||||
|
||||
const float Y_COPY = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS;
|
||||
const float GAP = Defaults::Game::HEIGHT * Defaults::Title::Layout::JAILGAMES_COPYRIGHT_GAP;
|
||||
const float Y_CENTRE = Y_COPY - GAP - (altura_max / 2.0F);
|
||||
const float X_INICIAL = (Defaults::Game::WIDTH - ancho_total) / 2.0F;
|
||||
const float Y_CENTRE = Y_COPY - GAP - (max_height / 2.0F);
|
||||
const float X_INICIAL = (Defaults::Game::WIDTH - total_width) / 2.0F;
|
||||
|
||||
float x_actual = X_INICIAL;
|
||||
for (auto& lletra : lletres_jailgames_) {
|
||||
lletra.position.x = x_actual + lletra.offset_centre;
|
||||
lletra.position.y = Y_CENTRE;
|
||||
x_actual += lletra.ancho + ESPAI_JAILGAMES;
|
||||
for (auto& letter : letters_jailgames_) {
|
||||
letter.position.x = x_actual + letter.center_offset;
|
||||
letter.position.y = Y_CENTRE;
|
||||
x_actual += letter.width + JAILGAMES_SPACING;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,12 +268,12 @@ void TitleScene::dibuixarPeuTitol(float spacing) const {
|
||||
const float COPYRIGHT_S = std::lerp(S::FOOTER_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_copyright_progress_));
|
||||
|
||||
const float JAILGAMES_RENDER_SCALE = Defaults::Title::Layout::JAILGAMES_SCALE * JAILGAMES_S;
|
||||
for (const auto& lletra : lletres_jailgames_) {
|
||||
for (const auto& letter : letters_jailgames_) {
|
||||
const Vec2 POS{
|
||||
.x = SCREEN_CENTRE_X + (JAILGAMES_S * (lletra.position.x - SCREEN_CENTRE_X)),
|
||||
.y = SCREEN_CENTRE_Y + (JAILGAMES_S * (lletra.position.y - SCREEN_CENTRE_Y)),
|
||||
.x = SCREEN_CENTRE_X + (JAILGAMES_S * (letter.position.x - SCREEN_CENTRE_X)),
|
||||
.y = SCREEN_CENTRE_Y + (JAILGAMES_S * (letter.position.y - SCREEN_CENTRE_Y)),
|
||||
};
|
||||
Rendering::renderShape(sdl_.getRenderer(), lletra.shape, POS, 0.0F, JAILGAMES_RENDER_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::JAILGAMES_LOGO);
|
||||
Rendering::renderShape(sdl_.getRenderer(), letter.shape, POS, 0.0F, JAILGAMES_RENDER_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::JAILGAMES_LOGO);
|
||||
}
|
||||
std::string copyright = Project::COPYRIGHT;
|
||||
for (char& c : copyright) {
|
||||
@@ -297,15 +297,15 @@ void TitleScene::update(float delta_time) {
|
||||
starfield_->update(delta_time);
|
||||
}
|
||||
if (ship_animator_ &&
|
||||
(estat_actual_ == TitleState::STARFIELD_FADE_IN ||
|
||||
estat_actual_ == TitleState::STARFIELD ||
|
||||
estat_actual_ == TitleState::MAIN ||
|
||||
estat_actual_ == TitleState::PLAYER_JOIN_PHASE)) {
|
||||
(current_state_ == TitleState::STARFIELD_FADE_IN ||
|
||||
current_state_ == TitleState::STARFIELD ||
|
||||
current_state_ == TitleState::MAIN ||
|
||||
current_state_ == TitleState::PLAYER_JOIN_PHASE)) {
|
||||
ship_animator_->update(delta_time);
|
||||
}
|
||||
updateFlashes(delta_time);
|
||||
|
||||
switch (estat_actual_) {
|
||||
switch (current_state_) {
|
||||
case TitleState::STARFIELD_FADE_IN:
|
||||
updateStarfieldFadeInState(delta_time);
|
||||
break;
|
||||
@@ -329,10 +329,10 @@ void TitleScene::update(float delta_time) {
|
||||
|
||||
void TitleScene::updateStarfieldFadeInState(float delta_time) {
|
||||
temps_acumulat_ += delta_time;
|
||||
const float PROGRESS = std::min(1.0F, temps_acumulat_ / DURACIO_FADE_IN);
|
||||
const float PROGRESS = std::min(1.0F, temps_acumulat_ / DURATION_FADE_IN);
|
||||
starfield_->setBrightness(PROGRESS * BRIGHTNESS_STARFIELD);
|
||||
if (temps_acumulat_ >= DURACIO_FADE_IN) {
|
||||
estat_actual_ = TitleState::STARFIELD;
|
||||
if (temps_acumulat_ >= DURATION_FADE_IN) {
|
||||
current_state_ = TitleState::STARFIELD;
|
||||
temps_acumulat_ = 0.0F;
|
||||
starfield_->setBrightness(BRIGHTNESS_STARFIELD);
|
||||
}
|
||||
@@ -340,16 +340,16 @@ void TitleScene::updateStarfieldFadeInState(float delta_time) {
|
||||
|
||||
void TitleScene::updateStarfieldState(float delta_time) {
|
||||
temps_acumulat_ += delta_time;
|
||||
if (temps_acumulat_ >= DURACIO_INIT) {
|
||||
estat_actual_ = TitleState::MAIN;
|
||||
temps_estat_main_ = 0.0F;
|
||||
animacio_activa_ = false;
|
||||
factor_lerp_ = 0.0F;
|
||||
if (temps_acumulat_ >= DURATION_INIT) {
|
||||
current_state_ = TitleState::MAIN;
|
||||
state_time_main_ = 0.0F;
|
||||
animation_active_ = false;
|
||||
lerp_factor_ = 0.0F;
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene::updateMainState(float delta_time) {
|
||||
temps_estat_main_ += delta_time;
|
||||
state_time_main_ += delta_time;
|
||||
|
||||
namespace S = Defaults::Title::Sequence;
|
||||
namespace Sh = Defaults::Title::Ships;
|
||||
@@ -364,38 +364,38 @@ void TitleScene::updateMainState(float delta_time) {
|
||||
constexpr float T_SHIPS_LANDED = T_SHIPS_START + Sh::ENTRY_DURATION + Sh::P2_ENTRY_DELAY;
|
||||
constexpr float T_PRESS_START_VISIBLE = T_SHIPS_LANDED + S::PRESS_START_DELAY_AFTER_SHIPS;
|
||||
|
||||
intro_logo_progress_ = std::clamp(temps_estat_main_ / S::LOGO_ENTRY_DURATION, 0.0F, 1.0F);
|
||||
intro_logo_progress_ = std::clamp(state_time_main_ / S::LOGO_ENTRY_DURATION, 0.0F, 1.0F);
|
||||
intro_jailgames_progress_ = std::clamp(
|
||||
(temps_estat_main_ - T_FOOTER_START) / S::JAILGAMES_ENTRY_DURATION,
|
||||
(state_time_main_ - T_FOOTER_START) / S::JAILGAMES_ENTRY_DURATION,
|
||||
0.0F,
|
||||
1.0F);
|
||||
intro_copyright_progress_ = std::clamp(
|
||||
(temps_estat_main_ - T_COPY_START) / S::COPYRIGHT_ENTRY_DURATION,
|
||||
(state_time_main_ - T_COPY_START) / S::COPYRIGHT_ENTRY_DURATION,
|
||||
0.0F,
|
||||
1.0F);
|
||||
|
||||
if (!ships_intro_launched_ && temps_estat_main_ >= T_SHIPS_START &&
|
||||
if (!ships_intro_launched_ && state_time_main_ >= T_SHIPS_START &&
|
||||
ship_animator_ != nullptr) {
|
||||
ship_animator_->setVisible(true);
|
||||
ship_animator_->startEntryAnimation();
|
||||
ships_intro_launched_ = true;
|
||||
}
|
||||
|
||||
if (!press_start_visible_ && temps_estat_main_ >= T_PRESS_START_VISIBLE) {
|
||||
if (!press_start_visible_ && state_time_main_ >= T_PRESS_START_VISIBLE) {
|
||||
press_start_visible_ = true;
|
||||
}
|
||||
|
||||
// L'oscil·lació suau del logo arrenca quan el logo ha aterrat. Així
|
||||
// l'amplitud creix gradualment (lerp) durant la resta de la intro.
|
||||
if (temps_estat_main_ < S::LOGO_ENTRY_DURATION) {
|
||||
factor_lerp_ = 0.0F;
|
||||
animacio_activa_ = false;
|
||||
} else if (temps_estat_main_ < S::LOGO_ENTRY_DURATION + DURACIO_LERP) {
|
||||
factor_lerp_ = (temps_estat_main_ - S::LOGO_ENTRY_DURATION) / DURACIO_LERP;
|
||||
animacio_activa_ = true;
|
||||
if (state_time_main_ < S::LOGO_ENTRY_DURATION) {
|
||||
lerp_factor_ = 0.0F;
|
||||
animation_active_ = false;
|
||||
} else if (state_time_main_ < S::LOGO_ENTRY_DURATION + DURATION_LERP) {
|
||||
lerp_factor_ = (state_time_main_ - S::LOGO_ENTRY_DURATION) / DURATION_LERP;
|
||||
animation_active_ = true;
|
||||
} else {
|
||||
factor_lerp_ = 1.0F;
|
||||
animacio_activa_ = true;
|
||||
lerp_factor_ = 1.0F;
|
||||
animation_active_ = true;
|
||||
}
|
||||
updateLogoAnimation(delta_time);
|
||||
}
|
||||
@@ -404,8 +404,8 @@ void TitleScene::updatePlayerJoinPhaseState(float delta_time) {
|
||||
temps_acumulat_ += delta_time;
|
||||
updateLogoAnimation(delta_time);
|
||||
|
||||
const bool P1_ABANS = match_config_.jugador1_actiu;
|
||||
const bool P2_ABANS = match_config_.jugador2_actiu;
|
||||
const bool P1_ABANS = match_config_.player1_active;
|
||||
const bool P2_ABANS = match_config_.player2_active;
|
||||
|
||||
if (checkStartGameButtonPressed()) {
|
||||
context_.setMatchConfig(match_config_);
|
||||
@@ -414,27 +414,27 @@ void TitleScene::updatePlayerJoinPhaseState(float delta_time) {
|
||||
temps_acumulat_ = 0.0F;
|
||||
}
|
||||
|
||||
if (temps_acumulat_ >= DURACIO_TRANSITION) {
|
||||
estat_actual_ = TitleState::BLACK_SCREEN;
|
||||
if (temps_acumulat_ >= DURATION_TRANSITION) {
|
||||
current_state_ = TitleState::BLACK_SCREEN;
|
||||
temps_acumulat_ = 0.0F;
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene::updateBlackScreenState(float delta_time) {
|
||||
temps_acumulat_ += delta_time;
|
||||
if (temps_acumulat_ >= DURACIO_BLACK_SCREEN) {
|
||||
if (temps_acumulat_ >= DURATION_BLACK_SCREEN) {
|
||||
context_.setNextScene(SceneType::GAME);
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene::handleSkipInput() {
|
||||
if (estat_actual_ != TitleState::STARFIELD_FADE_IN && estat_actual_ != TitleState::STARFIELD) {
|
||||
if (current_state_ != TitleState::STARFIELD_FADE_IN && current_state_ != TitleState::STARFIELD) {
|
||||
return;
|
||||
}
|
||||
if (!checkSkipButtonPressed()) {
|
||||
return;
|
||||
}
|
||||
estat_actual_ = TitleState::MAIN;
|
||||
current_state_ = TitleState::MAIN;
|
||||
starfield_->setBrightness(BRIGHTNESS_STARFIELD);
|
||||
|
||||
// Saltar la intro coreografiada: deixar tots els elements ja in-place.
|
||||
@@ -445,7 +445,7 @@ void TitleScene::handleSkipInput() {
|
||||
S::LOGO_ENTRY_DURATION + S::COPYRIGHT_STAGGER + S::COPYRIGHT_ENTRY_DURATION);
|
||||
constexpr float T_PRESS_START_VISIBLE = T_FOOTER_END + S::SHIPS_DELAY_AFTER_FOOTER +
|
||||
Sh::ENTRY_DURATION + Sh::P2_ENTRY_DELAY + S::PRESS_START_DELAY_AFTER_SHIPS;
|
||||
temps_estat_main_ = T_PRESS_START_VISIBLE;
|
||||
state_time_main_ = T_PRESS_START_VISIBLE;
|
||||
intro_logo_progress_ = 1.0F;
|
||||
intro_jailgames_progress_ = 1.0F;
|
||||
intro_copyright_progress_ = 1.0F;
|
||||
@@ -458,7 +458,7 @@ void TitleScene::handleSkipInput() {
|
||||
}
|
||||
|
||||
void TitleScene::handleStartInput() {
|
||||
if (estat_actual_ != TitleState::MAIN) {
|
||||
if (current_state_ != TitleState::MAIN) {
|
||||
return;
|
||||
}
|
||||
// No acceptar START fins que la intro coreografiada haja conclòs i el
|
||||
@@ -466,8 +466,8 @@ void TitleScene::handleStartInput() {
|
||||
if (!press_start_visible_) {
|
||||
return;
|
||||
}
|
||||
const bool P1_ABANS = match_config_.jugador1_actiu;
|
||||
const bool P2_ABANS = match_config_.jugador2_actiu;
|
||||
const bool P1_ABANS = match_config_.player1_active;
|
||||
const bool P2_ABANS = match_config_.player2_active;
|
||||
|
||||
if (!checkStartGameButtonPressed()) {
|
||||
return;
|
||||
@@ -479,7 +479,7 @@ void TitleScene::handleStartInput() {
|
||||
}
|
||||
|
||||
context_.setMatchConfig(match_config_);
|
||||
estat_actual_ = TitleState::PLAYER_JOIN_PHASE;
|
||||
current_state_ = TitleState::PLAYER_JOIN_PHASE;
|
||||
temps_acumulat_ = 0.0F;
|
||||
|
||||
triggerExitForJoinedPlayers(P1_ABANS, P2_ABANS, "");
|
||||
@@ -492,55 +492,55 @@ void TitleScene::triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_act
|
||||
if (ship_animator_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (match_config_.jugador1_actiu && !p1_was_active) {
|
||||
if (match_config_.player1_active && !p1_was_active) {
|
||||
ship_animator_->triggerExitAnimationForPlayer(1);
|
||||
std::cout << "[TitleScene] P1 " << log_prefix << "ship exiting\n";
|
||||
}
|
||||
if (match_config_.jugador2_actiu && !p2_was_active) {
|
||||
if (match_config_.player2_active && !p2_was_active) {
|
||||
ship_animator_->triggerExitAnimationForPlayer(2);
|
||||
std::cout << "[TitleScene] P2 " << log_prefix << "ship exiting\n";
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene::updateLogoAnimation(float delta_time) {
|
||||
if (!animacio_activa_) {
|
||||
if (!animation_active_) {
|
||||
return;
|
||||
}
|
||||
temps_animacio_ += delta_time * factor_lerp_;
|
||||
animation_time_ += delta_time * lerp_factor_;
|
||||
|
||||
const float TWO_PI = 2.0F * Defaults::Math::PI;
|
||||
const float OFFSET_X = ORBIT_AMPLITUDE_X * std::sin(TWO_PI * ORBIT_FREQUENCY_X * temps_animacio_);
|
||||
const float OFFSET_Y = ORBIT_AMPLITUDE_Y * std::sin((TWO_PI * ORBIT_FREQUENCY_Y * temps_animacio_) + ORBIT_PHASE_OFFSET);
|
||||
const float OFFSET_X = ORBIT_AMPLITUDE_X * std::sin(TWO_PI * ORBIT_FREQUENCY_X * animation_time_);
|
||||
const float OFFSET_Y = ORBIT_AMPLITUDE_Y * std::sin((TWO_PI * ORBIT_FREQUENCY_Y * animation_time_) + ORBIT_PHASE_OFFSET);
|
||||
|
||||
for (std::size_t i = 0; i < lletres_orni_.size(); ++i) {
|
||||
lletres_orni_[i].position.x = posicions_originals_orni_[i].x + std::round(OFFSET_X);
|
||||
lletres_orni_[i].position.y = posicions_originals_orni_[i].y + std::round(OFFSET_Y);
|
||||
for (std::size_t i = 0; i < letters_orni_.size(); ++i) {
|
||||
letters_orni_[i].position.x = original_positions_orni_[i].x + std::round(OFFSET_X);
|
||||
letters_orni_[i].position.y = original_positions_orni_[i].y + std::round(OFFSET_Y);
|
||||
}
|
||||
for (std::size_t i = 0; i < lletres_attack_.size(); ++i) {
|
||||
lletres_attack_[i].position.x = posicions_originals_attack_[i].x + std::round(OFFSET_X);
|
||||
lletres_attack_[i].position.y = posicions_originals_attack_[i].y + std::round(OFFSET_Y);
|
||||
for (std::size_t i = 0; i < letters_attack_.size(); ++i) {
|
||||
letters_attack_[i].position.x = original_positions_attack_[i].x + std::round(OFFSET_X);
|
||||
letters_attack_[i].position.y = original_positions_attack_[i].y + std::round(OFFSET_Y);
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene::draw() {
|
||||
if (starfield_ && estat_actual_ != TitleState::BLACK_SCREEN) {
|
||||
if (starfield_ && current_state_ != TitleState::BLACK_SCREEN) {
|
||||
starfield_->draw();
|
||||
}
|
||||
|
||||
if (ship_animator_ &&
|
||||
(estat_actual_ == TitleState::STARFIELD_FADE_IN ||
|
||||
estat_actual_ == TitleState::STARFIELD ||
|
||||
estat_actual_ == TitleState::MAIN ||
|
||||
estat_actual_ == TitleState::PLAYER_JOIN_PHASE)) {
|
||||
(current_state_ == TitleState::STARFIELD_FADE_IN ||
|
||||
current_state_ == TitleState::STARFIELD ||
|
||||
current_state_ == TitleState::MAIN ||
|
||||
current_state_ == TitleState::PLAYER_JOIN_PHASE)) {
|
||||
ship_animator_->draw();
|
||||
}
|
||||
drawFlashes();
|
||||
|
||||
if (estat_actual_ == TitleState::STARFIELD_FADE_IN || estat_actual_ == TitleState::STARFIELD) {
|
||||
if (current_state_ == TitleState::STARFIELD_FADE_IN || current_state_ == TitleState::STARFIELD) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (estat_actual_ != TitleState::MAIN && estat_actual_ != TitleState::PLAYER_JOIN_PHASE) {
|
||||
if (current_state_ != TitleState::MAIN && current_state_ != TitleState::PLAYER_JOIN_PHASE) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -552,52 +552,52 @@ void TitleScene::draw() {
|
||||
const float SCREEN_CENTRE_Y = Defaults::Game::HEIGHT / 2.0F;
|
||||
const float LOGO_RENDER_SCALE = Defaults::Title::Layout::LOGO_SCALE * LOGO_S;
|
||||
|
||||
if (animacio_activa_) {
|
||||
float temps_shadow = std::max(0.0F, temps_animacio_ - SHADOW_DELAY);
|
||||
if (animation_active_) {
|
||||
float temps_shadow = std::max(0.0F, animation_time_ - SHADOW_DELAY);
|
||||
const float TWO_PI = 2.0F * Defaults::Math::PI;
|
||||
const float SHADOW_OX = (ORBIT_AMPLITUDE_X * std::sin(TWO_PI * ORBIT_FREQUENCY_X * temps_shadow)) + SHADOW_OFFSET_X;
|
||||
const float SHADOW_OY = (ORBIT_AMPLITUDE_Y * std::sin((TWO_PI * ORBIT_FREQUENCY_Y * temps_shadow) + ORBIT_PHASE_OFFSET)) + SHADOW_OFFSET_Y;
|
||||
|
||||
for (std::size_t i = 0; i < lletres_orni_.size(); ++i) {
|
||||
const float BASE_X = posicions_originals_orni_[i].x + std::round(SHADOW_OX);
|
||||
const float BASE_Y = posicions_originals_orni_[i].y + std::round(SHADOW_OY);
|
||||
for (std::size_t i = 0; i < letters_orni_.size(); ++i) {
|
||||
const float BASE_X = original_positions_orni_[i].x + std::round(SHADOW_OX);
|
||||
const float BASE_Y = original_positions_orni_[i].y + std::round(SHADOW_OY);
|
||||
const Vec2 POS_SHADOW{
|
||||
.x = SCREEN_CENTRE_X + (LOGO_S * (BASE_X - SCREEN_CENTRE_X)),
|
||||
.y = SCREEN_CENTRE_Y + (LOGO_S * (BASE_Y - SCREEN_CENTRE_Y)),
|
||||
};
|
||||
Rendering::renderShape(sdl_.getRenderer(), lletres_orni_[i].shape, POS_SHADOW, 0.0F, LOGO_RENDER_SCALE, 1.0F, SHADOW_BRIGHTNESS, Defaults::Title::Colors::LOGO_SHADOW);
|
||||
Rendering::renderShape(sdl_.getRenderer(), letters_orni_[i].shape, POS_SHADOW, 0.0F, LOGO_RENDER_SCALE, 1.0F, SHADOW_BRIGHTNESS, Defaults::Title::Colors::LOGO_SHADOW);
|
||||
}
|
||||
for (std::size_t i = 0; i < lletres_attack_.size(); ++i) {
|
||||
const float BASE_X = posicions_originals_attack_[i].x + std::round(SHADOW_OX);
|
||||
const float BASE_Y = posicions_originals_attack_[i].y + std::round(SHADOW_OY);
|
||||
for (std::size_t i = 0; i < letters_attack_.size(); ++i) {
|
||||
const float BASE_X = original_positions_attack_[i].x + std::round(SHADOW_OX);
|
||||
const float BASE_Y = original_positions_attack_[i].y + std::round(SHADOW_OY);
|
||||
const Vec2 POS_SHADOW{
|
||||
.x = SCREEN_CENTRE_X + (LOGO_S * (BASE_X - SCREEN_CENTRE_X)),
|
||||
.y = SCREEN_CENTRE_Y + (LOGO_S * (BASE_Y - SCREEN_CENTRE_Y)),
|
||||
};
|
||||
Rendering::renderShape(sdl_.getRenderer(), lletres_attack_[i].shape, POS_SHADOW, 0.0F, LOGO_RENDER_SCALE, 1.0F, SHADOW_BRIGHTNESS, Defaults::Title::Colors::LOGO_SHADOW);
|
||||
Rendering::renderShape(sdl_.getRenderer(), letters_attack_[i].shape, POS_SHADOW, 0.0F, LOGO_RENDER_SCALE, 1.0F, SHADOW_BRIGHTNESS, Defaults::Title::Colors::LOGO_SHADOW);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& lletra : lletres_orni_) {
|
||||
for (const auto& letter : letters_orni_) {
|
||||
const Vec2 POS{
|
||||
.x = SCREEN_CENTRE_X + (LOGO_S * (lletra.position.x - SCREEN_CENTRE_X)),
|
||||
.y = SCREEN_CENTRE_Y + (LOGO_S * (lletra.position.y - SCREEN_CENTRE_Y)),
|
||||
.x = SCREEN_CENTRE_X + (LOGO_S * (letter.position.x - SCREEN_CENTRE_X)),
|
||||
.y = SCREEN_CENTRE_Y + (LOGO_S * (letter.position.y - SCREEN_CENTRE_Y)),
|
||||
};
|
||||
Rendering::renderShape(sdl_.getRenderer(), lletra.shape, POS, 0.0F, LOGO_RENDER_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::LOGO_MAIN);
|
||||
Rendering::renderShape(sdl_.getRenderer(), letter.shape, POS, 0.0F, LOGO_RENDER_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::LOGO_MAIN);
|
||||
}
|
||||
for (const auto& lletra : lletres_attack_) {
|
||||
for (const auto& letter : letters_attack_) {
|
||||
const Vec2 POS{
|
||||
.x = SCREEN_CENTRE_X + (LOGO_S * (lletra.position.x - SCREEN_CENTRE_X)),
|
||||
.y = SCREEN_CENTRE_Y + (LOGO_S * (lletra.position.y - SCREEN_CENTRE_Y)),
|
||||
.x = SCREEN_CENTRE_X + (LOGO_S * (letter.position.x - SCREEN_CENTRE_X)),
|
||||
.y = SCREEN_CENTRE_Y + (LOGO_S * (letter.position.y - SCREEN_CENTRE_Y)),
|
||||
};
|
||||
Rendering::renderShape(sdl_.getRenderer(), lletra.shape, POS, 0.0F, LOGO_RENDER_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::LOGO_MAIN);
|
||||
Rendering::renderShape(sdl_.getRenderer(), letter.shape, POS, 0.0F, LOGO_RENDER_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::LOGO_MAIN);
|
||||
}
|
||||
|
||||
const float SPACING = Defaults::Title::Layout::TEXT_SPACING;
|
||||
|
||||
if (press_start_visible_) {
|
||||
bool mostrar_text = true;
|
||||
if (estat_actual_ == TitleState::PLAYER_JOIN_PHASE) {
|
||||
if (current_state_ == TitleState::PLAYER_JOIN_PHASE) {
|
||||
const float FASE = temps_acumulat_ * BLINK_FREQUENCY * 2.0F * std::numbers::pi_v<float>;
|
||||
mostrar_text = (std::sin(FASE) > 0.0F);
|
||||
}
|
||||
@@ -622,14 +622,14 @@ auto TitleScene::checkStartGameButtonPressed() -> bool {
|
||||
bool any_pressed = false;
|
||||
for (auto action : START_GAME_BUTTONS) {
|
||||
if (input->checkActionPlayer1(action, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
if (!match_config_.jugador1_actiu) {
|
||||
match_config_.jugador1_actiu = true;
|
||||
if (!match_config_.player1_active) {
|
||||
match_config_.player1_active = true;
|
||||
any_pressed = true;
|
||||
}
|
||||
}
|
||||
if (input->checkActionPlayer2(action, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
if (!match_config_.jugador2_actiu) {
|
||||
match_config_.jugador2_actiu = true;
|
||||
if (!match_config_.player2_active) {
|
||||
match_config_.player2_active = true;
|
||||
any_pressed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,12 +48,12 @@ class TitleScene final : public Scene {
|
||||
BLACK_SCREEN,
|
||||
};
|
||||
|
||||
struct LetraLogo {
|
||||
struct LogoLetter {
|
||||
std::shared_ptr<Graphics::Shape> shape;
|
||||
Vec2 position;
|
||||
float ancho;
|
||||
float altura;
|
||||
float offset_centre;
|
||||
float width;
|
||||
float height;
|
||||
float center_offset;
|
||||
};
|
||||
|
||||
SDLManager& sdl_;
|
||||
@@ -77,22 +77,22 @@ class TitleScene final : public Scene {
|
||||
void triggerFlash(Vec2 pos);
|
||||
void updateFlashes(float delta_time);
|
||||
void drawFlashes();
|
||||
TitleState estat_actual_{TitleState::STARFIELD_FADE_IN};
|
||||
TitleState current_state_{TitleState::STARFIELD_FADE_IN};
|
||||
float temps_acumulat_{0.0F};
|
||||
|
||||
std::vector<LetraLogo> lletres_orni_;
|
||||
std::vector<LetraLogo> lletres_attack_;
|
||||
float y_attack_dinamica_{0.0F};
|
||||
std::vector<LogoLetter> letters_orni_;
|
||||
std::vector<LogoLetter> letters_attack_;
|
||||
float dynamic_attack_y_{0.0F};
|
||||
|
||||
std::vector<LetraLogo> lletres_jailgames_;
|
||||
std::vector<LogoLetter> letters_jailgames_;
|
||||
|
||||
float temps_animacio_{0.0F};
|
||||
std::vector<Vec2> posicions_originals_orni_;
|
||||
std::vector<Vec2> posicions_originals_attack_;
|
||||
float animation_time_{0.0F};
|
||||
std::vector<Vec2> original_positions_orni_;
|
||||
std::vector<Vec2> original_positions_attack_;
|
||||
|
||||
float temps_estat_main_{0.0F};
|
||||
bool animacio_activa_{false};
|
||||
float factor_lerp_{0.0F};
|
||||
float state_time_main_{0.0F};
|
||||
bool animation_active_{false};
|
||||
float lerp_factor_{0.0F};
|
||||
|
||||
// Progresos de la intro coreografiada al state MAIN.
|
||||
float intro_logo_progress_{0.0F};
|
||||
@@ -102,12 +102,12 @@ class TitleScene final : public Scene {
|
||||
bool ships_intro_launched_{false};
|
||||
|
||||
static constexpr float BRIGHTNESS_STARFIELD = 1.2F;
|
||||
static constexpr float DURACIO_FADE_IN = 3.0F;
|
||||
static constexpr float DURACIO_INIT = 4.0F;
|
||||
static constexpr float DURACIO_TRANSITION = 2.5F;
|
||||
static constexpr float ESPAI_ENTRE_LLETRES = 10.0F;
|
||||
static constexpr float DURATION_FADE_IN = 3.0F;
|
||||
static constexpr float DURATION_INIT = 4.0F;
|
||||
static constexpr float DURATION_TRANSITION = 2.5F;
|
||||
static constexpr float LETTER_SPACING = 10.0F;
|
||||
static constexpr float BLINK_FREQUENCY = 3.0F;
|
||||
static constexpr float DURACIO_BLACK_SCREEN = 2.0F;
|
||||
static constexpr float DURATION_BLACK_SCREEN = 2.0F;
|
||||
static constexpr int MUSIC_FADE = 1500;
|
||||
|
||||
static constexpr float ORBIT_AMPLITUDE_X = 4.0F;
|
||||
@@ -121,7 +121,7 @@ class TitleScene final : public Scene {
|
||||
static constexpr float SHADOW_OFFSET_X = 2.0F;
|
||||
static constexpr float SHADOW_OFFSET_Y = 2.0F;
|
||||
|
||||
static constexpr float DURACIO_LERP = 2.0F;
|
||||
static constexpr float DURATION_LERP = 2.0F;
|
||||
|
||||
// Càmera 3D: FOV vertical en radians.
|
||||
static constexpr float CAMERA_FOV_Y_RAD = 1.0472F; // 60°
|
||||
|
||||
@@ -16,152 +16,152 @@
|
||||
|
||||
namespace StageSystem {
|
||||
|
||||
SpawnController::SpawnController() = default;
|
||||
SpawnController::SpawnController() = default;
|
||||
|
||||
void SpawnController::configure(const StageConfig* config) {
|
||||
config_ = config;
|
||||
}
|
||||
|
||||
void SpawnController::start() {
|
||||
if (config_ == nullptr) {
|
||||
std::cerr << "[SpawnController] Error: config_ es null" << '\n';
|
||||
return;
|
||||
void SpawnController::configure(const StageConfig* config) {
|
||||
config_ = config;
|
||||
}
|
||||
|
||||
reset();
|
||||
generateSpawnEvents();
|
||||
|
||||
std::cout << "[SpawnController] Stage " << static_cast<int>(config_->stage_id)
|
||||
<< ": generats " << spawn_queue_.size() << " spawn events" << '\n';
|
||||
}
|
||||
|
||||
void SpawnController::reset() {
|
||||
spawn_queue_.clear();
|
||||
temps_transcorregut_ = 0.0F;
|
||||
index_spawn_actual_ = 0;
|
||||
}
|
||||
|
||||
void SpawnController::update(float delta_time, std::array<Enemy, 15>& orni_array, bool pausar) {
|
||||
if ((config_ == nullptr) || spawn_queue_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Increment timer only when not paused
|
||||
if (!pausar) {
|
||||
temps_transcorregut_ += delta_time;
|
||||
}
|
||||
|
||||
// Process spawn events
|
||||
while (index_spawn_actual_ < spawn_queue_.size()) {
|
||||
SpawnEvent& event = spawn_queue_[index_spawn_actual_];
|
||||
|
||||
if (event.spawnejat) {
|
||||
index_spawn_actual_++;
|
||||
continue;
|
||||
void SpawnController::start() {
|
||||
if (config_ == nullptr) {
|
||||
std::cerr << "[SpawnController] Error: config_ es null" << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
if (temps_transcorregut_ >= event.temps_spawn) {
|
||||
// Find first inactive enemy
|
||||
for (auto& enemy : orni_array) {
|
||||
if (!enemy.isActive()) {
|
||||
spawnEnemy(enemy, event.type, ship_position_);
|
||||
event.spawnejat = true;
|
||||
index_spawn_actual_++;
|
||||
reset();
|
||||
generateSpawnEvents();
|
||||
|
||||
std::cout << "[SpawnController] Stage " << static_cast<int>(config_->stage_id)
|
||||
<< ": generats " << spawn_queue_.size() << " spawn events" << '\n';
|
||||
}
|
||||
|
||||
void SpawnController::reset() {
|
||||
spawn_queue_.clear();
|
||||
temps_transcorregut_ = 0.0F;
|
||||
index_spawn_actual_ = 0;
|
||||
}
|
||||
|
||||
void SpawnController::update(float delta_time, std::array<Enemy, 15>& orni_array, bool pausar) {
|
||||
if ((config_ == nullptr) || spawn_queue_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Increment timer only when not paused
|
||||
if (!pausar) {
|
||||
temps_transcorregut_ += delta_time;
|
||||
}
|
||||
|
||||
// Process spawn events
|
||||
while (index_spawn_actual_ < spawn_queue_.size()) {
|
||||
SpawnEvent& event = spawn_queue_[index_spawn_actual_];
|
||||
|
||||
if (event.spawnejat) {
|
||||
index_spawn_actual_++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (temps_transcorregut_ >= event.temps_spawn) {
|
||||
// Find first inactive enemy
|
||||
for (auto& enemy : orni_array) {
|
||||
if (!enemy.isActive()) {
|
||||
spawnEnemy(enemy, event.type, ship_position_);
|
||||
event.spawnejat = true;
|
||||
index_spawn_actual_++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no slot available, try next frame
|
||||
if (!event.spawnejat) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no slot available, try next frame
|
||||
if (!event.spawnejat) {
|
||||
} else {
|
||||
// Not yet time for this spawn
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Not yet time for this spawn
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto SpawnController::allEnemiesSpawned() const -> bool {
|
||||
return index_spawn_actual_ >= spawn_queue_.size();
|
||||
}
|
||||
|
||||
auto SpawnController::allEnemiesDestroyed(const std::array<Enemy, 15>& orni_array) const -> bool {
|
||||
if (!allEnemiesSpawned()) {
|
||||
return false;
|
||||
auto SpawnController::allEnemiesSpawned() const -> bool {
|
||||
return index_spawn_actual_ >= spawn_queue_.size();
|
||||
}
|
||||
return std::ranges::all_of(orni_array, [](const Enemy& enemy) { return !enemy.isActive(); });
|
||||
}
|
||||
|
||||
auto SpawnController::getAliveEnemyCount(const std::array<Enemy, 15>& orni_array) -> uint8_t {
|
||||
uint8_t count = 0;
|
||||
for (const auto& enemy : orni_array) {
|
||||
if (enemy.isActive()) {
|
||||
count++;
|
||||
auto SpawnController::allEnemiesDestroyed(const std::array<Enemy, 15>& orni_array) const -> bool {
|
||||
if (!allEnemiesSpawned()) {
|
||||
return false;
|
||||
}
|
||||
return std::ranges::all_of(orni_array, [](const Enemy& enemy) { return !enemy.isActive(); });
|
||||
}
|
||||
|
||||
auto SpawnController::getAliveEnemyCount(const std::array<Enemy, 15>& orni_array) -> uint8_t {
|
||||
uint8_t count = 0;
|
||||
for (const auto& enemy : orni_array) {
|
||||
if (enemy.isActive()) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
auto SpawnController::countSpawnedEnemies() const -> uint8_t {
|
||||
return static_cast<uint8_t>(index_spawn_actual_);
|
||||
}
|
||||
|
||||
void SpawnController::generateSpawnEvents() {
|
||||
if (config_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < config_->total_enemies; i++) {
|
||||
float spawn_time = config_->config_spawn.delay_inicial +
|
||||
(i * config_->config_spawn.interval_spawn);
|
||||
|
||||
EnemyType type = selectRandomType();
|
||||
|
||||
spawn_queue_.push_back({spawn_time, type, false});
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
auto SpawnController::countSpawnedEnemies() const -> uint8_t {
|
||||
return static_cast<uint8_t>(index_spawn_actual_);
|
||||
}
|
||||
auto SpawnController::selectRandomType() const -> EnemyType {
|
||||
if (config_ == nullptr) {
|
||||
return EnemyType::PENTAGON;
|
||||
}
|
||||
|
||||
void SpawnController::generateSpawnEvents() {
|
||||
if (config_ == nullptr) {
|
||||
return;
|
||||
// Weighted random selection based on distribution
|
||||
int rand_val = std::rand() % 100;
|
||||
|
||||
if (std::cmp_less(rand_val, config_->distribucio.pentagon)) {
|
||||
return EnemyType::PENTAGON;
|
||||
}
|
||||
if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado) {
|
||||
return EnemyType::SQUARE;
|
||||
}
|
||||
return EnemyType::PINWHEEL;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < config_->total_enemies; i++) {
|
||||
float spawn_time = config_->config_spawn.delay_inicial +
|
||||
(i * config_->config_spawn.interval_spawn);
|
||||
void SpawnController::spawnEnemy(Enemy& enemy, EnemyType type, const Vec2* ship_pos) {
|
||||
// Initialize enemy (with safe spawn if ship_pos provided)
|
||||
enemy.init(type, ship_pos);
|
||||
|
||||
EnemyType type = selectRandomType();
|
||||
|
||||
spawn_queue_.push_back({spawn_time, type, false});
|
||||
}
|
||||
}
|
||||
|
||||
auto SpawnController::selectRandomType() const -> EnemyType {
|
||||
if (config_ == nullptr) {
|
||||
return EnemyType::PENTAGON;
|
||||
// Apply difficulty multipliers
|
||||
applyMultipliers(enemy);
|
||||
}
|
||||
|
||||
// Weighted random selection based on distribution
|
||||
int rand_val = std::rand() % 100;
|
||||
void SpawnController::applyMultipliers(Enemy& enemy) const {
|
||||
if (config_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (std::cmp_less(rand_val, config_->distribucio.pentagon)) {
|
||||
return EnemyType::PENTAGON;
|
||||
// Apply velocity multiplier
|
||||
float base_vel = enemy.getBaseVelocity();
|
||||
enemy.setVelocity(base_vel * config_->multiplicadors.velocity);
|
||||
|
||||
// Apply rotation multiplier
|
||||
float base_rot = enemy.getBaseRotation();
|
||||
enemy.setRotation(base_rot * config_->multiplicadors.rotation);
|
||||
|
||||
// Apply tracking strength (only affects SQUARE)
|
||||
enemy.setTrackingStrength(config_->multiplicadors.tracking_strength);
|
||||
}
|
||||
if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado) {
|
||||
return EnemyType::QUADRAT;
|
||||
}
|
||||
return EnemyType::MOLINILLO;
|
||||
}
|
||||
|
||||
void SpawnController::spawnEnemy(Enemy& enemy, EnemyType type, const Vec2* ship_pos) {
|
||||
// Initialize enemy (with safe spawn if ship_pos provided)
|
||||
enemy.init(type, ship_pos);
|
||||
|
||||
// Apply difficulty multipliers
|
||||
applyMultipliers(enemy);
|
||||
}
|
||||
|
||||
void SpawnController::applyMultipliers(Enemy& enemy) const {
|
||||
if (config_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply velocity multiplier
|
||||
float base_vel = enemy.getBaseVelocity();
|
||||
enemy.setVelocity(base_vel * config_->multiplicadors.velocity);
|
||||
|
||||
// Apply rotation multiplier
|
||||
float base_rot = enemy.getBaseRotation();
|
||||
enemy.setRotation(base_rot * config_->multiplicadors.rotation);
|
||||
|
||||
// Apply tracking strength (only affects QUADRAT)
|
||||
enemy.setTrackingStrength(config_->multiplicadors.tracking_strength);
|
||||
}
|
||||
|
||||
} // namespace StageSystem
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace StageSystem {
|
||||
struct MultiplicadorsDificultat {
|
||||
float velocity; // 0.5-2.0 típic
|
||||
float rotation; // 0.5-2.0 típic
|
||||
float tracking_strength; // 0.0-1.5 (aplicat a Cuadrado)
|
||||
float tracking_strength; // 0.0-1.5 (aplicat a Square)
|
||||
};
|
||||
|
||||
// Metadades del file YAML
|
||||
|
||||
@@ -20,10 +20,10 @@ namespace Systems::Collision {
|
||||
switch (type) {
|
||||
case EnemyType::PENTAGON:
|
||||
return Defaults::Enemies::Scoring::PENTAGON_SCORE;
|
||||
case EnemyType::QUADRAT:
|
||||
return Defaults::Enemies::Scoring::QUADRAT_SCORE;
|
||||
case EnemyType::MOLINILLO:
|
||||
return Defaults::Enemies::Scoring::MOLINILLO_SCORE;
|
||||
case EnemyType::SQUARE:
|
||||
return Defaults::Enemies::Scoring::SQUARE_SCORE;
|
||||
case EnemyType::PINWHEEL:
|
||||
return Defaults::Enemies::Scoring::PINWHEEL_SCORE;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -32,10 +32,10 @@ namespace Systems::Collision {
|
||||
switch (type) {
|
||||
case EnemyType::PENTAGON:
|
||||
return Defaults::Palette::PENTAGON;
|
||||
case EnemyType::QUADRAT:
|
||||
return Defaults::Palette::QUADRAT;
|
||||
case EnemyType::MOLINILLO:
|
||||
return Defaults::Palette::MOLINILLO;
|
||||
case EnemyType::SQUARE:
|
||||
return Defaults::Palette::SQUARE;
|
||||
case EnemyType::PINWHEEL:
|
||||
return Defaults::Palette::PINWHEEL;
|
||||
}
|
||||
return SDL_Color{};
|
||||
}
|
||||
@@ -58,16 +58,16 @@ namespace Systems::Collision {
|
||||
}
|
||||
ctx.floating_score_manager.crear(POINTS, ENEMY_POS);
|
||||
|
||||
enemy.destruir();
|
||||
enemy.destroy();
|
||||
|
||||
constexpr float VELOCITAT_EXPLOSIO = 80.0F; // px/s (explosión suave)
|
||||
constexpr float SPEED_EXPLOSIO = 80.0F; // px/s (explosión suave)
|
||||
const Vec2 INHERITED_VEL = ENEMY_VEL * Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE;
|
||||
ctx.debris_manager.explode(
|
||||
SHAPE,
|
||||
ENEMY_POS,
|
||||
0.0F, // angle (rotación interna del enemy)
|
||||
1.0F, // escala
|
||||
VELOCITAT_EXPLOSIO,
|
||||
SPEED_EXPLOSIO,
|
||||
BRIGHTNESS,
|
||||
INHERITED_VEL,
|
||||
0.0F, // sense herència angular: evita que els 5 trossos curvin en bloc
|
||||
@@ -141,7 +141,7 @@ namespace Systems::Collision {
|
||||
explodeNow(ctx, enemy, SHOOTER);
|
||||
} else {
|
||||
// Primer impacto → entra en estado herido (explosión diferida).
|
||||
enemy.herir(SHOOTER);
|
||||
enemy.hurt(SHOOTER);
|
||||
}
|
||||
|
||||
breakBullet(ctx.debris_manager, bullet);
|
||||
@@ -182,9 +182,9 @@ namespace Systems::Collision {
|
||||
}
|
||||
// El sano queda herido, propagando el shooter original.
|
||||
if (A_WOUNDED) {
|
||||
b.herir(a.getLastHitBy());
|
||||
b.hurt(a.getLastHitBy());
|
||||
} else {
|
||||
a.herir(b.getLastHitBy());
|
||||
a.hurt(b.getLastHitBy());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,7 +230,7 @@ namespace Systems::Collision {
|
||||
} else {
|
||||
// Primer impacte → estat HURT (rebot físic ja resolt per PhysicsWorld;
|
||||
// l'enemic no rep dany per decisió de disseny).
|
||||
ctx.ships[i].herir();
|
||||
ctx.ships[i].hurt();
|
||||
}
|
||||
}
|
||||
ctx.ships[i].setTouchingEnemyPrevFrame(TOUCHING_NOW);
|
||||
@@ -262,8 +262,8 @@ namespace Systems::Collision {
|
||||
continue;
|
||||
}
|
||||
const bool JUGADOR_ACTIU = (player_id == 0)
|
||||
? ctx.match_config.jugador1_actiu
|
||||
: ctx.match_config.jugador2_actiu;
|
||||
? ctx.match_config.player1_active
|
||||
: ctx.match_config.player2_active;
|
||||
if (!JUGADOR_ACTIU) {
|
||||
continue;
|
||||
}
|
||||
@@ -297,7 +297,7 @@ namespace Systems::Collision {
|
||||
}
|
||||
|
||||
void desactivateOutOfBoundsBullets(
|
||||
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BALES) * 2>& bullets,
|
||||
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS) * 2>& bullets,
|
||||
Effects::DebrisManager& debris_manager) {
|
||||
float min_x;
|
||||
float max_x;
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Systems::Collision {
|
||||
struct Context {
|
||||
std::array<Ship, 2>& ships;
|
||||
std::array<Enemy, Defaults::Entities::MAX_ORNIS>& enemies;
|
||||
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BALES) * 2>& bullets;
|
||||
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS) * 2>& bullets;
|
||||
std::array<float, 2>& hit_timer_per_player;
|
||||
std::array<int, 2>& score_per_player;
|
||||
std::array<int, 2>& lives_per_player;
|
||||
@@ -74,7 +74,7 @@ namespace Systems::Collision {
|
||||
// (8 fragments de l'octàgon) i el so HIT. Cal cridar-la després de detectAll()
|
||||
// perquè una bala que el mateix frame xoca i alhora surt es comptabilitzi com a impacte.
|
||||
void desactivateOutOfBoundsBullets(
|
||||
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BALES) * 2>& bullets,
|
||||
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS) * 2>& bullets,
|
||||
Effects::DebrisManager& debris_manager);
|
||||
|
||||
} // namespace Systems::Collision
|
||||
|
||||
@@ -11,99 +11,99 @@
|
||||
#include "game/scenes/game_scene.hpp" // GameOverState (definición completa)
|
||||
|
||||
namespace Systems::ContinueScreen {
|
||||
namespace {
|
||||
namespace {
|
||||
|
||||
// Si el countdown ha bajado de 0, transiciona a GAME_OVER con su timer.
|
||||
void checkAndApplyTimeout(Context& ctx) {
|
||||
if (ctx.counter < 0) {
|
||||
ctx.state = GameOverState::GAME_OVER;
|
||||
ctx.game_over_timer = Defaults::Game::GAME_OVER_DURATION;
|
||||
}
|
||||
}
|
||||
// Si el countdown ha bajado de 0, transiciona a GAME_OVER con su timer.
|
||||
void checkAndApplyTimeout(Context& ctx) {
|
||||
if (ctx.counter < 0) {
|
||||
ctx.state = GameOverState::GAME_OVER;
|
||||
ctx.game_over_timer = Defaults::Game::GAME_OVER_DURATION;
|
||||
}
|
||||
}
|
||||
|
||||
void revivePlayer(Context& ctx, uint8_t player_id) {
|
||||
ctx.score_per_player[player_id] = 0;
|
||||
ctx.lives_per_player[player_id] = Defaults::Game::STARTING_LIVES;
|
||||
ctx.hit_timer_per_player[player_id] = 0.0F;
|
||||
void revivePlayer(Context& ctx, uint8_t player_id) {
|
||||
ctx.score_per_player[player_id] = 0;
|
||||
ctx.lives_per_player[player_id] = Defaults::Game::STARTING_LIVES;
|
||||
ctx.hit_timer_per_player[player_id] = 0.0F;
|
||||
|
||||
if (player_id == 0) {
|
||||
ctx.match_config.jugador1_actiu = true;
|
||||
} else {
|
||||
ctx.match_config.jugador2_actiu = true;
|
||||
}
|
||||
if (player_id == 0) {
|
||||
ctx.match_config.player1_active = true;
|
||||
} else {
|
||||
ctx.match_config.player2_active = true;
|
||||
}
|
||||
|
||||
const Vec2 SPAWN = ctx.get_spawn_point(player_id);
|
||||
ctx.ships[player_id].init(&SPAWN, /*activar_invulnerabilitat=*/true);
|
||||
}
|
||||
const Vec2 SPAWN = ctx.get_spawn_point(player_id);
|
||||
ctx.ships[player_id].init(&SPAWN, /*activar_invulnerabilitat=*/true);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace
|
||||
|
||||
void update(Context& ctx, float delta_time) {
|
||||
ctx.tick_timer -= delta_time;
|
||||
if (ctx.tick_timer > 0.0F) {
|
||||
return;
|
||||
}
|
||||
ctx.counter--;
|
||||
ctx.tick_timer = Defaults::Game::CONTINUE_TICK_DURATION;
|
||||
|
||||
checkAndApplyTimeout(ctx);
|
||||
|
||||
// Solo pita el tick si seguimos en CONTINUE (no transitamos a GAME_OVER)
|
||||
if (ctx.state == GameOverState::CONTINUE) {
|
||||
Audio::get()->playSound(Defaults::Sound::CONTINUE, Audio::Group::GAME);
|
||||
}
|
||||
}
|
||||
|
||||
void processInput(Context& ctx) {
|
||||
auto* input = Input::get();
|
||||
|
||||
const bool P1_START = input->checkActionPlayer1(InputAction::START, Input::DO_NOT_ALLOW_REPEAT);
|
||||
const bool P2_START = input->checkActionPlayer2(InputAction::START, Input::DO_NOT_ALLOW_REPEAT);
|
||||
|
||||
if (P1_START || P2_START) {
|
||||
// ¿Quedan continues?
|
||||
if (!Defaults::Game::INFINITE_CONTINUES &&
|
||||
ctx.continues_used >= Defaults::Game::MAX_CONTINUES) {
|
||||
ctx.state = GameOverState::GAME_OVER;
|
||||
ctx.game_over_timer = Defaults::Game::GAME_OVER_DURATION;
|
||||
void update(Context& ctx, float delta_time) {
|
||||
ctx.tick_timer -= delta_time;
|
||||
if (ctx.tick_timer > 0.0F) {
|
||||
return;
|
||||
}
|
||||
if (!Defaults::Game::INFINITE_CONTINUES) {
|
||||
ctx.continues_used++;
|
||||
}
|
||||
|
||||
const uint8_t PRIMARY = P1_START ? 0 : 1;
|
||||
revivePlayer(ctx, PRIMARY);
|
||||
// Si ambos pulsan START, revivimos a los dos.
|
||||
if (P1_START && P2_START) {
|
||||
revivePlayer(ctx, 1);
|
||||
}
|
||||
|
||||
// Reanudar partida.
|
||||
ctx.state = GameOverState::NONE;
|
||||
ctx.counter = 0;
|
||||
ctx.tick_timer = 0.0F;
|
||||
|
||||
Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME);
|
||||
return;
|
||||
}
|
||||
|
||||
// THRUST/SHOOT acelera el countdown manualmente.
|
||||
const bool ACCEL = input->checkActionPlayer1(InputAction::THRUST, Input::DO_NOT_ALLOW_REPEAT) ||
|
||||
input->checkActionPlayer2(InputAction::THRUST, Input::DO_NOT_ALLOW_REPEAT) ||
|
||||
input->checkActionPlayer1(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT) ||
|
||||
input->checkActionPlayer2(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT);
|
||||
|
||||
if (ACCEL) {
|
||||
ctx.counter--;
|
||||
ctx.tick_timer = Defaults::Game::CONTINUE_TICK_DURATION;
|
||||
|
||||
checkAndApplyTimeout(ctx);
|
||||
|
||||
// Solo pita el tick si seguimos en CONTINUE (no transitamos a GAME_OVER)
|
||||
if (ctx.state == GameOverState::CONTINUE) {
|
||||
Audio::get()->playSound(Defaults::Sound::CONTINUE, Audio::Group::GAME);
|
||||
}
|
||||
|
||||
ctx.tick_timer = Defaults::Game::CONTINUE_TICK_DURATION;
|
||||
}
|
||||
}
|
||||
|
||||
void processInput(Context& ctx) {
|
||||
auto* input = Input::get();
|
||||
|
||||
const bool P1_START = input->checkActionPlayer1(InputAction::START, Input::DO_NOT_ALLOW_REPEAT);
|
||||
const bool P2_START = input->checkActionPlayer2(InputAction::START, Input::DO_NOT_ALLOW_REPEAT);
|
||||
|
||||
if (P1_START || P2_START) {
|
||||
// ¿Quedan continues?
|
||||
if (!Defaults::Game::INFINITE_CONTINUES &&
|
||||
ctx.continues_used >= Defaults::Game::MAX_CONTINUES) {
|
||||
ctx.state = GameOverState::GAME_OVER;
|
||||
ctx.game_over_timer = Defaults::Game::GAME_OVER_DURATION;
|
||||
return;
|
||||
}
|
||||
if (!Defaults::Game::INFINITE_CONTINUES) {
|
||||
ctx.continues_used++;
|
||||
}
|
||||
|
||||
const uint8_t PRIMARY = P1_START ? 0 : 1;
|
||||
revivePlayer(ctx, PRIMARY);
|
||||
// Si ambos pulsan START, revivimos a los dos.
|
||||
if (P1_START && P2_START) {
|
||||
revivePlayer(ctx, 1);
|
||||
}
|
||||
|
||||
// Reanudar partida.
|
||||
ctx.state = GameOverState::NONE;
|
||||
ctx.counter = 0;
|
||||
ctx.tick_timer = 0.0F;
|
||||
|
||||
Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME);
|
||||
return;
|
||||
}
|
||||
|
||||
// THRUST/SHOOT acelera el countdown manualmente.
|
||||
const bool ACCEL = input->checkActionPlayer1(InputAction::THRUST, Input::DO_NOT_ALLOW_REPEAT) ||
|
||||
input->checkActionPlayer2(InputAction::THRUST, Input::DO_NOT_ALLOW_REPEAT) ||
|
||||
input->checkActionPlayer1(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT) ||
|
||||
input->checkActionPlayer2(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT);
|
||||
|
||||
if (ACCEL) {
|
||||
ctx.counter--;
|
||||
checkAndApplyTimeout(ctx);
|
||||
|
||||
if (ctx.state == GameOverState::CONTINUE) {
|
||||
Audio::get()->playSound(Defaults::Sound::CONTINUE, Audio::Group::GAME);
|
||||
}
|
||||
|
||||
ctx.tick_timer = Defaults::Game::CONTINUE_TICK_DURATION;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Systems::ContinueScreen
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Systems::InitHud {
|
||||
auto computeShipPosition(float progress, const Vec2& final_position) -> Vec2 {
|
||||
const float EASED = Easing::easeOutQuad(progress);
|
||||
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
||||
// Y inicial: bajo la zona de juego (sale desde fuera).
|
||||
// Y inicial: bajo la zone de juego (sale desde fuera).
|
||||
const float Y_INI = zone.y + zone.h + Defaults::Hud::InitAnim::SHIP_SPAWN_Y_OFFSET;
|
||||
const float Y_ANIM = Y_INI + ((final_position.y - Y_INI) * EASED);
|
||||
return Vec2{.x = final_position.x, .y = Y_ANIM};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Cubre la animación INIT_HUD del comienzo de cada partida/stage:
|
||||
// 1. Crecimiento de los marcos del PLAYAREA con efecto pincel en 3 fases.
|
||||
// 2. Marcador subiendo desde abajo.
|
||||
// 3. Naves entrando desde la zona inferior hacia su spawn.
|
||||
// 3. Naves entrando desde la zone inferior hacia su spawn.
|
||||
//
|
||||
// Todas las funciones son puras (sin estado interno propio). GameScene aporta
|
||||
// el contexto que necesitan: posiciones finales, texto del scoreboard y el
|
||||
@@ -21,29 +21,29 @@
|
||||
|
||||
namespace Systems::InitHud {
|
||||
|
||||
// Convierte un progreso global 0..1 al sub-progreso de un elemento que solo
|
||||
// se anima en la ventana [ratio_init, ratio_end].
|
||||
// < ratio_init → 0.0 (no empezó)
|
||||
// > ratio_end → 1.0 (terminó)
|
||||
// en rango → interpolación lineal 0..1
|
||||
[[nodiscard]] auto computeRangeProgress(float global_progress,
|
||||
float ratio_init,
|
||||
float ratio_end) -> float;
|
||||
// Convierte un progreso global 0..1 al sub-progreso de un elemento que solo
|
||||
// se anima en la ventana [ratio_init, ratio_end].
|
||||
// < ratio_init → 0.0 (no empezó)
|
||||
// > ratio_end → 1.0 (terminó)
|
||||
// en rango → interpolación lineal 0..1
|
||||
[[nodiscard]] auto computeRangeProgress(float global_progress,
|
||||
float ratio_init,
|
||||
float ratio_end) -> float;
|
||||
|
||||
// Calcula posición Y animada de una nave durante INIT_HUD. La nave sube
|
||||
// desde 50 px bajo el PLAYAREA hasta `final_position` con easing.
|
||||
[[nodiscard]] auto computeShipPosition(float progress, const Vec2& final_position) -> Vec2;
|
||||
// Calcula posición Y animada de una nave durante INIT_HUD. La nave sube
|
||||
// desde 50 px bajo el PLAYAREA hasta `final_position` con easing.
|
||||
[[nodiscard]] auto computeShipPosition(float progress, const Vec2& final_position) -> Vec2;
|
||||
|
||||
// Dibuja los 4 lados del PLAYAREA con efecto pincel en 3 fases:
|
||||
// 0..33% → línea superior crece desde el centro hacia los lados.
|
||||
// 33..66% → líneas verticales bajan por los laterales.
|
||||
// 66..100% → línea inferior crece desde los lados hacia el centro.
|
||||
void drawBordersAnimated(Rendering::Renderer* renderer, float progress);
|
||||
// Dibuja los 4 lados del PLAYAREA con efecto pincel en 3 fases:
|
||||
// 0..33% → línea superior crece desde el centro hacia los lados.
|
||||
// 33..66% → líneas verticales bajan por los laterales.
|
||||
// 66..100% → línea inferior crece desde los lados hacia el centro.
|
||||
void drawBordersAnimated(Rendering::Renderer* renderer, float progress);
|
||||
|
||||
// Dibuja el scoreboard centrado, subiendo desde fuera de la pantalla
|
||||
// hasta su posición final con easing.
|
||||
void drawScoreboardAnimated(const Graphics::VectorText& text,
|
||||
const std::string& scoreboard_text,
|
||||
float progress);
|
||||
// Dibuja el scoreboard centrado, subiendo desde fuera de la pantalla
|
||||
// hasta su posición final con easing.
|
||||
void drawScoreboardAnimated(const Graphics::VectorText& text,
|
||||
const std::string& scoreboard_text,
|
||||
float progress);
|
||||
|
||||
} // namespace Systems::InitHud
|
||||
|
||||
Reference in New Issue
Block a user