afegida logica de continues

fix: el text no centrava correctament en horitzontal
This commit is contained in:
2025-12-17 13:31:32 +01:00
parent 3b432e6580
commit ec333efe66
4 changed files with 291 additions and 23 deletions

View File

@@ -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 P1_SPAWN_X_RATIO = 0.33f; // 33% desde izquierda
constexpr float P2_SPAWN_X_RATIO = 0.67f; // 67% desde izquierda constexpr float P2_SPAWN_X_RATIO = 0.67f; // 67% desde izquierda
constexpr float SPAWN_Y_RATIO = 0.75f; // 75% desde arriba 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 } // namespace Game
// Física (valores actuales del juego, sincronizados con joc_asteroides.cpp) // Física (valores actuales del juego, sincronizados con joc_asteroides.cpp)

View File

@@ -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) // Altura de un carácter escalado (necesario para ajustar Y)
const float char_height_scaled = char_height * escala; const float char_height_scaled = char_height * escala;
// Posición actual del centro del carácter (ajustada desde esquina superior // Posición X del borde izquierdo del carácter actual
// izquierda) // (se ajustará +char_width/2 para obtener el centro al renderizar)
float current_x = posicio.x; float current_x = posicio.x;
// Iterar sobre cada byte del string (con detecció UTF-8) // 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); auto it = chars_.find(c);
if (it != chars_.end()) { if (it != chars_.end()) {
// Renderizar carácter // Renderizar carácter
// Ajustar Y para que posicio represente esquina superior izquierda // Ajustar X e Y para que posicio represente esquina superior izquierda
// (render_shape espera el centro, así que sumamos la mitad de la altura) // (render_shape espera el centro, así que sumamos la mitad de ancho y altura)
Punt char_pos = {current_x, posicio.y + char_height_scaled / 2.0f}; 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); Rendering::render_shape(renderer_, it->second, char_pos, 0.0f, escala, true, 1.0f, brightness);
// Avanzar posición // 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 char_width_scaled = char_width * escala;
const float spacing_scaled = spacing * escala; const float spacing_scaled = spacing * escala;
// Ancho total = (número de caracteres × char_width) + (espacios entre // Contar caracteres visuals (no bytes) - manejar UTF-8
// caracteres) size_t visual_chars = 0;
float width = text.length() * char_width_scaled; for (size_t i = 0; i < text.length(); i++) {
unsigned char c = static_cast<unsigned char>(text[i]);
// Añadir spacing entre caracteres (n-1 espacios para n caracteres) // Detectar copyright UTF-8 (0xC2 0xA9) - igual que render()
if (text.length() > 1) { if (c == 0xC2 && i + 1 < text.length() &&
width += (text.length() - 1) * spacing_scaled; static_cast<unsigned char>(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 { float VectorText::get_text_height(float escala) const {

View File

@@ -153,7 +153,10 @@ void EscenaJoc::inicialitzar() {
// Initialize lives and game over state (independent lives per player) // Initialize lives and game over state (independent lives per player)
vides_per_jugador_[0] = Defaults::Game::STARTING_LIVES; vides_per_jugador_[0] = Defaults::Game::STARTING_LIVES;
vides_per_jugador_[1] = 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; game_over_timer_ = 0.0f;
// Initialize scores (separate per player) // Initialize scores (separate per player)
@@ -206,7 +209,7 @@ void EscenaJoc::inicialitzar() {
void EscenaJoc::actualitzar(float delta_time) { void EscenaJoc::actualitzar(float delta_time) {
// Processar disparos (state-based, no event-based) // Processar disparos (state-based, no event-based)
if (!game_over_) { if (estat_game_over_ == EstatGameOver::NONE) {
auto* input = Input::get(); auto* input = Input::get();
// Jugador 1 dispara (només si està actiu) // Jugador 1 dispara (només si està actiu)
@@ -222,10 +225,42 @@ void EscenaJoc::actualitzar(float delta_time) {
disparar_bala(1); 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);
}
} }
// Check game over state first // P1 can join if only P2 is active
if (game_over_) { if (!config_partida_.jugador1_actiu && config_partida_.jugador2_actiu) {
if (input->checkActionPlayer1(InputAction::START, Input::DO_NOT_ALLOW_REPEAT)) {
unir_jugador(0);
}
}
}
// 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: only update timer, enemies, bullets, and debris
game_over_timer_ -= delta_time; game_over_timer_ -= delta_time;
@@ -277,13 +312,14 @@ void EscenaJoc::actualitzar(float delta_time) {
// Set sentinel value to prevent re-entering this block // Set sentinel value to prevent re-entering this block
itocado_per_jugador_[i] = 999.0f; 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 p1_dead = !config_partida_.jugador1_actiu || vides_per_jugador_[0] <= 0;
bool p2_dead = !config_partida_.jugador2_actiu || vides_per_jugador_[1] <= 0; bool p2_dead = !config_partida_.jugador2_actiu || vides_per_jugador_[1] <= 0;
if (p1_dead && p2_dead) { if (p1_dead && p2_dead) {
game_over_ = true; estat_game_over_ = EstatGameOver::CONTINUE;
game_over_timer_ = Defaults::Game::GAME_OVER_DURATION; 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() { void EscenaJoc::dibuixar() {
// Check game over state // Handle CONTINUE screen
if (game_over_) { 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 // Game over: draw enemies, bullets, debris, and "GAME OVER" text
dibuixar_marges(); 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;
}

View File

@@ -24,6 +24,13 @@
#include "core/system/game_config.hpp" #include "core/system/game_config.hpp"
#include "core/types.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) // Classe principal del joc (escena)
class EscenaJoc { class EscenaJoc {
public: public:
@@ -53,8 +60,11 @@ class EscenaJoc {
// Lives and game over system // Lives and game over system
std::array<int, 2> vides_per_jugador_; // [0]=P1, [1]=P2 std::array<int, 2> vides_per_jugador_; // [0]=P1, [1]=P2
bool game_over_; // Game over state flag EstatGameOver estat_game_over_; // Game over state machine (NONE, CONTINUE, GAME_OVER)
float game_over_timer_; // Countdown timer for auto-return (seconds) 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_spawn_; // DEPRECATED: usar obtenir_punt_spawn(player_id)
Punt punt_mort_; // Death position (for respawn, legacy) Punt punt_mort_; // Death position (for respawn, legacy)
std::array<int, 2> puntuacio_per_jugador_; // [0]=P1, [1]=P2 std::array<int, 2> puntuacio_per_jugador_; // [0]=P1, [1]=P2
@@ -78,6 +88,12 @@ class EscenaJoc {
void disparar_bala(uint8_t player_id); // Shoot bullet from player 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 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 // [NEW] Stage system helpers
void dibuixar_missatge_stage(const std::string& missatge); void dibuixar_missatge_stage(const std::string& missatge);