- Agregar paréntesis explícitos en operaciones matemáticas para claridad
- Ejemplos: '1.0F - a * b' → '1.0F - (a * b)'
- 291 correcciones aplicadas automáticamente con clang-tidy
- Check 2/N completado
🤖 Generated with Claude Code
1430 lines
54 KiB
C++
1430 lines
54 KiB
C++
// escena_joc.cpp - Implementació de la lògica del joc
|
|
// © 1999 Visente i Sergi (versió Pascal)
|
|
// © 2025 Port a C++20 amb SDL3
|
|
|
|
#include "escena_joc.hpp"
|
|
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <ctime>
|
|
#include <iostream>
|
|
#include <vector>
|
|
|
|
#include "core/audio/audio.hpp"
|
|
#include "core/input/input.hpp"
|
|
#include "core/input/mouse.hpp"
|
|
#include "core/math/easing.hpp"
|
|
#include "core/rendering/line_renderer.hpp"
|
|
#include "core/system/context_escenes.hpp"
|
|
#include "core/system/global_events.hpp"
|
|
#include "game/stage_system/stage_loader.hpp"
|
|
|
|
// Using declarations per simplificar el codi
|
|
using GestorEscenes::ContextEscenes;
|
|
using Escena = ContextEscenes::Escena;
|
|
using Opcio = ContextEscenes::Opcio;
|
|
|
|
EscenaJoc::EscenaJoc(SDLManager& sdl, ContextEscenes& context)
|
|
: sdl_(sdl),
|
|
context_(context),
|
|
debris_manager_(sdl.obte_renderer()),
|
|
gestor_puntuacio_(sdl.obte_renderer()),
|
|
text_(sdl.obte_renderer()),
|
|
init_hud_rect_sound_played_(false) {
|
|
// Recuperar configuració de partida des del context
|
|
config_partida_ = context_.get_config_partida();
|
|
|
|
// Debug output de la configuració
|
|
std::cout << "[EscenaJoc] Configuració de partida - P1: "
|
|
<< (config_partida_.jugador1_actiu ? "ACTIU" : "INACTIU")
|
|
<< ", P2: "
|
|
<< (config_partida_.jugador2_actiu ? "ACTIU" : "INACTIU")
|
|
<< std::endl;
|
|
|
|
// Consumir opcions (preparació per MODE_DEMO futur)
|
|
auto opcio = context_.consumir_opcio();
|
|
(void)opcio; // Suprimir warning de variable no usada
|
|
|
|
// Inicialitzar naus amb renderer (P1=ship.shp, P2=ship2.shp)
|
|
naus_[0] = Nau(sdl.obte_renderer(), "ship.shp"); // Jugador 1: nave estàndar
|
|
naus_[1] = Nau(sdl.obte_renderer(), "ship2.shp"); // Jugador 2: interceptor amb ales
|
|
|
|
// Inicialitzar bales amb renderer
|
|
for (auto& bala : bales_) {
|
|
bala = Bala(sdl.obte_renderer());
|
|
}
|
|
|
|
// Inicialitzar enemics amb renderer
|
|
for (auto& enemy : orni_) {
|
|
enemy = Enemic(sdl.obte_renderer());
|
|
}
|
|
}
|
|
|
|
void EscenaJoc::executar() {
|
|
std::cout << "Escena Joc: Inicialitzant...\n";
|
|
|
|
// Inicialitzar estat del joc
|
|
inicialitzar();
|
|
|
|
SDL_Event event;
|
|
Uint64 last_time = SDL_GetTicks();
|
|
|
|
while (GestorEscenes::actual == Escena::JOC) {
|
|
// Calcular delta_time real
|
|
Uint64 current_time = SDL_GetTicks();
|
|
float delta_time = (current_time - last_time) / 1000.0F;
|
|
last_time = current_time;
|
|
|
|
// Limitar delta_time per evitar grans salts
|
|
if (delta_time > 0.05F) {
|
|
delta_time = 0.05F;
|
|
}
|
|
|
|
// Actualitzar comptador de FPS
|
|
sdl_.updateFPS(delta_time);
|
|
|
|
// Actualitzar visibilitat del cursor (auto-ocultar)
|
|
Mouse::updateCursorVisibility();
|
|
|
|
// Actualitzar sistema d'input ABANS del event loop
|
|
Input::get()->update();
|
|
|
|
// Processar events SDL
|
|
while (SDL_PollEvent(&event)) {
|
|
// Manejo de finestra
|
|
if (sdl_.handleWindowEvent(event)) {
|
|
continue;
|
|
}
|
|
|
|
// Events globals (F1/F2/F3/ESC/QUIT)
|
|
GlobalEvents::handle(event, sdl_, context_);
|
|
}
|
|
|
|
// Actualitzar física del joc amb delta_time real
|
|
actualitzar(delta_time);
|
|
|
|
// Actualitzar sistema d'audio
|
|
Audio::update();
|
|
|
|
// Actualitzar colors oscil·lats
|
|
sdl_.updateColors(delta_time);
|
|
|
|
// Netejar pantalla (usa color oscil·lat)
|
|
sdl_.neteja(0, 0, 0);
|
|
|
|
// Actualitzar context de renderitzat (factor d'escala global)
|
|
sdl_.updateRenderingContext();
|
|
|
|
// Dibuixar joc
|
|
dibuixar();
|
|
|
|
// Presentar renderer (swap buffers)
|
|
sdl_.presenta();
|
|
}
|
|
|
|
std::cout << "Escena Joc: Finalitzant...\n";
|
|
}
|
|
|
|
void EscenaJoc::inicialitzar() {
|
|
// Inicialitzar generador de números aleatoris
|
|
// Basat en el codi Pascal original: line 376
|
|
std::srand(static_cast<unsigned>(std::time(nullptr)));
|
|
|
|
// [NEW] Load stage configuration (only once)
|
|
if (!stage_config_) {
|
|
stage_config_ = StageSystem::StageLoader::carregar("data/stages/stages.yaml");
|
|
if (!stage_config_) {
|
|
std::cerr << "[EscenaJoc] Error: no s'ha pogut carregar stages.yaml" << std::endl;
|
|
// 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_->inicialitzar();
|
|
|
|
// [NEW] Set ship position reference for safe spawn (P1 for now, TODO: dual tracking)
|
|
stage_manager_->get_spawn_controller().set_ship_position(&naus_[0].get_centre());
|
|
|
|
// Inicialitzar timers de muerte per jugador
|
|
itocado_per_jugador_[0] = 0.0F;
|
|
itocado_per_jugador_[1] = 0.0F;
|
|
|
|
// Initialize lives and game over state (independent lives per player)
|
|
vides_per_jugador_[0] = Defaults::Game::STARTING_LIVES;
|
|
vides_per_jugador_[1] = Defaults::Game::STARTING_LIVES;
|
|
estat_game_over_ = EstatGameOver::NONE;
|
|
continue_counter_ = 0;
|
|
continue_tick_timer_ = 0.0F;
|
|
continues_usados_ = 0;
|
|
game_over_timer_ = 0.0F;
|
|
|
|
// Initialize scores (separate per player)
|
|
puntuacio_per_jugador_[0] = 0;
|
|
puntuacio_per_jugador_[1] = 0;
|
|
gestor_puntuacio_.reiniciar();
|
|
|
|
// DEPRECATED: punt_spawn_ ja no s'usa, es calcula dinàmicament amb obtenir_punt_spawn(player_id)
|
|
// const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
|
|
// punt_spawn_.x = zona.x + zona.w * 0.5f;
|
|
// punt_spawn_.y = zona.y + zona.h * Defaults::Game::INIT_HUD_SHIP_START_Y_RATIO;
|
|
|
|
// Inicialitzar naus segons configuració (només jugadors actius)
|
|
for (uint8_t i = 0; i < 2; i++) {
|
|
bool jugador_actiu = (i == 0) ? config_partida_.jugador1_actiu : config_partida_.jugador2_actiu;
|
|
|
|
if (jugador_actiu) {
|
|
// Jugador actiu: inicialitzar normalment
|
|
Punt spawn_pos = obtenir_punt_spawn(i);
|
|
naus_[i].inicialitzar(&spawn_pos, false); // No invulnerability at start
|
|
std::cout << "[EscenaJoc] Jugador " << (i + 1) << " inicialitzat\n";
|
|
} else {
|
|
// Jugador inactiu: marcar com a mort permanent
|
|
naus_[i].marcar_tocada();
|
|
itocado_per_jugador_[i] = 999.0F; // Valor sentinella (permanent inactiu)
|
|
vides_per_jugador_[i] = 0; // Sense vides
|
|
std::cout << "[EscenaJoc] Jugador " << (i + 1) << " inactiu\n";
|
|
}
|
|
}
|
|
|
|
// [MODIFIED] Initialize enemies as inactive (stage system will spawn them)
|
|
for (auto& enemy : orni_) {
|
|
enemy = Enemic(sdl_.obte_renderer());
|
|
enemy.set_ship_position(&naus_[0].get_centre()); // Set ship reference (P1 for now)
|
|
// DON'T call enemy.inicialitzar() here - stage system handles spawning
|
|
}
|
|
|
|
// Inicialitzar bales (now 6 instead of 3)
|
|
for (auto& bala : bales_) {
|
|
bala.inicialitzar();
|
|
}
|
|
|
|
// [ELIMINAT] Iniciar música de joc (ara es gestiona en stage_manager)
|
|
// La música s'inicia quan es transiciona de INIT_HUD a LEVEL_START
|
|
// Audio::get()->playMusic("game.ogg");
|
|
|
|
// Reset flag de sons d'animació
|
|
init_hud_rect_sound_played_ = false;
|
|
}
|
|
|
|
void EscenaJoc::actualitzar(float delta_time) {
|
|
// Processar disparos (state-based, no event-based)
|
|
if (estat_game_over_ == EstatGameOver::NONE) {
|
|
auto* input = Input::get();
|
|
|
|
// Jugador 1 dispara (només si està actiu)
|
|
if (config_partida_.jugador1_actiu) {
|
|
if (input->checkActionPlayer1(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) {
|
|
disparar_bala(0);
|
|
}
|
|
}
|
|
|
|
// Jugador 2 dispara (només si està actiu)
|
|
if (config_partida_.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 (config_partida_.jugador1_actiu && itocado_per_jugador_[0] != 999.0F) {
|
|
algun_jugador_viu = true;
|
|
}
|
|
if (config_partida_.jugador2_actiu && itocado_per_jugador_[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 = !config_partida_.jugador2_actiu || // Never joined
|
|
itocado_per_jugador_[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 = !config_partida_.jugador1_actiu || // Never joined
|
|
itocado_per_jugador_[0] == 999.0F; // Dead without lives
|
|
|
|
if (p1_no_juga) {
|
|
if (input->checkActionPlayer1(InputAction::START, Input::DO_NOT_ALLOW_REPEAT)) {
|
|
unir_jugador(0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle CONTINUE screen
|
|
if (estat_game_over_ == EstatGameOver::CONTINUE) {
|
|
actualitzar_continue(delta_time);
|
|
processar_input_continue();
|
|
|
|
// Still update enemies, bullets, and effects during continue screen
|
|
for (auto& enemy : orni_) {
|
|
enemy.actualitzar(delta_time);
|
|
}
|
|
for (auto& bala : bales_) {
|
|
bala.actualitzar(delta_time);
|
|
}
|
|
debris_manager_.actualitzar(delta_time);
|
|
gestor_puntuacio_.actualitzar(delta_time);
|
|
return;
|
|
}
|
|
|
|
// Handle final GAME OVER state
|
|
if (estat_game_over_ == EstatGameOver::GAME_OVER) {
|
|
// Game over: only update timer, enemies, bullets, and debris
|
|
game_over_timer_ -= delta_time;
|
|
|
|
if (game_over_timer_ <= 0.0F) {
|
|
// Aturar música de joc abans de tornar al títol
|
|
Audio::get()->stopMusic();
|
|
// Transició a pantalla de títol
|
|
context_.canviar_escena(Escena::TITOL);
|
|
GestorEscenes::actual = Escena::TITOL;
|
|
return;
|
|
}
|
|
|
|
// Enemies and bullets continue moving during game over
|
|
for (auto& enemy : orni_) {
|
|
enemy.actualitzar(delta_time);
|
|
}
|
|
|
|
for (auto& bala : bales_) {
|
|
bala.actualitzar(delta_time);
|
|
}
|
|
|
|
debris_manager_.actualitzar(delta_time);
|
|
gestor_puntuacio_.actualitzar(delta_time);
|
|
return;
|
|
}
|
|
|
|
// Check death sequence state for BOTH players
|
|
bool algun_jugador_mort = false;
|
|
for (uint8_t i = 0; i < 2; i++) {
|
|
if (itocado_per_jugador_[i] > 0.0F && itocado_per_jugador_[i] < 999.0F) {
|
|
algun_jugador_mort = true;
|
|
// Death sequence active: update timer
|
|
itocado_per_jugador_[i] += delta_time;
|
|
|
|
// Check if death duration completed (only trigger ONCE using sentinel value)
|
|
if (itocado_per_jugador_[i] >= Defaults::Game::DEATH_DURATION) {
|
|
// *** PHASE 3: RESPAWN OR GAME OVER ***
|
|
|
|
// Decrement lives for this player (only once)
|
|
vides_per_jugador_[i]--;
|
|
|
|
if (vides_per_jugador_[i] > 0) {
|
|
// Respawn ship en spawn position con invulnerabilidad
|
|
Punt spawn_pos = obtenir_punt_spawn(i);
|
|
naus_[i].inicialitzar(&spawn_pos, true);
|
|
itocado_per_jugador_[i] = 0.0F;
|
|
} else {
|
|
// Player is permanently dead (out of lives)
|
|
// Set sentinel value to prevent re-entering this block
|
|
itocado_per_jugador_[i] = 999.0F;
|
|
|
|
// Check if ALL ACTIVE players are dead (trigger continue screen)
|
|
bool p1_dead = !config_partida_.jugador1_actiu || vides_per_jugador_[0] <= 0;
|
|
bool p2_dead = !config_partida_.jugador2_actiu || vides_per_jugador_[1] <= 0;
|
|
|
|
if (p1_dead && p2_dead) {
|
|
estat_game_over_ = EstatGameOver::CONTINUE;
|
|
continue_counter_ = Defaults::Game::CONTINUE_COUNT_START;
|
|
continue_tick_timer_ = Defaults::Game::CONTINUE_TICK_DURATION;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If any player is dead, still update enemies/bullets/effects
|
|
if (algun_jugador_mort) {
|
|
// Enemies and bullets continue moving during death sequence
|
|
for (auto& enemy : orni_) {
|
|
enemy.actualitzar(delta_time);
|
|
}
|
|
|
|
for (auto& bala : bales_) {
|
|
bala.actualitzar(delta_time);
|
|
}
|
|
|
|
debris_manager_.actualitzar(delta_time);
|
|
gestor_puntuacio_.actualitzar(delta_time);
|
|
|
|
// Don't return - allow alive players to continue playing
|
|
}
|
|
|
|
// *** STAGE SYSTEM STATE MACHINE ***
|
|
|
|
StageSystem::EstatStage estat = stage_manager_->get_estat();
|
|
|
|
switch (estat) {
|
|
case StageSystem::EstatStage::INIT_HUD: {
|
|
// Update stage manager timer (pot canviar l'estat!)
|
|
stage_manager_->actualitzar(delta_time);
|
|
|
|
// [FIX] Si l'estat ha canviat durant actualitzar(), sortir immediatament
|
|
// per evitar recalcular la posició de la nau amb el nou timer
|
|
if (stage_manager_->get_estat() != StageSystem::EstatStage::INIT_HUD) {
|
|
break;
|
|
}
|
|
|
|
// Calcular global progress (0.0 al inicio → 1.0 al final)
|
|
float global_progress = 1.0F - (stage_manager_->get_timer_transicio() / Defaults::Game::INIT_HUD_DURATION);
|
|
global_progress = std::min(1.0F, global_progress);
|
|
|
|
// [NEW] Calcular progress independiente para cada nave
|
|
float ship1_progress = calcular_progress_rango(
|
|
global_progress,
|
|
Defaults::Game::INIT_HUD_SHIP1_RATIO_INIT,
|
|
Defaults::Game::INIT_HUD_SHIP1_RATIO_END);
|
|
|
|
float ship2_progress = calcular_progress_rango(
|
|
global_progress,
|
|
Defaults::Game::INIT_HUD_SHIP2_RATIO_INIT,
|
|
Defaults::Game::INIT_HUD_SHIP2_RATIO_END);
|
|
|
|
// [MODIFICAT] Animar AMBAS naus con sus progress respectivos
|
|
if (config_partida_.jugador1_actiu && ship1_progress < 1.0F) {
|
|
Punt pos_p1 = calcular_posicio_nau_init_hud(ship1_progress, 0);
|
|
naus_[0].set_centre(pos_p1);
|
|
}
|
|
|
|
if (config_partida_.jugador2_actiu && ship2_progress < 1.0F) {
|
|
Punt pos_p2 = calcular_posicio_nau_init_hud(ship2_progress, 1);
|
|
naus_[1].set_centre(pos_p2);
|
|
}
|
|
|
|
// Una vegada l'animació acaba, permetre control normal
|
|
// però mantenir la posició inicial especial fins LEVEL_START
|
|
|
|
break;
|
|
}
|
|
|
|
case StageSystem::EstatStage::LEVEL_START: {
|
|
// [DEBUG] Log entrada a LEVEL_START
|
|
static bool first_entry = true;
|
|
if (first_entry) {
|
|
std::cout << "[LEVEL_START] ENTERED with P1 pos.y=" << naus_[0].get_centre().y << std::endl;
|
|
first_entry = false;
|
|
}
|
|
|
|
// Update countdown timer
|
|
stage_manager_->actualitzar(delta_time);
|
|
|
|
// [NEW] Allow both ships movement and shooting during intro
|
|
for (uint8_t i = 0; i < 2; i++) {
|
|
bool jugador_actiu = (i == 0) ? config_partida_.jugador1_actiu : config_partida_.jugador2_actiu;
|
|
if (jugador_actiu && itocado_per_jugador_[i] == 0.0F) { // Only active, alive players
|
|
naus_[i].processar_input(delta_time, i);
|
|
naus_[i].actualitzar(delta_time);
|
|
}
|
|
}
|
|
|
|
// [NEW] Update bullets
|
|
for (auto& bala : bales_) {
|
|
bala.actualitzar(delta_time);
|
|
}
|
|
|
|
// [NEW] Update debris
|
|
debris_manager_.actualitzar(delta_time);
|
|
break;
|
|
}
|
|
|
|
case StageSystem::EstatStage::PLAYING: {
|
|
// [NEW] Update stage manager (spawns enemies, pause if BOTH dead)
|
|
bool pausar_spawn = (itocado_per_jugador_[0] > 0.0F && itocado_per_jugador_[1] > 0.0F);
|
|
stage_manager_->get_spawn_controller().actualitzar(delta_time, orni_, pausar_spawn);
|
|
|
|
// [NEW] Check stage completion (only when at least one player alive)
|
|
bool algun_jugador_viu = (itocado_per_jugador_[0] == 0.0F || itocado_per_jugador_[1] == 0.0F);
|
|
if (algun_jugador_viu) {
|
|
auto& spawn_ctrl = stage_manager_->get_spawn_controller();
|
|
if (spawn_ctrl.tots_enemics_destruits(orni_)) {
|
|
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) ? config_partida_.jugador1_actiu : config_partida_.jugador2_actiu;
|
|
if (jugador_actiu && itocado_per_jugador_[i] == 0.0F) { // Only active, alive players
|
|
naus_[i].processar_input(delta_time, i);
|
|
naus_[i].actualitzar(delta_time);
|
|
}
|
|
}
|
|
|
|
for (auto& enemy : orni_) {
|
|
enemy.actualitzar(delta_time);
|
|
}
|
|
|
|
for (auto& bala : bales_) {
|
|
bala.actualitzar(delta_time);
|
|
}
|
|
|
|
detectar_col·lisions_bales_enemics();
|
|
detectar_col·lisio_naus_enemics();
|
|
detectar_col·lisions_bales_jugadors();
|
|
debris_manager_.actualitzar(delta_time);
|
|
gestor_puntuacio_.actualitzar(delta_time);
|
|
break;
|
|
}
|
|
|
|
case StageSystem::EstatStage::LEVEL_COMPLETED:
|
|
// Update countdown timer
|
|
stage_manager_->actualitzar(delta_time);
|
|
|
|
// [NEW] Allow both ships movement and shooting during outro
|
|
for (uint8_t i = 0; i < 2; i++) {
|
|
bool jugador_actiu = (i == 0) ? config_partida_.jugador1_actiu : config_partida_.jugador2_actiu;
|
|
if (jugador_actiu && itocado_per_jugador_[i] == 0.0F) { // Only active, alive players
|
|
naus_[i].processar_input(delta_time, i);
|
|
naus_[i].actualitzar(delta_time);
|
|
}
|
|
}
|
|
|
|
// [NEW] Update bullets (allow last shots to continue)
|
|
for (auto& bala : bales_) {
|
|
bala.actualitzar(delta_time);
|
|
}
|
|
|
|
// [NEW] Update debris (from last destroyed enemies)
|
|
debris_manager_.actualitzar(delta_time);
|
|
gestor_puntuacio_.actualitzar(delta_time);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void EscenaJoc::dibuixar() {
|
|
// Handle CONTINUE screen
|
|
if (estat_game_over_ == EstatGameOver::CONTINUE) {
|
|
// Draw game background elements first
|
|
dibuixar_marges();
|
|
|
|
for (const auto& enemy : orni_) {
|
|
enemy.dibuixar();
|
|
}
|
|
|
|
for (const auto& bala : bales_) {
|
|
bala.dibuixar();
|
|
}
|
|
|
|
debris_manager_.dibuixar();
|
|
gestor_puntuacio_.dibuixar();
|
|
dibuixar_marcador();
|
|
|
|
// Draw CONTINUE screen overlay
|
|
dibuixar_continue();
|
|
return;
|
|
}
|
|
|
|
// Handle final GAME OVER state
|
|
if (estat_game_over_ == EstatGameOver::GAME_OVER) {
|
|
// Game over: draw enemies, bullets, debris, and "GAME OVER" text
|
|
dibuixar_marges();
|
|
|
|
for (const auto& enemy : orni_) {
|
|
enemy.dibuixar();
|
|
}
|
|
|
|
for (const auto& bala : bales_) {
|
|
bala.dibuixar();
|
|
}
|
|
|
|
debris_manager_.dibuixar();
|
|
gestor_puntuacio_.dibuixar();
|
|
|
|
// Draw centered "GAME OVER" text
|
|
const std::string game_over_text = "GAME OVER";
|
|
constexpr float escala = Defaults::Game::GameOverScreen::TEXT_SCALE;
|
|
constexpr float spacing = Defaults::Game::GameOverScreen::TEXT_SPACING;
|
|
|
|
// Calcular centre de l'àrea de joc 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_.render_centered(game_over_text, {centre_x, centre_y}, escala, spacing);
|
|
|
|
dibuixar_marcador();
|
|
return;
|
|
}
|
|
|
|
// [NEW] Stage state rendering
|
|
StageSystem::EstatStage estat = stage_manager_->get_estat();
|
|
|
|
switch (estat) {
|
|
case StageSystem::EstatStage::INIT_HUD: {
|
|
// Calcular progrés de cada animació independent
|
|
float timer = stage_manager_->get_timer_transicio();
|
|
float total_time = Defaults::Game::INIT_HUD_DURATION;
|
|
float global_progress = 1.0F - (timer / total_time);
|
|
|
|
// [NEW] Calcular progress independiente para cada elemento
|
|
float rect_progress = calcular_progress_rango(
|
|
global_progress,
|
|
Defaults::Game::INIT_HUD_RECT_RATIO_INIT,
|
|
Defaults::Game::INIT_HUD_RECT_RATIO_END);
|
|
|
|
float score_progress = calcular_progress_rango(
|
|
global_progress,
|
|
Defaults::Game::INIT_HUD_SCORE_RATIO_INIT,
|
|
Defaults::Game::INIT_HUD_SCORE_RATIO_END);
|
|
|
|
float ship1_progress = calcular_progress_rango(
|
|
global_progress,
|
|
Defaults::Game::INIT_HUD_SHIP1_RATIO_INIT,
|
|
Defaults::Game::INIT_HUD_SHIP1_RATIO_END);
|
|
|
|
float ship2_progress = calcular_progress_rango(
|
|
global_progress,
|
|
Defaults::Game::INIT_HUD_SHIP2_RATIO_INIT,
|
|
Defaults::Game::INIT_HUD_SHIP2_RATIO_END);
|
|
|
|
// Dibuixar elements animats
|
|
if (rect_progress > 0.0F) {
|
|
// [NOU] Reproduir so quan comença l'animació del rectangle
|
|
if (!init_hud_rect_sound_played_) {
|
|
Audio::get()->playSound(Defaults::Sound::INIT_HUD, Audio::Group::GAME);
|
|
init_hud_rect_sound_played_ = true;
|
|
}
|
|
|
|
dibuixar_marges_animat(rect_progress);
|
|
}
|
|
|
|
if (score_progress > 0.0F) {
|
|
dibuixar_marcador_animat(score_progress);
|
|
}
|
|
|
|
// [MODIFICAT] Dibuixar naus amb progress independent
|
|
if (ship1_progress > 0.0F && config_partida_.jugador1_actiu && !naus_[0].esta_tocada()) {
|
|
naus_[0].dibuixar();
|
|
}
|
|
|
|
if (ship2_progress > 0.0F && config_partida_.jugador2_actiu && !naus_[1].esta_tocada()) {
|
|
naus_[1].dibuixar();
|
|
}
|
|
|
|
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) ? config_partida_.jugador1_actiu : config_partida_.jugador2_actiu;
|
|
if (jugador_actiu && itocado_per_jugador_[i] == 0.0F) {
|
|
naus_[i].dibuixar();
|
|
}
|
|
}
|
|
|
|
// [NEW] Draw bullets
|
|
for (const auto& bala : bales_) {
|
|
bala.dibuixar();
|
|
}
|
|
|
|
// [NEW] Draw debris
|
|
debris_manager_.dibuixar();
|
|
gestor_puntuacio_.dibuixar();
|
|
|
|
// [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) ? config_partida_.jugador1_actiu : config_partida_.jugador2_actiu;
|
|
if (jugador_actiu && itocado_per_jugador_[i] == 0.0F) {
|
|
naus_[i].dibuixar();
|
|
}
|
|
}
|
|
|
|
for (const auto& enemy : orni_) {
|
|
enemy.dibuixar();
|
|
}
|
|
|
|
for (const auto& bala : bales_) {
|
|
bala.dibuixar();
|
|
}
|
|
|
|
debris_manager_.dibuixar();
|
|
gestor_puntuacio_.dibuixar();
|
|
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) ? config_partida_.jugador1_actiu : config_partida_.jugador2_actiu;
|
|
if (jugador_actiu && itocado_per_jugador_[i] == 0.0F) {
|
|
naus_[i].dibuixar();
|
|
}
|
|
}
|
|
|
|
// [NEW] Draw bullets (allow last shots to be visible)
|
|
for (const auto& bala : bales_) {
|
|
bala.dibuixar();
|
|
}
|
|
|
|
// [NEW] Draw debris (from last destroyed enemies)
|
|
debris_manager_.dibuixar();
|
|
gestor_puntuacio_.dibuixar();
|
|
|
|
// [EXISTING] Draw completion message and score
|
|
dibuixar_missatge_stage(StageSystem::Constants::MISSATGE_LEVEL_COMPLETED);
|
|
dibuixar_marcador();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void EscenaJoc::tocado(uint8_t player_id) {
|
|
// Death sequence: 3 phases
|
|
// Phase 1: First call (itocado_per_jugador_[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 actualitzar()
|
|
|
|
if (itocado_per_jugador_[player_id] == 0.0F) {
|
|
// *** PHASE 1: TRIGGER DEATH ***
|
|
|
|
// Mark ship as dead (stops rendering and input)
|
|
naus_[player_id].marcar_tocada();
|
|
|
|
// Create ship explosion
|
|
const Punt& ship_pos = naus_[player_id].get_centre();
|
|
float ship_angle = naus_[player_id].get_angle();
|
|
Punt vel_nau = naus_[player_id].get_velocitat_vector();
|
|
// Reduir a 80% la velocitat heretada per la nau (més realista)
|
|
Punt vel_nau_80 = {vel_nau.x * 0.8F, vel_nau.y * 0.8F};
|
|
|
|
debris_manager_.explotar(
|
|
naus_[player_id].get_forma(), // Ship shape (3 lines)
|
|
ship_pos, // Center position
|
|
ship_angle, // Ship orientation
|
|
1.0F, // Normal scale
|
|
Defaults::Physics::Debris::VELOCITAT_BASE, // 80 px/s
|
|
naus_[player_id].get_brightness(), // Heredar brightness
|
|
vel_nau_80, // Heredar 80% velocitat
|
|
0.0F, // Nave: trayectorias rectas (sin drotacio)
|
|
0.0F, // Sin herencia visual (rotación aleatoria)
|
|
Defaults::Sound::EXPLOSION2 // Sonido alternativo para la explosión
|
|
);
|
|
|
|
// Start death timer (non-zero to avoid re-triggering)
|
|
itocado_per_jugador_[player_id] = 0.001F;
|
|
}
|
|
// Phase 2 is automatic (debris updates in actualitzar())
|
|
// Phase 3 is handled in actualitzar() when itocado_per_jugador_ >= DEATH_DURATION
|
|
}
|
|
|
|
void EscenaJoc::dibuixar_marges() const {
|
|
// Dibuixar rectangle de la zona de joc
|
|
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_.obte_renderer(), x1, y1, x2, y1, true); // Top
|
|
Rendering::linea(sdl_.obte_renderer(), x1, y2, x2, y2, true); // Bottom
|
|
Rendering::linea(sdl_.obte_renderer(), x1, y1, x1, y2, true); // Left
|
|
Rendering::linea(sdl_.obte_renderer(), x2, y1, x2, y2, true); // Right
|
|
}
|
|
|
|
void EscenaJoc::dibuixar_marcador() {
|
|
// Construir text del marcador
|
|
std::string text = construir_marcador();
|
|
|
|
// Paràmetres de renderització
|
|
const float escala = 0.85F;
|
|
const float spacing = 0.0F;
|
|
|
|
// Calcular centre 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);
|
|
|
|
// Renderitzar centrat
|
|
text_.render_centered(text, {centre_x, centre_y}, escala, spacing);
|
|
}
|
|
|
|
void EscenaJoc::dibuixar_marges_animat(float progress) const {
|
|
// Animació seqüencial del rectangle amb efecte de "pinzell"
|
|
// Dos pinzells comencen al centre superior i baixen pels laterals
|
|
|
|
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
|
|
|
|
// Aplicar easing al progrés global
|
|
float eased_progress = Easing::ease_out_quad(progress);
|
|
|
|
// Coordenades del rectangle complet
|
|
int x1 = static_cast<int>(zona.x);
|
|
int y1 = static_cast<int>(zona.y);
|
|
int x2 = static_cast<int>(zona.x + zona.w);
|
|
int y2 = static_cast<int>(zona.y + zona.h);
|
|
int cx = (x1 + x2) / 2;
|
|
|
|
// Dividir en 3 fases de 33% cada una
|
|
constexpr float PHASE_1_END = 0.33F;
|
|
constexpr float PHASE_2_END = 0.66F;
|
|
|
|
// --- FASE 1: Línies horitzontals superiors (0-33%) ---
|
|
if (eased_progress > 0.0F) {
|
|
float phase1_progress = std::min(eased_progress / PHASE_1_END, 1.0F);
|
|
|
|
// Línia esquerra: creix des del centre cap a l'esquerra
|
|
int x1_phase1 = static_cast<int>(cx - ((cx - x1) * phase1_progress));
|
|
Rendering::linea(sdl_.obte_renderer(), cx, y1, x1_phase1, y1, true);
|
|
|
|
// Línia dreta: creix des del centre cap a la dreta
|
|
int x2_phase1 = static_cast<int>(cx + ((x2 - cx) * phase1_progress));
|
|
Rendering::linea(sdl_.obte_renderer(), cx, y1, x2_phase1, y1, true);
|
|
}
|
|
|
|
// --- FASE 2: Línies verticals laterals (33-66%) ---
|
|
if (eased_progress > PHASE_1_END) {
|
|
float phase2_progress = std::min((eased_progress - PHASE_1_END) / (PHASE_2_END - PHASE_1_END), 1.0F);
|
|
|
|
// Línia esquerra: creix des de dalt cap a baix
|
|
int y2_phase2 = static_cast<int>(y1 + ((y2 - y1) * phase2_progress));
|
|
Rendering::linea(sdl_.obte_renderer(), x1, y1, x1, y2_phase2, true);
|
|
|
|
// Línia dreta: creix des de dalt cap a baix
|
|
Rendering::linea(sdl_.obte_renderer(), x2, y1, x2, y2_phase2, true);
|
|
}
|
|
|
|
// --- FASE 3: Línies horitzontals inferiors (66-100%) ---
|
|
if (eased_progress > PHASE_2_END) {
|
|
float phase3_progress = (eased_progress - PHASE_2_END) / (1.0F - PHASE_2_END);
|
|
|
|
// Línia esquerra: creix des de l'esquerra cap al centre
|
|
int x_left_phase3 = static_cast<int>(x1 + ((cx - x1) * phase3_progress));
|
|
Rendering::linea(sdl_.obte_renderer(), x1, y2, x_left_phase3, y2, true);
|
|
|
|
// Línia dreta: creix des de la dreta cap al centre
|
|
int x_right_phase3 = static_cast<int>(x2 - ((x2 - cx) * phase3_progress));
|
|
Rendering::linea(sdl_.obte_renderer(), x2, y2, x_right_phase3, y2, true);
|
|
}
|
|
}
|
|
|
|
void EscenaJoc::dibuixar_marcador_animat(float progress) {
|
|
// Animació del marcador pujant des de baix amb easing
|
|
|
|
// Calcular progrés amb easing
|
|
float eased_progress = Easing::ease_out_quad(progress);
|
|
|
|
// Construir text
|
|
std::string text = construir_marcador();
|
|
|
|
// Paràmetres
|
|
const float escala = 0.85F;
|
|
const float spacing = 0.0F;
|
|
|
|
// Calcular centre de la zona del marcador
|
|
const SDL_FRect& scoreboard = Defaults::Zones::SCOREBOARD;
|
|
float centre_x = scoreboard.w / 2.0F;
|
|
float centre_y_final = scoreboard.y + (scoreboard.h / 2.0F);
|
|
|
|
// Posició Y inicial (offscreen, sota de la pantalla)
|
|
float centre_y_inicial = static_cast<float>(Defaults::Game::HEIGHT);
|
|
|
|
// Interpolació amb easing
|
|
float centre_y_animada = centre_y_inicial + ((centre_y_final - centre_y_inicial) * eased_progress);
|
|
|
|
// Renderitzar centrat en posició animada
|
|
text_.render_centered(text, {centre_x, centre_y_animada}, escala, spacing);
|
|
}
|
|
|
|
Punt EscenaJoc::calcular_posicio_nau_init_hud(float progress, uint8_t player_id) const {
|
|
// Animació de la nau pujant des de baix amb easing
|
|
// [MODIFICAT] Ambas naves usan ease_out_quad (desfase temporal via INIT/END)
|
|
|
|
// Aplicar easing (uniforme para ambas naves)
|
|
float eased_progress = Easing::ease_out_quad(progress);
|
|
|
|
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
|
|
|
|
// Calcular posició final segons jugador (reutilitza obtenir_punt_spawn)
|
|
Punt spawn_final = obtenir_punt_spawn(player_id);
|
|
float x_final = spawn_final.x;
|
|
float y_final = spawn_final.y;
|
|
|
|
// Y inicial: offscreen, 50px sota la zona de joc
|
|
float y_inicial = zona.y + zona.h + 50.0F;
|
|
|
|
// X no canvia (destí segons player_id)
|
|
// Y interpola amb easing
|
|
float y_animada = y_inicial + ((y_final - y_inicial) * eased_progress);
|
|
|
|
return {x_final, y_animada};
|
|
}
|
|
|
|
float EscenaJoc::calcular_progress_rango(float global_progress, float ratio_init, float ratio_end) const {
|
|
// Convierte global_progress (0.0→1.0) a element_progress usando ventana [INIT, END]
|
|
//
|
|
// Casos:
|
|
// - global_progress < INIT → 0.0 (no ha empezado)
|
|
// - global_progress > END → 1.0 (completado)
|
|
// - INIT ≤ global_progress ≤ END → interpola linealmente 0.0→1.0
|
|
|
|
// Validación de parámetros (evita división por cero)
|
|
if (ratio_init >= ratio_end) {
|
|
return (global_progress >= ratio_end) ? 1.0F : 0.0F;
|
|
}
|
|
|
|
if (global_progress < ratio_init) {
|
|
return 0.0F;
|
|
}
|
|
|
|
if (global_progress > ratio_end) {
|
|
return 1.0F;
|
|
}
|
|
|
|
// Normalizar rango [INIT, END] a [0.0, 1.0]
|
|
return (global_progress - ratio_init) / (ratio_end - ratio_init);
|
|
}
|
|
|
|
std::string EscenaJoc::construir_marcador() const {
|
|
// Puntuació P1 (6 dígits) - mostrar zeros si inactiu
|
|
std::string score_p1;
|
|
std::string vides_p1;
|
|
if (config_partida_.jugador1_actiu) {
|
|
score_p1 = std::to_string(puntuacio_per_jugador_[0]);
|
|
score_p1 = std::string(6 - std::min(6, static_cast<int>(score_p1.length())), '0') + score_p1;
|
|
vides_p1 = (vides_per_jugador_[0] < 10)
|
|
? "0" + std::to_string(vides_per_jugador_[0])
|
|
: std::to_string(vides_per_jugador_[0]);
|
|
} else {
|
|
score_p1 = "000000";
|
|
vides_p1 = "00";
|
|
}
|
|
|
|
// Nivell (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ó P2 (6 dígits) - mostrar zeros si inactiu
|
|
std::string score_p2;
|
|
std::string vides_p2;
|
|
if (config_partida_.jugador2_actiu) {
|
|
score_p2 = std::to_string(puntuacio_per_jugador_[1]);
|
|
score_p2 = std::string(6 - std::min(6, static_cast<int>(score_p2.length())), '0') + score_p2;
|
|
vides_p2 = (vides_per_jugador_[1] < 10)
|
|
? "0" + std::to_string(vides_per_jugador_[1])
|
|
: std::to_string(vides_per_jugador_[1]);
|
|
} else {
|
|
score_p2 = "000000";
|
|
vides_p2 = "00";
|
|
}
|
|
|
|
// Format: "123456 03 LEVEL 01 654321 02"
|
|
// Nota: dos espais entre seccions, mantenir ambdós slots sempre visibles
|
|
return score_p1 + " " + vides_p1 + " LEVEL " + stage_str + " " + score_p2 + " " + vides_p2;
|
|
}
|
|
|
|
void EscenaJoc::detectar_col·lisions_bales_enemics() {
|
|
// Constants amplificades per hitbox més generós (115%)
|
|
constexpr float RADI_BALA = Defaults::Entities::BULLET_RADIUS;
|
|
constexpr float RADI_ENEMIC = Defaults::Entities::ENEMY_RADIUS;
|
|
constexpr float SUMA_RADIS = (RADI_BALA + RADI_ENEMIC) * 1.15F; // 28.75 px
|
|
constexpr float SUMA_RADIS_QUADRAT = SUMA_RADIS * SUMA_RADIS; // 826.56
|
|
|
|
// Velocitat d'explosió reduïda per efecte suau
|
|
constexpr float VELOCITAT_EXPLOSIO = 80.0F; // px/s (en lloc de 80.0f per defecte)
|
|
|
|
// Iterar per totes les bales actives
|
|
for (auto& bala : bales_) {
|
|
if (!bala.esta_activa()) {
|
|
continue;
|
|
}
|
|
|
|
const Punt& pos_bala = bala.get_centre();
|
|
|
|
// Comprovar col·lisió amb tots els enemics actius
|
|
for (auto& enemic : orni_) {
|
|
if (!enemic.esta_actiu()) {
|
|
continue;
|
|
}
|
|
|
|
const Punt& pos_enemic = enemic.get_centre();
|
|
|
|
// Calcular distància quadrada (evita sqrt)
|
|
float dx = pos_bala.x - pos_enemic.x;
|
|
float dy = pos_bala.y - pos_enemic.y;
|
|
float distancia_quadrada = (dx * dx) + (dy * dy);
|
|
|
|
// Comprovar col·lisió
|
|
if (distancia_quadrada <= SUMA_RADIS_QUADRAT) {
|
|
// *** COL·LISIÓ DETECTADA ***
|
|
|
|
// 1. Calculate score for enemy type
|
|
int punts = 0;
|
|
switch (enemic.get_tipus()) {
|
|
case TipusEnemic::PENTAGON:
|
|
punts = Defaults::Enemies::Scoring::PENTAGON_SCORE;
|
|
break;
|
|
case TipusEnemic::QUADRAT:
|
|
punts = Defaults::Enemies::Scoring::QUADRAT_SCORE;
|
|
break;
|
|
case TipusEnemic::MOLINILLO:
|
|
punts = Defaults::Enemies::Scoring::MOLINILLO_SCORE;
|
|
break;
|
|
}
|
|
|
|
// 2. Add score to the player who shot it
|
|
uint8_t owner_id = bala.get_owner_id();
|
|
puntuacio_per_jugador_[owner_id] += punts;
|
|
|
|
// 3. Create floating score number
|
|
gestor_puntuacio_.crear(punts, pos_enemic);
|
|
|
|
// 4. Destruir enemic (marca com inactiu)
|
|
enemic.destruir();
|
|
|
|
// 2. Crear explosió de fragments
|
|
Punt vel_enemic = enemic.get_velocitat_vector();
|
|
debris_manager_.explotar(
|
|
enemic.get_forma(), // Forma vectorial del pentàgon
|
|
pos_enemic, // Posició central
|
|
0.0F, // Angle (enemic té rotació interna)
|
|
1.0F, // Escala normal
|
|
VELOCITAT_EXPLOSIO, // 50 px/s (explosió suau)
|
|
enemic.get_brightness(), // Heredar brightness
|
|
vel_enemic, // Heredar velocitat
|
|
enemic.get_drotacio(), // Heredar velocitat angular (trayectorias curvas)
|
|
0.0F // Sin herencia visual (rotación aleatoria)
|
|
);
|
|
|
|
// 3. Desactivar bala
|
|
bala.desactivar();
|
|
|
|
// 4. Eixir del bucle intern (bala només destrueix 1 enemic)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void EscenaJoc::detectar_col·lisio_naus_enemics() {
|
|
// Generous collision detection (80% hitbox)
|
|
constexpr float RADI_NAU = Defaults::Entities::SHIP_RADIUS;
|
|
constexpr float RADI_ENEMIC = Defaults::Entities::ENEMY_RADIUS;
|
|
constexpr float SUMA_RADIS =
|
|
(RADI_NAU + RADI_ENEMIC) * Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER;
|
|
constexpr float SUMA_RADIS_QUADRAT = SUMA_RADIS * SUMA_RADIS;
|
|
|
|
// Check collision for BOTH players
|
|
for (uint8_t i = 0; i < 2; i++) {
|
|
// Skip collisions if player is dead or invulnerable
|
|
if (itocado_per_jugador_[i] > 0.0F) continue;
|
|
if (!naus_[i].esta_viva()) continue;
|
|
if (naus_[i].es_invulnerable()) continue;
|
|
|
|
const Punt& pos_nau = naus_[i].get_centre();
|
|
|
|
// Check collision with all active enemies
|
|
for (const auto& enemic : orni_) {
|
|
if (!enemic.esta_actiu()) {
|
|
continue;
|
|
}
|
|
|
|
// Skip collision if enemy is invulnerable
|
|
if (enemic.es_invulnerable()) {
|
|
continue;
|
|
}
|
|
|
|
const Punt& pos_enemic = enemic.get_centre();
|
|
|
|
// Calculate squared distance (avoid sqrt)
|
|
float dx = static_cast<float>(pos_nau.x - pos_enemic.x);
|
|
float dy = static_cast<float>(pos_nau.y - pos_enemic.y);
|
|
float distancia_quadrada = (dx * dx) + (dy * dy);
|
|
|
|
// Check collision
|
|
if (distancia_quadrada <= SUMA_RADIS_QUADRAT) {
|
|
tocado(i); // Trigger death sequence for player i
|
|
break; // Only one collision per player per frame
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void EscenaJoc::detectar_col·lisions_bales_jugadors() {
|
|
// Skip if friendly fire disabled
|
|
if (!Defaults::Game::FRIENDLY_FIRE_ENABLED) {
|
|
return;
|
|
}
|
|
|
|
// Collision constants (exact hitbox, 1.0x amplification)
|
|
constexpr float RADI_NAU = Defaults::Entities::SHIP_RADIUS;
|
|
constexpr float RADI_BALA = Defaults::Entities::BULLET_RADIUS;
|
|
constexpr float SUMA_RADIS = (RADI_NAU + RADI_BALA) *
|
|
Defaults::Game::COLLISION_BULLET_PLAYER_AMPLIFIER; // 15.0 px
|
|
constexpr float SUMA_RADIS_QUADRAT = SUMA_RADIS * SUMA_RADIS; // 225.0
|
|
|
|
// Check all active bullets
|
|
for (auto& bala : bales_) {
|
|
if (!bala.esta_activa()) {
|
|
continue;
|
|
}
|
|
|
|
// Skip bullets in grace period (prevents instant self-collision)
|
|
if (bala.get_grace_timer() > 0.0F) {
|
|
continue;
|
|
}
|
|
|
|
const Punt& pos_bala = bala.get_centre();
|
|
uint8_t bullet_owner = bala.get_owner_id();
|
|
|
|
// Check collision with BOTH players
|
|
for (uint8_t player_id = 0; player_id < 2; player_id++) {
|
|
// Skip if player is dead, invulnerable, or inactive
|
|
if (itocado_per_jugador_[player_id] > 0.0F) continue;
|
|
if (!naus_[player_id].esta_viva()) continue;
|
|
if (naus_[player_id].es_invulnerable()) continue;
|
|
|
|
// Skip inactive players
|
|
bool jugador_actiu = (player_id == 0) ? config_partida_.jugador1_actiu
|
|
: config_partida_.jugador2_actiu;
|
|
if (!jugador_actiu) continue;
|
|
|
|
const Punt& pos_nau = naus_[player_id].get_centre();
|
|
|
|
// Calculate squared distance (avoid sqrt)
|
|
float dx = pos_bala.x - pos_nau.x;
|
|
float dy = pos_bala.y - pos_nau.y;
|
|
float distancia_quadrada = (dx * dx) + (dy * dy);
|
|
|
|
// Check collision
|
|
if (distancia_quadrada <= SUMA_RADIS_QUADRAT) {
|
|
// *** FRIENDLY FIRE HIT ***
|
|
|
|
if (bullet_owner == player_id) {
|
|
// CASE 1: Self-hit (own bullet)
|
|
// Player loses 1 life, no gain
|
|
tocado(player_id);
|
|
} else {
|
|
// CASE 2: Teammate hit
|
|
// Victim loses 1 life
|
|
tocado(player_id);
|
|
|
|
// Attacker gains 1 life (no cap)
|
|
vides_per_jugador_[bullet_owner]++;
|
|
}
|
|
|
|
// Play distinct sound
|
|
Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME);
|
|
|
|
// Deactivate bullet
|
|
bala.desactivar();
|
|
|
|
break; // Bullet only hits once per frame
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// [NEW] Stage system helper methods
|
|
|
|
void EscenaJoc::dibuixar_missatge_stage(const std::string& missatge) {
|
|
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>(missatge.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 = missatge.length();
|
|
}
|
|
|
|
// Create partial message (substring for typewriter)
|
|
std::string partial_message = missatge.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(missatge, escala_base, spacing);
|
|
|
|
// Auto-scale if text exceeds max width
|
|
float escala = (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(missatge, escala, spacing);
|
|
float text_height = text_.get_text_height(escala);
|
|
|
|
// 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)
|
|
Punt pos = {x, y};
|
|
text_.render(partial_message, pos, escala, spacing);
|
|
}
|
|
|
|
// ========================================
|
|
// Helper methods for 2-player support
|
|
// ========================================
|
|
|
|
Punt EscenaJoc::obtenir_punt_spawn(uint8_t player_id) const {
|
|
const SDL_FRect& zona = Defaults::Zones::PLAYAREA;
|
|
|
|
float x_ratio;
|
|
if (config_partida_.es_un_jugador()) {
|
|
// Un sol jugador: spawn al centre (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 {
|
|
zona.x + (zona.w * x_ratio),
|
|
zona.y + (zona.h * Defaults::Game::SPAWN_Y_RATIO)};
|
|
}
|
|
|
|
void EscenaJoc::disparar_bala(uint8_t player_id) {
|
|
// Verificar que el jugador está vivo
|
|
if (itocado_per_jugador_[player_id] > 0.0F) return;
|
|
if (!naus_[player_id].esta_viva()) return;
|
|
|
|
// Calcular posición en la punta de la nave
|
|
const Punt& ship_centre = naus_[player_id].get_centre();
|
|
float ship_angle = naus_[player_id].get_angle();
|
|
|
|
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;
|
|
Punt posicio_dispar = {tip_x, tip_y};
|
|
|
|
// Buscar primera bala inactiva en el pool del jugador
|
|
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 (!bales_[i].esta_activa()) {
|
|
bales_[i].disparar(posicio_dispar, ship_angle, player_id);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==================== CONTINUE & JOIN SYSTEM ====================
|
|
|
|
void EscenaJoc::check_and_apply_continue_timeout() {
|
|
if (continue_counter_ < 0) {
|
|
estat_game_over_ = EstatGameOver::GAME_OVER;
|
|
game_over_timer_ = Defaults::Game::GAME_OVER_DURATION;
|
|
}
|
|
}
|
|
|
|
void EscenaJoc::actualitzar_continue(float delta_time) {
|
|
continue_tick_timer_ -= delta_time;
|
|
|
|
if (continue_tick_timer_ <= 0.0F) {
|
|
continue_counter_--;
|
|
continue_tick_timer_ = Defaults::Game::CONTINUE_TICK_DURATION;
|
|
|
|
// Check if timeout reached (counter < 0)
|
|
check_and_apply_continue_timeout();
|
|
|
|
// Play sound only if still in CONTINUE state (not transitioned to GAME_OVER)
|
|
if (estat_game_over_ == EstatGameOver::CONTINUE) {
|
|
Audio::get()->playSound(Defaults::Sound::CONTINUE, Audio::Group::GAME);
|
|
}
|
|
}
|
|
}
|
|
|
|
void EscenaJoc::processar_input_continue() {
|
|
auto* input = Input::get();
|
|
|
|
// Check START for both players
|
|
bool p1_start = input->checkActionPlayer1(InputAction::START, Input::DO_NOT_ALLOW_REPEAT);
|
|
bool p2_start = input->checkActionPlayer2(InputAction::START, Input::DO_NOT_ALLOW_REPEAT);
|
|
|
|
if (p1_start || p2_start) {
|
|
// Check continue limit (skip if infinite continues)
|
|
if (!Defaults::Game::INFINITE_CONTINUES && continues_usados_ >= Defaults::Game::MAX_CONTINUES) {
|
|
// Max continues reached → final game over
|
|
estat_game_over_ = EstatGameOver::GAME_OVER;
|
|
game_over_timer_ = Defaults::Game::GAME_OVER_DURATION;
|
|
return;
|
|
}
|
|
|
|
// Only increment if not infinite
|
|
if (!Defaults::Game::INFINITE_CONTINUES) {
|
|
continues_usados_++;
|
|
}
|
|
|
|
// Determine which player(s) to revive
|
|
uint8_t player_to_revive = p1_start ? 0 : 1;
|
|
|
|
// Reset score and lives (KEEP level and enemies!)
|
|
puntuacio_per_jugador_[player_to_revive] = 0;
|
|
vides_per_jugador_[player_to_revive] = Defaults::Game::STARTING_LIVES;
|
|
itocado_per_jugador_[player_to_revive] = 0.0F;
|
|
|
|
// Activate player if not already
|
|
if (player_to_revive == 0) {
|
|
config_partida_.jugador1_actiu = true;
|
|
} else {
|
|
config_partida_.jugador2_actiu = true;
|
|
}
|
|
|
|
// Spawn with invulnerability
|
|
Punt spawn_pos = obtenir_punt_spawn(player_to_revive);
|
|
naus_[player_to_revive].inicialitzar(&spawn_pos, true);
|
|
|
|
// Check if other player wants to continue too
|
|
if (p1_start && p2_start) {
|
|
uint8_t other_player = 1;
|
|
puntuacio_per_jugador_[other_player] = 0;
|
|
vides_per_jugador_[other_player] = Defaults::Game::STARTING_LIVES;
|
|
itocado_per_jugador_[other_player] = 0.0F;
|
|
config_partida_.jugador2_actiu = true;
|
|
Punt spawn_pos2 = obtenir_punt_spawn(other_player);
|
|
naus_[other_player].inicialitzar(&spawn_pos2, true);
|
|
}
|
|
|
|
// Resume game
|
|
estat_game_over_ = EstatGameOver::NONE;
|
|
continue_counter_ = 0;
|
|
continue_tick_timer_ = 0.0F;
|
|
|
|
// Play continue confirmation sound
|
|
Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME);
|
|
|
|
return;
|
|
}
|
|
|
|
// Check THRUST/FIRE to accelerate countdown (DO_NOT_ALLOW_REPEAT to avoid spam)
|
|
bool thrust_p1 = input->checkActionPlayer1(InputAction::THRUST, Input::DO_NOT_ALLOW_REPEAT);
|
|
bool thrust_p2 = input->checkActionPlayer2(InputAction::THRUST, Input::DO_NOT_ALLOW_REPEAT);
|
|
bool fire_p1 = input->checkActionPlayer1(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT);
|
|
bool fire_p2 = input->checkActionPlayer2(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT);
|
|
|
|
if (thrust_p1 || thrust_p2 || fire_p1 || fire_p2) {
|
|
continue_counter_--;
|
|
|
|
// Check if timeout reached (counter < 0)
|
|
check_and_apply_continue_timeout();
|
|
|
|
// Play sound only if still in CONTINUE state
|
|
if (estat_game_over_ == EstatGameOver::CONTINUE) {
|
|
Audio::get()->playSound(Defaults::Sound::CONTINUE, Audio::Group::GAME);
|
|
}
|
|
|
|
// Reset timer to prevent double-decrement
|
|
continue_tick_timer_ = Defaults::Game::CONTINUE_TICK_DURATION;
|
|
}
|
|
}
|
|
|
|
void EscenaJoc::dibuixar_continue() {
|
|
const SDL_FRect& play_area = Defaults::Zones::PLAYAREA;
|
|
constexpr float spacing = 4.0F;
|
|
|
|
// "CONTINUE" text (using constants)
|
|
const std::string continue_text = "CONTINUE";
|
|
float escala_continue = Defaults::Game::ContinueScreen::CONTINUE_TEXT_SCALE;
|
|
float y_ratio_continue = Defaults::Game::ContinueScreen::CONTINUE_TEXT_Y_RATIO;
|
|
|
|
float centre_x = play_area.x + (play_area.w / 2.0F);
|
|
float centre_y_continue = play_area.y + (play_area.h * y_ratio_continue);
|
|
|
|
text_.render_centered(continue_text, {centre_x, 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_.render_centered(counter_str, {centre_x, 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_usados_);
|
|
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_.render_centered(continues_text, {centre_x, centre_y_info}, escala_info, spacing);
|
|
}
|
|
}
|
|
|
|
void EscenaJoc::unir_jugador(uint8_t player_id) {
|
|
// Activate player
|
|
if (player_id == 0) {
|
|
config_partida_.jugador1_actiu = true;
|
|
} else {
|
|
config_partida_.jugador2_actiu = true;
|
|
}
|
|
|
|
// Reset stats
|
|
vides_per_jugador_[player_id] = Defaults::Game::STARTING_LIVES;
|
|
puntuacio_per_jugador_[player_id] = 0;
|
|
itocado_per_jugador_[player_id] = 0.0F;
|
|
|
|
// Spawn with invulnerability
|
|
Punt spawn_pos = obtenir_punt_spawn(player_id);
|
|
naus_[player_id].inicialitzar(&spawn_pos, true);
|
|
|
|
// No visual message, just spawn (per user requirement)
|
|
|
|
std::cout << "[EscenaJoc] Jugador " << (int)(player_id + 1) << " s'ha unit a la partida!" << std::endl;
|
|
}
|