afegit segon jugador

This commit is contained in:
2025-12-10 17:18:34 +01:00
parent aca1f5200b
commit 087b8d346d
9 changed files with 304 additions and 182 deletions

View File

@@ -1,8 +1,8 @@
# ship2.shp - Nau del jugador (triangle amb base còncava - punta de fletxa) # ship.shp - Nau del jugador 1 (triangle amb base còncava - punta de fletxa)
# © 1999 Visente i Sergi (versió Pascal) # © 1999 Visente i Sergi (versió Pascal)
# © 2025 Port a C++20 amb SDL3 # © 2025 Port a C++20 amb SDL3
name: ship2 name: ship
scale: 1.0 scale: 1.0
center: 0, 0 center: 0, 0

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

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

View File

@@ -138,6 +138,11 @@ constexpr float INIT_HUD_SHIP_RATIO = 1.0f; // Proporción animación nave
// Posición inicial de la nave en INIT_HUD (75% de altura de zona de juego) // Posición inicial de la nave en INIT_HUD (75% de altura de zona de juego)
constexpr float INIT_HUD_SHIP_START_Y_RATIO = 0.75f; // 75% desde el top de PLAYAREA 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
} // namespace Game } // namespace Game
// Física (valores actuales del juego, sincronizados con joc_asteroides.cpp) // Física (valores actuales del juego, sincronizados con joc_asteroides.cpp)
@@ -245,6 +250,23 @@ constexpr const char* LOGO = "effects/logo.wav"; //
constexpr const char* GOOD_JOB_COMMANDER = "voices/good_job_commander.wav"; // Voz: "Good job, commander" constexpr const char* GOOD_JOB_COMMANDER = "voices/good_job_commander.wav"; // Voz: "Good job, commander"
} // namespace Sound } // 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) // Enemy type configuration (tipus d'enemics)
namespace Enemies { namespace Enemies {
// Pentagon (esquivador - zigzag evasion) // Pentagon (esquivador - zigzag evasion)

View File

@@ -36,7 +36,7 @@ void Bala::inicialitzar() {
velocitat_ = 0.0f; velocitat_ = 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 // Activar bala i posicionar-la a la nau
// Basat en joc_asteroides.cpp línies 188-200 // Basat en joc_asteroides.cpp línies 188-200
@@ -50,6 +50,9 @@ void Bala::disparar(const Punt& posicio, float angle) {
// Angle = angle de la nau (dispara en la direcció que apunta) // Angle = angle de la nau (dispara en la direcció que apunta)
angle_ = angle; angle_ = angle;
// Almacenar propietario (0=P1, 1=P2)
owner_id_ = owner_id;
// Velocitat alta (el joc Pascal original usava 7 px/frame) // Velocitat alta (el joc Pascal original usava 7 px/frame)
// 7 px/frame × 20 FPS = 140 px/s // 7 px/frame × 20 FPS = 140 px/s
velocitat_ = 140.0f; velocitat_ = 140.0f;

View File

@@ -17,13 +17,14 @@ class Bala {
Bala(SDL_Renderer* renderer); Bala(SDL_Renderer* renderer);
void inicialitzar(); 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 actualitzar(float delta_time);
void dibuixar() const; void dibuixar() const;
// Getters (API pública sense canvis) // Getters (API pública sense canvis)
bool esta_activa() const { return esta_; } bool esta_activa() const { return esta_; }
const Punt& get_centre() const { return centre_; } const Punt& get_centre() const { return centre_; }
uint8_t get_owner_id() const { return owner_id_; }
void desactivar() { esta_ = false; } void desactivar() { esta_ = false; }
private: private:
@@ -37,6 +38,7 @@ class Bala {
float angle_; float angle_;
float velocitat_; float velocitat_;
bool esta_; bool esta_;
uint8_t owner_id_; // 0=P1, 1=P2
float brightness_; // Factor de brillantor (0.0-1.0) float brightness_; // Factor de brillantor (0.0-1.0)
void mou(float delta_time); void mou(float delta_time);

View File

@@ -14,7 +14,7 @@
#include "core/rendering/shape_renderer.hpp" #include "core/rendering/shape_renderer.hpp"
#include "game/constants.hpp" #include "game/constants.hpp"
Nau::Nau(SDL_Renderer* renderer) Nau::Nau(SDL_Renderer* renderer, const char* shape_file)
: renderer_(renderer), : renderer_(renderer),
centre_({0.0f, 0.0f}), centre_({0.0f, 0.0f}),
angle_(0.0f), angle_(0.0f),
@@ -23,10 +23,10 @@ Nau::Nau(SDL_Renderer* renderer)
brightness_(Defaults::Brightness::NAU), brightness_(Defaults::Brightness::NAU),
invulnerable_timer_(0.0f) { invulnerable_timer_(0.0f) {
// [NUEVO] Carregar forma compartida des de fitxer // [NUEVO] Carregar forma compartida des de fitxer
forma_ = Graphics::ShapeLoader::load("ship.shp"); forma_ = Graphics::ShapeLoader::load(shape_file);
if (!forma_ || !forma_->es_valida()) { 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;
} }
} }
@@ -64,7 +64,7 @@ void Nau::inicialitzar(const Punt* spawn_point, bool activar_invulnerabilitat) {
esta_tocada_ = false; 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) // Processar input continu (com teclapuls() del Pascal original)
// Basat en joc_asteroides.cpp línies 66-85 // Basat en joc_asteroides.cpp línies 66-85
// Només processa input si la nau està viva // Només processa input si la nau està viva
@@ -74,17 +74,28 @@ void Nau::processar_input(float delta_time) {
// Obtenir estat actual del teclat (no events, sinó estat continu) // Obtenir estat actual del teclat (no events, sinó estat continu)
const bool* keyboard_state = SDL_GetKeyboardState(nullptr); const bool* keyboard_state = SDL_GetKeyboardState(nullptr);
// Seleccionar controles según player_id
SDL_Scancode key_right = (player_id == 0)
? Defaults::Controls::P1::ROTATE_RIGHT
: Defaults::Controls::P2::ROTATE_RIGHT;
SDL_Scancode key_left = (player_id == 0)
? Defaults::Controls::P1::ROTATE_LEFT
: Defaults::Controls::P2::ROTATE_LEFT;
SDL_Scancode key_thrust = (player_id == 0)
? Defaults::Controls::P1::THRUST
: Defaults::Controls::P2::THRUST;
// Rotació // Rotació
if (keyboard_state[SDL_SCANCODE_RIGHT]) { if (keyboard_state[key_right]) {
angle_ += Defaults::Physics::ROTATION_SPEED * delta_time; angle_ += Defaults::Physics::ROTATION_SPEED * delta_time;
} }
if (keyboard_state[SDL_SCANCODE_LEFT]) { if (keyboard_state[key_left]) {
angle_ -= Defaults::Physics::ROTATION_SPEED * delta_time; angle_ -= Defaults::Physics::ROTATION_SPEED * delta_time;
} }
// Acceleració // Acceleració
if (keyboard_state[SDL_SCANCODE_UP]) { if (keyboard_state[key_thrust]) {
if (velocitat_ < Defaults::Physics::MAX_VELOCITY) { if (velocitat_ < Defaults::Physics::MAX_VELOCITY) {
velocitat_ += Defaults::Physics::ACCELERATION * delta_time; velocitat_ += Defaults::Physics::ACCELERATION * delta_time;
if (velocitat_ > Defaults::Physics::MAX_VELOCITY) { if (velocitat_ > Defaults::Physics::MAX_VELOCITY) {

View File

@@ -15,10 +15,10 @@ class Nau {
public: public:
Nau() Nau()
: renderer_(nullptr) {} : renderer_(nullptr) {}
Nau(SDL_Renderer* renderer); Nau(SDL_Renderer* renderer, const char* shape_file = "ship.shp");
void inicialitzar(const Punt* spawn_point = nullptr, bool activar_invulnerabilitat = false); void inicialitzar(const Punt* spawn_point = nullptr, bool activar_invulnerabilitat = false);
void processar_input(float delta_time); void processar_input(float delta_time, uint8_t player_id);
void actualitzar(float delta_time); void actualitzar(float delta_time);
void dibuixar() const; void dibuixar() const;

View File

@@ -28,14 +28,15 @@ EscenaJoc::EscenaJoc(SDLManager& sdl, ContextEscenes& context)
context_(context), context_(context),
debris_manager_(sdl.obte_renderer()), debris_manager_(sdl.obte_renderer()),
gestor_puntuacio_(sdl.obte_renderer()), gestor_puntuacio_(sdl.obte_renderer()),
nau_(sdl.obte_renderer()),
itocado_(0),
puntuacio_total_(0),
text_(sdl.obte_renderer()) { text_(sdl.obte_renderer()) {
// Consumir opcions (preparació per MODE_DEMO futur) // Consumir opcions (preparació per MODE_DEMO futur)
auto opcio = context_.consumir_opcio(); auto opcio = context_.consumir_opcio();
(void)opcio; // Suprimir warning de variable no usada (void)opcio; // Suprimir warning de variable no usada
// Inicialitzar naus amb renderer (P1=ship.shp, P2=ship2.shp)
naus_[0] = Nau(sdl.obte_renderer(), "ship.shp"); // Jugador 1: nave estàndar
naus_[1] = Nau(sdl.obte_renderer(), "ship2.shp"); // Jugador 2: interceptor amb ales
// Inicialitzar bales amb renderer // Inicialitzar bales amb renderer
for (auto& bala : bales_) { for (auto& bala : bales_) {
bala = Bala(sdl.obte_renderer()); bala = Bala(sdl.obte_renderer());
@@ -132,37 +133,43 @@ void EscenaJoc::inicialitzar() {
stage_manager_ = std::make_unique<StageSystem::StageManager>(stage_config_.get()); stage_manager_ = std::make_unique<StageSystem::StageManager>(stage_config_.get());
stage_manager_->inicialitzar(); stage_manager_->inicialitzar();
// [NEW] Set ship position reference for safe spawn // [NEW] Set ship position reference for safe spawn (P1 for now, TODO: dual tracking)
stage_manager_->get_spawn_controller().set_ship_position(&nau_.get_centre()); stage_manager_->get_spawn_controller().set_ship_position(&naus_[0].get_centre());
// Inicialitzar estat de col·lisió // Inicialitzar timers de muerte per jugador
itocado_ = 0; itocado_per_jugador_[0] = 0.0f;
itocado_per_jugador_[1] = 0.0f;
// Initialize lives and game over state // Initialize lives and game over state (independent lives per player)
num_vides_ = Defaults::Game::STARTING_LIVES; vides_per_jugador_[0] = Defaults::Game::STARTING_LIVES;
vides_per_jugador_[1] = Defaults::Game::STARTING_LIVES;
game_over_ = false; game_over_ = false;
game_over_timer_ = 0.0f; game_over_timer_ = 0.0f;
// Initialize score // Initialize scores (separate per player)
puntuacio_total_ = 0; puntuacio_per_jugador_[0] = 0;
puntuacio_per_jugador_[1] = 0;
gestor_puntuacio_.reiniciar(); gestor_puntuacio_.reiniciar();
// Set spawn point to center X, 75% Y (same as INIT_HUD final position) // Set spawn point to center X, 75% Y (legacy, for INIT_HUD animation)
const SDL_FRect& zona = Defaults::Zones::PLAYAREA; const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
punt_spawn_.x = zona.x + zona.w * 0.5f; punt_spawn_.x = zona.x + zona.w * 0.5f;
punt_spawn_.y = zona.y + zona.h * Defaults::Game::INIT_HUD_SHIP_START_Y_RATIO; punt_spawn_.y = zona.y + zona.h * Defaults::Game::INIT_HUD_SHIP_START_Y_RATIO;
// Inicialitzar nau amb posició especial // Inicialitzar AMBAS naus amb posicions específiques
nau_.inicialitzar(&punt_spawn_); for (uint8_t i = 0; i < 2; i++) {
Punt spawn_pos = obtenir_punt_spawn(i);
naus_[i].inicialitzar(&spawn_pos, false); // No invulnerability at start
}
// [MODIFIED] Initialize enemies as inactive (stage system will spawn them) // [MODIFIED] Initialize enemies as inactive (stage system will spawn them)
for (auto& enemy : orni_) { for (auto& enemy : orni_) {
enemy = Enemic(sdl_.obte_renderer()); enemy = Enemic(sdl_.obte_renderer());
enemy.set_ship_position(&nau_.get_centre()); // Set ship reference for tracking enemy.set_ship_position(&naus_[0].get_centre()); // Set ship reference (P1 for now)
// DON'T call enemy.inicialitzar() here - stage system handles spawning // DON'T call enemy.inicialitzar() here - stage system handles spawning
} }
// Inicialitzar bales // Inicialitzar bales (now 6 instead of 3)
for (auto& bala : bales_) { for (auto& bala : bales_) {
bala.inicialitzar(); bala.inicialitzar();
} }
@@ -201,30 +208,43 @@ void EscenaJoc::actualitzar(float delta_time) {
return; return;
} }
// Check death sequence state // Check death sequence state for BOTH players
if (itocado_ > 0.0f) { bool algun_jugador_mort = false;
// Death sequence active: update timer for (uint8_t i = 0; i < 2; i++) {
itocado_ += delta_time; if (itocado_per_jugador_[i] > 0.0f && itocado_per_jugador_[i] < 999.0f) {
algun_jugador_mort = true;
// Death sequence active: update timer
itocado_per_jugador_[i] += delta_time;
// Check if death duration completed // Check if death duration completed (only trigger ONCE using sentinel value)
if (itocado_ >= Defaults::Game::DEATH_DURATION) { if (itocado_per_jugador_[i] >= Defaults::Game::DEATH_DURATION) {
// *** PHASE 3: RESPAWN OR GAME OVER *** // *** PHASE 3: RESPAWN OR GAME OVER ***
// Decrement lives // Decrement lives for this player (only once)
num_vides_--; vides_per_jugador_[i]--;
if (num_vides_ > 0) { if (vides_per_jugador_[i] > 0) {
// Respawn ship en posición de muerte con invulnerabilidad // Respawn ship en spawn position con invulnerabilidad
nau_.inicialitzar(&punt_mort_, true); Punt spawn_pos = obtenir_punt_spawn(i);
itocado_ = 0.0f; naus_[i].inicialitzar(&spawn_pos, true);
} else { itocado_per_jugador_[i] = 0.0f;
// Game over } else {
game_over_ = true; // Player is permanently dead (out of lives)
game_over_timer_ = Defaults::Game::GAME_OVER_DURATION; // Set sentinel value to prevent re-entering this block
itocado_ = 0.0f; itocado_per_jugador_[i] = 999.0f;
// Check if BOTH players are dead (game over)
if (vides_per_jugador_[0] <= 0 && vides_per_jugador_[1] <= 0) {
game_over_ = true;
game_over_timer_ = Defaults::Game::GAME_OVER_DURATION;
}
}
} }
} }
}
// If any player is dead, still update enemies/bullets/effects
if (algun_jugador_mort) {
// Enemies and bullets continue moving during death sequence // Enemies and bullets continue moving during death sequence
for (auto& enemy : orni_) { for (auto& enemy : orni_) {
enemy.actualitzar(delta_time); enemy.actualitzar(delta_time);
@@ -236,7 +256,8 @@ void EscenaJoc::actualitzar(float delta_time) {
debris_manager_.actualitzar(delta_time); debris_manager_.actualitzar(delta_time);
gestor_puntuacio_.actualitzar(delta_time); gestor_puntuacio_.actualitzar(delta_time);
return;
// Don't return - allow alive players to continue playing
} }
// *** STAGE SYSTEM STATE MACHINE *** // *** STAGE SYSTEM STATE MACHINE ***
@@ -263,10 +284,10 @@ void EscenaJoc::actualitzar(float delta_time) {
float ship_anim_progress = ship_progress / Defaults::Game::INIT_HUD_SHIP_RATIO; float ship_anim_progress = ship_progress / Defaults::Game::INIT_HUD_SHIP_RATIO;
ship_anim_progress = std::min(1.0f, ship_anim_progress); ship_anim_progress = std::min(1.0f, ship_anim_progress);
// Actualitzar posició de la nau segons animació // Actualitzar posició de la nau P1 segons animació (P2 apareix directamente)
if (ship_anim_progress < 1.0f) { if (ship_anim_progress < 1.0f) {
Punt pos_animada = calcular_posicio_nau_init_hud(ship_anim_progress); Punt pos_animada = calcular_posicio_nau_init_hud(ship_anim_progress);
nau_.set_centre(pos_animada); naus_[0].set_centre(pos_animada); // Solo P1 animación
} }
// Una vegada l'animació acaba, permetre control normal // Una vegada l'animació acaba, permetre control normal
@@ -279,16 +300,20 @@ void EscenaJoc::actualitzar(float delta_time) {
// [DEBUG] Log entrada a LEVEL_START // [DEBUG] Log entrada a LEVEL_START
static bool first_entry = true; static bool first_entry = true;
if (first_entry) { if (first_entry) {
std::cout << "[LEVEL_START] ENTERED with pos.y=" << nau_.get_centre().y << std::endl; std::cout << "[LEVEL_START] ENTERED with P1 pos.y=" << naus_[0].get_centre().y << std::endl;
first_entry = false; first_entry = false;
} }
// Update countdown timer // Update countdown timer
stage_manager_->actualitzar(delta_time); stage_manager_->actualitzar(delta_time);
// [NEW] Allow ship movement and shooting during intro // [NEW] Allow both ships movement and shooting during intro
nau_.processar_input(delta_time); for (uint8_t i = 0; i < 2; i++) {
nau_.actualitzar(delta_time); if (itocado_per_jugador_[i] == 0.0f) { // Only alive players
naus_[i].processar_input(delta_time, i);
naus_[i].actualitzar(delta_time);
}
}
// [NEW] Update bullets // [NEW] Update bullets
for (auto& bala : bales_) { for (auto& bala : bales_) {
@@ -301,12 +326,13 @@ void EscenaJoc::actualitzar(float delta_time) {
} }
case StageSystem::EstatStage::PLAYING: { case StageSystem::EstatStage::PLAYING: {
// [NEW] Update stage manager (spawns enemies, pass pause flag) // [NEW] Update stage manager (spawns enemies, pause if BOTH dead)
bool pausar_spawn = (itocado_ > 0.0f); // Pause during death animation bool pausar_spawn = (itocado_per_jugador_[0] > 0.0f && itocado_per_jugador_[1] > 0.0f);
stage_manager_->get_spawn_controller().actualitzar(delta_time, orni_, pausar_spawn); stage_manager_->get_spawn_controller().actualitzar(delta_time, orni_, pausar_spawn);
// [NEW] Check stage completion (only when not in death sequence) // [NEW] Check stage completion (only when at least one player alive)
if (itocado_ == 0.0f) { bool algun_jugador_viu = (itocado_per_jugador_[0] == 0.0f || itocado_per_jugador_[1] == 0.0f);
if (algun_jugador_viu) {
auto& spawn_ctrl = stage_manager_->get_spawn_controller(); auto& spawn_ctrl = stage_manager_->get_spawn_controller();
if (spawn_ctrl.tots_enemics_destruits(orni_)) { if (spawn_ctrl.tots_enemics_destruits(orni_)) {
stage_manager_->stage_completat(); stage_manager_->stage_completat();
@@ -315,9 +341,13 @@ void EscenaJoc::actualitzar(float delta_time) {
} }
} }
// [EXISTING] Normal gameplay // [EXISTING] Normal gameplay - update BOTH players
nau_.processar_input(delta_time); for (uint8_t i = 0; i < 2; i++) {
nau_.actualitzar(delta_time); if (itocado_per_jugador_[i] == 0.0f) { // Only alive players
naus_[i].processar_input(delta_time, i);
naus_[i].actualitzar(delta_time);
}
}
for (auto& enemy : orni_) { for (auto& enemy : orni_) {
enemy.actualitzar(delta_time); enemy.actualitzar(delta_time);
@@ -328,7 +358,7 @@ void EscenaJoc::actualitzar(float delta_time) {
} }
detectar_col·lisions_bales_enemics(); detectar_col·lisions_bales_enemics();
detectar_col·lisio_nau_enemics(); detectar_col·lisio_naus_enemics();
debris_manager_.actualitzar(delta_time); debris_manager_.actualitzar(delta_time);
gestor_puntuacio_.actualitzar(delta_time); gestor_puntuacio_.actualitzar(delta_time);
break; break;
@@ -338,9 +368,13 @@ void EscenaJoc::actualitzar(float delta_time) {
// Update countdown timer // Update countdown timer
stage_manager_->actualitzar(delta_time); stage_manager_->actualitzar(delta_time);
// [NEW] Allow ship movement and shooting during outro // [NEW] Allow both ships movement and shooting during outro
nau_.processar_input(delta_time); for (uint8_t i = 0; i < 2; i++) {
nau_.actualitzar(delta_time); if (itocado_per_jugador_[i] == 0.0f) { // Only alive players
naus_[i].processar_input(delta_time, i);
naus_[i].actualitzar(delta_time);
}
}
// [NEW] Update bullets (allow last shots to continue) // [NEW] Update bullets (allow last shots to continue)
for (auto& bala : bales_) { for (auto& bala : bales_) {
@@ -420,9 +454,10 @@ void EscenaJoc::dibuixar() {
dibuixar_marcador_animat(score_progress); dibuixar_marcador_animat(score_progress);
} }
// Dibuixar nau (usant el sistema normal, la posició ja està actualitzada) // Dibuixar nau P1 (usant el sistema normal, la posició ja està actualitzada)
if (ship_progress > 0.0f && !nau_.esta_tocada()) { // Durante INIT_HUD solo se anima P1
nau_.dibuixar(); if (ship_progress > 0.0f && !naus_[0].esta_tocada()) {
naus_[0].dibuixar();
} }
break; break;
@@ -430,9 +465,11 @@ void EscenaJoc::dibuixar() {
case StageSystem::EstatStage::LEVEL_START: case StageSystem::EstatStage::LEVEL_START:
dibuixar_marges(); dibuixar_marges();
// [NEW] Draw ship if alive // [NEW] Draw both ships if alive
if (itocado_ == 0.0f) { for (uint8_t i = 0; i < 2; i++) {
nau_.dibuixar(); if (itocado_per_jugador_[i] == 0.0f) {
naus_[i].dibuixar();
}
} }
// [NEW] Draw bullets // [NEW] Draw bullets
@@ -452,9 +489,11 @@ void EscenaJoc::dibuixar() {
case StageSystem::EstatStage::PLAYING: case StageSystem::EstatStage::PLAYING:
dibuixar_marges(); dibuixar_marges();
// [EXISTING] Normal rendering // [EXISTING] Normal rendering - both ships
if (itocado_ == 0.0f) { for (uint8_t i = 0; i < 2; i++) {
nau_.dibuixar(); if (itocado_per_jugador_[i] == 0.0f) {
naus_[i].dibuixar();
}
} }
for (const auto& enemy : orni_) { for (const auto& enemy : orni_) {
@@ -472,9 +511,11 @@ void EscenaJoc::dibuixar() {
case StageSystem::EstatStage::LEVEL_COMPLETED: case StageSystem::EstatStage::LEVEL_COMPLETED:
dibuixar_marges(); dibuixar_marges();
// [NEW] Draw ship if alive // [NEW] Draw both ships if alive
if (itocado_ == 0.0f) { for (uint8_t i = 0; i < 2; i++) {
nau_.dibuixar(); if (itocado_per_jugador_[i] == 0.0f) {
naus_[i].dibuixar();
}
} }
// [NEW] Draw bullets (allow last shots to be visible) // [NEW] Draw bullets (allow last shots to be visible)
@@ -499,97 +540,60 @@ void EscenaJoc::processar_input(const SDL_Event& event) {
return; return;
} }
// Ignore ship controls during death sequence
if (itocado_ > 0.0f) {
return;
}
// Processament d'input per events puntuals (no continus) // Processament d'input per events puntuals (no continus)
// L'input continu (fletxes) es processa en actualitzar() amb // L'input continu (fletxes/WASD) es processa en actualitzar() amb
// SDL_GetKeyboardState() // SDL_GetKeyboardState()
if (event.type == SDL_EVENT_KEY_DOWN) { if (event.type == SDL_EVENT_KEY_DOWN) {
switch (event.key.key) { // P1 shoot
case SDLK_SPACE: { if (event.key.key == Defaults::Controls::P1::SHOOT) {
// No disparar si la nau està morta disparar_bala(0);
if (!nau_.esta_viva()) { }
break; // P2 shoot
} else if (event.key.key == Defaults::Controls::P2::SHOOT) {
disparar_bala(1);
// Disparar bala des del front de la nau
// El ship.shp té el front a (0, -12) en coordenades locals
// 1. Calcular posició del front de la nau
constexpr float LOCAL_TIP_X = 0.0f;
constexpr float LOCAL_TIP_Y = -12.0f;
const Punt& ship_centre = nau_.get_centre();
float ship_angle = nau_.get_angle();
// Aplicar transformació: rotació + trasllació
float cos_a = std::cos(ship_angle);
float sin_a = std::sin(ship_angle);
float tip_x = LOCAL_TIP_X * cos_a - LOCAL_TIP_Y * sin_a + ship_centre.x;
float tip_y = LOCAL_TIP_X * sin_a + LOCAL_TIP_Y * cos_a + ship_centre.y;
Punt posicio_dispar = {tip_x, tip_y};
// 2. Buscar primera bala inactiva i disparar
for (auto& bala : bales_) {
if (!bala.esta_activa()) {
bala.disparar(posicio_dispar, ship_angle);
break; // Només una bala per polsació
}
}
break;
}
default:
break;
} }
} }
} }
void EscenaJoc::tocado() { void EscenaJoc::tocado(uint8_t player_id) {
// Death sequence: 3 phases // Death sequence: 3 phases
// Phase 1: First call (itocado_ == 0) - trigger explosion // Phase 1: First call (itocado_per_jugador_[player_id] == 0) - trigger explosion
// Phase 2: Animation (0 < itocado_ < 3.0s) - debris animation // Phase 2: Animation (0 < itocado_ < 3.0s) - debris animation
// Phase 3: Respawn or game over (itocado_ >= 3.0s) - handled in actualitzar() // Phase 3: Respawn or game over (itocado_ >= 3.0s) - handled in actualitzar()
if (itocado_ == 0.0f) { if (itocado_per_jugador_[player_id] == 0.0f) {
// *** PHASE 1: TRIGGER DEATH *** // *** PHASE 1: TRIGGER DEATH ***
// Guardar posición de muerte para respawn
punt_mort_ = nau_.get_centre();
// Mark ship as dead (stops rendering and input) // Mark ship as dead (stops rendering and input)
nau_.marcar_tocada(); naus_[player_id].marcar_tocada();
// Create ship explosion // Create ship explosion
const Punt& ship_pos = nau_.get_centre(); const Punt& ship_pos = naus_[player_id].get_centre();
float ship_angle = nau_.get_angle(); float ship_angle = naus_[player_id].get_angle();
Punt vel_nau = nau_.get_velocitat_vector(); Punt vel_nau = naus_[player_id].get_velocitat_vector();
// Reduir a 80% la velocitat heretada per la nau (més realista) // Reduir a 80% la velocitat heretada per la nau (més realista)
Punt vel_nau_80 = {vel_nau.x * 0.8f, vel_nau.y * 0.8f}; Punt vel_nau_80 = {vel_nau.x * 0.8f, vel_nau.y * 0.8f};
debris_manager_.explotar( debris_manager_.explotar(
nau_.get_forma(), // Ship shape (3 lines) naus_[player_id].get_forma(), // Ship shape (3 lines)
ship_pos, // Center position ship_pos, // Center position
ship_angle, // Ship orientation ship_angle, // Ship orientation
1.0f, // Normal scale 1.0f, // Normal scale
Defaults::Physics::Debris::VELOCITAT_BASE, // 80 px/s Defaults::Physics::Debris::VELOCITAT_BASE, // 80 px/s
nau_.get_brightness(), // Heredar brightness naus_[player_id].get_brightness(), // Heredar brightness
vel_nau_80, // Heredar 80% velocitat vel_nau_80, // Heredar 80% velocitat
0.0f, // Nave: trayectorias rectas (sin drotacio) 0.0f, // Nave: trayectorias rectas (sin drotacio)
0.0f // Sin herencia visual (rotación aleatoria) 0.0f // Sin herencia visual (rotación aleatoria)
); );
Audio::get()->playSound(Defaults::Sound::EXPLOSION, Audio::Group::GAME);
// Start death timer (non-zero to avoid re-triggering) // Start death timer (non-zero to avoid re-triggering)
itocado_ = 0.001f; itocado_per_jugador_[player_id] = 0.001f;
} }
// Phase 2 is automatic (debris updates in actualitzar()) // Phase 2 is automatic (debris updates in actualitzar())
// Phase 3 is handled in actualitzar() when itocado_ >= DEATH_DURATION // Phase 3 is handled in actualitzar() when itocado_per_jugador_ >= DEATH_DURATION
} }
void EscenaJoc::dibuixar_marges() const { void EscenaJoc::dibuixar_marges() const {
@@ -750,26 +754,30 @@ Punt EscenaJoc::calcular_posicio_nau_init_hud(float progress) const {
} }
std::string EscenaJoc::construir_marcador() const { std::string EscenaJoc::construir_marcador() const {
// Puntuació P1 (5 dígits) // Puntuació P1 (6 dígits)
std::string score_p1 = std::to_string(puntuacio_total_); std::string score_p1 = std::to_string(puntuacio_per_jugador_[0]);
score_p1 = std::string(6 - std::min(6, static_cast<int>(score_p1.length())), '0') + score_p1; score_p1 = std::string(6 - std::min(6, static_cast<int>(score_p1.length())), '0') + score_p1;
// Vides P1 (2 dígits) // Vides P1 (2 dígits)
std::string vides_p1 = (num_vides_ < 10) ? "0" + std::to_string(num_vides_) std::string vides_p1 = (vides_per_jugador_[0] < 10)
: std::to_string(num_vides_); ? "0" + std::to_string(vides_per_jugador_[0])
: std::to_string(vides_per_jugador_[0]);
// Nivell (2 dígits) // Nivell (2 dígits)
uint8_t stage_num = stage_manager_->get_stage_actual(); uint8_t stage_num = stage_manager_->get_stage_actual();
std::string stage_str = (stage_num < 10) ? "0" + std::to_string(stage_num) std::string stage_str = (stage_num < 10) ? "0" + std::to_string(stage_num)
: std::to_string(stage_num); : std::to_string(stage_num);
// Puntuació P2 (sempre 00000 per ara) // Puntuació P2 (6 dígits)
std::string score_p2 = "000000"; std::string score_p2 = std::to_string(puntuacio_per_jugador_[1]);
score_p2 = std::string(6 - std::min(6, static_cast<int>(score_p2.length())), '0') + score_p2;
// Vides P2 (sempre 03 per ara) // Vides P2 (2 dígits)
std::string vides_p2 = "03"; std::string vides_p2 = (vides_per_jugador_[1] < 10)
? "0" + std::to_string(vides_per_jugador_[1])
: std::to_string(vides_per_jugador_[1]);
// Format: "12345 03 LEVEL 01 00000 03" // Format: "123456 03 LEVEL 01 654321 02"
// Nota: dos espais entre seccions // Nota: dos espais entre seccions
return score_p1 + " " + vides_p1 + " LEVEL " + stage_str + " " + score_p2 + " " + vides_p2; return score_p1 + " " + vides_p1 + " LEVEL " + stage_str + " " + score_p2 + " " + vides_p2;
} }
@@ -823,8 +831,9 @@ void EscenaJoc::detectar_col·lisions_bales_enemics() {
break; break;
} }
// 2. Add to total score // 2. Add score to the player who shot it
puntuacio_total_ += punts; uint8_t owner_id = bala.get_owner_id();
puntuacio_per_jugador_[owner_id] += punts;
// 3. Create floating score number // 3. Create floating score number
gestor_puntuacio_.crear(punts, pos_enemic); gestor_puntuacio_.crear(punts, pos_enemic);
@@ -856,12 +865,7 @@ void EscenaJoc::detectar_col·lisions_bales_enemics() {
} }
} }
void EscenaJoc::detectar_col·lisio_nau_enemics() { void EscenaJoc::detectar_col·lisio_naus_enemics() {
// Skip collisions if ship is dead or invulnerable
if (!nau_.esta_viva() || nau_.es_invulnerable()) {
return;
}
// Generous collision detection (80% hitbox) // Generous collision detection (80% hitbox)
constexpr float RADI_NAU = Defaults::Entities::SHIP_RADIUS; constexpr float RADI_NAU = Defaults::Entities::SHIP_RADIUS;
constexpr float RADI_ENEMIC = Defaults::Entities::ENEMY_RADIUS; constexpr float RADI_ENEMIC = Defaults::Entities::ENEMY_RADIUS;
@@ -869,30 +873,38 @@ void EscenaJoc::detectar_col·lisio_nau_enemics() {
(RADI_NAU + RADI_ENEMIC) * Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER; (RADI_NAU + RADI_ENEMIC) * Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER;
constexpr float SUMA_RADIS_QUADRAT = SUMA_RADIS * SUMA_RADIS; constexpr float SUMA_RADIS_QUADRAT = SUMA_RADIS * SUMA_RADIS;
const Punt& pos_nau = nau_.get_centre(); // Check collision for BOTH players
for (uint8_t i = 0; i < 2; i++) {
// Skip collisions if player is dead or invulnerable
if (itocado_per_jugador_[i] > 0.0f) continue;
if (!naus_[i].esta_viva()) continue;
if (naus_[i].es_invulnerable()) continue;
// Check collision with all active enemies const Punt& pos_nau = naus_[i].get_centre();
for (const auto& enemic : orni_) {
if (!enemic.esta_actiu()) {
continue;
}
// [NEW] Skip collision if enemy is invulnerable // Check collision with all active enemies
if (enemic.es_invulnerable()) { for (const auto& enemic : orni_) {
continue; if (!enemic.esta_actiu()) {
} continue;
}
const Punt& pos_enemic = enemic.get_centre(); // Skip collision if enemy is invulnerable
if (enemic.es_invulnerable()) {
continue;
}
// Calculate squared distance (avoid sqrt) const Punt& pos_enemic = enemic.get_centre();
float dx = static_cast<float>(pos_nau.x - pos_enemic.x);
float dy = static_cast<float>(pos_nau.y - pos_enemic.y);
float distancia_quadrada = dx * dx + dy * dy;
// Check collision // Calculate squared distance (avoid sqrt)
if (distancia_quadrada <= SUMA_RADIS_QUADRAT) { float dx = static_cast<float>(pos_nau.x - pos_enemic.x);
tocado(); // Trigger death sequence float dy = static_cast<float>(pos_nau.y - pos_enemic.y);
return; // Only one collision per frame float distancia_quadrada = dx * dx + dy * dy;
// Check collision
if (distancia_quadrada <= SUMA_RADIS_QUADRAT) {
tocado(i); // Trigger death sequence for player i
break; // Only one collision per player per frame
}
} }
} }
} }
@@ -964,3 +976,46 @@ void EscenaJoc::dibuixar_missatge_stage(const std::string& missatge) {
Punt pos = {x, y}; Punt pos = {x, y};
text_.render(partial_message, pos, escala, spacing); text_.render(partial_message, pos, escala, spacing);
} }
// ========================================
// Helper methods for 2-player support
// ========================================
Punt EscenaJoc::obtenir_punt_spawn(uint8_t player_id) const {
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
float x_ratio = (player_id == 0)
? Defaults::Game::P1_SPAWN_X_RATIO
: Defaults::Game::P2_SPAWN_X_RATIO;
return {
zona.x + zona.w * x_ratio,
zona.y + zona.h * Defaults::Game::SPAWN_Y_RATIO
};
}
void EscenaJoc::disparar_bala(uint8_t player_id) {
// Verificar que el jugador está vivo
if (itocado_per_jugador_[player_id] > 0.0f) return;
if (!naus_[player_id].esta_viva()) return;
// Calcular posición en la punta de la nave
const Punt& ship_centre = naus_[player_id].get_centre();
float ship_angle = naus_[player_id].get_angle();
constexpr float LOCAL_TIP_X = 0.0f;
constexpr float LOCAL_TIP_Y = -12.0f;
float cos_a = std::cos(ship_angle);
float sin_a = std::sin(ship_angle);
float tip_x = LOCAL_TIP_X * cos_a - LOCAL_TIP_Y * sin_a + ship_centre.x;
float tip_y = LOCAL_TIP_X * sin_a + LOCAL_TIP_Y * cos_a + ship_centre.y;
Punt posicio_dispar = {tip_x, tip_y};
// Buscar primera bala inactiva en el pool del jugador
int start_idx = player_id * 3; // P1=[0,1,2], P2=[3,4,5]
for (int i = start_idx; i < start_idx + 3; i++) {
if (!bales_[i].esta_activa()) {
bales_[i].disparar(posicio_dispar, ship_angle, player_id);
break;
}
}
}

View File

@@ -45,19 +45,19 @@ class EscenaJoc {
Effects::GestorPuntuacioFlotant gestor_puntuacio_; Effects::GestorPuntuacioFlotant gestor_puntuacio_;
// Estat del joc // Estat del joc
Nau nau_; std::array<Nau, 2> naus_; // [0]=P1, [1]=P2
std::array<Enemic, Constants::MAX_ORNIS> orni_; 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_; 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 // Lives and game over system
int num_vides_; // Current lives count std::array<int, 2> vides_per_jugador_; // [0]=P1, [1]=P2
bool game_over_; // Game over state flag bool game_over_; // Game over state flag
float game_over_timer_; // Countdown timer for auto-return (seconds) float game_over_timer_; // Countdown timer for auto-return (seconds)
Punt punt_spawn_; // Configurable spawn point Punt punt_spawn_; // Configurable spawn point (legacy)
Punt punt_mort_; // Death position (for respawn) Punt punt_mort_; // Death position (for respawn, legacy)
int puntuacio_total_; // Current score std::array<int, 2> puntuacio_per_jugador_; // [0]=P1, [1]=P2
// Text vectorial // Text vectorial
Graphics::VectorText text_; Graphics::VectorText text_;
@@ -67,11 +67,13 @@ class EscenaJoc {
std::unique_ptr<StageSystem::StageManager> stage_manager_; std::unique_ptr<StageSystem::StageManager> stage_manager_;
// Funcions privades // Funcions privades
void tocado(); void tocado(uint8_t player_id);
void detectar_col·lisions_bales_enemics(); // Col·lisions bala-enemic 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 dibuixar_marges() const; // Dibuixar vores de la zona de joc void dibuixar_marges() const; // Dibuixar vores de la zona de joc
void dibuixar_marcador(); // Dibuixar marcador de puntuació 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] Stage system helpers // [NEW] Stage system helpers
void dibuixar_missatge_stage(const std::string& missatge); void dibuixar_missatge_stage(const std::string& missatge);