1046 lines
39 KiB
C++
1046 lines
39 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 "core/audio/audio.hpp"
|
|
#include "core/entities/entity_loader.hpp"
|
|
#include "core/input/input.hpp"
|
|
#include "core/locale/locale.hpp"
|
|
#include "core/system/scene_context.hpp"
|
|
#include "core/system/service_menu.hpp"
|
|
#include "game/entities/bullet_registry.hpp"
|
|
#include "game/entities/enemy_registry.hpp"
|
|
#include "game/entities/player_config.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()),
|
|
firework_manager_(sdl.getRenderer()),
|
|
floating_score_manager_(sdl.getRenderer()),
|
|
trail_manager_(sdl.getRenderer()),
|
|
text_(sdl.getRenderer()),
|
|
starfield_parallax_(sdl.getRenderer()),
|
|
playfield_(sdl.getRenderer()),
|
|
border_(sdl.getRenderer()) {
|
|
// 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_.player1_active ? "ACTIU" : "INACTIU")
|
|
<< ", P2: "
|
|
<< (match_config_.player2_active ? "ACTIU" : "INACTIU")
|
|
<< '\n';
|
|
|
|
// Consumir opciones (preparació per MODE_DEMO futur)
|
|
auto option = context_.consumeOption();
|
|
(void)option; // Suprimir warning de variable no usada
|
|
|
|
// Carregar la configuració del player des de YAML. Sense fallback: si
|
|
// falla, abortem (la nau no és construïble sense paràmetres).
|
|
auto player_yaml = Entities::EntityLoader::load("player");
|
|
if (!player_yaml) {
|
|
std::cerr << "[GameScene] FATAL: no s'ha pogut carregar data/entities/player/player.yaml\n";
|
|
std::exit(EXIT_FAILURE);
|
|
}
|
|
auto player_config = PlayerConfig::fromYaml(*player_yaml);
|
|
if (!player_config) {
|
|
std::cerr << "[GameScene] FATAL: player.yaml mal format\n";
|
|
std::exit(EXIT_FAILURE);
|
|
}
|
|
|
|
// Carregar les configuracions dels 3 enemics. Sense fallback: si falla,
|
|
// abortem (els enemics no es poden construir sense els seus paràmetres).
|
|
if (!EnemyRegistry::loadAll()) {
|
|
std::cerr << "[GameScene] FATAL: no s'han pogut carregar els enemics YAML\n";
|
|
std::exit(EXIT_FAILURE);
|
|
}
|
|
|
|
// Carregar la configuració de la bala. Cal abans de construir el pool de
|
|
// bullets, ja que cada Bullet llegeix el registry al seu ctor.
|
|
if (!BulletRegistry::load()) {
|
|
std::cerr << "[GameScene] FATAL: no s'ha pogut carregar bullet.yaml\n";
|
|
std::exit(EXIT_FAILURE);
|
|
}
|
|
|
|
// Inicialitzar naves: P1 amb el shape del YAML, P2 amb override visual.
|
|
ships_[0] = Ship(sdl.getRenderer(), *player_config); // Jugador 1: nau estàndard
|
|
ships_[1] = Ship(sdl.getRenderer(), *player_config, "ship2.shp"); // Jugador 2: interceptor amb ales
|
|
|
|
// Inicialitzar balas con renderer
|
|
std::ranges::fill(bullets_, Bullet(sdl.getRenderer()));
|
|
|
|
// Inicialitzar enemigos con renderer
|
|
std::ranges::fill(enemies_, Enemy(sdl.getRenderer()));
|
|
|
|
// 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 zone de juego.
|
|
physics_world_.clear();
|
|
physics_world_.setBounds(Defaults::Zones::PLAYAREA);
|
|
|
|
// Connectar els impactes contra les parets al border (bump + flash).
|
|
physics_world_.setBoundsHitCallback([this](const Physics::BoundsHit& hit) {
|
|
if (hit.impact_speed < Defaults::Border::BUMP_MIN_VELOCITY) {
|
|
return;
|
|
}
|
|
const float STRENGTH = std::min(
|
|
1.0F,
|
|
hit.impact_speed / Defaults::Border::BUMP_VELOCITY_REFERENCE);
|
|
border_.bumpAt(hit.contact_point, STRENGTH);
|
|
});
|
|
|
|
// Fireworks generen una ripple gran al playfield (ona d'aigua centrada al burst).
|
|
firework_manager_.setSpawnCallback([this](Vec2 origin) {
|
|
playfield_.notifyExplosion(origin);
|
|
});
|
|
|
|
// Explosions properes a una paret també generen bump (falloff lineal amb la distància).
|
|
debris_manager_.setExplosionCallback([this](Vec2 center) {
|
|
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
|
const float DIST_LEFT = std::abs(center.x - zone.x);
|
|
const float DIST_RIGHT = std::abs((zone.x + zone.w) - center.x);
|
|
const float DIST_TOP = std::abs(center.y - zone.y);
|
|
const float DIST_BOTTOM = std::abs((zone.y + zone.h) - center.y);
|
|
const float MIN_DIST = std::min({DIST_LEFT, DIST_RIGHT, DIST_TOP, DIST_BOTTOM});
|
|
if (MIN_DIST > Defaults::Border::EXPLOSION_FALLOFF_PX) {
|
|
return;
|
|
}
|
|
const float FALLOFF = 1.0F - (MIN_DIST / Defaults::Border::EXPLOSION_FALLOFF_PX);
|
|
border_.bumpAt(center, Defaults::Border::EXPLOSION_BASE_STRENGTH * FALLOFF);
|
|
});
|
|
|
|
// Load stage configuration
|
|
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)
|
|
}
|
|
|
|
// Initialize stage manager
|
|
stage_manager_ = std::make_unique<StageSystem::StageManager>(stage_config_.get());
|
|
stage_manager_->init();
|
|
|
|
// 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();
|
|
|
|
// Inicialitzar naves segons configuración (solo jugadors active)
|
|
for (uint8_t i = 0; i < 2; i++) {
|
|
bool player_active = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
|
|
|
|
if (player_active) {
|
|
// Jugador active: init normalment
|
|
Vec2 spawn_pos = getSpawnPoint(i);
|
|
ships_[i].init(&spawn_pos, false); // No invulnerability at start
|
|
// Registrar el cuerpo físico de la nave en el mundo
|
|
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] = Defaults::Game::HIT_TIMER_INACTIVE_PLAYER;
|
|
lives_per_player_[i] = 0; // Sin vides
|
|
std::cout << "[GameScene] Jugador " << (i + 1) << " inactiu\n";
|
|
}
|
|
}
|
|
|
|
// 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.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.
|
|
// 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());
|
|
}
|
|
|
|
// Reset flag de sons de animación
|
|
init_hud_rect_sound_played_ = false;
|
|
}
|
|
|
|
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::update(float delta_time) {
|
|
// Pausa global: mentre el menu de servei esta obert, congelem la lògica
|
|
// de joc. El draw() segueix executant-se per a mantenir l'escena visible
|
|
// sota el menu.
|
|
if (const auto* menu = System::ServiceMenu::get(); menu != nullptr && menu->isOpen()) {
|
|
return;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
trail_manager_.update(delta_time, ships_);
|
|
// Starfield: world_velocity = -mitjana_de_naus_actives. Si dues naus van en
|
|
// sentits oposats, es cancel·len → estrelles quietes (cap jugador "guanya").
|
|
// Si només n'hi ha una activa, segueix la seva velocitat.
|
|
Vec2 ship_vel_avg{.x = 0.0F, .y = 0.0F};
|
|
int n_active = 0;
|
|
for (const auto& ship : ships_) {
|
|
if (ship.isActive()) {
|
|
const Vec2 V = ship.getVelocityVector();
|
|
ship_vel_avg.x += V.x;
|
|
ship_vel_avg.y += V.y;
|
|
n_active++;
|
|
}
|
|
}
|
|
if (n_active > 0) {
|
|
ship_vel_avg.x /= static_cast<float>(n_active);
|
|
ship_vel_avg.y /= static_cast<float>(n_active);
|
|
}
|
|
starfield_parallax_.update(delta_time, Vec2{.x = -ship_vel_avg.x, .y = -ship_vel_avg.y});
|
|
playfield_.update(delta_time);
|
|
border_.update(delta_time);
|
|
|
|
// Notificar al playfield que la nau es mou (genera ripples petites a cadència).
|
|
for (std::size_t id = 0; id < ships_.size(); id++) {
|
|
if (ships_[id].isActive()) {
|
|
playfield_.notifyShipMoving(static_cast<std::uint8_t>(id),
|
|
ships_[id].getCenter(),
|
|
ships_[id].getSpeed(),
|
|
delta_time);
|
|
}
|
|
}
|
|
}
|
|
|
|
void GameScene::stepShootingInput() {
|
|
auto* input = Input::get();
|
|
if (match_config_.player1_active &&
|
|
input->checkActionPlayer1(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) {
|
|
fireBullet(0);
|
|
}
|
|
if (match_config_.player2_active &&
|
|
input->checkActionPlayer2(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) {
|
|
fireBullet(1);
|
|
}
|
|
}
|
|
|
|
void GameScene::stepMidGameJoin() {
|
|
// Permitir join solo durante PLAYING.
|
|
if (stage_manager_->getState() != 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_.player1_active && hit_timer_per_player_[0] != Defaults::Game::HIT_TIMER_INACTIVE_PLAYER) ||
|
|
(match_config_.player2_active && hit_timer_per_player_[1] != Defaults::Game::HIT_TIMER_INACTIVE_PLAYER);
|
|
if (!ALGU_VIU) {
|
|
return;
|
|
}
|
|
|
|
auto* input = Input::get();
|
|
for (uint8_t pid = 0; pid < 2; pid++) {
|
|
const bool ACTIU = (pid == 0) ? match_config_.player1_active
|
|
: match_config_.player2_active;
|
|
const bool MUERTO_SIN_VIDAS = hit_timer_per_player_[pid] == Defaults::Game::HIT_TIMER_INACTIVE_PLAYER;
|
|
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) {
|
|
joinPlayer(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 getSpawnPoint(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);
|
|
}
|
|
Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_);
|
|
debris_manager_.update(delta_time);
|
|
firework_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);
|
|
}
|
|
Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_);
|
|
debris_manager_.update(delta_time);
|
|
firework_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] >= Defaults::Game::HIT_TIMER_INACTIVE_PLAYER) {
|
|
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 = getSpawnPoint(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] = Defaults::Game::HIT_TIMER_INACTIVE_PLAYER;
|
|
const bool P1_DEAD = !match_config_.player1_active || lives_per_player_[0] <= 0;
|
|
const bool P2_DEAD = !match_config_.player2_active || 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);
|
|
firework_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_->getState();
|
|
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_->getState() != StageSystem::EstatStage::INIT_HUD) {
|
|
return;
|
|
}
|
|
|
|
float global_progress = 1.0F - (stage_manager_->getTransitionTimer() / 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_.player1_active && SHIP1_P < 1.0F) {
|
|
ships_[0].setCenter(Systems::InitHud::computeShipPosition(SHIP1_P, getSpawnPoint(0)));
|
|
}
|
|
if (match_config_.player2_active && SHIP2_P < 1.0F) {
|
|
ships_[1].setCenter(Systems::InitHud::computeShipPosition(SHIP2_P, getSpawnPoint(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_.player1_active : match_config_.player2_active;
|
|
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);
|
|
}
|
|
Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_);
|
|
debris_manager_.update(delta_time);
|
|
firework_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().allEnemiesDestroyed(enemies_)) {
|
|
stage_manager_->markStageCompleted();
|
|
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_.player1_active : match_config_.player2_active;
|
|
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);
|
|
}
|
|
|
|
// Col·lisions primer, després desactivació per fora-de-zone: així una bala que
|
|
// el mateix frame xoca amb un enemic i alhora surt del PLAYAREA es compta com a
|
|
// impacte abans no se la trenqui per sortir.
|
|
runCollisionDetections();
|
|
for (auto& bullet : bullets_) {
|
|
bullet.update(delta_time);
|
|
}
|
|
Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_);
|
|
debris_manager_.update(delta_time);
|
|
firework_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_.player1_active : match_config_.player2_active;
|
|
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);
|
|
}
|
|
Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_);
|
|
debris_manager_.update(delta_time);
|
|
firework_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_,
|
|
.firework_manager = firework_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() {
|
|
if (game_over_state_ == GameOverState::CONTINUE) {
|
|
drawContinueState();
|
|
return;
|
|
}
|
|
|
|
if (game_over_state_ == GameOverState::GAME_OVER) {
|
|
drawGameOverState();
|
|
return;
|
|
}
|
|
|
|
switch (stage_manager_->getState()) {
|
|
case StageSystem::EstatStage::INIT_HUD:
|
|
drawInitHudState();
|
|
break;
|
|
case StageSystem::EstatStage::LEVEL_START:
|
|
drawLevelStartState();
|
|
break;
|
|
case StageSystem::EstatStage::PLAYING:
|
|
drawPlayingState();
|
|
break;
|
|
case StageSystem::EstatStage::LEVEL_COMPLETED:
|
|
drawLevelCompletedState();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void GameScene::drawEnemies() const {
|
|
for (const auto& enemy : enemies_) {
|
|
enemy.draw();
|
|
}
|
|
}
|
|
|
|
void GameScene::drawBullets() const {
|
|
for (const auto& bullet : bullets_) {
|
|
bullet.draw();
|
|
}
|
|
}
|
|
|
|
void GameScene::drawActiveShipsAlive() const {
|
|
for (uint8_t i = 0; i < 2; i++) {
|
|
bool player_active = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
|
|
if (player_active && hit_timer_per_player_[i] == 0.0F) {
|
|
ships_[i].draw();
|
|
}
|
|
}
|
|
}
|
|
|
|
void GameScene::drawContinueState() {
|
|
starfield_parallax_.draw();
|
|
border_.draw();
|
|
drawEnemies();
|
|
drawBullets();
|
|
debris_manager_.draw();
|
|
firework_manager_.draw();
|
|
floating_score_manager_.draw();
|
|
drawScoreboard();
|
|
drawContinue();
|
|
}
|
|
|
|
void GameScene::drawGameOverState() {
|
|
starfield_parallax_.draw();
|
|
border_.draw();
|
|
drawEnemies();
|
|
drawBullets();
|
|
debris_manager_.draw();
|
|
firework_manager_.draw();
|
|
floating_score_manager_.draw();
|
|
|
|
const std::string GAME_OVER_TEXT = Locale::get().text("game_screen.game_over");
|
|
constexpr float SCALE = Defaults::Game::GameOverScreen::TEXT_SCALE;
|
|
constexpr float SPACING = Defaults::Game::GameOverScreen::TEXT_SPACING;
|
|
|
|
const SDL_FRect& play_area = Defaults::Zones::PLAYAREA;
|
|
float center_x = play_area.x + (play_area.w / 2.0F);
|
|
float center_y = play_area.y + (play_area.h / 2.0F);
|
|
|
|
text_.renderCentered(GAME_OVER_TEXT, {.x = center_x, .y = center_y}, SCALE, SPACING);
|
|
|
|
drawScoreboard();
|
|
}
|
|
|
|
void GameScene::drawInitHudState() {
|
|
float timer = stage_manager_->getTransitionTimer();
|
|
float total_time = Defaults::Game::INIT_HUD_DURATION;
|
|
float global_progress = 1.0F - (timer / total_time);
|
|
|
|
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);
|
|
|
|
// Capa de fons més profunda: estrelles 2D (apareixen senceres des del frame 0).
|
|
starfield_parallax_.draw();
|
|
// Graella de fons al darrere (timer intern propi, cobreix tot l'INIT_HUD).
|
|
playfield_.draw();
|
|
|
|
if (rect_progress > 0.0F) {
|
|
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);
|
|
}
|
|
|
|
if (ship1_progress > 0.0F && match_config_.player1_active && ships_[0].isActive()) {
|
|
ships_[0].draw();
|
|
}
|
|
|
|
if (ship2_progress > 0.0F && match_config_.player2_active && ships_[1].isActive()) {
|
|
ships_[1].draw();
|
|
}
|
|
}
|
|
|
|
void GameScene::drawLevelStartState() {
|
|
starfield_parallax_.draw();
|
|
playfield_.draw();
|
|
border_.draw();
|
|
trail_manager_.draw();
|
|
drawActiveShipsAlive();
|
|
drawBullets();
|
|
debris_manager_.draw();
|
|
firework_manager_.draw();
|
|
floating_score_manager_.draw();
|
|
drawStageMessage(stage_manager_->getLevelStartMessage());
|
|
drawScoreboard();
|
|
}
|
|
|
|
void GameScene::drawPlayingState() {
|
|
starfield_parallax_.draw();
|
|
playfield_.draw();
|
|
border_.draw();
|
|
trail_manager_.draw();
|
|
drawActiveShipsAlive();
|
|
drawEnemies();
|
|
drawBullets();
|
|
debris_manager_.draw();
|
|
firework_manager_.draw();
|
|
floating_score_manager_.draw();
|
|
drawScoreboard();
|
|
}
|
|
|
|
void GameScene::drawLevelCompletedState() {
|
|
starfield_parallax_.draw();
|
|
playfield_.draw();
|
|
border_.draw();
|
|
trail_manager_.draw();
|
|
drawActiveShipsAlive();
|
|
drawBullets();
|
|
debris_manager_.draw();
|
|
firework_manager_.draw();
|
|
floating_score_manager_.draw();
|
|
drawStageMessage(Locale::get().text("stage.completed"));
|
|
drawScoreboard();
|
|
}
|
|
|
|
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 ***
|
|
|
|
// Capturar velocitat ABANS del markHit (que la reseteja a zero).
|
|
// Sense això, els debris no hereten cap inèrcia de la nau.
|
|
const Vec2 SHIP_VEL_PRE_DEATH = ships_[player_id].getVelocityVector();
|
|
const Vec2 SHIP_POS = ships_[player_id].getCenter();
|
|
const float SHIP_ANGLE = ships_[player_id].getAngle();
|
|
const float SHIP_BRIGHT = ships_[player_id].getBrightness();
|
|
|
|
// Mark ship as dead (stops rendering and input)
|
|
ships_[player_id].markHit();
|
|
|
|
const Vec2 INHERITED_VEL = SHIP_VEL_PRE_DEATH *
|
|
Defaults::Physics::Debris::SHIP_VELOCITY_INHERITANCE;
|
|
|
|
// Mateixa dispersió i efecte que els debris d'enemic (lifetime,
|
|
// friction, segment_multiplier alineats); només canvien sound i color.
|
|
debris_manager_.explode(
|
|
ships_[player_id].getShape(),
|
|
SHIP_POS,
|
|
SHIP_ANGLE,
|
|
1.0F,
|
|
Defaults::Physics::Debris::SPEED_BASE,
|
|
SHIP_BRIGHT,
|
|
INHERITED_VEL,
|
|
0.0F, // sense herència angular
|
|
0.0F, // sin herencia visual
|
|
Defaults::Sound::EXPLOSION2,
|
|
ships_[player_id].getConfig().colors.normal,
|
|
Defaults::Physics::Debris::ENEMY_LIFETIME,
|
|
Defaults::Physics::Debris::ENEMY_FRICTION,
|
|
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER);
|
|
|
|
// Start death timer (non-zero to avoid re-triggering)
|
|
hit_timer_per_player_[player_id] = Defaults::Game::HIT_TIMER_TRIGGER_DEATH;
|
|
}
|
|
// Phase 2 is automatic (debris updates in update())
|
|
// Phase 3 is handled in update() when hit_timer_per_player_ >= DEATH_DURATION
|
|
}
|
|
|
|
void GameScene::drawScoreboard() {
|
|
// Construir text del marcador
|
|
std::string text = buildScoreboard();
|
|
|
|
// Parámetros de renderització
|
|
const float SCALE = Defaults::Hud::SCOREBOARD_TEXT_SCALE;
|
|
const float SPACING = Defaults::Hud::SCOREBOARD_TEXT_SPACING;
|
|
|
|
// Calcular centro de la zone del marcador
|
|
const SDL_FRect& scoreboard_zone = Defaults::Zones::SCOREBOARD;
|
|
float center_x = scoreboard_zone.w / 2.0F;
|
|
float center_y = scoreboard_zone.y + (scoreboard_zone.h / 2.0F);
|
|
|
|
// Renderizar centrat
|
|
text_.renderCentered(text, {.x = center_x, .y = center_y}, SCALE, SPACING);
|
|
}
|
|
|
|
auto GameScene::buildScoreboard() const -> std::string {
|
|
// Puntuación P1 (6 dígits) - mostrar zeros si inactiu
|
|
std::string score_p1;
|
|
std::string vides_p1;
|
|
if (match_config_.player1_active) {
|
|
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_->getCurrentStage();
|
|
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_.player2_active) {
|
|
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 + " " + Locale::get().text("hud.level") + stage_str + " " + score_p2 + " " + vides_p2;
|
|
}
|
|
|
|
// [NEW] Stage system helper methods
|
|
|
|
void GameScene::drawStageMessage(const std::string& message) {
|
|
constexpr float BASE_SCALE = 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_->getState() == 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_->getTransitionTimer();
|
|
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 = Graphics::VectorText::getTextWidth(message, BASE_SCALE, SPACING);
|
|
|
|
// Auto-scale if text exceeds max width
|
|
float scale = (text_width_at_base <= MAX_WIDTH)
|
|
? BASE_SCALE
|
|
: MAX_WIDTH / text_width_at_base;
|
|
|
|
// Recalculate dimensions with final scale (using FULL message for centering)
|
|
float full_text_width = Graphics::VectorText::getTextWidth(message, scale, SPACING);
|
|
float text_height = Graphics::VectorText::getTextHeight(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) amb el color
|
|
// ambre neon del "PRESS START" del títol — unifica el feel dels missatges.
|
|
Vec2 pos = {.x = x, .y = y};
|
|
text_.render(partial_message, pos, scale, SPACING, 1.0F, Defaults::Title::Colors::PRESS_START);
|
|
}
|
|
|
|
// ========================================
|
|
// Helper methods for 2-player support
|
|
// ========================================
|
|
|
|
auto GameScene::getSpawnPoint(uint8_t player_id) const -> Vec2 {
|
|
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
|
|
|
|
float x_ratio;
|
|
if (match_config_.isSinglePlayer()) {
|
|
// 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 = zone.x + (zone.w * x_ratio),
|
|
.y = zone.y + (zone.h * Defaults::Game::SPAWN_Y_RATIO)};
|
|
}
|
|
|
|
void GameScene::fireBullet(uint8_t player_id) {
|
|
// Verificar que el player está vivo
|
|
if (hit_timer_per_player_[player_id] > 0.0F) {
|
|
return;
|
|
}
|
|
if (!ships_[player_id].isActive()) {
|
|
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 = Defaults::Hud::Tips::LOCAL_X;
|
|
constexpr float LOCAL_TIP_Y = Defaults::Hud::Tips::LOCAL_Y;
|
|
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 fire_position = {.x = tip_x, .y = tip_y};
|
|
|
|
// Buscar primera bullet inactiva en el pool del player.
|
|
// El pool global té MAX_BULLETS slots per jugador (P1=[0..MAX-1], P2=[MAX..2*MAX-1]).
|
|
constexpr int SLOTS_PER_PLAYER = Defaults::Entities::MAX_BULLETS;
|
|
const int START_IDX = player_id * SLOTS_PER_PLAYER;
|
|
for (int i = START_IDX; i < START_IDX + SLOTS_PER_PLAYER; i++) {
|
|
if (!bullets_[i].isActive()) {
|
|
bullets_[i].fire(fire_position, ship_angle, player_id, ships_[player_id].getConfig().weapon.bullet_speed);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void GameScene::drawContinue() {
|
|
const SDL_FRect& play_area = Defaults::Zones::PLAYAREA;
|
|
constexpr float SPACING = 4.0F;
|
|
|
|
// "CONTINUE" text (using constants)
|
|
const std::string CONTINUE_TEXT = Locale::get().text("game_screen.continue");
|
|
float escala_continue = Defaults::Game::ContinueScreen::CONTINUE_TEXT_SCALE;
|
|
float y_ratio_continue = Defaults::Game::ContinueScreen::CONTINUE_TEXT_Y_RATIO;
|
|
|
|
float center_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 = center_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 = center_x, .y = centre_y_counter}, escala_counter, SPACING);
|
|
|
|
// "CONTINUES LEFT" (conditional + using constants)
|
|
if (!Defaults::Game::INFINITE_CONTINUES) {
|
|
const std::string CONTINUES_TEXT = localeSubstitute(
|
|
Locale::get().text("game_screen.continues_left"),
|
|
"{n}",
|
|
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 = center_x, .y = centre_y_info}, escala_info, SPACING);
|
|
}
|
|
}
|
|
|
|
void GameScene::joinPlayer(uint8_t player_id) {
|
|
// Activate player
|
|
if (player_id == 0) {
|
|
match_config_.player1_active = true;
|
|
} else {
|
|
match_config_.player2_active = 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 = getSpawnPoint(player_id);
|
|
ships_[player_id].init(&spawn_pos, true);
|
|
|
|
// Registrar el cos físic al món. Si el jugador començà inactiu, el
|
|
// constructor no l'havia afegit; sense això, applyForce s'acumula
|
|
// però mai s'integra → la nau no es desplaça.
|
|
physics_world_.addBody(&ships_[player_id].getBody());
|
|
|
|
std::cout << "[GameScene] Jugador " << (int)(player_id + 1) << " s'ha unit a la match!" << '\n';
|
|
}
|