// game_scene.cpp - Implementació de la lógica del juego // © 1999 Visente i Sergi (versión Pascal) // © 2025 Port a C++20 con SDL3 #include "game_scene.hpp" #include #include #include #include #include #include #include "core/audio/audio.hpp" #include "core/entities/entity.hpp" #include "core/input/input.hpp" #include "core/input/mouse.hpp" #include "core/math/easing.hpp" #include "core/physics/collision.hpp" #include "core/rendering/line_renderer.hpp" #include "core/system/scene_context.hpp" #include "core/system/global_events.hpp" #include "game/stage_system/stage_loader.hpp" // Using declarations per simplificar el codi using SceneManager::SceneContext; using SceneType = SceneContext::SceneType; using Option = SceneContext::Option; GameScene::GameScene(SDLManager& sdl, SceneContext& context) : sdl_(sdl), context_(context), debris_manager_(sdl.getRenderer()), floating_score_manager_(sdl.getRenderer()), text_(sdl.getRenderer()), init_hud_rect_sound_played_(false) { // Recuperar configuración de match des del context match_config_ = context_.getMatchConfig(); // Debug output de la configuración std::cout << "[GameScene] Configuración de match - P1: " << (match_config_.jugador1_actiu ? "ACTIU" : "INACTIU") << ", P2: " << (match_config_.jugador2_actiu ? "ACTIU" : "INACTIU") << '\n'; // Consumir opciones (preparació per MODE_DEMO futur) auto option = context_.consumeOption(); (void)option; // Suprimir warning de variable no usada // Inicialitzar naves con renderer (P1=ship.shp, P2=ship2.shp) ships_[0] = Ship(sdl.getRenderer(), "ship.shp"); // Jugador 1: nave estàndar ships_[1] = Ship(sdl.getRenderer(), "ship2.shp"); // Jugador 2: interceptor con ales // Inicialitzar balas con renderer for (auto& bullet : bullets_) { bullet = Bullet(sdl.getRenderer()); } // Inicialitzar enemigos con renderer for (auto& enemy : enemies_) { enemy = Enemy(sdl.getRenderer()); } } void GameScene::run() { std::cout << "SceneType Juego: Inicialitzant...\n"; // Inicialitzar state del juego init(); SDL_Event event; Uint64 last_time = SDL_GetTicks(); while (SceneManager::actual == SceneType::GAME) { // Calcular delta_time real Uint64 current_time = SDL_GetTicks(); float delta_time = (current_time - last_time) / 1000.0F; last_time = current_time; // Limitar delta_time per evitar grandes salts delta_time = std::min(delta_time, 0.05F); // Actualitzar counter de FPS sdl_.updateFPS(delta_time); // Actualitzar visibilitat del cursor (auto-ocultar) Mouse::updateCursorVisibility(); // Actualitzar sistema de input ABANS del event loop Input::get()->update(); // Processar events SDL while (SDL_PollEvent(&event)) { // Manejo de finestra if (sdl_.handleWindowEvent(event)) { continue; } // Events globals (F1/F2/F3/ESC/QUIT) GlobalEvents::handle(event, sdl_, context_); } // Actualitzar física del juego con delta_time real update(delta_time); // Actualitzar sistema de audio Audio::update(); // Actualitzar colors oscil·lats sdl_.updateColors(delta_time); // Netejar pantalla (usa color oscil·lat) sdl_.clear(0, 0, 0); // Actualitzar context de renderizado (factor de scale global) sdl_.updateRenderingContext(); // Dibuixar juego draw(); // Presentar renderer (swap buffers) sdl_.present(); } std::cout << "SceneType Juego: Finalitzant...\n"; } void GameScene::init() { // Inicialitzar generador de números aleatoris // Basat en el codi Pascal original: line 376 std::srand(static_cast(std::time(nullptr))); // [NEW] Load stage configuration (only once) if (!stage_config_) { stage_config_ = StageSystem::StageLoader::load("data/stages/stages.yaml"); if (!stage_config_) { std::cerr << "[GameScene] Error: no s'ha pogut load stages.yaml" << '\n'; // Continue without stage system (will crash, but helps debugging) } } // [NEW] Initialize stage manager stage_manager_ = std::make_unique(stage_config_.get()); stage_manager_->init(); // [NEW] Set ship position reference for safe spawn (P1 for now, TODO: dual tracking) stage_manager_->getSpawnController().set_ship_position(&ships_[0].getCenter()); // Inicialitzar timers de muerte per player hit_timer_per_player_[0] = 0.0F; hit_timer_per_player_[1] = 0.0F; // Initialize lives and game over state (independent lives per player) lives_per_player_[0] = Defaults::Game::STARTING_LIVES; lives_per_player_[1] = Defaults::Game::STARTING_LIVES; game_over_state_ = GameOverState::NONE; continue_counter_ = 0; continue_tick_timer_ = 0.0F; continues_used_ = 0; game_over_timer_ = 0.0F; // Initialize scores (separate per player) score_per_player_[0] = 0; score_per_player_[1] = 0; floating_score_manager_.reset(); // DEPRECATED: spawn_position_ ya no s'usa, es calcula dinàmicament con obtenir_punt_spawn(player_id) // const SDL_FRect& zona = Defaults::Zones::PLAYAREA; // spawn_position_.x = zona.x + zona.w * 0.5f; // spawn_position_.y = zona.y + zona.h * Defaults::Game::INIT_HUD_SHIP_START_Y_RATIO; // Inicialitzar naves segons configuración (solo jugadors active) for (uint8_t i = 0; i < 2; i++) { bool jugador_actiu = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; if (jugador_actiu) { // Jugador active: init normalment Vec2 spawn_pos = obtenir_punt_spawn(i); ships_[i].init(&spawn_pos, false); // No invulnerability at start std::cout << "[GameScene] Jugador " << (i + 1) << " inicialitzat\n"; } else { // Jugador inactiu: marcar como a mort permanent ships_[i].markHit(); hit_timer_per_player_[i] = 999.0F; // Valor sentinella (permanent inactiu) lives_per_player_[i] = 0; // Sin vides std::cout << "[GameScene] Jugador " << (i + 1) << " inactiu\n"; } } // [MODIFIED] Initialize enemies as inactive (stage system will spawn them) for (auto& enemy : enemies_) { enemy = Enemy(sdl_.getRenderer()); enemy.set_ship_position(&ships_[0].getCenter()); // Set ship reference (P1 for now) // DON'T call enemy.init() here - stage system handles spawning } // Inicialitzar balas (now 6 instead of 3) for (auto& bullet : bullets_) { bullet.init(); } // [ELIMINAT] Iniciar música de juego (ara es gestiona en stage_manager) // La música s'inicia cuando es transiciona de INIT_HUD a LEVEL_START // Audio::get()->playMusic("game.ogg"); // Reset flag de sons de animación init_hud_rect_sound_played_ = false; } void GameScene::update(float delta_time) { // Processar disparos (state-based, no event-based) if (game_over_state_ == GameOverState::NONE) { auto* input = Input::get(); // Jugador 1 dispara (solo si está active) if (match_config_.jugador1_actiu) { if (input->checkActionPlayer1(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) { disparar_bala(0); } } // Jugador 2 dispara (solo si está active) if (match_config_.jugador2_actiu) { if (input->checkActionPlayer2(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) { disparar_bala(1); } } // [FIXED] Allow mid-game join: inactive or dead player presses START // Only during PLAYING state (not INIT_HUD, CONTINUE, GAME_OVER) if (stage_manager_->get_estat() == StageSystem::EstatStage::PLAYING) { // Check if at least one player is alive and playing (game in progress) bool algun_jugador_viu = false; if (match_config_.jugador1_actiu && hit_timer_per_player_[0] != 999.0F) { algun_jugador_viu = true; } if (match_config_.jugador2_actiu && hit_timer_per_player_[1] != 999.0F) { algun_jugador_viu = true; } // Only allow join if there's an active game if (algun_jugador_viu) { // P2 can join if not currently playing (never joined OR dead without lives) bool p2_no_juga = !match_config_.jugador2_actiu || // Never joined hit_timer_per_player_[1] == 999.0F; // Dead without lives if (p2_no_juga) { if (input->checkActionPlayer2(InputAction::START, Input::DO_NOT_ALLOW_REPEAT)) { unir_jugador(1); } } // P1 can join if not currently playing (never joined OR dead without lives) bool p1_no_juga = !match_config_.jugador1_actiu || // Never joined hit_timer_per_player_[0] == 999.0F; // Dead without lives if (p1_no_juga) { if (input->checkActionPlayer1(InputAction::START, Input::DO_NOT_ALLOW_REPEAT)) { unir_jugador(0); } } } } } // Handle CONTINUE screen if (game_over_state_ == GameOverState::CONTINUE) { actualitzar_continue(delta_time); processar_input_continue(); // Still update enemies, bullets, and effects during continue screen for (auto& enemy : enemies_) { enemy.update(delta_time); } for (auto& bullet : bullets_) { bullet.update(delta_time); } debris_manager_.update(delta_time); floating_score_manager_.update(delta_time); return; } // Handle final GAME OVER state if (game_over_state_ == GameOverState::GAME_OVER) { // Game over: only update timer, enemies, bullets, and debris game_over_timer_ -= delta_time; if (game_over_timer_ <= 0.0F) { // Aturar música de juego antes de tornar al título Audio::get()->stopMusic(); // Transición a pantalla de título context_.setNextScene(SceneType::TITLE); SceneManager::actual = SceneType::TITLE; return; } // Enemies and bullets continue moving during game over for (auto& enemy : enemies_) { enemy.update(delta_time); } for (auto& bullet : bullets_) { bullet.update(delta_time); } debris_manager_.update(delta_time); floating_score_manager_.update(delta_time); return; } // Check death sequence state for BOTH players bool algun_jugador_mort = false; for (uint8_t i = 0; i < 2; i++) { if (hit_timer_per_player_[i] > 0.0F && hit_timer_per_player_[i] < 999.0F) { algun_jugador_mort = true; // Death sequence active: update timer hit_timer_per_player_[i] += delta_time; // Check if death duration completed (only trigger ONCE using sentinel value) if (hit_timer_per_player_[i] >= Defaults::Game::DEATH_DURATION) { // *** PHASE 3: RESPAWN OR GAME OVER *** // Decrement lives for this player (only once) lives_per_player_[i]--; if (lives_per_player_[i] > 0) { // Respawn ship en spawn position con invulnerabilidad Vec2 spawn_pos = obtenir_punt_spawn(i); ships_[i].init(&spawn_pos, true); hit_timer_per_player_[i] = 0.0F; } else { // Player is permanently dead (out of lives) // Set sentinel value to prevent re-entering this block hit_timer_per_player_[i] = 999.0F; // Check if ALL ACTIVE players are dead (trigger continue screen) bool p1_dead = !match_config_.jugador1_actiu || lives_per_player_[0] <= 0; bool p2_dead = !match_config_.jugador2_actiu || lives_per_player_[1] <= 0; if (p1_dead && p2_dead) { game_over_state_ = GameOverState::CONTINUE; continue_counter_ = Defaults::Game::CONTINUE_COUNT_START; continue_tick_timer_ = Defaults::Game::CONTINUE_TICK_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 : enemies_) { enemy.update(delta_time); } for (auto& bullet : bullets_) { bullet.update(delta_time); } debris_manager_.update(delta_time); floating_score_manager_.update(delta_time); // Don't return - allow alive players to continue playing } // *** STAGE SYSTEM STATE MACHINE *** StageSystem::EstatStage state = stage_manager_->get_estat(); switch (state) { case StageSystem::EstatStage::INIT_HUD: { // Update stage manager timer (pot canviar l'state!) stage_manager_->update(delta_time); // [FIX] Si l'state ha canviat durante update(), salir immediatament // per evitar recalcular la posición de la ship con el nuevo timer if (stage_manager_->get_estat() != StageSystem::EstatStage::INIT_HUD) { break; } // Calcular global progress (0.0 al inicio → 1.0 al final) float global_progress = 1.0F - (stage_manager_->get_timer_transicio() / Defaults::Game::INIT_HUD_DURATION); global_progress = std::min(1.0F, global_progress); // [NEW] Calcular progress independiente para cada nave float ship1_progress = calcular_progress_rango( global_progress, Defaults::Game::INIT_HUD_SHIP1_RATIO_INIT, Defaults::Game::INIT_HUD_SHIP1_RATIO_END); float ship2_progress = calcular_progress_rango( global_progress, Defaults::Game::INIT_HUD_SHIP2_RATIO_INIT, Defaults::Game::INIT_HUD_SHIP2_RATIO_END); // [MODIFICAT] Animar AMBAS naves con sus progress respectivos if (match_config_.jugador1_actiu && ship1_progress < 1.0F) { Vec2 pos_p1 = calcular_posicio_nau_init_hud(ship1_progress, 0); ships_[0].setCenter(pos_p1); } if (match_config_.jugador2_actiu && ship2_progress < 1.0F) { Vec2 pos_p2 = calcular_posicio_nau_init_hud(ship2_progress, 1); ships_[1].setCenter(pos_p2); } // Una vez l'animación acaba, permetre control normal // pero mantenir la posición inicial especial hasta LEVEL_START break; } case StageSystem::EstatStage::LEVEL_START: { // [DEBUG] Log entrada a LEVEL_START static bool first_entry = true; if (first_entry) { std::cout << "[LEVEL_START] ENTERED with P1 pos.y=" << ships_[0].getCenter().y << '\n'; first_entry = false; } // Update countdown timer stage_manager_->update(delta_time); // [NEW] Allow both ships movement and shooting during intro for (uint8_t i = 0; i < 2; i++) { bool jugador_actiu = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; if (jugador_actiu && hit_timer_per_player_[i] == 0.0F) { // Only active, alive players ships_[i].processInput(delta_time, i); ships_[i].update(delta_time); } } // [NEW] Update bullets for (auto& bullet : bullets_) { bullet.update(delta_time); } // [NEW] Update debris debris_manager_.update(delta_time); break; } case StageSystem::EstatStage::PLAYING: { // [NEW] Update stage manager (spawns enemies, pause if BOTH dead) bool pause_spawn = (hit_timer_per_player_[0] > 0.0F && hit_timer_per_player_[1] > 0.0F); stage_manager_->getSpawnController().update(delta_time, enemies_, pause_spawn); // [NEW] Check stage completion (only when at least one player alive) bool algun_jugador_viu = (hit_timer_per_player_[0] == 0.0F || hit_timer_per_player_[1] == 0.0F); if (algun_jugador_viu) { auto& spawn_ctrl = stage_manager_->getSpawnController(); if (spawn_ctrl.tots_enemics_destruits(enemies_)) { stage_manager_->stage_completat(); Audio::get()->playSound(Defaults::Sound::GOOD_JOB_COMMANDER, Audio::Group::GAME); break; } } // [EXISTING] Normal gameplay - update active players for (uint8_t i = 0; i < 2; i++) { bool jugador_actiu = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; if (jugador_actiu && hit_timer_per_player_[i] == 0.0F) { // Only active, alive players ships_[i].processInput(delta_time, i); ships_[i].update(delta_time); } } for (auto& enemy : enemies_) { enemy.update(delta_time); } for (auto& bullet : bullets_) { bullet.update(delta_time); } detectar_col·lisions_bales_enemics(); detectar_col·lisio_naus_enemics(); detectar_col·lisions_bales_jugadors(); debris_manager_.update(delta_time); floating_score_manager_.update(delta_time); break; } case StageSystem::EstatStage::LEVEL_COMPLETED: // Update countdown timer stage_manager_->update(delta_time); // [NEW] Allow both ships movement and shooting during outro for (uint8_t i = 0; i < 2; i++) { bool jugador_actiu = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; if (jugador_actiu && hit_timer_per_player_[i] == 0.0F) { // Only active, alive players ships_[i].processInput(delta_time, i); ships_[i].update(delta_time); } } // [NEW] Update bullets (allow last shots to continue) for (auto& bullet : bullets_) { bullet.update(delta_time); } // [NEW] Update debris (from last destroyed enemies) debris_manager_.update(delta_time); floating_score_manager_.update(delta_time); break; } } void GameScene::draw() { // Handle CONTINUE screen if (game_over_state_ == GameOverState::CONTINUE) { // Draw game background elements first dibuixar_marges(); for (const auto& enemy : enemies_) { enemy.draw(); } for (const auto& bullet : bullets_) { bullet.draw(); } debris_manager_.draw(); floating_score_manager_.draw(); dibuixar_marcador(); // Draw CONTINUE screen overlay dibuixar_continue(); return; } // Handle final GAME OVER state if (game_over_state_ == GameOverState::GAME_OVER) { // Game over: draw enemies, bullets, debris, and "GAME OVER" text dibuixar_marges(); for (const auto& enemy : enemies_) { enemy.draw(); } for (const auto& bullet : bullets_) { bullet.draw(); } debris_manager_.draw(); floating_score_manager_.draw(); // Draw centered "GAME OVER" text const std::string game_over_text = "GAME OVER"; constexpr float scale = Defaults::Game::GameOverScreen::TEXT_SCALE; constexpr float spacing = Defaults::Game::GameOverScreen::TEXT_SPACING; // Calcular centro de l'àrea de juego usant constants const SDL_FRect& play_area = Defaults::Zones::PLAYAREA; float centre_x = play_area.x + (play_area.w / 2.0F); float centre_y = play_area.y + (play_area.h / 2.0F); text_.renderCentered(game_over_text, {.x = centre_x, .y = centre_y}, scale, spacing); dibuixar_marcador(); return; } // [NEW] Stage state rendering StageSystem::EstatStage state = stage_manager_->get_estat(); switch (state) { case StageSystem::EstatStage::INIT_HUD: { // Calcular progrés de cada animación independent float timer = stage_manager_->get_timer_transicio(); float total_time = Defaults::Game::INIT_HUD_DURATION; float global_progress = 1.0F - (timer / total_time); // [NEW] Calcular progress independiente para cada elemento float rect_progress = calcular_progress_rango( global_progress, Defaults::Game::INIT_HUD_RECT_RATIO_INIT, Defaults::Game::INIT_HUD_RECT_RATIO_END); float score_progress = calcular_progress_rango( global_progress, Defaults::Game::INIT_HUD_SCORE_RATIO_INIT, Defaults::Game::INIT_HUD_SCORE_RATIO_END); float ship1_progress = calcular_progress_rango( global_progress, Defaults::Game::INIT_HUD_SHIP1_RATIO_INIT, Defaults::Game::INIT_HUD_SHIP1_RATIO_END); float ship2_progress = calcular_progress_rango( global_progress, Defaults::Game::INIT_HUD_SHIP2_RATIO_INIT, Defaults::Game::INIT_HUD_SHIP2_RATIO_END); // Dibuixar elements animats if (rect_progress > 0.0F) { // [NOU] Reproduir so cuando comença l'animación del rectangle if (!init_hud_rect_sound_played_) { Audio::get()->playSound(Defaults::Sound::INIT_HUD, Audio::Group::GAME); init_hud_rect_sound_played_ = true; } dibuixar_marges_animat(rect_progress); } if (score_progress > 0.0F) { dibuixar_marcador_animat(score_progress); } // [MODIFICAT] Dibuixar naves con progress independent if (ship1_progress > 0.0F && match_config_.jugador1_actiu && !ships_[0].isHit()) { ships_[0].draw(); } if (ship2_progress > 0.0F && match_config_.jugador2_actiu && !ships_[1].isHit()) { ships_[1].draw(); } break; } case StageSystem::EstatStage::LEVEL_START: dibuixar_marges(); // [NEW] Draw both ships if active and alive for (uint8_t i = 0; i < 2; i++) { bool jugador_actiu = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; if (jugador_actiu && hit_timer_per_player_[i] == 0.0F) { ships_[i].draw(); } } // [NEW] Draw bullets for (const auto& bullet : bullets_) { bullet.draw(); } // [NEW] Draw debris debris_manager_.draw(); floating_score_manager_.draw(); // [EXISTING] Draw intro message and score dibuixar_missatge_stage(stage_manager_->get_missatge_level_start()); dibuixar_marcador(); break; case StageSystem::EstatStage::PLAYING: dibuixar_marges(); // [EXISTING] Normal rendering - active ships for (uint8_t i = 0; i < 2; i++) { bool jugador_actiu = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; if (jugador_actiu && hit_timer_per_player_[i] == 0.0F) { ships_[i].draw(); } } for (const auto& enemy : enemies_) { enemy.draw(); } for (const auto& bullet : bullets_) { bullet.draw(); } debris_manager_.draw(); floating_score_manager_.draw(); dibuixar_marcador(); break; case StageSystem::EstatStage::LEVEL_COMPLETED: dibuixar_marges(); // [NEW] Draw both ships if active and alive for (uint8_t i = 0; i < 2; i++) { bool jugador_actiu = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; if (jugador_actiu && hit_timer_per_player_[i] == 0.0F) { ships_[i].draw(); } } // [NEW] Draw bullets (allow last shots to be visible) for (const auto& bullet : bullets_) { bullet.draw(); } // [NEW] Draw debris (from last destroyed enemies) debris_manager_.draw(); floating_score_manager_.draw(); // [EXISTING] Draw completion message and score dibuixar_missatge_stage(StageSystem::Constants::MISSATGE_LEVEL_COMPLETED); dibuixar_marcador(); break; } } void GameScene::tocado(uint8_t player_id) { // Death sequence: 3 phases // Phase 1: First call (hit_timer_per_player_[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 update() if (hit_timer_per_player_[player_id] == 0.0F) { // *** PHASE 1: TRIGGER DEATH *** // Mark ship as dead (stops rendering and input) ships_[player_id].markHit(); // Create ship explosion const Vec2& ship_pos = ships_[player_id].getCenter(); float ship_angle = ships_[player_id].getAngle(); Vec2 vel_nau = ships_[player_id].getVelocityVector(); // Reduir a 80% la velocity heretada per la ship (més realista) Vec2 vel_nau_80 = {.x = vel_nau.x * 0.8F, .y = vel_nau.y * 0.8F}; debris_manager_.explode( ships_[player_id].getShape(), // Ship shape (3 lines) ship_pos, // Center position ship_angle, // Ship orientation 1.0F, // Normal scale Defaults::Physics::Debris::VELOCITAT_BASE, // 80 px/s ships_[player_id].getBrightness(), // Heredar brightness vel_nau_80, // Heredar 80% velocity 0.0F, // Nave: trayectorias rectas (sin drotacio) 0.0F, // Sin herencia visual (rotación aleatoria) Defaults::Sound::EXPLOSION2 // Sonido alternativo para la explosión ); // Start death timer (non-zero to avoid re-triggering) hit_timer_per_player_[player_id] = 0.001F; } // Phase 2 is automatic (debris updates in update()) // Phase 3 is handled in update() when hit_timer_per_player_ >= DEATH_DURATION } void GameScene::dibuixar_marges() const { // Dibuixar rectangle de la zona de juego const SDL_FRect& zona = Defaults::Zones::PLAYAREA; // Coordenades dels cantons int x1 = static_cast(zona.x); int y1 = static_cast(zona.y); int x2 = static_cast(zona.x + zona.w); int y2 = static_cast(zona.y + zona.h); // 4 línies per formar el rectangle Rendering::linea(sdl_.getRenderer(), x1, y1, x2, y1); // Top Rendering::linea(sdl_.getRenderer(), x1, y2, x2, y2); // Bottom Rendering::linea(sdl_.getRenderer(), x1, y1, x1, y2); // Left Rendering::linea(sdl_.getRenderer(), x2, y1, x2, y2); // Right } void GameScene::dibuixar_marcador() { // Construir text del marcador std::string text = buildScoreboard(); // Parámetros de renderització const float scale = 0.85F; const float spacing = 0.0F; // Calcular centro de la zona del marcador const SDL_FRect& scoreboard = Defaults::Zones::SCOREBOARD; float centre_x = scoreboard.w / 2.0F; float centre_y = scoreboard.y + (scoreboard.h / 2.0F); // Renderizar centrat text_.renderCentered(text, {.x = centre_x, .y = centre_y}, scale, spacing); } void GameScene::dibuixar_marges_animat(float progress) const { // Animación seqüencial del rectangle con efecte de "pinzell" // Dos pinzells comencen al centro superior i baixen por los laterals const SDL_FRect& zona = Defaults::Zones::PLAYAREA; // Aplicar easing al progrés global float eased_progress = Easing::ease_out_quad(progress); // Coordenades del rectangle complet int x1 = static_cast(zona.x); int y1 = static_cast(zona.y); int x2 = static_cast(zona.x + zona.w); int y2 = static_cast(zona.y + zona.h); int cx = (x1 + x2) / 2; // Dividir en 3 fases de 33% cada una constexpr float PHASE_1_END = 0.33F; constexpr float PHASE_2_END = 0.66F; // --- FASE 1: Línies horitzontals superiors (0-33%) --- if (eased_progress > 0.0F) { float phase1_progress = std::min(eased_progress / PHASE_1_END, 1.0F); // Línia izquierda: creix des del centro hacia l'izquierda int x1_phase1 = static_cast(cx - ((cx - x1) * phase1_progress)); Rendering::linea(sdl_.getRenderer(), cx, y1, x1_phase1, y1); // Línia derecha: creix des del centro hacia la derecha int x2_phase1 = static_cast(cx + ((x2 - cx) * phase1_progress)); Rendering::linea(sdl_.getRenderer(), cx, y1, x2_phase1, y1); } // --- FASE 2: Línies verticals laterals (33-66%) --- if (eased_progress > PHASE_1_END) { float phase2_progress = std::min((eased_progress - PHASE_1_END) / (PHASE_2_END - PHASE_1_END), 1.0F); // Línia izquierda: creix desde dalt hacia baix int y2_phase2 = static_cast(y1 + ((y2 - y1) * phase2_progress)); Rendering::linea(sdl_.getRenderer(), x1, y1, x1, y2_phase2); // Línia derecha: creix desde dalt hacia baix Rendering::linea(sdl_.getRenderer(), x2, y1, x2, y2_phase2); } // --- FASE 3: Línies horitzontals inferiors (66-100%) --- if (eased_progress > PHASE_2_END) { float phase3_progress = (eased_progress - PHASE_2_END) / (1.0F - PHASE_2_END); // Línia izquierda: creix desde l'izquierda hacia el centro int x_left_phase3 = static_cast(x1 + ((cx - x1) * phase3_progress)); Rendering::linea(sdl_.getRenderer(), x1, y2, x_left_phase3, y2); // Línia derecha: creix desde la derecha hacia el centro int x_right_phase3 = static_cast(x2 - ((x2 - cx) * phase3_progress)); Rendering::linea(sdl_.getRenderer(), x2, y2, x_right_phase3, y2); } } void GameScene::dibuixar_marcador_animat(float progress) { // Animación del marcador pujant desde baix con easing // Calcular progrés con easing float eased_progress = Easing::ease_out_quad(progress); // Construir text std::string text = buildScoreboard(); // Parámetros const float scale = 0.85F; const float spacing = 0.0F; // Calcular centro de la zona del marcador const SDL_FRect& scoreboard = Defaults::Zones::SCOREBOARD; float centre_x = scoreboard.w / 2.0F; float centre_y_final = scoreboard.y + (scoreboard.h / 2.0F); // Posición Y inicial (offscreen, sota de la pantalla) auto centre_y_inicial = static_cast(Defaults::Game::HEIGHT); // Interpolació con easing float centre_y_animada = centre_y_inicial + ((centre_y_final - centre_y_inicial) * eased_progress); // Renderizar centrat en posición animada text_.renderCentered(text, {.x = centre_x, .y = centre_y_animada}, scale, spacing); } Vec2 GameScene::calcular_posicio_nau_init_hud(float progress, uint8_t player_id) const { // Animación de la ship pujant desde baix con easing // [MODIFICAT] Ambas naves usan ease_out_quad (desfase temporal via INIT/END) // Aplicar easing (uniforme para ambas naves) float eased_progress = Easing::ease_out_quad(progress); const SDL_FRect& zona = Defaults::Zones::PLAYAREA; // Calcular posición final segons player (reutilitza obtenir_punt_spawn) Vec2 spawn_final = obtenir_punt_spawn(player_id); float x_final = spawn_final.x; float y_final = spawn_final.y; // Y inicial: offscreen, 50px sota la zona de juego float y_inicial = zona.y + zona.h + 50.0F; // X no canvia (destí segons player_id) // Y interpola con easing float y_animada = y_inicial + ((y_final - y_inicial) * eased_progress); return {.x = x_final, .y = y_animada}; } float GameScene::calcular_progress_rango(float global_progress, float ratio_init, float ratio_end) const { // Convierte global_progress (0.0→1.0) a element_progress usando ventana [INIT, END] // // Casos: // - global_progress < INIT → 0.0 (no ha empezado) // - global_progress > END → 1.0 (completado) // - INIT ≤ global_progress ≤ END → interpola linealmente 0.0→1.0 // Validación de parámetros (evita división por cero) if (ratio_init >= ratio_end) { return (global_progress >= ratio_end) ? 1.0F : 0.0F; } if (global_progress < ratio_init) { return 0.0F; } if (global_progress > ratio_end) { return 1.0F; } // Normalizar rango [INIT, END] a [0.0, 1.0] return (global_progress - ratio_init) / (ratio_end - ratio_init); } std::string GameScene::buildScoreboard() const { // Puntuación P1 (6 dígits) - mostrar zeros si inactiu std::string score_p1; std::string vides_p1; if (match_config_.jugador1_actiu) { score_p1 = std::to_string(score_per_player_[0]); score_p1 = std::string(6 - std::min(6, static_cast(score_p1.length())), '0') + score_p1; vides_p1 = (lives_per_player_[0] < 10) ? "0" + std::to_string(lives_per_player_[0]) : std::to_string(lives_per_player_[0]); } else { score_p1 = "000000"; vides_p1 = "00"; } // Nivel (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ón P2 (6 dígits) - mostrar zeros si inactiu std::string score_p2; std::string vides_p2; if (match_config_.jugador2_actiu) { score_p2 = std::to_string(score_per_player_[1]); score_p2 = std::string(6 - std::min(6, static_cast(score_p2.length())), '0') + score_p2; vides_p2 = (lives_per_player_[1] < 10) ? "0" + std::to_string(lives_per_player_[1]) : std::to_string(lives_per_player_[1]); } else { score_p2 = "000000"; vides_p2 = "00"; } // Format: "123456 03 LEVEL 01 654321 02" // Nota: dos espais entre seccions, mantenir ambdós slots siempre visibles return score_p1 + " " + vides_p1 + " LEVEL " + stage_str + " " + score_p2 + " " + vides_p2; } void GameScene::detectar_col·lisions_bales_enemics() { // Amplificador per hitbox més generós (115%) constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER; // Velocidad de explosión reduïda per efecte suau constexpr float VELOCITAT_EXPLOSIO = 80.0F; // px/s (en lloc de 80.0f per defecte) // Iterar per todas las balas i enemigos for (auto& bullet : bullets_) { for (auto& enemy : enemies_) { // Comprovar colisión utilitzant la interfície genèrica if (Physics::check_collision(bullet, enemy, AMPLIFIER)) { // *** COL·LISIÓ DETECTADA *** const Vec2& pos_enemic = enemy.getCenter(); // 1. Calculate score for enemy type int points = 0; switch (enemy.getType()) { case EnemyType::PENTAGON: points = Defaults::Enemies::Scoring::PENTAGON_SCORE; break; case EnemyType::QUADRAT: points = Defaults::Enemies::Scoring::QUADRAT_SCORE; break; case EnemyType::MOLINILLO: points = Defaults::Enemies::Scoring::MOLINILLO_SCORE; break; } // 2. Add score to the player who shot it uint8_t owner_id = bullet.get_owner_id(); score_per_player_[owner_id] += points; // 3. Create floating score number floating_score_manager_.crear(points, pos_enemic); // 4. Destruir enemy (marca como inactiu) enemy.destruir(); // 2. Crear explosión de fragments Vec2 vel_enemic = enemy.getVelocityVector(); debris_manager_.explode( enemy.getShape(), // Forma vectorial del pentágono pos_enemic, // Posición central 0.0F, // Angle (enemy té rotación interna) 1.0F, // Escala normal VELOCITAT_EXPLOSIO, // 50 px/s (explosión suau) enemy.getBrightness(), // Heredar brightness vel_enemic, // Heredar velocity enemy.get_drotacio(), // Heredar velocity angular (trayectorias curvas) 0.0F // Sin herencia visual (rotación aleatoria) ); // 3. Desactivar bullet bullet.desactivar(); // 4. Eixir del bucle intern (bullet solo destrueix 1 enemy) break; } } } } void GameScene::detectar_col·lisio_naus_enemics() { // Amplificador per hitbox generós (80%) constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER; // Check collision for BOTH players for (uint8_t i = 0; i < 2; i++) { // Skip collisions if player is dead or invulnerable if (hit_timer_per_player_[i] > 0.0F) { continue; } if (!ships_[i].isAlive()) { continue; } if (ships_[i].isInvulnerable()) { continue; } // Check collision with all active enemies for (const auto& enemy : enemies_) { // Skip collision if enemy is invulnerable if (enemy.isInvulnerable()) { continue; } // Comprovar colisión utilitzant la interfície genèrica if (Physics::check_collision(ships_[i], enemy, AMPLIFIER)) { tocado(i); // Trigger death sequence for player i break; // Only one collision per player per frame } } } } void GameScene::detectar_col·lisions_bales_jugadors() { // Skip if friendly fire disabled if (!Defaults::Game::FRIENDLY_FIRE_ENABLED) { return; } // Amplificador per hitbox exacte (100%) constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_PLAYER_AMPLIFIER; // Check all active bullets for (auto& bullet : bullets_) { if (!bullet.esta_activa()) { continue; } // Skip bullets in grace period (prevents instant self-collision) if (bullet.get_grace_timer() > 0.0F) { continue; } uint8_t bullet_owner = bullet.get_owner_id(); // Check collision with BOTH players for (uint8_t player_id = 0; player_id < 2; player_id++) { // Skip if player is dead, invulnerable, or inactive if (hit_timer_per_player_[player_id] > 0.0F) { continue; } if (!ships_[player_id].isAlive()) { continue; } if (ships_[player_id].isInvulnerable()) { continue; } // Skip inactive players bool jugador_actiu = (player_id == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; if (!jugador_actiu) { continue; } // Comprovar colisión utilitzant la interfície genèrica if (Physics::check_collision(bullet, ships_[player_id], AMPLIFIER)) { // *** FRIENDLY FIRE HIT *** if (bullet_owner == player_id) { // CASE 1: Self-hit (own bullet) // Player loses 1 life, no gain tocado(player_id); } else { // CASE 2: Teammate hit // Victim loses 1 life tocado(player_id); // Attacker gains 1 life (no sin) lives_per_player_[bullet_owner]++; } // Play distinct sound Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME); // Deactivate bullet bullet.desactivar(); break; // Bullet only hits once per frame } } } } // [NEW] Stage system helper methods void GameScene::dibuixar_missatge_stage(const std::string& message) { constexpr float escala_base = 1.0F; constexpr float spacing = 2.0F; const SDL_FRect& play_area = Defaults::Zones::PLAYAREA; const float max_width = play_area.w * Defaults::Game::STAGE_MESSAGE_MAX_WIDTH_RATIO; // ========== TYPEWRITER EFFECT (PARAMETRIZED) ========== // Get state-specific timing configuration float total_time; float typing_ratio; if (stage_manager_->get_estat() == StageSystem::EstatStage::LEVEL_START) { total_time = Defaults::Game::LEVEL_START_DURATION; typing_ratio = Defaults::Game::LEVEL_START_TYPING_RATIO; } else { // LEVEL_COMPLETED total_time = Defaults::Game::LEVEL_COMPLETED_DURATION; typing_ratio = Defaults::Game::LEVEL_COMPLETED_TYPING_RATIO; } // Calculate progress from timer (0.0 at start → 1.0 at end) float remaining_time = stage_manager_->get_timer_transicio(); float progress = 1.0F - (remaining_time / total_time); // Determine how many characters to show size_t visible_chars; if (typing_ratio > 0.0F && progress < typing_ratio) { // Typewriter phase: show partial text float typing_progress = progress / typing_ratio; // Normalize to 0.0-1.0 visible_chars = static_cast(message.length() * typing_progress); if (visible_chars == 0 && progress > 0.0F) { visible_chars = 1; // Show at least 1 character after first frame } } else { // Display phase: show complete text // (Either after typing phase, or immediately if typing_ratio == 0.0) visible_chars = message.length(); } // Create partial message (substring for typewriter) std::string partial_message = message.substr(0, visible_chars); // =================================================== // Calculate text width at base scale (using FULL message for position calculation) float text_width_at_base = text_.get_text_width(message, escala_base, spacing); // Auto-scale if text exceeds max width float scale = (text_width_at_base <= max_width) ? escala_base : max_width / text_width_at_base; // Recalculate dimensions with final scale (using FULL message for centering) float full_text_width = text_.get_text_width(message, scale, spacing); float text_height = text_.get_text_height(scale); // Calculate position as if FULL text was there (for fixed position typewriter) float x = play_area.x + ((play_area.w - full_text_width) / 2.0F); float y = play_area.y + (play_area.h * Defaults::Game::STAGE_MESSAGE_Y_RATIO) - (text_height / 2.0F); // Render only the partial message (typewriter effect) Vec2 pos = {.x = x, .y = y}; text_.render(partial_message, pos, scale, spacing); } // ======================================== // Helper methods for 2-player support // ======================================== Vec2 GameScene::obtenir_punt_spawn(uint8_t player_id) const { const SDL_FRect& zona = Defaults::Zones::PLAYAREA; float x_ratio; if (match_config_.es_un_jugador()) { // Un sol player: spawn al centro (50%) x_ratio = 0.5F; } else { // Dos jugadors: spawn a posicions separades x_ratio = (player_id == 0) ? Defaults::Game::P1_SPAWN_X_RATIO // 33% : Defaults::Game::P2_SPAWN_X_RATIO; // 67% } return { .x = zona.x + (zona.w * x_ratio), .y = zona.y + (zona.h * Defaults::Game::SPAWN_Y_RATIO)}; } void GameScene::disparar_bala(uint8_t player_id) { // Verificar que el player está vivo if (hit_timer_per_player_[player_id] > 0.0F) { return; } if (!ships_[player_id].isAlive()) { return; } // Calcular posición en la punta de la nave const Vec2& ship_centre = ships_[player_id].getCenter(); float ship_angle = ships_[player_id].getAngle(); 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; Vec2 posicio_dispar = {.x = tip_x, .y = tip_y}; // Buscar primera bullet inactiva en el pool del player 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 (!bullets_[i].esta_activa()) { bullets_[i].disparar(posicio_dispar, ship_angle, player_id); break; } } } // ==================== CONTINUE & JOIN SYSTEM ==================== void GameScene::check_and_apply_continue_timeout() { if (continue_counter_ < 0) { game_over_state_ = GameOverState::GAME_OVER; game_over_timer_ = Defaults::Game::GAME_OVER_DURATION; } } void GameScene::actualitzar_continue(float delta_time) { continue_tick_timer_ -= delta_time; if (continue_tick_timer_ <= 0.0F) { continue_counter_--; continue_tick_timer_ = Defaults::Game::CONTINUE_TICK_DURATION; // Check if timeout reached (counter < 0) check_and_apply_continue_timeout(); // Play sound only if still in CONTINUE state (not transitioned to GAME_OVER) if (game_over_state_ == GameOverState::CONTINUE) { Audio::get()->playSound(Defaults::Sound::CONTINUE, Audio::Group::GAME); } } } void GameScene::processar_input_continue() { auto* input = Input::get(); // Check START for both players bool p1_start = input->checkActionPlayer1(InputAction::START, Input::DO_NOT_ALLOW_REPEAT); bool p2_start = input->checkActionPlayer2(InputAction::START, Input::DO_NOT_ALLOW_REPEAT); if (p1_start || p2_start) { // Check continue limit (skip if infinite continues) if (!Defaults::Game::INFINITE_CONTINUES && continues_used_ >= Defaults::Game::MAX_CONTINUES) { // Max continues reached → final game over game_over_state_ = GameOverState::GAME_OVER; game_over_timer_ = Defaults::Game::GAME_OVER_DURATION; return; } // Only increment if not infinite if (!Defaults::Game::INFINITE_CONTINUES) { continues_used_++; } // Determine which player(s) to revive uint8_t player_to_revive = p1_start ? 0 : 1; // Reset score and lives (KEEP level and enemies!) score_per_player_[player_to_revive] = 0; lives_per_player_[player_to_revive] = Defaults::Game::STARTING_LIVES; hit_timer_per_player_[player_to_revive] = 0.0F; // Activate player if not already if (player_to_revive == 0) { match_config_.jugador1_actiu = true; } else { match_config_.jugador2_actiu = true; } // Spawn with invulnerability Vec2 spawn_pos = obtenir_punt_spawn(player_to_revive); ships_[player_to_revive].init(&spawn_pos, true); // Check if other player wants to continue too if (p1_start && p2_start) { uint8_t other_player = 1; score_per_player_[other_player] = 0; lives_per_player_[other_player] = Defaults::Game::STARTING_LIVES; hit_timer_per_player_[other_player] = 0.0F; match_config_.jugador2_actiu = true; Vec2 spawn_pos2 = obtenir_punt_spawn(other_player); ships_[other_player].init(&spawn_pos2, true); } // Resume game game_over_state_ = GameOverState::NONE; continue_counter_ = 0; continue_tick_timer_ = 0.0F; // Play continue confirmation sound Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME); return; } // Check THRUST/FIRE to accelerate countdown (DO_NOT_ALLOW_REPEAT to avoid spam) bool thrust_p1 = input->checkActionPlayer1(InputAction::THRUST, Input::DO_NOT_ALLOW_REPEAT); bool thrust_p2 = input->checkActionPlayer2(InputAction::THRUST, Input::DO_NOT_ALLOW_REPEAT); bool fire_p1 = input->checkActionPlayer1(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT); bool fire_p2 = input->checkActionPlayer2(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT); if (thrust_p1 || thrust_p2 || fire_p1 || fire_p2) { continue_counter_--; // Check if timeout reached (counter < 0) check_and_apply_continue_timeout(); // Play sound only if still in CONTINUE state if (game_over_state_ == GameOverState::CONTINUE) { Audio::get()->playSound(Defaults::Sound::CONTINUE, Audio::Group::GAME); } // Reset timer to prevent double-decrement continue_tick_timer_ = Defaults::Game::CONTINUE_TICK_DURATION; } } void GameScene::dibuixar_continue() { const SDL_FRect& play_area = Defaults::Zones::PLAYAREA; constexpr float spacing = 4.0F; // "CONTINUE" text (using constants) const std::string continue_text = "CONTINUE"; float escala_continue = Defaults::Game::ContinueScreen::CONTINUE_TEXT_SCALE; float y_ratio_continue = Defaults::Game::ContinueScreen::CONTINUE_TEXT_Y_RATIO; float centre_x = play_area.x + (play_area.w / 2.0F); float centre_y_continue = play_area.y + (play_area.h * y_ratio_continue); text_.renderCentered(continue_text, {.x = centre_x, .y = centre_y_continue}, escala_continue, spacing); // Countdown number (using constants) const std::string counter_str = std::to_string(continue_counter_); float escala_counter = Defaults::Game::ContinueScreen::COUNTER_TEXT_SCALE; float y_ratio_counter = Defaults::Game::ContinueScreen::COUNTER_TEXT_Y_RATIO; float centre_y_counter = play_area.y + (play_area.h * y_ratio_counter); text_.renderCentered(counter_str, {.x = centre_x, .y = centre_y_counter}, escala_counter, spacing); // "CONTINUES LEFT" (conditional + using constants) if (!Defaults::Game::INFINITE_CONTINUES) { const std::string continues_text = "CONTINUES LEFT: " + std::to_string(Defaults::Game::MAX_CONTINUES - continues_used_); float escala_info = Defaults::Game::ContinueScreen::INFO_TEXT_SCALE; float y_ratio_info = Defaults::Game::ContinueScreen::INFO_TEXT_Y_RATIO; float centre_y_info = play_area.y + (play_area.h * y_ratio_info); text_.renderCentered(continues_text, {.x = centre_x, .y = centre_y_info}, escala_info, spacing); } } void GameScene::unir_jugador(uint8_t player_id) { // Activate player if (player_id == 0) { match_config_.jugador1_actiu = true; } else { match_config_.jugador2_actiu = true; } // Reset stats lives_per_player_[player_id] = Defaults::Game::STARTING_LIVES; score_per_player_[player_id] = 0; hit_timer_per_player_[player_id] = 0.0F; // Spawn with invulnerability Vec2 spawn_pos = obtenir_punt_spawn(player_id); ships_[player_id].init(&spawn_pos, true); // No visual message, just spawn (per user requirement) std::cout << "[GameScene] Jugador " << (int)(player_id + 1) << " s'ha unit a la match!" << '\n'; }