Fase 9d: descomponer GameScene::update en sub-pasos privados

update() pasa de 339 LOC monolitico (cognitive complexity ~137) a
18 LOC orquestadores. Cada seccion logica vive en su propio metodo
privado con responsabilidad unica:

- stepPhysics(dt): physics_world.update + postUpdate.
- stepShootingInput(): SHOOT de P1/P2.
- stepMidGameJoin(): START de jugador inactivo o muerto sin vidas.
- stepContinueScreen(dt): wrapping del Systems::ContinueScreen +
  update de fondo. Devuelve true si frame debe terminar.
- stepGameOver(dt): timer final + transicion a TITLE. Devuelve true
  si frame debe terminar.
- stepDeathSequence(dt): death timer/respawn/transicion a CONTINUE.
- stepStageStateMachine(dt): despacha a runStage{InitHud,LevelStart,
  Playing,LevelCompleted} segun el estado actual.
- runCollisionDetections(): construye el Systems::Collision::Context
  y llama detectAll.

GameScene.cpp acumulado tras Fase 9 (a+b+c+d): 1429 -> 1015 LOC.
update() solo: 339 -> 18 LOC. Smoke test xvfb OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 07:57:36 +02:00
parent a4942fcbae
commit 808abb28ea
2 changed files with 287 additions and 295 deletions
+267 -295
View File
@@ -226,10 +226,28 @@ void GameScene::init() {
}
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.
// 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);
@@ -240,331 +258,285 @@ void GameScene::update(float delta_time) {
for (auto& bullet : bullets_) {
bullet.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);
}
}
}
}
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);
}
}
// Handle CONTINUE screen
if (game_over_state_ == GameOverState::CONTINUE) {
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);
// 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);
void GameScene::stepMidGameJoin() {
// Permitir join solo durante PLAYING.
if (stage_manager_->get_estat() != StageSystem::EstatStage::PLAYING) {
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);
// 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;
}
// Check death sequence state for BOTH players
bool algun_jugador_mort = false;
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);
SceneManager::actual = 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;
}
auto GameScene::stepDeathSequence(float delta_time) -> bool {
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) {
algun_jugador_mort = true;
// Death sequence active: update timer
hit_timer_per_player_[i] += delta_time;
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;
// 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 ***
if (hit_timer_per_player_[i] < Defaults::Game::DEATH_DURATION) {
continue;
}
// Decrement lives for this player (only once)
lives_per_player_[i]--;
// *** 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;
}
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;
}
}
}
// 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;
}
}
// If any player is dead, still update enemies/bullets/effects
if (algun_jugador_mort) {
// Enemies and bullets continue moving during death sequence
// 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);
// Don't return - allow alive players to continue playing
}
return algun_mort;
}
// *** 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 = 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);
// [MODIFICAT] Animar AMBAS naves con sus progress respectivos
if (match_config_.jugador1_actiu && ship1_progress < 1.0F) {
Vec2 pos_p1 = Systems::InitHud::computeShipPosition(ship1_progress, obtenir_punt_spawn(0));
ships_[0].setCenter(pos_p1);
}
if (match_config_.jugador2_actiu && ship2_progress < 1.0F) {
Vec2 pos_p2 = Systems::InitHud::computeShipPosition(ship2_progress, obtenir_punt_spawn(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
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: {
// [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);
case StageSystem::EstatStage::LEVEL_START:
runStageLevelStart(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);
}
{
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);
}
debris_manager_.update(delta_time);
floating_score_manager_.update(delta_time);
case StageSystem::EstatStage::PLAYING:
runStagePlaying(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);
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) {