Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 54031e3520 | |||
| 8b9d26a02c | |||
| 3d5277a395 | |||
| 2555157bd7 | |||
| 461eaedecf | |||
| 1891c9e49e | |||
| 829a895464 | |||
| 8bc259b25a | |||
| ec333efe66 | |||
| 3b432e6580 | |||
| 886ec8ab1d | |||
| bc5982b286 | |||
| 75a4a1b3b9 | |||
| f3f0bfcd9a | |||
| c959e0e3a0 | |||
| 8b896912b2 | |||
| 3d0057220d | |||
| 0c75f56cb5 | |||
| 0ceaa75862 | |||
| 087b8d346d | |||
| aca1f5200b | |||
| 3b638f4715 | |||
| 9a5adcbcc5 | |||
| d0be5ea2d1 | |||
| 07e00fff09 | |||
| b4e0ca7eca | |||
| b8173b205b | |||
| 57d623d6bc |
278
CLAUDE.md
278
CLAUDE.md
@@ -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
|
||||
|
||||
@@ -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
2134
data/gamecontrollerdb.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
28
data/shapes/ship2_perspective.shp
Normal file
28
data/shapes/ship2_perspective.shp
Normal 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
27
data/shapes/ship3.shp
Normal 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
|
||||
21
data/shapes/ship_perspective.shp
Normal file
21
data/shapes/ship_perspective.shp
Normal 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
|
||||
BIN
data/sounds/effects/continue.wav
Normal file
BIN
data/sounds/effects/continue.wav
Normal file
Binary file not shown.
BIN
data/sounds/effects/explosion2.wav
Normal file
BIN
data/sounds/effects/explosion2.wav
Normal file
Binary file not shown.
BIN
data/sounds/effects/init_hud.wav
Normal file
BIN
data/sounds/effects/init_hud.wav
Normal file
Binary file not shown.
BIN
data/sounds/effects/start.wav
Normal file
BIN
data/sounds/effects/start.wav
Normal file
Binary file not shown.
Binary file not shown.
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
606
source/core/input/input.cpp
Normal 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
162
source/core/input/input.hpp
Normal 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_;
|
||||
};
|
||||
60
source/core/input/input_types.cpp
Normal file
60
source/core/input/input_types.cpp
Normal 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)}};
|
||||
41
source/core/input/input_types.hpp
Normal file
41
source/core/input/input_types.hpp
Normal 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};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
44
source/core/math/easing.hpp
Normal file
44
source/core/math/easing.hpp
Normal 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
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
#include "resource_helper.hpp"
|
||||
|
||||
#include "resource_loader.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
|
||||
#include "resource_loader.hpp"
|
||||
|
||||
namespace Resource {
|
||||
namespace Helper {
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
51
source/core/system/game_config.hpp
Normal file
51
source/core/system/game_config.hpp
Normal 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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
332
source/game/title/ship_animator.cpp
Normal file
332
source/game/title/ship_animator.cpp
Normal 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
|
||||
98
source/game/title/ship_animator.hpp
Normal file
98
source/game/title/ship_animator.hpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user