578 lines
18 KiB
C++
578 lines
18 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/mouse.hpp"
|
|
#include "core/rendering/line_renderer.hpp"
|
|
#include "core/system/gestor_escenes.hpp"
|
|
#include "core/system/global_events.hpp"
|
|
#include "game/stage_system/stage_loader.hpp"
|
|
|
|
EscenaJoc::EscenaJoc(SDLManager& sdl)
|
|
: sdl_(sdl),
|
|
debris_manager_(sdl.obte_renderer()),
|
|
nau_(sdl.obte_renderer()),
|
|
itocado_(0),
|
|
text_(sdl.obte_renderer()) {
|
|
// 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 == GestorEscenes::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();
|
|
|
|
// Processar events SDL
|
|
while (SDL_PollEvent(&event)) {
|
|
// Manejo de finestra
|
|
if (sdl_.handleWindowEvent(event)) {
|
|
continue;
|
|
}
|
|
|
|
// Events globals (F1/F2/F3/ESC/QUIT)
|
|
if (GlobalEvents::handle(event, sdl_)) {
|
|
continue;
|
|
}
|
|
|
|
// Processament específic del joc (SPACE per disparar)
|
|
processar_input(event);
|
|
}
|
|
|
|
// 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();
|
|
|
|
// Inicialitzar estat de col·lisió
|
|
itocado_ = 0;
|
|
|
|
// Initialize lives and game over state
|
|
num_vides_ = Defaults::Game::STARTING_LIVES;
|
|
game_over_ = false;
|
|
game_over_timer_ = 0.0f;
|
|
|
|
// Set spawn point to center of play area
|
|
Constants::obtenir_centre_zona(punt_spawn_.x, punt_spawn_.y);
|
|
|
|
// Inicialitzar nau
|
|
nau_.inicialitzar();
|
|
|
|
// [MODIFIED] Initialize enemies as inactive (stage system will spawn them)
|
|
for (auto& enemy : orni_) {
|
|
enemy = Enemic(sdl_.obte_renderer());
|
|
enemy.set_ship_position(&nau_.get_centre()); // Set ship reference for tracking
|
|
// DON'T call enemy.inicialitzar() here - stage system handles spawning
|
|
}
|
|
|
|
// Inicialitzar bales
|
|
for (auto& bala : bales_) {
|
|
bala.inicialitzar();
|
|
}
|
|
|
|
// Iniciar música de joc (sense stopMusic, ja s'ha parat en destructor de TITOL)
|
|
Audio::get()->playMusic("game.ogg");
|
|
}
|
|
|
|
void EscenaJoc::actualitzar(float delta_time) {
|
|
// Check game over state first
|
|
if (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();
|
|
// Auto-transition to title screen
|
|
GestorEscenes::actual = GestorEscenes::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);
|
|
return;
|
|
}
|
|
|
|
// Check death sequence state
|
|
if (itocado_ > 0.0f) {
|
|
// Death sequence active: update timer
|
|
itocado_ += delta_time;
|
|
|
|
// Check if death duration completed
|
|
if (itocado_ >= Defaults::Game::DEATH_DURATION) {
|
|
// *** PHASE 3: RESPAWN OR GAME OVER ***
|
|
|
|
// Decrement lives
|
|
num_vides_--;
|
|
|
|
if (num_vides_ > 0) {
|
|
// Respawn ship
|
|
nau_.inicialitzar(&punt_spawn_);
|
|
itocado_ = 0.0f;
|
|
} else {
|
|
// Game over
|
|
game_over_ = true;
|
|
game_over_timer_ = Defaults::Game::GAME_OVER_DURATION;
|
|
itocado_ = 0.0f;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
return;
|
|
}
|
|
|
|
// *** STAGE SYSTEM STATE MACHINE ***
|
|
|
|
StageSystem::EstatStage estat = stage_manager_->get_estat();
|
|
|
|
switch (estat) {
|
|
case StageSystem::EstatStage::LEVEL_START:
|
|
// Frozen gameplay, countdown timer only
|
|
stage_manager_->actualitzar(delta_time);
|
|
break;
|
|
|
|
case StageSystem::EstatStage::PLAYING: {
|
|
// [NEW] Update stage manager (spawns enemies, pass pause flag)
|
|
bool pausar_spawn = (itocado_ > 0.0f); // Pause during death animation
|
|
stage_manager_->get_spawn_controller().actualitzar(delta_time, orni_, pausar_spawn);
|
|
|
|
// [NEW] Check stage completion (only when not in death sequence)
|
|
if (itocado_ == 0.0f) {
|
|
auto& spawn_ctrl = stage_manager_->get_spawn_controller();
|
|
if (spawn_ctrl.tots_enemics_destruits(orni_)) {
|
|
stage_manager_->stage_completat();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// [EXISTING] Normal gameplay
|
|
nau_.processar_input(delta_time);
|
|
nau_.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_nau_enemics();
|
|
debris_manager_.actualitzar(delta_time);
|
|
break;
|
|
}
|
|
|
|
case StageSystem::EstatStage::LEVEL_COMPLETED:
|
|
// Frozen gameplay, countdown timer only
|
|
stage_manager_->actualitzar(delta_time);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void EscenaJoc::dibuixar() {
|
|
// Draw borders (always visible)
|
|
dibuixar_marges();
|
|
|
|
// Check game over state
|
|
if (game_over_) {
|
|
// Game over: draw enemies, bullets, debris, and "GAME OVER" text
|
|
|
|
for (const auto& enemy : orni_) {
|
|
enemy.dibuixar();
|
|
}
|
|
|
|
for (const auto& bala : bales_) {
|
|
bala.dibuixar();
|
|
}
|
|
|
|
debris_manager_.dibuixar();
|
|
|
|
// Draw centered "GAME OVER" text
|
|
const std::string game_over_text = "GAME OVER";
|
|
constexpr float escala = 2.0f;
|
|
constexpr float spacing = 4.0f;
|
|
|
|
float text_width = text_.get_text_width(game_over_text, escala, spacing);
|
|
float text_height = text_.get_text_height(escala);
|
|
|
|
const SDL_FRect& play_area = Defaults::Zones::PLAYAREA;
|
|
float x = play_area.x + (play_area.w - text_width) / 2.0f;
|
|
float y = play_area.y + (play_area.h - text_height) / 2.0f;
|
|
|
|
text_.render(game_over_text, {x, y}, escala, spacing);
|
|
|
|
dibuixar_marcador();
|
|
return;
|
|
}
|
|
|
|
// [NEW] Stage state rendering
|
|
StageSystem::EstatStage estat = stage_manager_->get_estat();
|
|
|
|
switch (estat) {
|
|
case StageSystem::EstatStage::LEVEL_START:
|
|
dibuixar_missatge_stage(StageSystem::Constants::MISSATGE_LEVEL_START);
|
|
dibuixar_marcador();
|
|
break;
|
|
|
|
case StageSystem::EstatStage::PLAYING:
|
|
// [EXISTING] Normal rendering
|
|
if (itocado_ == 0.0f) {
|
|
nau_.dibuixar();
|
|
}
|
|
|
|
for (const auto& enemy : orni_) {
|
|
enemy.dibuixar();
|
|
}
|
|
|
|
for (const auto& bala : bales_) {
|
|
bala.dibuixar();
|
|
}
|
|
|
|
debris_manager_.dibuixar();
|
|
dibuixar_marcador();
|
|
break;
|
|
|
|
case StageSystem::EstatStage::LEVEL_COMPLETED:
|
|
dibuixar_missatge_stage(StageSystem::Constants::MISSATGE_LEVEL_COMPLETED);
|
|
dibuixar_marcador();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void EscenaJoc::processar_input(const SDL_Event& event) {
|
|
// Ignore ship controls during game over
|
|
if (game_over_) {
|
|
return;
|
|
}
|
|
|
|
// Ignore ship controls during death sequence
|
|
if (itocado_ > 0.0f) {
|
|
return;
|
|
}
|
|
|
|
// Processament d'input per events puntuals (no continus)
|
|
// L'input continu (fletxes) es processa en actualitzar() amb
|
|
// SDL_GetKeyboardState()
|
|
|
|
if (event.type == SDL_EVENT_KEY_DOWN) {
|
|
switch (event.key.key) {
|
|
case SDLK_SPACE: {
|
|
// No disparar si la nau està morta
|
|
if (!nau_.esta_viva()) {
|
|
break;
|
|
}
|
|
|
|
// Disparar bala des del front de la nau
|
|
// El ship.shp té el front a (0, -12) en coordenades locals
|
|
|
|
// 1. Calcular posició del front de la nau
|
|
constexpr float LOCAL_TIP_X = 0.0f;
|
|
constexpr float LOCAL_TIP_Y = -12.0f;
|
|
|
|
const Punt& ship_centre = nau_.get_centre();
|
|
float ship_angle = nau_.get_angle();
|
|
|
|
// Aplicar transformació: rotació + trasllació
|
|
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};
|
|
|
|
// 2. Buscar primera bala inactiva i disparar
|
|
for (auto& bala : bales_) {
|
|
if (!bala.esta_activa()) {
|
|
bala.disparar(posicio_dispar, ship_angle);
|
|
break; // Només una bala per polsació
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void EscenaJoc::tocado() {
|
|
// Death sequence: 3 phases
|
|
// Phase 1: First call (itocado_ == 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_ == 0.0f) {
|
|
// *** PHASE 1: TRIGGER DEATH ***
|
|
|
|
// Mark ship as dead (stops rendering and input)
|
|
nau_.marcar_tocada();
|
|
|
|
// Create ship explosion
|
|
const Punt& ship_pos = nau_.get_centre();
|
|
float ship_angle = nau_.get_angle();
|
|
|
|
debris_manager_.explotar(
|
|
nau_.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
|
|
);
|
|
|
|
// Start death timer (non-zero to avoid re-triggering)
|
|
itocado_ = 0.001f;
|
|
}
|
|
// Phase 2 is automatic (debris updates in actualitzar())
|
|
// Phase 3 is handled in actualitzar() when itocado_ >= 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() {
|
|
// [MODIFIED] Display current stage number from stage manager
|
|
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);
|
|
|
|
std::string text = "SCORE: 01000 LIFES: " + std::to_string(num_vides_) +
|
|
" LEVEL: " + stage_str;
|
|
|
|
// Paràmetres de renderització
|
|
const float escala = 0.85f;
|
|
const float spacing = 0.0f;
|
|
|
|
// Calcular dimensions del text
|
|
float text_width = text_.get_text_width(text, escala, spacing);
|
|
float text_height = text_.get_text_height(escala);
|
|
|
|
// Centrat horitzontal dins de la zona del marcador
|
|
float x = (Defaults::Zones::SCOREBOARD.w - text_width) / 2.0f;
|
|
|
|
// Centrat vertical dins de la zona del marcador
|
|
float y = Defaults::Zones::SCOREBOARD.y +
|
|
(Defaults::Zones::SCOREBOARD.h - text_height) / 2.0f;
|
|
|
|
// Renderitzar
|
|
text_.render(text, {x, y}, escala, spacing);
|
|
}
|
|
|
|
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 = 50.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. Destruir enemic (marca com inactiu)
|
|
enemic.destruir();
|
|
|
|
// 2. Crear explosió de fragments
|
|
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)
|
|
);
|
|
|
|
// 3. Desactivar bala
|
|
bala.desactivar();
|
|
|
|
// 4. Eixir del bucle intern (bala només destrueix 1 enemic)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void EscenaJoc::detectar_col·lisio_nau_enemics() {
|
|
// Only check collisions if ship is alive
|
|
if (!nau_.esta_viva()) {
|
|
return;
|
|
}
|
|
|
|
// 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;
|
|
|
|
const Punt& pos_nau = nau_.get_centre();
|
|
|
|
// Check collision with all active enemies
|
|
for (const auto& enemic : orni_) {
|
|
if (!enemic.esta_actiu()) {
|
|
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(); // Trigger death sequence
|
|
return; // Only one collision per frame
|
|
}
|
|
}
|
|
}
|
|
|
|
// [NEW] Stage system helper methods
|
|
|
|
void EscenaJoc::dibuixar_missatge_stage(const std::string& missatge) {
|
|
constexpr float escala = 1.5f;
|
|
constexpr float spacing = 3.0f;
|
|
|
|
float text_width = text_.get_text_width(missatge, escala, spacing);
|
|
float text_height = text_.get_text_height(escala);
|
|
|
|
const SDL_FRect& play_area = Defaults::Zones::PLAYAREA;
|
|
float x = play_area.x + (play_area.w - text_width) / 2.0f;
|
|
float y = play_area.y + (play_area.h - text_height) / 2.0f;
|
|
|
|
Punt pos = {static_cast<float>(x), static_cast<float>(y)};
|
|
text_.render(missatge, pos, escala, spacing);
|
|
}
|