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,12 +105,29 @@ 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
@@ -86,6 +135,65 @@ constexpr float LEVEL_START_TYPING_RATIO = 0.3f; // 30% escribiendo, 70%
// Transición LEVEL_COMPLETED (mensaje "GOOD JOB COMMANDER!")
constexpr float LEVEL_COMPLETED_DURATION = 3.0f; // Duración total
constexpr float LEVEL_COMPLETED_TYPING_RATIO = 0.0f; // 0.0 = sin typewriter (directo)
// Transición INIT_HUD (animación inicial del HUD)
constexpr float INIT_HUD_DURATION = 3.0f; // Duración total del estado
// Ratios de animación (inicio y fin como porcentajes del tiempo total)
// RECT (rectángulo de 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)
@@ -180,6 +288,7 @@ 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 const char* TITLE_TRACK = "title.ogg"; // Pista de titulo
constexpr int FADE_DURATION_MS = 1000; // Fade out duration
} // namespace Music
@@ -187,11 +296,34 @@ constexpr int FADE_DURATION_MS = 1000; // Fade out duration
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 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)
@@ -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

@@ -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;
}
} 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

@@ -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,17 +4,17 @@
#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:
public:
// Singleton
static Loader& get();
@@ -33,7 +33,7 @@ public:
void setBasePath(const std::string& path);
std::string getBasePath() const;
private:
private:
Loader() = default;
~Loader() = default;

View File

@@ -27,7 +27,7 @@ struct ResourceEntry {
// Classe principal per gestionar paquets de recursos
class Pack {
public:
public:
Pack() = default;
~Pack() = default;
@@ -47,7 +47,7 @@ public:
// Validació
bool validatePack() const;
private:
private:
// Constants
static constexpr const char* MAGIC_HEADER = "ORNI";
static constexpr uint32_t VERSION = 1;

View File

@@ -3,12 +3,14 @@
#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:
public:
// Tipus d'escena del joc
enum class Escena {
LOGO, // Pantalla d'inici (logo JAILGAMES)
@@ -58,9 +60,20 @@ public:
opcio_ = Opcio::NONE;
}
private:
// 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

@@ -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;
@@ -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

@@ -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,6 +39,8 @@ class Bala {
float angle_;
float velocitat_;
bool esta_;
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

@@ -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,30 +54,38 @@ 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]) {
// 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]) {
if (input->checkActionPlayer1(InputAction::LEFT, Input::ALLOW_REPEAT)) {
angle_ -= Defaults::Physics::ROTATION_SPEED * delta_time;
}
// Acceleració
if (keyboard_state[SDL_SCANCODE_UP]) {
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) {
@@ -83,6 +93,25 @@ void Nau::processar_input(float delta_time) {
}
}
}
} 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;
}
}
}
}
}
void Nau::actualitzar(float delta_time) {
@@ -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; }
@@ -51,6 +55,7 @@ class Nau {
float velocitat_; // Velocitat (px/s)
bool esta_tocada_;
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 tocado(uint8_t player_id);
void detectar_col·lisions_bales_enemics(); // Col·lisions bala-enemic
void detectar_col·lisio_nau_enemics(); // Ship-enemy collision detection
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,9 +220,7 @@ 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)
{
} else if (nou_estat == EstatAnimacio::POST_EXPLOSION) {
Audio::get()->playMusic("title.ogg");
}
@@ -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;
@@ -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";
}
}
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);
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";
}
}
}
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,8 +52,10 @@ class EscenaTitol {
SDLManager& sdl_;
GestorEscenes::ContextEscenes& context_;
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
@@ -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;

View File

@@ -78,8 +78,8 @@ struct ConfigSistemaStages {
// 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 = {
// 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!",
@@ -91,10 +91,9 @@ namespace Constants {
"SENSORS DETECT HOSTILE ORNIS...",
"UNIDENTIFIED ROLLING OBJECTS INBOUND!",
"ENEMY FORCES MOBILIZING!",
"PREPARE FOR IMPACT!"
};
"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) {

View File

@@ -5,18 +5,19 @@
#include <memory>
#include <string>
#include "external/fkyaml_node.hpp"
#include "stage_config.hpp"
namespace StageSystem {
class StageLoader {
public:
public:
// Carregar configuració des de fitxer YAML
// Retorna nullptr si hi ha errors
static std::unique_ptr<ConfigSistemaStages> carregar(const std::string& path);
private:
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);

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;
@@ -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;

View File

@@ -13,6 +13,7 @@ namespace StageSystem {
// Estats del stage system
enum class EstatStage {
INIT_HUD, // Animació inicial del HUD (3s)
LEVEL_START, // Pantalla "ENEMY INCOMING" (3s)
PLAYING, // Gameplay normal
LEVEL_COMPLETED // Pantalla "GOOD JOB COMMANDER!" (3s)
@@ -52,6 +53,7 @@ class StageManager {
// 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);

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