// 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 #include #include #include #include #include "core/audio/audio.hpp" #include "core/input/mouse.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()), nau_(sdl.obte_renderer()), itocado_(0), text_(sdl.obte_renderer()) { // Consumir opcions (preparació per MODE_DEMO futur) auto opcio = context_.consumir_opcio(); (void)opcio; // Suprimir warning de variable no usada // 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(); // 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_, context_)) { 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(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(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(); // 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); 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(); Audio::get()->playSound(Defaults::Sound::GOOD_JOB_COMMANDER, Audio::Group::GAME); 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(); Punt vel_nau = nau_.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( 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 nau_.get_brightness(), // Heredar brightness vel_nau_80 // Heredar 60% velocitat ); // 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(zona.x); int y1 = static_cast(zona.y); int x2 = static_cast(zona.x + zona.w); int y2 = static_cast(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 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 ); // 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(pos_nau.x - pos_enemic.x); float dy = static_cast(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.0f; constexpr float spacing = 2.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(x), static_cast(y)}; text_.render(missatge, pos, escala, spacing); }