// escena_titol.cpp - Implementació de l'escena de títol // © 2025 Port a C++20 #include "escena_titol.hpp" #include #include #include #include #include "core/audio/audio.hpp" #include "core/graphics/shape_loader.hpp" #include "core/input/mouse.hpp" #include "core/rendering/shape_renderer.hpp" #include "core/system/gestor_escenes.hpp" #include "core/system/global_events.hpp" #include "project.h" namespace { // Brightness del starfield (1.0 = default, >1.0 més brillant, <1.0 menys brillant) constexpr float BRIGHTNESS_STARFIELD = 1.2f; } // namespace EscenaTitol::EscenaTitol(SDLManager& sdl) : sdl_(sdl), text_(sdl.obte_renderer()), estat_actual_(EstatTitol::INIT), temps_acumulat_(0.0f) { std::cout << "Escena Titol: Inicialitzant...\n"; // Crear starfield de fons Punt centre_pantalla{ Defaults::Game::WIDTH / 2.0f, Defaults::Game::HEIGHT / 2.0f}; SDL_FRect area_completa{ 0, 0, static_cast(Defaults::Game::WIDTH), static_cast(Defaults::Game::HEIGHT)}; starfield_ = std::make_unique( sdl_.obte_renderer(), centre_pantalla, area_completa, 150 // densitat: 150 estrelles (50 per capa) ); // Configurar brightness del starfield starfield_->set_brightness(BRIGHTNESS_STARFIELD); // Inicialitzar lletres del títol "ORNI ATTACK!" inicialitzar_titol(); // Iniciar música de títol si no està sonant if (Audio::get()->getMusicState() != Audio::MusicState::PLAYING) { Audio::get()->playMusic("title.ogg"); } } EscenaTitol::~EscenaTitol() { // Aturar música de títol quan es destrueix l'escena Audio::get()->stopMusic(); } void EscenaTitol::inicialitzar_titol() { using namespace Graphics; // === LÍNIA 1: "ORNI" === std::vector fitxers_orni = { "title/letra_o.shp", "title/letra_r.shp", "title/letra_n.shp", "title/letra_i.shp"}; // Pas 1: Carregar formes i calcular amplades per "ORNI" float ancho_total_orni = 0.0f; for (const auto& fitxer : fitxers_orni) { auto forma = ShapeLoader::load(fitxer); if (!forma || !forma->es_valida()) { std::cerr << "[EscenaTitol] Error carregant " << fitxer << std::endl; continue; } // Calcular bounding box de la forma (trobar ancho i altura) float min_x = FLT_MAX; float max_x = -FLT_MAX; float min_y = FLT_MAX; float max_y = -FLT_MAX; for (const auto& prim : forma->get_primitives()) { for (const auto& punt : prim.points) { min_x = std::min(min_x, punt.x); max_x = std::max(max_x, punt.x); min_y = std::min(min_y, punt.y); max_y = std::max(max_y, punt.y); } } float ancho_sin_escalar = max_x - min_x; float altura_sin_escalar = max_y - min_y; // Escalar ancho, altura i offset amb ESCALA_TITULO float ancho = ancho_sin_escalar * ESCALA_TITULO; float altura = altura_sin_escalar * ESCALA_TITULO; float offset_centre = (forma->get_centre().x - min_x) * ESCALA_TITULO; lletres_orni_.push_back({forma, {0.0f, 0.0f}, ancho, altura, offset_centre}); ancho_total_orni += ancho; } // Afegir espaiat entre lletres ancho_total_orni += ESPAI_ENTRE_LLETRES * (lletres_orni_.size() - 1); // Calcular posició inicial (centrat horitzontal) per "ORNI" float x_inicial_orni = (Defaults::Game::WIDTH - ancho_total_orni) / 2.0f; float x_actual = x_inicial_orni; for (auto& lletra : lletres_orni_) { lletra.posicio.x = x_actual + lletra.offset_centre; lletra.posicio.y = Y_ORNI; x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES; } std::cout << "[EscenaTitol] Línia 1 (ORNI): " << lletres_orni_.size() << " lletres, ancho total: " << ancho_total_orni << " px\n"; // === Calcular posició Y dinàmica per "ATTACK!" === // Totes les lletres ORNI tenen la mateixa altura, utilitzem la primera float altura_orni = lletres_orni_.empty() ? 50.0f : lletres_orni_[0].altura; y_attack_dinamica_ = Y_ORNI + altura_orni + SEPARACION_LINEAS; std::cout << "[EscenaTitol] Altura ORNI: " << altura_orni << " px, Y_ATTACK dinàmica: " << y_attack_dinamica_ << " px\n"; // === LÍNIA 2: "ATTACK!" === std::vector fitxers_attack = { "title/letra_a.shp", "title/letra_t.shp", "title/letra_t.shp", // T repetida "title/letra_a.shp", // A repetida "title/letra_c.shp", "title/letra_k.shp", "title/letra_exclamacion.shp"}; // Pas 1: Carregar formes i calcular amplades per "ATTACK!" float ancho_total_attack = 0.0f; for (const auto& fitxer : fitxers_attack) { auto forma = ShapeLoader::load(fitxer); if (!forma || !forma->es_valida()) { std::cerr << "[EscenaTitol] Error carregant " << fitxer << std::endl; continue; } // Calcular bounding box de la forma (trobar ancho i altura) float min_x = FLT_MAX; float max_x = -FLT_MAX; float min_y = FLT_MAX; float max_y = -FLT_MAX; for (const auto& prim : forma->get_primitives()) { for (const auto& punt : prim.points) { min_x = std::min(min_x, punt.x); max_x = std::max(max_x, punt.x); min_y = std::min(min_y, punt.y); max_y = std::max(max_y, punt.y); } } float ancho_sin_escalar = max_x - min_x; float altura_sin_escalar = max_y - min_y; // Escalar ancho, altura i offset amb ESCALA_TITULO float ancho = ancho_sin_escalar * ESCALA_TITULO; float altura = altura_sin_escalar * ESCALA_TITULO; float offset_centre = (forma->get_centre().x - min_x) * ESCALA_TITULO; lletres_attack_.push_back({forma, {0.0f, 0.0f}, ancho, altura, offset_centre}); ancho_total_attack += ancho; } // Afegir espaiat entre lletres ancho_total_attack += ESPAI_ENTRE_LLETRES * (lletres_attack_.size() - 1); // Calcular posició inicial (centrat horitzontal) per "ATTACK!" float x_inicial_attack = (Defaults::Game::WIDTH - ancho_total_attack) / 2.0f; x_actual = x_inicial_attack; for (auto& lletra : lletres_attack_) { lletra.posicio.x = x_actual + lletra.offset_centre; lletra.posicio.y = y_attack_dinamica_; // Usar posició dinàmica x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES; } std::cout << "[EscenaTitol] Línia 2 (ATTACK!): " << lletres_attack_.size() << " lletres, ancho total: " << ancho_total_attack << " px\n"; } void EscenaTitol::executar() { SDL_Event event; Uint64 last_time = SDL_GetTicks(); while (GestorEscenes::actual == GestorEscenes::Escena::TITOL) { // 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/F4/ESC/QUIT) if (GlobalEvents::handle(event, sdl_)) { continue; } // Processar events de l'escena processar_events(event); } // Actualitzar lògica actualitzar(delta_time); // Actualitzar sistema d'audio Audio::update(); // Actualitzar colors oscil·lats sdl_.updateColors(delta_time); // Netejar pantalla sdl_.neteja(0, 0, 0); // Actualitzar context de renderitzat (factor d'escala global) sdl_.updateRenderingContext(); // Dibuixar dibuixar(); // Presentar renderer (swap buffers) sdl_.presenta(); } std::cout << "Escena Titol: Finalitzant...\n"; } void EscenaTitol::actualitzar(float delta_time) { // Actualitzar starfield (sempre actiu) if (starfield_) { starfield_->actualitzar(delta_time); } switch (estat_actual_) { case EstatTitol::INIT: temps_acumulat_ += delta_time; if (temps_acumulat_ >= DURACIO_INIT) { estat_actual_ = EstatTitol::MAIN; } break; case EstatTitol::MAIN: // No hi ha lògica d'actualització en l'estat MAIN break; case EstatTitol::TRANSITION: temps_acumulat_ += delta_time; if (temps_acumulat_ >= DURACIO_TRANSITION) { // Transició a JOC (la música ja s'ha parat en el fade) GestorEscenes::actual = GestorEscenes::Escena::JOC; } break; } } void EscenaTitol::dibuixar() { // Dibuixar starfield de fons (sempre, en tots els estats) if (starfield_) { starfield_->dibuixar(); } // En l'estat INIT, només mostrar starfield (sense text) if (estat_actual_ == EstatTitol::INIT) { return; } // Estat MAIN i TRANSITION: Dibuixar títol i text (sobre el starfield) if (estat_actual_ == EstatTitol::MAIN || estat_actual_ == EstatTitol::TRANSITION) { // === Dibuixar lletres del títol "ORNI ATTACK!" === // Dibuixar "ORNI" (línia 1) for (const auto& lletra : lletres_orni_) { Rendering::render_shape( sdl_.obte_renderer(), lletra.forma, lletra.posicio, 0.0f, // sense rotació ESCALA_TITULO, // escala 80% true, // dibuixar 1.0f // progrés complet (totalment visible) ); } // Dibuixar "ATTACK!" (línia 2) for (const auto& lletra : lletres_attack_) { Rendering::render_shape( sdl_.obte_renderer(), lletra.forma, lletra.posicio, 0.0f, // sense rotació ESCALA_TITULO, // escala 80% true, // dibuixar 1.0f // progrés complet (totalment visible) ); } // === Text "PRESS BUTTON TO PLAY" === // En estat MAIN: sempre visible // En estat TRANSITION: parpellejant (blink amb sinusoide) const float spacing = 2.0f; // Espai entre caràcters (usat també per copyright) bool mostrar_text = true; if (estat_actual_ == EstatTitol::TRANSITION) { // Parpelleig: sin oscil·la entre -1 i 1, volem ON quan > 0 float fase = temps_acumulat_ * BLINK_FREQUENCY * 2.0f * 3.14159f; // 2π × freq × temps mostrar_text = (std::sin(fase) > 0.0f); } if (mostrar_text) { const std::string main_text = "PRESS BUTTON TO PLAY"; const float escala_main = 1.0f; float text_width = text_.get_text_width(main_text, escala_main, spacing); float x_center = (Defaults::Game::WIDTH - text_width) / 2.0f; float altura_attack = lletres_attack_.empty() ? 50.0f : lletres_attack_[0].altura; float y_center = y_attack_dinamica_ + altura_attack + 70.0f; text_.render(main_text, Punt{x_center, y_center}, escala_main, spacing); } // === Copyright a la part inferior (centrat horitzontalment) === // Convert to uppercase since VectorText only supports A-Z std::string copyright = Project::COPYRIGHT; for (char& c : copyright) { if (c >= 'a' && c <= 'z') { c = c - 32; // Convert to uppercase } } const float escala_copy = 0.6f; float copy_width = text_.get_text_width(copyright, escala_copy, spacing); float copy_height = text_.get_text_height(escala_copy); float x_copy = (Defaults::Game::WIDTH - copy_width) / 2.0f; float y_copy = Defaults::Game::HEIGHT - copy_height - 20.0f; // 20px des del fons text_.render(copyright, Punt{x_copy, y_copy}, escala_copy, spacing); } } void EscenaTitol::processar_events(const SDL_Event& event) { // Qualsevol tecla o clic de ratolí if (event.type == SDL_EVENT_KEY_DOWN || event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { switch (estat_actual_) { case EstatTitol::INIT: // Saltar a MAIN estat_actual_ = EstatTitol::MAIN; break; case EstatTitol::MAIN: // Iniciar transició amb fade-out de música estat_actual_ = EstatTitol::TRANSITION; temps_acumulat_ = 0.0f; // Reset del comptador Audio::get()->fadeOutMusic(MUSIC_FADE); // Fade de 300ms break; case EstatTitol::TRANSITION: // Ignorar inputs durant la transició break; } } }