// game_scene.cpp - Implementació de la lógica del juego // © 2026 JailDesigner #include "game_scene.hpp" #include #include #include #include #include #include "core/audio/audio.hpp" #include "core/input/input.hpp" #include "core/rendering/line_renderer.hpp" #include "core/system/scene_context.hpp" #include "game/stage_system/stage_loader.hpp" #include "game/systems/collision_system.hpp" #include "game/systems/continue_system.hpp" #include "game/systems/init_hud_animator.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()), firework_manager_(sdl.getRenderer()), floating_score_manager_(sdl.getRenderer()), text_(sdl.getRenderer()) { // 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 std::ranges::fill(bullets_, Bullet(sdl.getRenderer())); // Inicialitzar enemigos con renderer std::ranges::fill(enemies_, Enemy(sdl.getRenderer())); // Inicialitzar generador de números aleatoris // Basat en el codi Pascal original: line 376 std::srand(static_cast(std::time(nullptr))); // Configurar el mundo físico con los límites de la zona de juego. physics_world_.clear(); physics_world_.setBounds(Defaults::Zones::PLAYAREA); // Load stage configuration 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) } // Initialize stage manager stage_manager_ = std::make_unique(stage_config_.get()); stage_manager_->init(); // Set ship position reference for safe spawn (P1 for now, TODO: dual tracking) stage_manager_->getSpawnController().setShipPosition(&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(); // 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 = getSpawnPoint(i); ships_[i].init(&spawn_pos, false); // No invulnerability at start // Registrar el cuerpo físico de la nave en el mundo physics_world_.addBody(&ships_[i].getBody()); std::cout << "[GameScene] Jugador " << (i + 1) << " inicialitzat\n"; } else { // Jugador inactiu: marcar como a mort permanent ships_[i].markHit(); hit_timer_per_player_[i] = Defaults::Game::HIT_TIMER_INACTIVE_PLAYER; lives_per_player_[i] = 0; // Sin vides std::cout << "[GameScene] Jugador " << (i + 1) << " inactiu\n"; } } // Initialize enemies as inactive (stage system will spawn them). // Registramos el body al world incluso inactivo: con radius=0 no colisiona // ni se mueve, y al init() del stage system se activa sin re-registrar. for (auto& enemy : enemies_) { enemy.setShipPosition(&ships_[0].getCenter()); // Set ship reference (P1 for now) physics_world_.addBody(&enemy.getBody()); // DON'T call enemy.init() here - stage system handles spawning } // Inicialitzar balas. // Se registran en el physics_world para integración cinemática. // Como su body_.radius=0, no colisionan físicamente con nadie (las // colisiones de gameplay se gestionan en detectar_col·lisions_*). for (auto& bullet : bullets_) { bullet.init(); physics_world_.addBody(&bullet.getBody()); } // Reset flag de sons de animación init_hud_rect_sound_played_ = false; } auto GameScene::isFinished() const -> bool { return context_.nextScene() != SceneType::GAME; } void GameScene::handleEvent(const SDL_Event& event) { // GameScene no procesa eventos puntuales SDL: la lógica de input se // resuelve en update() consultando Input::checkAction. (void)event; } void GameScene::update(float delta_time) { // Orquestador delgado: cada paso vive en su propia función para // mantener update() legible y reducir complejidad cognitiva. stepPhysics(delta_time); if (game_over_state_ == GameOverState::NONE) { stepShootingInput(); stepMidGameJoin(); } if (stepContinueScreen(delta_time)) { return; } if (stepGameOver(delta_time)) { return; } stepDeathSequence(delta_time); stepStageStateMachine(delta_time); } void GameScene::stepPhysics(float delta_time) { // Las fuerzas aplicadas en el frame N-1 por processInput/AI se integran // ahora; postUpdate sincroniza los mirrors (center_/angle_) antes de la // lógica de juego que los lee. physics_world_.update(delta_time); for (auto& ship : ships_) { ship.postUpdate(delta_time); } for (auto& enemy : enemies_) { enemy.postUpdate(delta_time); } for (auto& bullet : bullets_) { bullet.postUpdate(delta_time); } } void GameScene::stepShootingInput() { auto* input = Input::get(); if (match_config_.jugador1_actiu && input->checkActionPlayer1(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) { fireBullet(0); } if (match_config_.jugador2_actiu && input->checkActionPlayer2(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) { fireBullet(1); } } void GameScene::stepMidGameJoin() { // Permitir join solo durante PLAYING. if (stage_manager_->getState() != StageSystem::EstatStage::PLAYING) { return; } // Solo se permite join si hay al menos un jugador vivo (no se puede // hacer join en pantalla vacía). const bool ALGU_VIU = (match_config_.jugador1_actiu && hit_timer_per_player_[0] != Defaults::Game::HIT_TIMER_INACTIVE_PLAYER) || (match_config_.jugador2_actiu && hit_timer_per_player_[1] != Defaults::Game::HIT_TIMER_INACTIVE_PLAYER); if (!ALGU_VIU) { return; } auto* input = Input::get(); for (uint8_t pid = 0; pid < 2; pid++) { const bool ACTIU = (pid == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; const bool MUERTO_SIN_VIDAS = hit_timer_per_player_[pid] == Defaults::Game::HIT_TIMER_INACTIVE_PLAYER; if (ACTIU && !MUERTO_SIN_VIDAS) { continue; // jugador ya está jugando } const bool START_PRESSED = (pid == 0) ? input->checkActionPlayer1(InputAction::START, Input::DO_NOT_ALLOW_REPEAT) : input->checkActionPlayer2(InputAction::START, Input::DO_NOT_ALLOW_REPEAT); if (START_PRESSED) { joinPlayer(pid); } } } auto GameScene::stepContinueScreen(float delta_time) -> bool { if (game_over_state_ != GameOverState::CONTINUE) { return false; } Systems::ContinueScreen::Context cont_ctx{ .state = game_over_state_, .counter = continue_counter_, .tick_timer = continue_tick_timer_, .continues_used = continues_used_, .game_over_timer = game_over_timer_, .lives_per_player = lives_per_player_, .score_per_player = score_per_player_, .hit_timer_per_player = hit_timer_per_player_, .ships = ships_, .match_config = match_config_, .get_spawn_point = [this](uint8_t pid) { return getSpawnPoint(pid); }, }; Systems::ContinueScreen::update(cont_ctx, delta_time); Systems::ContinueScreen::processInput(cont_ctx); // Enemies, bullets y efectos siguen moviéndose en background. for (auto& enemy : enemies_) { enemy.update(delta_time); } for (auto& bullet : bullets_) { bullet.update(delta_time); } debris_manager_.update(delta_time); firework_manager_.update(delta_time); floating_score_manager_.update(delta_time); return true; } auto GameScene::stepGameOver(float delta_time) -> bool { if (game_over_state_ != GameOverState::GAME_OVER) { return false; } game_over_timer_ -= delta_time; if (game_over_timer_ <= 0.0F) { Audio::get()->stopMusic(); context_.setNextScene(SceneType::TITLE); return true; } // Enemies, bullets y efectos siguen moviéndose como fondo. for (auto& enemy : enemies_) { enemy.update(delta_time); } for (auto& bullet : bullets_) { bullet.update(delta_time); } debris_manager_.update(delta_time); firework_manager_.update(delta_time); floating_score_manager_.update(delta_time); return true; } void GameScene::stepDeathSequence(float delta_time) { bool algun_mort = false; for (uint8_t i = 0; i < 2; i++) { if (hit_timer_per_player_[i] <= 0.0F || hit_timer_per_player_[i] >= Defaults::Game::HIT_TIMER_INACTIVE_PLAYER) { continue; } algun_mort = true; hit_timer_per_player_[i] += delta_time; if (hit_timer_per_player_[i] < Defaults::Game::DEATH_DURATION) { continue; } // *** PHASE 3: RESPAWN OR GAME OVER *** lives_per_player_[i]--; if (lives_per_player_[i] > 0) { Vec2 spawn_pos = getSpawnPoint(i); ships_[i].init(&spawn_pos, /*activar_invulnerabilitat=*/true); hit_timer_per_player_[i] = 0.0F; continue; } // Sin vidas: marcar definitivamente muerto y comprobar transición a CONTINUE. hit_timer_per_player_[i] = Defaults::Game::HIT_TIMER_INACTIVE_PLAYER; const bool P1_DEAD = !match_config_.jugador1_actiu || lives_per_player_[0] <= 0; const 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; } } // Si hay algún muerto, los enemigos/balas/efectos siguen actualizándose // aunque otros jugadores aún jueguen. if (algun_mort) { for (auto& enemy : enemies_) { enemy.update(delta_time); } for (auto& bullet : bullets_) { bullet.update(delta_time); } debris_manager_.update(delta_time); firework_manager_.update(delta_time); floating_score_manager_.update(delta_time); } // El bool 'algun_mort' es puramente interno: no aporta nada al caller // (la stage state machine sigue corriendo aunque haya jugadores muriendo), // así que la función no devuelve nada. } void GameScene::stepStageStateMachine(float delta_time) { const StageSystem::EstatStage STATE = stage_manager_->getState(); switch (STATE) { case StageSystem::EstatStage::INIT_HUD: runStageInitHud(delta_time); break; case StageSystem::EstatStage::LEVEL_START: runStageLevelStart(delta_time); break; case StageSystem::EstatStage::PLAYING: runStagePlaying(delta_time); break; case StageSystem::EstatStage::LEVEL_COMPLETED: runStageLevelCompleted(delta_time); break; } } void GameScene::runStageInitHud(float delta_time) { // Update stage manager timer (puede cambiar el state). stage_manager_->update(delta_time); // Si el state cambió, salir para no usar el timer del nuevo state. if (stage_manager_->getState() != StageSystem::EstatStage::INIT_HUD) { return; } float global_progress = 1.0F - (stage_manager_->getTransitionTimer() / Defaults::Game::INIT_HUD_DURATION); global_progress = std::min(1.0F, global_progress); const float SHIP1_P = Systems::InitHud::computeRangeProgress( global_progress, Defaults::Game::INIT_HUD_SHIP1_RATIO_INIT, Defaults::Game::INIT_HUD_SHIP1_RATIO_END); const float SHIP2_P = Systems::InitHud::computeRangeProgress( global_progress, Defaults::Game::INIT_HUD_SHIP2_RATIO_INIT, Defaults::Game::INIT_HUD_SHIP2_RATIO_END); if (match_config_.jugador1_actiu && SHIP1_P < 1.0F) { ships_[0].setCenter(Systems::InitHud::computeShipPosition(SHIP1_P, getSpawnPoint(0))); } if (match_config_.jugador2_actiu && SHIP2_P < 1.0F) { ships_[1].setCenter(Systems::InitHud::computeShipPosition(SHIP2_P, getSpawnPoint(1))); } } void GameScene::runStageLevelStart(float delta_time) { stage_manager_->update(delta_time); // Ambas naves pueden moverse y disparar durante el intro. for (uint8_t i = 0; i < 2; i++) { const bool ACTIU = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; if (ACTIU && hit_timer_per_player_[i] == 0.0F) { ships_[i].processInput(delta_time, i); ships_[i].update(delta_time); } } for (auto& bullet : bullets_) { bullet.update(delta_time); } debris_manager_.update(delta_time); firework_manager_.update(delta_time); } void GameScene::runStagePlaying(float delta_time) { const 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); // Stage completado: cuando al menos un jugador está vivo y todos los enemies muertos. const bool ALGU_VIU = (hit_timer_per_player_[0] == 0.0F || hit_timer_per_player_[1] == 0.0F); if (ALGU_VIU && stage_manager_->getSpawnController().allEnemiesDestroyed(enemies_)) { stage_manager_->markStageCompleted(); Audio::get()->playSound(Defaults::Sound::GOOD_JOB_COMMANDER, Audio::Group::GAME); return; } // Gameplay normal: ships activos + entidades + colisiones + efectos. for (uint8_t i = 0; i < 2; i++) { const bool ACTIU = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; if (ACTIU && hit_timer_per_player_[i] == 0.0F) { 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); } runCollisionDetections(); debris_manager_.update(delta_time); firework_manager_.update(delta_time); floating_score_manager_.update(delta_time); } void GameScene::runStageLevelCompleted(float delta_time) { stage_manager_->update(delta_time); for (uint8_t i = 0; i < 2; i++) { const bool ACTIU = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; if (ACTIU && hit_timer_per_player_[i] == 0.0F) { ships_[i].processInput(delta_time, i); ships_[i].update(delta_time); } } for (auto& bullet : bullets_) { bullet.update(delta_time); } debris_manager_.update(delta_time); firework_manager_.update(delta_time); floating_score_manager_.update(delta_time); } void GameScene::runCollisionDetections() { Systems::Collision::Context col_ctx{ .ships = ships_, .enemies = enemies_, .bullets = bullets_, .hit_timer_per_player = hit_timer_per_player_, .score_per_player = score_per_player_, .lives_per_player = lives_per_player_, .debris_manager = debris_manager_, .firework_manager = firework_manager_, .floating_score_manager = floating_score_manager_, .match_config = match_config_, .on_player_hit = [this](uint8_t pid) { tocado(pid); }, }; Systems::Collision::detectAll(col_ctx); } void GameScene::draw() { if (game_over_state_ == GameOverState::CONTINUE) { drawContinueState(); return; } if (game_over_state_ == GameOverState::GAME_OVER) { drawGameOverState(); return; } switch (stage_manager_->getState()) { case StageSystem::EstatStage::INIT_HUD: drawInitHudState(); break; case StageSystem::EstatStage::LEVEL_START: drawLevelStartState(); break; case StageSystem::EstatStage::PLAYING: drawPlayingState(); break; case StageSystem::EstatStage::LEVEL_COMPLETED: drawLevelCompletedState(); break; } } void GameScene::drawEnemies() const { for (const auto& enemy : enemies_) { enemy.draw(); } } void GameScene::drawBullets() const { for (const auto& bullet : bullets_) { bullet.draw(); } } void GameScene::drawActiveShipsAlive() const { 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(); } } } void GameScene::drawContinueState() { drawMargins(); drawEnemies(); drawBullets(); debris_manager_.draw(); firework_manager_.draw(); floating_score_manager_.draw(); drawScoreboard(); drawContinue(); } void GameScene::drawGameOverState() { drawMargins(); drawEnemies(); drawBullets(); debris_manager_.draw(); firework_manager_.draw(); floating_score_manager_.draw(); const std::string GAME_OVER_TEXT = "GAME OVER"; constexpr float SCALE = Defaults::Game::GameOverScreen::TEXT_SCALE; constexpr float SPACING = Defaults::Game::GameOverScreen::TEXT_SPACING; 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); drawScoreboard(); } void GameScene::drawInitHudState() { float timer = stage_manager_->getTransitionTimer(); float total_time = Defaults::Game::INIT_HUD_DURATION; float global_progress = 1.0F - (timer / total_time); float rect_progress = Systems::InitHud::computeRangeProgress( global_progress, Defaults::Game::INIT_HUD_RECT_RATIO_INIT, Defaults::Game::INIT_HUD_RECT_RATIO_END); float score_progress = Systems::InitHud::computeRangeProgress( global_progress, Defaults::Game::INIT_HUD_SCORE_RATIO_INIT, Defaults::Game::INIT_HUD_SCORE_RATIO_END); float ship1_progress = Systems::InitHud::computeRangeProgress( global_progress, Defaults::Game::INIT_HUD_SHIP1_RATIO_INIT, Defaults::Game::INIT_HUD_SHIP1_RATIO_END); float ship2_progress = Systems::InitHud::computeRangeProgress( global_progress, Defaults::Game::INIT_HUD_SHIP2_RATIO_INIT, Defaults::Game::INIT_HUD_SHIP2_RATIO_END); if (rect_progress > 0.0F) { if (!init_hud_rect_sound_played_) { Audio::get()->playSound(Defaults::Sound::INIT_HUD, Audio::Group::GAME); init_hud_rect_sound_played_ = true; } Systems::InitHud::drawBordersAnimated(sdl_.getRenderer(), rect_progress); } if (score_progress > 0.0F) { Systems::InitHud::drawScoreboardAnimated(text_, buildScoreboard(), score_progress); } if (ship1_progress > 0.0F && match_config_.jugador1_actiu && ships_[0].isActive()) { ships_[0].draw(); } if (ship2_progress > 0.0F && match_config_.jugador2_actiu && ships_[1].isActive()) { ships_[1].draw(); } } void GameScene::drawLevelStartState() { drawMargins(); drawActiveShipsAlive(); drawBullets(); debris_manager_.draw(); firework_manager_.draw(); floating_score_manager_.draw(); drawStageMessage(stage_manager_->getLevelStartMessage()); drawScoreboard(); } void GameScene::drawPlayingState() { drawMargins(); drawActiveShipsAlive(); drawEnemies(); drawBullets(); debris_manager_.draw(); firework_manager_.draw(); floating_score_manager_.draw(); drawScoreboard(); } void GameScene::drawLevelCompletedState() { drawMargins(); drawActiveShipsAlive(); drawBullets(); debris_manager_.draw(); firework_manager_.draw(); floating_score_manager_.draw(); drawStageMessage(StageSystem::Constants::MISSATGE_LEVEL_COMPLETED); drawScoreboard(); } 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 la velocity heretada per la ship segons defaults (més realista) constexpr float INHERIT = Defaults::Physics::Debris::SHIP_VELOCITY_INHERITANCE; Vec2 vel_nau_80 = {.x = vel_nau.x * INHERIT, .y = vel_nau.y * INHERIT}; 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 Defaults::Palette::SHIP // Debris hereda color de la nave ); // Start death timer (non-zero to avoid re-triggering) hit_timer_per_player_[player_id] = Defaults::Game::HIT_TIMER_TRIGGER_DEATH; } // Phase 2 is automatic (debris updates in update()) // Phase 3 is handled in update() when hit_timer_per_player_ >= DEATH_DURATION } void GameScene::drawMargins() 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::drawScoreboard() { // Construir text del marcador std::string text = buildScoreboard(); // Parámetros de renderització const float SCALE = Defaults::Hud::SCOREBOARD_TEXT_SCALE; const float SPACING = Defaults::Hud::SCOREBOARD_TEXT_SPACING; // Calcular centro de la zona del marcador const SDL_FRect& scoreboard_zone = Defaults::Zones::SCOREBOARD; float centre_x = scoreboard_zone.w / 2.0F; float centre_y = scoreboard_zone.y + (scoreboard_zone.h / 2.0F); // Renderizar centrat text_.renderCentered(text, {.x = centre_x, .y = centre_y}, SCALE, SPACING); } auto GameScene::buildScoreboard() const -> std::string { // 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_->getCurrentStage(); 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; } // [NEW] Stage system helper methods void GameScene::drawStageMessage(const std::string& message) { constexpr float BASE_SCALE = 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_->getState() == 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_->getTransitionTimer(); 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 = Graphics::VectorText::getTextWidth(message, BASE_SCALE, SPACING); // Auto-scale if text exceeds max width float scale = (text_width_at_base <= MAX_WIDTH) ? BASE_SCALE : MAX_WIDTH / text_width_at_base; // Recalculate dimensions with final scale (using FULL message for centering) float full_text_width = Graphics::VectorText::getTextWidth(message, scale, SPACING); float text_height = Graphics::VectorText::getTextHeight(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 // ======================================== auto GameScene::getSpawnPoint(uint8_t player_id) const -> Vec2 { const SDL_FRect& zona = Defaults::Zones::PLAYAREA; float x_ratio; if (match_config_.isSinglePlayer()) { // 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::fireBullet(uint8_t player_id) { // Verificar que el player está vivo if (hit_timer_per_player_[player_id] > 0.0F) { return; } if (!ships_[player_id].isActive()) { 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 = Defaults::Hud::Tips::LOCAL_X; constexpr float LOCAL_TIP_Y = Defaults::Hud::Tips::LOCAL_Y; 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. // El pool global té MAX_BALES slots per jugador (P1=[0..MAX-1], P2=[MAX..2*MAX-1]). constexpr int SLOTS_PER_PLAYER = Defaults::Entities::MAX_BALES; const int START_IDX = player_id * SLOTS_PER_PLAYER; for (int i = START_IDX; i < START_IDX + SLOTS_PER_PLAYER; i++) { if (!bullets_[i].isActive()) { bullets_[i].disparar(posicio_dispar, ship_angle, player_id); break; } } } void GameScene::drawContinue() { 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::joinPlayer(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 = getSpawnPoint(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'; }