// title_scene.cpp - Implementació de l'escena de título // © 2026 JailDesigner #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/rendering/shape_renderer.hpp" #include "core/system/scene_context.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()) { std::cout << "SceneType Titol: Inicialitzant...\n"; // Inicialitzar configuración de match (sin player active per defecte) match_config_.jugador1_actiu = false; match_config_.jugador2_actiu = false; match_config_.mode = GameConfig::Mode::NORMAL; // Processar opción del context auto option = context_.consumeOption(); if (option == Option::JUMP_TO_TITLE_MAIN) { std::cout << "SceneType Titol: Opción 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ón if (estat_actual_ == TitleState::MAIN) { // Si saltem a MAIN, starfield instantàniament brillant starfield_->set_brightness(BRIGHTNESS_STARFIELD); } else { // Flux normal: comença con brightness 0.0 per fade-in starfield_->set_brightness(0.0F); } // Inicialitzar animador de naves 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ítulo "ORNI ATTACK!" inicialitzar_titol(); // Logo JAILGAMES pequeño sobre el copyright inferior. inicialitzarJailgames(); // Iniciar música de título si no está sonant if (Audio::getMusicState() != Audio::MusicState::PLAYING) { Audio::get()->playMusic("title.ogg"); } } TitleScene::~TitleScene() { // Aturar música de título cuando 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& file : fitxers_orni) { auto shape = ShapeLoader::load(file); if (!shape || !shape->isValid()) { std::cerr << "[TitleScene] Error carregant " << file << '\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 con 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; } // Añadir espaiat entre lletres ancho_total_orni += ESPAI_ENTRE_LLETRES * (lletres_orni_.size() - 1); // Calcular posición 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ón Y dinàmica per "ATTACK!" === // Todas las lletres ORNI tenen la misma 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& file : fitxers_attack) { auto shape = ShapeLoader::load(file); if (!shape || !shape->isValid()) { std::cerr << "[TitleScene] Error carregant " << file << '\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 con 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; } // Añadir espaiat entre lletres ancho_total_attack += ESPAI_ENTRE_LLETRES * (lletres_attack_.size() - 1); // Calcular posición 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ón 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ón 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ón: Posicions originals guardades\n"; } void TitleScene::inicialitzarJailgames() { using namespace Graphics; // Mismas letras que la LogoScene, mismo orden (J-A-I-L-G-A-M-E-S). const std::vector FITXERS = { "logo/letra_j.shp", "logo/letra_a.shp", "logo/letra_i.shp", "logo/letra_l.shp", "logo/letra_g.shp", "logo/letra_a.shp", "logo/letra_m.shp", "logo/letra_e.shp", "logo/letra_s.shp"}; constexpr float SCALE = Defaults::Title::Layout::JAILGAMES_SCALE; // Pas 1: carregar formes i calcular amplada/altura escalades. float ancho_total = 0.0F; float altura_max = 0.0F; for (const auto& file : FITXERS) { auto shape = ShapeLoader::load(file); if (!shape || !shape->isValid()) { std::cerr << "[TitleScene] Error carregant " << file << '\n'; continue; } 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); } } const float ANCHO = (max_x - min_x) * SCALE; const float ALTURA = (max_y - min_y) * SCALE; const float OFFSET_CENTRE = (shape->getCenter().x - min_x) * SCALE; lletres_jailgames_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ANCHO, ALTURA, OFFSET_CENTRE}); ancho_total += ANCHO; altura_max = std::max(altura_max, ALTURA); } // Espaiat entre lletres (proporcional a la escala, para que no quede pegado). constexpr float ESPAI_JAILGAMES = ESPAI_ENTRE_LLETRES * SCALE; if (!lletres_jailgames_.empty()) { ancho_total += ESPAI_JAILGAMES * static_cast(lletres_jailgames_.size() - 1); } // Pas 2: centrar horizontalmente y colocar JUST encima de la línea de copyright. const float Y_COPYRIGHT = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS; const float GAP = Defaults::Game::HEIGHT * Defaults::Title::Layout::JAILGAMES_COPYRIGHT_GAP; const float Y_CENTRE = Y_COPYRIGHT - GAP - (altura_max / 2.0F); const float X_INICIAL = (Defaults::Game::WIDTH - ancho_total) / 2.0F; float x_actual = X_INICIAL; for (auto& lletra : lletres_jailgames_) { lletra.position.x = x_actual + lletra.offset_centre; lletra.position.y = Y_CENTRE; x_actual += lletra.ancho + ESPAI_JAILGAMES; } } void TitleScene::dibuixarPeuTitol(float spacing) const { // Logo JAILGAMES pequeño sobre el copyright. for (const auto& lletra : lletres_jailgames_) { Rendering::render_shape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::JAILGAMES_SCALE, 1.0F); } // Copyright en una sola línea, centrado, en mayúsculas. std::string copyright = Project::COPYRIGHT; for (char& c : copyright) { if (c >= 'a' && c <= 'z') { c = static_cast(c - 32); } } const float Y_COPY = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS; const float CENTRE_X = Defaults::Game::WIDTH / 2.0F; text_.renderCentered(copyright, {.x = CENTRE_X, .y = Y_COPY}, Defaults::Title::Layout::COPYRIGHT_SCALE, spacing); } auto TitleScene::isFinished() const -> bool { return context_.nextScene() != SceneType::TITLE; } void TitleScene::update(float delta_time) { // Actualitzar starfield (siempre active) if (starfield_) { starfield_->update(delta_time); } // Actualitzar naves (cuando 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ón a STARFIELD cuando 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 value 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; // Sin animación aún // Naves esperaran ENTRANCE_DELAY antes de entrar (no start aquí) } break; case TitleState::MAIN: { temps_estat_main_ += delta_time; // Iniciar animación de entrada de naves despué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ón completa (12s+) else { factor_lerp_ = 1.0F; animacio_activa_ = true; } // Actualitzar animación del logo actualitzar_animacio_logo(delta_time); break; } case TitleState::PLAYER_JOIN_PHASE: temps_acumulat_ += delta_time; // Continuar animación orbital durante la transición actualitzar_animacio_logo(delta_time); // [NOU] Continuar comprovant si l'altre player quiere unir-se durante la transición ("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ón de salida per la ship que acaba de 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 cuando el segon player s'uneix Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME); // Reiniciar el timer per allargar el time de transición temps_acumulat_ = 0.0F; std::cout << "[TitleScene] Segon player s'ha unit - so i timer reiniciats\n"; } } if (temps_acumulat_ >= DURACIO_TRANSITION) { // Transición 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ón a escena GAME (el Director detecta isFinished()). context_.setNextScene(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; // Naves esperaran ENTRANCE_DELAY antes de entrar (no start aquí) } } // Verificar boton START para start 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 durante el delay (naves aún invisibles), saltar-las a FLOATING if (ship_animator_ && !ship_animator_->is_visible()) { ship_animator_->set_visible(true); ship_animator_->skip_to_floating_state(); } // Configurar match antes de canviar de escena context_.setMatchConfig(match_config_); std::cout << "[TitleScene] Configuración de match - P1: " << (match_config_.jugador1_actiu ? "ACTIU" : "INACTIU") << ", P2: " << (match_config_.jugador2_actiu ? "ACTIU" : "INACTIU") << '\n'; // El setNextScene a GAME se hace al final de BLACK_SCREEN para no // saltar la animación de salida (isFinished() lo recoge entonces). estat_actual_ = TitleState::PLAYER_JOIN_PHASE; temps_acumulat_ = 0.0F; // Trigger animación de salida NOMÉS per las naves 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) { // Solo calcular i aplicar offsets si l'animación 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 todas las 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 todas las 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 todos los estats excepte BLACK_SCREEN) if (starfield_ && estat_actual_ != TitleState::BLACK_SCREEN) { starfield_->draw(); } // Dibuixar naves (después starfield, antes 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 los estats STARFIELD_FADE_IN i STARFIELD, solo mostrar starfield (sin text) if (estat_actual_ == TitleState::STARFIELD_FADE_IN || estat_actual_ == TitleState::STARFIELD) { return; } // Estat MAIN i PLAYER_JOIN_PHASE: Dibuixar título i text (sobre el starfield) // BLACK_SCREEN: no draw res (fons negre ya está netejat) if (estat_actual_ == TitleState::MAIN || estat_actual_ == TitleState::PLAYER_JOIN_PHASE) { // === Calcular i renderizar ombra (solo si animación 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: siempre visible // En state TRANSITION: parpellejant (blink con 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 cuando > 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 MAIN_SCALE = 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}, MAIN_SCALE, SPACING); } dibuixarPeuTitol(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::handleEvent(const SDL_Event& event) { // La lógica de input se decide en update() consultando Input::checkAction; // aquí no hay eventos puntuales que procesar. (void)event; }