From 087b8d346d309e6c3a254132f6b0fc67f8128875 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 10 Dec 2025 17:18:34 +0100 Subject: [PATCH] afegit segon jugador --- data/shapes/ship.shp | 4 +- data/shapes/ship2.shp | 27 +++ source/core/defaults.hpp | 22 ++ source/game/entities/bala.cpp | 5 +- source/game/entities/bala.hpp | 4 +- source/game/entities/nau.cpp | 25 +- source/game/entities/nau.hpp | 4 +- source/game/escenes/escena_joc.cpp | 371 +++++++++++++++++------------ source/game/escenes/escena_joc.hpp | 24 +- 9 files changed, 304 insertions(+), 182 deletions(-) create mode 100644 data/shapes/ship2.shp diff --git a/data/shapes/ship.shp b/data/shapes/ship.shp index 48f8b9f..f4f683e 100644 --- a/data/shapes/ship.shp +++ b/data/shapes/ship.shp @@ -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) # © 2025 Port a C++20 amb SDL3 -name: ship2 +name: ship scale: 1.0 center: 0, 0 diff --git a/data/shapes/ship2.shp b/data/shapes/ship2.shp new file mode 100644 index 0000000..bc85e5a --- /dev/null +++ b/data/shapes/ship2.shp @@ -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 diff --git a/source/core/defaults.hpp b/source/core/defaults.hpp index ded7223..1a2a65d 100644 --- a/source/core/defaults.hpp +++ b/source/core/defaults.hpp @@ -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) 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 // 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" } // 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) diff --git a/source/game/entities/bala.cpp b/source/game/entities/bala.cpp index bcd955f..e240e2a 100644 --- a/source/game/entities/bala.cpp +++ b/source/game/entities/bala.cpp @@ -36,7 +36,7 @@ void Bala::inicialitzar() { 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 // 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; + // 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; diff --git a/source/game/entities/bala.hpp b/source/game/entities/bala.hpp index 2b3b283..61ae324 100644 --- a/source/game/entities/bala.hpp +++ b/source/game/entities/bala.hpp @@ -17,13 +17,14 @@ 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_; } void desactivar() { esta_ = false; } private: @@ -37,6 +38,7 @@ class Bala { float angle_; float velocitat_; bool esta_; + uint8_t owner_id_; // 0=P1, 1=P2 float brightness_; // Factor de brillantor (0.0-1.0) void mou(float delta_time); diff --git a/source/game/entities/nau.cpp b/source/game/entities/nau.cpp index d6877ad..d1494b2 100644 --- a/source/game/entities/nau.cpp +++ b/source/game/entities/nau.cpp @@ -14,7 +14,7 @@ #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), @@ -23,10 +23,10 @@ Nau::Nau(SDL_Renderer* renderer) brightness_(Defaults::Brightness::NAU), invulnerable_timer_(0.0f) { // [NUEVO] Carregar forma compartida des de fitxer - forma_ = Graphics::ShapeLoader::load("ship.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; } } @@ -64,7 +64,7 @@ void Nau::inicialitzar(const Punt* spawn_point, bool activar_invulnerabilitat) { 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 @@ -74,17 +74,28 @@ void Nau::processar_input(float delta_time) { // Obtenir estat actual del teclat (no events, sinó estat continu) 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ó - if (keyboard_state[SDL_SCANCODE_RIGHT]) { + if (keyboard_state[key_right]) { 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; } // Acceleració - if (keyboard_state[SDL_SCANCODE_UP]) { + if (keyboard_state[key_thrust]) { if (velocitat_ < Defaults::Physics::MAX_VELOCITY) { velocitat_ += Defaults::Physics::ACCELERATION * delta_time; if (velocitat_ > Defaults::Physics::MAX_VELOCITY) { diff --git a/source/game/entities/nau.hpp b/source/game/entities/nau.hpp index bf268f2..bf76097 100644 --- a/source/game/entities/nau.hpp +++ b/source/game/entities/nau.hpp @@ -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, 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 dibuixar() const; diff --git a/source/game/escenes/escena_joc.cpp b/source/game/escenes/escena_joc.cpp index 9deecc3..71ce792 100644 --- a/source/game/escenes/escena_joc.cpp +++ b/source/game/escenes/escena_joc.cpp @@ -28,14 +28,15 @@ EscenaJoc::EscenaJoc(SDLManager& sdl, ContextEscenes& context) context_(context), debris_manager_(sdl.obte_renderer()), gestor_puntuacio_(sdl.obte_renderer()), - nau_(sdl.obte_renderer()), - itocado_(0), - puntuacio_total_(0), text_(sdl.obte_renderer()) { // Consumir opcions (preparació per MODE_DEMO futur) auto opcio = context_.consumir_opcio(); (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 for (auto& bala : bales_) { bala = Bala(sdl.obte_renderer()); @@ -132,37 +133,43 @@ void EscenaJoc::inicialitzar() { stage_manager_ = std::make_unique(stage_config_.get()); stage_manager_->inicialitzar(); - // [NEW] Set ship position reference for safe spawn - stage_manager_->get_spawn_controller().set_ship_position(&nau_.get_centre()); + // [NEW] Set ship position reference for safe spawn (P1 for now, TODO: dual tracking) + stage_manager_->get_spawn_controller().set_ship_position(&naus_[0].get_centre()); - // Inicialitzar estat de col·lisió - itocado_ = 0; + // Inicialitzar timers de muerte per jugador + itocado_per_jugador_[0] = 0.0f; + itocado_per_jugador_[1] = 0.0f; - // Initialize lives and game over state - num_vides_ = Defaults::Game::STARTING_LIVES; + // Initialize lives and game over state (independent lives per player) + vides_per_jugador_[0] = Defaults::Game::STARTING_LIVES; + vides_per_jugador_[1] = Defaults::Game::STARTING_LIVES; game_over_ = false; game_over_timer_ = 0.0f; - // Initialize score - puntuacio_total_ = 0; + // Initialize scores (separate per player) + puntuacio_per_jugador_[0] = 0; + puntuacio_per_jugador_[1] = 0; 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; punt_spawn_.x = zona.x + zona.w * 0.5f; punt_spawn_.y = zona.y + zona.h * Defaults::Game::INIT_HUD_SHIP_START_Y_RATIO; - // Inicialitzar nau amb posició especial - nau_.inicialitzar(&punt_spawn_); + // Inicialitzar AMBAS naus amb posicions específiques + 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) for (auto& enemy : orni_) { 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 } - // Inicialitzar bales + // Inicialitzar bales (now 6 instead of 3) for (auto& bala : bales_) { bala.inicialitzar(); } @@ -201,30 +208,43 @@ void EscenaJoc::actualitzar(float delta_time) { return; } - // Check death sequence state - if (itocado_ > 0.0f) { - // Death sequence active: update timer - itocado_ += delta_time; + // Check death sequence state for BOTH players + bool algun_jugador_mort = false; + for (uint8_t i = 0; i < 2; i++) { + 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 - if (itocado_ >= Defaults::Game::DEATH_DURATION) { - // *** PHASE 3: RESPAWN OR GAME OVER *** + // Check if death duration completed (only trigger ONCE using sentinel value) + if (itocado_per_jugador_[i] >= Defaults::Game::DEATH_DURATION) { + // *** PHASE 3: RESPAWN OR GAME OVER *** - // Decrement lives - num_vides_--; + // Decrement lives for this player (only once) + vides_per_jugador_[i]--; - if (num_vides_ > 0) { - // Respawn ship en posición de muerte con invulnerabilidad - nau_.inicialitzar(&punt_mort_, true); - itocado_ = 0.0f; - } else { - // Game over - game_over_ = true; - game_over_timer_ = Defaults::Game::GAME_OVER_DURATION; - itocado_ = 0.0f; + if (vides_per_jugador_[i] > 0) { + // Respawn ship en spawn position con invulnerabilidad + Punt spawn_pos = obtenir_punt_spawn(i); + naus_[i].inicialitzar(&spawn_pos, true); + itocado_per_jugador_[i] = 0.0f; + } else { + // Player is permanently dead (out of lives) + // Set sentinel value to prevent re-entering this block + 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 for (auto& enemy : orni_) { enemy.actualitzar(delta_time); @@ -236,7 +256,8 @@ void EscenaJoc::actualitzar(float delta_time) { debris_manager_.actualitzar(delta_time); gestor_puntuacio_.actualitzar(delta_time); - return; + + // Don't return - allow alive players to continue playing } // *** 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; 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) { 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 @@ -279,16 +300,20 @@ void EscenaJoc::actualitzar(float delta_time) { // [DEBUG] Log entrada a LEVEL_START static bool first_entry = true; 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; } // Update countdown timer stage_manager_->actualitzar(delta_time); - // [NEW] Allow ship movement and shooting during intro - nau_.processar_input(delta_time); - nau_.actualitzar(delta_time); + // [NEW] Allow both ships movement and shooting during intro + for (uint8_t i = 0; i < 2; i++) { + 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 for (auto& bala : bales_) { @@ -301,12 +326,13 @@ void EscenaJoc::actualitzar(float delta_time) { } case StageSystem::EstatStage::PLAYING: { - // [NEW] Update stage manager (spawns enemies, pass pause flag) - bool pausar_spawn = (itocado_ > 0.0f); // Pause during death animation + // [NEW] Update stage manager (spawns enemies, pause if BOTH dead) + 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); - // [NEW] Check stage completion (only when not in death sequence) - if (itocado_ == 0.0f) { + // [NEW] Check stage completion (only when at least one player alive) + 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(); if (spawn_ctrl.tots_enemics_destruits(orni_)) { stage_manager_->stage_completat(); @@ -315,9 +341,13 @@ void EscenaJoc::actualitzar(float delta_time) { } } - // [EXISTING] Normal gameplay - nau_.processar_input(delta_time); - nau_.actualitzar(delta_time); + // [EXISTING] Normal gameplay - update BOTH players + for (uint8_t i = 0; i < 2; i++) { + 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_) { enemy.actualitzar(delta_time); @@ -328,7 +358,7 @@ void EscenaJoc::actualitzar(float delta_time) { } detectar_col·lisions_bales_enemics(); - detectar_col·lisio_nau_enemics(); + detectar_col·lisio_naus_enemics(); debris_manager_.actualitzar(delta_time); gestor_puntuacio_.actualitzar(delta_time); break; @@ -338,9 +368,13 @@ void EscenaJoc::actualitzar(float delta_time) { // Update countdown timer stage_manager_->actualitzar(delta_time); - // [NEW] Allow ship movement and shooting during outro - nau_.processar_input(delta_time); - nau_.actualitzar(delta_time); + // [NEW] Allow both ships movement and shooting during outro + for (uint8_t i = 0; i < 2; i++) { + 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) for (auto& bala : bales_) { @@ -420,9 +454,10 @@ void EscenaJoc::dibuixar() { dibuixar_marcador_animat(score_progress); } - // Dibuixar nau (usant el sistema normal, la posició ja està actualitzada) - if (ship_progress > 0.0f && !nau_.esta_tocada()) { - nau_.dibuixar(); + // Dibuixar nau P1 (usant el sistema normal, la posició ja està actualitzada) + // Durante INIT_HUD solo se anima P1 + if (ship_progress > 0.0f && !naus_[0].esta_tocada()) { + naus_[0].dibuixar(); } break; @@ -430,9 +465,11 @@ void EscenaJoc::dibuixar() { case StageSystem::EstatStage::LEVEL_START: dibuixar_marges(); - // [NEW] Draw ship if alive - if (itocado_ == 0.0f) { - nau_.dibuixar(); + // [NEW] Draw both ships if alive + for (uint8_t i = 0; i < 2; i++) { + if (itocado_per_jugador_[i] == 0.0f) { + naus_[i].dibuixar(); + } } // [NEW] Draw bullets @@ -452,9 +489,11 @@ void EscenaJoc::dibuixar() { case StageSystem::EstatStage::PLAYING: dibuixar_marges(); - // [EXISTING] Normal rendering - if (itocado_ == 0.0f) { - nau_.dibuixar(); + // [EXISTING] Normal rendering - both ships + for (uint8_t i = 0; i < 2; i++) { + if (itocado_per_jugador_[i] == 0.0f) { + naus_[i].dibuixar(); + } } for (const auto& enemy : orni_) { @@ -472,9 +511,11 @@ void EscenaJoc::dibuixar() { case StageSystem::EstatStage::LEVEL_COMPLETED: dibuixar_marges(); - // [NEW] Draw ship if alive - if (itocado_ == 0.0f) { - nau_.dibuixar(); + // [NEW] Draw both ships if alive + for (uint8_t i = 0; i < 2; i++) { + if (itocado_per_jugador_[i] == 0.0f) { + naus_[i].dibuixar(); + } } // [NEW] Draw bullets (allow last shots to be visible) @@ -499,97 +540,60 @@ void EscenaJoc::processar_input(const SDL_Event& event) { return; } - // Ignore ship controls during death sequence - if (itocado_ > 0.0f) { - return; - } - // Processament d'input per events puntuals (no continus) - // L'input continu (fletxes) es processa en actualitzar() amb + // L'input continu (fletxes/WASD) es processa en actualitzar() amb // SDL_GetKeyboardState() if (event.type == SDL_EVENT_KEY_DOWN) { - switch (event.key.key) { - case SDLK_SPACE: { - // No disparar si la nau està morta - if (!nau_.esta_viva()) { - break; - } - - // 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; + // P1 shoot + if (event.key.key == Defaults::Controls::P1::SHOOT) { + disparar_bala(0); + } + // P2 shoot + else if (event.key.key == Defaults::Controls::P2::SHOOT) { + disparar_bala(1); } } } -void EscenaJoc::tocado() { +void EscenaJoc::tocado(uint8_t player_id) { // 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 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 *** - // Guardar posición de muerte para respawn - punt_mort_ = nau_.get_centre(); - // Mark ship as dead (stops rendering and input) - nau_.marcar_tocada(); + naus_[player_id].marcar_tocada(); // Create ship explosion - const Punt& ship_pos = nau_.get_centre(); - float ship_angle = nau_.get_angle(); - Punt vel_nau = nau_.get_velocitat_vector(); + const Punt& ship_pos = naus_[player_id].get_centre(); + float ship_angle = naus_[player_id].get_angle(); + Punt vel_nau = naus_[player_id].get_velocitat_vector(); // 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}; debris_manager_.explotar( - nau_.get_forma(), // Ship shape (3 lines) + naus_[player_id].get_forma(), // Ship shape (3 lines) ship_pos, // Center position ship_angle, // Ship orientation 1.0f, // Normal scale Defaults::Physics::Debris::VELOCITAT_BASE, // 80 px/s - nau_.get_brightness(), // Heredar brightness + naus_[player_id].get_brightness(), // Heredar brightness vel_nau_80, // Heredar 80% velocitat 0.0f, // Nave: trayectorias rectas (sin drotacio) 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) - itocado_ = 0.001f; + itocado_per_jugador_[player_id] = 0.001f; } // 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 { @@ -750,26 +754,30 @@ Punt EscenaJoc::calcular_posicio_nau_init_hud(float progress) const { } std::string EscenaJoc::construir_marcador() const { - // Puntuació P1 (5 dígits) - std::string score_p1 = std::to_string(puntuacio_total_); + // Puntuació P1 (6 dígits) + std::string score_p1 = std::to_string(puntuacio_per_jugador_[0]); score_p1 = std::string(6 - std::min(6, static_cast(score_p1.length())), '0') + score_p1; // Vides P1 (2 dígits) - std::string vides_p1 = (num_vides_ < 10) ? "0" + std::to_string(num_vides_) - : std::to_string(num_vides_); + std::string vides_p1 = (vides_per_jugador_[0] < 10) + ? "0" + std::to_string(vides_per_jugador_[0]) + : std::to_string(vides_per_jugador_[0]); // Nivell (2 dígits) uint8_t stage_num = stage_manager_->get_stage_actual(); std::string stage_str = (stage_num < 10) ? "0" + std::to_string(stage_num) : std::to_string(stage_num); - // Puntuació P2 (sempre 00000 per ara) - std::string score_p2 = "000000"; + // Puntuació P2 (6 dígits) + std::string score_p2 = std::to_string(puntuacio_per_jugador_[1]); + score_p2 = std::string(6 - std::min(6, static_cast(score_p2.length())), '0') + score_p2; - // Vides P2 (sempre 03 per ara) - std::string vides_p2 = "03"; + // Vides P2 (2 dígits) + 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 return score_p1 + " " + vides_p1 + " LEVEL " + stage_str + " " + score_p2 + " " + vides_p2; } @@ -823,8 +831,9 @@ void EscenaJoc::detectar_col·lisions_bales_enemics() { break; } - // 2. Add to total score - puntuacio_total_ += punts; + // 2. Add score to the player who shot it + uint8_t owner_id = bala.get_owner_id(); + puntuacio_per_jugador_[owner_id] += punts; // 3. Create floating score number gestor_puntuacio_.crear(punts, pos_enemic); @@ -856,12 +865,7 @@ void EscenaJoc::detectar_col·lisions_bales_enemics() { } } -void EscenaJoc::detectar_col·lisio_nau_enemics() { - // Skip collisions if ship is dead or invulnerable - if (!nau_.esta_viva() || nau_.es_invulnerable()) { - return; - } - +void EscenaJoc::detectar_col·lisio_naus_enemics() { // Generous collision detection (80% hitbox) constexpr float RADI_NAU = Defaults::Entities::SHIP_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; 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 - for (const auto& enemic : orni_) { - if (!enemic.esta_actiu()) { - continue; - } + const Punt& pos_nau = naus_[i].get_centre(); - // [NEW] Skip collision if enemy is invulnerable - if (enemic.es_invulnerable()) { - continue; - } + // Check collision with all active enemies + for (const auto& enemic : orni_) { + 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) - float dx = static_cast(pos_nau.x - pos_enemic.x); - float dy = static_cast(pos_nau.y - pos_enemic.y); - float distancia_quadrada = dx * dx + dy * dy; + const Punt& pos_enemic = enemic.get_centre(); - // Check collision - if (distancia_quadrada <= SUMA_RADIS_QUADRAT) { - tocado(); // Trigger death sequence - return; // Only one collision per frame + // Calculate squared distance (avoid sqrt) + float dx = static_cast(pos_nau.x - pos_enemic.x); + float dy = static_cast(pos_nau.y - pos_enemic.y); + 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}; 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; + } + } +} diff --git a/source/game/escenes/escena_joc.hpp b/source/game/escenes/escena_joc.hpp index f933a86..cff003d 100644 --- a/source/game/escenes/escena_joc.hpp +++ b/source/game/escenes/escena_joc.hpp @@ -45,19 +45,19 @@ class EscenaJoc { Effects::GestorPuntuacioFlotant gestor_puntuacio_; // Estat del joc - Nau nau_; + std::array naus_; // [0]=P1, [1]=P2 std::array orni_; - std::array bales_; + std::array bales_; // 6 balas: P1=[0,1,2], P2=[3,4,5] Poligon chatarra_cosmica_; - float itocado_; // Death timer (seconds) + std::array 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 - Punt punt_mort_; // Death position (for respawn) - int puntuacio_total_; // Current score + std::array vides_per_jugador_; // [0]=P1, [1]=P2 + bool game_over_; // Game over state flag + float game_over_timer_; // Countdown timer for auto-return (seconds) + Punt punt_spawn_; // Configurable spawn point (legacy) + Punt punt_mort_; // Death position (for respawn, legacy) + std::array puntuacio_per_jugador_; // [0]=P1, [1]=P2 // Text vectorial Graphics::VectorText text_; @@ -67,11 +67,13 @@ class EscenaJoc { std::unique_ptr stage_manager_; // 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 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] Stage system helpers void dibuixar_missatge_stage(const std::string& missatge);