// escena_titol.cpp - Implementació de l'escena de títol // © 2025 Port a C++20 #include "title_scene.hpp" #include #include #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/scene_context.hpp" #include "core/system/global_events.hpp" #include "project.h" // Using declarations per simplificar el codi using SceneManager::SceneContext; using SceneType = SceneContext::SceneType; using Option = SceneContext::Option; TitleScene::TitleScene(SDLManager& sdl, SceneContext& context) : sdl_(sdl), context_(context), text_(sdl.getRenderer()), estat_actual_(TitleState::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 << "SceneType Titol: Inicialitzant...\n"; // Inicialitzar configuració de match (cap player active per defecte) match_config_.jugador1_actiu = false; match_config_.jugador2_actiu = false; match_config_.mode = GameConfig::Mode::NORMAL; // Processar opció del context auto option = context_.consumeOption(); if (option == Option::JUMP_TO_TITLE_MAIN) { std::cout << "SceneType Titol: Opció JUMP_TO_TITLE_MAIN activada\n"; estat_actual_ = TitleState::MAIN; temps_estat_main_ = 0.0F; } // Crear starfield de fons Vec2 centre_pantalla{ .x = Defaults::Game::WIDTH / 2.0F, .y = 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_.getRenderer(), centre_pantalla, area_completa, 150 // densitat: 150 estrelles (50 per capa) ); // Brightness depèn de l'opció if (estat_actual_ == TitleState::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_.getRenderer()); ship_animator_->init(); if (estat_actual_ == TitleState::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"); } } TitleScene::~TitleScene() { // Aturar música de títol quan es destrueix l'escena Audio::get()->stopMusic(); } void TitleScene::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 shape = ShapeLoader::load(fitxer); if (!shape || !shape->isValid()) { std::cerr << "[TitleScene] Error carregant " << fitxer << '\n'; continue; } // Calcular bounding box de la shape (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 : shape->get_primitives()) { for (const auto& point : prim.points) { min_x = std::min(min_x, point.x); max_x = std::max(max_x, point.x); min_y = std::min(min_y, point.y); max_y = std::max(max_y, point.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 = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE; lletres_orni_.push_back({shape, {.x = 0.0F, .y = 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.position.x = x_actual + lletra.offset_centre; lletra.position.y = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS; x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES; } std::cout << "[TitleScene] 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 << "[TitleScene] 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 shape = ShapeLoader::load(fitxer); if (!shape || !shape->isValid()) { std::cerr << "[TitleScene] Error carregant " << fitxer << '\n'; continue; } // Calcular bounding box de la shape (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 : shape->get_primitives()) { for (const auto& point : prim.points) { min_x = std::min(min_x, point.x); max_x = std::max(max_x, point.x); min_y = std::min(min_y, point.y); max_y = std::max(max_y, point.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 = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE; lletres_attack_.push_back({shape, {.x = 0.0F, .y = 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.position.x = x_actual + lletra.offset_centre; lletra.position.y = y_attack_dinamica_; // Usar posició dinàmica x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES; } std::cout << "[TitleScene] 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.position); } posicions_originals_attack_.clear(); for (const auto& lletra : lletres_attack_) { posicions_originals_attack_.push_back(lletra.position); } std::cout << "[TitleScene] Animació: Posicions originals guardades\n"; } void TitleScene::run() { SDL_Event event; Uint64 last_time = SDL_GetTicks(); while (SceneManager::actual == SceneType::TITLE) { // 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 delta_time = std::min(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 update(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'scale global) sdl_.updateRenderingContext(); // Dibuixar draw(); // Presentar renderer (swap buffers) sdl_.presenta(); } std::cout << "SceneType Titol: Finalitzant...\n"; } void TitleScene::update(float delta_time) { // Actualitzar starfield (sempre active) if (starfield_) { starfield_->update(delta_time); } // Actualitzar naus (quan visibles) if (ship_animator_ && (estat_actual_ == TitleState::STARFIELD_FADE_IN || estat_actual_ == TitleState::STARFIELD || estat_actual_ == TitleState::MAIN || estat_actual_ == TitleState::PLAYER_JOIN_PHASE)) { ship_animator_->update(delta_time); } switch (estat_actual_) { case TitleState::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_ = TitleState::STARFIELD; temps_acumulat_ = 0.0F; // Reset timer per al següent state starfield_->set_brightness(BRIGHTNESS_STARFIELD); // Assegurar valor final } break; } case TitleState::STARFIELD: temps_acumulat_ += delta_time; if (temps_acumulat_ >= DURACIO_INIT) { estat_actual_ = TitleState::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 TitleState::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 TitleState::PLAYER_JOIN_PHASE: temps_acumulat_ += delta_time; // Continuar animació orbital durant la transició actualitzar_animacio_logo(delta_time); // [NOU] Continuar comprovant si l'altre player vol unir-se durant la transició ("late join") { bool p1_actiu_abans = match_config_.jugador1_actiu; bool p2_actiu_abans = match_config_.jugador2_actiu; if (checkStartGameButtonPressed()) { // Updates match_config_ if pressed, logs are in the method context_.setMatchConfig(match_config_); // Trigger animació de sortida per la ship que acaba d'unir-se if (ship_animator_) { if (match_config_.jugador1_actiu && !p1_actiu_abans) { ship_animator_->trigger_exit_animation_for_player(1); std::cout << "[TitleScene] P1 late join - ship exiting\n"; } if (match_config_.jugador2_actiu && !p2_actiu_abans) { ship_animator_->trigger_exit_animation_for_player(2); std::cout << "[TitleScene] P2 late join - ship exiting\n"; } } // Reproducir so de START quan el segon player s'uneix Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME); // Reiniciar el timer per allargar el time de transició temps_acumulat_ = 0.0F; std::cout << "[TitleScene] Segon player s'ha unit - so i timer reiniciats\n"; } } if (temps_acumulat_ >= DURACIO_TRANSITION) { // Transició a pantalla negra estat_actual_ = TitleState::BLACK_SCREEN; temps_acumulat_ = 0.0F; std::cout << "[TitleScene] Passant a BLACK_SCREEN\n"; } break; case TitleState::BLACK_SCREEN: temps_acumulat_ += delta_time; // No animation, no input checking - just wait if (temps_acumulat_ >= DURACIO_BLACK_SCREEN) { // Transició a escena GAME SceneManager::actual = SceneType::GAME; std::cout << "[TitleScene] Canviant a escena GAME\n"; } break; } // Verificar botones de skip (FIRE/THRUST/START) para saltar escenas ANTES de MAIN if (estat_actual_ == TitleState::STARFIELD_FADE_IN || estat_actual_ == TitleState::STARFIELD) { if (checkSkipButtonPressed()) { // Saltar a MAIN estat_actual_ = TitleState::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 match desde MAIN if (estat_actual_ == TitleState::MAIN) { // Guardar state anterior per detectar qui ha premut START AQUEST frame bool p1_actiu_abans = match_config_.jugador1_actiu; bool p2_actiu_abans = match_config_.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 match abans de canviar d'escena context_.setMatchConfig(match_config_); std::cout << "[TitleScene] Configuració de match - P1: " << (match_config_.jugador1_actiu ? "ACTIU" : "INACTIU") << ", P2: " << (match_config_.jugador2_actiu ? "ACTIU" : "INACTIU") << '\n'; context_.setNextScene(SceneType::GAME); estat_actual_ = TitleState::PLAYER_JOIN_PHASE; temps_acumulat_ = 0.0F; // Trigger animació de sortida NOMÉS per les naus que han premut START if (ship_animator_) { if (match_config_.jugador1_actiu && !p1_actiu_abans) { ship_animator_->trigger_exit_animation_for_player(1); std::cout << "[TitleScene] P1 ship exiting\n"; } if (match_config_.jugador2_actiu && !p2_actiu_abans) { ship_animator_->trigger_exit_animation_for_player(2); std::cout << "[TitleScene] P2 ship exiting\n"; } } Audio::get()->fadeOutMusic(MUSIC_FADE); Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME); } } } void TitleScene::actualitzar_animacio_logo(float delta_time) { // Només calcular i aplicar offsets si l'animació està activa if (animacio_activa_) { // Acumular time 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].position.x = posicions_originals_orni_[i].x + static_cast(std::round(offset_x)); lletres_orni_[i].position.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].position.x = posicions_originals_attack_[i].x + static_cast(std::round(offset_x)); lletres_attack_[i].position.y = posicions_originals_attack_[i].y + static_cast(std::round(offset_y)); } } } void TitleScene::draw() { // Dibuixar starfield de fons (en tots els estats excepte BLACK_SCREEN) if (starfield_ && estat_actual_ != TitleState::BLACK_SCREEN) { starfield_->draw(); } // Dibuixar naus (després starfield, abans logo) if (ship_animator_ && (estat_actual_ == TitleState::STARFIELD_FADE_IN || estat_actual_ == TitleState::STARFIELD || estat_actual_ == TitleState::MAIN || estat_actual_ == TitleState::PLAYER_JOIN_PHASE)) { ship_animator_->draw(); } // En els estats STARFIELD_FADE_IN i STARFIELD, només mostrar starfield (sense text) if (estat_actual_ == TitleState::STARFIELD_FADE_IN || estat_actual_ == TitleState::STARFIELD) { return; } // Estat MAIN i PLAYER_JOIN_PHASE: Dibuixar títol i text (sobre el starfield) // BLACK_SCREEN: no draw res (fons negre ja està netejat) if (estat_actual_ == TitleState::MAIN || estat_actual_ == TitleState::PLAYER_JOIN_PHASE) { // === Calcular i renderitzar ombra (només si animació activa) === if (animacio_activa_) { float temps_shadow = temps_animacio_ - SHADOW_DELAY; temps_shadow = std::max(temps_shadow, 0.0F); // Evitar time 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) { Vec2 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_.getRenderer(), lletres_orni_[i].shape, pos_shadow, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F, // progress = 1.0 (totalment visible) SHADOW_BRIGHTNESS // brightness = 0.4 (brightness reduïda) ); } // Ombra "ATTACK!" for (size_t i = 0; i < lletres_attack_.size(); ++i) { Vec2 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_.getRenderer(), lletres_attack_[i].shape, pos_shadow, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 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_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F // Brillantor completa ); } // Dibuixar "ATTACK!" (línia 2) for (const auto& lletra : lletres_attack_) { Rendering::render_shape( sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F // Brillantor completa ); } // === Text "PRESS START TO PLAY" === // En state MAIN: sempre visible // En state TRANSITION: parpellejant (blink amb sinusoide) const float spacing = Defaults::Title::Layout::TEXT_SPACING; bool mostrar_text = true; if (estat_actual_ == TitleState::PLAYER_JOIN_PHASE) { // Parpelleig: sin oscil·la entre -1 i 1, volem ON quan > 0 float fase = temps_acumulat_ * BLINK_FREQUENCY * 2.0F * std::numbers::pi_v; // 2π × freq × time 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_.renderCentered(main_text, {.x = centre_x, .y = 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_.renderCentered(copyright_original, {.x = centre_x, .y = y_line1}, escala_copy, spacing); text_.renderCentered(copyright_port, {.x = centre_x, .y = y_line2}, escala_copy, spacing); } } auto TitleScene::checkSkipButtonPressed() -> bool { return Input::get()->checkAnyPlayerAction(ARCADE_BUTTONS); } auto TitleScene::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 (!match_config_.jugador1_actiu) { match_config_.jugador1_actiu = true; any_pressed = true; std::cout << "[TitleScene] P1 pressed START\n"; } } if (input->checkActionPlayer2(action, Input::DO_NOT_ALLOW_REPEAT)) { if (!match_config_.jugador2_actiu) { match_config_.jugador2_actiu = true; any_pressed = true; std::cout << "[TitleScene] P2 pressed START\n"; } } } return any_pressed; } void TitleScene::processar_events(const SDL_Event& event) { // No procesar eventos genéricos aquí - la lógica se movió a update() }