// 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/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()) { // 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(); // 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(); // [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; game_over_ = false; game_over_timer_ = 0.0f; // Initialize scores (separate per player) puntuacio_per_jugador_[0] = 0; puntuacio_per_jugador_[1] = 0; gestor_puntuacio_.reiniciar(); // Set spawn point to center X, 75% Y (legacy, for INIT_HUD animation) 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 AMBAS naus amb posicions específiques for (uint8_t i = 0; i < 2; i++) { Punt spawn_pos = obtenir_punt_spawn(i); naus_[i].inicialitzar(&spawn_pos, false); // No invulnerability at start } // [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"); } 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); 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 BOTH players are dead (game over) if (vides_per_jugador_[0] <= 0 && vides_per_jugador_[1] <= 0) { game_over_ = true; game_over_timer_ = Defaults::Game::GAME_OVER_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 progrés de l'animació de la nau float ship_progress = 1.0f - (stage_manager_->get_timer_transicio() / Defaults::Game::INIT_HUD_DURATION); ship_progress = std::min(1.0f, ship_progress); // Calcular quant ha avançat l'animació de la nau float ship_anim_progress = ship_progress / Defaults::Game::INIT_HUD_SHIP_RATIO; ship_anim_progress = std::min(1.0f, ship_anim_progress); // Actualitzar posició de la nau P1 segons animació (P2 apareix directamente) if (ship_anim_progress < 1.0f) { Punt pos_animada = calcular_posicio_nau_init_hud(ship_anim_progress); naus_[0].set_centre(pos_animada); // Solo P1 animación } // 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++) { if (itocado_per_jugador_[i] == 0.0f) { // Only 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 BOTH players for (uint8_t i = 0; i < 2; i++) { if (itocado_per_jugador_[i] == 0.0f) { // Only 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(); 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++) { if (itocado_per_jugador_[i] == 0.0f) { // Only 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() { // Check game over state if (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 = 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::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); // Progrés del rectangle (empieza inmediatament) float rect_progress = global_progress / Defaults::Game::INIT_HUD_RECT_RATIO; rect_progress = std::min(1.0f, rect_progress); // Progrés del marcador (empieza inmediatament) float score_progress = global_progress / Defaults::Game::INIT_HUD_SCORE_RATIO; score_progress = std::min(1.0f, score_progress); // Progrés de la nau (empieza inmediatament) float ship_progress = global_progress / Defaults::Game::INIT_HUD_SHIP_RATIO; ship_progress = std::min(1.0f, ship_progress); // Dibuixar elements animats if (rect_progress > 0.0f) { dibuixar_marges_animat(rect_progress); } if (score_progress > 0.0f) { dibuixar_marcador_animat(score_progress); } // Dibuixar nau P1 (usant el sistema normal, la posició ja està actualitzada) // Durante INIT_HUD solo se anima P1 if (ship_progress > 0.0f && !naus_[0].esta_tocada()) { naus_[0].dibuixar(); } break; } case StageSystem::EstatStage::LEVEL_START: dibuixar_marges(); // [NEW] Draw both ships if alive for (uint8_t i = 0; i < 2; i++) { if (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 - both ships for (uint8_t i = 0; i < 2; i++) { if (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 alive for (uint8_t i = 0; i < 2; i++) { if (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::processar_input(const SDL_Event& event) { // Ignore ship controls during game over if (game_over_) { return; } // Processament d'input per events puntuals (no continus) // L'input continu (fletxes/WASD) es processa en actualitzar() amb // SDL_GetKeyboardState() if (event.type == SDL_EVENT_KEY_DOWN) { // P1 shoot if (event.key.key == Defaults::Controls::P1::SHOOT) { disparar_bala(0); } // P2 shoot else if (event.key.key == Defaults::Controls::P2::SHOOT) { disparar_bala(1); } } } 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) ); Audio::get()->playSound(Defaults::Sound::EXPLOSION, Audio::Group::GAME); // 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(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() { // Construir text del marcador std::string text = construir_marcador(); // 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::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(zona.x); int y1 = static_cast(zona.y); int x2 = static_cast(zona.x + zona.w); int y2 = static_cast(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(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(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(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(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(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 dimensions float text_width = text_.get_text_width(text, escala, spacing); float text_height = text_.get_text_height(escala); // Posició X final (centrada horitzontalment) float x_final = (Defaults::Zones::SCOREBOARD.w - text_width) / 2.0f; // Posició Y final (centrada verticalment en la zona de scoreboard) float y_final = Defaults::Zones::SCOREBOARD.y + (Defaults::Zones::SCOREBOARD.h - text_height) / 2.0f; // Posició Y inicial (offscreen, sota de la pantalla) float y_inicial = static_cast(Defaults::Game::HEIGHT) + text_height; // Interpolació amb easing float y_animada = y_inicial + (y_final - y_inicial) * eased_progress; // Renderitzar en posició animada text_.render(text, {x_final, y_animada}, escala, spacing); } Punt EscenaJoc::calcular_posicio_nau_init_hud(float progress) const { // Animació de la nau pujant des de baix amb easing // Calcular progrés amb easing float eased_progress = Easing::ease_out_quad(progress); const SDL_FRect& zona = Defaults::Zones::PLAYAREA; // Posició X final (centre de la zona de joc) float x_final = zona.x + zona.w / 2.0f; // Posició Y final (75% de l'altura de la zona de joc) float y_final = zona.y + zona.h * Defaults::Game::INIT_HUD_SHIP_START_Y_RATIO; // Posició Y inicial (offscreen, sota de la zona de joc) float y_inicial = zona.y + zona.h + 50.0f; // 50px sota // X no canvia (sempre centrada) // Y interpola amb easing float y_animada = y_inicial + (y_final - y_inicial) * eased_progress; return {x_final, y_animada}; } std::string EscenaJoc::construir_marcador() const { // Puntuació P1 (6 dígits) std::string score_p1 = std::to_string(puntuacio_per_jugador_[0]); score_p1 = std::string(6 - std::min(6, static_cast(score_p1.length())), '0') + score_p1; // Vides P1 (2 dígits) std::string vides_p1 = (vides_per_jugador_[0] < 10) ? "0" + std::to_string(vides_per_jugador_[0]) : std::to_string(vides_per_jugador_[0]); // 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) std::string score_p2 = std::to_string(puntuacio_per_jugador_[1]); score_p2 = std::string(6 - std::min(6, static_cast(score_p2.length())), '0') + score_p2; // Vides P2 (2 dígits) std::string vides_p2 = (vides_per_jugador_[1] < 10) ? "0" + std::to_string(vides_per_jugador_[1]) : std::to_string(vides_per_jugador_[1]); // Format: "123456 03 LEVEL 01 654321 02" // Nota: dos espais entre seccions 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(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(i); // Trigger death sequence for player i break; // Only one collision per player 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; constexpr float max_width_ratio = 0.9f; // 90% del ancho disponible const SDL_FRect& play_area = Defaults::Zones::PLAYAREA; const float max_width = play_area.w * max_width_ratio; // 558px // ========== 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(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 * 0.25f) - (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 = (player_id == 0) ? Defaults::Game::P1_SPAWN_X_RATIO : Defaults::Game::P2_SPAWN_X_RATIO; 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; } } }