Files
orni-attack/source/game/scenes/game_scene.cpp
T
JailDesigner 27242f54fe Fase 6d: migrar Enemy al sistema de fisica vectorial
Segunda entidad migrada. Los enemigos (Pentagon, Quadrat, Molinillo)
ahora viven en el PhysicsWorld con velocidad vectorial. Las colisiones
entre enemigos quedan habilitadas automaticamente (novedad: antes no
se chocaban).

Cambios en enemy.hpp:
- Eliminado: float velocity_ (escalar)
- Eliminado: void mou() (lo hace el world)
- Anadido: override postUpdate()
- Anadido: helper privado setVelocityFromAngle(angle, speed)
- Anadido: direction_change_timer_ para zigzag periodico del Pentagon

Cambios en enemy.cpp:
- Constructor configura body_ (mass=5 default, radius=0 inactivo,
  restitution=1.0 elastico, sin damping)
- init() ajusta masa por tipo:
  * Pentagon: 5.0 (esquivador ligero)
  * Quadrat: 8.0 (tanque pesado)
  * Molinillo: 4.0 (agil rapido)
- init() setea body_.radius = ENEMY_RADIUS al spawn
- behaviorPentagon: zigzag por probabilidad temporal (0.8/s) en lugar
  de detectar paredes; el rebote contra muros lo hace PhysicsWorld
- behaviorQuadrat: tracking discreto cada TRACKING_INTERVAL — mezcla
  velocity actual con direccion al ship (LERP por tracking_strength)
- behaviorMolinillo: solo boost de rotacion visual cerca del ship;
  movimiento puramente lineal integrado por el world
- destruir() pone velocity=0, angular=0, radius=0
- postUpdate() sincroniza center_ desde body_.position
- setVelocity(speed) mantiene la direccion, cambia solo la magnitud

Renames a camelBack (.clang-tidy del proyecto):
- get_drotacio -> getRotationDelta
- get_base_velocity -> getBaseVelocity, get_base_rotation -> getBaseRotation
- set_ship_position -> setShipPosition
- set_velocity -> setVelocity, set_rotation -> setRotation
- set_tracking_strength -> setTrackingStrength
- get_temps_invulnerabilitat -> getInvulnerabilityTime
- actualitzar_animacio -> updateAnimation
- actualitzar_palpitacio -> updatePalpitation
- actualitzar_rotacio_accelerada -> updateRotationAcceleration
- comportament_pentagon/quadrat/molinillo -> behaviorPentagon/Quadrat/Molinillo
- calcular_escala_actual -> computeCurrentScale
- intent_spawn_safe -> attemptSafeSpawn
(callsites actualizados en spawn_controller y game_scene)

Cambios en GameScene:
- En init(): physics_world_.addBody(&enemy.getBody()) por cada slot
  (los inactivos tienen radius=0, no estorban)
- En update(): postUpdate() de cada enemy tras physics_world_.update

Cambios de comportamiento visibles esperados:
- Enemigos rebotan elasticamente contra paredes (restitution=1.0)
- Enemigos se chocan entre si (impulsos elasticos con masas distintas
  por tipo: Quadrat empuja mas, Molinillo rebota mas)
- Pentagon zigzag periodico en lugar de solo al chocar pared
- Molinillo: comportamiento mas predecible (linea recta)

Aviso: Bullet sigue con su movimiento ad-hoc (Fase 6e pendiente).

Smoke test xvfb OK. Validacion gameplay del usuario pendiente.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:41:05 +02:00

1423 lines
53 KiB
C++

// 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 <algorithm>
#include <cmath>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <vector>
#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<unsigned>(std::time(nullptr)));
// Configurar el mundo físico con los límites de la zona de juego.
// Las entidades se registrarán cada una al inicializarse (Fase 6c-e).
physics_world_.clear();
physics_world_.setBounds(Defaults::Zones::PLAYAREA);
// [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<StageSystem::StageManager>(stage_config_.get());
stage_manager_->init();
// [NEW] 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();
// 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
// Registrar el cuerpo físico de la nave en el mundo (Fase 6c)
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] = 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).
// 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 = Enemy(sdl_.getRenderer());
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 (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) {
// === FÍSICA: integrar bodies del frame anterior y resolver colisiones ===
// Se ejecuta al inicio del frame: las fuerzas aplicadas en el frame N-1
// por processInput/AI se integran ahora, y 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);
}
// 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<int>(zona.x);
int y1 = static_cast<int>(zona.y);
int x2 = static_cast<int>(zona.x + zona.w);
int y2 = static_cast<int>(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<int>(zona.x);
int y1 = static_cast<int>(zona.y);
int x2 = static_cast<int>(zona.x + zona.w);
int y2 = static_cast<int>(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<int>(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<int>(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<int>(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<int>(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<int>(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<float>(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<int>(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<int>(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.getRotationDelta(), // 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<size_t>(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';
}