afegida logica de continues
fix: el text no centrava correctament en horitzontal
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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<unsigned char>(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<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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
// Check game over state first
|
||||
if (game_over_) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -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<int, 2> 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<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
|
||||
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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user