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

@@ -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<StageSystem::StageManager>(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<int>(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<int>(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<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;
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<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
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;
}
}
}