Files
orni-attack/source/game/scenes/game_scene.cpp
T
JailDesigner efbf2457a1 Lint: inicializadores + retornos const-ref + warnings preexistentes
Primera tanda mecánica sobre el lint pendiente. Arregla la causa raíz, no
silencia diagnósticos. Detalle por categoría:

- Uninit members (cppcheck warnings) → inicializadores en declaración:
  Bullet (esta_, owner_id_, grace_timer_), Enemy (drotacio_, rotacio_,
  esta_, type_, tracking_timer_, ship_position_, tracking_strength_,
  direction_change_timer_, timer_invulnerabilitat_), Ship (is_hit_,
  invulnerable_timer_), Shape (escala_defecte_) y TitleShip (todos los
  miembros del struct, que viven dentro de un std::array<,2>).

- returnByReference (cppcheck performance) → return const T&:
  Shape::getName, ResourceLoader::getBasePath. De paso, Shape::get_nom
  se renombra a getName y get_num_primitives a getNumPrimitives para
  cumplir la convención camelBack del proyecto (lint del .clang-tidy).

- useInitializationList (cppcheck performance) →
  Starfield::shape_estrella_ pasa a la lista de inicialización (reordenada
  según la declaración para no disparar -Wreorder-ctor).

- noExplicitConstructor (cppcheck style) → explicit en ctores de 1 arg:
  Bullet(Renderer*), Enemy(Renderer*), Ship(Renderer*,...) y VectorText(Renderer*).

- variableScope (cppcheck style) → en vector_text.cpp se elimina la
  variable 'c' intermedia y se usa el literal '\\xA9' directamente en el
  único punto donde se necesita.

- constParameterReference (cppcheck style) → drawScoreboardAnimated pasa
  el VectorText por const ref (la API render/renderCentered es const).

- Warnings preexistentes del compilador (resueltos de paso):
  - stage_config.hpp: stage_id <= 255 sobre uint8_t era siempre true; se
    elimina la comparación redundante y se explica con comentario.
  - director.cpp: 'struct stat st = {.st_dev = 0};' disparaba
    -Wmissing-field-initializers; pasa a 'struct stat st{};' (zero-init
    completo, robusto a las variantes específicas del SO).
  - game_scene.cpp: stepDeathSequence devolvía un bool [[nodiscard]] que
    el caller ignoraba; el valor era puramente interno. Cambiada la
    firma a void.

- cppcheck: añadido --suppress=useStlAlgorithm. Las 26 sugerencias
  'Consider using std::any_of/find_if/count_if' son cosméticas y no
  aportan claridad sobre las raw loops actuales.

- .clang-tidy de source/core/audio/ eliminado: deshabilitaba todos los
  checks en ese subdirectorio por dependencia de jail_audio.hpp, pero
  impedía ejecutar 'make tidy' (clang-tidy aborta con "no checks
  enabled" al primer archivo del directorio). El proyecto pasa a usar
  el mismo patrón de CCAE: solo source/external/ y source/legacy/
  quedan fuera del lint.

- lint-reports/ añadido a .gitignore. Carpeta donde 'make tidy' y
  'make cppcheck' vuelcan su salida completa para inspección posterior.

Build limpio, cero warnings activos.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:29:36 +02:00

967 lines
36 KiB
C++

// game_scene.cpp - Implementació de la lógica del juego
// © 2026 JailDesigner
#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/math/easing.hpp"
#include "core/physics/collision.hpp"
#include "core/rendering/line_renderer.hpp"
#include "core/system/scene_context.hpp"
#include "game/stage_system/stage_loader.hpp"
#include "game/systems/collision_system.hpp"
#include "game/systems/continue_system.hpp"
#include "game/systems/init_hud_animator.hpp"
// Using declarations per simplificar el codi
using SceneManager::SceneContext;
using SceneType = SceneContext::SceneType;
using Option = SceneContext::Option;
GameScene::GameScene(SDLManager& sdl, SceneContext& context)
: sdl_(sdl),
context_(context),
debris_manager_(sdl.getRenderer()),
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());
}
// El resto del estado del juego (física, stages, naves, vidas, puntuación)
// se inicializa en init(), que se llama al final del constructor para que
// la escena esté lista en cuanto el Director la haya construido.
init();
}
auto GameScene::isFinished() const -> bool {
return context_.nextScene() != SceneType::GAME;
}
void GameScene::handleEvent(const SDL_Event& event) {
// GameScene no procesa eventos puntuales SDL: la lógica de input se
// resuelve en update() consultando Input::checkAction.
(void)event;
}
void GameScene::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).
// Se registran en el physics_world para integración cinemática.
// Como su body_.radius=0, no colisionan físicamente con nadie (las
// colisiones de gameplay se gestionan en detectar_col·lisions_*).
for (auto& bullet : bullets_) {
bullet.init();
physics_world_.addBody(&bullet.getBody());
}
// [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) {
// Orquestador delgado: cada paso vive en su propia función para
// mantener update() legible y reducir complejidad cognitiva.
stepPhysics(delta_time);
if (game_over_state_ == GameOverState::NONE) {
stepShootingInput();
stepMidGameJoin();
}
if (stepContinueScreen(delta_time)) {
return;
}
if (stepGameOver(delta_time)) {
return;
}
stepDeathSequence(delta_time);
stepStageStateMachine(delta_time);
}
void GameScene::stepPhysics(float delta_time) {
// Las fuerzas aplicadas en el frame N-1 por processInput/AI se integran
// ahora; postUpdate sincroniza los mirrors (center_/angle_) antes de la
// lógica de juego que los lee.
physics_world_.update(delta_time);
for (auto& ship : ships_) {
ship.postUpdate(delta_time);
}
for (auto& enemy : enemies_) {
enemy.postUpdate(delta_time);
}
for (auto& bullet : bullets_) {
bullet.postUpdate(delta_time);
}
}
void GameScene::stepShootingInput() {
auto* input = Input::get();
if (match_config_.jugador1_actiu &&
input->checkActionPlayer1(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) {
disparar_bala(0);
}
if (match_config_.jugador2_actiu &&
input->checkActionPlayer2(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) {
disparar_bala(1);
}
}
void GameScene::stepMidGameJoin() {
// Permitir join solo durante PLAYING.
if (stage_manager_->get_estat() != StageSystem::EstatStage::PLAYING) {
return;
}
// Solo se permite join si hay al menos un jugador vivo (no se puede
// hacer join en pantalla vacía).
const bool ALGU_VIU =
(match_config_.jugador1_actiu && hit_timer_per_player_[0] != 999.0F) ||
(match_config_.jugador2_actiu && hit_timer_per_player_[1] != 999.0F);
if (!ALGU_VIU) {
return;
}
auto* input = Input::get();
for (uint8_t pid = 0; pid < 2; pid++) {
const bool ACTIU = (pid == 0) ? match_config_.jugador1_actiu
: match_config_.jugador2_actiu;
const bool MUERTO_SIN_VIDAS = hit_timer_per_player_[pid] == 999.0F;
if (ACTIU && !MUERTO_SIN_VIDAS) {
continue; // jugador ya está jugando
}
const bool START_PRESSED = (pid == 0)
? input->checkActionPlayer1(InputAction::START, Input::DO_NOT_ALLOW_REPEAT)
: input->checkActionPlayer2(InputAction::START, Input::DO_NOT_ALLOW_REPEAT);
if (START_PRESSED) {
unir_jugador(pid);
}
}
}
auto GameScene::stepContinueScreen(float delta_time) -> bool {
if (game_over_state_ != GameOverState::CONTINUE) {
return false;
}
Systems::ContinueScreen::Context cont_ctx{
.state = game_over_state_,
.counter = continue_counter_,
.tick_timer = continue_tick_timer_,
.continues_used = continues_used_,
.game_over_timer = game_over_timer_,
.lives_per_player = lives_per_player_,
.score_per_player = score_per_player_,
.hit_timer_per_player = hit_timer_per_player_,
.ships = ships_,
.match_config = match_config_,
.get_spawn_point = [this](uint8_t pid) { return obtenir_punt_spawn(pid); },
};
Systems::ContinueScreen::update(cont_ctx, delta_time);
Systems::ContinueScreen::processInput(cont_ctx);
// Enemies, bullets y efectos siguen moviéndose en background.
for (auto& enemy : enemies_) {
enemy.update(delta_time);
}
for (auto& bullet : bullets_) {
bullet.update(delta_time);
}
debris_manager_.update(delta_time);
floating_score_manager_.update(delta_time);
return true;
}
auto GameScene::stepGameOver(float delta_time) -> bool {
if (game_over_state_ != GameOverState::GAME_OVER) {
return false;
}
game_over_timer_ -= delta_time;
if (game_over_timer_ <= 0.0F) {
Audio::get()->stopMusic();
context_.setNextScene(SceneType::TITLE);
return true;
}
// Enemies, bullets y efectos siguen moviéndose como fondo.
for (auto& enemy : enemies_) {
enemy.update(delta_time);
}
for (auto& bullet : bullets_) {
bullet.update(delta_time);
}
debris_manager_.update(delta_time);
floating_score_manager_.update(delta_time);
return true;
}
void GameScene::stepDeathSequence(float delta_time) {
bool algun_mort = false;
for (uint8_t i = 0; i < 2; i++) {
if (hit_timer_per_player_[i] <= 0.0F || hit_timer_per_player_[i] >= 999.0F) {
continue;
}
algun_mort = true;
hit_timer_per_player_[i] += delta_time;
if (hit_timer_per_player_[i] < Defaults::Game::DEATH_DURATION) {
continue;
}
// *** PHASE 3: RESPAWN OR GAME OVER ***
lives_per_player_[i]--;
if (lives_per_player_[i] > 0) {
Vec2 spawn_pos = obtenir_punt_spawn(i);
ships_[i].init(&spawn_pos, /*activar_invulnerabilitat=*/true);
hit_timer_per_player_[i] = 0.0F;
continue;
}
// Sin vidas: marcar definitivamente muerto y comprobar transición a CONTINUE.
hit_timer_per_player_[i] = 999.0F;
const bool P1_DEAD = !match_config_.jugador1_actiu || lives_per_player_[0] <= 0;
const bool P2_DEAD = !match_config_.jugador2_actiu || lives_per_player_[1] <= 0;
if (P1_DEAD && P2_DEAD) {
game_over_state_ = GameOverState::CONTINUE;
continue_counter_ = Defaults::Game::CONTINUE_COUNT_START;
continue_tick_timer_ = Defaults::Game::CONTINUE_TICK_DURATION;
}
}
// Si hay algún muerto, los enemigos/balas/efectos siguen actualizándose
// aunque otros jugadores aún jueguen.
if (algun_mort) {
for (auto& enemy : enemies_) {
enemy.update(delta_time);
}
for (auto& bullet : bullets_) {
bullet.update(delta_time);
}
debris_manager_.update(delta_time);
floating_score_manager_.update(delta_time);
}
// El bool 'algun_mort' es puramente interno: no aporta nada al caller
// (la stage state machine sigue corriendo aunque haya jugadores muriendo),
// así que la función no devuelve nada.
}
void GameScene::stepStageStateMachine(float delta_time) {
const StageSystem::EstatStage STATE = stage_manager_->get_estat();
switch (STATE) {
case StageSystem::EstatStage::INIT_HUD:
runStageInitHud(delta_time);
break;
case StageSystem::EstatStage::LEVEL_START:
runStageLevelStart(delta_time);
break;
case StageSystem::EstatStage::PLAYING:
runStagePlaying(delta_time);
break;
case StageSystem::EstatStage::LEVEL_COMPLETED:
runStageLevelCompleted(delta_time);
break;
}
}
void GameScene::runStageInitHud(float delta_time) {
// Update stage manager timer (puede cambiar el state).
stage_manager_->update(delta_time);
// Si el state cambió, salir para no usar el timer del nuevo state.
if (stage_manager_->get_estat() != StageSystem::EstatStage::INIT_HUD) {
return;
}
float global_progress = 1.0F - (stage_manager_->get_timer_transicio() / Defaults::Game::INIT_HUD_DURATION);
global_progress = std::min(1.0F, global_progress);
const float SHIP1_P = Systems::InitHud::computeRangeProgress(
global_progress,
Defaults::Game::INIT_HUD_SHIP1_RATIO_INIT,
Defaults::Game::INIT_HUD_SHIP1_RATIO_END);
const float SHIP2_P = Systems::InitHud::computeRangeProgress(
global_progress,
Defaults::Game::INIT_HUD_SHIP2_RATIO_INIT,
Defaults::Game::INIT_HUD_SHIP2_RATIO_END);
if (match_config_.jugador1_actiu && SHIP1_P < 1.0F) {
ships_[0].setCenter(Systems::InitHud::computeShipPosition(SHIP1_P, obtenir_punt_spawn(0)));
}
if (match_config_.jugador2_actiu && SHIP2_P < 1.0F) {
ships_[1].setCenter(Systems::InitHud::computeShipPosition(SHIP2_P, obtenir_punt_spawn(1)));
}
}
void GameScene::runStageLevelStart(float delta_time) {
stage_manager_->update(delta_time);
// Ambas naves pueden moverse y disparar durante el intro.
for (uint8_t i = 0; i < 2; i++) {
const bool ACTIU = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu;
if (ACTIU && hit_timer_per_player_[i] == 0.0F) {
ships_[i].processInput(delta_time, i);
ships_[i].update(delta_time);
}
}
for (auto& bullet : bullets_) {
bullet.update(delta_time);
}
debris_manager_.update(delta_time);
}
void GameScene::runStagePlaying(float delta_time) {
const bool PAUSE_SPAWN = (hit_timer_per_player_[0] > 0.0F && hit_timer_per_player_[1] > 0.0F);
stage_manager_->getSpawnController().update(delta_time, enemies_, PAUSE_SPAWN);
// Stage completado: cuando al menos un jugador está vivo y todos los enemies muertos.
const bool ALGU_VIU = (hit_timer_per_player_[0] == 0.0F || hit_timer_per_player_[1] == 0.0F);
if (ALGU_VIU && stage_manager_->getSpawnController().tots_enemics_destruits(enemies_)) {
stage_manager_->stage_completat();
Audio::get()->playSound(Defaults::Sound::GOOD_JOB_COMMANDER, Audio::Group::GAME);
return;
}
// Gameplay normal: ships activos + entidades + colisiones + efectos.
for (uint8_t i = 0; i < 2; i++) {
const bool ACTIU = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu;
if (ACTIU && hit_timer_per_player_[i] == 0.0F) {
ships_[i].processInput(delta_time, i);
ships_[i].update(delta_time);
}
}
for (auto& enemy : enemies_) {
enemy.update(delta_time);
}
for (auto& bullet : bullets_) {
bullet.update(delta_time);
}
runCollisionDetections();
debris_manager_.update(delta_time);
floating_score_manager_.update(delta_time);
}
void GameScene::runStageLevelCompleted(float delta_time) {
stage_manager_->update(delta_time);
for (uint8_t i = 0; i < 2; i++) {
const bool ACTIU = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu;
if (ACTIU && hit_timer_per_player_[i] == 0.0F) {
ships_[i].processInput(delta_time, i);
ships_[i].update(delta_time);
}
}
for (auto& bullet : bullets_) {
bullet.update(delta_time);
}
debris_manager_.update(delta_time);
floating_score_manager_.update(delta_time);
}
void GameScene::runCollisionDetections() {
Systems::Collision::Context col_ctx{
.ships = ships_,
.enemies = enemies_,
.bullets = bullets_,
.hit_timer_per_player = hit_timer_per_player_,
.score_per_player = score_per_player_,
.lives_per_player = lives_per_player_,
.debris_manager = debris_manager_,
.floating_score_manager = floating_score_manager_,
.match_config = match_config_,
.on_player_hit = [this](uint8_t pid) { tocado(pid); },
};
Systems::Collision::detectAll(col_ctx);
}
void GameScene::draw() {
// 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 = Systems::InitHud::computeRangeProgress(
global_progress,
Defaults::Game::INIT_HUD_RECT_RATIO_INIT,
Defaults::Game::INIT_HUD_RECT_RATIO_END);
float score_progress = Systems::InitHud::computeRangeProgress(
global_progress,
Defaults::Game::INIT_HUD_SCORE_RATIO_INIT,
Defaults::Game::INIT_HUD_SCORE_RATIO_END);
float ship1_progress = Systems::InitHud::computeRangeProgress(
global_progress,
Defaults::Game::INIT_HUD_SHIP1_RATIO_INIT,
Defaults::Game::INIT_HUD_SHIP1_RATIO_END);
float ship2_progress = Systems::InitHud::computeRangeProgress(
global_progress,
Defaults::Game::INIT_HUD_SHIP2_RATIO_INIT,
Defaults::Game::INIT_HUD_SHIP2_RATIO_END);
// 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;
}
Systems::InitHud::drawBordersAnimated(sdl_.getRenderer(), rect_progress);
}
if (score_progress > 0.0F) {
Systems::InitHud::drawScoreboardAnimated(text_, buildScoreboard(), 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
Defaults::Palette::SHIP // Debris hereda color de la nave
);
// 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);
}
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;
}
// [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;
}
}
}
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';
}