4 Commits

Author SHA1 Message Date
a3aeed4b7c afegides musiques
afegit control de brillo al starfield
2025-12-03 19:27:36 +01:00
3b0354da54 afegit titol al TITOL 2025-12-03 17:40:27 +01:00
622ccd22bc afegits diferents enemics 2025-12-03 13:47:31 +01:00
1441134aea afegida colisió nau-enemics i game over 2025-12-03 12:04:44 +01:00
34 changed files with 1237 additions and 230 deletions

View File

@@ -1,7 +1,7 @@
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(orni VERSION 0.3.1)
project(orni VERSION 0.4.0)
# Info del proyecto
set(PROJECT_LONG_NAME "Orni Attack")

View File

@@ -318,7 +318,7 @@ endif
# Backup to remote server
backup:
@echo "Backing up project to maverick:/home/sergio/git-backup/asteroids..."
@echo "Backing up project to maverick:/home/sergio/git-backup/orni..."
rsync -a --delete \
--exclude='build/' \
--exclude='*.o' \
@@ -326,7 +326,7 @@ backup:
--exclude='orni' \
--exclude='orni_debug' \
--exclude='*_release/' \
$(DIR_ROOT) maverick:/home/sergio/git-backup/asteroids/
$(DIR_ROOT) maverick:/home/sergio/git-backup/orni/
@echo "Backup completed successfully"
# Help target

BIN
data/music/game.ogg Normal file

Binary file not shown.

BIN
data/music/title.ogg Normal file

Binary file not shown.

View File

@@ -0,0 +1,30 @@
# enemy_pinwheel.shp - ORNI enemic (molinillo de 4 triangles)
# © 2025 Port a C++20 amb SDL3
name: enemy_pinwheel
scale: 1.0
center: 0, 0
# Molinillo: 4 triangles, un en cada quadrant
# Cada triangle comparteix el centre (0,0) i té:
# - Un vèrtex en un eix (±20, 0) o (0, ±20)
# - Un vèrtex en la diagonal del quadrant (±14.14, ±14.14)
# - El tercer vèrtex al centre (0,0)
#
# Geometria:
# Triangle 1 (quadrant superior-dret): centre → eix dret → diagonal
# Triangle 2 (quadrant superior-esq): centre → eix superior → diagonal
# Triangle 3 (quadrant inferior-esq): centre → eix esquerre → diagonal
# Triangle 4 (quadrant inferior-dret): centre → eix inferior → diagonal
# Triangle 1: quadrant superior-dret
polyline: 0,0 20,0 14.14,-14.14 0,0
# Triangle 2: quadrant superior-esquerre
polyline: 0,0 0,-20 -14.14,-14.14 0,0
# Triangle 3: quadrant inferior-esquerre
polyline: 0,0 -20,0 -14.14,14.14 0,0
# Triangle 4: quadrant inferior-dret
polyline: 0,0 0,20 14.14,14.14 0,0

View File

@@ -0,0 +1,19 @@
# enemy_square.shp - ORNI enemic (quadrat regular)
# © 2025 Port a C++20 amb SDL3
name: enemy_square
scale: 1.0
center: 0, 0
# Quadrat regular radi=20 (circumscrit)
# 4 punts equidistants al voltant d'un cercle (90° entre ells)
# Començant a angle=-90° (amunt), rotant sentit horari
#
# Angles: -90°, 0°, 90°, 180°
# Conversió polar→cartesià (SDL: Y creix cap avall):
# angle=-90°: (0.00, -20.00)
# angle=0°: (20.00, 0.00)
# angle=90°: (0.00, 20.00)
# angle=180°: (-20.00, 0.00)
polyline: 0,-20 20,0 0,20 -20,0 0,-20

View File

@@ -0,0 +1,10 @@
# letra_a.shp
# Generado automáticamente desde jailgames.svg
# Dimensiones: 137.50 x 100.00 px
name: letra_a
scale: 1.0
center: 68.75, 50.00
polyline: 0.00,100.00 0.00,75.00 37.50,0.00 100.00,0.00 137.50,75.00 137.50,100.00 100.00,100.00 100.00,87.50 37.50,87.50 37.50,100.00 0.00,100.00
polyline: 62.50,25.00 50.00,50.00 50.00,62.50 87.50,62.50 87.50,50.00 75.00,25.00 62.50,25.00

View File

@@ -0,0 +1,9 @@
# letra_c.shp
# Generado automáticamente desde jailgames.svg
# Dimensiones: 137.50 x 100.00 px
name: letra_c
scale: 1.0
center: 68.75, 50.00
polyline: 12.50,100.00 0.00,87.50 0.00,12.50 12.50,0.00 125.00,0.00 137.50,12.50 137.50,37.50 100.00,37.50 100.00,25.00 37.50,25.00 37.50,75.00 100.00,75.00 100.00,62.50 137.50,62.50 137.50,87.50 125.00,100.00 12.50,100.00

View File

@@ -0,0 +1,10 @@
# letra_exclamacion.shp
# Generado automáticamente desde jailgames.svg
# Dimensiones: 37.51 x 100.00 px
name: letra_exclamacion
scale: 1.0
center: 18.75, 50.00
polyline: 0.00,62.50 0.00,0.00 37.51,0.00 37.51,62.50 0.00,62.50
polyline: 0.00,100.00 0.00,75.00 37.51,75.00 37.51,100.00 0.00,100.00

View File

@@ -0,0 +1,9 @@
# letra_i.shp
# Generado automáticamente desde jailgames.svg
# Dimensiones: 37.50 x 100.00 px
name: letra_i
scale: 1.0
center: 18.75, 50.00
polyline: 0.00,0.00 37.50,0.00 37.50,100.00 0.00,100.00 0.00,0.00

View File

@@ -0,0 +1,9 @@
# letra_k.shp
# Generado automáticamente desde jailgames.svg
# Dimensiones: 137.50 x 100.00 px
name: letra_k
scale: 1.0
center: 68.75, 50.00
polyline: 0.00,100.00 0.00,0.00 37.50,0.00 37.50,37.50 50.00,37.50 100.00,0.00 137.50,0.00 137.50,25.00 87.06,50.00 137.50,75.00 137.50,100.00 100.00,100.00 50.00,62.50 37.50,62.50 37.50,100.00 0.00,100.00

View File

@@ -0,0 +1,9 @@
# letra_n.shp
# Generado automáticamente desde jailgames.svg
# Dimensiones: 137.50 x 100.00 px
name: letra_n
scale: 1.0
center: 68.75, 50.00
polyline: 0.00,100.00 0.00,0.00 50.00,0.00 100.00,50.00 100.00,0.00 137.50,0.00 137.50,100.00 87.50,100.00 37.50,50.00 37.50,100.00 0.00,100.00

View File

@@ -0,0 +1,10 @@
# letra_o.shp
# Generado automáticamente desde jailgames.svg
# Dimensiones: 137.50 x 100.00 px
name: letra_o
scale: 1.0
center: 68.75, 50.00
polyline: 12.50,100.00 0.00,87.50 0.00,12.50 12.50,0.00 125.00,0.00 137.50,12.50 137.50,87.50 125.00,100.00 12.50,100.00
polyline: 100.00,25.00 37.50,25.00 37.50,75.00 100.00,75.00 100.00,25.00

View File

@@ -0,0 +1,10 @@
# letra_r.shp
# Generado automáticamente desde jailgames.svg
# Dimensiones: 137.50 x 100.00 px
name: letra_r
scale: 1.0
center: 68.75, 50.00
polyline: 0.00,100.00 0.00,0.00 125.00,0.00 137.50,12.50 137.50,62.50 125.00,62.50 137.50,75.00 137.50,100.00 100.00,100.00 100.00,75.00 37.50,75.00 37.50,100.00 0.00,100.00
polyline: 37.50,50.00 100.00,50.00 100.00,25.00 37.50,25.00 37.50,50.00

View File

@@ -0,0 +1,9 @@
# letra_t.shp
# Generado automáticamente desde jailgames.svg
# Dimensiones: 137.50 x 100.00 px
name: letra_t
scale: 1.0
center: 68.75, 50.00
polyline: 0.00,25.00 0.00,0.00 137.50,0.00 137.50,25.00 87.50,25.00 87.50,100.00 50.00,100.00 50.00,25.00 0.00,25.00

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 7875 4016" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.792849,0,0,0.792849,84.327,350.707)">
<path d="M896,1693L896,1531.23L1219.53,1531.23L1219.53,560.632L1543.07,560.632L1543.07,1531.23L1381.3,1531.23L1381.3,1693L896,1693Z" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:7.01px;"/>
<path d="M2028.37,1369.47L2028.37,1693L1704.83,1693L1704.83,722.399L1866.6,722.399L1866.6,560.632L2351.9,560.632L2351.9,722.399L2513.67,722.399L2513.67,1693L2190.14,1693L2190.14,1369.47L2028.37,1369.47ZM2028.37,722.399L2028.37,1207.7L2190.14,1207.7L2190.14,722.399L2028.37,722.399Z" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:7.01px;"/>
<rect x="2675.44" y="560.632" width="323.534" height="1132.37" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:7.01px;"/>
<path d="M3160.74,560.632L3484.27,560.632L3484.27,1531.23L3807.8,1531.23L3807.8,1693L3160.74,1693L3160.74,560.632Z" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:7.01px;"/>
<path d="M4131.34,560.632L4616.64,560.632L4616.64,722.399L4293.1,722.399L4293.1,1531.23L4454.87,1531.23L4454.87,1045.93L4778.4,1045.93L4778.4,1693L4131.34,1693L4131.34,1531.23L3969.57,1531.23L3969.57,722.399L4131.34,722.399L4131.34,560.632Z" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:7.01px;"/>
<path d="M5263.71,1369.47L5263.71,1693L4940.17,1693L4940.17,722.399L5101.94,722.399L5101.94,560.632L5587.24,560.632L5587.24,722.399L5749.01,722.399L5749.01,1693L5425.47,1693L5425.47,1369.47L5263.71,1369.47ZM5263.71,722.399L5263.71,1207.7L5425.47,1207.7L5425.47,722.399L5263.71,722.399Z" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:7.01px;"/>
<path d="M6719.61,1207.7L6557.84,1207.7L6557.84,1369.47L6396.07,1369.47L6396.07,1207.7L6234.31,1207.7L6234.31,1693L5910.77,1693L5910.77,560.632L6072.54,560.632L6072.54,722.399L6234.31,722.399L6234.31,884.166L6396.07,884.166L6396.07,1045.93L6557.84,1045.93L6557.84,884.166L6719.61,884.166L6719.61,722.399L6881.37,722.399L6881.37,560.632L7043.14,560.632L7043.14,1693L6719.61,1693L6719.61,1207.7Z" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:7.01px;"/>
<path d="M7851.98,884.166L7851.98,1045.93L7528.44,1045.93L7528.44,1531.23L8013.74,1531.23L8013.74,1693L7204.91,1693L7204.91,560.632L8013.74,560.632L8013.74,722.399L7528.44,722.399L7528.44,884.166L7851.98,884.166Z" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:7.01px;"/>
<path d="M8175.51,1531.23L8499.04,1531.23L8499.04,1207.7L8337.28,1207.7L8337.28,1045.93L8175.51,1045.93L8175.51,722.399L8337.28,722.399L8337.28,560.632L8822.58,560.632L8822.58,722.399L8499.04,722.399L8499.04,1045.93L8660.81,1045.93L8660.81,1207.7L8822.58,1207.7L8822.58,1531.23L8660.81,1531.23L8660.81,1693L8175.51,1693L8175.51,1531.23Z" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:7.01px;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -73,6 +73,14 @@ constexpr float ENEMY_RADIUS = 20.0f;
constexpr float BULLET_RADIUS = 5.0f;
} // namespace Entities
// Game rules (lives, respawn, game over)
namespace Game {
constexpr int STARTING_LIVES = 3; // Initial lives
constexpr float DEATH_DURATION = 3.0f; // Seconds of death animation
constexpr float GAME_OVER_DURATION = 5.0f; // Seconds to display game over
constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 0.80f; // 80% hitbox (generous)
} // namespace Game
// Física (valores actuales del juego, sincronizados con joc_asteroides.cpp)
namespace Physics {
constexpr float ROTATION_SPEED = 3.14f; // rad/s (~180°/s)
@@ -90,7 +98,8 @@ 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ó mínima (rad/s ~5.7°/s)
constexpr float ROTACIO_MAX = 0.3f; // Rotació màxima (rad/s ~17.2°/s)
constexpr float TEMPS_VIDA = 2.0f; // Duració màxima (segons)
constexpr float TEMPS_VIDA = 2.0f; // Duració màxima (segons) - enemy/bullet debris
constexpr float TEMPS_VIDA_NAU = 3.0f; // Ship debris lifetime (matches DEATH_DURATION)
constexpr float SHRINK_RATE = 0.5f; // Reducció de mida (factor/s)
} // namespace Debris
} // namespace Physics
@@ -164,4 +173,58 @@ constexpr bool ENABLED = true; // Sonidos habilitados
constexpr const char* EXPLOSION = "explosion.wav"; // Explosión
constexpr const char* LASER = "laser_shoot.wav"; // Disparo
} // namespace Sound
// Enemy type configuration (tipus d'enemics)
namespace Enemies {
// Pentagon (esquivador - zigzag evasion)
namespace Pentagon {
constexpr float VELOCITAT = 35.0f; // px/s (slightly slower)
constexpr float CANVI_ANGLE_PROB = 0.20f; // 20% per wall hit (frequent zigzag)
constexpr float CANVI_ANGLE_MAX = 1.0f; // Max random angle change (rad)
constexpr float DROTACIO_MIN = 0.5f; // Min visual rotation (rad/s)
constexpr float DROTACIO_MAX = 2.5f; // Max visual rotation (rad/s)
constexpr const char* SHAPE_FILE = "enemy_pentagon.shp";
} // namespace Pentagon
// Quadrat (perseguidor - tracks player)
namespace Quadrat {
constexpr float VELOCITAT = 40.0f; // px/s (medium speed)
constexpr float TRACKING_STRENGTH = 0.5f; // Interpolation toward player (0.0-1.0)
constexpr float TRACKING_INTERVAL = 1.0f; // Seconds between angle updates
constexpr float DROTACIO_MIN = 0.2f; // Slow rotation
constexpr float DROTACIO_MAX = 1.0f;
constexpr const char* SHAPE_FILE = "enemy_square.shp";
} // namespace Quadrat
// Molinillo (agressiu - fast straight lines, proximity spin-up)
namespace Molinillo {
constexpr float VELOCITAT = 50.0f; // px/s (fastest)
constexpr float CANVI_ANGLE_PROB = 0.05f; // 5% per wall hit (rare direction change)
constexpr float CANVI_ANGLE_MAX = 0.3f; // Small angle adjustments
constexpr float DROTACIO_MIN = 2.0f; // Base rotation (rad/s)
constexpr float DROTACIO_MAX = 4.0f;
constexpr float DROTACIO_PROXIMITY_MULTIPLIER = 3.0f; // Spin-up multiplier when near ship
constexpr float PROXIMITY_DISTANCE = 100.0f; // Distance threshold (px)
constexpr const char* SHAPE_FILE = "enemy_pinwheel.shp";
} // namespace Molinillo
// Animation parameters (shared)
namespace Animation {
// Palpitation
constexpr float PALPITACIO_TRIGGER_PROB = 0.01f; // 1% chance per second
constexpr float PALPITACIO_DURACIO_MIN = 1.0f; // Min duration (seconds)
constexpr float PALPITACIO_DURACIO_MAX = 3.0f; // Max duration (seconds)
constexpr float PALPITACIO_AMPLITUD_MIN = 0.08f; // Min scale variation
constexpr float PALPITACIO_AMPLITUD_MAX = 0.20f; // Max scale variation
constexpr float PALPITACIO_FREQ_MIN = 1.5f; // Min frequency (Hz)
constexpr float PALPITACIO_FREQ_MAX = 3.0f; // Max frequency (Hz)
// Rotation acceleration
constexpr float ROTACIO_ACCEL_TRIGGER_PROB = 0.005f; // 0.5% chance per second
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.5f; // Min speed multiplier
constexpr float ROTACIO_ACCEL_MULTIPLIER_MAX = 2.5f; // Max speed multiplier
} // namespace Animation
} // namespace Enemies
} // namespace Defaults

View File

@@ -17,10 +17,10 @@ Starfield::Starfield(SDL_Renderer* renderer,
const Punt& punt_fuga,
const SDL_FRect& area,
int densitat)
: renderer_(renderer)
, punt_fuga_(punt_fuga)
, area_(area)
, densitat_(densitat) {
: renderer_(renderer),
punt_fuga_(punt_fuga),
area_(area),
densitat_(densitat) {
// Carregar forma d'estrella
shape_estrella_ = std::make_shared<Shape>("data/shapes/star.shp");
@@ -84,9 +84,9 @@ void Starfield::inicialitzar_estrella(Estrella& estrella) {
// Verificar si una estrella està fora de l'àrea
bool Starfield::fora_area(const Estrella& estrella) const {
return (estrella.posicio.x < area_.x ||
estrella.posicio.x > area_.x + area_.w ||
estrella.posicio.y < area_.y ||
estrella.posicio.y > area_.y + area_.h);
estrella.posicio.x > area_.x + area_.w ||
estrella.posicio.y < area_.y ||
estrella.posicio.y > area_.y + area_.h);
}
// Calcular escala dinàmica segons distància del centre
@@ -96,16 +96,19 @@ float Starfield::calcular_escala(const Estrella& estrella) const {
// Interpolació lineal basada en distància del centre
// distancia_centre: 0.0 (centre) → 1.0 (vora)
return capa.escala_min +
(capa.escala_max - capa.escala_min) * estrella.distancia_centre;
(capa.escala_max - capa.escala_min) * estrella.distancia_centre;
}
// Calcular brightness dinàmica segons distància del centre
float Starfield::calcular_brightness(const Estrella& estrella) const {
// Interpolació lineal: estrelles properes (vora) més brillants
// distancia_centre: 0.0 (centre, llunyanes) → 1.0 (vora, properes)
return Defaults::Brightness::STARFIELD_MIN +
(Defaults::Brightness::STARFIELD_MAX - Defaults::Brightness::STARFIELD_MIN) *
estrella.distancia_centre;
float brightness_base = Defaults::Brightness::STARFIELD_MIN +
(Defaults::Brightness::STARFIELD_MAX - Defaults::Brightness::STARFIELD_MIN) *
estrella.distancia_centre;
// Aplicar multiplicador i limitar a 1.0
return std::min(1.0f, brightness_base * multiplicador_brightness_);
}
// Actualitzar posicions de les estrelles
@@ -135,6 +138,11 @@ void Starfield::actualitzar(float delta_time) {
}
}
// Establir multiplicador de brightness
void Starfield::set_brightness(float multiplier) {
multiplicador_brightness_ = std::max(0.0f, multiplier); // Evitar valors negatius
}
// Dibuixar totes les estrelles
void Starfield::dibuixar() {
if (!shape_estrella_->es_valida()) {

View File

@@ -15,66 +15,68 @@ namespace Graphics {
// Configuració per cada capa de profunditat
struct CapaConfig {
float velocitat_base; // Velocitat base d'aquesta capa (px/s)
float escala_min; // Escala mínima prop del centre
float escala_max; // Escala màxima al límit de pantalla
int num_estrelles; // Nombre d'estrelles en aquesta capa
float velocitat_base; // Velocitat base d'aquesta capa (px/s)
float escala_min; // Escala mínima prop del centre
float escala_max; // Escala màxima al límit de pantalla
int num_estrelles; // Nombre d'estrelles en aquesta capa
};
// Classe Starfield - camp d'estrelles animat amb efecte de profunditat
class Starfield {
public:
// Constructor
// - renderer: SDL renderer
// - punt_fuga: punt d'origen/fuga des d'on surten les estrelles
// - area: rectangle on actuen les estrelles (SDL_FRect)
// - densitat: nombre total d'estrelles (es divideix entre capes)
Starfield(SDL_Renderer* renderer,
const Punt& punt_fuga,
const SDL_FRect& area,
int densitat = 150);
public:
// Constructor
// - renderer: SDL renderer
// - punt_fuga: punt d'origen/fuga des d'on surten les estrelles
// - area: rectangle on actuen les estrelles (SDL_FRect)
// - densitat: nombre total d'estrelles (es divideix entre capes)
Starfield(SDL_Renderer* renderer,
const Punt& punt_fuga,
const SDL_FRect& area,
int densitat = 150);
// Actualitzar posicions de les estrelles
void actualitzar(float delta_time);
// Actualitzar posicions de les estrelles
void actualitzar(float delta_time);
// Dibuixar totes les estrelles
void dibuixar();
// Dibuixar totes les estrelles
void dibuixar();
// Setters per ajustar paràmetres en temps real
void set_punt_fuga(const Punt& punt) { punt_fuga_ = punt; }
// Setters per ajustar paràmetres en temps real
void set_punt_fuga(const Punt& punt) { punt_fuga_ = punt; }
void set_brightness(float multiplier);
private:
// Estructura interna per cada estrella
struct Estrella {
Punt posicio; // Posició actual
float angle; // Angle de moviment (radians)
float distancia_centre; // Distància normalitzada del centre (0.0-1.0)
int capa; // Índex de capa (0=lluny, 1=mitjà, 2=prop)
};
private:
// Estructura interna per cada estrella
struct Estrella {
Punt posicio; // Posició actual
float angle; // Angle de moviment (radians)
float distancia_centre; // Distància normalitzada del centre (0.0-1.0)
int capa; // Índex de capa (0=lluny, 1=mitjà, 2=prop)
};
// Inicialitzar una estrella (nova o regenerada)
void inicialitzar_estrella(Estrella& estrella);
// Inicialitzar una estrella (nova o regenerada)
void inicialitzar_estrella(Estrella& estrella);
// Verificar si una estrella està fora de l'àrea
bool fora_area(const Estrella& estrella) const;
// Verificar si una estrella està fora de l'àrea
bool fora_area(const Estrella& estrella) const;
// Calcular escala dinàmica segons distància del centre
float calcular_escala(const Estrella& estrella) const;
// Calcular escala dinàmica segons distància del centre
float calcular_escala(const Estrella& estrella) const;
// Calcular brightness dinàmica segons distància del centre
float calcular_brightness(const Estrella& estrella) const;
// Calcular brightness dinàmica segons distància del centre
float calcular_brightness(const Estrella& estrella) const;
// Dades
std::vector<Estrella> estrelles_;
std::vector<CapaConfig> capes_; // Configuració de les 3 capes
std::shared_ptr<Shape> shape_estrella_;
SDL_Renderer* renderer_;
// Dades
std::vector<Estrella> estrelles_;
std::vector<CapaConfig> capes_; // Configuració de les 3 capes
std::shared_ptr<Shape> shape_estrella_;
SDL_Renderer* renderer_;
// Configuració
Punt punt_fuga_; // Punt d'origen de les estrelles
SDL_FRect area_; // Àrea activa
float radi_max_; // Distància màxima del centre al límit de pantalla
int densitat_; // Nombre total d'estrelles
// Configuració
Punt punt_fuga_; // Punt d'origen de les estrelles
SDL_FRect area_; // Àrea activa
float radi_max_; // Distància màxima del centre al límit de pantalla
int densitat_; // Nombre total d'estrelles
float multiplicador_brightness_{1.0f}; // Multiplicador de brillantor (1.0 = default)
};
} // namespace Graphics

View File

@@ -58,8 +58,7 @@ bool linea(SDL_Renderer* renderer, int x1, int y1, int x2, int y2, bool dibuixar
SDL_SetRenderDrawColor(renderer, color_final.r, color_final.g, color_final.b, 255);
// Renderitzar amb coordenades físiques
SDL_RenderLine(renderer, static_cast<float>(px1), static_cast<float>(py1),
static_cast<float>(px2), static_cast<float>(py2));
SDL_RenderLine(renderer, static_cast<float>(px1), static_cast<float>(py1), static_cast<float>(px2), static_cast<float>(py2));
}
// Algorisme de Bresenham original (conservat per a futura detecció de

View File

@@ -7,13 +7,14 @@
#include <cstdlib>
#include <iostream>
#include "core/audio/audio.hpp"
#include "core/audio/audio_cache.hpp"
#include "core/defaults.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "game/escenes/escena_joc.hpp"
#include "game/escenes/escena_logo.hpp"
#include "game/escenes/escena_titol.hpp"
#include "game/options.hpp"
#include "core/audio/audio.hpp"
#include "core/defaults.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "gestor_escenes.hpp"
#include "project.h"
@@ -161,6 +162,13 @@ auto Director::run() -> int {
// Inicialitzar sistema d'audio
Audio::init();
// Precachejar música per evitar lag al començar
AudioCache::getMusic("title.ogg");
if (Options::console) {
std::cout << "Música precachejada: "
<< AudioCache::getMusicCacheSize() << " fitxers\n";
}
// Bucle principal de gestió d'escenes
while (GestorEscenes::actual != GestorEscenes::Escena::EIXIR) {
switch (GestorEscenes::actual) {

View File

@@ -3,9 +3,9 @@
#include "global_events.hpp"
#include "core/input/mouse.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "gestor_escenes.hpp"
#include "core/input/mouse.hpp"
namespace GlobalEvents {

View File

@@ -21,25 +21,53 @@ Enemic::Enemic(SDL_Renderer* renderer)
drotacio_(0.0f),
rotacio_(0.0f),
esta_(false),
brightness_(Defaults::Brightness::ENEMIC) {
// [NUEVO] Carregar forma compartida des de fitxer
forma_ = Graphics::ShapeLoader::load("enemy_pentagon.shp");
if (!forma_ || !forma_->es_valida()) {
std::cerr << "[Enemic] Error: no s'ha pogut carregar enemy_pentagon.shp"
<< std::endl;
}
brightness_(Defaults::Brightness::ENEMIC),
tipus_(TipusEnemic::PENTAGON),
tracking_timer_(0.0f),
ship_position_(nullptr) {
// [NUEVO] Forma es carrega a inicialitzar() segons el tipus
// Constructor no carrega forma per permetre tipus diferents
}
void Enemic::inicialitzar() {
// Inicialitzar enemic (pentàgon)
// Copiat de joc_asteroides.cpp línies 41-54
void Enemic::inicialitzar(TipusEnemic tipus) {
// Guardar tipus
tipus_ = tipus;
// [NUEVO] Ja no cal crear_poligon_regular - la geometria es carrega del
// fitxer Només inicialitzem l'estat de la instància
// Carregar forma segons el tipus
const char* shape_file;
float drotacio_min, drotacio_max;
switch (tipus_) {
case TipusEnemic::PENTAGON:
shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE;
velocitat_ = Defaults::Enemies::Pentagon::VELOCITAT;
drotacio_min = Defaults::Enemies::Pentagon::DROTACIO_MIN;
drotacio_max = Defaults::Enemies::Pentagon::DROTACIO_MAX;
break;
case TipusEnemic::QUADRAT:
shape_file = Defaults::Enemies::Quadrat::SHAPE_FILE;
velocitat_ = Defaults::Enemies::Quadrat::VELOCITAT;
drotacio_min = Defaults::Enemies::Quadrat::DROTACIO_MIN;
drotacio_max = Defaults::Enemies::Quadrat::DROTACIO_MAX;
tracking_timer_ = 0.0f;
break;
case TipusEnemic::MOLINILLO:
shape_file = Defaults::Enemies::Molinillo::SHAPE_FILE;
velocitat_ = Defaults::Enemies::Molinillo::VELOCITAT;
drotacio_min = Defaults::Enemies::Molinillo::DROTACIO_MIN;
drotacio_max = Defaults::Enemies::Molinillo::DROTACIO_MAX;
break;
}
// Carregar forma
forma_ = Graphics::ShapeLoader::load(shape_file);
if (!forma_ || !forma_->es_valida()) {
std::cerr << "[Enemic] Error: no s'ha pogut carregar " << shape_file << std::endl;
}
// Posició aleatòria dins de l'àrea de joc
// Calcular rangs segurs amb radi de l'enemic
float min_x, max_x, min_y, max_y;
Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS,
min_x,
@@ -47,7 +75,6 @@ void Enemic::inicialitzar() {
min_y,
max_y);
// Spawn aleatori dins dels límits segurs
int range_x = static_cast<int>(max_x - min_x);
int range_y = static_cast<int>(max_y - min_y);
centre_.x = static_cast<float>((std::rand() % range_x) + static_cast<int>(min_x));
@@ -56,14 +83,17 @@ void Enemic::inicialitzar() {
// Angle aleatori de moviment
angle_ = (std::rand() % 360) * Constants::PI / 180.0f;
// Velocitat (2 px/frame original * 20 FPS = 40 px/s)
velocitat_ = 40.0f;
// Rotació visual aleatòria (rad/s)
// Original Pascal: random * 0.1 rad/frame * 20 FPS ≈ 2 rad/s
drotacio_ = (static_cast<float>(std::rand()) / RAND_MAX) * 2.0f;
// Rotació visual aleatòria (rad/s) dins del rang del tipus
float drotacio_range = drotacio_max - drotacio_min;
drotacio_ = drotacio_min + (static_cast<float>(std::rand()) / RAND_MAX) * drotacio_range;
rotacio_ = 0.0f;
// Inicialitzar estat d'animació
animacio_ = AnimacioEnemic(); // Reset to defaults
animacio_.drotacio_base = drotacio_;
animacio_.drotacio_objetivo = drotacio_;
animacio_.drotacio_t = 1.0f; // Start without interpolating
// Activar
esta_ = true;
}
@@ -73,6 +103,9 @@ void Enemic::actualitzar(float delta_time) {
// Moviment autònom
mou(delta_time);
// Actualitzar animacions (palpitació, rotació accelerada)
actualitzar_animacio(delta_time);
// Rotació visual (time-based: drotacio_ està en rad/s)
rotacio_ += drotacio_ * delta_time;
}
@@ -80,21 +113,31 @@ void Enemic::actualitzar(float delta_time) {
void Enemic::dibuixar() const {
if (esta_ && forma_) {
// [NUEVO] Usar render_shape en lloc de rota_pol
Rendering::render_shape(renderer_, forma_, centre_, rotacio_, 1.0f, true, 1.0f, brightness_);
// [NUEVO] Usar render_shape amb escala animada
float escala = calcular_escala_actual();
Rendering::render_shape(renderer_, forma_, centre_, rotacio_, escala, true, 1.0f, brightness_);
}
}
void Enemic::mou(float delta_time) {
// Moviment autònom d'ORNI (enemic pentàgon)
// Basat EXACTAMENT en el codi Pascal original: ASTEROID.PAS lines 279-293
// Copiat EXACTAMENT de joc_asteroides.cpp línies 348-394
//
// IMPORTANT: El Pascal original NO té canvi aleatori continu!
// Només ajusta l'angle quan toca una paret.
// Dispatcher: crida el comportament específic segons el tipus
switch (tipus_) {
case TipusEnemic::PENTAGON:
comportament_pentagon(delta_time);
break;
case TipusEnemic::QUADRAT:
comportament_quadrat(delta_time);
break;
case TipusEnemic::MOLINILLO:
comportament_molinillo(delta_time);
break;
}
}
void Enemic::comportament_pentagon(float delta_time) {
// Pentagon: zigzag esquivador (frequent direction changes)
// Similar a comportament original però amb probabilitat més alta
// Calcular nova posició PROPUESTA (time-based, però lògica Pascal)
// velocitat_ ja està en px/s (40 px/s), multiplicar per delta_time
float velocitat_efectiva = velocitat_ * delta_time;
// Calcular desplaçament (angle-PI/2 perquè angle=0 apunta amunt)
@@ -104,7 +147,7 @@ void Enemic::mou(float delta_time) {
float new_y = centre_.y + dy;
float new_x = centre_.x + dx;
// Obtenir límits segurs compensant el radi de l'enemic
// Obtenir límits segurs
float min_x, max_x, min_y, max_y;
Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS,
min_x,
@@ -112,33 +155,242 @@ void Enemic::mou(float delta_time) {
min_y,
max_y);
// Lògica Pascal: Actualitza Y si dins, sinó ajusta angle aleatòriament
// if (dy>marge_dalt) and (dy<marge_baix) then orni.centre.y:=round(Dy)
// else orni.angle:=orni.angle+(random(256)/512)*(random(3)-1);
// CORRECCIÓ: Usar inequalitats inclusives (>= i <=) per evitar fugides
// Zigzag: canvi d'angle més freqüent en tocar límits
if (new_y >= min_y && new_y <= max_y) {
centre_.y = new_y;
} else {
// Pequeño ajuste aleatorio: (random(256)/512)*(random(3)-1)
// random(256) = 0..255, /512 = 0..0.498
// random(3) = 0,1,2, -1 = -1,0,1
// Resultado: ±0.5 rad aprox
float rand1 = (static_cast<float>(std::rand() % 256) / 512.0f);
int rand2 = (std::rand() % 3) - 1; // -1, 0, o 1
angle_ += rand1 * static_cast<float>(rand2);
// Probabilitat més alta de canvi d'angle
if (static_cast<float>(std::rand()) / RAND_MAX < Defaults::Enemies::Pentagon::CANVI_ANGLE_PROB) {
float rand_angle = (static_cast<float>(std::rand()) / RAND_MAX) *
Defaults::Enemies::Pentagon::CANVI_ANGLE_MAX;
angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle;
}
}
// Lògica Pascal: Actualitza X si dins, sinó ajusta angle aleatòriament
// if (dx>marge_esq) and (dx<marge_dret) then orni.centre.x:=round(Dx)
// else orni.angle:=orni.angle+(random(256)/512)*(random(3)-1);
// CORRECCIÓ: Usar inequalitats inclusives (>= i <=) per evitar fugides
if (new_x >= min_x && new_x <= max_x) {
centre_.x = new_x;
} else {
float rand1 = (static_cast<float>(std::rand() % 256) / 512.0f);
int rand2 = (std::rand() % 3) - 1;
angle_ += rand1 * static_cast<float>(rand2);
if (static_cast<float>(std::rand()) / RAND_MAX < Defaults::Enemies::Pentagon::CANVI_ANGLE_PROB) {
float rand_angle = (static_cast<float>(std::rand()) / RAND_MAX) *
Defaults::Enemies::Pentagon::CANVI_ANGLE_MAX;
angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle;
}
}
}
void Enemic::comportament_quadrat(float delta_time) {
// Quadrat: perseguidor (tracks player position)
// Update tracking timer
tracking_timer_ += delta_time;
// Periodically update angle toward ship
if (tracking_timer_ >= Defaults::Enemies::Quadrat::TRACKING_INTERVAL) {
tracking_timer_ = 0.0f;
if (ship_position_) {
// Calculate angle to ship
float dx = ship_position_->x - centre_.x;
float dy = ship_position_->y - centre_.y;
float target_angle = std::atan2(dy, dx) + Constants::PI / 2.0f;
// Interpolate toward target angle
float angle_diff = target_angle - angle_;
// Normalize angle difference to [-π, π]
while (angle_diff > Constants::PI) angle_diff -= 2.0f * Constants::PI;
while (angle_diff < -Constants::PI) angle_diff += 2.0f * Constants::PI;
// Apply tracking strength
angle_ += angle_diff * Defaults::Enemies::Quadrat::TRACKING_STRENGTH;
}
}
// Nota: La rotació visual (rotacio_ += drotacio_) ja es fa a actualitzar()
// Move in current direction
float velocitat_efectiva = velocitat_ * delta_time;
float dy = velocitat_efectiva * std::sin(angle_ - Constants::PI / 2.0f);
float dx = velocitat_efectiva * std::cos(angle_ - Constants::PI / 2.0f);
float new_y = centre_.y + dy;
float new_x = centre_.x + dx;
// Obtenir límits segurs
float min_x, max_x, min_y, max_y;
Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS,
min_x,
max_x,
min_y,
max_y);
// Bounce on walls (simple reflection)
if (new_y >= min_y && new_y <= max_y) {
centre_.y = new_y;
} else {
angle_ = -angle_; // Vertical reflection
}
if (new_x >= min_x && new_x <= max_x) {
centre_.x = new_x;
} else {
angle_ = Constants::PI - angle_; // Horizontal reflection
}
}
void Enemic::comportament_molinillo(float delta_time) {
// Molinillo: agressiu (fast, straight lines, proximity spin-up)
// Check proximity to ship for spin-up effect
if (ship_position_) {
float dx = ship_position_->x - centre_.x;
float dy = ship_position_->y - centre_.y;
float distance = std::sqrt(dx * dx + dy * dy);
if (distance < Defaults::Enemies::Molinillo::PROXIMITY_DISTANCE) {
// Temporarily boost rotation speed when near ship
float boost = Defaults::Enemies::Molinillo::DROTACIO_PROXIMITY_MULTIPLIER;
drotacio_ = animacio_.drotacio_base * boost;
} else {
// Normal rotation speed
drotacio_ = animacio_.drotacio_base;
}
}
// Fast straight-line movement
float velocitat_efectiva = velocitat_ * delta_time;
float dy = velocitat_efectiva * std::sin(angle_ - Constants::PI / 2.0f);
float dx = velocitat_efectiva * std::cos(angle_ - Constants::PI / 2.0f);
float new_y = centre_.y + dy;
float new_x = centre_.x + dx;
// Obtenir límits segurs
float min_x, max_x, min_y, max_y;
Constants::obtenir_limits_zona_segurs(Defaults::Entities::ENEMY_RADIUS,
min_x,
max_x,
min_y,
max_y);
// Rare angle changes on wall hits
if (new_y >= min_y && new_y <= max_y) {
centre_.y = new_y;
} else {
if (static_cast<float>(std::rand()) / RAND_MAX < Defaults::Enemies::Molinillo::CANVI_ANGLE_PROB) {
float rand_angle = (static_cast<float>(std::rand()) / RAND_MAX) *
Defaults::Enemies::Molinillo::CANVI_ANGLE_MAX;
angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle;
}
}
if (new_x >= min_x && new_x <= max_x) {
centre_.x = new_x;
} else {
if (static_cast<float>(std::rand()) / RAND_MAX < Defaults::Enemies::Molinillo::CANVI_ANGLE_PROB) {
float rand_angle = (static_cast<float>(std::rand()) / RAND_MAX) *
Defaults::Enemies::Molinillo::CANVI_ANGLE_MAX;
angle_ += (std::rand() % 2 == 0) ? rand_angle : -rand_angle;
}
}
}
void Enemic::actualitzar_animacio(float delta_time) {
actualitzar_palpitacio(delta_time);
actualitzar_rotacio_accelerada(delta_time);
}
void Enemic::actualitzar_palpitacio(float delta_time) {
if (animacio_.palpitacio_activa) {
// Advance phase (2π * frequency * dt)
animacio_.palpitacio_fase += 2.0f * Constants::PI * animacio_.palpitacio_frequencia * delta_time;
// Decrement timer
animacio_.palpitacio_temps_restant -= delta_time;
// Deactivate when timer expires
if (animacio_.palpitacio_temps_restant <= 0.0f) {
animacio_.palpitacio_activa = false;
}
} else {
// Random trigger (probability per second)
float rand_val = static_cast<float>(std::rand()) / RAND_MAX;
float trigger_prob = Defaults::Enemies::Animation::PALPITACIO_TRIGGER_PROB * delta_time;
if (rand_val < trigger_prob) {
// Activate palpitation
animacio_.palpitacio_activa = true;
animacio_.palpitacio_fase = 0.0f;
// Randomize parameters
float freq_range = Defaults::Enemies::Animation::PALPITACIO_FREQ_MAX -
Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN;
animacio_.palpitacio_frequencia = Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN +
(static_cast<float>(std::rand()) / RAND_MAX) * freq_range;
float amp_range = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MAX -
Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN;
animacio_.palpitacio_amplitud = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN +
(static_cast<float>(std::rand()) / RAND_MAX) * amp_range;
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 +
(static_cast<float>(std::rand()) / RAND_MAX) * dur_range;
}
}
}
void Enemic::actualitzar_rotacio_accelerada(float delta_time) {
if (animacio_.drotacio_t < 1.0f) {
// Transitioning to new target
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; // Reached target
drotacio_ = animacio_.drotacio_base;
} else {
// Smoothstep interpolation: t² * (3 - 2t)
float t = animacio_.drotacio_t;
float smooth_t = t * t * (3.0f - 2.0f * t);
// Interpolate between base and target
float initial = animacio_.drotacio_base;
float target = animacio_.drotacio_objetivo;
drotacio_ = initial + (target - initial) * smooth_t;
}
} else {
// Random trigger for new acceleration
float rand_val = static_cast<float>(std::rand()) / RAND_MAX;
float trigger_prob = Defaults::Enemies::Animation::ROTACIO_ACCEL_TRIGGER_PROB * delta_time;
if (rand_val < trigger_prob) {
// Start new transition
animacio_.drotacio_t = 0.0f;
// Randomize target speed (multiplier * base)
float mult_range = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MAX -
Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN;
float multiplier = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN +
(static_cast<float>(std::rand()) / RAND_MAX) * mult_range;
animacio_.drotacio_objetivo = animacio_.drotacio_base * multiplier;
// Randomize duration
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 +
(static_cast<float>(std::rand()) / RAND_MAX) * dur_range;
}
}
}
float Enemic::calcular_escala_actual() const {
float escala = 1.0f;
if (animacio_.palpitacio_activa) {
// Add pulsating scale variation
escala += animacio_.palpitacio_amplitud * std::sin(animacio_.palpitacio_fase);
}
return escala;
}

View File

@@ -10,13 +10,36 @@
#include "core/graphics/shape.hpp"
#include "core/types.hpp"
// Tipus d'enemic
enum class TipusEnemic : uint8_t {
PENTAGON = 0, // Pentàgon esquivador (zigzag)
QUADRAT = 1, // Quadrat perseguidor (tracks ship)
MOLINILLO = 2 // Molinillo agressiu (fast, spinning)
};
// Estat d'animació (palpitació i rotació accelerada)
struct AnimacioEnemic {
// Palpitation (breathing effect)
bool palpitacio_activa = false;
float palpitacio_fase = 0.0f; // Phase in cycle (0.0-2π)
float palpitacio_frequencia = 2.0f; // Hz (cycles per second)
float palpitacio_amplitud = 0.15f; // Scale variation (±15%)
float palpitacio_temps_restant = 0.0f; // Time remaining (seconds)
// Rotation acceleration (long-term spin modulation)
float drotacio_base = 0.0f; // Base rotation speed (rad/s)
float drotacio_objetivo = 0.0f; // Target rotation speed (rad/s)
float drotacio_t = 0.0f; // Interpolation progress (0.0-1.0)
float drotacio_duracio = 0.0f; // Duration of transition (seconds)
};
class Enemic {
public:
Enemic()
: renderer_(nullptr) {}
Enemic(SDL_Renderer* renderer);
void inicialitzar();
void inicialitzar(TipusEnemic tipus = TipusEnemic::PENTAGON);
void actualitzar(float delta_time);
void dibuixar() const;
@@ -26,6 +49,9 @@ class Enemic {
const std::shared_ptr<Graphics::Shape>& get_forma() const { return forma_; }
void destruir() { esta_ = false; }
// Set ship position reference for tracking behavior
void set_ship_position(const Punt* ship_pos) { ship_position_ = ship_pos; }
private:
SDL_Renderer* renderer_;
@@ -34,12 +60,32 @@ class Enemic {
// [NUEVO] Estat de la instància (separat de la geometria)
Punt centre_;
float angle_; // Angle de moviment
float angle_; // Angle de moviment
float velocitat_;
float drotacio_; // Delta rotació visual (rad/s)
float rotacio_; // Rotació visual acumulada
float drotacio_; // Delta rotació visual (rad/s)
float rotacio_; // Rotació visual acumulada
bool esta_;
float brightness_; // Factor de brillantor (0.0-1.0)
// [NEW] Enemy type and configuration
TipusEnemic tipus_;
// [NEW] Animation state
AnimacioEnemic animacio_;
// [NEW] Behavior state (type-specific)
float tracking_timer_; // For Quadrat: time since last angle update
const Punt* ship_position_; // Pointer to ship position (for tracking)
// [EXISTING] Private methods
void mou(float delta_time);
// [NEW] Private methods
void actualitzar_animacio(float delta_time);
void actualitzar_palpitacio(float delta_time);
void actualitzar_rotacio_accelerada(float delta_time);
void comportament_pentagon(float delta_time);
void comportament_quadrat(float delta_time);
void comportament_molinillo(float delta_time);
float calcular_escala_actual() const; // Returns scale with palpitation applied
};

View File

@@ -29,7 +29,7 @@ Nau::Nau(SDL_Renderer* renderer)
}
}
void Nau::inicialitzar() {
void Nau::inicialitzar(const Punt* spawn_point) {
// Inicialització de la nau (triangle)
// Basat en el codi Pascal original: lines 380-384
// Copiat de joc_asteroides.cpp línies 30-44
@@ -37,11 +37,17 @@ void Nau::inicialitzar() {
// [NUEVO] Ja no cal configurar punts polars - la geometria es carrega del
// fitxer Només inicialitzem l'estat de la instància
// Posició inicial al centre de l'àrea de joc
float centre_x, centre_y;
Constants::obtenir_centre_zona(centre_x, centre_y);
centre_.x = centre_x; // 320
centre_.y = centre_y; // 213 (not 240!)
// Use custom spawn point if provided, otherwise use center
if (spawn_point) {
centre_.x = spawn_point->x;
centre_.y = spawn_point->y;
} else {
// Default: center of play area
float centre_x, centre_y;
Constants::obtenir_centre_zona(centre_x, centre_y);
centre_.x = static_cast<int>(centre_x);
centre_.y = static_cast<int>(centre_y);
}
// Estat inicial
angle_ = 0.0f;

View File

@@ -16,7 +16,7 @@ class Nau {
: renderer_(nullptr) {}
Nau(SDL_Renderer* renderer);
void inicialitzar();
void inicialitzar(const Punt* spawn_point = nullptr);
void processar_input(float delta_time);
void actualitzar(float delta_time);
void dibuixar() const;
@@ -25,6 +25,7 @@ class Nau {
const Punt& get_centre() const { return centre_; }
float get_angle() const { return angle_; }
bool esta_viva() const { return !esta_tocada_; }
const std::shared_ptr<Graphics::Shape>& get_forma() const { return forma_; }
// Col·lisions (Fase 10)
void marcar_tocada() { esta_tocada_ = true; }
@@ -38,8 +39,8 @@ class Nau {
// [NUEVO] Estat de la instància (separat de la geometria)
Punt centre_;
float angle_; // Angle d'orientació
float velocitat_; // Velocitat (px/s)
float angle_; // Angle d'orientació
float velocitat_; // Velocitat (px/s)
bool esta_tocada_;
float brightness_; // Factor de brillantor (0.0-1.0)

View File

@@ -108,67 +108,201 @@ void EscenaJoc::inicialitzar() {
// Inicialitzar estat de col·lisió
itocado_ = 0;
// Initialize lives and game over state
num_vides_ = Defaults::Game::STARTING_LIVES;
game_over_ = false;
game_over_timer_ = 0.0f;
// Set spawn point to center of play area
Constants::obtenir_centre_zona(punt_spawn_.x, punt_spawn_.y);
// Inicialitzar nau
nau_.inicialitzar();
// Inicialitzar enemics (ORNIs)
// Inicialitzar enemics (ORNIs) amb tipus aleatoris
for (auto& enemy : orni_) {
enemy.inicialitzar();
// Random type distribution: ~40% Pentagon, ~30% Quadrat, ~30% Molinillo
int rand_val = std::rand() % 10;
TipusEnemic tipus;
if (rand_val < 4) {
tipus = TipusEnemic::PENTAGON;
} else if (rand_val < 7) {
tipus = TipusEnemic::QUADRAT;
} else {
tipus = TipusEnemic::MOLINILLO;
}
enemy.inicialitzar(tipus);
enemy.set_ship_position(&nau_.get_centre()); // Set ship reference for tracking
}
// Inicialitzar bales
for (auto& bala : bales_) {
bala.inicialitzar();
}
// Iniciar música de joc (sense stopMusic, ja s'ha parat en destructor de TITOL)
Audio::get()->playMusic("game.ogg");
}
void EscenaJoc::actualitzar(float delta_time) {
// Actualitzar nau (input + física)
// Check game over state first
if (game_over_) {
// Game over: only update timer, enemies, bullets, and debris
game_over_timer_ -= delta_time;
if (game_over_timer_ <= 0.0f) {
// Aturar música de joc abans de tornar al títol
Audio::get()->stopMusic();
// Auto-transition to title screen
GestorEscenes::actual = GestorEscenes::Escena::TITOL;
return;
}
// Enemies and bullets continue moving during game over
for (auto& enemy : orni_) {
enemy.actualitzar(delta_time);
}
for (auto& bala : bales_) {
bala.actualitzar(delta_time);
}
debris_manager_.actualitzar(delta_time);
return;
}
// Check death sequence state
if (itocado_ > 0.0f) {
// Death sequence active: update timer
itocado_ += delta_time;
// Check if death duration completed
if (itocado_ >= Defaults::Game::DEATH_DURATION) {
// *** PHASE 3: RESPAWN OR GAME OVER ***
// Decrement lives
num_vides_--;
if (num_vides_ > 0) {
// Respawn ship
nau_.inicialitzar(&punt_spawn_);
itocado_ = 0.0f;
} else {
// Game over
game_over_ = true;
game_over_timer_ = Defaults::Game::GAME_OVER_DURATION;
itocado_ = 0.0f;
}
}
// Enemies and bullets continue moving during death sequence
for (auto& enemy : orni_) {
enemy.actualitzar(delta_time);
}
for (auto& bala : bales_) {
bala.actualitzar(delta_time);
}
debris_manager_.actualitzar(delta_time);
return;
}
// *** NORMAL GAMEPLAY ***
// Update ship (input + physics)
nau_.processar_input(delta_time);
nau_.actualitzar(delta_time);
// Actualitzar moviment i rotació dels enemics (ORNIs)
// Update enemy movement and rotation
for (auto& enemy : orni_) {
enemy.actualitzar(delta_time);
}
// Actualitzar moviment de bales (Fase 9)
// Update bullet movement
for (auto& bala : bales_) {
bala.actualitzar(delta_time);
}
// Detectar col·lisions bala-enemic (Fase 10)
// Detect collisions
detectar_col·lisions_bales_enemics();
detectar_col·lisio_nau_enemics(); // New collision check
// Actualitzar fragments d'explosions
// Update debris
debris_manager_.actualitzar(delta_time);
}
void EscenaJoc::dibuixar() {
// Dibuixar marges de la zona de joc
// Draw borders (always visible)
dibuixar_marges();
// Dibuixar nau
nau_.dibuixar();
// Check game over state
if (game_over_) {
// Game over: draw enemies, bullets, debris, and "GAME OVER" text
// Dibuixar ORNIs (enemics)
for (const auto& enemy : orni_) {
enemy.dibuixar();
}
for (const auto& bala : bales_) {
bala.dibuixar();
}
debris_manager_.dibuixar();
// Draw centered "GAME OVER" text
const std::string game_over_text = "GAME OVER";
constexpr float escala = 2.0f;
constexpr float spacing = 4.0f;
float text_width = text_.get_text_width(game_over_text, escala, spacing);
float text_height = text_.get_text_height(escala);
const SDL_FRect& play_area = Defaults::Zones::PLAYAREA;
float x = play_area.x + (play_area.w - text_width) / 2.0f;
float y = play_area.y + (play_area.h - text_height) / 2.0f;
text_.render(game_over_text, {x, y}, escala, spacing);
dibuixar_marcador();
return;
}
// During death sequence, don't draw ship (debris draws automatically)
if (itocado_ == 0.0f) {
nau_.dibuixar();
}
// Draw enemies (always)
for (const auto& enemy : orni_) {
enemy.dibuixar();
}
// Dibuixar bales (Fase 9)
// Draw bullets (always)
for (const auto& bala : bales_) {
bala.dibuixar();
}
// Dibuixar fragments d'explosions (després d'altres objectes)
// Draw debris
debris_manager_.dibuixar();
// Dibuixar marcador
// Draw scoreboard
dibuixar_marcador();
}
void EscenaJoc::processar_input(const SDL_Event& event) {
// Ignore ship controls during game over
if (game_over_) {
return;
}
// Ignore ship controls during death sequence
if (itocado_ > 0.0f) {
return;
}
// Processament d'input per events puntuals (no continus)
// L'input continu (fletxes) es processa en actualitzar() amb
// SDL_GetKeyboardState()
@@ -217,7 +351,34 @@ void EscenaJoc::processar_input(const SDL_Event& event) {
}
void EscenaJoc::tocado() {
// TODO: Implementar seqüència de mort
// Death sequence: 3 phases
// Phase 1: First call (itocado_ == 0) - trigger explosion
// Phase 2: Animation (0 < itocado_ < 3.0s) - debris animation
// Phase 3: Respawn or game over (itocado_ >= 3.0s) - handled in actualitzar()
if (itocado_ == 0.0f) {
// *** PHASE 1: TRIGGER DEATH ***
// Mark ship as dead (stops rendering and input)
nau_.marcar_tocada();
// Create ship explosion
const Punt& ship_pos = nau_.get_centre();
float ship_angle = nau_.get_angle();
debris_manager_.explotar(
nau_.get_forma(), // Ship shape (3 lines)
ship_pos, // Center position
ship_angle, // Ship orientation
1.0f, // Normal scale
Defaults::Physics::Debris::VELOCITAT_BASE // 80 px/s
);
// Start death timer (non-zero to avoid re-triggering)
itocado_ = 0.001f;
}
// Phase 2 is automatic (debris updates in actualitzar())
// Phase 3 is handled in actualitzar() when itocado_ >= DEATH_DURATION
}
void EscenaJoc::dibuixar_marges() const {
@@ -238,8 +399,8 @@ void EscenaJoc::dibuixar_marges() const {
}
void EscenaJoc::dibuixar_marcador() {
// Text estàtic (hardcoded)
const std::string text = "SCORE: 01000 LIFE: 3 LEVEL: 01";
// Display actual lives count (user requested "LIFES" plural English)
std::string text = "SCORE: 01000 LIFES: " + std::to_string(num_vides_) + " LEVEL: 01";
// Paràmetres de renderització
const float escala = 0.85f;
@@ -316,3 +477,39 @@ void EscenaJoc::detectar_col·lisions_bales_enemics() {
}
}
}
void EscenaJoc::detectar_col·lisio_nau_enemics() {
// Only check collisions if ship is alive
if (!nau_.esta_viva()) {
return;
}
// Generous collision detection (80% hitbox)
constexpr float RADI_NAU = Defaults::Entities::SHIP_RADIUS;
constexpr float RADI_ENEMIC = Defaults::Entities::ENEMY_RADIUS;
constexpr float SUMA_RADIS =
(RADI_NAU + RADI_ENEMIC) * Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER;
constexpr float SUMA_RADIS_QUADRAT = SUMA_RADIS * SUMA_RADIS;
const Punt& pos_nau = nau_.get_centre();
// Check collision with all active enemies
for (const auto& enemic : orni_) {
if (!enemic.esta_actiu()) {
continue;
}
const Punt& pos_enemic = enemic.get_centre();
// Calculate squared distance (avoid sqrt)
float dx = static_cast<float>(pos_nau.x - pos_enemic.x);
float dy = static_cast<float>(pos_nau.y - pos_enemic.y);
float distancia_quadrada = dx * dx + dy * dy;
// Check collision
if (distancia_quadrada <= SUMA_RADIS_QUADRAT) {
tocado(); // Trigger death sequence
return; // Only one collision per frame
}
}
}

View File

@@ -10,14 +10,14 @@
#include <array>
#include <cstdint>
#include "core/graphics/vector_text.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/types.hpp"
#include "../constants.hpp"
#include "../effects/debris_manager.hpp"
#include "../entities/bala.hpp"
#include "../entities/enemic.hpp"
#include "../entities/nau.hpp"
#include "core/graphics/vector_text.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/types.hpp"
// Classe principal del joc (escena)
class EscenaJoc {
@@ -42,7 +42,13 @@ class EscenaJoc {
std::array<Enemic, Constants::MAX_ORNIS> orni_;
std::array<Bala, Constants::MAX_BALES> bales_;
Poligon chatarra_cosmica_;
uint16_t itocado_;
float itocado_; // Death timer (seconds)
// Lives and game over system
int num_vides_; // Current lives count
bool game_over_; // Game over state flag
float game_over_timer_; // Countdown timer for auto-return (seconds)
Punt punt_spawn_; // Configurable spawn point
// Text vectorial
Graphics::VectorText text_;
@@ -50,6 +56,7 @@ class EscenaJoc {
// Funcions privades
void tocado();
void detectar_col·lisions_bales_enemics(); // Col·lisions bala-enemic
void detectar_col·lisio_nau_enemics(); // Ship-enemy collision detection
void dibuixar_marges() const; // Dibuixar vores de la zona de joc
void dibuixar_marcador(); // Dibuixar marcador de puntuació
};

View File

@@ -200,6 +200,10 @@ void EscenaLogo::canviar_estat(EstatAnimacio nou_estat) {
std::mt19937 g(rd());
std::shuffle(ordre_explosio_.begin(), ordre_explosio_.end(), g);
}
else if (nou_estat == EstatAnimacio::POST_EXPLOSION)
{
Audio::get()->playMusic("title.ogg");
}
std::cout << "[EscenaLogo] Canvi a estat: " << static_cast<int>(nou_estat)
<< "\n";
@@ -288,6 +292,7 @@ void EscenaLogo::actualitzar(float delta_time) {
case EstatAnimacio::POST_EXPLOSION:
if (temps_estat_actual_ >= DURACIO_POST_EXPLOSION) {
// Iniciar música de títol abans de la transició
GestorEscenes::actual = GestorEscenes::Escena::TITOL;
}
break;

View File

@@ -9,11 +9,11 @@
#include <memory>
#include <vector>
#include "game/effects/debris_manager.hpp"
#include "core/defaults.hpp"
#include "core/graphics/shape.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/types.hpp"
#include "../effects/debris_manager.hpp"
#include "core/defaults.hpp"
class EscenaLogo {
public:

View File

@@ -3,15 +3,24 @@
#include "escena_titol.hpp"
#include <cfloat>
#include <cmath>
#include <iostream>
#include <string>
#include "core/audio/audio.hpp"
#include "core/graphics/shape_loader.hpp"
#include "core/input/mouse.hpp"
#include "core/rendering/shape_renderer.hpp"
#include "core/system/gestor_escenes.hpp"
#include "core/system/global_events.hpp"
#include "project.h"
namespace {
// Brightness del starfield (1.0 = default, >1.0 més brillant, <1.0 menys brillant)
constexpr float BRIGHTNESS_STARFIELD = 1.2f;
} // namespace
EscenaTitol::EscenaTitol(SDLManager& sdl)
: sdl_(sdl),
text_(sdl.obte_renderer()),
@@ -25,7 +34,8 @@ EscenaTitol::EscenaTitol(SDLManager& sdl)
Defaults::Game::HEIGHT / 2.0f};
SDL_FRect area_completa{
0, 0,
0,
0,
static_cast<float>(Defaults::Game::WIDTH),
static_cast<float>(Defaults::Game::HEIGHT)};
@@ -35,6 +45,159 @@ EscenaTitol::EscenaTitol(SDLManager& sdl)
area_completa,
150 // densitat: 150 estrelles (50 per capa)
);
// Configurar brightness del starfield
starfield_->set_brightness(BRIGHTNESS_STARFIELD);
// Inicialitzar lletres del títol "ORNI ATTACK!"
inicialitzar_titol();
// Iniciar música de títol si no està sonant
if (Audio::get()->getMusicState() != Audio::MusicState::PLAYING) {
Audio::get()->playMusic("title.ogg");
}
}
EscenaTitol::~EscenaTitol() {
// Aturar música de títol quan es destrueix l'escena
Audio::get()->stopMusic();
}
void EscenaTitol::inicialitzar_titol() {
using namespace Graphics;
// === LÍNIA 1: "ORNI" ===
std::vector<std::string> fitxers_orni = {
"title/letra_o.shp",
"title/letra_r.shp",
"title/letra_n.shp",
"title/letra_i.shp"};
// Pas 1: Carregar formes i calcular amplades per "ORNI"
float ancho_total_orni = 0.0f;
for (const auto& fitxer : fitxers_orni) {
auto forma = ShapeLoader::load(fitxer);
if (!forma || !forma->es_valida()) {
std::cerr << "[EscenaTitol] Error carregant " << fitxer << std::endl;
continue;
}
// Calcular bounding box de la forma (trobar ancho i altura)
float min_x = FLT_MAX;
float max_x = -FLT_MAX;
float min_y = FLT_MAX;
float max_y = -FLT_MAX;
for (const auto& prim : forma->get_primitives()) {
for (const auto& punt : prim.points) {
min_x = std::min(min_x, punt.x);
max_x = std::max(max_x, punt.x);
min_y = std::min(min_y, punt.y);
max_y = std::max(max_y, punt.y);
}
}
float ancho_sin_escalar = max_x - min_x;
float altura_sin_escalar = max_y - min_y;
// Escalar ancho, altura i offset amb ESCALA_TITULO
float ancho = ancho_sin_escalar * ESCALA_TITULO;
float altura = altura_sin_escalar * ESCALA_TITULO;
float offset_centre = (forma->get_centre().x - min_x) * ESCALA_TITULO;
lletres_orni_.push_back({forma, {0.0f, 0.0f}, ancho, altura, offset_centre});
ancho_total_orni += ancho;
}
// Afegir espaiat entre lletres
ancho_total_orni += ESPAI_ENTRE_LLETRES * (lletres_orni_.size() - 1);
// Calcular posició inicial (centrat horitzontal) per "ORNI"
float x_inicial_orni = (Defaults::Game::WIDTH - ancho_total_orni) / 2.0f;
float x_actual = x_inicial_orni;
for (auto& lletra : lletres_orni_) {
lletra.posicio.x = x_actual + lletra.offset_centre;
lletra.posicio.y = Y_ORNI;
x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES;
}
std::cout << "[EscenaTitol] Línia 1 (ORNI): " << lletres_orni_.size()
<< " lletres, ancho total: " << ancho_total_orni << " px\n";
// === Calcular posició Y dinàmica per "ATTACK!" ===
// Totes les lletres ORNI tenen la mateixa altura, utilitzem la primera
float altura_orni = lletres_orni_.empty() ? 50.0f : lletres_orni_[0].altura;
y_attack_dinamica_ = Y_ORNI + altura_orni + SEPARACION_LINEAS;
std::cout << "[EscenaTitol] Altura ORNI: " << altura_orni
<< " px, Y_ATTACK dinàmica: " << y_attack_dinamica_ << " px\n";
// === LÍNIA 2: "ATTACK!" ===
std::vector<std::string> fitxers_attack = {
"title/letra_a.shp",
"title/letra_t.shp",
"title/letra_t.shp", // T repetida
"title/letra_a.shp", // A repetida
"title/letra_c.shp",
"title/letra_k.shp",
"title/letra_exclamacion.shp"};
// Pas 1: Carregar formes i calcular amplades per "ATTACK!"
float ancho_total_attack = 0.0f;
for (const auto& fitxer : fitxers_attack) {
auto forma = ShapeLoader::load(fitxer);
if (!forma || !forma->es_valida()) {
std::cerr << "[EscenaTitol] Error carregant " << fitxer << std::endl;
continue;
}
// Calcular bounding box de la forma (trobar ancho i altura)
float min_x = FLT_MAX;
float max_x = -FLT_MAX;
float min_y = FLT_MAX;
float max_y = -FLT_MAX;
for (const auto& prim : forma->get_primitives()) {
for (const auto& punt : prim.points) {
min_x = std::min(min_x, punt.x);
max_x = std::max(max_x, punt.x);
min_y = std::min(min_y, punt.y);
max_y = std::max(max_y, punt.y);
}
}
float ancho_sin_escalar = max_x - min_x;
float altura_sin_escalar = max_y - min_y;
// Escalar ancho, altura i offset amb ESCALA_TITULO
float ancho = ancho_sin_escalar * ESCALA_TITULO;
float altura = altura_sin_escalar * ESCALA_TITULO;
float offset_centre = (forma->get_centre().x - min_x) * ESCALA_TITULO;
lletres_attack_.push_back({forma, {0.0f, 0.0f}, ancho, altura, offset_centre});
ancho_total_attack += ancho;
}
// Afegir espaiat entre lletres
ancho_total_attack += ESPAI_ENTRE_LLETRES * (lletres_attack_.size() - 1);
// Calcular posició inicial (centrat horitzontal) per "ATTACK!"
float x_inicial_attack = (Defaults::Game::WIDTH - ancho_total_attack) / 2.0f;
x_actual = x_inicial_attack;
for (auto& lletra : lletres_attack_) {
lletra.posicio.x = x_actual + lletra.offset_centre;
lletra.posicio.y = y_attack_dinamica_; // Usar posició dinàmica
x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES;
}
std::cout << "[EscenaTitol] Línia 2 (ATTACK!): " << lletres_attack_.size()
<< " lletres, ancho total: " << ancho_total_attack << " px\n";
}
void EscenaTitol::executar() {
@@ -112,9 +275,18 @@ void EscenaTitol::actualitzar(float delta_time) {
estat_actual_ = EstatTitol::MAIN;
}
break;
case EstatTitol::MAIN:
// No hi ha lògica d'actualització en l'estat MAIN
break;
case EstatTitol::TRANSITION:
temps_acumulat_ += delta_time;
if (temps_acumulat_ >= DURACIO_TRANSITION) {
// Transició a JOC (la música ja s'ha parat en el fade)
GestorEscenes::actual = GestorEscenes::Escena::JOC;
}
break;
}
}
@@ -129,22 +301,63 @@ void EscenaTitol::dibuixar() {
return;
}
// Estat MAIN: Dibuixar text de títol i copyright (sobre el starfield)
if (estat_actual_ == EstatTitol::MAIN) {
// Text principal centrat (vertical i horitzontalment)
const std::string main_text = "PRESS BUTTON TO PLAY";
const float escala_main = 1.0f;
const float spacing = 2.0f;
// Estat MAIN i TRANSITION: Dibuixar títol i text (sobre el starfield)
if (estat_actual_ == EstatTitol::MAIN || estat_actual_ == EstatTitol::TRANSITION) {
// === Dibuixar lletres del títol "ORNI ATTACK!" ===
float text_width = text_.get_text_width(main_text, escala_main, spacing);
float text_height = text_.get_text_height(escala_main);
// Dibuixar "ORNI" (línia 1)
for (const auto& lletra : lletres_orni_) {
Rendering::render_shape(
sdl_.obte_renderer(),
lletra.forma,
lletra.posicio,
0.0f, // sense rotació
ESCALA_TITULO, // escala 80%
true, // dibuixar
1.0f // progrés complet (totalment visible)
);
}
float x_center = (Defaults::Game::WIDTH - text_width) / 2.0f;
float y_center = (Defaults::Game::HEIGHT - text_height) / 2.0f;
// Dibuixar "ATTACK!" (línia 2)
for (const auto& lletra : lletres_attack_) {
Rendering::render_shape(
sdl_.obte_renderer(),
lletra.forma,
lletra.posicio,
0.0f, // sense rotació
ESCALA_TITULO, // escala 80%
true, // dibuixar
1.0f // progrés complet (totalment visible)
);
}
text_.render(main_text, Punt{x_center, y_center}, escala_main, spacing);
// === Text "PRESS BUTTON TO PLAY" ===
// En estat MAIN: sempre visible
// En estat TRANSITION: parpellejant (blink amb sinusoide)
// Copyright a la part inferior (centrat horitzontalment)
const float spacing = 2.0f; // Espai entre caràcters (usat també per copyright)
bool mostrar_text = true;
if (estat_actual_ == EstatTitol::TRANSITION) {
// Parpelleig: sin oscil·la entre -1 i 1, volem ON quan > 0
float fase = temps_acumulat_ * BLINK_FREQUENCY * 2.0f * 3.14159f; // 2π × freq × temps
mostrar_text = (std::sin(fase) > 0.0f);
}
if (mostrar_text) {
const std::string main_text = "PRESS BUTTON TO PLAY";
const float escala_main = 1.0f;
float text_width = text_.get_text_width(main_text, escala_main, spacing);
float x_center = (Defaults::Game::WIDTH - text_width) / 2.0f;
float altura_attack = lletres_attack_.empty() ? 50.0f : lletres_attack_[0].altura;
float y_center = y_attack_dinamica_ + altura_attack + 70.0f;
text_.render(main_text, Punt{x_center, y_center}, escala_main, spacing);
}
// === Copyright a la part inferior (centrat horitzontalment) ===
// Convert to uppercase since VectorText only supports A-Z
std::string copyright = Project::COPYRIGHT;
for (char& c : copyright) {
@@ -168,15 +381,21 @@ void EscenaTitol::processar_events(const SDL_Event& event) {
// Qualsevol tecla o clic de ratolí
if (event.type == SDL_EVENT_KEY_DOWN ||
event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
switch (estat_actual_) {
case EstatTitol::INIT:
// Saltar a MAIN
estat_actual_ = EstatTitol::MAIN;
break;
case EstatTitol::MAIN:
// Anar al joc
GestorEscenes::actual = GestorEscenes::Escena::JOC;
// Iniciar transició amb fade-out de música
estat_actual_ = EstatTitol::TRANSITION;
temps_acumulat_ = 0.0f; // Reset del comptador
Audio::get()->fadeOutMusic(MUSIC_FADE); // Fade de 300ms
break;
case EstatTitol::TRANSITION:
// Ignorar inputs durant la transició
break;
}
}

View File

@@ -7,35 +7,62 @@
#include <SDL3/SDL.h>
#include <memory>
#include <vector>
#include "core/defaults.hpp"
#include "core/graphics/shape.hpp"
#include "core/graphics/starfield.hpp"
#include "core/graphics/vector_text.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/defaults.hpp"
#include "core/types.hpp"
class EscenaTitol {
public:
explicit EscenaTitol(SDLManager& sdl);
~EscenaTitol(); // Destructor per aturar música
void executar(); // Bucle principal de l'escena
private:
// Màquina d'estats per la pantalla de títol
enum class EstatTitol {
INIT, // Pantalla negra inicial (2 segons)
MAIN // Pantalla de títol amb text
INIT, // Pantalla negra inicial (2 segons)
MAIN, // Pantalla de títol amb text
TRANSITION // Transició amb fade-out de música i text parpellejant
};
// Estructura per emmagatzemar informació de cada lletra del títol
struct LetraLogo {
std::shared_ptr<Graphics::Shape> forma; // Forma vectorial de la lletra
Punt posicio; // Posició en pantalla
float ancho; // Amplada escalada
float altura; // Altura escalada
float offset_centre; // Offset del centre per posicionament
};
SDLManager& sdl_;
Graphics::VectorText text_; // Sistema de text vectorial
Graphics::VectorText text_; // Sistema de text vectorial
std::unique_ptr<Graphics::Starfield> starfield_; // Camp d'estrelles de fons
EstatTitol estat_actual_; // Estat actual de la màquina
float temps_acumulat_; // Temps acumulat per l'estat INIT
EstatTitol estat_actual_; // Estat actual de la màquina
float temps_acumulat_; // Temps acumulat per l'estat INIT
// Lletres del títol "ORNI ATTACK!"
std::vector<LetraLogo> lletres_orni_; // Lletres de "ORNI" (línia 1)
std::vector<LetraLogo> lletres_attack_; // Lletres de "ATTACK!" (línia 2)
float y_attack_dinamica_; // Posició Y calculada dinàmicament per "ATTACK!"
// Constants
static constexpr float DURACIO_INIT = 2.0f; // Duració de l'estat INIT (2 segons)
static constexpr float DURACIO_INIT = 4.0f; // Duració de l'estat INIT (2 segons)
static constexpr float DURACIO_TRANSITION = 1.5f; // Duració de la transició (1.5 segons)
static constexpr float ESCALA_TITULO = 0.6f; // Escala per les lletres del títol (50%)
static constexpr float ESPAI_ENTRE_LLETRES = 10.0f; // Espai entre lletres
static constexpr float Y_ORNI = 150.0f; // Posició Y de "ORNI"
static constexpr float SEPARACION_LINEAS = 10.0f; // Separació entre "ORNI" i "ATTACK!" (0.0f = pegades)
static constexpr float BLINK_FREQUENCY = 3.0f; // Freqüència de parpelleig (3 Hz)
static constexpr int MUSIC_FADE = 1000; // Duracio del fade de la musica del titol al començar a jugar
// Mètodes privats
void actualitzar(float delta_time);
void dibuixar();
void processar_events(const SDL_Event& event);
void inicialitzar_titol(); // Carrega i posiciona les lletres del títol
};

View File

@@ -44,8 +44,9 @@ def apply_transform(punto, matrix):
def parse_svg_path(d_attr):
"""
Convierte comandos SVG path (M y L) a lista de puntos [(x, y), ...]
Ejemplo: "M896,1693L896,1531.23L1219.53,1531.23..." → [(896, 1693), (896, 1531.23), ...]
Convierte comandos SVG path (M y L) a lista de polylines separadas.
Cada comando M inicia una nueva polyline.
Retorna: lista de listas de puntos [[(x,y), ...], [(x,y), ...], ...]
"""
# Reemplazar comas por espacios para facilitar parsing
d_attr = d_attr.replace(',', ' ')
@@ -55,7 +56,9 @@ def parse_svg_path(d_attr):
d_attr = re.sub(r'([ML])', r'|\1', d_attr)
commands = [c.strip() for c in d_attr.split('|') if c.strip()]
points = []
polylines = [] # Lista de polylines
current_polyline = [] # Polyline actual
for cmd in commands:
if not cmd:
continue
@@ -70,18 +73,29 @@ def parse_svg_path(d_attr):
# Parsear pares de coordenadas
coords = coords_str.split()
# Si es comando M (MoveTo), empezar nueva polyline
if cmd_letter == 'M':
# Guardar polyline anterior si tiene puntos
if current_polyline:
polylines.append(current_polyline)
current_polyline = []
# Procesar en pares (x, y)
i = 0
while i < len(coords) - 1:
try:
x = float(coords[i])
y = float(coords[i + 1])
points.append((x, y))
current_polyline.append((x, y))
i += 2
except (ValueError, IndexError):
i += 1
return points
# No olvidar la última polyline
if current_polyline:
polylines.append(current_polyline)
return polylines
def rect_to_points(rect_elem):
@@ -103,11 +117,22 @@ def rect_to_points(rect_elem):
]
def calc_bounding_box(puntos):
def calc_bounding_box(polylines):
"""
Calcula bounding box de una lista de puntos
Calcula bounding box de una o varias polylines
polylines puede ser:
- lista de puntos [(x,y), ...]
- lista de polylines [[(x,y), ...], [(x,y), ...]]
Retorna: (min_x, max_x, min_y, max_y, ancho, alto)
"""
# Aplanar si es lista de polylines
puntos = []
if polylines and isinstance(polylines[0], list):
for polyline in polylines:
puntos.extend(polyline)
else:
puntos = polylines
if not puntos:
return (0, 0, 0, 0, 0, 0)
@@ -125,17 +150,18 @@ def calc_bounding_box(puntos):
return (min_x, max_x, min_y, max_y, ancho, alto)
def normalizar_letra(nombre, puntos, altura_objetivo=100.0):
def normalizar_letra(nombre, polylines, altura_objetivo=100.0):
"""
Escala y traslada letra para que tenga altura_objetivo pixels
y esté centrada en origen (0, 0) en esquina superior izquierda
Retorna: dict con puntos normalizados, centro, ancho, alto
polylines: lista de polylines [[(x,y), ...], [(x,y), ...]]
Retorna: dict con polylines normalizadas, centro, ancho, alto
"""
if not puntos:
if not polylines:
return None
min_x, max_x, min_y, max_y, ancho, alto = calc_bounding_box(puntos)
min_x, max_x, min_y, max_y, ancho, alto = calc_bounding_box(polylines)
if alto == 0:
print(f" [WARN] Letra {nombre}: altura cero")
@@ -144,14 +170,26 @@ def normalizar_letra(nombre, puntos, altura_objetivo=100.0):
# Factor de escala basado en altura
escala = altura_objetivo / alto
# Normalizar puntos:
# Normalizar cada polyline:
# 1. Trasladar a origen (restar min_x, min_y)
# 2. Aplicar escala
puntos_norm = []
for x, y in puntos:
x_norm = (x - min_x) * escala
y_norm = (y - min_y) * escala
puntos_norm.append((x_norm, y_norm))
# 3. Cerrar polyline (último punto = primer punto)
polylines_norm = []
total_puntos = 0
for polyline in polylines:
polyline_norm = []
for x, y in polyline:
x_norm = (x - min_x) * escala
y_norm = (y - min_y) * escala
polyline_norm.append((x_norm, y_norm))
# Cerrar polyline si no está cerrada
if polyline_norm and polyline_norm[0] != polyline_norm[-1]:
polyline_norm.append(polyline_norm[0])
polylines_norm.append(polyline_norm)
total_puntos += len(polyline_norm)
# Calcular dimensiones finales
ancho_norm = ancho * escala
@@ -162,10 +200,11 @@ def normalizar_letra(nombre, puntos, altura_objetivo=100.0):
return {
'nombre': nombre,
'puntos': puntos_norm,
'polylines': polylines_norm,
'centro': centro,
'ancho': ancho_norm,
'alto': alto_norm
'alto': alto_norm,
'total_puntos': total_puntos
}
@@ -178,6 +217,7 @@ def generar_shp(letra_norm, output_dir):
scale: 1.0
center: cx, cy
polyline: x1,y1 x2,y2 x3,y3 ...
polyline: x1,y1 x2,y2 x3,y3 ... (si hay múltiples formas)
"""
if not letra_norm:
return
@@ -198,13 +238,13 @@ def generar_shp(letra_norm, output_dir):
f.write(f"center: {letra_norm['centro'][0]:.2f}, {letra_norm['centro'][1]:.2f}\n")
f.write(f"\n")
# Polyline con todos los puntos
f.write("polyline: ")
puntos_str = " ".join([f"{x:.2f},{y:.2f}" for x, y in letra_norm['puntos']])
f.write(puntos_str)
f.write("\n")
# Generar una línea polyline por cada forma
for polyline in letra_norm['polylines']:
puntos_str = " ".join([f"{x:.2f},{y:.2f}" for x, y in polyline])
f.write(f"polyline: {puntos_str}\n")
print(f"{nombre_archivo:20} ({len(letra_norm['puntos']):3} puntos, "
print(f"{nombre_archivo:20} ({len(letra_norm['polylines'])} formas, "
f"{letra_norm['total_puntos']:3} puntos, "
f"{letra_norm['ancho']:6.2f} x {letra_norm['alto']:6.2f} px)")
@@ -232,14 +272,18 @@ def parse_svg(filepath):
print(f"[INFO] Transform matrix: {transform_matrix}")
# Extraer paths y rects
paths = group.findall('svg:path', ns)
rects = group.findall('svg:rect', ns)
# Extraer paths y rects (buscar recursivamente en todos los descendientes)
paths = group.findall('.//svg:path', ns)
rects = group.findall('.//svg:rect', ns)
print(f"[INFO] Encontrados {len(paths)} paths y {len(rects)} rects")
# Nombres de las letras para paths (sin I que es un rect)
nombres_paths = ['J', 'A', 'L', 'G', 'A', 'M', 'E', 'S']
# Nombres de las letras para "ORNI ATTACK!"
# Grupo 1 (top): O, R, N, I (3 paths + 1 rect)
# Grupo 2 (bottom): A, T, T, A, C, K, ! (6 paths + 1 path para !)
# Total: 9 paths + 1 rect
# Asumiendo orden de aparición en SVG:
nombres_paths = ['O', 'R', 'N', 'A', 'T', 'T', 'A', 'C', 'K', 'EXCLAMACION']
letras = []
@@ -252,15 +296,18 @@ def parse_svg(filepath):
if not d_attr:
continue
# Parsear puntos del path
puntos = parse_svg_path(d_attr)
# Parsear polylines del path (ahora retorna lista de polylines)
polylines = parse_svg_path(d_attr)
# Aplicar transformación
puntos = [apply_transform(p, transform_matrix) for p in puntos]
# Aplicar transformación a cada polyline
polylines_transformed = []
for polyline in polylines:
polyline_transformed = [apply_transform(p, transform_matrix) for p in polyline]
polylines_transformed.append(polyline_transformed)
letras.append({
'nombre': nombres_paths[i],
'puntos': puntos
'polylines': polylines_transformed
})
# Procesar rects (la letra I es un rect)
@@ -268,11 +315,12 @@ def parse_svg(filepath):
puntos = rect_to_points(rect)
# Aplicar transformación
puntos = [apply_transform(p, transform_matrix) for p in puntos]
puntos_transformed = [apply_transform(p, transform_matrix) for p in puntos]
# Rect es una sola polyline
letras.append({
'nombre': 'I',
'puntos': puntos
'polylines': [puntos_transformed]
})
return letras
@@ -326,7 +374,7 @@ def main():
for nombre in sorted(letras_unicas.keys()):
letra = letras_unicas[nombre]
letra_norm = normalizar_letra(nombre, letra['puntos'], altura_objetivo=100.0)
letra_norm = normalizar_letra(nombre, letra['polylines'], altura_objetivo=100.0)
if letra_norm:
generar_shp(letra_norm, output_dir)