From ec333efe66de031974730a4e449db422046306c2 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 17 Dec 2025 13:31:32 +0100 Subject: [PATCH] afegida logica de continues fix: el text no centrava correctament en horitzontal --- source/core/defaults.hpp | 21 +++ source/core/graphics/vector_text.cpp | 31 ++-- source/game/escenes/escena_joc.cpp | 242 ++++++++++++++++++++++++++- source/game/escenes/escena_joc.hpp | 20 ++- 4 files changed, 291 insertions(+), 23 deletions(-) diff --git a/source/core/defaults.hpp b/source/core/defaults.hpp index e39b904..ffd95f9 100644 --- a/source/core/defaults.hpp +++ b/source/core/defaults.hpp @@ -157,6 +157,27 @@ constexpr float INIT_HUD_SHIP_START_Y_RATIO = 0.75f; // 75% desde el top de PLA 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 + +// Continue system behavior +constexpr int CONTINUE_COUNT_START = 9; // Countdown starts at 9 +constexpr float CONTINUE_TICK_DURATION = 1.0f; // Seconds per countdown tick +constexpr int MAX_CONTINUES = 3; // Maximum continues per game +constexpr bool INFINITE_CONTINUES = false; // If true, unlimited continues + +// Continue screen visual configuration +namespace ContinueScreen { +// "CONTINUE" text +constexpr float CONTINUE_TEXT_SCALE = 2.0f; // Text size +constexpr float CONTINUE_TEXT_Y_RATIO = 0.35f; // 35% from top of PLAYAREA + +// Countdown number (9, 8, 7...) +constexpr float COUNTER_TEXT_SCALE = 4.0f; // Text size (large) +constexpr float COUNTER_TEXT_Y_RATIO = 0.50f; // 50% from top of PLAYAREA + +// "CONTINUES LEFT: X" text +constexpr float INFO_TEXT_SCALE = 1.0f; // Text size (small) +constexpr float INFO_TEXT_Y_RATIO = 0.65f; // 65% from top of PLAYAREA +} // namespace ContinueScreen } // namespace Game // Física (valores actuales del juego, sincronizados con joc_asteroides.cpp) diff --git a/source/core/graphics/vector_text.cpp b/source/core/graphics/vector_text.cpp index c4a9d58..b48c150 100644 --- a/source/core/graphics/vector_text.cpp +++ b/source/core/graphics/vector_text.cpp @@ -195,8 +195,8 @@ void VectorText::render(const std::string& text, const Punt& posicio, float esca // Altura de un carácter escalado (necesario para ajustar Y) const float char_height_scaled = char_height * escala; - // Posición actual del centro del carácter (ajustada desde esquina superior - // izquierda) + // Posición X del borde izquierdo del carácter actual + // (se ajustará +char_width/2 para obtener el centro al renderizar) float current_x = posicio.x; // Iterar sobre cada byte del string (con detecció UTF-8) @@ -220,9 +220,9 @@ void VectorText::render(const std::string& text, const Punt& posicio, float esca auto it = chars_.find(c); if (it != chars_.end()) { // Renderizar carácter - // Ajustar Y para que posicio represente esquina superior izquierda - // (render_shape espera el centro, así que sumamos la mitad de la altura) - Punt char_pos = {current_x, posicio.y + char_height_scaled / 2.0f}; + // Ajustar X e Y para que posicio represente esquina superior izquierda + // (render_shape espera el centro, así que sumamos la mitad de ancho y altura) + Punt char_pos = {current_x + char_width_scaled / 2.0f, posicio.y + char_height_scaled / 2.0f}; Rendering::render_shape(renderer_, it->second, char_pos, 0.0f, escala, true, 1.0f, brightness); // Avanzar posición @@ -244,16 +244,23 @@ float VectorText::get_text_width(const std::string& text, float escala, float sp const float char_width_scaled = char_width * escala; const float spacing_scaled = spacing * escala; - // Ancho total = (número de caracteres × char_width) + (espacios entre - // caracteres) - float width = text.length() * char_width_scaled; + // Contar caracteres visuals (no bytes) - manejar UTF-8 + size_t visual_chars = 0; + for (size_t i = 0; i < text.length(); i++) { + unsigned char c = static_cast(text[i]); - // Añadir spacing entre caracteres (n-1 espacios para n caracteres) - if (text.length() > 1) { - width += (text.length() - 1) * spacing_scaled; + // Detectar copyright UTF-8 (0xC2 0xA9) - igual que render() + if (c == 0xC2 && i + 1 < text.length() && + static_cast(text[i + 1]) == 0xA9) { + visual_chars++; // Un caràcter visual (©) + i++; // Saltar el següent byte + } else { + visual_chars++; // Caràcter normal + } } - return width; + // Ancho total = todos los caracteres VISUALES + spacing entre ellos + return visual_chars * char_width_scaled + (visual_chars - 1) * spacing_scaled; } float VectorText::get_text_height(float escala) const { diff --git a/source/game/escenes/escena_joc.cpp b/source/game/escenes/escena_joc.cpp index ca959b7..6b91d32 100644 --- a/source/game/escenes/escena_joc.cpp +++ b/source/game/escenes/escena_joc.cpp @@ -153,7 +153,10 @@ void EscenaJoc::inicialitzar() { // 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; + estat_game_over_ = EstatGameOver::NONE; + continue_counter_ = 0; + continue_tick_timer_ = 0.0f; + continues_usados_ = 0; game_over_timer_ = 0.0f; // Initialize scores (separate per player) @@ -206,7 +209,7 @@ void EscenaJoc::inicialitzar() { void EscenaJoc::actualitzar(float delta_time) { // Processar disparos (state-based, no event-based) - if (!game_over_) { + if (estat_game_over_ == EstatGameOver::NONE) { auto* input = Input::get(); // Jugador 1 dispara (només si està actiu) @@ -222,10 +225,42 @@ void EscenaJoc::actualitzar(float delta_time) { disparar_bala(1); } } + + // [NEW] Allow mid-game join: inactive player presses START + // P2 can join if only P1 is active + if (config_partida_.jugador1_actiu && !config_partida_.jugador2_actiu) { + if (input->checkActionPlayer2(InputAction::START, Input::DO_NOT_ALLOW_REPEAT)) { + unir_jugador(1); + } + } + + // P1 can join if only P2 is active + if (!config_partida_.jugador1_actiu && config_partida_.jugador2_actiu) { + if (input->checkActionPlayer1(InputAction::START, Input::DO_NOT_ALLOW_REPEAT)) { + unir_jugador(0); + } + } } - // Check game over state first - if (game_over_) { + // Handle CONTINUE screen + if (estat_game_over_ == EstatGameOver::CONTINUE) { + actualitzar_continue(delta_time); + processar_input_continue(); + + // Still update enemies, bullets, and effects during continue screen + for (auto& enemy : orni_) { + enemy.actualitzar(delta_time); + } + for (auto& bala : bales_) { + bala.actualitzar(delta_time); + } + debris_manager_.actualitzar(delta_time); + gestor_puntuacio_.actualitzar(delta_time); + return; + } + + // Handle final GAME OVER state + if (estat_game_over_ == EstatGameOver::GAME_OVER) { // Game over: only update timer, enemies, bullets, and debris game_over_timer_ -= delta_time; @@ -277,13 +312,14 @@ void EscenaJoc::actualitzar(float delta_time) { // Set sentinel value to prevent re-entering this block itocado_per_jugador_[i] = 999.0f; - // Check if ALL ACTIVE players are dead (game over) + // Check if ALL ACTIVE players are dead (trigger continue screen) bool p1_dead = !config_partida_.jugador1_actiu || vides_per_jugador_[0] <= 0; bool p2_dead = !config_partida_.jugador2_actiu || vides_per_jugador_[1] <= 0; if (p1_dead && p2_dead) { - game_over_ = true; - game_over_timer_ = Defaults::Game::GAME_OVER_DURATION; + estat_game_over_ = EstatGameOver::CONTINUE; + continue_counter_ = Defaults::Game::CONTINUE_COUNT_START; + continue_tick_timer_ = Defaults::Game::CONTINUE_TICK_DURATION; } } } @@ -450,8 +486,30 @@ void EscenaJoc::actualitzar(float delta_time) { } void EscenaJoc::dibuixar() { - // Check game over state - if (game_over_) { + // Handle CONTINUE screen + if (estat_game_over_ == EstatGameOver::CONTINUE) { + // Draw game background elements first + dibuixar_marges(); + + for (const auto& enemy : orni_) { + enemy.dibuixar(); + } + + for (const auto& bala : bales_) { + bala.dibuixar(); + } + + debris_manager_.dibuixar(); + gestor_puntuacio_.dibuixar(); + dibuixar_marcador(); + + // Draw CONTINUE screen overlay + dibuixar_continue(); + return; + } + + // Handle final GAME OVER state + if (estat_game_over_ == EstatGameOver::GAME_OVER) { // Game over: draw enemies, bullets, debris, and "GAME OVER" text dibuixar_marges(); @@ -1121,3 +1179,169 @@ void EscenaJoc::disparar_bala(uint8_t player_id) { } } } + +// ==================== CONTINUE & JOIN SYSTEM ==================== + +void EscenaJoc::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; + + // Play tick sound + Audio::get()->playSound("continue_tick"); + + if (continue_counter_ <= 0) { + // Timeout → final game over + estat_game_over_ = EstatGameOver::GAME_OVER; + game_over_timer_ = Defaults::Game::GAME_OVER_DURATION; + } + } +} + +void EscenaJoc::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_usados_ >= Defaults::Game::MAX_CONTINUES) { + // Max continues reached → final game over + estat_game_over_ = EstatGameOver::GAME_OVER; + game_over_timer_ = Defaults::Game::GAME_OVER_DURATION; + return; + } + + // Only increment if not infinite + if (!Defaults::Game::INFINITE_CONTINUES) { + continues_usados_++; + } + + // Determine which player(s) to revive + uint8_t player_to_revive = p1_start ? 0 : 1; + + // Reset score and lives (KEEP level and enemies!) + puntuacio_per_jugador_[player_to_revive] = 0; + vides_per_jugador_[player_to_revive] = Defaults::Game::STARTING_LIVES; + itocado_per_jugador_[player_to_revive] = 0.0f; + + // Activate player if not already + if (player_to_revive == 0) { + config_partida_.jugador1_actiu = true; + } else { + config_partida_.jugador2_actiu = true; + } + + // Spawn with invulnerability + Punt spawn_pos = obtenir_punt_spawn(player_to_revive); + naus_[player_to_revive].inicialitzar(&spawn_pos, true); + + // Check if other player wants to continue too + if (p1_start && p2_start) { + uint8_t other_player = 1; + puntuacio_per_jugador_[other_player] = 0; + vides_per_jugador_[other_player] = Defaults::Game::STARTING_LIVES; + itocado_per_jugador_[other_player] = 0.0f; + config_partida_.jugador2_actiu = true; + Punt spawn_pos2 = obtenir_punt_spawn(other_player); + naus_[other_player].inicialitzar(&spawn_pos2, true); + } + + // Resume game + estat_game_over_ = EstatGameOver::NONE; + continue_counter_ = 0; + continue_tick_timer_ = 0.0f; + + // Play continue confirmation sound + Audio::get()->playSound("continue_confirm"); + + 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_--; + + // Play tick sound on manual decrement + Audio::get()->playSound("continue_tick"); + + if (continue_counter_ <= 0) { + estat_game_over_ = EstatGameOver::GAME_OVER; + game_over_timer_ = Defaults::Game::GAME_OVER_DURATION; + } + + // Reset timer to prevent double-decrement + continue_tick_timer_ = Defaults::Game::CONTINUE_TICK_DURATION; + } +} + +void EscenaJoc::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 text_width_continue = text_.get_text_width(continue_text, escala_continue, spacing); + float x_continue = play_area.x + (play_area.w - text_width_continue) / 2.0f; + float y_continue = play_area.y + play_area.h * y_ratio_continue; + + text_.render(continue_text, {x_continue, 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 text_width_counter = text_.get_text_width(counter_str, escala_counter, spacing); + float x_counter = play_area.x + (play_area.w - text_width_counter) / 2.0f; + float y_counter = play_area.y + play_area.h * y_ratio_counter; + + text_.render(counter_str, {x_counter, 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_usados_); + float escala_info = Defaults::Game::ContinueScreen::INFO_TEXT_SCALE; + float y_ratio_info = Defaults::Game::ContinueScreen::INFO_TEXT_Y_RATIO; + + float text_width_info = text_.get_text_width(continues_text, escala_info, spacing); + float x_info = play_area.x + (play_area.w - text_width_info) / 2.0f; + float y_info = play_area.y + play_area.h * y_ratio_info; + + text_.render(continues_text, {x_info, y_info}, escala_info, spacing); + } +} + +void EscenaJoc::unir_jugador(uint8_t player_id) { + // Activate player + if (player_id == 0) { + config_partida_.jugador1_actiu = true; + } else { + config_partida_.jugador2_actiu = true; + } + + // Reset stats + vides_per_jugador_[player_id] = Defaults::Game::STARTING_LIVES; + puntuacio_per_jugador_[player_id] = 0; + itocado_per_jugador_[player_id] = 0.0f; + + // Spawn with invulnerability + Punt spawn_pos = obtenir_punt_spawn(player_id); + naus_[player_id].inicialitzar(&spawn_pos, true); + + // No visual message, just spawn (per user requirement) + + std::cout << "[EscenaJoc] Jugador " << (int)(player_id + 1) << " s'ha unit a la partida!" << std::endl; +} diff --git a/source/game/escenes/escena_joc.hpp b/source/game/escenes/escena_joc.hpp index db93d6b..1bd9e3f 100644 --- a/source/game/escenes/escena_joc.hpp +++ b/source/game/escenes/escena_joc.hpp @@ -24,6 +24,13 @@ #include "core/system/game_config.hpp" #include "core/types.hpp" +// Game over state machine +enum class EstatGameOver { + NONE, // Normal gameplay + CONTINUE, // Continue countdown screen (9→0) + GAME_OVER // Final game over (returning to title) +}; + // Classe principal del joc (escena) class EscenaJoc { public: @@ -53,8 +60,11 @@ class EscenaJoc { // Lives and game over system 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) + EstatGameOver estat_game_over_; // Game over state machine (NONE, CONTINUE, GAME_OVER) + int continue_counter_; // Continue countdown (9→0) + float continue_tick_timer_; // Timer for countdown tick (1.0s) + int continues_usados_; // Continues used this game (0-3 max) + float game_over_timer_; // Final GAME OVER timer before title screen // Punt punt_spawn_; // DEPRECATED: usar obtenir_punt_spawn(player_id) Punt punt_mort_; // Death position (for respawn, legacy) std::array puntuacio_per_jugador_; // [0]=P1, [1]=P2 @@ -78,6 +88,12 @@ class EscenaJoc { 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] Continue & Join system + void unir_jugador(uint8_t player_id); // Join inactive player mid-game + void processar_input_continue(); // Handle input during continue screen + void actualitzar_continue(float delta_time); // Update continue countdown + void dibuixar_continue(); // Draw continue screen + // [NEW] Stage system helpers void dibuixar_missatge_stage(const std::string& missatge);