// 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/input.hpp" #include "core/input/mouse.hpp" #include "core/rendering/shape_renderer.hpp" #include "core/system/context_escenes.hpp" #include "core/system/global_events.hpp" #include "project.h" // Using declarations per simplificar el codi using GestorEscenes::ContextEscenes; using Escena = ContextEscenes::Escena; using Opcio = ContextEscenes::Opcio; EscenaTitol::EscenaTitol(SDLManager& sdl, ContextEscenes& context) : sdl_(sdl), context_(context), text_(sdl.obte_renderer()), estat_actual_(EstatTitol::STARFIELD_FADE_IN), temps_acumulat_(0.0f), temps_animacio_(0.0f), temps_estat_main_(0.0f), animacio_activa_(false), factor_lerp_(0.0f) { std::cout << "Escena Titol: Inicialitzant...\n"; // Inicialitzar configuració de partida (cap jugador actiu per defecte) config_partida_.jugador1_actiu = false; config_partida_.jugador2_actiu = false; config_partida_.mode = GameConfig::Mode::NORMAL; // Processar opció del context auto opcio = context_.consumir_opcio(); if (opcio == Opcio::JUMP_TO_TITLE_MAIN) { std::cout << "Escena Titol: Opció JUMP_TO_TITLE_MAIN activada\n"; estat_actual_ = EstatTitol::MAIN; temps_estat_main_ = 0.0f; } // 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) ); // Brightness depèn de l'opció if (estat_actual_ == EstatTitol::MAIN) { // Si saltem a MAIN, starfield instantàniament brillant starfield_->set_brightness(BRIGHTNESS_STARFIELD); } else { // Flux normal: comença amb brightness 0.0 per fade-in starfield_->set_brightness(0.0f); } // Inicialitzar animador de naus 3D ship_animator_ = std::make_unique(sdl_.obte_renderer()); ship_animator_->inicialitzar(); if (estat_actual_ == EstatTitol::MAIN) { // Jump to MAIN: empezar entrada inmediatamente ship_animator_->set_visible(true); ship_animator_->start_entry_animation(); } else { // Flux normal: NO empezar entrada todavía (esperaran a MAIN) ship_animator_->set_visible(false); } // 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 LOGO_SCALE float ancho = ancho_sin_escalar * Defaults::Title::Layout::LOGO_SCALE; float altura = altura_sin_escalar * Defaults::Title::Layout::LOGO_SCALE; float offset_centre = (forma->get_centre().x - min_x) * Defaults::Title::Layout::LOGO_SCALE; 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 = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS; 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; float y_orni = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS; float separacion_lineas = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_LINE_SPACING; 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 LOGO_SCALE float ancho = ancho_sin_escalar * Defaults::Title::Layout::LOGO_SCALE; float altura = altura_sin_escalar * Defaults::Title::Layout::LOGO_SCALE; float offset_centre = (forma->get_centre().x - min_x) * Defaults::Title::Layout::LOGO_SCALE; 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"; // Guardar posicions originals per l'animació orbital posicions_originals_orni_.clear(); for (const auto& lletra : lletres_orni_) { posicions_originals_orni_.push_back(lletra.posicio); } posicions_originals_attack_.clear(); for (const auto& lletra : lletres_attack_) { posicions_originals_attack_.push_back(lletra.posicio); } std::cout << "[EscenaTitol] Animació: Posicions originals guardades\n"; } void EscenaTitol::executar() { SDL_Event event; Uint64 last_time = SDL_GetTicks(); while (GestorEscenes::actual == 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(); // 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/F4/ESC/QUIT) if (GlobalEvents::handle(event, sdl_, context_)) { 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); } // Actualitzar naus (quan visibles) if (ship_animator_ && (estat_actual_ == EstatTitol::STARFIELD_FADE_IN || estat_actual_ == EstatTitol::STARFIELD || estat_actual_ == EstatTitol::MAIN || estat_actual_ == EstatTitol::PLAYER_JOIN_PHASE)) { ship_animator_->actualitzar(delta_time); } switch (estat_actual_) { case EstatTitol::STARFIELD_FADE_IN: { temps_acumulat_ += delta_time; // Calcular progrés del fade (0.0 → 1.0) float progress = std::min(1.0f, temps_acumulat_ / DURACIO_FADE_IN); // Lerp brightness de 0.0 a BRIGHTNESS_STARFIELD float brightness_actual = progress * BRIGHTNESS_STARFIELD; starfield_->set_brightness(brightness_actual); // Transició a STARFIELD quan el fade es completa if (temps_acumulat_ >= DURACIO_FADE_IN) { estat_actual_ = EstatTitol::STARFIELD; temps_acumulat_ = 0.0f; // Reset timer per al següent estat starfield_->set_brightness(BRIGHTNESS_STARFIELD); // Assegurar valor final } break; } case EstatTitol::STARFIELD: temps_acumulat_ += delta_time; if (temps_acumulat_ >= DURACIO_INIT) { estat_actual_ = EstatTitol::MAIN; temps_estat_main_ = 0.0f; // Reset timer al entrar a MAIN animacio_activa_ = false; // Comença estàtic factor_lerp_ = 0.0f; // Sense animació encara // Naus esperaran ENTRANCE_DELAY abans d'entrar (no iniciar aquí) } break; case EstatTitol::MAIN: { temps_estat_main_ += delta_time; // Iniciar animació d'entrada de naus després del delay if (temps_estat_main_ >= Defaults::Title::Ships::ENTRANCE_DELAY) { if (ship_animator_ && !ship_animator_->is_visible()) { ship_animator_->set_visible(true); ship_animator_->start_entry_animation(); } } // Fase 1: Estàtic (0-10s) if (temps_estat_main_ < DELAY_INICI_ANIMACIO) { factor_lerp_ = 0.0f; animacio_activa_ = false; } // Fase 2: Lerp (10-12s) else if (temps_estat_main_ < DELAY_INICI_ANIMACIO + DURACIO_LERP) { float temps_lerp = temps_estat_main_ - DELAY_INICI_ANIMACIO; factor_lerp_ = temps_lerp / DURACIO_LERP; // 0.0 → 1.0 linealment animacio_activa_ = true; } // Fase 3: Animació completa (12s+) else { factor_lerp_ = 1.0f; animacio_activa_ = true; } // Actualitzar animació del logo actualitzar_animacio_logo(delta_time); break; } case EstatTitol::PLAYER_JOIN_PHASE: temps_acumulat_ += delta_time; // Continuar animació orbital durant la transició actualitzar_animacio_logo(delta_time); // [NOU] Continuar comprovant si l'altre jugador vol unir-se durant la transició ("late join") { bool p1_actiu_abans = config_partida_.jugador1_actiu; bool p2_actiu_abans = config_partida_.jugador2_actiu; if (checkStartGameButtonPressed()) { // Updates config_partida_ if pressed, logs are in the method context_.set_config_partida(config_partida_); // Trigger animació de sortida per la nau que acaba d'unir-se if (ship_animator_) { if (config_partida_.jugador1_actiu && !p1_actiu_abans) { ship_animator_->trigger_exit_animation_for_player(1); std::cout << "[EscenaTitol] P1 late join - ship exiting\n"; } if (config_partida_.jugador2_actiu && !p2_actiu_abans) { ship_animator_->trigger_exit_animation_for_player(2); std::cout << "[EscenaTitol] P2 late join - ship exiting\n"; } } // Reproducir so de START quan el segon jugador s'uneix Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME); // Reiniciar el timer per allargar el temps de transició temps_acumulat_ = 0.0f; std::cout << "[EscenaTitol] Segon jugador s'ha unit - so i timer reiniciats\n"; } } if (temps_acumulat_ >= DURACIO_TRANSITION) { // Transició a pantalla negra estat_actual_ = EstatTitol::BLACK_SCREEN; temps_acumulat_ = 0.0f; std::cout << "[EscenaTitol] Passant a BLACK_SCREEN\n"; } break; case EstatTitol::BLACK_SCREEN: temps_acumulat_ += delta_time; // No animation, no input checking - just wait if (temps_acumulat_ >= DURACIO_BLACK_SCREEN) { // Transició a escena JOC GestorEscenes::actual = Escena::JOC; std::cout << "[EscenaTitol] Canviant a escena JOC\n"; } break; } // Verificar botones de skip (FIRE/THRUST/START) para saltar escenas ANTES de MAIN if (estat_actual_ == EstatTitol::STARFIELD_FADE_IN || estat_actual_ == EstatTitol::STARFIELD) { if (checkSkipButtonPressed()) { // Saltar a MAIN estat_actual_ = EstatTitol::MAIN; starfield_->set_brightness(BRIGHTNESS_STARFIELD); temps_estat_main_ = 0.0f; // Naus esperaran ENTRANCE_DELAY abans d'entrar (no iniciar aquí) } } // Verificar boton START para iniciar partida desde MAIN if (estat_actual_ == EstatTitol::MAIN) { // Guardar estat anterior per detectar qui ha premut START AQUEST frame bool p1_actiu_abans = config_partida_.jugador1_actiu; bool p2_actiu_abans = config_partida_.jugador2_actiu; if (checkStartGameButtonPressed()) { // Si START es prem durant el delay (naus encara invisibles), saltar-les a FLOATING if (ship_animator_ && !ship_animator_->is_visible()) { ship_animator_->set_visible(true); ship_animator_->skip_to_floating_state(); } // Configurar partida abans de canviar d'escena context_.set_config_partida(config_partida_); std::cout << "[EscenaTitol] Configuració de partida - P1: " << (config_partida_.jugador1_actiu ? "ACTIU" : "INACTIU") << ", P2: " << (config_partida_.jugador2_actiu ? "ACTIU" : "INACTIU") << std::endl; context_.canviar_escena(Escena::JOC); estat_actual_ = EstatTitol::PLAYER_JOIN_PHASE; temps_acumulat_ = 0.0f; // Trigger animació de sortida NOMÉS per les naus que han premut START if (ship_animator_) { if (config_partida_.jugador1_actiu && !p1_actiu_abans) { ship_animator_->trigger_exit_animation_for_player(1); std::cout << "[EscenaTitol] P1 ship exiting\n"; } if (config_partida_.jugador2_actiu && !p2_actiu_abans) { ship_animator_->trigger_exit_animation_for_player(2); std::cout << "[EscenaTitol] P2 ship exiting\n"; } } Audio::get()->fadeOutMusic(MUSIC_FADE); Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME); } } } void EscenaTitol::actualitzar_animacio_logo(float delta_time) { // Només calcular i aplicar offsets si l'animació està activa if (animacio_activa_) { // Acumular temps escalat temps_animacio_ += delta_time * factor_lerp_; // Usar amplituds i freqüències completes float amplitude_x_actual = ORBIT_AMPLITUDE_X; float amplitude_y_actual = ORBIT_AMPLITUDE_Y; float frequency_x_actual = ORBIT_FREQUENCY_X; float frequency_y_actual = ORBIT_FREQUENCY_Y; // Calcular offset orbital float offset_x = amplitude_x_actual * std::sin(2.0f * Defaults::Math::PI * frequency_x_actual * temps_animacio_); float offset_y = amplitude_y_actual * std::sin(2.0f * Defaults::Math::PI * frequency_y_actual * temps_animacio_ + ORBIT_PHASE_OFFSET); // Aplicar offset a totes les lletres de "ORNI" for (size_t i = 0; i < lletres_orni_.size(); ++i) { lletres_orni_[i].posicio.x = posicions_originals_orni_[i].x + static_cast(std::round(offset_x)); lletres_orni_[i].posicio.y = posicions_originals_orni_[i].y + static_cast(std::round(offset_y)); } // Aplicar offset a totes les lletres de "ATTACK!" for (size_t i = 0; i < lletres_attack_.size(); ++i) { lletres_attack_[i].posicio.x = posicions_originals_attack_[i].x + static_cast(std::round(offset_x)); lletres_attack_[i].posicio.y = posicions_originals_attack_[i].y + static_cast(std::round(offset_y)); } } } void EscenaTitol::dibuixar() { // Dibuixar starfield de fons (en tots els estats excepte BLACK_SCREEN) if (starfield_ && estat_actual_ != EstatTitol::BLACK_SCREEN) { starfield_->dibuixar(); } // Dibuixar naus (després starfield, abans logo) if (ship_animator_ && (estat_actual_ == EstatTitol::STARFIELD_FADE_IN || estat_actual_ == EstatTitol::STARFIELD || estat_actual_ == EstatTitol::MAIN || estat_actual_ == EstatTitol::PLAYER_JOIN_PHASE)) { ship_animator_->dibuixar(); } // En els estats STARFIELD_FADE_IN i STARFIELD, només mostrar starfield (sense text) if (estat_actual_ == EstatTitol::STARFIELD_FADE_IN || estat_actual_ == EstatTitol::STARFIELD) { return; } // Estat MAIN i PLAYER_JOIN_PHASE: Dibuixar títol i text (sobre el starfield) // BLACK_SCREEN: no dibuixar res (fons negre ja està netejat) if (estat_actual_ == EstatTitol::MAIN || estat_actual_ == EstatTitol::PLAYER_JOIN_PHASE) { // === Calcular i renderitzar ombra (només si animació activa) === if (animacio_activa_) { float temps_shadow = temps_animacio_ - SHADOW_DELAY; if (temps_shadow < 0.0f) temps_shadow = 0.0f; // Evitar temps negatiu // Usar amplituds i freqüències completes per l'ombra float amplitude_x_shadow = ORBIT_AMPLITUDE_X; float amplitude_y_shadow = ORBIT_AMPLITUDE_Y; float frequency_x_shadow = ORBIT_FREQUENCY_X; float frequency_y_shadow = ORBIT_FREQUENCY_Y; // Calcular offset de l'ombra float shadow_offset_x = amplitude_x_shadow * std::sin(2.0f * Defaults::Math::PI * frequency_x_shadow * temps_shadow) + SHADOW_OFFSET_X; float shadow_offset_y = amplitude_y_shadow * std::sin(2.0f * Defaults::Math::PI * frequency_y_shadow * temps_shadow + ORBIT_PHASE_OFFSET) + SHADOW_OFFSET_Y; // === RENDERITZAR OMBRA PRIMER (darrera del logo principal) === // Ombra "ORNI" for (size_t i = 0; i < lletres_orni_.size(); ++i) { Punt pos_shadow; pos_shadow.x = posicions_originals_orni_[i].x + static_cast(std::round(shadow_offset_x)); pos_shadow.y = posicions_originals_orni_[i].y + static_cast(std::round(shadow_offset_y)); Rendering::render_shape( sdl_.obte_renderer(), lletres_orni_[i].forma, pos_shadow, 0.0f, Defaults::Title::Layout::LOGO_SCALE, true, 1.0f, // progress = 1.0 (totalment visible) SHADOW_BRIGHTNESS // brightness = 0.4 (brillantor reduïda) ); } // Ombra "ATTACK!" for (size_t i = 0; i < lletres_attack_.size(); ++i) { Punt pos_shadow; pos_shadow.x = posicions_originals_attack_[i].x + static_cast(std::round(shadow_offset_x)); pos_shadow.y = posicions_originals_attack_[i].y + static_cast(std::round(shadow_offset_y)); Rendering::render_shape( sdl_.obte_renderer(), lletres_attack_[i].forma, pos_shadow, 0.0f, Defaults::Title::Layout::LOGO_SCALE, true, 1.0f, // progress = 1.0 (totalment visible) SHADOW_BRIGHTNESS); } } // === RENDERITZAR LOGO PRINCIPAL (damunt) === // Dibuixar "ORNI" (línia 1) for (const auto& lletra : lletres_orni_) { Rendering::render_shape( sdl_.obte_renderer(), lletra.forma, lletra.posicio, 0.0f, Defaults::Title::Layout::LOGO_SCALE, true, 1.0f // Brillantor completa ); } // Dibuixar "ATTACK!" (línia 2) for (const auto& lletra : lletres_attack_) { Rendering::render_shape( sdl_.obte_renderer(), lletra.forma, lletra.posicio, 0.0f, Defaults::Title::Layout::LOGO_SCALE, true, 1.0f // Brillantor completa ); } // === Text "PRESS START TO PLAY" === // En estat MAIN: sempre visible // En estat TRANSITION: parpellejant (blink amb sinusoide) const float spacing = Defaults::Title::Layout::TEXT_SPACING; bool mostrar_text = true; if (estat_actual_ == EstatTitol::PLAYER_JOIN_PHASE) { // 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 START TO PLAY"; const float escala_main = Defaults::Title::Layout::PRESS_START_SCALE; float centre_x = Defaults::Game::WIDTH / 2.0f; float centre_y = Defaults::Game::HEIGHT * Defaults::Title::Layout::PRESS_START_POS; text_.render_centered(main_text, {centre_x, centre_y}, escala_main, spacing); } // === Copyright a la part inferior (centrat horitzontalment, dues línies) === const float escala_copy = Defaults::Title::Layout::COPYRIGHT_SCALE; const float copy_height = text_.get_text_height(escala_copy); const float line_spacing = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT_LINE_SPACING; // Línea 1: Original (© 1999 Visente i Sergi) std::string copyright_original = Project::COPYRIGHT_ORIGINAL; for (char& c : copyright_original) { if (c >= 'a' && c <= 'z') c = c - 32; // Uppercase } // Línea 2: Port (© 2025 jaildesigner) std::string copyright_port = Project::COPYRIGHT_PORT; for (char& c : copyright_port) { if (c >= 'a' && c <= 'z') c = c - 32; // Uppercase } // Calcular posicions (anclatge des del top + separació) float y_line1 = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS; float y_line2 = y_line1 + copy_height + line_spacing; // Línea 2 debajo de línea 1 // Renderitzar línees centrades float centre_x = Defaults::Game::WIDTH / 2.0f; text_.render_centered(copyright_original, {centre_x, y_line1}, escala_copy, spacing); text_.render_centered(copyright_port, {centre_x, y_line2}, escala_copy, spacing); } } auto EscenaTitol::checkSkipButtonPressed() -> bool { return Input::get()->checkAnyPlayerAction(ARCADE_BUTTONS); } auto EscenaTitol::checkStartGameButtonPressed() -> bool { auto* input = Input::get(); bool any_pressed = false; for (auto action : START_GAME_BUTTONS) { if (input->checkActionPlayer1(action, Input::DO_NOT_ALLOW_REPEAT)) { if (!config_partida_.jugador1_actiu) { config_partida_.jugador1_actiu = true; any_pressed = true; std::cout << "[EscenaTitol] P1 pressed START\n"; } } if (input->checkActionPlayer2(action, Input::DO_NOT_ALLOW_REPEAT)) { if (!config_partida_.jugador2_actiu) { config_partida_.jugador2_actiu = true; any_pressed = true; std::cout << "[EscenaTitol] P2 pressed START\n"; } } } return any_pressed; } void EscenaTitol::processar_events(const SDL_Event& event) { // No procesar eventos genéricos aquí - la lógica se movió a actualitzar() }