28 Commits

Author SHA1 Message Date
54031e3520 afegit friendly fire 2025-12-17 19:39:33 +01:00
8b9d26a02c delay en naus en titol 2025-12-17 18:55:41 +01:00
3d5277a395 fix: ratolí visible en fullscreen 2025-12-17 18:36:12 +01:00
2555157bd7 fix: en alguns casos no podies tornar a unirte a la partida 2025-12-17 18:16:46 +01:00
461eaedecf retocs en nave2 2025-12-17 17:55:14 +01:00
1891c9e49e eliminades shapes sobrants 2025-12-17 17:44:23 +01:00
829a895464 continue counter ara arriba fins a 0 2025-12-17 17:21:03 +01:00
8bc259b25a nous sons 2025-12-17 17:05:42 +01:00
ec333efe66 afegida logica de continues
fix: el text no centrava correctament en horitzontal
2025-12-17 13:31:32 +01:00
3b432e6580 layout de TITOL 2025-12-17 11:32:37 +01:00
886ec8ab1d amagat el cursor d'inici en mode finestra 2025-12-16 22:47:12 +01:00
bc5982b286 treballant en les naus de title 2025-12-16 22:14:55 +01:00
75a4a1b3b9 millorada la JOIN_PHASE i fase final de TITOL 2025-12-16 12:34:19 +01:00
f3f0bfcd9a afegit so a init_hud 2025-12-16 10:05:18 +01:00
c959e0e3a0 animacions de INIT_HUD amb control d'inici i final 2025-12-16 09:39:53 +01:00
8b896912b2 centralitzada la gestio d'SKIP per a les escenes 2025-12-16 08:33:29 +01:00
3d0057220d afegides tecles d'START. ja comença el joc amb el numero correcte de jugadors 2025-12-12 16:40:46 +01:00
0c75f56cb5 treballant en context per a jugador 1, jugador 2 o els dos 2025-12-12 10:43:17 +01:00
0ceaa75862 integrada classe Input 2025-12-11 12:41:03 +01:00
087b8d346d afegit segon jugador 2025-12-10 17:18:34 +01:00
aca1f5200b els enemics poden morir mentre fan spawn 2025-12-10 11:58:26 +01:00
3b638f4715 respawn de nau i invulnerabilitat 2025-12-10 11:35:45 +01:00
9a5adcbcc5 revisat el marcador
modificada la shape 03
2025-12-10 11:05:15 +01:00
d0be5ea2d1 millorades les definicions de zones 2025-12-10 08:20:43 +01:00
07e00fff09 eliminada ship2.shp i substituida ship.shp 2025-12-10 07:51:02 +01:00
b4e0ca7eca INIT_HUD amb temps de les animacions per percentatge
ordenats en subcarpetes els fitxers d'audio
corregit typo LIFES a LIVES
2025-12-09 22:57:01 +01:00
b8173b205b acabat INIT_HUD 2025-12-09 22:17:35 +01:00
57d623d6bc treballant en INIT_HUD 2025-12-09 22:09:24 +01:00
74 changed files with 6483 additions and 849 deletions

278
CLAUDE.md
View File

@@ -505,6 +505,284 @@ void dibuixar() {
}
```
## Title Screen Ship System (BETA 3.0)
The title screen features two 3D ships floating on the starfield with perspective rendering, entry/exit animations, and subtle floating motion.
### Architecture Overview
**Files:**
- `source/game/title/ship_animator.hpp/cpp` - Ship animation state machine
- `source/core/rendering/shape_renderer.hpp/cpp` - 3D rotation + perspective projection
- `source/core/defaults.hpp` - Title::Ships namespace with all constants
- `source/game/escenes/escena_titol.hpp/cpp` - Integration with title scene
**Design Philosophy:**
- **Static 3D rotation**: Ships have fixed pitch/yaw/roll angles (not recalculated per frame)
- **Simple Z-axis simulation**: Scale changes simulate depth, not full perspective recalculation
- **State machine**: ENTERING → FLOATING → EXITING states
- **Easing functions**: Smooth transitions with ease_out_quad (entry) and ease_in_quad (exit)
- **Sinusoidal floating**: Organic motion using X/Y oscillation with phase offset
### 3D Rendering System
#### Rotation3D Struct (shape_renderer.hpp)
```cpp
struct Rotation3D {
float pitch; // X-axis rotation (nose up/down)
float yaw; // Y-axis rotation (turn left/right)
float roll; // Z-axis rotation (bank left/right)
Rotation3D() : pitch(0.0f), yaw(0.0f), roll(0.0f) {}
Rotation3D(float p, float y, float r) : pitch(p), yaw(y), roll(r) {}
bool has_rotation() const {
return pitch != 0.0f || yaw != 0.0f || roll != 0.0f;
}
};
```
#### 3D Transformation Pipeline (shape_renderer.cpp)
```cpp
static Punt apply_3d_rotation(float x, float y, const Rotation3D& rot) {
float z = 0.0f; // All 2D points start at Z=0
// 1. Pitch (X-axis): Rotate around horizontal axis
float cos_pitch = std::cos(rot.pitch);
float sin_pitch = std::sin(rot.pitch);
float y1 = y * cos_pitch - z * sin_pitch;
float z1 = y * sin_pitch + z * cos_pitch;
// 2. Yaw (Y-axis): Rotate around vertical axis
float cos_yaw = std::cos(rot.yaw);
float sin_yaw = std::sin(rot.yaw);
float x2 = x * cos_yaw + z1 * sin_yaw;
float z2 = -x * sin_yaw + z1 * cos_yaw;
// 3. Roll (Z-axis): Rotate around depth axis
float cos_roll = std::cos(rot.roll);
float sin_roll = std::sin(rot.roll);
float x3 = x2 * cos_roll - y1 * sin_roll;
float y3 = x2 * sin_roll + y1 * cos_roll;
// 4. Perspective projection (Z-divide)
constexpr float perspective_factor = 500.0f;
float scale_factor = perspective_factor / (perspective_factor + z2);
return {x3 * scale_factor, y3 * scale_factor};
}
```
**Rendering order**: 3D rotation → perspective → 2D scale → 2D rotation → translation
**Backward compatibility**: Optional `rotation_3d` parameter (default nullptr) - existing code unaffected
### Ship Animation State Machine
#### States (ship_animator.hpp)
```cpp
enum class EstatNau {
ENTERING, // Entering from off-screen
FLOATING, // Floating at target position
EXITING // Flying towards vanishing point
};
struct NauTitol {
int jugador_id; // 1 or 2
EstatNau estat; // Current state
float temps_estat; // Time in current state
Punt posicio_inicial; // Start position
Punt posicio_objectiu; // Target position
Punt posicio_actual; // Current interpolated position
float escala_inicial; // Start scale
float escala_objectiu; // Target scale
float escala_actual; // Current interpolated scale
Rotation3D rotacio_3d; // STATIC 3D rotation (never changes)
float fase_oscilacio; // Oscillation phase accumulator
std::shared_ptr<Graphics::Shape> forma;
bool visible;
};
```
#### State Transitions
**ENTERING** (2.0s):
- Ships appear from beyond screen edges (calculated radially from clock positions)
- Lerp position: off-screen → target (clock 8 / clock 4)
- Lerp scale: 1.0 → 0.6 (perspective effect)
- Easing: `ease_out_quad` (smooth deceleration)
- Transition: → FLOATING when complete
**FLOATING** (indefinite):
- Sinusoidal oscillation on X/Y axes
- Different frequencies (0.5 Hz / 0.7 Hz) with phase offset (π/2)
- Creates organic circular/elliptical motion
- Scale constant at 0.6
- Transition: → EXITING when START pressed
**EXITING** (1.0s):
- Ships fly towards vanishing point (center: 320, 240)
- Lerp position: current → vanishing point
- Lerp scale: current → 0.0 (simulates Z → infinity)
- Easing: `ease_in_quad` (acceleration)
- Edge case: If START pressed during ENTERING, ships fly from mid-animation position
- Marks invisible when complete
### Configuration (defaults.hpp)
```cpp
namespace Title {
namespace Ships {
// Clock positions (polar coordinates from center 320, 240)
constexpr float CLOCK_8_ANGLE = 150.0f * Math::PI / 180.0f; // Bottom-left
constexpr float CLOCK_4_ANGLE = 30.0f * Math::PI / 180.0f; // Bottom-right
constexpr float CLOCK_RADIUS = 150.0f;
// Target positions (pre-calculated)
constexpr float P1_TARGET_X = 190.0f; // Clock 8
constexpr float P1_TARGET_Y = 315.0f;
constexpr float P2_TARGET_X = 450.0f; // Clock 4
constexpr float P2_TARGET_Y = 315.0f;
// 3D rotations (STATIC - tuned for subtle effect)
constexpr float P1_PITCH = 0.1f; // ~6° nose-up
constexpr float P1_YAW = -0.15f; // ~9° turn left
constexpr float P1_ROLL = -0.05f; // ~3° bank left
constexpr float P2_PITCH = 0.1f; // ~6° nose-up
constexpr float P2_YAW = 0.15f; // ~9° turn right
constexpr float P2_ROLL = 0.05f; // ~3° bank right
// Scales
constexpr float ENTRY_SCALE_START = 1.0f;
constexpr float FLOATING_SCALE = 0.6f;
// Animation durations
constexpr float ENTRY_DURATION = 2.0f;
constexpr float EXIT_DURATION = 1.0f;
constexpr float ENTRY_OFFSET = 200.0f; // Distance beyond screen edge
// Floating oscillation
constexpr float FLOAT_AMPLITUDE_X = 6.0f;
constexpr float FLOAT_AMPLITUDE_Y = 4.0f;
constexpr float FLOAT_FREQUENCY_X = 0.5f;
constexpr float FLOAT_FREQUENCY_Y = 0.7f;
constexpr float FLOAT_PHASE_OFFSET = 1.57f; // π/2 (90°)
// Vanishing point
constexpr float VANISHING_POINT_X = 320.0f;
constexpr float VANISHING_POINT_Y = 240.0f;
}
}
```
### Integration with EscenaTitol
#### Constructor
```cpp
// Initialize ships after starfield
ship_animator_ = std::make_unique<Title::ShipAnimator>(sdl_.obte_renderer());
ship_animator_->inicialitzar();
if (estat_actual_ == EstatTitol::MAIN) {
// Jump to MAIN: ships already in position (no entry animation)
ship_animator_->set_visible(true);
} else {
// Normal flow: ships enter during STARFIELD_FADE_IN
ship_animator_->set_visible(true);
ship_animator_->start_entry_animation();
}
```
#### Update Loop
```cpp
// Update ships in visible states
if (ship_animator_ &&
(estat_actual_ == EstatTitol::STARFIELD_FADE_IN ||
estat_actual_ == EstatTitol::STARFIELD ||
estat_actual_ == EstatTitol::MAIN ||
estat_actual_ == EstatTitol::PLAYER_JOIN_PHASE)) {
ship_animator_->actualitzar(delta_time);
}
// Trigger exit when START pressed
if (checkStartGameButtonPressed()) {
estat_actual_ = EstatTitol::PLAYER_JOIN_PHASE;
ship_animator_->trigger_exit_animation(); // Edge case: handles mid-ENTERING
Audio::get()->fadeOutMusic(MUSIC_FADE);
}
```
#### Draw Loop
```cpp
// Draw order: starfield → ships → logo → text
if (starfield_) starfield_->dibuixar();
if (ship_animator_ &&
(estat_actual_ == EstatTitol::STARFIELD_FADE_IN ||
estat_actual_ == EstatTitol::STARFIELD ||
estat_actual_ == EstatTitol::MAIN ||
estat_actual_ == EstatTitol::PLAYER_JOIN_PHASE)) {
ship_animator_->dibuixar();
}
// Logo and text drawn after ships (foreground)
```
### Timing & Visibility
**Timeline:**
1. **STARFIELD_FADE_IN** (3.0s): Ships enter from off-screen
2. **STARFIELD** (4.0s): Ships floating
3. **MAIN** (indefinite): Ships floating + logo + text visible
4. **PLAYER_JOIN_PHASE** (2.5s): Ships exit (1.0s) + text blink
5. **BLACK_SCREEN** (2.0s): Ships already invisible (exit completed at 1.0s)
**Automatic visibility management:**
- Ships marked `visible = false` when exit animation completes (actualitzar_exiting)
- No manual hiding needed - state machine handles it
### Tuning Notes
**If ships look distorted:**
- Reduce rotation angles (P1_PITCH, P1_YAW, P1_ROLL, P2_*)
- Current values (0.1, 0.15, 0.05) are tuned for subtle 3D effect
- Angles in radians: 0.1 rad ≈ 6°, 0.15 rad ≈ 9°
**If ships are too large/small:**
- Adjust FLOATING_SCALE (currently 0.6)
- Adjust ENTRY_SCALE_START (currently 1.0)
**If floating motion is too jerky/smooth:**
- Adjust FLOAT_AMPLITUDE_X/Y (currently 6.0/4.0 pixels)
- Adjust FLOAT_FREQUENCY_X/Y (currently 0.5/0.7 Hz)
**If entry/exit animations are too fast/slow:**
- Adjust ENTRY_DURATION (currently 2.0s)
- Adjust EXIT_DURATION (currently 1.0s)
### Implementation Phases (Completed)
**Phase 1**: 3D infrastructure (Rotation3D, render_shape extension)
**Phase 2**: Foundation (ship_animator files, constants)
**Phase 3**: Configuration & loading (shape loading, initialization)
**Phase 4**: Floating animation (sinusoidal oscillation)
**Phase 5**: Entry animation (off-screen → position with easing)
**Phase 6**: Exit animation (position → vanishing point)
**Phase 7**: EscenaTitol integration (constructor, update, draw)
**Phase 8**: Polish & tuning (angles, scales, edge cases)
**Phase 9**: Documentation (CLAUDE.md, code comments)
## Migration Progress
### ✅ Phase 0: Project Setup

View File

@@ -1,11 +1,13 @@
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(orni VERSION 0.5.0)
project(orni VERSION 0.7.0)
# Info del proyecto
set(PROJECT_LONG_NAME "Orni Attack")
set(PROJECT_COPYRIGHT "© 1999 Visente i Sergi, 2025 Port")
set(PROJECT_COPYRIGHT_ORIGINAL "© 1999 Visente i Sergi")
set(PROJECT_COPYRIGHT_PORT "© 2025 JailDesigner")
set(PROJECT_COPYRIGHT "${PROJECT_COPYRIGHT_ORIGINAL}, ${PROJECT_COPYRIGHT_PORT}")
# Establecer estándar de C++
set(CMAKE_CXX_STANDARD 20)

2134
data/gamecontrollerdb.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -7,5 +7,5 @@ scale: 1.0
center: 10, 20
# Trazo continuo (barra superior + lateral derecho + barra media + lateral derecho + barra inferior)
polyline: 2,10 18,10 18,20 14,20
polyline: 14,20 18,20 18,30 2,30
polyline: 2,10 18,10 18,20 8,20
polyline: 8,20 18,20 18,30 2,30

View File

@@ -1,4 +1,4 @@
# ship.shp - Nau del jugador (triangle)
# ship.shp - Nau del jugador 1 (triangle amb base còncava - punta de fletxa)
# © 1999 Visente i Sergi (versió Pascal)
# © 2025 Port a C++20 amb SDL3
@@ -6,15 +6,19 @@ name: ship
scale: 1.0
center: 0, 0
# Triangle: punta amunt, base avall
# Triangle amb base còncava tipus "punta de fletxa"
# Punts originals (polar):
# p1: r=12, angle=270° (3π/2) → punta amunt
# p2: r=12, angle=45° (π/4) → base dreta-darrere
# p3: r=12, angle=135° (3π/4) → base esquerra-darrere
#
# MODIFICACIÓ: afegit p4 al mig de la base, desplaçat cap al centre
# p4: (0, 4) → punt central de la base, cap endins
#
# Conversió polar→cartesià (angle-90° perquè origen visual és amunt):
# p1: (0, -12)
# p2: (8.49, 8.49)
# p3: (-8.49, 8.49)
# p1: (0, -12) → punta
# p2: (8.49, 8.49) → base dreta
# p4: (0, 4) → base centre (cap endins)
# p3: (-8.49, 8.49) → base esquerra
polyline: 0,-12 8.49,8.49 -8.49,8.49 0,-12
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12

View File

@@ -1,4 +1,4 @@
# ship2.shp - Nau del jugador (triangle amb base còncava - punta de fletxa)
# ship2.shp - Nau del jugador 2 (triangle amb circulito central)
# © 1999 Visente i Sergi (versió Pascal)
# © 2025 Port a C++20 amb SDL3
@@ -7,6 +7,7 @@ scale: 1.0
center: 0, 0
# Triangle amb base còncava tipus "punta de fletxa"
# (Mateix que ship.shp)
# Punts originals (polar):
# p1: r=12, angle=270° (3π/2) → punta amunt
# p2: r=12, angle=45° (π/4) → base dreta-darrere
@@ -21,4 +22,9 @@ center: 0, 0
# p4: (0, 4) → base centre (cap endins)
# p3: (-8.49, 8.49) → base esquerra
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
#polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
polyline: 0,-12 8.49,8.49 -8.49,8.49 0,-12
# Circulito central (octàgon r=2.5)
# Distintiu visual del jugador 2
polyline: 0,-2.5 1.77,-1.77 2.5,0 1.77,1.77 0,2.5 -1.77,1.77 -2.5,0 -1.77,-1.77 0,-2.5

View File

@@ -0,0 +1,28 @@
# ship2_perspective.shp - Nave P2 con perspectiva pre-calculada
# Posición optimizada: "4 del reloj" (Abajo-Derecha)
# Dirección: Volando hacia el fondo (centro pantalla)
name: ship2_perspective
scale: 1.0
center: 0, 0
# TRANSFORMACIÓN APLICADA:
# 1. Rotación -45° (apuntando al centro desde abajo-dcha)
# 2. Proyección de perspectiva:
# - Punta (p1): Reducida al 60% (simula lejanía)
# - Base (p2, p3): Aumentada al 110% (simula cercanía)
# 3. Flip horizontal (simétrica a ship_starfield.shp)
#
# Nuevos Punts (aprox):
# p1 (Punta): (-4, -4) -> Lejos, pequeña y apuntando arriba-izq
# p2 (Ala Izq): (-3, 11) -> Cerca, lado interior
# p4 (Base Cnt): (3, 5) -> Centro base
# p3 (Ala Dcha): (11, 2) -> Cerca, lado exterior (más grande)
#polyline: -4,-4 -3,11 3,5 11,2 -4,-4
polyline: -4,-4 -3,11 11,2 -4,-4
# Circulito central (octàgon r=2.5)
# Distintiu visual del jugador 2
# Sin perspectiva (está en el centro de la nave)
polyline: 0,-2.5 1.77,-1.77 2.5,0 1.77,1.77 0,2.5 -1.77,1.77 -2.5,0 -1.77,-1.77 0,-2.5

27
data/shapes/ship3.shp Normal file
View File

@@ -0,0 +1,27 @@
# ship2.shp - Nau del jugador 2 (interceptor amb ales)
# © 2025 Orni Attack - Jugador 2
name: ship2
scale: 1.0
center: 0, 0
# Interceptor amb ales laterals
# Disseny més ample i agressiu que P1
#
# Geometria:
# - Punta més curta i ampla
# - Ales laterals pronunciades
# - Base més ampla per estabilitat visual
#
# Punts (cartesianes, Y negatiu = amunt):
# p1: (0, -10) → punta (més curta que P1)
# p2: (4, -6) → transició ala dreta
# p3: (10, 2) → punta ala dreta (més ampla)
# p4: (6, 8) → base ala dreta
# p5: (0, 6) → base centre (menys còncava)
# p6: (-6, 8) → base ala esquerra
# p7: (-10, 2) → punta ala esquerra
# p8: (-4, -6) → transició ala esquerra
# p1: (0, -10) → tanca
polyline: 0,-10 4,-6 10,2 6,8 0,6 -6,8 -10,2 -4,-6 0,-10

View File

@@ -0,0 +1,21 @@
# ship_perspective.shp - Nave con perspectiva pre-calculada
# Posición optimizada: "8 del reloj" (Abajo-Izquierda)
# Dirección: Volando hacia el fondo (centro pantalla)
name: ship_perspective
scale: 1.0
center: 0, 0
# TRANSFORMACIÓN APLICADA:
# 1. Rotación +45° (apuntando al centro desde abajo-izq)
# 2. Proyección de perspectiva:
# - Punta (p1): Reducida al 60% (simula lejanía)
# - Base (p2, p3): Aumentada al 110% (simula cercanía)
#
# Nuevos Puntos (aprox):
# p1 (Punta): (4, -4) -> Lejos, pequeña y apuntando arriba-dcha
# p2 (Ala Dcha): (3, 11) -> Cerca, lado interior
# p4 (Base Cnt): (-3, 5) -> Centro base
# p3 (Ala Izq): (-11, 2) -> Cerca, lado exterior (más grande)
polyline: 4,-4 3,11 -3,5 -11,2 4,-4

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -3,10 +3,10 @@
#include "core/audio/audio_cache.hpp"
#include "core/resources/resource_helper.hpp"
#include <iostream>
#include "core/resources/resource_helper.hpp"
// Inicialització de variables estàtiques
std::unordered_map<std::string, JA_Sound_t*> AudioCache::sounds_;
std::unordered_map<std::string, JA_Music_t*> AudioCache::musics_;
@@ -23,7 +23,7 @@ JA_Sound_t* AudioCache::getSound(const std::string& name) {
// Normalize path: "laser_shoot.wav" → "sounds/laser_shoot.wav"
std::string normalized = name;
if (normalized.find("sounds/") != 0 && normalized.find('/') == std::string::npos) {
if (normalized.find("sounds/") != 0) {
normalized = "sounds/" + normalized;
}
@@ -57,7 +57,7 @@ JA_Music_t* AudioCache::getMusic(const std::string& name) {
// Normalize path: "title.ogg" → "music/title.ogg"
std::string normalized = name;
if (normalized.find("music/") != 0 && normalized.find('/') == std::string::npos) {
if (normalized.find("music/") != 0) {
normalized = "music/" + normalized;
}

View File

@@ -22,44 +22,76 @@ constexpr int WIDTH = 640;
constexpr int HEIGHT = 480;
} // namespace Game
// Zones del joc (SDL_FRect amb càlculs automàtics)
// Zones del joc (SDL_FRect amb càlculs automàtics basat en percentatges)
namespace Zones {
// --- CONFIGURACIÓ DE PORCENTATGES ---
// Basats en valors originals 640x480
// Ajusta estos valors per canviar proporcions
// Totes les zones definides com a percentatges de Game::WIDTH (640) i Game::HEIGHT (480)
constexpr float PLAYAREA_MARGIN_HORIZONTAL_PERCENT = 10.0f / Game::WIDTH;
constexpr float PLAYAREA_MARGIN_VERTICAL_PERCENT = 10.0f / Game::HEIGHT;
constexpr float SCOREBOARD_HEIGHT_PERCENT = 48.0f / Game::HEIGHT;
// Percentatges d'alçada (divisió vertical)
constexpr float SCOREBOARD_TOP_HEIGHT_PERCENT = 0.02f; // 10% superior
constexpr float MAIN_PLAYAREA_HEIGHT_PERCENT = 0.88f; // 80% central
constexpr float SCOREBOARD_BOTTOM_HEIGHT_PERCENT = 0.10f; // 10% inferior
// --- CÀLCULS AUTOMÀTICS ---
// Estos valors es recalculen si canvien Game::WIDTH o Game::HEIGHT
// Padding horizontal per a PLAYAREA (dins de MAIN_PLAYAREA)
constexpr float PLAYAREA_PADDING_HORIZONTAL_PERCENT = 0.015f; // 5% a cada costat
constexpr float PLAYAREA_MARGIN_H =
Game::WIDTH * PLAYAREA_MARGIN_HORIZONTAL_PERCENT;
constexpr float PLAYAREA_MARGIN_V =
Game::HEIGHT * PLAYAREA_MARGIN_VERTICAL_PERCENT;
constexpr float SCOREBOARD_H = Game::HEIGHT * SCOREBOARD_HEIGHT_PERCENT;
// --- CÀLCULS AUTOMÀTICS DE PÍXELS ---
// Càlculs automàtics a partir dels percentatges
// Alçades
constexpr float SCOREBOARD_TOP_H = Game::HEIGHT * SCOREBOARD_TOP_HEIGHT_PERCENT;
constexpr float MAIN_PLAYAREA_H = Game::HEIGHT * MAIN_PLAYAREA_HEIGHT_PERCENT;
constexpr float SCOREBOARD_BOTTOM_H = Game::HEIGHT * SCOREBOARD_BOTTOM_HEIGHT_PERCENT;
// Posicions Y
constexpr float SCOREBOARD_TOP_Y = 0.0f;
constexpr float MAIN_PLAYAREA_Y = SCOREBOARD_TOP_H;
constexpr float SCOREBOARD_BOTTOM_Y = MAIN_PLAYAREA_Y + MAIN_PLAYAREA_H;
// Padding horizontal de PLAYAREA
constexpr float PLAYAREA_PADDING_H = Game::WIDTH * PLAYAREA_PADDING_HORIZONTAL_PERCENT;
// --- ZONES FINALS (SDL_FRect) ---
// Zona de joc principal
// Ocupa: tot menys marges (dalt, esq, dret) i scoreboard (baix)
constexpr SDL_FRect PLAYAREA = {
PLAYAREA_MARGIN_H, // x = 10.0
PLAYAREA_MARGIN_V, // y = 10.0
Game::WIDTH - 2.0f * PLAYAREA_MARGIN_H, // width = 620.0
Game::HEIGHT - PLAYAREA_MARGIN_V - SCOREBOARD_H // height = 406.0
// Marcador superior (reservat per a futur ús)
// Ocupa: 10% superior (0-48px)
constexpr SDL_FRect SCOREBOARD_TOP = {
0.0f, // x = 0.0
SCOREBOARD_TOP_Y, // y = 0.0
static_cast<float>(Game::WIDTH), // w = 640.0
SCOREBOARD_TOP_H // h = 48.0
};
// Zona de marcador
// Ocupa: tot l'ample, 64px d'alçada en la part inferior
// Àrea de joc principal (contenidor del 80% central, sense padding)
// Ocupa: 10-90% (48-432px), ample complet
constexpr SDL_FRect MAIN_PLAYAREA = {
0.0f, // x = 0.0
MAIN_PLAYAREA_Y, // y = 48.0
static_cast<float>(Game::WIDTH), // w = 640.0
MAIN_PLAYAREA_H // h = 384.0
};
// Zona de joc real (amb padding horizontal del 5%)
// Ocupa: dins de MAIN_PLAYAREA, amb marges laterals
// S'utilitza per a límits del joc, col·lisions, spawn
constexpr SDL_FRect PLAYAREA = {
PLAYAREA_PADDING_H, // x = 32.0
MAIN_PLAYAREA_Y, // y = 48.0 (igual que MAIN_PLAYAREA)
Game::WIDTH - 2.0f * PLAYAREA_PADDING_H, // w = 576.0
MAIN_PLAYAREA_H // h = 384.0 (igual que MAIN_PLAYAREA)
};
// Marcador inferior (marcador actual)
// Ocupa: 10% inferior (432-480px)
constexpr SDL_FRect SCOREBOARD = {
0.0f, // x = 0.0
Game::HEIGHT - SCOREBOARD_H, // y = 416.0
static_cast<float>(Game::WIDTH), // width = 640.0
SCOREBOARD_H // height = 64.0
SCOREBOARD_BOTTOM_Y, // y = 432.0
static_cast<float>(Game::WIDTH), // w = 640.0
SCOREBOARD_BOTTOM_H // h = 48.0
};
// Padding horizontal del marcador (per alinear zones esquerra/dreta amb PLAYAREA)
constexpr float SCOREBOARD_PADDING_H = 0.0f; // Game::WIDTH * 0.015f;
} // namespace Zones
// Objetos del juego
@@ -73,19 +105,95 @@ constexpr float ENEMY_RADIUS = 20.0f;
constexpr float BULLET_RADIUS = 3.0f;
} // namespace Entities
// Ship (nave del jugador)
namespace Ship {
// Invulnerabilidad post-respawn
constexpr float INVULNERABILITY_DURATION = 3.0f; // Segundos de invulnerabilidad
// Parpadeo visual durante invulnerabilidad
constexpr float BLINK_VISIBLE_TIME = 0.1f; // Tiempo visible (segundos)
constexpr float BLINK_INVISIBLE_TIME = 0.1f; // Tiempo invisible (segundos)
// Frecuencia total: 0.2s/ciclo = 5 Hz (~15 parpadeos en 3s)
} // namespace Ship
// Game rules (lives, respawn, game over)
namespace Game {
constexpr int STARTING_LIVES = 3; // Initial lives
constexpr float DEATH_DURATION = 3.0f; // Seconds of death animation
constexpr float GAME_OVER_DURATION = 5.0f; // Seconds to display game over
constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 0.80f; // 80% hitbox (generous)
// Friendly fire system
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0f; // Hitbox exacto (100%)
constexpr float BULLET_GRACE_PERIOD = 0.2f; // Inmunidad post-disparo (s)
// Transición LEVEL_START (mensajes aleatorios PRE-level)
constexpr float LEVEL_START_DURATION = 3.0f; // Duración total
constexpr float LEVEL_START_TYPING_RATIO = 0.3f; // 30% escribiendo, 70% mostrando
constexpr float LEVEL_START_DURATION = 3.0f; // Duración total
constexpr float LEVEL_START_TYPING_RATIO = 0.3f; // 30% escribiendo, 70% mostrando
// Transición LEVEL_COMPLETED (mensaje "GOOD JOB COMMANDER!")
constexpr float LEVEL_COMPLETED_DURATION = 3.0f; // Duración total
constexpr float LEVEL_COMPLETED_TYPING_RATIO = 0.0f; // 0.0 = sin typewriter (directo)
constexpr float LEVEL_COMPLETED_DURATION = 3.0f; // Duración total
constexpr float LEVEL_COMPLETED_TYPING_RATIO = 0.0f; // 0.0 = sin typewriter (directo)
// Transición INIT_HUD (animación inicial del HUD)
constexpr float INIT_HUD_DURATION = 3.0f; // Duración total del estado
// Ratios de animación (inicio y fin como porcentajes del tiempo total)
// RECT (rectángulo de marges)
constexpr float INIT_HUD_RECT_RATIO_INIT = 0.30f;
constexpr float INIT_HUD_RECT_RATIO_END = 0.85f;
// SCORE (marcador de puntuación)
constexpr float INIT_HUD_SCORE_RATIO_INIT = 0.60f;
constexpr float INIT_HUD_SCORE_RATIO_END = 0.90f;
// SHIP1 (nave jugador 1)
constexpr float INIT_HUD_SHIP1_RATIO_INIT = 0.0f;
constexpr float INIT_HUD_SHIP1_RATIO_END = 1.0f;
// SHIP2 (nave jugador 2)
constexpr float INIT_HUD_SHIP2_RATIO_INIT = 0.20f;
constexpr float INIT_HUD_SHIP2_RATIO_END = 1.0f;
// Posición inicial de la nave en INIT_HUD (75% de altura de zona de juego)
constexpr float INIT_HUD_SHIP_START_Y_RATIO = 0.75f; // 75% desde el top de PLAYAREA
// Spawn positions (distribución horizontal para 2 jugadores)
constexpr float P1_SPAWN_X_RATIO = 0.33f; // 33% desde izquierda
constexpr float P2_SPAWN_X_RATIO = 0.67f; // 67% desde izquierda
constexpr float SPAWN_Y_RATIO = 0.75f; // 75% desde arriba
// Continue system behavior
constexpr int CONTINUE_COUNT_START = 9; // Countdown starts at 9
constexpr float CONTINUE_TICK_DURATION = 1.0f; // Seconds per countdown tick
constexpr int MAX_CONTINUES = 3; // Maximum continues per game
constexpr bool INFINITE_CONTINUES = false; // If true, unlimited continues
// Continue screen visual configuration
namespace ContinueScreen {
// "CONTINUE" text
constexpr float CONTINUE_TEXT_SCALE = 2.0f; // Text size
constexpr float CONTINUE_TEXT_Y_RATIO = 0.30f; // 35% from top of PLAYAREA
// Countdown number (9, 8, 7...)
constexpr float COUNTER_TEXT_SCALE = 4.0f; // Text size (large)
constexpr float COUNTER_TEXT_Y_RATIO = 0.50f; // 50% from top of PLAYAREA
// "CONTINUES LEFT: X" text
constexpr float INFO_TEXT_SCALE = 0.7f; // Text size (small)
constexpr float INFO_TEXT_Y_RATIO = 0.75f; // 65% from top of PLAYAREA
} // namespace ContinueScreen
// Game Over screen visual configuration
namespace GameOverScreen {
constexpr float TEXT_SCALE = 2.0f; // "GAME OVER" text size
constexpr float TEXT_SPACING = 4.0f; // Character spacing
} // namespace GameOverScreen
// Stage message configuration (LEVEL_START, LEVEL_COMPLETED)
constexpr float STAGE_MESSAGE_Y_RATIO = 0.25f; // 25% from top of PLAYAREA
constexpr float STAGE_MESSAGE_MAX_WIDTH_RATIO = 0.9f; // 90% of PLAYAREA width
} // namespace Game
// Física (valores actuales del juego, sincronizados con joc_asteroides.cpp)
@@ -117,7 +225,7 @@ constexpr float FRICCIO_ANGULAR = 0.5f; // Desacceleració angular (rad/s²
// Angular velocity cap 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 VELOCITAT_ROT_MAX = 1.5f; // rad/s (~86°/s)
} // namespace Debris
} // namespace Physics
@@ -177,21 +285,45 @@ constexpr bool ENABLED = true; // Audio habilitado por defecto
// Música (pistas de fondo)
namespace Music {
constexpr float VOLUME = 0.8F; // Volumen música
constexpr bool ENABLED = true; // Música habilitada
constexpr const char* GAME_TRACK = "game.ogg"; // Pista de juego
constexpr int FADE_DURATION_MS = 1000; // Fade out duration
constexpr float VOLUME = 0.8F; // Volumen música
constexpr bool ENABLED = true; // Música habilitada
constexpr const char* GAME_TRACK = "game.ogg"; // Pista de juego
constexpr const char* TITLE_TRACK = "title.ogg"; // Pista de titulo
constexpr int FADE_DURATION_MS = 1000; // Fade out duration
} // namespace Music
// Efectes de so (sons puntuals)
namespace Sound {
constexpr float VOLUME = 1.0F; // Volumen efectos
constexpr bool ENABLED = true; // Sonidos habilitados
constexpr const char* EXPLOSION = "explosion.wav"; // Explosión
constexpr const char* LASER = "laser_shoot.wav"; // Disparo
constexpr const char* GOOD_JOB_COMMANDER = "good_job_commander.wav"; // Voz: "Good job, commander"
constexpr float VOLUME = 1.0F; // Volumen efectos
constexpr bool ENABLED = true; // Sonidos habilitados
constexpr const char* CONTINUE = "effects/continue.wav"; // Cuenta atras
constexpr const char* EXPLOSION = "effects/explosion.wav"; // Explosión
constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa
constexpr const char* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; // Friendly fire hit
constexpr const char* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD
constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo
constexpr const char* LOGO = "effects/logo.wav"; // Logo
constexpr const char* START = "effects/start.wav"; // El jugador pulsa START
constexpr const char* GOOD_JOB_COMMANDER = "voices/good_job_commander.wav"; // Voz: "Good job, commander"
} // namespace Sound
// Controls (mapeo de teclas para los jugadores)
namespace Controls {
namespace P1 {
constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_RIGHT;
constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_LEFT;
constexpr SDL_Scancode THRUST = SDL_SCANCODE_UP;
constexpr SDL_Keycode SHOOT = SDLK_SPACE;
} // namespace P1
namespace P2 {
constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_D;
constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_A;
constexpr SDL_Scancode THRUST = SDL_SCANCODE_W;
constexpr SDL_Keycode SHOOT = SDLK_LSHIFT;
} // namespace P2
} // namespace Controls
// Enemy type configuration (tipus d'enemics)
namespace Enemies {
// Pentagon (esquivador - zigzag evasion)
@@ -216,11 +348,11 @@ constexpr const char* SHAPE_FILE = "enemy_square.shp";
// Molinillo (agressiu - fast straight lines, proximity spin-up)
namespace Molinillo {
constexpr float VELOCITAT = 50.0f; // px/s (fastest)
constexpr float CANVI_ANGLE_PROB = 0.05f; // 5% per wall hit (rare direction change)
constexpr float CANVI_ANGLE_MAX = 0.3f; // Small angle adjustments
constexpr float DROTACIO_MIN = 3.0f; // Base rotation (rad/s) [+50%]
constexpr float DROTACIO_MAX = 6.0f; // [+50%]
constexpr float VELOCITAT = 50.0f; // px/s (fastest)
constexpr float CANVI_ANGLE_PROB = 0.05f; // 5% per wall hit (rare direction change)
constexpr float CANVI_ANGLE_MAX = 0.3f; // Small angle adjustments
constexpr float DROTACIO_MIN = 3.0f; // Base rotation (rad/s) [+50%]
constexpr float DROTACIO_MAX = 6.0f; // [+50%]
constexpr float DROTACIO_PROXIMITY_MULTIPLIER = 3.0f; // Spin-up multiplier when near ship
constexpr float PROXIMITY_DISTANCE = 100.0f; // Distance threshold (px)
constexpr const char* SHAPE_FILE = "enemy_pinwheel.shp";
@@ -247,36 +379,150 @@ constexpr float ROTACIO_ACCEL_MULTIPLIER_MAX = 4.0f; // Max speed multiplier [m
// Spawn safety and invulnerability system
namespace Spawn {
// Safe spawn distance from player
constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0f; // 3x ship radius
constexpr float SAFETY_DISTANCE = Defaults::Entities::SHIP_RADIUS * SAFETY_DISTANCE_MULTIPLIER; // 36.0f px
constexpr int MAX_SPAWN_ATTEMPTS = 50; // Max attempts to find safe position
// Safe spawn distance from player
constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0f; // 3x ship radius
constexpr float SAFETY_DISTANCE = Defaults::Entities::SHIP_RADIUS * SAFETY_DISTANCE_MULTIPLIER; // 36.0f px
constexpr int MAX_SPAWN_ATTEMPTS = 50; // Max attempts to find safe position
// Invulnerability system
constexpr float INVULNERABILITY_DURATION = 3.0f; // Seconds
constexpr float INVULNERABILITY_BRIGHTNESS_START = 0.3f; // Dim
constexpr float INVULNERABILITY_BRIGHTNESS_END = 0.7f; // Normal (same as Defaults::Brightness::ENEMIC)
constexpr float INVULNERABILITY_SCALE_START = 0.0f; // Invisible
constexpr float INVULNERABILITY_SCALE_END = 1.0f; // Full size
// Invulnerability system
constexpr float INVULNERABILITY_DURATION = 3.0f; // Seconds
constexpr float INVULNERABILITY_BRIGHTNESS_START = 0.3f; // Dim
constexpr float INVULNERABILITY_BRIGHTNESS_END = 0.7f; // Normal (same as Defaults::Brightness::ENEMIC)
constexpr float INVULNERABILITY_SCALE_START = 0.0f; // Invisible
constexpr float INVULNERABILITY_SCALE_END = 1.0f; // Full size
} // namespace Spawn
// Scoring system (puntuació per tipus d'enemic)
namespace Scoring {
constexpr int PENTAGON_SCORE = 100; // Pentàgon (esquivador, 35 px/s)
constexpr int QUADRAT_SCORE = 150; // Quadrat (perseguidor, 40 px/s)
constexpr int MOLINILLO_SCORE = 200; // Molinillo (agressiu, 50 px/s)
constexpr int PENTAGON_SCORE = 100; // Pentàgon (esquivador, 35 px/s)
constexpr int QUADRAT_SCORE = 150; // Quadrat (perseguidor, 40 px/s)
constexpr int MOLINILLO_SCORE = 200; // Molinillo (agressiu, 50 px/s)
} // namespace Scoring
} // namespace Enemies
// Title scene ship animations (naus 3D flotants a l'escena de títol)
namespace Title {
namespace Ships {
// ============================================================
// PARÀMETRES BASE (ajustar aquí per experimentar)
// ============================================================
// 1. Escala global de les naus
constexpr float SHIP_BASE_SCALE = 2.5f; // Multiplicador (1.0 = mida original del .shp)
// 2. Altura vertical (cercanía al centro)
// Ratio Y desde el centro de la pantalla (0.0 = centro, 1.0 = bottom de pantalla)
constexpr float TARGET_Y_RATIO = 0.15625f;
// 3. Radio orbital (distancia radial desde centro en coordenadas polares)
constexpr float CLOCK_RADIUS = 150.0f; // Distància des del centre
// 4. Ángulos de posición (clock positions en coordenadas polares)
// En coordenades de pantalla: 0° = dreta, 90° = baix, 180° = esquerra, 270° = dalt
constexpr float CLOCK_8_ANGLE = 150.0f * Math::PI / 180.0f; // 8 o'clock (bottom-left)
constexpr float CLOCK_4_ANGLE = 30.0f * Math::PI / 180.0f; // 4 o'clock (bottom-right)
// 5. Radio máximo de la forma de la nave (para calcular offset automáticamente)
constexpr float SHIP_MAX_RADIUS = 30.0f; // Radi del cercle circumscrit a ship_starfield.shp
// 6. Margen de seguridad para offset de entrada
constexpr float ENTRY_OFFSET_MARGIN = 227.5f; // Para offset total de ~340px (ajustado)
// ============================================================
// VALORS DERIVATS (calculats automàticament - NO modificar)
// ============================================================
// Centre de la pantalla (punt de referència)
constexpr float CENTER_X = Game::WIDTH / 2.0f; // 320.0f
constexpr float CENTER_Y = Game::HEIGHT / 2.0f; // 240.0f
// Posicions target (calculades dinàmicament des dels paràmetres base)
// Nota: std::cos/sin no són constexpr en C++20, però funcionen en runtime
// Les funcions inline són optimitzades pel compilador (zero overhead)
inline float P1_TARGET_X() {
return CENTER_X + CLOCK_RADIUS * std::cos(CLOCK_8_ANGLE);
}
inline float P1_TARGET_Y() {
return CENTER_Y + (Game::HEIGHT / 2.0f) * TARGET_Y_RATIO;
}
inline float P2_TARGET_X() {
return CENTER_X + CLOCK_RADIUS * std::cos(CLOCK_4_ANGLE);
}
inline float P2_TARGET_Y() {
return CENTER_Y + (Game::HEIGHT / 2.0f) * TARGET_Y_RATIO;
}
// Escales d'animació (relatives a SHIP_BASE_SCALE)
constexpr float ENTRY_SCALE_START = 1.5f * SHIP_BASE_SCALE; // Entrada: 50% més gran
constexpr float FLOATING_SCALE = 1.0f * SHIP_BASE_SCALE; // Flotant: escala base
// Offset d'entrada (ajustat automàticament a l'escala)
// Fórmula: (radi màxim de la nau * escala d'entrada) + marge
constexpr float ENTRY_OFFSET = (SHIP_MAX_RADIUS * ENTRY_SCALE_START) + ENTRY_OFFSET_MARGIN;
// Punt de fuga (centre per a l'animació de sortida)
constexpr float VANISHING_POINT_X = CENTER_X; // 320.0f
constexpr float VANISHING_POINT_Y = CENTER_Y; // 240.0f
// ============================================================
// ANIMACIONS (durades, oscil·lacions, delays)
// ============================================================
// Durades d'animació
constexpr float ENTRY_DURATION = 2.0f; // Entrada (segons)
constexpr float EXIT_DURATION = 1.0f; // Sortida (segons)
// Flotació (oscil·lació reduïda i diferenciada per nau)
constexpr float FLOAT_AMPLITUDE_X = 4.0f; // Amplitud X (píxels)
constexpr float FLOAT_AMPLITUDE_Y = 2.5f; // Amplitud Y (píxels)
// Freqüències base
constexpr float FLOAT_FREQUENCY_X_BASE = 0.5f; // Hz
constexpr float FLOAT_FREQUENCY_Y_BASE = 0.7f; // Hz
constexpr float FLOAT_PHASE_OFFSET = 1.57f; // π/2 (90°)
// Delays d'entrada (per a entrada escalonada)
constexpr float P1_ENTRY_DELAY = 0.0f; // P1 entra immediatament
constexpr float P2_ENTRY_DELAY = 0.5f; // P2 entra 0.5s després
// Delay global abans d'iniciar l'animació d'entrada al estat MAIN
constexpr float ENTRANCE_DELAY = 5.0f; // Temps d'espera abans que les naus entrin
// Multiplicadors de freqüència per a cada nau (variació sutil ±12%)
constexpr float P1_FREQUENCY_MULTIPLIER = 0.88f; // 12% més lenta
constexpr float P2_FREQUENCY_MULTIPLIER = 1.12f; // 12% més ràpida
} // namespace Ships
namespace Layout {
// Posicions verticals (anclatges des del TOP de pantalla lògica, 0.0-1.0)
constexpr float LOGO_POS = 0.20f; // Logo "ORNI"
constexpr float PRESS_START_POS = 0.75f; // "PRESS START TO PLAY"
constexpr float COPYRIGHT1_POS = 0.90f; // Primera línia copyright
// Separacions relatives (proporció respecte Game::HEIGHT = 480px)
constexpr float LOGO_LINE_SPACING = 0.02f; // Entre "ORNI" i "ATTACK!" (10px)
constexpr float COPYRIGHT_LINE_SPACING = 0.0f; // Entre línies copyright (5px)
// Factors d'escala
constexpr float LOGO_SCALE = 0.6f; // Escala "ORNI ATTACK!"
constexpr float PRESS_START_SCALE = 1.0f; // Escala "PRESS START TO PLAY"
constexpr float COPYRIGHT_SCALE = 0.5f; // Escala copyright
// Espaiat entre caràcters (usat per VectorText)
constexpr float TEXT_SPACING = 2.0f;
} // namespace Layout
} // namespace Title
// Floating score numbers (números flotants de puntuació)
namespace FloatingScore {
constexpr float LIFETIME = 2.0f; // Duració màxima (segons)
constexpr float VELOCITY_Y = -30.0f; // Velocitat vertical (px/s, negatiu = amunt)
constexpr float VELOCITY_X = 0.0f; // Velocitat horizontal (px/s)
constexpr float SCALE = 0.75f; // Escala del text (0.75 = 75% del marcador)
constexpr float SPACING = 0.0f; // Espaiat entre caràcters
constexpr int MAX_CONCURRENT = 15; // Pool size (= MAX_ORNIS)
constexpr float LIFETIME = 2.0f; // Duració màxima (segons)
constexpr float VELOCITY_Y = -30.0f; // Velocitat vertical (px/s, negatiu = amunt)
constexpr float VELOCITY_X = 0.0f; // Velocitat horizontal (px/s)
constexpr float SCALE = 0.45f; // Escala del text (0.6 = 60% del marcador)
constexpr float SPACING = 0.0f; // Espaiat entre caràcters
constexpr int MAX_CONCURRENT = 15; // Pool size (= MAX_ORNIS)
} // namespace FloatingScore
} // namespace Defaults

View File

@@ -3,10 +3,10 @@
#include "core/graphics/shape_loader.hpp"
#include "core/resources/resource_helper.hpp"
#include <iostream>
#include "core/resources/resource_helper.hpp"
namespace Graphics {
// Inicialització de variables estàtiques

View File

@@ -106,7 +106,7 @@ float Starfield::calcular_brightness(const Estrella& estrella) const {
// distancia_centre: 0.0 (centre, llunyanes) → 1.0 (vora, properes)
float brightness_base = Defaults::Brightness::STARFIELD_MIN +
(Defaults::Brightness::STARFIELD_MAX - Defaults::Brightness::STARFIELD_MIN) *
estrella.distancia_centre;
estrella.distancia_centre;
// Aplicar multiplicador i limitar a 1.0
return std::min(1.0f, brightness_base * multiplicador_brightness_);

View File

@@ -72,10 +72,10 @@ class Starfield {
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
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)
};

View File

@@ -195,8 +195,8 @@ void VectorText::render(const std::string& text, const Punt& posicio, float esca
// Altura de un carácter escalado (necesario para ajustar Y)
const float char_height_scaled = char_height * escala;
// Posición actual del centro del carácter (ajustada desde esquina superior
// izquierda)
// Posición X del borde izquierdo del carácter actual
// (se ajustará +char_width/2 para obtener el centro al renderizar)
float current_x = posicio.x;
// Iterar sobre cada byte del string (con detecció UTF-8)
@@ -220,9 +220,9 @@ void VectorText::render(const std::string& text, const Punt& posicio, float esca
auto it = chars_.find(c);
if (it != chars_.end()) {
// Renderizar carácter
// Ajustar Y para que posicio represente esquina superior izquierda
// (render_shape espera el centro, así que sumamos la mitad de la altura)
Punt char_pos = {current_x, posicio.y + char_height_scaled / 2.0f};
// Ajustar X e Y para que posicio represente esquina superior izquierda
// (render_shape espera el centro, así que sumamos la mitad de ancho y altura)
Punt char_pos = {current_x + char_width_scaled / 2.0f, posicio.y + char_height_scaled / 2.0f};
Rendering::render_shape(renderer_, it->second, char_pos, 0.0f, escala, true, 1.0f, brightness);
// Avanzar posición
@@ -236,6 +236,21 @@ void VectorText::render(const std::string& text, const Punt& posicio, float esca
}
}
void VectorText::render_centered(const std::string& text, const Punt& centre_punt, float escala, float spacing, float brightness) {
// Calcular dimensions del text
float text_width = get_text_width(text, escala, spacing);
float text_height = get_text_height(escala);
// Calcular posició de l'esquina superior esquerra
// restant la meitat de les dimensions del punt central
Punt posicio_esquerra = {
centre_punt.x - (text_width / 2.0f),
centre_punt.y - (text_height / 2.0f)};
// Delegar al mètode render() existent
render(text, posicio_esquerra, escala, spacing, brightness);
}
float VectorText::get_text_width(const std::string& text, float escala, float spacing) const {
if (text.empty()) {
return 0.0f;
@@ -244,16 +259,23 @@ float VectorText::get_text_width(const std::string& text, float escala, float sp
const float char_width_scaled = char_width * escala;
const float spacing_scaled = spacing * escala;
// Ancho total = (número de caracteres × char_width) + (espacios entre
// caracteres)
float width = text.length() * char_width_scaled;
// Contar caracteres visuals (no bytes) - manejar UTF-8
size_t visual_chars = 0;
for (size_t i = 0; i < text.length(); i++) {
unsigned char c = static_cast<unsigned char>(text[i]);
// Añadir spacing entre caracteres (n-1 espacios para n caracteres)
if (text.length() > 1) {
width += (text.length() - 1) * spacing_scaled;
// Detectar copyright UTF-8 (0xC2 0xA9) - igual que render()
if (c == 0xC2 && i + 1 < text.length() &&
static_cast<unsigned char>(text[i + 1]) == 0xA9) {
visual_chars++; // Un caràcter visual (©)
i++; // Saltar el següent byte
} else {
visual_chars++; // Caràcter normal
}
}
return width;
// Ancho total = todos los caracteres VISUALES + spacing entre ellos
return visual_chars * char_width_scaled + (visual_chars - 1) * spacing_scaled;
}
float VectorText::get_text_height(float escala) const {

View File

@@ -27,6 +27,14 @@ class VectorText {
// - brightness: factor de brillantor (0.0-1.0, default 1.0 = màxima brillantor)
void render(const std::string& text, const Punt& posicio, float escala = 1.0f, float spacing = 2.0f, float brightness = 1.0f);
// Renderizar string centrado en un punto
// - text: cadena a renderizar
// - centre_punt: punto central del texto (no esquina superior izquierda)
// - escala: factor de escala (1.0 = 20×40 px por carácter)
// - spacing: espacio entre caracteres en píxeles (a escala 1.0)
// - brightness: factor de brillantor (0.0-1.0, default 1.0 = màxima brillantor)
void render_centered(const std::string& text, const Punt& centre_punt, float escala = 1.0f, float spacing = 2.0f, float brightness = 1.0f);
// Calcular ancho total de un string (útil para centrado)
float get_text_width(const std::string& text, float escala = 1.0f, float spacing = 2.0f) const;

606
source/core/input/input.cpp Normal file
View File

@@ -0,0 +1,606 @@
#include "core/input/input.hpp"
#include <SDL3/SDL.h> // Para SDL_GetGamepadAxis, SDL_GamepadAxis, SDL_GamepadButton, SDL_GetError, SDL_JoystickID, SDL_AddGamepadMappingsFromFile, SDL_Event, SDL_EventType, SDL_GetGamepadButton, SDL_GetKeyboardState, SDL_INIT_GAMEPAD, SDL_InitSubSystem, SDL_LogError, SDL_OpenGamepad, SDL_PollEvent, SDL_WasInit, Sint16, SDL_Gamepad, SDL_LogCategory, SDL_Scancode
#include <iostream> // Para basic_ostream, operator<<, cout, cerr
#include <memory> // Para shared_ptr, __shared_ptr_access, allocator, operator==, make_shared
#include <ranges> // Para __find_if_fn, find_if
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
#include <utility> // Para pair, move
#include "game/options.hpp" // Para Options::controls
// Singleton
Input* Input::instance = nullptr;
// Inicializa la instancia única del singleton
void Input::init(const std::string& game_controller_db_path) {
Input::instance = new Input(game_controller_db_path);
}
// Libera la instancia
void Input::destroy() { delete Input::instance; }
// Obtiene la instancia
auto Input::get() -> Input* { return Input::instance; }
// Constructor
Input::Input(std::string game_controller_db_path)
: gamepad_mappings_file_(std::move(game_controller_db_path)) {
// Inicializar bindings del teclado (valores por defecto)
// Estos serán sobrescritos por applyPlayer1BindingsFromOptions()
keyboard_.bindings = {
// Movimiento del jugador
{Action::LEFT, KeyState{.scancode = SDL_SCANCODE_LEFT}},
{Action::RIGHT, KeyState{.scancode = SDL_SCANCODE_RIGHT}},
{Action::THRUST, KeyState{.scancode = SDL_SCANCODE_UP}},
{Action::SHOOT, KeyState{.scancode = SDL_SCANCODE_SPACE}},
// Inputs de sistema (globales)
{Action::WINDOW_DEC_ZOOM, KeyState{.scancode = SDL_SCANCODE_F1}},
{Action::WINDOW_INC_ZOOM, KeyState{.scancode = SDL_SCANCODE_F2}},
{Action::TOGGLE_FULLSCREEN, KeyState{.scancode = SDL_SCANCODE_F3}},
{Action::TOGGLE_VSYNC, KeyState{.scancode = SDL_SCANCODE_F4}},
{Action::EXIT, KeyState{.scancode = SDL_SCANCODE_ESCAPE}}};
initSDLGamePad(); // Inicializa el subsistema SDL_INIT_GAMEPAD
}
// Asigna inputs a teclas
void Input::bindKey(Action action, SDL_Scancode code) {
keyboard_.bindings[action].scancode = code;
}
// Aplica las teclas configuradas desde Options
void Input::applyKeyboardBindingsFromOptions() {
bindKey(Action::LEFT, Options::keyboard_controls.key_left);
bindKey(Action::RIGHT, Options::keyboard_controls.key_right);
bindKey(Action::THRUST, Options::keyboard_controls.key_thrust);
}
// Aplica configuración de botones del gamepad desde Options al primer gamepad conectado
void Input::applyGamepadBindingsFromOptions() {
// Si no hay gamepads conectados, no hay nada que hacer
if (gamepads_.empty()) {
return;
}
// Obtener el primer gamepad conectado
const auto& gamepad = gamepads_[0];
// Aplicar bindings desde Options
// Los valores pueden ser:
// - 0-20+: Botones SDL_GamepadButton (DPAD, face buttons, shoulders)
// - 100: L2 trigger
// - 101: R2 trigger
// - 200+: Ejes del stick analógico
gamepad->bindings[Action::LEFT].button = Options::gamepad_controls.button_left;
gamepad->bindings[Action::RIGHT].button = Options::gamepad_controls.button_right;
gamepad->bindings[Action::THRUST].button = Options::gamepad_controls.button_thrust;
}
// Asigna inputs a botones del mando
void Input::bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action, SDL_GamepadButton button) {
if (gamepad != nullptr) {
gamepad->bindings[action].button = button;
}
}
// Asigna inputs a botones del mando
void Input::bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action_target, Action action_source) {
if (gamepad != nullptr) {
gamepad->bindings[action_target].button = gamepad->bindings[action_source].button;
}
}
// Comprueba si alguna acción está activa
auto Input::checkAction(Action action, bool repeat, bool check_keyboard, const std::shared_ptr<Gamepad>& gamepad) -> bool {
bool success_keyboard = false;
bool success_controller = false;
if (check_keyboard) {
if (repeat) { // El usuario quiere saber si está pulsada (estado mantenido)
success_keyboard = keyboard_.bindings[action].is_held;
} else { // El usuario quiere saber si ACABA de ser pulsada (evento de un solo fotograma)
success_keyboard = keyboard_.bindings[action].just_pressed;
}
}
// Si gamepad es nullptr pero hay mandos conectados, usar el primero
std::shared_ptr<Gamepad> active_gamepad = gamepad;
if (active_gamepad == nullptr && !gamepads_.empty()) {
active_gamepad = gamepads_[0];
}
if (active_gamepad != nullptr) {
success_controller = checkAxisInput(action, active_gamepad, repeat);
if (!success_controller) {
success_controller = checkTriggerInput(action, active_gamepad, repeat);
}
if (!success_controller) {
if (repeat) { // El usuario quiere saber si está pulsada (estado mantenido)
success_controller = active_gamepad->bindings[action].is_held;
} else { // El usuario quiere saber si ACABA de ser pulsada (evento de un solo fotograma)
success_controller = active_gamepad->bindings[action].just_pressed;
}
}
}
return (success_keyboard || success_controller);
}
// Comprueba si hay almenos una acción activa
auto Input::checkAnyInput(bool check_keyboard, const std::shared_ptr<Gamepad>& gamepad) -> bool {
// Obtenemos el número total de acciones posibles para iterar sobre ellas.
// --- Comprobación del Teclado ---
if (check_keyboard) {
for (const auto& pair : keyboard_.bindings) {
// Simplemente leemos el estado pre-calculado por Input::update().
// Ya no se llama a SDL_GetKeyboardState ni se modifica el estado '.active'.
if (pair.second.just_pressed) {
return true; // Se encontró una acción recién pulsada.
}
}
}
// Si gamepad es nullptr pero hay mandos conectados, usar el primero
std::shared_ptr<Gamepad> active_gamepad = gamepad;
if (active_gamepad == nullptr && !gamepads_.empty()) {
active_gamepad = gamepads_[0];
}
// --- Comprobación del Mando ---
// Comprobamos si hay mandos y si el índice solicitado es válido.
if (active_gamepad != nullptr) {
// Iteramos sobre todas las acciones, no sobre el número de mandos.
for (const auto& pair : active_gamepad->bindings) {
// Leemos el estado pre-calculado para el mando y la acción específicos.
if (pair.second.just_pressed) {
return true; // Se encontró una acción recién pulsada en el mando.
}
}
}
// Si llegamos hasta aquí, no se detectó ninguna nueva pulsación.
return false;
}
// Comprueba si hay algún botón pulsado
auto Input::checkAnyButton(bool repeat) -> bool {
// Solo comprueba los botones definidos previamente
for (auto bi : BUTTON_INPUTS) {
// Comprueba el teclado
if (checkAction(bi, repeat, CHECK_KEYBOARD)) {
return true;
}
// Comprueba los mandos
for (const auto& gamepad : gamepads_) {
if (checkAction(bi, repeat, DO_NOT_CHECK_KEYBOARD, gamepad)) {
return true;
}
}
}
return false;
}
// Comprueba si algún jugador (P1 o P2) presionó alguna acción de una lista
auto Input::checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat) -> bool {
for (const auto& action : actions) {
if (checkActionPlayer1(action, repeat) || checkActionPlayer2(action, repeat)) {
return true;
}
}
return false;
}
// Comprueba si hay algun mando conectado
auto Input::gameControllerFound() const -> bool { return !gamepads_.empty(); }
// Obten el nombre de un mando de juego
auto Input::getControllerName(const std::shared_ptr<Gamepad>& gamepad) -> std::string {
return gamepad == nullptr ? std::string() : gamepad->name;
}
// Obtiene la lista de nombres de mandos
auto Input::getControllerNames() const -> std::vector<std::string> {
std::vector<std::string> names;
for (const auto& gamepad : gamepads_) {
names.push_back(gamepad->name);
}
return names;
}
// Obten el número de mandos conectados
auto Input::getNumGamepads() const -> int { return gamepads_.size(); }
// Obtiene el gamepad a partir de un event.id
auto Input::getGamepad(SDL_JoystickID id) const -> std::shared_ptr<Input::Gamepad> {
for (const auto& gamepad : gamepads_) {
if (gamepad->instance_id == id) {
return gamepad;
}
}
return nullptr;
}
auto Input::getGamepadByName(const std::string& name) const -> std::shared_ptr<Input::Gamepad> {
for (const auto& gamepad : gamepads_) {
if (gamepad && gamepad->name == name) {
return gamepad;
}
}
return nullptr;
}
// Obtiene el SDL_GamepadButton asignado a un action
auto Input::getControllerBinding(const std::shared_ptr<Gamepad>& gamepad, Action action) -> SDL_GamepadButton {
return static_cast<SDL_GamepadButton>(gamepad->bindings[action].button);
}
// Comprueba el eje del mando
auto Input::checkAxisInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool {
// Obtener el binding configurado para esta acción
auto& binding = gamepad->bindings[action];
// Solo revisar ejes si el binding está configurado como eje (valores 200+)
// 200 = Left stick izquierda, 201 = Left stick derecha
if (binding.button < 200) {
// El binding no es un eje, no revisar axis
return false;
}
// Determinar qué eje y dirección revisar según el binding
bool axis_active_now = false;
if (binding.button == 200) {
// Left stick izquierda
axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX) < -AXIS_THRESHOLD;
} else if (binding.button == 201) {
// Left stick derecha
axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX) > AXIS_THRESHOLD;
} else {
// Binding de eje no soportado
return false;
}
if (repeat) {
// Si se permite repetir, simplemente devolvemos el estado actual
return axis_active_now;
} // Si no se permite repetir, aplicamos la lógica de transición
if (axis_active_now && !binding.axis_active) {
// Transición de inactivo a activo
binding.axis_active = true;
return true;
}
if (!axis_active_now && binding.axis_active) {
// Transición de activo a inactivo
binding.axis_active = false;
}
// Mantener el estado actual
return false;
}
// Comprueba los triggers del mando como botones digitales
auto Input::checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool {
// Solo manejamos botones específicos que pueden ser triggers
if (gamepad->bindings[action].button != static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)) {
// Solo procesamos L2 y R2 como triggers
int button = gamepad->bindings[action].button;
// Verificar si el botón mapeado corresponde a un trigger virtual
// (Para esto necesitamos valores especiales que representen L2/R2 como botones)
bool trigger_active_now = false;
// Usamos constantes especiales para L2 y R2 como botones
if (button == TRIGGER_L2_AS_BUTTON) { // L2 como botón
Sint16 trigger_value = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER);
trigger_active_now = trigger_value > TRIGGER_THRESHOLD;
} else if (button == TRIGGER_R2_AS_BUTTON) { // R2 como botón
Sint16 trigger_value = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER);
trigger_active_now = trigger_value > TRIGGER_THRESHOLD;
} else {
return false; // No es un trigger
}
// Referencia al binding correspondiente
auto& binding = gamepad->bindings[action];
if (repeat) {
// Si se permite repetir, simplemente devolvemos el estado actual
return trigger_active_now;
}
// Si no se permite repetir, aplicamos la lógica de transición
if (trigger_active_now && !binding.trigger_active) {
// Transición de inactivo a activo
binding.trigger_active = true;
return true;
}
if (!trigger_active_now && binding.trigger_active) {
// Transición de activo a inactivo
binding.trigger_active = false;
}
// Mantener el estado actual
return false;
}
return false;
}
void Input::addGamepadMappingsFromFile() {
if (SDL_AddGamepadMappingsFromFile(gamepad_mappings_file_.c_str()) < 0) {
std::cout << "Error, could not load " << gamepad_mappings_file_.c_str() << " file: " << SDL_GetError() << '\n';
}
}
void Input::discoverGamepads() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
handleEvent(event); // Comprueba mandos conectados
}
}
void Input::initSDLGamePad() {
if (SDL_WasInit(SDL_INIT_GAMEPAD) != 1) {
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_GAMEPAD could not initialize! SDL Error: %s", SDL_GetError());
} else {
addGamepadMappingsFromFile();
discoverGamepads();
std::cout << "\n** INPUT SYSTEM **\n";
std::cout << "Input System initialized successfully\n";
}
}
}
void Input::resetInputStates() {
// Resetear todos los KeyBindings.active a false
for (auto& key : keyboard_.bindings) {
key.second.is_held = false;
key.second.just_pressed = false;
}
// Resetear todos los ControllerBindings.active a false
for (const auto& gamepad : gamepads_) {
for (auto& binding : gamepad->bindings) {
binding.second.is_held = false;
binding.second.just_pressed = false;
binding.second.trigger_active = false;
}
}
}
void Input::update() {
// --- TECLADO ---
const bool* key_states = SDL_GetKeyboardState(nullptr);
// Actualizar bindings globales (F1-F4, ESC)
for (auto& binding : keyboard_.bindings) {
bool key_is_down_now = key_states[binding.second.scancode];
// El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
binding.second.just_pressed = key_is_down_now && !binding.second.is_held;
binding.second.is_held = key_is_down_now;
}
// Actualizar bindings de jugador 1
for (auto& binding : player1_keyboard_bindings_) {
bool key_is_down_now = key_states[binding.second.scancode];
binding.second.just_pressed = key_is_down_now && !binding.second.is_held;
binding.second.is_held = key_is_down_now;
}
// Actualizar bindings de jugador 2
for (auto& binding : player2_keyboard_bindings_) {
bool key_is_down_now = key_states[binding.second.scancode];
binding.second.just_pressed = key_is_down_now && !binding.second.is_held;
binding.second.is_held = key_is_down_now;
}
// --- MANDOS ---
for (const auto& gamepad : gamepads_) {
for (auto& binding : gamepad->bindings) {
bool button_is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(binding.second.button))) != 0;
// El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
binding.second.just_pressed = button_is_down_now && !binding.second.is_held;
binding.second.is_held = button_is_down_now;
}
}
}
auto Input::handleEvent(const SDL_Event& event) -> std::string {
switch (event.type) {
case SDL_EVENT_GAMEPAD_ADDED:
return addGamepad(event.gdevice.which);
case SDL_EVENT_GAMEPAD_REMOVED:
return removeGamepad(event.gdevice.which);
}
return {};
}
auto Input::addGamepad(int device_index) -> std::string {
SDL_Gamepad* pad = SDL_OpenGamepad(device_index);
if (pad == nullptr) {
std::cerr << "Error al abrir el gamepad: " << SDL_GetError() << '\n';
return {};
}
auto gamepad = std::make_shared<Gamepad>(pad);
auto name = gamepad->name;
std::cout << "Gamepad connected (" << name << ")" << '\n';
gamepads_.push_back(std::move(gamepad));
return name + " CONNECTED";
}
auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
auto it = std::ranges::find_if(gamepads_, [id](const std::shared_ptr<Gamepad>& gamepad) {
return gamepad->instance_id == id;
});
if (it != gamepads_.end()) {
std::string name = (*it)->name;
std::cout << "Gamepad disconnected (" << name << ")" << '\n';
gamepads_.erase(it);
return name + " DISCONNECTED";
}
std::cerr << "No se encontró el gamepad con ID " << id << '\n';
return {};
}
void Input::printConnectedGamepads() const {
if (gamepads_.empty()) {
std::cout << "No hay gamepads conectados." << '\n';
return;
}
std::cout << "Gamepads conectados:\n";
for (const auto& gamepad : gamepads_) {
std::string name = gamepad->name.empty() ? "Desconocido" : gamepad->name;
std::cout << " - ID: " << gamepad->instance_id
<< ", Nombre: " << name << ")" << '\n';
}
}
auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr<Input::Gamepad> {
// Si no hay gamepads disponibles, devolver gamepad por defecto
if (gamepads_.empty()) {
return nullptr;
}
// Buscar por nombre
for (const auto& gamepad : gamepads_) {
if (gamepad && gamepad->name == gamepad_name) {
return gamepad;
}
}
// Si no se encuentra por nombre, devolver el primer gamepad válido
for (const auto& gamepad : gamepads_) {
if (gamepad) {
return gamepad;
}
}
// Si llegamos aquí, no hay gamepads válidos
return nullptr;
}
// ========== MÉTODOS ESPECÍFICOS POR JUGADOR (ORNI) ==========
// Aplica configuración de controles del jugador 1
void Input::applyPlayer1BindingsFromOptions() {
// 1. Aplicar bindings de teclado (NO usar bindKey, llenar mapa específico)
player1_keyboard_bindings_[Action::LEFT].scancode = Options::player1.keyboard.key_left;
player1_keyboard_bindings_[Action::RIGHT].scancode = Options::player1.keyboard.key_right;
player1_keyboard_bindings_[Action::THRUST].scancode = Options::player1.keyboard.key_thrust;
player1_keyboard_bindings_[Action::SHOOT].scancode = Options::player1.keyboard.key_shoot;
player1_keyboard_bindings_[Action::START].scancode = Options::player1.keyboard.key_start;
// 2. Encontrar gamepad por nombre (o usar primer gamepad como fallback)
std::shared_ptr<Gamepad> gamepad = nullptr;
if (Options::player1.gamepad_name.empty()) {
// Fallback: usar primer gamepad disponible
gamepad = (gamepads_.size() > 0) ? gamepads_[0] : nullptr;
} else {
// Buscar por nombre
gamepad = findAvailableGamepadByName(Options::player1.gamepad_name);
}
if (!gamepad) {
player1_gamepad_ = nullptr;
return;
}
// 3. Aplicar bindings de gamepad
gamepad->bindings[Action::LEFT].button = Options::player1.gamepad.button_left;
gamepad->bindings[Action::RIGHT].button = Options::player1.gamepad.button_right;
gamepad->bindings[Action::THRUST].button = Options::player1.gamepad.button_thrust;
gamepad->bindings[Action::SHOOT].button = Options::player1.gamepad.button_shoot;
// 4. Cachear referencia
player1_gamepad_ = gamepad;
}
// Aplica configuración de controles del jugador 2
void Input::applyPlayer2BindingsFromOptions() {
// 1. Aplicar bindings de teclado (mapa específico de P2, no sobrescribe P1)
player2_keyboard_bindings_[Action::LEFT].scancode = Options::player2.keyboard.key_left;
player2_keyboard_bindings_[Action::RIGHT].scancode = Options::player2.keyboard.key_right;
player2_keyboard_bindings_[Action::THRUST].scancode = Options::player2.keyboard.key_thrust;
player2_keyboard_bindings_[Action::SHOOT].scancode = Options::player2.keyboard.key_shoot;
player2_keyboard_bindings_[Action::START].scancode = Options::player2.keyboard.key_start;
// 2. Encontrar gamepad por nombre (o usar segundo gamepad como fallback)
std::shared_ptr<Gamepad> gamepad = nullptr;
if (Options::player2.gamepad_name.empty()) {
// Fallback: usar segundo gamepad disponible
gamepad = (gamepads_.size() > 1) ? gamepads_[1] : nullptr;
} else {
// Buscar por nombre
gamepad = findAvailableGamepadByName(Options::player2.gamepad_name);
}
if (!gamepad) {
player2_gamepad_ = nullptr;
return;
}
// 3. Aplicar bindings de gamepad
gamepad->bindings[Action::LEFT].button = Options::player2.gamepad.button_left;
gamepad->bindings[Action::RIGHT].button = Options::player2.gamepad.button_right;
gamepad->bindings[Action::THRUST].button = Options::player2.gamepad.button_thrust;
gamepad->bindings[Action::SHOOT].button = Options::player2.gamepad.button_shoot;
// 4. Cachear referencia
player2_gamepad_ = gamepad;
}
// Consulta de input para jugador 1
auto Input::checkActionPlayer1(Action action, bool repeat) -> bool {
// Comprobar teclado con el mapa específico de P1
bool keyboard_active = false;
if (player1_keyboard_bindings_.contains(action)) {
if (repeat) {
keyboard_active = player1_keyboard_bindings_[action].is_held;
} else {
keyboard_active = player1_keyboard_bindings_[action].just_pressed;
}
}
// Comprobar gamepad de P1
bool gamepad_active = false;
if (player1_gamepad_) {
gamepad_active = checkAction(action, repeat, DO_NOT_CHECK_KEYBOARD, player1_gamepad_);
}
return keyboard_active || gamepad_active;
}
// Consulta de input para jugador 2
auto Input::checkActionPlayer2(Action action, bool repeat) -> bool {
// Comprobar teclado con el mapa específico de P2
bool keyboard_active = false;
if (player2_keyboard_bindings_.contains(action)) {
if (repeat) {
keyboard_active = player2_keyboard_bindings_[action].is_held;
} else {
keyboard_active = player2_keyboard_bindings_[action].just_pressed;
}
}
// Comprobar gamepad de P2
bool gamepad_active = false;
if (player2_gamepad_) {
gamepad_active = checkAction(action, repeat, DO_NOT_CHECK_KEYBOARD, player2_gamepad_);
}
return keyboard_active || gamepad_active;
}

162
source/core/input/input.hpp Normal file
View File

@@ -0,0 +1,162 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_Scancode, SDL_GamepadButton, SDL_JoystickID, SDL_CloseGamepad, SDL_Gamepad, SDL_GetGamepadJoystick, SDL_GetGamepadName, SDL_GetGamepadPath, SDL_GetJoystickID, Sint16, Uint8, SDL_Event
#include <array> // Para array
#include <memory> // Para shared_ptr
#include <span> // Para span
#include <string> // Para string, basic_string
#include <unordered_map> // Para unordered_map
#include <utility> // Para pair
#include <vector> // Para vector
#include "core/input/input_types.hpp" // for InputAction
// --- Clase Input: gestiona la entrada de teclado y mandos (singleton) ---
class Input {
public:
// --- Constantes ---
static constexpr bool ALLOW_REPEAT = true; // Permite repetición
static constexpr bool DO_NOT_ALLOW_REPEAT = false; // No permite repetición
static constexpr bool CHECK_KEYBOARD = true; // Comprueba teclado
static constexpr bool DO_NOT_CHECK_KEYBOARD = false; // No comprueba teclado
static constexpr int TRIGGER_L2_AS_BUTTON = 100; // L2 como botón
static constexpr int TRIGGER_R2_AS_BUTTON = 101; // R2 como botón
// --- Tipos ---
using Action = InputAction; // Alias para mantener compatibilidad
// --- Estructuras ---
struct KeyState {
Uint8 scancode{0}; // Scancode asociado
bool is_held{false}; // Está pulsada ahora mismo
bool just_pressed{false}; // Se acaba de pulsar en este fotograma
};
struct ButtonState {
int button{static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)}; // GameControllerButton asociado
bool is_held{false}; // Está pulsada ahora mismo
bool just_pressed{false}; // Se acaba de pulsar en este fotograma
bool axis_active{false}; // Estado del eje
bool trigger_active{false}; // Estado del trigger como botón digital
};
struct Keyboard {
std::unordered_map<Action, KeyState> bindings; // Mapa de acciones a estados de tecla
};
struct Gamepad {
SDL_Gamepad* pad{nullptr}; // Puntero al gamepad SDL
SDL_JoystickID instance_id{0}; // ID de instancia del joystick
std::string name; // Nombre del gamepad
std::string path; // Ruta del dispositivo
std::unordered_map<Action, ButtonState> bindings; // Mapa de acciones a estados de botón
explicit Gamepad(SDL_Gamepad* gamepad)
: pad(gamepad),
instance_id(SDL_GetJoystickID(SDL_GetGamepadJoystick(gamepad))),
name(std::string(SDL_GetGamepadName(gamepad))),
path(std::string(SDL_GetGamepadPath(pad))),
bindings{
// Movimiento y acciones del jugador
{Action::LEFT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}},
{Action::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}},
{Action::THRUST, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}},
{Action::SHOOT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH)}}} {}
~Gamepad() {
if (pad != nullptr) {
SDL_CloseGamepad(pad);
}
}
// Reasigna un botón a una acción
void rebindAction(Action action, SDL_GamepadButton new_button) {
bindings[action].button = static_cast<int>(new_button);
}
};
// --- Tipos ---
using Gamepads = std::vector<std::shared_ptr<Gamepad>>; // Vector de gamepads
// --- Singleton ---
static void init(const std::string& game_controller_db_path);
static void destroy();
static auto get() -> Input*;
// --- Actualización del sistema ---
void update(); // Actualiza estados de entrada
// --- Configuración de controles ---
void bindKey(Action action, SDL_Scancode code);
void applyKeyboardBindingsFromOptions();
void applyGamepadBindingsFromOptions();
// Configuración por jugador (Orni - dos jugadores)
void applyPlayer1BindingsFromOptions();
void applyPlayer2BindingsFromOptions();
static void bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action, SDL_GamepadButton button);
static void bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action_target, Action action_source);
// --- Consulta de entrada ---
auto checkAction(Action action, bool repeat = true, bool check_keyboard = true, const std::shared_ptr<Gamepad>& gamepad = nullptr) -> bool;
auto checkAnyInput(bool check_keyboard = true, const std::shared_ptr<Gamepad>& gamepad = nullptr) -> bool;
auto checkAnyButton(bool repeat = DO_NOT_ALLOW_REPEAT) -> bool;
void resetInputStates();
// Consulta por jugador (Orni - dos jugadores)
auto checkActionPlayer1(Action action, bool repeat = true) -> bool;
auto checkActionPlayer2(Action action, bool repeat = true) -> bool;
// Check if any player pressed any action from a list
auto checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat = DO_NOT_ALLOW_REPEAT) -> bool;
// --- Gestión de gamepads ---
[[nodiscard]] auto gameControllerFound() const -> bool;
[[nodiscard]] auto getNumGamepads() const -> int;
auto getGamepad(SDL_JoystickID id) const -> std::shared_ptr<Gamepad>;
auto getGamepadByName(const std::string& name) const -> std::shared_ptr<Input::Gamepad>;
auto getGamepads() const -> const Gamepads& { return gamepads_; }
auto findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr<Gamepad>;
static auto getControllerName(const std::shared_ptr<Gamepad>& gamepad) -> std::string;
auto getControllerNames() const -> std::vector<std::string>;
[[nodiscard]] static auto getControllerBinding(const std::shared_ptr<Gamepad>& gamepad, Action action) -> SDL_GamepadButton;
void printConnectedGamepads() const;
// --- Eventos ---
auto handleEvent(const SDL_Event& event) -> std::string;
private:
// --- Constantes ---
static constexpr Sint16 AXIS_THRESHOLD = 30000; // Umbral para ejes analógicos
static constexpr Sint16 TRIGGER_THRESHOLD = 16384; // Umbral para triggers (50% del rango)
static constexpr std::array<Action, 1> BUTTON_INPUTS = {Action::SHOOT}; // Inputs que usan botones
// --- Métodos ---
explicit Input(std::string game_controller_db_path);
~Input() = default;
void initSDLGamePad();
static auto checkAxisInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool;
static auto checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool;
auto addGamepad(int device_index) -> std::string;
auto removeGamepad(SDL_JoystickID id) -> std::string;
void addGamepadMappingsFromFile();
void discoverGamepads();
// --- Variables miembro ---
static Input* instance; // Instancia única del singleton
Gamepads gamepads_; // Lista de gamepads conectados
Keyboard keyboard_{}; // Estado del teclado (solo acciones globales)
std::string gamepad_mappings_file_; // Ruta al archivo de mappings
// Referencias cacheadas a gamepads por jugador (Orni)
std::shared_ptr<Gamepad> player1_gamepad_;
std::shared_ptr<Gamepad> player2_gamepad_;
// Mapas de bindings separados por jugador (Orni - dos jugadores)
std::unordered_map<Action, KeyState> player1_keyboard_bindings_;
std::unordered_map<Action, KeyState> player2_keyboard_bindings_;
};

View File

@@ -0,0 +1,60 @@
#include "input_types.hpp"
#include <utility> // Para pair
// Definición de los mapas
const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
{InputAction::LEFT, "LEFT"},
{InputAction::RIGHT, "RIGHT"},
{InputAction::THRUST, "THRUST"},
{InputAction::SHOOT, "SHOOT"},
{InputAction::WINDOW_INC_ZOOM, "WINDOW_INC_ZOOM"},
{InputAction::WINDOW_DEC_ZOOM, "WINDOW_DEC_ZOOM"},
{InputAction::TOGGLE_FULLSCREEN, "TOGGLE_FULLSCREEN"},
{InputAction::TOGGLE_VSYNC, "TOGGLE_VSYNC"},
{InputAction::EXIT, "EXIT"},
{InputAction::NONE, "NONE"}};
const std::unordered_map<std::string, InputAction> STRING_TO_ACTION = {
{"LEFT", InputAction::LEFT},
{"RIGHT", InputAction::RIGHT},
{"THRUST", InputAction::THRUST},
{"SHOOT", InputAction::SHOOT},
{"WINDOW_INC_ZOOM", InputAction::WINDOW_INC_ZOOM},
{"WINDOW_DEC_ZOOM", InputAction::WINDOW_DEC_ZOOM},
{"TOGGLE_FULLSCREEN", InputAction::TOGGLE_FULLSCREEN},
{"TOGGLE_VSYNC", InputAction::TOGGLE_VSYNC},
{"EXIT", InputAction::EXIT},
{"NONE", InputAction::NONE}};
const std::unordered_map<SDL_GamepadButton, std::string> BUTTON_TO_STRING = {
{SDL_GAMEPAD_BUTTON_WEST, "WEST"},
{SDL_GAMEPAD_BUTTON_NORTH, "NORTH"},
{SDL_GAMEPAD_BUTTON_EAST, "EAST"},
{SDL_GAMEPAD_BUTTON_SOUTH, "SOUTH"},
{SDL_GAMEPAD_BUTTON_START, "START"},
{SDL_GAMEPAD_BUTTON_BACK, "BACK"},
{SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, "LEFT_SHOULDER"},
{SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, "RIGHT_SHOULDER"},
{SDL_GAMEPAD_BUTTON_DPAD_UP, "DPAD_UP"},
{SDL_GAMEPAD_BUTTON_DPAD_DOWN, "DPAD_DOWN"},
{SDL_GAMEPAD_BUTTON_DPAD_LEFT, "DPAD_LEFT"},
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, "DPAD_RIGHT"},
{static_cast<SDL_GamepadButton>(100), "L2_AS_BUTTON"},
{static_cast<SDL_GamepadButton>(101), "R2_AS_BUTTON"}};
const std::unordered_map<std::string, SDL_GamepadButton> STRING_TO_BUTTON = {
{"WEST", SDL_GAMEPAD_BUTTON_WEST},
{"NORTH", SDL_GAMEPAD_BUTTON_NORTH},
{"EAST", SDL_GAMEPAD_BUTTON_EAST},
{"SOUTH", SDL_GAMEPAD_BUTTON_SOUTH},
{"START", SDL_GAMEPAD_BUTTON_START},
{"BACK", SDL_GAMEPAD_BUTTON_BACK},
{"LEFT_SHOULDER", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
{"RIGHT_SHOULDER", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
{"DPAD_UP", SDL_GAMEPAD_BUTTON_DPAD_UP},
{"DPAD_DOWN", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
{"DPAD_LEFT", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
{"DPAD_RIGHT", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
{"L2_AS_BUTTON", static_cast<SDL_GamepadButton>(100)},
{"R2_AS_BUTTON", static_cast<SDL_GamepadButton>(101)}};

View File

@@ -0,0 +1,41 @@
#pragma once
#include <SDL3/SDL.h>
#include <array>
#include <string>
#include <unordered_map>
// --- Enums ---
enum class InputAction : int { // Acciones de entrada posibles en el juego
// Inputs de juego (movimiento y acción)
LEFT, // Rotar izquierda
RIGHT, // Rotar derecha
THRUST, // Acelerar
SHOOT, // Disparar
START, // Empezar partida
// Inputs de sistema (globales)
WINDOW_INC_ZOOM, // F2
WINDOW_DEC_ZOOM, // F1
TOGGLE_FULLSCREEN, // F3
TOGGLE_VSYNC, // F4
EXIT, // ESC
// Input obligatorio
NONE,
SIZE,
};
// --- Variables ---
extern const std::unordered_map<InputAction, std::string> ACTION_TO_STRING; // Mapeo de acción a string
extern const std::unordered_map<std::string, InputAction> STRING_TO_ACTION; // Mapeo de string a acción
extern const std::unordered_map<SDL_GamepadButton, std::string> BUTTON_TO_STRING; // Mapeo de botón a string
extern const std::unordered_map<std::string, SDL_GamepadButton> STRING_TO_BUTTON; // Mapeo de string a botón
// --- Constantes ---
// Physical arcade buttons (excludes directional controls LEFT/RIGHT)
static constexpr std::array<InputAction, 3> ARCADE_BUTTONS = {
InputAction::SHOOT,
InputAction::THRUST,
InputAction::START};

View File

@@ -1,29 +1,44 @@
#include "core/input/mouse.hpp"
#include <iostream>
namespace Mouse {
Uint32 cursor_hide_time = 3000; // Tiempo en milisegundos para ocultar el cursor
Uint32 last_mouse_move_time = 0; // Última vez que el ratón se movió
bool cursor_visible = true; // Estado del cursor
bool cursor_visible = false; // Estado del cursor (inicia ocult)
// Modo forzado: Usado cuando SDLManager entra en pantalla completa.
// Cuando está activado, el cursor permanece oculto independientemente del movimiento del ratón.
// SDLManager controla esto mediante llamadas a setForceHidden().
bool force_hidden = false;
// Temps d'inicialització per ignorar esdeveniments fantasma de SDL
Uint32 initialization_time = 0;
constexpr Uint32 IGNORE_MOTION_DURATION = 1000; // Ignorar primers 1000ms
void forceHide() {
// Forçar ocultació sincronitzant estat SDL i estat intern
std::cout << "[Mouse::forceHide] Ocultant cursor i sincronitzant estat. cursor_visible=" << cursor_visible
<< " -> false" << std::endl;
SDL_HideCursor();
cursor_visible = false;
last_mouse_move_time = 0;
initialization_time = SDL_GetTicks(); // Marcar temps per ignorar esdeveniments inicials
std::cout << "[Mouse::forceHide] Ignorant moviments durant " << IGNORE_MOTION_DURATION << "ms" << std::endl;
}
void setForceHidden(bool force) {
force_hidden = force;
if (force) {
// Entrando en modo oculto forzado: ocultar cursor inmediatamente
if (cursor_visible) {
SDL_HideCursor();
cursor_visible = false;
}
SDL_HideCursor();
cursor_visible = false;
} else {
// Saliendo de modo oculto forzado: mostrar cursor y resetear temporizador
SDL_ShowCursor();
cursor_visible = true;
// Saliendo de modo oculto forzado: NO mostrar cursor automáticamente
// El cursor permanece oculto hasta que haya movimiento de ratón (handleEvent)
last_mouse_move_time = SDL_GetTicks(); // Resetear temporizador
// cursor_visible permanece false - handleEvent lo cambiará al detectar movimiento
}
}
@@ -39,8 +54,18 @@ void handleEvent(const SDL_Event& event) {
// MODO NORMAL: Mostrar cursor al mover el ratón
if (event.type == SDL_EVENT_MOUSE_MOTION) {
last_mouse_move_time = SDL_GetTicks();
Uint32 current_time = SDL_GetTicks();
// Ignorar esdeveniments fantasma de SDL durant el període inicial
if (initialization_time > 0 && (current_time - initialization_time < IGNORE_MOTION_DURATION)) {
std::cout << "[Mouse::handleEvent] Ignorant moviment fantasma de SDL. time=" << current_time
<< " (inicialització fa " << (current_time - initialization_time) << "ms)" << std::endl;
return;
}
last_mouse_move_time = current_time;
if (!cursor_visible) {
std::cout << "[Mouse::handleEvent] Mostrant cursor per moviment REAL. time=" << last_mouse_move_time << std::endl;
SDL_ShowCursor();
cursor_visible = true;
}
@@ -56,6 +81,8 @@ void updateCursorVisibility() {
// MODO NORMAL: Auto-ocultar basado en timeout
Uint32 current_time = SDL_GetTicks();
if (cursor_visible && (current_time - last_mouse_move_time > cursor_hide_time)) {
std::cout << "[Mouse::updateCursorVisibility] Ocultant cursor per timeout. current=" << current_time
<< " last=" << last_mouse_move_time << " diff=" << (current_time - last_mouse_move_time) << std::endl;
SDL_HideCursor();
cursor_visible = false;
}

View File

@@ -7,6 +7,7 @@ extern Uint32 cursor_hide_time; // Tiempo en milisegundos para ocultar el c
extern Uint32 last_mouse_move_time; // Última vez que el ratón se movió
extern bool cursor_visible; // Estado del cursor
void forceHide(); // Forçar ocultació del cursor (sincronitza estat intern)
void handleEvent(const SDL_Event& event);
void updateCursorVisibility();

View File

@@ -0,0 +1,44 @@
// easing.hpp - Funcions d'interpolació i easing
// © 2025 Orni Attack
#pragma once
namespace Easing {
// Ease-out quadratic: empieza rápido, desacelera suavemente
// t = progreso normalizado [0.0 - 1.0]
// retorna valor interpolado [0.0 - 1.0]
inline float ease_out_quad(float t) {
return 1.0f - (1.0f - t) * (1.0f - t);
}
// Ease-in quadratic: empieza lento, acelera
// t = progreso normalizado [0.0 - 1.0]
// retorna valor interpolado [0.0 - 1.0]
inline float ease_in_quad(float t) {
return t * t;
}
// Ease-in-out quadratic: acelera al inicio, desacelera al final
// t = progreso normalizado [0.0 - 1.0]
// retorna valor interpolado [0.0 - 1.0]
inline float ease_in_out_quad(float t) {
return (t < 0.5f)
? 2.0f * t * t
: 1.0f - (-2.0f * t + 2.0f) * (-2.0f * t + 2.0f) / 2.0f;
}
// Ease-out cubic: desaceleración más suave que quadratic
// t = progreso normalizado [0.0 - 1.0]
// retorna valor interpolado [0.0 - 1.0]
inline float ease_out_cubic(float t) {
float t1 = 1.0f - t;
return 1.0f - t1 * t1 * t1;
}
// Interpolación lineal básica (para referencia)
inline float lerp(float start, float end, float t) {
return start + (end - start) * t;
}
} // namespace Easing

View File

@@ -141,6 +141,12 @@ SDLManager::SDLManager(int width, int height, bool fullscreen)
// Configurar viewport scaling
updateLogicalPresentation();
// Inicialitzar sistema de cursor
// En fullscreen: forzar ocultació permanent
if (is_fullscreen_) {
Mouse::setForceHidden(true);
}
std::cout << "SDL3 inicialitzat: " << current_width_ << "x" << current_height_
<< " (logic: " << Defaults::Game::WIDTH << "x"
<< Defaults::Game::HEIGHT << ")";
@@ -148,9 +154,6 @@ SDLManager::SDLManager(int width, int height, bool fullscreen)
std::cout << " [FULLSCREEN]";
}
std::cout << std::endl;
// Inicialitzar mòdul Mouse amb l'estat actual de fullscreen
Mouse::setForceHidden(is_fullscreen_);
}
SDLManager::~SDLManager() {

View File

@@ -13,8 +13,8 @@
class SDLManager {
public:
SDLManager(); // Constructor per defecte (usa Defaults::)
SDLManager(int width, int height, bool fullscreen); // Constructor amb configuració
SDLManager(); // Constructor per defecte (usa Defaults::)
SDLManager(int width, int height, bool fullscreen); // Constructor amb configuració
~SDLManager();
// No permetre còpia ni assignació
@@ -22,11 +22,11 @@ class SDLManager {
SDLManager& operator=(const SDLManager&) = delete;
// [NUEVO] Gestió de finestra dinàmica
void increaseWindowSize(); // F2: +100px
void decreaseWindowSize(); // F1: -100px
void toggleFullscreen(); // F3
void toggleVSync(); // F4
bool handleWindowEvent(const SDL_Event& event); // Per a SDL_EVENT_WINDOW_RESIZED
void increaseWindowSize(); // F2: +100px
void decreaseWindowSize(); // F1: -100px
void toggleFullscreen(); // F3
void toggleVSync(); // F4
bool handleWindowEvent(const SDL_Event& event); // Per a SDL_EVENT_WINDOW_RESIZED
// Funcions principals (renderitzat)
void neteja(uint8_t r = 0, uint8_t g = 0, uint8_t b = 0);

View File

@@ -10,17 +10,55 @@
namespace Rendering {
// Helper: aplicar rotació 3D a un punt 2D (assumeix Z=0)
static Punt apply_3d_rotation(float x, float y, const Rotation3D& rot) {
float z = 0.0f; // Tots els punts 2D comencen a Z=0
// Pitch (rotació eix X): cabeceo arriba/baix
float cos_pitch = std::cos(rot.pitch);
float sin_pitch = std::sin(rot.pitch);
float y1 = y * cos_pitch - z * sin_pitch;
float z1 = y * sin_pitch + z * cos_pitch;
// Yaw (rotació eix Y): guiñada esquerra/dreta
float cos_yaw = std::cos(rot.yaw);
float sin_yaw = std::sin(rot.yaw);
float x2 = x * cos_yaw + z1 * sin_yaw;
float z2 = -x * sin_yaw + z1 * cos_yaw;
// Roll (rotació eix Z): alabeo lateral
float cos_roll = std::cos(rot.roll);
float sin_roll = std::sin(rot.roll);
float x3 = x2 * cos_roll - y1 * sin_roll;
float y3 = x2 * sin_roll + y1 * cos_roll;
// Proyecció perspectiva (Z-divide simple)
// Naus volen cap al punt de fuga (320, 240) a "infinit" (Z → +∞)
// Z més gran = més lluny = més petit a pantalla
constexpr float perspective_factor = 500.0f;
float scale_factor = perspective_factor / (perspective_factor + z2);
return {x3 * scale_factor, y3 * scale_factor};
}
// Helper: transformar un punt amb rotació, escala i trasllació
static Punt transform_point(const Punt& point, const Punt& shape_centre, const Punt& posicio, float angle, float escala) {
static Punt transform_point(const Punt& point, const Punt& shape_centre, const Punt& posicio, float angle, float escala, const Rotation3D* rotation_3d) {
// 1. Centrar el punt respecte al centre de la forma
float centered_x = point.x - shape_centre.x;
float centered_y = point.y - shape_centre.y;
// 2. Aplicar escala al punt centrat
// 2. Aplicar rotació 3D (si es proporciona)
if (rotation_3d && rotation_3d->has_rotation()) {
Punt rotated_3d = apply_3d_rotation(centered_x, centered_y, *rotation_3d);
centered_x = rotated_3d.x;
centered_y = rotated_3d.y;
}
// 3. Aplicar escala al punt (després de rotació 3D)
float scaled_x = centered_x * escala;
float scaled_y = centered_y * escala;
// 3. Aplicar rotació
// 4. Aplicar rotació 2D (Z-axis, tradicional)
// IMPORTANT: En el sistema original, angle=0 apunta AMUNT (no dreta)
// Per això usem (angle - PI/2) per compensar
// Però aquí angle ja ve en el sistema correcte del joc
@@ -30,7 +68,7 @@ static Punt transform_point(const Punt& point, const Punt& shape_centre, const P
float rotated_x = scaled_x * cos_a - scaled_y * sin_a;
float rotated_y = scaled_x * sin_a + scaled_y * cos_a;
// 4. Aplicar trasllació a posició mundial
// 5. Aplicar trasllació a posició mundial
return {rotated_x + posicio.x, rotated_y + posicio.y};
}
@@ -41,7 +79,8 @@ void render_shape(SDL_Renderer* renderer,
float escala,
bool dibuixar,
float progress,
float brightness) {
float brightness,
const Rotation3D* rotation_3d) {
// Verificar que la forma és vàlida
if (!shape || !shape->es_valida()) {
return;
@@ -60,16 +99,16 @@ void render_shape(SDL_Renderer* renderer,
if (primitive.type == Graphics::PrimitiveType::POLYLINE) {
// POLYLINE: connectar punts consecutius
for (size_t i = 0; i < primitive.points.size() - 1; i++) {
Punt p1 = transform_point(primitive.points[i], shape_centre, posicio, angle, escala);
Punt p2 = transform_point(primitive.points[i + 1], shape_centre, posicio, angle, escala);
Punt p1 = transform_point(primitive.points[i], shape_centre, posicio, angle, escala, rotation_3d);
Punt p2 = transform_point(primitive.points[i + 1], shape_centre, posicio, angle, escala, rotation_3d);
linea(renderer, static_cast<int>(p1.x), static_cast<int>(p1.y), static_cast<int>(p2.x), static_cast<int>(p2.y), dibuixar, brightness);
}
} else { // PrimitiveType::LINE
// LINE: exactament 2 punts
if (primitive.points.size() >= 2) {
Punt p1 = transform_point(primitive.points[0], shape_centre, posicio, angle, escala);
Punt p2 = transform_point(primitive.points[1], shape_centre, posicio, angle, escala);
Punt p1 = transform_point(primitive.points[0], shape_centre, posicio, angle, escala, rotation_3d);
Punt p2 = transform_point(primitive.points[1], shape_centre, posicio, angle, escala, rotation_3d);
linea(renderer, static_cast<int>(p1.x), static_cast<int>(p1.y), static_cast<int>(p2.x), static_cast<int>(p2.y), dibuixar, brightness);
}

View File

@@ -12,6 +12,26 @@
namespace Rendering {
// Estructura per rotacions 3D (pitch, yaw, roll)
struct Rotation3D {
float pitch; // Rotació eix X (cabeceo arriba/baix)
float yaw; // Rotació eix Y (guiñada esquerra/dreta)
float roll; // Rotació eix Z (alabeo lateral)
Rotation3D()
: pitch(0.0f),
yaw(0.0f),
roll(0.0f) {}
Rotation3D(float p, float y, float r)
: pitch(p),
yaw(y),
roll(r) {}
bool has_rotation() const {
return pitch != 0.0f || yaw != 0.0f || roll != 0.0f;
}
};
// Renderitzar forma amb transformacions
// - renderer: SDL renderer
// - shape: forma vectorial a dibuixar
@@ -28,6 +48,7 @@ void render_shape(SDL_Renderer* renderer,
float escala = 1.0f,
bool dibuixar = true,
float progress = 1.0f,
float brightness = 1.0f);
float brightness = 1.0f,
const Rotation3D* rotation_3d = nullptr);
} // namespace Rendering

View File

@@ -3,11 +3,11 @@
#include "resource_helper.hpp"
#include "resource_loader.hpp"
#include <algorithm>
#include <iostream>
#include "resource_loader.hpp"
namespace Resource {
namespace Helper {

View File

@@ -4,50 +4,50 @@
#pragma once
#include "resource_pack.hpp"
#include <memory>
#include <string>
#include <vector>
#include "resource_pack.hpp"
namespace Resource {
// Singleton per gestionar la càrrega de recursos
class Loader {
public:
// Singleton
static Loader& get();
public:
// Singleton
static Loader& get();
// Inicialització
bool initialize(const std::string& pack_file, bool enable_fallback);
// Inicialització
bool initialize(const std::string& pack_file, bool enable_fallback);
// Càrrega de recursos
std::vector<uint8_t> loadResource(const std::string& filename);
bool resourceExists(const std::string& filename);
// Càrrega de recursos
std::vector<uint8_t> loadResource(const std::string& filename);
bool resourceExists(const std::string& filename);
// Validació
bool validatePack();
bool isPackLoaded() const;
// Validació
bool validatePack();
bool isPackLoaded() const;
// Estat
void setBasePath(const std::string& path);
std::string getBasePath() const;
// Estat
void setBasePath(const std::string& path);
std::string getBasePath() const;
private:
Loader() = default;
~Loader() = default;
private:
Loader() = default;
~Loader() = default;
// No es pot copiar ni moure
Loader(const Loader&) = delete;
Loader& operator=(const Loader&) = delete;
// No es pot copiar ni moure
Loader(const Loader&) = delete;
Loader& operator=(const Loader&) = delete;
// Dades
std::unique_ptr<Pack> pack_;
bool fallback_enabled_ = false;
std::string base_path_;
// Dades
std::unique_ptr<Pack> pack_;
bool fallback_enabled_ = false;
std::string base_path_;
// Funcions auxiliars
std::vector<uint8_t> loadFromFilesystem(const std::string& filename);
// Funcions auxiliars
std::vector<uint8_t> loadFromFilesystem(const std::string& filename);
};
} // namespace Resource

View File

@@ -13,55 +13,55 @@ namespace Resource {
// Capçalera del fitxer de paquet
struct PackHeader {
char magic[4]; // "ORNI"
uint32_t version; // Versió del format (1)
char magic[4]; // "ORNI"
uint32_t version; // Versió del format (1)
};
// Entrada de recurs dins el paquet
struct ResourceEntry {
std::string filename; // Nom del recurs (amb barres normals)
uint64_t offset; // Posició dins el bloc de dades
uint64_t size; // Mida en bytes
uint32_t checksum; // Checksum CRC32 per verificació
std::string filename; // Nom del recurs (amb barres normals)
uint64_t offset; // Posició dins el bloc de dades
uint64_t size; // Mida en bytes
uint32_t checksum; // Checksum CRC32 per verificació
};
// Classe principal per gestionar paquets de recursos
class Pack {
public:
Pack() = default;
~Pack() = default;
public:
Pack() = default;
~Pack() = default;
// Afegir fitxers al paquet
bool addFile(const std::string& filepath, const std::string& pack_name);
bool addDirectory(const std::string& dir_path, const std::string& base_path = "");
// Afegir fitxers al paquet
bool addFile(const std::string& filepath, const std::string& pack_name);
bool addDirectory(const std::string& dir_path, const std::string& base_path = "");
// Guardar i carregar paquets
bool savePack(const std::string& pack_file);
bool loadPack(const std::string& pack_file);
// Guardar i carregar paquets
bool savePack(const std::string& pack_file);
bool loadPack(const std::string& pack_file);
// Accés a recursos
std::vector<uint8_t> getResource(const std::string& filename);
bool hasResource(const std::string& filename) const;
std::vector<std::string> getResourceList() const;
// Accés a recursos
std::vector<uint8_t> getResource(const std::string& filename);
bool hasResource(const std::string& filename) const;
std::vector<std::string> getResourceList() const;
// Validació
bool validatePack() const;
// Validació
bool validatePack() const;
private:
// Constants
static constexpr const char* MAGIC_HEADER = "ORNI";
static constexpr uint32_t VERSION = 1;
static constexpr const char* DEFAULT_ENCRYPT_KEY = "ORNI_RESOURCES_2025";
private:
// Constants
static constexpr const char* MAGIC_HEADER = "ORNI";
static constexpr uint32_t VERSION = 1;
static constexpr const char* DEFAULT_ENCRYPT_KEY = "ORNI_RESOURCES_2025";
// Dades del paquet
std::unordered_map<std::string, ResourceEntry> resources_;
std::vector<uint8_t> data_;
// Dades del paquet
std::unordered_map<std::string, ResourceEntry> resources_;
std::vector<uint8_t> data_;
// Funcions auxiliars
std::vector<uint8_t> readFile(const std::string& filepath);
uint32_t calculateChecksum(const std::vector<uint8_t>& data) const;
void encryptData(std::vector<uint8_t>& data, const std::string& key);
void decryptData(std::vector<uint8_t>& data, const std::string& key);
// Funcions auxiliars
std::vector<uint8_t> readFile(const std::string& filepath);
uint32_t calculateChecksum(const std::vector<uint8_t>& data) const;
void encryptData(std::vector<uint8_t>& data, const std::string& key);
void decryptData(std::vector<uint8_t>& data, const std::string& key);
};
} // namespace Resource

View File

@@ -3,64 +3,77 @@
#pragma once
#include "core/system/game_config.hpp"
namespace GestorEscenes {
// Context de transició entre escenes
// Conté l'escena destinació i opcions específiques per aquella escena
class ContextEscenes {
public:
// Tipus d'escena del joc
enum class Escena {
LOGO, // Pantalla d'inici (logo JAILGAMES)
TITOL, // Pantalla de títol amb menú
JOC, // Joc principal (Asteroids)
EIXIR // Sortir del programa
};
public:
// Tipus d'escena del joc
enum class Escena {
LOGO, // Pantalla d'inici (logo JAILGAMES)
TITOL, // Pantalla de títol amb menú
JOC, // Joc principal (Asteroids)
EIXIR // Sortir del programa
};
// Opcions específiques per a cada escena
enum class Opcio {
NONE, // Sense opcions especials (comportament per defecte)
JUMP_TO_TITLE_MAIN, // TITOL: Saltar directament a MAIN (starfield instantani)
// MODE_DEMO, // JOC: Mode demostració amb IA (futur)
};
// Opcions específiques per a cada escena
enum class Opcio {
NONE, // Sense opcions especials (comportament per defecte)
JUMP_TO_TITLE_MAIN, // TITOL: Saltar directament a MAIN (starfield instantani)
// MODE_DEMO, // JOC: Mode demostració amb IA (futur)
};
// Constructor inicial amb escena LOGO i sense opcions
ContextEscenes()
: escena_desti_(Escena::LOGO),
opcio_(Opcio::NONE) {}
// Constructor inicial amb escena LOGO i sense opcions
ContextEscenes()
: escena_desti_(Escena::LOGO),
opcio_(Opcio::NONE) {}
// Canviar escena amb opció específica
void canviar_escena(Escena nova_escena, Opcio opcio = Opcio::NONE) {
escena_desti_ = nova_escena;
opcio_ = opcio;
}
// Canviar escena amb opció específica
void canviar_escena(Escena nova_escena, Opcio opcio = Opcio::NONE) {
escena_desti_ = nova_escena;
opcio_ = opcio;
}
// Consultar escena destinació
[[nodiscard]] auto escena_desti() const -> Escena {
return escena_desti_;
}
// Consultar escena destinació
[[nodiscard]] auto escena_desti() const -> Escena {
return escena_desti_;
}
// Consultar opció actual
[[nodiscard]] auto opcio() const -> Opcio {
return opcio_;
}
// Consultar opció actual
[[nodiscard]] auto opcio() const -> Opcio {
return opcio_;
}
// Consumir opció (retorna valor i reseteja a NONE)
// Utilitzar quan l'escena processa l'opció
[[nodiscard]] auto consumir_opcio() -> Opcio {
Opcio valor = opcio_;
opcio_ = Opcio::NONE;
return valor;
}
// Consumir opció (retorna valor i reseteja a NONE)
// Utilitzar quan l'escena processa l'opció
[[nodiscard]] auto consumir_opcio() -> Opcio {
Opcio valor = opcio_;
opcio_ = Opcio::NONE;
return valor;
}
// Reset opció a NONE (sense retornar valor)
void reset_opcio() {
opcio_ = Opcio::NONE;
}
// Reset opció a NONE (sense retornar valor)
void reset_opcio() {
opcio_ = Opcio::NONE;
}
private:
Escena escena_desti_; // Escena a la qual transicionar
Opcio opcio_; // Opció específica per l'escena
// Configurar partida abans de transicionar a JOC
void set_config_partida(const GameConfig::ConfigPartida& config) {
config_partida_ = config;
}
// Obtenir configuració de partida (consumit per EscenaJoc)
[[nodiscard]] const GameConfig::ConfigPartida& get_config_partida() const {
return config_partida_;
}
private:
Escena escena_desti_; // Escena a la qual transicionar
Opcio opcio_; // Opció específica per l'escena
GameConfig::ConfigPartida config_partida_; // Configuració de partida (jugadors actius, mode)
};
// Variable global inline per gestionar l'escena actual (backward compatibility)

View File

@@ -7,9 +7,12 @@
#include <cstdlib>
#include <iostream>
#include "context_escenes.hpp"
#include "core/audio/audio.hpp"
#include "core/audio/audio_cache.hpp"
#include "core/defaults.hpp"
#include "core/input/input.hpp"
#include "core/input/mouse.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/resources/resource_helper.hpp"
#include "core/resources/resource_loader.hpp"
@@ -18,7 +21,6 @@
#include "game/escenes/escena_logo.hpp"
#include "game/escenes/escena_titol.hpp"
#include "game/options.hpp"
#include "context_escenes.hpp"
#include "project.h"
#ifndef _WIN32
@@ -88,12 +90,21 @@ Director::Director(std::vector<std::string> const& args) {
// Carregar o crear configuració
Options::loadFromFile();
// Inicialitzar sistema d'input
Input::init("data/gamecontrollerdb.txt");
// Aplicar configuració de controls dels jugadors
Input::get()->applyPlayer1BindingsFromOptions();
Input::get()->applyPlayer2BindingsFromOptions();
if (Options::console) {
std::cout << "Configuració carregada\n";
std::cout << " Finestra: " << Options::window.width << "×"
<< Options::window.height << '\n';
std::cout << " Física: rotation=" << Options::physics.rotation_speed
<< " rad/s\n";
std::cout << " Input: " << Input::get()->getNumGamepads()
<< " gamepad(s) detectat(s)\n";
}
std::cout << '\n';
@@ -103,6 +114,9 @@ Director::~Director() {
// Guardar opcions
Options::saveToFile();
// Cleanup input
Input::destroy();
// Cleanup audio
Audio::destroy();
@@ -204,6 +218,12 @@ auto Director::run() -> int {
// Crear gestor SDL amb configuració de Options
SDLManager sdl(initial_width, initial_height, Options::window.fullscreen);
// CRÍTIC: Forçar ocultació del cursor DESPRÉS de tota la inicialització SDL
// Això evita que SDL mostre el cursor automàticament durant la creació de la finestra
if (!Options::window.fullscreen) {
Mouse::forceHide();
}
// Inicialitzar sistema d'audio
Audio::init();
Audio::get()->setMusicVolume(1.0);
@@ -211,6 +231,7 @@ auto Director::run() -> int {
// Precachejar música per evitar lag al començar
AudioCache::getMusic("title.ogg");
AudioCache::getMusic("game.ogg");
if (Options::console) {
std::cout << "Música precachejada: "
<< AudioCache::getMusicCacheSize() << " fitxers\n";
@@ -219,7 +240,7 @@ auto Director::run() -> int {
// Crear context d'escenes
ContextEscenes context;
#ifdef _DEBUG
context.canviar_escena(Escena::JOC);
context.canviar_escena(Escena::TITOL);
#else
context.canviar_escena(Escena::LOGO);
#endif

View File

@@ -0,0 +1,51 @@
#pragma once
#include <cstdint>
namespace GameConfig {
// Mode de joc
enum class Mode {
NORMAL, // Partida normal
DEMO // Mode demostració (futur)
};
// Configuració d'una partida
struct ConfigPartida {
bool jugador1_actiu{false}; // És actiu el jugador 1?
bool jugador2_actiu{false}; // És actiu el jugador 2?
Mode mode{Mode::NORMAL}; // Mode de joc
// Mètodes auxiliars
// Retorna true si només hi ha un jugador actiu
[[nodiscard]] bool es_un_jugador() const {
return (jugador1_actiu && !jugador2_actiu) ||
(!jugador1_actiu && jugador2_actiu);
}
// Retorna true si hi ha dos jugadors actius
[[nodiscard]] bool son_dos_jugadors() const {
return jugador1_actiu && jugador2_actiu;
}
// Retorna true si no hi ha cap jugador actiu
[[nodiscard]] bool cap_jugador() const {
return !jugador1_actiu && !jugador2_actiu;
}
// Compte de jugadors actius (0, 1 o 2)
[[nodiscard]] uint8_t compte_jugadors() const {
return (jugador1_actiu ? 1 : 0) + (jugador2_actiu ? 1 : 0);
}
// Retorna l'ID de l'únic jugador actiu (0 o 1)
// Només vàlid si es_un_jugador() retorna true
[[nodiscard]] uint8_t id_unic_jugador() const {
if (jugador1_actiu && !jugador2_actiu) return 0;
if (!jugador1_actiu && jugador2_actiu) return 1;
return 0; // Fallback (cal comprovar es_un_jugador() primer)
}
};
} // namespace GameConfig

View File

@@ -3,9 +3,12 @@
#include "global_events.hpp"
#include <iostream>
#include "context_escenes.hpp"
#include "core/input/input.hpp"
#include "core/input/mouse.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "context_escenes.hpp"
// Using declarations per simplificar el codi
using GestorEscenes::ContextEscenes;
@@ -14,40 +17,53 @@ using Escena = ContextEscenes::Escena;
namespace GlobalEvents {
bool handle(const SDL_Event& event, SDLManager& sdl, ContextEscenes& context) {
// Tecles globals de finestra (F1/F2/F3)
if (event.type == SDL_EVENT_KEY_DOWN) {
switch (event.key.key) {
case SDLK_F1:
sdl.decreaseWindowSize();
return true;
case SDLK_F2:
sdl.increaseWindowSize();
return true;
case SDLK_F3:
sdl.toggleFullscreen();
return true;
case SDLK_F4:
sdl.toggleVSync();
return true;
case SDLK_ESCAPE:
context.canviar_escena(Escena::EIXIR);
GestorEscenes::actual = Escena::EIXIR;
return true;
default:
break;
}
// 1. Permitir que Input procese el evento (para hotplug de gamepads)
auto event_msg = Input::get()->handleEvent(event);
if (!event_msg.empty()) {
std::cout << "[Input] " << event_msg << std::endl;
}
// Tancar finestra
// 2. Procesar SDL_EVENT_QUIT directamente (no es input de juego)
if (event.type == SDL_EVENT_QUIT) {
context.canviar_escena(Escena::EIXIR);
GestorEscenes::actual = Escena::EIXIR;
return true;
}
// Gestió del ratolí (auto-ocultar)
// 3. Gestió del ratolí (auto-ocultar)
Mouse::handleEvent(event);
// 4. Procesar acciones globales directamente desde eventos SDL
// (NO usar Input::checkAction() para evitar desfase de timing)
if (event.type == SDL_EVENT_KEY_DOWN) {
switch (event.key.scancode) {
case SDL_SCANCODE_F1:
sdl.decreaseWindowSize();
return true;
case SDL_SCANCODE_F2:
sdl.increaseWindowSize();
return true;
case SDL_SCANCODE_F3:
sdl.toggleFullscreen();
return true;
case SDL_SCANCODE_F4:
sdl.toggleVSync();
return true;
case SDL_SCANCODE_ESCAPE:
context.canviar_escena(Escena::EIXIR);
GestorEscenes::actual = Escena::EIXIR;
return true;
default:
// Tecla no global
break;
}
}
return false; // Event no processat
}

View File

@@ -8,7 +8,9 @@
// Forward declarations
class SDLManager;
namespace GestorEscenes { class ContextEscenes; }
namespace GestorEscenes {
class ContextEscenes;
}
namespace GlobalEvents {
// Processa events globals (F1/F2/F3/ESC/QUIT)

View File

@@ -18,9 +18,9 @@ struct Debris {
float acceleracio; // Acceleració negativa (fricció) en px/s²
// Rotació
float angle_rotacio; // Angle de rotació acumulat (radians)
float velocitat_rot; // Velocitat de rotació de TRAYECTORIA (rad/s)
float velocitat_rot_visual; // Velocitat de rotació VISUAL del segment (rad/s)
float angle_rotacio; // Angle de rotació acumulat (radians)
float velocitat_rot; // Velocitat de rotació de TRAYECTORIA (rad/s)
float velocitat_rot_visual; // Velocitat de rotació VISUAL del segment (rad/s)
// Estat de vida
float temps_vida; // Temps transcorregut (segons)

View File

@@ -51,13 +51,14 @@ void DebrisManager::explotar(const std::shared_ptr<Graphics::Shape>& shape,
float brightness,
const Punt& velocitat_objecte,
float velocitat_angular,
float factor_herencia_visual) {
float factor_herencia_visual,
const std::string& sound) {
if (!shape || !shape->es_valida()) {
return;
}
// Reproducir sonido de explosión
Audio::get()->playSound(Defaults::Sound::EXPLOSION, Audio::Group::GAME);
Audio::get()->playSound(sound, Audio::Group::GAME);
// Obtenir centre de la forma per a transformacions
const Punt& shape_centre = shape->get_centre();

View File

@@ -37,7 +37,8 @@ class DebrisManager {
float brightness = 1.0f,
const Punt& velocitat_objecte = {0.0f, 0.0f},
float velocitat_angular = 0.0f,
float factor_herencia_visual = 0.0f);
float factor_herencia_visual = 0.0f,
const std::string& sound = Defaults::Sound::EXPLOSION);
// Actualitzar tots els fragments actius
void actualitzar(float delta_time);

View File

@@ -8,7 +8,8 @@
namespace Effects {
GestorPuntuacioFlotant::GestorPuntuacioFlotant(SDL_Renderer* renderer)
: renderer_(renderer), text_(renderer) {
: renderer_(renderer),
text_(renderer) {
// Inicialitzar tots els slots com inactius
for (auto& pf : pool_) {
pf.actiu = false;
@@ -25,7 +26,7 @@ void GestorPuntuacioFlotant::crear(int punts, const Punt& posicio) {
pf->text = std::to_string(punts);
pf->posicio = posicio;
pf->velocitat = {Defaults::FloatingScore::VELOCITY_X,
Defaults::FloatingScore::VELOCITY_Y};
Defaults::FloatingScore::VELOCITY_Y};
pf->temps_vida = 0.0f;
pf->temps_max = Defaults::FloatingScore::LIFETIME;
pf->brightness = 1.0f;
@@ -60,16 +61,11 @@ void GestorPuntuacioFlotant::dibuixar() {
if (!pf.actiu)
continue;
// 1. Calcular dimensions del text per centrar-lo
// Renderitzar centrat amb brightness (fade)
constexpr float escala = Defaults::FloatingScore::SCALE;
constexpr float spacing = Defaults::FloatingScore::SPACING;
float text_width = text_.get_text_width(pf.text, escala, spacing);
// 2. Centrar text sobre la posició
Punt render_pos = {pf.posicio.x - text_width / 2.0f, pf.posicio.y};
// 3. Renderitzar amb brightness (fade)
text_.render(pf.text, render_pos, escala, spacing, pf.brightness);
text_.render_centered(pf.text, pf.posicio, escala, spacing, pf.brightness);
}
}

View File

@@ -17,38 +17,38 @@ namespace Effects {
// Gestor de números de puntuació flotants
// Manté un pool de PuntuacioFlotant i gestiona el seu cicle de vida
class GestorPuntuacioFlotant {
public:
explicit GestorPuntuacioFlotant(SDL_Renderer* renderer);
public:
explicit GestorPuntuacioFlotant(SDL_Renderer* renderer);
// Crear número flotant
// - punts: valor numèric (100, 150, 200)
// - posicio: on apareix (normalment centre d'enemic destruït)
void crear(int punts, const Punt& posicio);
// Crear número flotant
// - punts: valor numèric (100, 150, 200)
// - posicio: on apareix (normalment centre d'enemic destruït)
void crear(int punts, const Punt& posicio);
// Actualitzar tots els números actius
void actualitzar(float delta_time);
// Actualitzar tots els números actius
void actualitzar(float delta_time);
// Dibuixar tots els números actius
void dibuixar();
// Dibuixar tots els números actius
void dibuixar();
// Reiniciar tots (neteja)
void reiniciar();
// Reiniciar tots (neteja)
void reiniciar();
// Obtenir número actius (debug)
int get_num_actius() const;
// Obtenir número actius (debug)
int get_num_actius() const;
private:
SDL_Renderer* renderer_;
Graphics::VectorText text_; // Sistema de text vectorial
private:
SDL_Renderer* renderer_;
Graphics::VectorText text_; // Sistema de text vectorial
// Pool de números flotants (màxim concurrent)
// Màxim 15 enemics simultanis = màxim 15 números
static constexpr int MAX_PUNTUACIONS =
Defaults::FloatingScore::MAX_CONCURRENT;
std::array<PuntuacioFlotant, MAX_PUNTUACIONS> pool_;
// Pool de números flotants (màxim concurrent)
// Màxim 15 enemics simultanis = màxim 15 números
static constexpr int MAX_PUNTUACIONS =
Defaults::FloatingScore::MAX_CONCURRENT;
std::array<PuntuacioFlotant, MAX_PUNTUACIONS> pool_;
// Trobar primer slot inactiu
PuntuacioFlotant* trobar_slot_lliure();
// Trobar primer slot inactiu
PuntuacioFlotant* trobar_slot_lliure();
};
} // namespace Effects

View File

@@ -12,22 +12,22 @@ namespace Effects {
// PuntuacioFlotant: text animat que mostra punts guanyats
// S'activa quan es destrueix un enemic i s'esvaeix després d'un temps
struct PuntuacioFlotant {
// Text a mostrar (e.g., "100", "150", "200")
std::string text;
// Text a mostrar (e.g., "100", "150", "200")
std::string text;
// Posició actual (coordenades mundials)
Punt posicio;
// Posició actual (coordenades mundials)
Punt posicio;
// Animació de moviment
Punt velocitat; // px/s (normalment cap amunt: {0.0f, -30.0f})
// Animació de moviment
Punt velocitat; // px/s (normalment cap amunt: {0.0f, -30.0f})
// Animació 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)
// Animació 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)
// Estat
bool actiu;
// Estat
bool actiu;
};
} // namespace Effects

View File

@@ -19,6 +19,7 @@ Bala::Bala(SDL_Renderer* renderer)
angle_(0.0f),
velocitat_(0.0f),
esta_(false),
grace_timer_(0.0f),
brightness_(Defaults::Brightness::BALA) {
// [NUEVO] Carregar forma compartida des de fitxer
forma_ = Graphics::ShapeLoader::load("bullet.shp");
@@ -34,9 +35,10 @@ void Bala::inicialitzar() {
centre_ = {0.0f, 0.0f};
angle_ = 0.0f;
velocitat_ = 0.0f;
grace_timer_ = 0.0f;
}
void Bala::disparar(const Punt& posicio, float angle) {
void Bala::disparar(const Punt& posicio, float angle, uint8_t owner_id) {
// Activar bala i posicionar-la a la nau
// Basat en joc_asteroides.cpp línies 188-200
@@ -50,16 +52,30 @@ void Bala::disparar(const Punt& posicio, float angle) {
// Angle = angle de la nau (dispara en la direcció que apunta)
angle_ = angle;
// Almacenar propietario (0=P1, 1=P2)
owner_id_ = owner_id;
// Velocitat alta (el joc Pascal original usava 7 px/frame)
// 7 px/frame × 20 FPS = 140 px/s
velocitat_ = 140.0f;
// Activar grace period (prevents instant self-collision)
grace_timer_ = Defaults::Game::BULLET_GRACE_PERIOD;
// Reproducir sonido de disparo láser
Audio::get()->playSound(Defaults::Sound::LASER, Audio::Group::GAME);
}
void Bala::actualitzar(float delta_time) {
if (esta_) {
// Decrementar grace timer
if (grace_timer_ > 0.0f) {
grace_timer_ -= delta_time;
if (grace_timer_ < 0.0f) {
grace_timer_ = 0.0f;
}
}
mou(delta_time);
}
}

View File

@@ -17,13 +17,15 @@ class Bala {
Bala(SDL_Renderer* renderer);
void inicialitzar();
void disparar(const Punt& posicio, float angle);
void disparar(const Punt& posicio, float angle, uint8_t owner_id);
void actualitzar(float delta_time);
void dibuixar() const;
// Getters (API pública sense canvis)
bool esta_activa() const { return esta_; }
const Punt& get_centre() const { return centre_; }
uint8_t get_owner_id() const { return owner_id_; }
float get_grace_timer() const { return grace_timer_; }
void desactivar() { esta_ = false; }
private:
@@ -37,7 +39,9 @@ class Bala {
float angle_;
float velocitat_;
bool esta_;
float brightness_; // Factor de brillantor (0.0-1.0)
uint8_t owner_id_; // 0=P1, 1=P2
float grace_timer_; // Grace period timer (0.0 = vulnerable)
float brightness_; // Factor de brillantor (0.0-1.0)
void mou(float delta_time);
};

View File

@@ -25,7 +25,7 @@ Enemic::Enemic(SDL_Renderer* renderer)
tipus_(TipusEnemic::PENTAGON),
tracking_timer_(0.0f),
ship_position_(nullptr),
tracking_strength_(0.5f), // Default tracking strength
tracking_strength_(0.5f), // Default tracking strength
timer_invulnerabilitat_(0.0f) { // Start vulnerable
// [NUEVO] Forma es carrega a inicialitzar() segons el tipus
// Constructor no carrega forma per permetre tipus diferents
@@ -126,7 +126,7 @@ void Enemic::inicialitzar(TipusEnemic tipus, const Punt* ship_pos) {
// [NEW] Inicialitzar invulnerabilitat
timer_invulnerabilitat_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; // 3.0s
brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START; // 0.3f
brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START; // 0.3f
// Activar
esta_ = true;
@@ -144,7 +144,7 @@ void Enemic::actualitzar(float delta_time) {
// [NEW] Update brightness with LERP during invulnerability
float t_inv = timer_invulnerabilitat_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
float t = 1.0f - t_inv; // 0.0 → 1.0
float t = 1.0f - t_inv; // 0.0 → 1.0
float smooth_t = t * t * (3.0f - 2.0f * t); // smoothstep
constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START;

View File

@@ -54,8 +54,7 @@ class Enemic {
Punt get_velocitat_vector() const {
return {
velocitat_ * std::cos(angle_ - Constants::PI / 2.0f),
velocitat_ * std::sin(angle_ - Constants::PI / 2.0f)
};
velocitat_ * std::sin(angle_ - Constants::PI / 2.0f)};
}
// Set ship position reference for tracking behavior
@@ -68,7 +67,10 @@ class Enemic {
// [NEW] Setters for difficulty multipliers (stage system)
void set_velocity(float vel) { velocitat_ = vel; }
void set_rotation(float rot) { drotacio_ = rot; animacio_.drotacio_base = rot; }
void set_rotation(float rot) {
drotacio_ = rot;
animacio_.drotacio_base = rot;
}
void set_tracking_strength(float strength);
// [NEW] Invulnerability queries

View File

@@ -11,25 +11,27 @@
#include "core/defaults.hpp"
#include "core/graphics/shape_loader.hpp"
#include "core/input/input.hpp"
#include "core/rendering/shape_renderer.hpp"
#include "game/constants.hpp"
Nau::Nau(SDL_Renderer* renderer)
Nau::Nau(SDL_Renderer* renderer, const char* shape_file)
: renderer_(renderer),
centre_({0.0f, 0.0f}),
angle_(0.0f),
velocitat_(0.0f),
esta_tocada_(false),
brightness_(Defaults::Brightness::NAU) {
brightness_(Defaults::Brightness::NAU),
invulnerable_timer_(0.0f) {
// [NUEVO] Carregar forma compartida des de fitxer
forma_ = Graphics::ShapeLoader::load("ship2.shp");
forma_ = Graphics::ShapeLoader::load(shape_file);
if (!forma_ || !forma_->es_valida()) {
std::cerr << "[Nau] Error: no s'ha pogut carregar ship.shp" << std::endl;
std::cerr << "[Nau] Error: no s'ha pogut carregar " << shape_file << std::endl;
}
}
void Nau::inicialitzar(const Punt* spawn_point) {
void Nau::inicialitzar(const Punt* spawn_point, bool activar_invulnerabilitat) {
// Inicialització de la nau (triangle)
// Basat en el codi Pascal original: lines 380-384
// Copiat de joc_asteroides.cpp línies 30-44
@@ -52,34 +54,61 @@ void Nau::inicialitzar(const Punt* spawn_point) {
// Estat inicial
angle_ = 0.0f;
velocitat_ = 0.0f;
// Activar invulnerabilidad solo si es respawn
if (activar_invulnerabilitat) {
invulnerable_timer_ = Defaults::Ship::INVULNERABILITY_DURATION;
} else {
invulnerable_timer_ = 0.0f;
}
esta_tocada_ = false;
}
void Nau::processar_input(float delta_time) {
void Nau::processar_input(float delta_time, uint8_t player_id) {
// Processar input continu (com teclapuls() del Pascal original)
// Basat en joc_asteroides.cpp línies 66-85
// Només processa input si la nau està viva
if (esta_tocada_)
return;
// Obtenir estat actual del teclat (no events, sinó estat continu)
const bool* keyboard_state = SDL_GetKeyboardState(nullptr);
auto* input = Input::get();
// Rotació
if (keyboard_state[SDL_SCANCODE_RIGHT]) {
angle_ += Defaults::Physics::ROTATION_SPEED * delta_time;
}
// Processar input segons el jugador
if (player_id == 0) {
// Jugador 1
if (input->checkActionPlayer1(InputAction::RIGHT, Input::ALLOW_REPEAT)) {
angle_ += Defaults::Physics::ROTATION_SPEED * delta_time;
}
if (keyboard_state[SDL_SCANCODE_LEFT]) {
angle_ -= Defaults::Physics::ROTATION_SPEED * delta_time;
}
if (input->checkActionPlayer1(InputAction::LEFT, Input::ALLOW_REPEAT)) {
angle_ -= Defaults::Physics::ROTATION_SPEED * delta_time;
}
// Acceleració
if (keyboard_state[SDL_SCANCODE_UP]) {
if (velocitat_ < Defaults::Physics::MAX_VELOCITY) {
velocitat_ += Defaults::Physics::ACCELERATION * delta_time;
if (velocitat_ > Defaults::Physics::MAX_VELOCITY) {
velocitat_ = Defaults::Physics::MAX_VELOCITY;
if (input->checkActionPlayer1(InputAction::THRUST, Input::ALLOW_REPEAT)) {
if (velocitat_ < Defaults::Physics::MAX_VELOCITY) {
velocitat_ += Defaults::Physics::ACCELERATION * delta_time;
if (velocitat_ > Defaults::Physics::MAX_VELOCITY) {
velocitat_ = Defaults::Physics::MAX_VELOCITY;
}
}
}
} else {
// Jugador 2
if (input->checkActionPlayer2(InputAction::RIGHT, Input::ALLOW_REPEAT)) {
angle_ += Defaults::Physics::ROTATION_SPEED * delta_time;
}
if (input->checkActionPlayer2(InputAction::LEFT, Input::ALLOW_REPEAT)) {
angle_ -= Defaults::Physics::ROTATION_SPEED * delta_time;
}
if (input->checkActionPlayer2(InputAction::THRUST, Input::ALLOW_REPEAT)) {
if (velocitat_ < Defaults::Physics::MAX_VELOCITY) {
velocitat_ += Defaults::Physics::ACCELERATION * delta_time;
if (velocitat_ > Defaults::Physics::MAX_VELOCITY) {
velocitat_ = Defaults::Physics::MAX_VELOCITY;
}
}
}
}
@@ -90,6 +119,14 @@ void Nau::actualitzar(float delta_time) {
if (esta_tocada_)
return;
// Decrementar timer de invulnerabilidad
if (invulnerable_timer_ > 0.0f) {
invulnerable_timer_ -= delta_time;
if (invulnerable_timer_ < 0.0f) {
invulnerable_timer_ = 0.0f;
}
}
// Aplicar física (moviment + fricció)
aplicar_fisica(delta_time);
}
@@ -99,6 +136,19 @@ void Nau::dibuixar() const {
if (esta_tocada_)
return;
// Si invulnerable, parpadear (toggle on/off)
if (es_invulnerable()) {
// Calcular ciclo de parpadeo
float blink_cycle = Defaults::Ship::BLINK_VISIBLE_TIME +
Defaults::Ship::BLINK_INVISIBLE_TIME;
float time_in_cycle = std::fmod(invulnerable_timer_, blink_cycle);
// Si estamos en fase invisible, no dibujar
if (time_in_cycle < Defaults::Ship::BLINK_INVISIBLE_TIME) {
return; // No dibujar durante fase invisible
}
}
if (!forma_)
return;

View File

@@ -15,10 +15,10 @@ class Nau {
public:
Nau()
: renderer_(nullptr) {}
Nau(SDL_Renderer* renderer);
Nau(SDL_Renderer* renderer, const char* shape_file = "ship.shp");
void inicialitzar(const Punt* spawn_point = nullptr);
void processar_input(float delta_time);
void inicialitzar(const Punt* spawn_point = nullptr, bool activar_invulnerabilitat = false);
void processar_input(float delta_time, uint8_t player_id);
void actualitzar(float delta_time);
void dibuixar() const;
@@ -26,15 +26,19 @@ class Nau {
const Punt& get_centre() const { return centre_; }
float get_angle() const { return angle_; }
bool esta_viva() const { return !esta_tocada_; }
bool esta_tocada() const { return esta_tocada_; }
bool es_invulnerable() const { return invulnerable_timer_ > 0.0f; }
const std::shared_ptr<Graphics::Shape>& get_forma() const { return forma_; }
float get_brightness() const { return brightness_; }
Punt get_velocitat_vector() const {
return {
velocitat_ * std::cos(angle_ - Constants::PI / 2.0f),
velocitat_ * std::sin(angle_ - Constants::PI / 2.0f)
};
velocitat_ * std::sin(angle_ - Constants::PI / 2.0f)};
}
// Setters
void set_centre(const Punt& nou_centre) { centre_ = nou_centre; }
// Col·lisions (Fase 10)
void marcar_tocada() { esta_tocada_ = true; }
@@ -50,7 +54,8 @@ class Nau {
float angle_; // Angle d'orientació
float velocitat_; // Velocitat (px/s)
bool esta_tocada_;
float brightness_; // Factor de brillantor (0.0-1.0)
float brightness_; // Factor de brillantor (0.0-1.0)
float invulnerable_timer_; // 0.0f = vulnerable, >0.0f = invulnerable
void aplicar_fisica(float delta_time);
};

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@
#include <array>
#include <cstdint>
#include <memory>
#include "../constants.hpp"
#include "../effects/debris_manager.hpp"
@@ -20,9 +21,15 @@
#include "core/graphics/vector_text.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/system/context_escenes.hpp"
#include "core/system/game_config.hpp"
#include "core/types.hpp"
#include <memory>
// Game over state machine
enum class EstatGameOver {
NONE, // Normal gameplay
CONTINUE, // Continue countdown screen (9→0)
GAME_OVER // Final game over (returning to title)
};
// Classe principal del joc (escena)
class EscenaJoc {
@@ -34,29 +41,33 @@ class EscenaJoc {
void inicialitzar();
void actualitzar(float delta_time);
void dibuixar();
void processar_input(const SDL_Event& event);
private:
SDLManager& sdl_;
GestorEscenes::ContextEscenes& context_;
GameConfig::ConfigPartida config_partida_; // Configuració de jugadors actius
// Efectes visuals
Effects::DebrisManager debris_manager_;
Effects::GestorPuntuacioFlotant gestor_puntuacio_;
// Estat del joc
Nau nau_;
std::array<Nau, 2> naus_; // [0]=P1, [1]=P2
std::array<Enemic, Constants::MAX_ORNIS> orni_;
std::array<Bala, Constants::MAX_BALES> bales_;
std::array<Bala, Constants::MAX_BALES * 2> bales_; // 6 balas: P1=[0,1,2], P2=[3,4,5]
Poligon chatarra_cosmica_;
float itocado_; // Death timer (seconds)
std::array<float, 2> itocado_per_jugador_; // Death timers per player (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
int puntuacio_total_; // Current score
std::array<int, 2> vides_per_jugador_; // [0]=P1, [1]=P2
EstatGameOver estat_game_over_; // Game over state machine (NONE, CONTINUE, GAME_OVER)
int continue_counter_; // Continue countdown (9→0)
float continue_tick_timer_; // Timer for countdown tick (1.0s)
int continues_usados_; // Continues used this game (0-3 max)
float game_over_timer_; // Final GAME OVER timer before title screen
// Punt punt_spawn_; // DEPRECATED: usar obtenir_punt_spawn(player_id)
Punt punt_mort_; // Death position (for respawn, legacy)
std::array<int, 2> puntuacio_per_jugador_; // [0]=P1, [1]=P2
// Text vectorial
Graphics::VectorText text_;
@@ -65,15 +76,39 @@ class EscenaJoc {
std::unique_ptr<StageSystem::ConfigSistemaStages> stage_config_;
std::unique_ptr<StageSystem::StageManager> stage_manager_;
// Control de sons d'animació INIT_HUD
bool init_hud_rect_sound_played_; // Flag para evitar repetir sonido del rectángulo
// 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ó
void tocado(uint8_t player_id);
void detectar_col·lisions_bales_enemics(); // Col·lisions bala-enemic
void detectar_col·lisio_naus_enemics(); // Ship-enemy collision detection (plural)
void detectar_col·lisions_bales_jugadors(); // Bullet-player collision detection (friendly fire)
void dibuixar_marges() const; // Dibuixar vores de la zona de joc
void dibuixar_marcador(); // Dibuixar marcador de puntuació
void disparar_bala(uint8_t player_id); // Shoot bullet from player
Punt obtenir_punt_spawn(uint8_t player_id) const; // Get spawn position for player
// [NEW] Continue & Join system
void unir_jugador(uint8_t player_id); // Join inactive player mid-game
void processar_input_continue(); // Handle input during continue screen
void actualitzar_continue(float delta_time); // Update continue countdown
void check_and_apply_continue_timeout(); // Check if continue timed out and transition to GAME_OVER
void dibuixar_continue(); // Draw continue screen
// [NEW] Stage system helpers
void dibuixar_missatge_stage(const std::string& missatge);
// [NEW] Funcions d'animació per INIT_HUD
void dibuixar_marges_animat(float progress) const; // Rectangle amb creixement uniforme
void dibuixar_marcador_animat(float progress); // Marcador que puja des de baix
Punt calcular_posicio_nau_init_hud(float progress, uint8_t player_id) const; // Posició animada de la nau
// [NEW] Función helper del sistema de animación INIT_HUD
float calcular_progress_rango(float global_progress, float ratio_init, float ratio_end) const;
// [NEW] Funció helper del marcador
std::string construir_marcador() const;
};
#endif // ESCENA_JOC_HPP

View File

@@ -11,6 +11,7 @@
#include "core/audio/audio.hpp"
#include "core/graphics/shape_loader.hpp"
#include "core/input/input.hpp"
#include "core/input/mouse.hpp"
#include "core/rendering/shape_renderer.hpp"
#include "core/system/context_escenes.hpp"
@@ -61,6 +62,12 @@ EscenaLogo::EscenaLogo(SDLManager& sdl, ContextEscenes& context)
inicialitzar_lletres();
}
EscenaLogo::~EscenaLogo() {
// Aturar tots els sons i la música
Audio::get()->stopAllSounds();
std::cout << "Escena Logo: Sons aturats\n";
}
void EscenaLogo::executar() {
SDL_Event event;
Uint64 last_time = SDL_GetTicks();
@@ -82,6 +89,9 @@ void EscenaLogo::executar() {
// Actualitzar visibilitat del cursor (auto-ocultar)
Mouse::updateCursorVisibility();
// Actualitzar sistema d'input ABANS del event loop
Input::get()->update();
// Processar events SDL
while (SDL_PollEvent(&event)) {
// Manejo de finestra
@@ -210,10 +220,8 @@ void EscenaLogo::canviar_estat(EstatAnimacio nou_estat) {
std::random_device rd;
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");
} else if (nou_estat == EstatAnimacio::POST_EXPLOSION) {
Audio::get()->playMusic("title.ogg");
}
std::cout << "[EscenaLogo] Canvi a estat: " << static_cast<int>(nou_estat)
@@ -236,13 +244,13 @@ void EscenaLogo::actualitzar_explosions(float delta_time) {
const auto& lletra = lletres_[index_actual];
debris_manager_->explotar(
lletra.forma, // Forma a explotar
lletra.posicio, // Posició
0.0f, // Angle (sense rotació)
ESCALA_FINAL, // Escala (lletres a escala final)
VELOCITAT_EXPLOSIO, // Velocitat base
1.0f, // Brightness màxim (per defecte)
{0.0f, 0.0f} // Sense velocitat (per defecte)
lletra.forma, // Forma a explotar
lletra.posicio, // Posició
0.0f, // Angle (sense rotació)
ESCALA_FINAL, // Escala (lletres a escala final)
VELOCITAT_EXPLOSIO, // Velocitat base
1.0f, // Brightness màxim (per defecte)
{0.0f, 0.0f} // Sense velocitat (per defecte)
);
std::cout << "[EscenaLogo] Explota lletra " << lletra_explosio_index_ << "\n";
@@ -281,7 +289,7 @@ void EscenaLogo::actualitzar(float delta_time) {
// Reproduir so quan la lletra comença a aparèixer (progress > 0)
if (letra_progress > 0.0f) {
Audio::get()->playSound("logo.wav", Audio::Group::INTERFACE);
Audio::get()->playSound(Defaults::Sound::LOGO, Audio::Group::GAME);
so_reproduit_[i] = true;
}
}
@@ -312,6 +320,12 @@ void EscenaLogo::actualitzar(float delta_time) {
break;
}
// Verificar botones de skip (SHOOT P1/P2)
if (checkSkipButtonPressed()) {
context_.canviar_escena(Escena::TITOL, Opcio::JUMP_TO_TITLE_MAIN);
GestorEscenes::actual = Escena::TITOL;
}
// Actualitzar animacions de debris
debris_manager_->actualitzar(delta_time);
}
@@ -404,16 +418,10 @@ void EscenaLogo::dibuixar() {
sdl_.presenta();
}
void EscenaLogo::processar_events(const SDL_Event& event) {
// Qualsevol tecla o clic de ratolí salta a la pantalla de títol
if (event.type == SDL_EVENT_KEY_DOWN ||
event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
// Utilitzar context per especificar escena i opció
context_.canviar_escena(
Escena::TITOL,
Opcio::JUMP_TO_TITLE_MAIN
);
// Backward compatibility: També actualitzar GestorEscenes::actual
GestorEscenes::actual = Escena::TITOL;
}
auto EscenaLogo::checkSkipButtonPressed() -> bool {
return Input::get()->checkAnyPlayerAction(ARCADE_BUTTONS);
}
void EscenaLogo::processar_events(const SDL_Event& event) {
// No procesar eventos genéricos aquí - la lógica se movió a actualitzar()
}

View File

@@ -6,19 +6,22 @@
#include <SDL3/SDL.h>
#include <array>
#include <memory>
#include <vector>
#include "game/effects/debris_manager.hpp"
#include "core/defaults.hpp"
#include "core/graphics/shape.hpp"
#include "core/input/input_types.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/system/context_escenes.hpp"
#include "core/types.hpp"
#include "game/effects/debris_manager.hpp"
class EscenaLogo {
public:
explicit EscenaLogo(SDLManager& sdl, GestorEscenes::ContextEscenes& context);
~EscenaLogo(); // Destructor per aturar sons
void executar(); // Bucle principal de l'escena
private:
@@ -80,6 +83,7 @@ class EscenaLogo {
void actualitzar_explosions(float delta_time);
void dibuixar();
void processar_events(const SDL_Event& event);
auto checkSkipButtonPressed() -> bool;
// Mètodes de gestió d'estats
void canviar_estat(EstatAnimacio nou_estat);

View File

@@ -10,6 +10,7 @@
#include "core/audio/audio.hpp"
#include "core/graphics/shape_loader.hpp"
#include "core/input/input.hpp"
#include "core/input/mouse.hpp"
#include "core/rendering/shape_renderer.hpp"
#include "core/system/context_escenes.hpp"
@@ -33,6 +34,11 @@ EscenaTitol::EscenaTitol(SDLManager& sdl, ContextEscenes& context)
factor_lerp_(0.0f) {
std::cout << "Escena Titol: Inicialitzant...\n";
// Inicialitzar configuració de partida (cap jugador actiu per defecte)
config_partida_.jugador1_actiu = false;
config_partida_.jugador2_actiu = false;
config_partida_.mode = GameConfig::Mode::NORMAL;
// Processar opció del context
auto opcio = context_.consumir_opcio();
@@ -69,6 +75,19 @@ EscenaTitol::EscenaTitol(SDLManager& sdl, ContextEscenes& context)
starfield_->set_brightness(0.0f);
}
// Inicialitzar animador de naus 3D
ship_animator_ = std::make_unique<Title::ShipAnimator>(sdl_.obte_renderer());
ship_animator_->inicialitzar();
if (estat_actual_ == EstatTitol::MAIN) {
// Jump to MAIN: empezar entrada inmediatamente
ship_animator_->set_visible(true);
ship_animator_->start_entry_animation();
} else {
// Flux normal: NO empezar entrada todavía (esperaran a MAIN)
ship_animator_->set_visible(false);
}
// Inicialitzar lletres del títol "ORNI ATTACK!"
inicialitzar_titol();
@@ -121,10 +140,10 @@ void EscenaTitol::inicialitzar_titol() {
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;
// Escalar ancho, altura i offset amb LOGO_SCALE
float ancho = ancho_sin_escalar * Defaults::Title::Layout::LOGO_SCALE;
float altura = altura_sin_escalar * Defaults::Title::Layout::LOGO_SCALE;
float offset_centre = (forma->get_centre().x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
lletres_orni_.push_back({forma, {0.0f, 0.0f}, ancho, altura, offset_centre});
@@ -140,7 +159,7 @@ void EscenaTitol::inicialitzar_titol() {
for (auto& lletra : lletres_orni_) {
lletra.posicio.x = x_actual + lletra.offset_centre;
lletra.posicio.y = Y_ORNI;
lletra.posicio.y = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS;
x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES;
}
@@ -150,7 +169,9 @@ void EscenaTitol::inicialitzar_titol() {
// === 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;
float y_orni = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS;
float separacion_lineas = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_LINE_SPACING;
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";
@@ -193,10 +214,10 @@ void EscenaTitol::inicialitzar_titol() {
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;
// Escalar ancho, altura i offset amb LOGO_SCALE
float ancho = ancho_sin_escalar * Defaults::Title::Layout::LOGO_SCALE;
float altura = altura_sin_escalar * Defaults::Title::Layout::LOGO_SCALE;
float offset_centre = (forma->get_centre().x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
lletres_attack_.push_back({forma, {0.0f, 0.0f}, ancho, altura, offset_centre});
@@ -254,6 +275,9 @@ void EscenaTitol::executar() {
// Actualitzar visibilitat del cursor (auto-ocultar)
Mouse::updateCursorVisibility();
// Actualitzar sistema d'input ABANS del event loop
Input::get()->update();
// Processar events SDL
while (SDL_PollEvent(&event)) {
// Manejo de finestra
@@ -301,6 +325,15 @@ void EscenaTitol::actualitzar(float delta_time) {
starfield_->actualitzar(delta_time);
}
// Actualitzar naus (quan visibles)
if (ship_animator_ &&
(estat_actual_ == EstatTitol::STARFIELD_FADE_IN ||
estat_actual_ == EstatTitol::STARFIELD ||
estat_actual_ == EstatTitol::MAIN ||
estat_actual_ == EstatTitol::PLAYER_JOIN_PHASE)) {
ship_animator_->actualitzar(delta_time);
}
switch (estat_actual_) {
case EstatTitol::STARFIELD_FADE_IN: {
temps_acumulat_ += delta_time;
@@ -315,7 +348,7 @@ void EscenaTitol::actualitzar(float delta_time) {
// Transició a STARFIELD quan el fade es completa
if (temps_acumulat_ >= DURACIO_FADE_IN) {
estat_actual_ = EstatTitol::STARFIELD;
temps_acumulat_ = 0.0f; // Reset timer per al següent estat
temps_acumulat_ = 0.0f; // Reset timer per al següent estat
starfield_->set_brightness(BRIGHTNESS_STARFIELD); // Assegurar valor final
}
break;
@@ -328,12 +361,22 @@ void EscenaTitol::actualitzar(float delta_time) {
temps_estat_main_ = 0.0f; // Reset timer al entrar a MAIN
animacio_activa_ = false; // Comença estàtic
factor_lerp_ = 0.0f; // Sense animació encara
// Naus esperaran ENTRANCE_DELAY abans d'entrar (no iniciar aquí)
}
break;
case EstatTitol::MAIN: {
temps_estat_main_ += delta_time;
// Iniciar animació d'entrada de naus després del delay
if (temps_estat_main_ >= Defaults::Title::Ships::ENTRANCE_DELAY) {
if (ship_animator_ && !ship_animator_->is_visible()) {
ship_animator_->set_visible(true);
ship_animator_->start_entry_animation();
}
}
// Fase 1: Estàtic (0-10s)
if (temps_estat_main_ < DELAY_INICI_ANIMACIO) {
factor_lerp_ = 0.0f;
@@ -356,17 +399,115 @@ void EscenaTitol::actualitzar(float delta_time) {
break;
}
case EstatTitol::TRANSITION_TO_GAME:
case EstatTitol::PLAYER_JOIN_PHASE:
temps_acumulat_ += delta_time;
// Continuar animació orbital durant la transició
actualitzar_animacio_logo(delta_time);
// [NOU] Continuar comprovant si l'altre jugador vol unir-se durant la transició ("late join")
{
bool p1_actiu_abans = config_partida_.jugador1_actiu;
bool p2_actiu_abans = config_partida_.jugador2_actiu;
if (checkStartGameButtonPressed()) {
// Updates config_partida_ if pressed, logs are in the method
context_.set_config_partida(config_partida_);
// Trigger animació de sortida per la nau que acaba d'unir-se
if (ship_animator_) {
if (config_partida_.jugador1_actiu && !p1_actiu_abans) {
ship_animator_->trigger_exit_animation_for_player(1);
std::cout << "[EscenaTitol] P1 late join - ship exiting\n";
}
if (config_partida_.jugador2_actiu && !p2_actiu_abans) {
ship_animator_->trigger_exit_animation_for_player(2);
std::cout << "[EscenaTitol] P2 late join - ship exiting\n";
}
}
// Reproducir so de START quan el segon jugador s'uneix
Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME);
// Reiniciar el timer per allargar el temps de transició
temps_acumulat_ = 0.0f;
std::cout << "[EscenaTitol] Segon jugador s'ha unit - so i timer reiniciats\n";
}
}
if (temps_acumulat_ >= DURACIO_TRANSITION) {
// Transició a JOC (la música ja s'ha parat en el fade)
GestorEscenes::actual = Escena::JOC;
// Transició a pantalla negra
estat_actual_ = EstatTitol::BLACK_SCREEN;
temps_acumulat_ = 0.0f;
std::cout << "[EscenaTitol] Passant a BLACK_SCREEN\n";
}
break;
case EstatTitol::BLACK_SCREEN:
temps_acumulat_ += delta_time;
// No animation, no input checking - just wait
if (temps_acumulat_ >= DURACIO_BLACK_SCREEN) {
// Transició a escena JOC
GestorEscenes::actual = Escena::JOC;
std::cout << "[EscenaTitol] Canviant a escena JOC\n";
}
break;
}
// Verificar botones de skip (FIRE/THRUST/START) para saltar escenas ANTES de MAIN
if (estat_actual_ == EstatTitol::STARFIELD_FADE_IN || estat_actual_ == EstatTitol::STARFIELD) {
if (checkSkipButtonPressed()) {
// Saltar a MAIN
estat_actual_ = EstatTitol::MAIN;
starfield_->set_brightness(BRIGHTNESS_STARFIELD);
temps_estat_main_ = 0.0f;
// Naus esperaran ENTRANCE_DELAY abans d'entrar (no iniciar aquí)
}
}
// Verificar boton START para iniciar partida desde MAIN
if (estat_actual_ == EstatTitol::MAIN) {
// Guardar estat anterior per detectar qui ha premut START AQUEST frame
bool p1_actiu_abans = config_partida_.jugador1_actiu;
bool p2_actiu_abans = config_partida_.jugador2_actiu;
if (checkStartGameButtonPressed()) {
// Si START es prem durant el delay (naus encara invisibles), saltar-les a FLOATING
if (ship_animator_ && !ship_animator_->is_visible()) {
ship_animator_->set_visible(true);
ship_animator_->skip_to_floating_state();
}
// Configurar partida abans de canviar d'escena
context_.set_config_partida(config_partida_);
std::cout << "[EscenaTitol] Configuració de partida - P1: "
<< (config_partida_.jugador1_actiu ? "ACTIU" : "INACTIU")
<< ", P2: "
<< (config_partida_.jugador2_actiu ? "ACTIU" : "INACTIU")
<< std::endl;
context_.canviar_escena(Escena::JOC);
estat_actual_ = EstatTitol::PLAYER_JOIN_PHASE;
temps_acumulat_ = 0.0f;
// Trigger animació de sortida NOMÉS per les naus que han premut START
if (ship_animator_) {
if (config_partida_.jugador1_actiu && !p1_actiu_abans) {
ship_animator_->trigger_exit_animation_for_player(1);
std::cout << "[EscenaTitol] P1 ship exiting\n";
}
if (config_partida_.jugador2_actiu && !p2_actiu_abans) {
ship_animator_->trigger_exit_animation_for_player(2);
std::cout << "[EscenaTitol] P2 ship exiting\n";
}
}
Audio::get()->fadeOutMusic(MUSIC_FADE);
Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME);
}
}
}
@@ -401,18 +542,28 @@ void EscenaTitol::actualitzar_animacio_logo(float delta_time) {
}
void EscenaTitol::dibuixar() {
// Dibuixar starfield de fons (sempre, en tots els estats)
if (starfield_) {
// Dibuixar starfield de fons (en tots els estats excepte BLACK_SCREEN)
if (starfield_ && estat_actual_ != EstatTitol::BLACK_SCREEN) {
starfield_->dibuixar();
}
// Dibuixar naus (després starfield, abans logo)
if (ship_animator_ &&
(estat_actual_ == EstatTitol::STARFIELD_FADE_IN ||
estat_actual_ == EstatTitol::STARFIELD ||
estat_actual_ == EstatTitol::MAIN ||
estat_actual_ == EstatTitol::PLAYER_JOIN_PHASE)) {
ship_animator_->dibuixar();
}
// En els estats STARFIELD_FADE_IN i STARFIELD, només mostrar starfield (sense text)
if (estat_actual_ == EstatTitol::STARFIELD_FADE_IN || estat_actual_ == EstatTitol::STARFIELD) {
return;
}
// Estat MAIN i TRANSITION_TO_GAME: Dibuixar títol i text (sobre el starfield)
if (estat_actual_ == EstatTitol::MAIN || estat_actual_ == EstatTitol::TRANSITION_TO_GAME) {
// Estat MAIN i PLAYER_JOIN_PHASE: Dibuixar títol i text (sobre el starfield)
// BLACK_SCREEN: no dibuixar res (fons negre ja està netejat)
if (estat_actual_ == EstatTitol::MAIN || estat_actual_ == EstatTitol::PLAYER_JOIN_PHASE) {
// === Calcular i renderitzar ombra (només si animació activa) ===
if (animacio_activa_) {
float temps_shadow = temps_animacio_ - SHADOW_DELAY;
@@ -441,7 +592,7 @@ void EscenaTitol::dibuixar() {
lletres_orni_[i].forma,
pos_shadow,
0.0f,
ESCALA_TITULO,
Defaults::Title::Layout::LOGO_SCALE,
true,
1.0f, // progress = 1.0 (totalment visible)
SHADOW_BRIGHTNESS // brightness = 0.4 (brillantor reduïda)
@@ -459,7 +610,7 @@ void EscenaTitol::dibuixar() {
lletres_attack_[i].forma,
pos_shadow,
0.0f,
ESCALA_TITULO,
Defaults::Title::Layout::LOGO_SCALE,
true,
1.0f, // progress = 1.0 (totalment visible)
SHADOW_BRIGHTNESS);
@@ -475,7 +626,7 @@ void EscenaTitol::dibuixar() {
lletra.forma,
lletra.posicio,
0.0f,
ESCALA_TITULO,
Defaults::Title::Layout::LOGO_SCALE,
true,
1.0f // Brillantor completa
);
@@ -488,92 +639,92 @@ void EscenaTitol::dibuixar() {
lletra.forma,
lletra.posicio,
0.0f,
ESCALA_TITULO,
Defaults::Title::Layout::LOGO_SCALE,
true,
1.0f // Brillantor completa
);
}
// === Text "PRESS BUTTON TO PLAY" ===
// === Text "PRESS START TO PLAY" ===
// En estat MAIN: sempre visible
// En estat TRANSITION: parpellejant (blink amb sinusoide)
const float spacing = 2.0f; // Espai entre caràcters (usat també per copyright)
const float spacing = Defaults::Title::Layout::TEXT_SPACING;
bool mostrar_text = true;
if (estat_actual_ == EstatTitol::TRANSITION_TO_GAME) {
if (estat_actual_ == EstatTitol::PLAYER_JOIN_PHASE) {
// 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;
const std::string main_text = "PRESS START TO PLAY";
const float escala_main = Defaults::Title::Layout::PRESS_START_SCALE;
float text_width = text_.get_text_width(main_text, escala_main, spacing);
float centre_x = Defaults::Game::WIDTH / 2.0f;
float centre_y = Defaults::Game::HEIGHT * Defaults::Title::Layout::PRESS_START_POS;
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);
text_.render_centered(main_text, {centre_x, centre_y}, 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) {
if (c >= 'a' && c <= 'z') {
c = c - 32; // Convert to uppercase
// === Copyright a la part inferior (centrat horitzontalment, dues línies) ===
const float escala_copy = Defaults::Title::Layout::COPYRIGHT_SCALE;
const float copy_height = text_.get_text_height(escala_copy);
const float line_spacing = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT_LINE_SPACING;
// Línea 1: Original (© 1999 Visente i Sergi)
std::string copyright_original = Project::COPYRIGHT_ORIGINAL;
for (char& c : copyright_original) {
if (c >= 'a' && c <= 'z') c = c - 32; // Uppercase
}
// Línea 2: Port (© 2025 jaildesigner)
std::string copyright_port = Project::COPYRIGHT_PORT;
for (char& c : copyright_port) {
if (c >= 'a' && c <= 'z') c = c - 32; // Uppercase
}
// Calcular posicions (anclatge des del top + separació)
float y_line1 = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS;
float y_line2 = y_line1 + copy_height + line_spacing; // Línea 2 debajo de línea 1
// Renderitzar línees centrades
float centre_x = Defaults::Game::WIDTH / 2.0f;
text_.render_centered(copyright_original, {centre_x, y_line1}, escala_copy, spacing);
text_.render_centered(copyright_port, {centre_x, y_line2}, escala_copy, spacing);
}
}
auto EscenaTitol::checkSkipButtonPressed() -> bool {
return Input::get()->checkAnyPlayerAction(ARCADE_BUTTONS);
}
auto EscenaTitol::checkStartGameButtonPressed() -> bool {
auto* input = Input::get();
bool any_pressed = false;
for (auto action : START_GAME_BUTTONS) {
if (input->checkActionPlayer1(action, Input::DO_NOT_ALLOW_REPEAT)) {
if (!config_partida_.jugador1_actiu) {
config_partida_.jugador1_actiu = true;
any_pressed = true;
std::cout << "[EscenaTitol] P1 pressed START\n";
}
}
if (input->checkActionPlayer2(action, Input::DO_NOT_ALLOW_REPEAT)) {
if (!config_partida_.jugador2_actiu) {
config_partida_.jugador2_actiu = true;
any_pressed = true;
std::cout << "[EscenaTitol] P2 pressed START\n";
}
}
const float escala_copy = 0.6f;
float copy_width = text_.get_text_width(copyright, escala_copy, spacing);
float copy_height = text_.get_text_height(escala_copy);
float x_copy = (Defaults::Game::WIDTH - copy_width) / 2.0f;
float y_copy = Defaults::Game::HEIGHT - copy_height - 20.0f; // 20px des del fons
text_.render(copyright, Punt{x_copy, y_copy}, escala_copy, spacing);
}
return any_pressed;
}
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::STARFIELD_FADE_IN:
// Saltar directament a MAIN (ometre fade-in i starfield)
estat_actual_ = EstatTitol::MAIN;
starfield_->set_brightness(BRIGHTNESS_STARFIELD); // Assegurar brightness final
temps_estat_main_ = 0.0f; // Reset timer per animació de títol
break;
case EstatTitol::STARFIELD:
// Saltar a MAIN
estat_actual_ = EstatTitol::MAIN;
temps_estat_main_ = 0.0f; // Reset timer
break;
case EstatTitol::MAIN:
// Utilitzar context per transició a JOC
context_.canviar_escena(Escena::JOC);
// NO actualitzar GestorEscenes::actual aquí!
// La transició es fa en l'estat TRANSITION_TO_GAME
// Iniciar transició amb fade-out de música
estat_actual_ = EstatTitol::TRANSITION_TO_GAME;
temps_acumulat_ = 0.0f; // Reset del comptador
Audio::get()->fadeOutMusic(MUSIC_FADE); // Fade
Audio::get()->playSound(Defaults::Sound::LASER, Audio::Group::GAME);
break;
case EstatTitol::TRANSITION_TO_GAME:
// Ignorar inputs durant la transició
break;
}
}
// No procesar eventos genéricos aquí - la lógica se movió a actualitzar()
}

View File

@@ -6,6 +6,7 @@
#include <SDL3/SDL.h>
#include <array>
#include <memory>
#include <vector>
@@ -13,9 +14,16 @@
#include "core/graphics/shape.hpp"
#include "core/graphics/starfield.hpp"
#include "core/graphics/vector_text.hpp"
#include "core/input/input_types.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/system/context_escenes.hpp"
#include "core/system/game_config.hpp"
#include "core/types.hpp"
#include "game/title/ship_animator.hpp"
// Botones para INICIAR PARTIDA desde MAIN (solo START)
static constexpr std::array<InputAction, 1> START_GAME_BUTTONS = {
InputAction::START};
class EscenaTitol {
public:
@@ -26,10 +34,11 @@ class EscenaTitol {
private:
// Màquina d'estats per la pantalla de títol
enum class EstatTitol {
STARFIELD_FADE_IN, // Fade-in del starfield (1.5s)
STARFIELD, // Pantalla con el campo de estrellas
MAIN, // Pantalla de títol amb text
TRANSITION_TO_GAME // Transició amb fade-out de música i text parpellejant
STARFIELD_FADE_IN, // Fade-in del starfield (3.0s)
STARFIELD, // Pantalla amb camp d'estrelles (4.0s)
MAIN, // Pantalla de títol amb text (indefinit, fins START)
PLAYER_JOIN_PHASE, // Fase d'unió de jugadors: fade-out música + text parpellejant (2.5s)
BLACK_SCREEN // Pantalla negra de transició (2.0s)
};
// Estructura per emmagatzemar informació de cada lletra del títol
@@ -43,10 +52,12 @@ class EscenaTitol {
SDLManager& sdl_;
GestorEscenes::ContextEscenes& context_;
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
GameConfig::ConfigPartida config_partida_; // Configuració de jugadors actius
Graphics::VectorText text_; // Sistema de text vectorial
std::unique_ptr<Graphics::Starfield> starfield_; // Camp d'estrelles de fons
std::unique_ptr<Title::ShipAnimator> ship_animator_; // Naus 3D flotants
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)
@@ -67,13 +78,11 @@ class EscenaTitol {
static constexpr float BRIGHTNESS_STARFIELD = 1.2f; // Brightness del starfield (>1.0 = més brillant)
static constexpr float DURACIO_FADE_IN = 3.0f; // Duració del fade-in del starfield (1.5 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 DURACIO_TRANSITION = 2.5f; // Duració de la transició (1.5 segons)
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
static constexpr float DURACIO_BLACK_SCREEN = 2.0f; // Duració pantalla negra (2 segons)
static constexpr int MUSIC_FADE = 1500; // Duracio del fade de la musica del titol al començar a jugar
// Constants d'animació del logo
static constexpr float ORBIT_AMPLITUDE_X = 4.0f; // Amplitud oscil·lació horitzontal (píxels)
@@ -97,5 +106,7 @@ class EscenaTitol {
void actualitzar_animacio_logo(float delta_time); // Actualitza l'animació orbital del logo
void dibuixar();
void processar_events(const SDL_Event& event);
auto checkSkipButtonPressed() -> bool;
auto checkStartGameButtonPressed() -> bool;
void inicialitzar_titol(); // Carrega i posiciona les lletres del títol
};

View File

@@ -3,6 +3,7 @@
#include <fstream>
#include <iostream>
#include <string>
#include <unordered_map>
#include "core/defaults.hpp"
#include "external/fkyaml_node.hpp"
@@ -10,6 +11,173 @@
namespace Options {
// ========== FUNCIONS AUXILIARS PER CONVERSIÓ DE CONTROLES ==========
// Mapa de SDL_Scancode a string
static const std::unordered_map<SDL_Scancode, std::string> SCANCODE_TO_STRING = {
{SDL_SCANCODE_A, "A"},
{SDL_SCANCODE_B, "B"},
{SDL_SCANCODE_C, "C"},
{SDL_SCANCODE_D, "D"},
{SDL_SCANCODE_E, "E"},
{SDL_SCANCODE_F, "F"},
{SDL_SCANCODE_G, "G"},
{SDL_SCANCODE_H, "H"},
{SDL_SCANCODE_I, "I"},
{SDL_SCANCODE_J, "J"},
{SDL_SCANCODE_K, "K"},
{SDL_SCANCODE_L, "L"},
{SDL_SCANCODE_M, "M"},
{SDL_SCANCODE_N, "N"},
{SDL_SCANCODE_O, "O"},
{SDL_SCANCODE_P, "P"},
{SDL_SCANCODE_Q, "Q"},
{SDL_SCANCODE_R, "R"},
{SDL_SCANCODE_S, "S"},
{SDL_SCANCODE_T, "T"},
{SDL_SCANCODE_U, "U"},
{SDL_SCANCODE_V, "V"},
{SDL_SCANCODE_W, "W"},
{SDL_SCANCODE_X, "X"},
{SDL_SCANCODE_Y, "Y"},
{SDL_SCANCODE_Z, "Z"},
{SDL_SCANCODE_1, "1"},
{SDL_SCANCODE_2, "2"},
{SDL_SCANCODE_3, "3"},
{SDL_SCANCODE_4, "4"},
{SDL_SCANCODE_5, "5"},
{SDL_SCANCODE_6, "6"},
{SDL_SCANCODE_7, "7"},
{SDL_SCANCODE_8, "8"},
{SDL_SCANCODE_9, "9"},
{SDL_SCANCODE_0, "0"},
{SDL_SCANCODE_RETURN, "RETURN"},
{SDL_SCANCODE_ESCAPE, "ESCAPE"},
{SDL_SCANCODE_BACKSPACE, "BACKSPACE"},
{SDL_SCANCODE_TAB, "TAB"},
{SDL_SCANCODE_SPACE, "SPACE"},
{SDL_SCANCODE_UP, "UP"},
{SDL_SCANCODE_DOWN, "DOWN"},
{SDL_SCANCODE_LEFT, "LEFT"},
{SDL_SCANCODE_RIGHT, "RIGHT"},
{SDL_SCANCODE_LSHIFT, "LSHIFT"},
{SDL_SCANCODE_RSHIFT, "RSHIFT"},
{SDL_SCANCODE_LCTRL, "LCTRL"},
{SDL_SCANCODE_RCTRL, "RCTRL"},
{SDL_SCANCODE_LALT, "LALT"},
{SDL_SCANCODE_RALT, "RALT"}};
// Mapa invers: string a SDL_Scancode
static const std::unordered_map<std::string, SDL_Scancode> STRING_TO_SCANCODE = {
{"A", SDL_SCANCODE_A},
{"B", SDL_SCANCODE_B},
{"C", SDL_SCANCODE_C},
{"D", SDL_SCANCODE_D},
{"E", SDL_SCANCODE_E},
{"F", SDL_SCANCODE_F},
{"G", SDL_SCANCODE_G},
{"H", SDL_SCANCODE_H},
{"I", SDL_SCANCODE_I},
{"J", SDL_SCANCODE_J},
{"K", SDL_SCANCODE_K},
{"L", SDL_SCANCODE_L},
{"M", SDL_SCANCODE_M},
{"N", SDL_SCANCODE_N},
{"O", SDL_SCANCODE_O},
{"P", SDL_SCANCODE_P},
{"Q", SDL_SCANCODE_Q},
{"R", SDL_SCANCODE_R},
{"S", SDL_SCANCODE_S},
{"T", SDL_SCANCODE_T},
{"U", SDL_SCANCODE_U},
{"V", SDL_SCANCODE_V},
{"W", SDL_SCANCODE_W},
{"X", SDL_SCANCODE_X},
{"Y", SDL_SCANCODE_Y},
{"Z", SDL_SCANCODE_Z},
{"1", SDL_SCANCODE_1},
{"2", SDL_SCANCODE_2},
{"3", SDL_SCANCODE_3},
{"4", SDL_SCANCODE_4},
{"5", SDL_SCANCODE_5},
{"6", SDL_SCANCODE_6},
{"7", SDL_SCANCODE_7},
{"8", SDL_SCANCODE_8},
{"9", SDL_SCANCODE_9},
{"0", SDL_SCANCODE_0},
{"RETURN", SDL_SCANCODE_RETURN},
{"ESCAPE", SDL_SCANCODE_ESCAPE},
{"BACKSPACE", SDL_SCANCODE_BACKSPACE},
{"TAB", SDL_SCANCODE_TAB},
{"SPACE", SDL_SCANCODE_SPACE},
{"UP", SDL_SCANCODE_UP},
{"DOWN", SDL_SCANCODE_DOWN},
{"LEFT", SDL_SCANCODE_LEFT},
{"RIGHT", SDL_SCANCODE_RIGHT},
{"LSHIFT", SDL_SCANCODE_LSHIFT},
{"RSHIFT", SDL_SCANCODE_RSHIFT},
{"LCTRL", SDL_SCANCODE_LCTRL},
{"RCTRL", SDL_SCANCODE_RCTRL},
{"LALT", SDL_SCANCODE_LALT},
{"RALT", SDL_SCANCODE_RALT}};
// Mapa de botó de gamepad (int) a string
static const std::unordered_map<int, std::string> BUTTON_TO_STRING = {
{SDL_GAMEPAD_BUTTON_SOUTH, "SOUTH"}, // A (Xbox), Cross (PS)
{SDL_GAMEPAD_BUTTON_EAST, "EAST"}, // B (Xbox), Circle (PS)
{SDL_GAMEPAD_BUTTON_WEST, "WEST"}, // X (Xbox), Square (PS)
{SDL_GAMEPAD_BUTTON_NORTH, "NORTH"}, // Y (Xbox), Triangle (PS)
{SDL_GAMEPAD_BUTTON_BACK, "BACK"},
{SDL_GAMEPAD_BUTTON_START, "START"},
{SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, "LEFT_SHOULDER"},
{SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, "RIGHT_SHOULDER"},
{SDL_GAMEPAD_BUTTON_DPAD_UP, "DPAD_UP"},
{SDL_GAMEPAD_BUTTON_DPAD_DOWN, "DPAD_DOWN"},
{SDL_GAMEPAD_BUTTON_DPAD_LEFT, "DPAD_LEFT"},
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, "DPAD_RIGHT"},
{100, "L2_AS_BUTTON"}, // Trigger L2 com a botó digital
{101, "R2_AS_BUTTON"} // Trigger R2 com a botó digital
};
// Mapa invers: string a botó de gamepad
static const std::unordered_map<std::string, int> STRING_TO_BUTTON = {
{"SOUTH", SDL_GAMEPAD_BUTTON_SOUTH},
{"EAST", SDL_GAMEPAD_BUTTON_EAST},
{"WEST", SDL_GAMEPAD_BUTTON_WEST},
{"NORTH", SDL_GAMEPAD_BUTTON_NORTH},
{"BACK", SDL_GAMEPAD_BUTTON_BACK},
{"START", SDL_GAMEPAD_BUTTON_START},
{"LEFT_SHOULDER", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
{"RIGHT_SHOULDER", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
{"DPAD_UP", SDL_GAMEPAD_BUTTON_DPAD_UP},
{"DPAD_DOWN", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
{"DPAD_LEFT", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
{"DPAD_RIGHT", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
{"L2_AS_BUTTON", 100},
{"R2_AS_BUTTON", 101}};
static auto scancodeToString(SDL_Scancode code) -> std::string {
auto it = SCANCODE_TO_STRING.find(code);
return (it != SCANCODE_TO_STRING.end()) ? it->second : "UNKNOWN";
}
static auto stringToScancode(const std::string& str) -> SDL_Scancode {
auto it = STRING_TO_SCANCODE.find(str);
return (it != STRING_TO_SCANCODE.end()) ? it->second : SDL_SCANCODE_UNKNOWN;
}
static auto buttonToString(int button) -> std::string {
auto it = BUTTON_TO_STRING.find(button);
return (it != BUTTON_TO_STRING.end()) ? it->second : "UNKNOWN";
}
static auto stringToButton(const std::string& str) -> int {
auto it = STRING_TO_BUTTON.find(str);
return (it != STRING_TO_BUTTON.end()) ? it->second : SDL_GAMEPAD_BUTTON_INVALID;
}
// ========== FI FUNCIONS AUXILIARS ==========
// Inicialitzar opcions amb valors per defecte de Defaults::
void init() {
#ifdef _DEBUG
@@ -278,6 +446,80 @@ static void loadAudioConfigFromYaml(const fkyaml::node& yaml) {
}
}
// Carregar controls del jugador 1 des de YAML
static void loadPlayer1ControlsFromYaml(const fkyaml::node& yaml) {
if (!yaml.contains("player1")) return;
const auto& p1 = yaml["player1"];
// Carregar controls de teclat
if (p1.contains("keyboard")) {
const auto& kb = p1["keyboard"];
if (kb.contains("key_left"))
player1.keyboard.key_left = stringToScancode(kb["key_left"].get_value<std::string>());
if (kb.contains("key_right"))
player1.keyboard.key_right = stringToScancode(kb["key_right"].get_value<std::string>());
if (kb.contains("key_thrust"))
player1.keyboard.key_thrust = stringToScancode(kb["key_thrust"].get_value<std::string>());
if (kb.contains("key_shoot"))
player1.keyboard.key_shoot = stringToScancode(kb["key_shoot"].get_value<std::string>());
}
// Carregar controls de gamepad
if (p1.contains("gamepad")) {
const auto& gp = p1["gamepad"];
if (gp.contains("button_left"))
player1.gamepad.button_left = stringToButton(gp["button_left"].get_value<std::string>());
if (gp.contains("button_right"))
player1.gamepad.button_right = stringToButton(gp["button_right"].get_value<std::string>());
if (gp.contains("button_thrust"))
player1.gamepad.button_thrust = stringToButton(gp["button_thrust"].get_value<std::string>());
if (gp.contains("button_shoot"))
player1.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>());
}
// Carregar nom del gamepad
if (p1.contains("gamepad_name"))
player1.gamepad_name = p1["gamepad_name"].get_value<std::string>();
}
// Carregar controls del jugador 2 des de YAML
static void loadPlayer2ControlsFromYaml(const fkyaml::node& yaml) {
if (!yaml.contains("player2")) return;
const auto& p2 = yaml["player2"];
// Carregar controls de teclat
if (p2.contains("keyboard")) {
const auto& kb = p2["keyboard"];
if (kb.contains("key_left"))
player2.keyboard.key_left = stringToScancode(kb["key_left"].get_value<std::string>());
if (kb.contains("key_right"))
player2.keyboard.key_right = stringToScancode(kb["key_right"].get_value<std::string>());
if (kb.contains("key_thrust"))
player2.keyboard.key_thrust = stringToScancode(kb["key_thrust"].get_value<std::string>());
if (kb.contains("key_shoot"))
player2.keyboard.key_shoot = stringToScancode(kb["key_shoot"].get_value<std::string>());
}
// Carregar controls de gamepad
if (p2.contains("gamepad")) {
const auto& gp = p2["gamepad"];
if (gp.contains("button_left"))
player2.gamepad.button_left = stringToButton(gp["button_left"].get_value<std::string>());
if (gp.contains("button_right"))
player2.gamepad.button_right = stringToButton(gp["button_right"].get_value<std::string>());
if (gp.contains("button_thrust"))
player2.gamepad.button_thrust = stringToButton(gp["button_thrust"].get_value<std::string>());
if (gp.contains("button_shoot"))
player2.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>());
}
// Carregar nom del gamepad
if (p2.contains("gamepad_name"))
player2.gamepad_name = p2["gamepad_name"].get_value<std::string>();
}
// Carregar configuració des del fitxer YAML
auto loadFromFile() -> bool {
const std::string CONFIG_VERSION = std::string(Project::VERSION);
@@ -325,6 +567,8 @@ auto loadFromFile() -> bool {
loadGameplayConfigFromYaml(yaml);
loadRenderingConfigFromYaml(yaml);
loadAudioConfigFromYaml(yaml);
loadPlayer1ControlsFromYaml(yaml);
loadPlayer2ControlsFromYaml(yaml);
if (console) {
std::cout << "Config carregada correctament des de: " << config_file_path
@@ -345,6 +589,40 @@ auto loadFromFile() -> bool {
}
}
// Guardar controls del jugador 1 a YAML
static void savePlayer1ControlsToYaml(std::ofstream& file) {
file << "# CONTROLS JUGADOR 1\n";
file << "player1:\n";
file << " keyboard:\n";
file << " key_left: " << scancodeToString(player1.keyboard.key_left) << "\n";
file << " key_right: " << scancodeToString(player1.keyboard.key_right) << "\n";
file << " key_thrust: " << scancodeToString(player1.keyboard.key_thrust) << "\n";
file << " key_shoot: " << scancodeToString(player1.keyboard.key_shoot) << "\n";
file << " gamepad:\n";
file << " button_left: " << buttonToString(player1.gamepad.button_left) << "\n";
file << " button_right: " << buttonToString(player1.gamepad.button_right) << "\n";
file << " button_thrust: " << buttonToString(player1.gamepad.button_thrust) << "\n";
file << " button_shoot: " << buttonToString(player1.gamepad.button_shoot) << "\n";
file << " gamepad_name: \"" << player1.gamepad_name << "\" # Buit = primer disponible\n\n";
}
// Guardar controls del jugador 2 a YAML
static void savePlayer2ControlsToYaml(std::ofstream& file) {
file << "# CONTROLS JUGADOR 2\n";
file << "player2:\n";
file << " keyboard:\n";
file << " key_left: " << scancodeToString(player2.keyboard.key_left) << "\n";
file << " key_right: " << scancodeToString(player2.keyboard.key_right) << "\n";
file << " key_thrust: " << scancodeToString(player2.keyboard.key_thrust) << "\n";
file << " key_shoot: " << scancodeToString(player2.keyboard.key_shoot) << "\n";
file << " gamepad:\n";
file << " button_left: " << buttonToString(player2.gamepad.button_left) << "\n";
file << " button_right: " << buttonToString(player2.gamepad.button_right) << "\n";
file << " button_thrust: " << buttonToString(player2.gamepad.button_thrust) << "\n";
file << " button_shoot: " << buttonToString(player2.gamepad.button_shoot) << "\n";
file << " gamepad_name: \"" << player2.gamepad_name << "\" # Buit = segon disponible\n\n";
}
// Guardar configuració al fitxer YAML
auto saveToFile() -> bool {
std::ofstream file(config_file_path);
@@ -399,7 +677,11 @@ auto saveToFile() -> bool {
file << " volume: " << audio.music.volume << " # 0.0 to 1.0\n";
file << " sound:\n";
file << " enabled: " << (audio.sound.enabled ? "true" : "false") << "\n";
file << " volume: " << audio.sound.volume << " # 0.0 to 1.0\n";
file << " volume: " << audio.sound.volume << " # 0.0 to 1.0\n\n";
// Guardar controls de jugadors
savePlayer1ControlsToYaml(file);
savePlayer2ControlsToYaml(file);
file.close();

View File

@@ -1,5 +1,7 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_Scancode
#include <string>
namespace Options {
@@ -48,6 +50,29 @@ struct Audio {
float volume{1.0f};
};
// Controles de jugadors
struct KeyboardControls {
SDL_Scancode key_left{SDL_SCANCODE_LEFT};
SDL_Scancode key_right{SDL_SCANCODE_RIGHT};
SDL_Scancode key_thrust{SDL_SCANCODE_UP};
SDL_Scancode key_shoot{SDL_SCANCODE_SPACE};
SDL_Scancode key_start{SDL_SCANCODE_1};
};
struct GamepadControls {
int button_left{SDL_GAMEPAD_BUTTON_DPAD_LEFT};
int button_right{SDL_GAMEPAD_BUTTON_DPAD_RIGHT};
int button_thrust{SDL_GAMEPAD_BUTTON_WEST}; // X button
int button_shoot{SDL_GAMEPAD_BUTTON_SOUTH}; // A button
};
struct PlayerControls {
KeyboardControls keyboard{};
GamepadControls gamepad{};
std::string gamepad_name{""}; // Buit = auto-assignar per índex
};
// Variables globals (inline per evitar ODR violations)
inline std::string version{}; // Versió del config per validació
@@ -58,6 +83,31 @@ inline Gameplay gameplay{};
inline Rendering rendering{};
inline Audio audio{};
// Controles per jugador
inline PlayerControls player1{
.keyboard =
{.key_left = SDL_SCANCODE_LEFT,
.key_right = SDL_SCANCODE_RIGHT,
.key_thrust = SDL_SCANCODE_UP,
.key_shoot = SDL_SCANCODE_SPACE,
.key_start = SDL_SCANCODE_1},
.gamepad_name = "" // Primer gamepad disponible
};
inline PlayerControls player2{
.keyboard =
{.key_left = SDL_SCANCODE_A,
.key_right = SDL_SCANCODE_D,
.key_thrust = SDL_SCANCODE_W,
.key_shoot = SDL_SCANCODE_LSHIFT,
.key_start = SDL_SCANCODE_2},
.gamepad_name = "" // Segon gamepad disponible
};
// Per compatibilitat amb pollo (no utilitzat en orni, però necessari per Input)
inline KeyboardControls keyboard_controls{};
inline GamepadControls gamepad_controls{};
inline std::string config_file_path{}; // Establert per setConfigFile()
// Funcions públiques

View File

@@ -9,7 +9,10 @@
namespace StageSystem {
SpawnController::SpawnController()
: config_(nullptr), temps_transcorregut_(0.0f), index_spawn_actual_(0), ship_position_(nullptr) {}
: config_(nullptr),
temps_transcorregut_(0.0f),
index_spawn_actual_(0),
ship_position_(nullptr) {}
void SpawnController::configurar(const ConfigStage* config) {
config_ = config;
@@ -114,7 +117,7 @@ void SpawnController::generar_spawn_events() {
for (uint8_t i = 0; i < config_->total_enemics; i++) {
float spawn_time = config_->config_spawn.delay_inicial +
(i * config_->config_spawn.interval_spawn);
(i * config_->config_spawn.interval_spawn);
TipusEnemic tipus = seleccionar_tipus_aleatori();

View File

@@ -15,44 +15,44 @@ namespace StageSystem {
// Informació de spawn planificat
struct SpawnEvent {
float temps_spawn; // Temps absolut (segons) per spawnejar
TipusEnemic tipus; // Tipus d'enemic
bool spawnejat; // Ja s'ha processat?
float temps_spawn; // Temps absolut (segons) per spawnejar
TipusEnemic tipus; // Tipus d'enemic
bool spawnejat; // Ja s'ha processat?
};
class SpawnController {
public:
SpawnController();
public:
SpawnController();
// Configuration
void configurar(const ConfigStage* config); // Set stage config
void iniciar(); // Generate spawn schedule
void reset(); // Clear all pending spawns
// Configuration
void configurar(const ConfigStage* config); // Set stage config
void iniciar(); // Generate spawn schedule
void reset(); // Clear all pending spawns
// Update
void actualitzar(float delta_time, std::array<Enemic, 15>& orni_array, bool pausar = false);
// Update
void actualitzar(float delta_time, std::array<Enemic, 15>& orni_array, bool pausar = false);
// Status queries
bool tots_enemics_spawnejats() const;
bool tots_enemics_destruits(const std::array<Enemic, 15>& orni_array) const;
uint8_t get_enemics_vius(const std::array<Enemic, 15>& orni_array) const;
uint8_t get_enemics_spawnejats() const;
// Status queries
bool tots_enemics_spawnejats() const;
bool tots_enemics_destruits(const std::array<Enemic, 15>& orni_array) const;
uint8_t get_enemics_vius(const std::array<Enemic, 15>& orni_array) const;
uint8_t get_enemics_spawnejats() const;
// [NEW] Set ship position reference for safe spawn
void set_ship_position(const Punt* ship_pos) { ship_position_ = ship_pos; }
// [NEW] Set ship position reference for safe spawn
void set_ship_position(const Punt* ship_pos) { ship_position_ = ship_pos; }
private:
const ConfigStage* config_; // Non-owning pointer to current stage config
std::vector<SpawnEvent> spawn_queue_;
float temps_transcorregut_; // Elapsed time since stage start
uint8_t index_spawn_actual_; // Next spawn to process
private:
const ConfigStage* config_; // Non-owning pointer to current stage config
std::vector<SpawnEvent> spawn_queue_;
float temps_transcorregut_; // Elapsed time since stage start
uint8_t index_spawn_actual_; // Next spawn to process
// Spawn generation
void generar_spawn_events();
TipusEnemic seleccionar_tipus_aleatori() const;
void spawn_enemic(Enemic& enemic, TipusEnemic tipus, const Punt* ship_pos = nullptr);
void aplicar_multiplicadors(Enemic& enemic) const;
const Punt* ship_position_; // [NEW] Non-owning pointer to ship position
// Spawn generation
void generar_spawn_events();
TipusEnemic seleccionar_tipus_aleatori() const;
void spawn_enemic(Enemic& enemic, TipusEnemic tipus, const Punt* ship_pos = nullptr);
void aplicar_multiplicadors(Enemic& enemic) const;
const Punt* ship_position_; // [NEW] Non-owning pointer to ship position
};
} // namespace StageSystem

View File

@@ -19,82 +19,81 @@ enum class ModeSpawn {
// Configuració de spawn
struct ConfigSpawn {
ModeSpawn mode;
float delay_inicial; // Segons abans del primer spawn
float interval_spawn; // Segons entre spawns consecutius
ModeSpawn mode;
float delay_inicial; // Segons abans del primer spawn
float interval_spawn; // Segons entre spawns consecutius
};
// Distribució de tipus d'enemics (percentatges)
struct DistribucioEnemics {
uint8_t pentagon; // 0-100
uint8_t quadrat; // 0-100
uint8_t molinillo; // 0-100
// Suma ha de ser 100, validat en StageLoader
uint8_t pentagon; // 0-100
uint8_t quadrat; // 0-100
uint8_t molinillo; // 0-100
// Suma ha de ser 100, validat en StageLoader
};
// Multiplicadors de dificultat
struct MultiplicadorsDificultat {
float velocitat; // 0.5-2.0 típic
float rotacio; // 0.5-2.0 típic
float tracking_strength; // 0.0-1.5 (aplicat a Quadrat)
float velocitat; // 0.5-2.0 típic
float rotacio; // 0.5-2.0 típic
float tracking_strength; // 0.0-1.5 (aplicat a Quadrat)
};
// Metadades del fitxer YAML
struct MetadataStages {
std::string version;
uint8_t total_stages;
std::string descripcio;
std::string version;
uint8_t total_stages;
std::string descripcio;
};
// Configuració completa d'un stage
struct ConfigStage {
uint8_t stage_id; // 1-10
uint8_t total_enemics; // 5-15
ConfigSpawn config_spawn;
DistribucioEnemics distribucio;
MultiplicadorsDificultat multiplicadors;
uint8_t stage_id; // 1-10
uint8_t total_enemics; // 5-15
ConfigSpawn config_spawn;
DistribucioEnemics distribucio;
MultiplicadorsDificultat multiplicadors;
// Validació
bool es_valid() const {
return stage_id >= 1 && stage_id <= 255 &&
total_enemics > 0 && total_enemics <= 15 &&
distribucio.pentagon + distribucio.quadrat + distribucio.molinillo == 100;
}
// Validació
bool es_valid() const {
return stage_id >= 1 && stage_id <= 255 &&
total_enemics > 0 && total_enemics <= 15 &&
distribucio.pentagon + distribucio.quadrat + distribucio.molinillo == 100;
}
};
// Configuració completa del sistema (carregada des de YAML)
struct ConfigSistemaStages {
MetadataStages metadata;
std::vector<ConfigStage> stages; // Índex [0] = stage 1
MetadataStages metadata;
std::vector<ConfigStage> stages; // Índex [0] = stage 1
// Obtenir configuració d'un stage específic
const ConfigStage* obte_stage(uint8_t stage_id) const {
if (stage_id < 1 || stage_id > stages.size()) {
return nullptr;
// Obtenir configuració d'un stage específic
const ConfigStage* obte_stage(uint8_t stage_id) const {
if (stage_id < 1 || stage_id > stages.size()) {
return nullptr;
}
return &stages[stage_id - 1];
}
return &stages[stage_id - 1];
}
};
// Constants per missatges de transició
namespace Constants {
// Pool de missatges per inici de level (selecció aleatòria)
inline constexpr std::array<const char*, 12> MISSATGES_LEVEL_START = {
"ORNI ALERT!",
"INCOMING ORNIS!",
"ROLLING THREAT!",
"ENEMY WAVE!",
"WAVE OF ORNIS DETECTED!",
"NEXT SWARM APPROACHING!",
"BRACE FOR THE NEXT WAVE!",
"ANOTHER ATTACK INCOMING!",
"SENSORS DETECT HOSTILE ORNIS...",
"UNIDENTIFIED ROLLING OBJECTS INBOUND!",
"ENEMY FORCES MOBILIZING!",
"PREPARE FOR IMPACT!"
};
// Pool de missatges per inici de level (selecció aleatòria)
inline constexpr std::array<const char*, 12> MISSATGES_LEVEL_START = {
"ORNI ALERT!",
"INCOMING ORNIS!",
"ROLLING THREAT!",
"ENEMY WAVE!",
"WAVE OF ORNIS DETECTED!",
"NEXT SWARM APPROACHING!",
"BRACE FOR THE NEXT WAVE!",
"ANOTHER ATTACK INCOMING!",
"SENSORS DETECT HOSTILE ORNIS...",
"UNIDENTIFIED ROLLING OBJECTS INBOUND!",
"ENEMY FORCES MOBILIZING!",
"PREPARE FOR IMPACT!"};
constexpr const char* MISSATGE_LEVEL_COMPLETED = "GOOD JOB COMMANDER!";
}
constexpr const char* MISSATGE_LEVEL_COMPLETED = "GOOD JOB COMMANDER!";
} // namespace Constants
} // namespace StageSystem

View File

@@ -3,13 +3,13 @@
#include "stage_loader.hpp"
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
#include <fstream>
#include <iostream>
#include <sstream>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
namespace StageSystem {
std::unique_ptr<ConfigSistemaStages> StageLoader::carregar(const std::string& path) {
@@ -88,8 +88,8 @@ bool StageLoader::parse_metadata(const fkyaml::node& yaml, MetadataStages& meta)
meta.version = yaml["version"].get_value<std::string>();
meta.total_stages = yaml["total_stages"].get_value<uint8_t>();
meta.descripcio = yaml.contains("description")
? yaml["description"].get_value<std::string>()
: "";
? yaml["description"].get_value<std::string>()
: "";
return true;
} catch (const std::exception& e) {

View File

@@ -5,28 +5,29 @@
#include <memory>
#include <string>
#include "external/fkyaml_node.hpp"
#include "stage_config.hpp"
namespace StageSystem {
class StageLoader {
public:
// Carregar configuració des de fitxer YAML
// Retorna nullptr si hi ha errors
static std::unique_ptr<ConfigSistemaStages> carregar(const std::string& path);
public:
// Carregar configuració des de fitxer YAML
// Retorna nullptr si hi ha errors
static std::unique_ptr<ConfigSistemaStages> carregar(const std::string& path);
private:
// Parsing helpers (implementats en .cpp)
static bool parse_metadata(const fkyaml::node& yaml, MetadataStages& meta);
static bool parse_stage(const fkyaml::node& yaml, ConfigStage& stage);
static bool parse_spawn_config(const fkyaml::node& yaml, ConfigSpawn& config);
static bool parse_distribution(const fkyaml::node& yaml, DistribucioEnemics& dist);
static bool parse_multipliers(const fkyaml::node& yaml, MultiplicadorsDificultat& mult);
static ModeSpawn parse_spawn_mode(const std::string& mode_str);
private:
// Parsing helpers (implementats en .cpp)
static bool parse_metadata(const fkyaml::node& yaml, MetadataStages& meta);
static bool parse_stage(const fkyaml::node& yaml, ConfigStage& stage);
static bool parse_spawn_config(const fkyaml::node& yaml, ConfigSpawn& config);
static bool parse_distribution(const fkyaml::node& yaml, DistribucioEnemics& dist);
static bool parse_multipliers(const fkyaml::node& yaml, MultiplicadorsDificultat& mult);
static ModeSpawn parse_spawn_mode(const std::string& mode_str);
// Validació
static bool validar_config(const ConfigSistemaStages& config);
// Validació
static bool validar_config(const ConfigSistemaStages& config);
};
} // namespace StageSystem

View File

@@ -5,6 +5,7 @@
#include <iostream>
#include "core/audio/audio.hpp"
#include "core/defaults.hpp"
namespace StageSystem {
@@ -22,7 +23,7 @@ StageManager::StageManager(const ConfigSistemaStages* config)
void StageManager::inicialitzar() {
stage_actual_ = 1;
carregar_stage(stage_actual_);
canviar_estat(EstatStage::LEVEL_START);
canviar_estat(EstatStage::INIT_HUD);
std::cout << "[StageManager] Inicialitzat a stage " << static_cast<int>(stage_actual_)
<< std::endl;
@@ -30,6 +31,10 @@ void StageManager::inicialitzar() {
void StageManager::actualitzar(float delta_time, bool pausar_spawn) {
switch (estat_) {
case EstatStage::INIT_HUD:
processar_init_hud(delta_time);
break;
case EstatStage::LEVEL_START:
processar_level_start(delta_time);
break;
@@ -52,8 +57,8 @@ void StageManager::stage_completat() {
bool StageManager::tot_completat() const {
return stage_actual_ >= config_->metadata.total_stages &&
estat_ == EstatStage::LEVEL_COMPLETED &&
timer_transicio_ <= 0.0f;
estat_ == EstatStage::LEVEL_COMPLETED &&
timer_transicio_ <= 0.0f;
}
const ConfigStage* StageManager::get_config_actual() const {
@@ -64,7 +69,9 @@ void StageManager::canviar_estat(EstatStage nou_estat) {
estat_ = nou_estat;
// Set timer based on state type
if (nou_estat == EstatStage::LEVEL_START) {
if (nou_estat == EstatStage::INIT_HUD) {
timer_transicio_ = Defaults::Game::INIT_HUD_DURATION;
} else if (nou_estat == EstatStage::LEVEL_START) {
timer_transicio_ = Defaults::Game::LEVEL_START_DURATION;
} else if (nou_estat == EstatStage::LEVEL_COMPLETED) {
timer_transicio_ = Defaults::Game::LEVEL_COMPLETED_DURATION;
@@ -74,10 +81,19 @@ void StageManager::canviar_estat(EstatStage nou_estat) {
if (nou_estat == EstatStage::LEVEL_START) {
size_t index = static_cast<size_t>(std::rand()) % Constants::MISSATGES_LEVEL_START.size();
missatge_level_start_actual_ = Constants::MISSATGES_LEVEL_START[index];
// [NOU] Iniciar música al entrar en LEVEL_START (després de INIT_HUD)
// Només si no està sonant ja (per evitar reiniciar en loops posteriors)
if (Audio::get()->getMusicState() != Audio::MusicState::PLAYING) {
Audio::get()->playMusic("game.ogg");
}
}
std::cout << "[StageManager] Canvi d'estat: ";
switch (nou_estat) {
case EstatStage::INIT_HUD:
std::cout << "INIT_HUD";
break;
case EstatStage::LEVEL_START:
std::cout << "LEVEL_START";
break;
@@ -91,6 +107,14 @@ void StageManager::canviar_estat(EstatStage nou_estat) {
std::cout << std::endl;
}
void StageManager::processar_init_hud(float delta_time) {
timer_transicio_ -= delta_time;
if (timer_transicio_ <= 0.0f) {
canviar_estat(EstatStage::LEVEL_START);
}
}
void StageManager::processar_level_start(float delta_time) {
timer_transicio_ -= delta_time;
@@ -103,8 +127,8 @@ void StageManager::processar_playing(float delta_time, bool pausar_spawn) {
// Update spawn controller (pauses when pausar_spawn = true)
// Note: The actual enemy array update happens in EscenaJoc::actualitzar()
// This is just for internal timekeeping
(void)delta_time; // Spawn controller is updated externally
(void)pausar_spawn; // Passed to spawn_controller_.actualitzar() by EscenaJoc
(void)delta_time; // Spawn controller is updated externally
(void)pausar_spawn; // Passed to spawn_controller_.actualitzar() by EscenaJoc
}
void StageManager::processar_level_completed(float delta_time) {

View File

@@ -13,49 +13,51 @@ namespace StageSystem {
// Estats del stage system
enum class EstatStage {
LEVEL_START, // Pantalla "ENEMY INCOMING" (3s)
PLAYING, // Gameplay normal
LEVEL_COMPLETED // Pantalla "GOOD JOB COMMANDER!" (3s)
INIT_HUD, // Animació inicial del HUD (3s)
LEVEL_START, // Pantalla "ENEMY INCOMING" (3s)
PLAYING, // Gameplay normal
LEVEL_COMPLETED // Pantalla "GOOD JOB COMMANDER!" (3s)
};
class StageManager {
public:
explicit StageManager(const ConfigSistemaStages* config);
public:
explicit StageManager(const ConfigSistemaStages* config);
// Lifecycle
void inicialitzar(); // Reset to stage 1
void actualitzar(float delta_time, bool pausar_spawn = false);
// Lifecycle
void inicialitzar(); // Reset to stage 1
void actualitzar(float delta_time, bool pausar_spawn = false);
// Stage progression
void stage_completat(); // Call when all enemies destroyed
bool tot_completat() const; // All 10 stages done?
// Stage progression
void stage_completat(); // Call when all enemies destroyed
bool tot_completat() const; // All 10 stages done?
// Current state queries
EstatStage get_estat() const { return estat_; }
uint8_t get_stage_actual() const { return stage_actual_; }
const ConfigStage* get_config_actual() const;
float get_timer_transicio() const { return timer_transicio_; }
const std::string& get_missatge_level_start() const { return missatge_level_start_actual_; }
// Current state queries
EstatStage get_estat() const { return estat_; }
uint8_t get_stage_actual() const { return stage_actual_; }
const ConfigStage* get_config_actual() const;
float get_timer_transicio() const { return timer_transicio_; }
const std::string& get_missatge_level_start() const { return missatge_level_start_actual_; }
// Spawn control (delegate to SpawnController)
SpawnController& get_spawn_controller() { return spawn_controller_; }
const SpawnController& get_spawn_controller() const { return spawn_controller_; }
// Spawn control (delegate to SpawnController)
SpawnController& get_spawn_controller() { return spawn_controller_; }
const SpawnController& get_spawn_controller() const { return spawn_controller_; }
private:
const ConfigSistemaStages* config_; // Non-owning pointer
SpawnController spawn_controller_;
private:
const ConfigSistemaStages* config_; // Non-owning pointer
SpawnController spawn_controller_;
EstatStage estat_;
uint8_t stage_actual_; // 1-10
float timer_transicio_; // Timer for LEVEL_START/LEVEL_COMPLETED (3.0s → 0.0s)
std::string missatge_level_start_actual_; // Missatge seleccionat per al level actual
EstatStage estat_;
uint8_t stage_actual_; // 1-10
float timer_transicio_; // Timer for LEVEL_START/LEVEL_COMPLETED (3.0s → 0.0s)
std::string missatge_level_start_actual_; // Missatge seleccionat per al level actual
// State transitions
void canviar_estat(EstatStage nou_estat);
void processar_level_start(float delta_time);
void processar_playing(float delta_time, bool pausar_spawn);
void processar_level_completed(float delta_time);
void carregar_stage(uint8_t stage_id);
// State transitions
void canviar_estat(EstatStage nou_estat);
void processar_init_hud(float delta_time);
void processar_level_start(float delta_time);
void processar_playing(float delta_time, bool pausar_spawn);
void processar_level_completed(float delta_time);
void carregar_stage(uint8_t stage_id);
};
} // namespace StageSystem

View File

@@ -0,0 +1,332 @@
// ship_animator.cpp - Implementació del sistema d'animació de naus
// © 2025 Port a C++20 amb SDL3
#include "ship_animator.hpp"
#include <cmath>
#include "core/defaults.hpp"
#include "core/graphics/shape_loader.hpp"
#include "core/math/easing.hpp"
namespace Title {
ShipAnimator::ShipAnimator(SDL_Renderer* renderer)
: renderer_(renderer) {
}
void ShipAnimator::inicialitzar() {
// Carregar formes de naus amb perspectiva pre-calculada
auto forma_p1 = Graphics::ShapeLoader::load("ship_perspective.shp"); // Perspectiva esquerra
auto forma_p2 = Graphics::ShapeLoader::load("ship2_perspective.shp"); // Perspectiva dreta
// Configurar nau P1
naus_[0].jugador_id = 1;
naus_[0].forma = forma_p1;
configurar_nau_p1(naus_[0]);
// Configurar nau P2
naus_[1].jugador_id = 2;
naus_[1].forma = forma_p2;
configurar_nau_p2(naus_[1]);
}
void ShipAnimator::actualitzar(float delta_time) {
// Dispatcher segons estat de cada nau
for (auto& nau : naus_) {
if (!nau.visible) continue;
switch (nau.estat) {
case EstatNau::ENTERING:
actualitzar_entering(nau, delta_time);
break;
case EstatNau::FLOATING:
actualitzar_floating(nau, delta_time);
break;
case EstatNau::EXITING:
actualitzar_exiting(nau, delta_time);
break;
}
}
}
void ShipAnimator::dibuixar() const {
for (const auto& nau : naus_) {
if (!nau.visible) continue;
// Renderitzar nau (perspectiva ja incorporada a la forma)
Rendering::render_shape(
renderer_,
nau.forma,
nau.posicio_actual,
0.0f, // angle (rotació 2D no utilitzada)
nau.escala_actual,
true, // dibuixar
1.0f, // progress (sempre visible)
1.0f // brightness (brillantor màxima)
);
}
}
void ShipAnimator::start_entry_animation() {
using namespace Defaults::Title::Ships;
// Configurar nau P1 per a l'animació d'entrada
naus_[0].estat = EstatNau::ENTERING;
naus_[0].temps_estat = 0.0f;
naus_[0].posicio_inicial = calcular_posicio_fora_pantalla(CLOCK_8_ANGLE);
naus_[0].posicio_actual = naus_[0].posicio_inicial;
naus_[0].escala_actual = naus_[0].escala_inicial;
// Configurar nau P2 per a l'animació d'entrada
naus_[1].estat = EstatNau::ENTERING;
naus_[1].temps_estat = 0.0f;
naus_[1].posicio_inicial = calcular_posicio_fora_pantalla(CLOCK_4_ANGLE);
naus_[1].posicio_actual = naus_[1].posicio_inicial;
naus_[1].escala_actual = naus_[1].escala_inicial;
}
void ShipAnimator::trigger_exit_animation() {
// Configurar ambdues naus per a l'animació de sortida
for (auto& nau : naus_) {
// Canviar estat a EXITING
nau.estat = EstatNau::EXITING;
nau.temps_estat = 0.0f;
// Preservar posició actual (pot estar a mig camí si START es prem durant ENTERING)
nau.posicio_inicial = nau.posicio_actual;
// La escala objectiu es preserva per a calcular la interpolació
// (escala_actual pot ser diferent si està en ENTERING)
}
}
void ShipAnimator::skip_to_floating_state() {
// Posar ambdues naus directament en estat FLOATING
for (auto& nau : naus_) {
nau.estat = EstatNau::FLOATING;
nau.temps_estat = 0.0f;
nau.fase_oscilacio = 0.0f;
// Posar en posició objectiu (sense animació)
nau.posicio_actual = nau.posicio_objectiu;
nau.escala_actual = nau.escala_objectiu;
// NO establir visibilitat aquí - ja ho fa el caller
// (evita fer visibles ambdues naus quan només una ha premut START)
}
}
bool ShipAnimator::is_visible() const {
// Retorna true si almenys una nau és visible
for (const auto& nau : naus_) {
if (nau.visible) {
return true;
}
}
return false;
}
void ShipAnimator::trigger_exit_animation_for_player(int jugador_id) {
// Trobar la nau del jugador especificat
for (auto& nau : naus_) {
if (nau.jugador_id == jugador_id) {
// Canviar estat a EXITING només per aquesta nau
nau.estat = EstatNau::EXITING;
nau.temps_estat = 0.0f;
// Preservar posició actual (pot estar a mig camí si START es prem durant ENTERING)
nau.posicio_inicial = nau.posicio_actual;
// La escala objectiu es preserva per a calcular la interpolació
// (escala_actual pot ser diferent si està en ENTERING)
break; // Només una nau per jugador
}
}
}
void ShipAnimator::set_visible(bool visible) {
for (auto& nau : naus_) {
nau.visible = visible;
}
}
bool ShipAnimator::is_animation_complete() const {
// Comprovar si totes les naus són invisibles (han completat l'animació de sortida)
for (const auto& nau : naus_) {
if (nau.visible) {
return false; // Encara hi ha alguna nau visible
}
}
return true; // Totes les naus són invisibles
}
// Mètodes d'animació (stubs)
void ShipAnimator::actualitzar_entering(NauTitol& nau, float delta_time) {
using namespace Defaults::Title::Ships;
nau.temps_estat += delta_time;
// Esperar al delay abans de començar l'animació
if (nau.temps_estat < nau.entry_delay) {
// Encara en delay: la nau es queda fora de pantalla (posició inicial)
nau.posicio_actual = nau.posicio_inicial;
nau.escala_actual = nau.escala_inicial;
return;
}
// Càlcul del progrés (restant el delay)
float elapsed = nau.temps_estat - nau.entry_delay;
float progress = std::min(1.0f, elapsed / ENTRY_DURATION);
// Aplicar easing (ease_out_quad per arribada suau)
float eased_progress = Easing::ease_out_quad(progress);
// Lerp posició (inicial → objectiu)
nau.posicio_actual.x = Easing::lerp(nau.posicio_inicial.x, nau.posicio_objectiu.x, eased_progress);
nau.posicio_actual.y = Easing::lerp(nau.posicio_inicial.y, nau.posicio_objectiu.y, eased_progress);
// Lerp escala (gran → normal)
nau.escala_actual = Easing::lerp(nau.escala_inicial, nau.escala_objectiu, eased_progress);
// Transicionar a FLOATING quan completi
if (elapsed >= ENTRY_DURATION) {
nau.estat = EstatNau::FLOATING;
nau.temps_estat = 0.0f;
nau.fase_oscilacio = 0.0f; // Reiniciar fase d'oscil·lació
}
}
void ShipAnimator::actualitzar_floating(NauTitol& nau, float delta_time) {
using namespace Defaults::Title::Ships;
// Actualitzar temps i fase d'oscil·lació
nau.temps_estat += delta_time;
nau.fase_oscilacio += delta_time;
// Oscil·lació sinusoïdal X/Y (paràmetres específics per nau)
float offset_x = nau.amplitude_x * std::sin(2.0f * Defaults::Math::PI * nau.frequency_x * nau.fase_oscilacio);
float offset_y = nau.amplitude_y * std::sin(2.0f * Defaults::Math::PI * nau.frequency_y * nau.fase_oscilacio + FLOAT_PHASE_OFFSET);
// Aplicar oscil·lació a la posició objectiu
nau.posicio_actual.x = nau.posicio_objectiu.x + offset_x;
nau.posicio_actual.y = nau.posicio_objectiu.y + offset_y;
// Escala constant (sense "breathing" per ara)
nau.escala_actual = nau.escala_objectiu;
}
void ShipAnimator::actualitzar_exiting(NauTitol& nau, float delta_time) {
using namespace Defaults::Title::Ships;
nau.temps_estat += delta_time;
// Calcular progrés (0.0 → 1.0)
float progress = std::min(1.0f, nau.temps_estat / EXIT_DURATION);
// Aplicar easing (ease_in_quad per acceleració cap al punt de fuga)
float eased_progress = Easing::ease_in_quad(progress);
// Punt de fuga (centre del starfield)
constexpr Punt punt_fuga{VANISHING_POINT_X, VANISHING_POINT_Y};
// Lerp posició cap al punt de fuga (preservar posició inicial actual)
// Nota: posicio_inicial conté la posició on estava quan es va activar EXITING
nau.posicio_actual.x = Easing::lerp(nau.posicio_inicial.x, punt_fuga.x, eased_progress);
nau.posicio_actual.y = Easing::lerp(nau.posicio_inicial.y, punt_fuga.y, eased_progress);
// Escala redueix a 0 (simula Z → infinit)
nau.escala_actual = nau.escala_objectiu * (1.0f - eased_progress);
// Marcar invisible quan l'animació completi
if (progress >= 1.0f) {
nau.visible = false;
}
}
// Configuració
void ShipAnimator::configurar_nau_p1(NauTitol& nau) {
using namespace Defaults::Title::Ships;
// Estat inicial: FLOATING (per test estàtic)
nau.estat = EstatNau::FLOATING;
nau.temps_estat = 0.0f;
// Posicions (clock 8, bottom-left)
nau.posicio_objectiu = {P1_TARGET_X(), P1_TARGET_Y()};
// Calcular posició inicial (fora de pantalla)
nau.posicio_inicial = calcular_posicio_fora_pantalla(CLOCK_8_ANGLE);
nau.posicio_actual = nau.posicio_inicial; // Començar fora de pantalla
// Escales
nau.escala_objectiu = FLOATING_SCALE;
nau.escala_actual = FLOATING_SCALE;
nau.escala_inicial = ENTRY_SCALE_START;
// Flotació
nau.fase_oscilacio = 0.0f;
// Paràmetres d'entrada
nau.entry_delay = P1_ENTRY_DELAY;
// Paràmetres d'oscil·lació específics P1
nau.amplitude_x = FLOAT_AMPLITUDE_X;
nau.amplitude_y = FLOAT_AMPLITUDE_Y;
nau.frequency_x = FLOAT_FREQUENCY_X_BASE * P1_FREQUENCY_MULTIPLIER;
nau.frequency_y = FLOAT_FREQUENCY_Y_BASE * P1_FREQUENCY_MULTIPLIER;
// Visibilitat
nau.visible = true;
}
void ShipAnimator::configurar_nau_p2(NauTitol& nau) {
using namespace Defaults::Title::Ships;
// Estat inicial: FLOATING (per test estàtic)
nau.estat = EstatNau::FLOATING;
nau.temps_estat = 0.0f;
// Posicions (clock 4, bottom-right)
nau.posicio_objectiu = {P2_TARGET_X(), P2_TARGET_Y()};
// Calcular posició inicial (fora de pantalla)
nau.posicio_inicial = calcular_posicio_fora_pantalla(CLOCK_4_ANGLE);
nau.posicio_actual = nau.posicio_inicial; // Començar fora de pantalla
// Escales
nau.escala_objectiu = FLOATING_SCALE;
nau.escala_actual = FLOATING_SCALE;
nau.escala_inicial = ENTRY_SCALE_START;
// Flotació
nau.fase_oscilacio = 0.0f;
// Paràmetres d'entrada
nau.entry_delay = P2_ENTRY_DELAY;
// Paràmetres d'oscil·lació específics P2
nau.amplitude_x = FLOAT_AMPLITUDE_X;
nau.amplitude_y = FLOAT_AMPLITUDE_Y;
nau.frequency_x = FLOAT_FREQUENCY_X_BASE * P2_FREQUENCY_MULTIPLIER;
nau.frequency_y = FLOAT_FREQUENCY_Y_BASE * P2_FREQUENCY_MULTIPLIER;
// Visibilitat
nau.visible = true;
}
Punt ShipAnimator::calcular_posicio_fora_pantalla(float angle_rellotge) const {
using namespace Defaults::Title::Ships;
// Convertir angle del rellotge a radians (per exemple: 240° per clock 8)
// Calcular posició en direcció radial des del centre, però més lluny
// ENTRY_OFFSET es calcula automàticament: (SHIP_MAX_RADIUS * ENTRY_SCALE_START) + ENTRY_OFFSET_MARGIN
float extended_radius = CLOCK_RADIUS + ENTRY_OFFSET;
float x = Defaults::Game::WIDTH / 2.0f + extended_radius * std::cos(angle_rellotge);
float y = Defaults::Game::HEIGHT / 2.0f + extended_radius * std::sin(angle_rellotge);
return {x, y};
}
} // namespace Title

View File

@@ -0,0 +1,98 @@
// ship_animator.hpp - Sistema d'animació de naus per a l'escena de títol
// © 2025 Port a C++20 amb SDL3
#pragma once
#include <SDL3/SDL.h>
#include <array>
#include <memory>
#include "core/graphics/shape.hpp"
#include "core/rendering/shape_renderer.hpp"
#include "core/types.hpp"
namespace Title {
// Estats de l'animació de la nau
enum class EstatNau {
ENTERING, // Entrant des de fora de pantalla
FLOATING, // Flotant en posició estàtica
EXITING // Volant cap al punt de fuga
};
// Dades d'una nau individual al títol
struct NauTitol {
// Identificació
int jugador_id; // 1 o 2
// Estat
EstatNau estat;
float temps_estat; // Temps acumulat en l'estat actual
// Posicions
Punt posicio_inicial; // Posició d'inici (fora de pantalla per ENTERING)
Punt posicio_objectiu; // Posició objectiu (rellotge 8 o 4)
Punt posicio_actual; // Posició interpolada actual
// Escales (simulació eix Z)
float escala_inicial; // Escala d'inici (més gran = més a prop)
float escala_objectiu; // Escala objectiu (mida flotació)
float escala_actual; // Escala interpolada actual
// Flotació
float fase_oscilacio; // Acumulador de fase per moviment sinusoïdal
// Paràmetres d'entrada
float entry_delay; // Delay abans d'entrar (0.0 per P1, 0.5 per P2)
// Paràmetres d'oscil·lació per nau
float amplitude_x;
float amplitude_y;
float frequency_x;
float frequency_y;
// Forma
std::shared_ptr<Graphics::Shape> forma;
// Visibilitat
bool visible;
};
// Gestor d'animació de naus per a l'escena de títol
class ShipAnimator {
public:
explicit ShipAnimator(SDL_Renderer* renderer);
// Cicle de vida
void inicialitzar();
void actualitzar(float delta_time);
void dibuixar() const;
// Control d'estat (cridat per EscenaTitol)
void start_entry_animation();
void trigger_exit_animation(); // Anima totes les naus
void trigger_exit_animation_for_player(int jugador_id); // Anima només una nau (P1=1, P2=2)
void skip_to_floating_state(); // Salta directament a FLOATING sense animació
// Control de visibilitat
void set_visible(bool visible);
bool is_animation_complete() const;
bool is_visible() const; // Comprova si alguna nau és visible
private:
SDL_Renderer* renderer_;
std::array<NauTitol, 2> naus_; // Naus P1 i P2
// Mètodes d'animació
void actualitzar_entering(NauTitol& nau, float delta_time);
void actualitzar_floating(NauTitol& nau, float delta_time);
void actualitzar_exiting(NauTitol& nau, float delta_time);
// Configuració
void configurar_nau_p1(NauTitol& nau);
void configurar_nau_p2(NauTitol& nau);
Punt calcular_posicio_fora_pantalla(float angle_rellotge) const;
};
} // namespace Title

View File

@@ -5,5 +5,7 @@ constexpr const char* NAME = "@PROJECT_NAME@";
constexpr const char* LONG_NAME = "@PROJECT_LONG_NAME@";
constexpr const char* VERSION = "@PROJECT_VERSION@";
constexpr const char* COPYRIGHT = "@PROJECT_COPYRIGHT@";
constexpr const char* COPYRIGHT_ORIGINAL = "@PROJECT_COPYRIGHT_ORIGINAL@";
constexpr const char* COPYRIGHT_PORT = "@PROJECT_COPYRIGHT_PORT@";
constexpr const char* GIT_HASH = "@GIT_HASH@";
} // namespace Project