// title_scene.cpp - Implementació de l'escena de títol 3D real // © 2026 JailDesigner #include "title_scene.hpp" #include #include #include #include #include #include #include "core/audio/audio.hpp" #include "core/defaults.hpp" #include "core/graphics/shape_loader.hpp" #include "core/input/input.hpp" #include "core/math/easing.hpp" #include "core/rendering/shape_renderer.hpp" #include "core/system/scene_context.hpp" #include "project.h" using SceneManager::SceneContext; using SceneType = SceneContext::SceneType; using Option = SceneContext::Option; namespace { // Botons per iniciar partida des de MAIN (només START). constexpr std::array START_GAME_BUTTONS = {InputAction::START}; } // namespace TitleScene::TitleScene(SDLManager& sdl, SceneContext& context) : sdl_(sdl), context_(context), text_(sdl.getRenderer()) { std::cout << "SceneType Titol: Inicialitzant...\n"; match_config_.player1_active = false; match_config_.player2_active = false; match_config_.mode = GameConfig::Mode::NORMAL; auto option = context_.consumeOption(); if (option == Option::JUMP_TO_TITLE_MAIN) { std::cout << "SceneType Titol: Opció JUMP_TO_TITLE_MAIN activada\n"; current_state_ = TitleState::MAIN; state_time_main_ = 0.0F; } // Càmera 3D: posicionada a l'origen, mirant cap a +Z, amb Y cap amunt. camera_ = std::make_unique( Vec3{.x = 0.0F, .y = 0.0F, .z = 0.0F}, Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F}, Vec3{.x = 0.0F, .y = 1.0F, .z = 0.0F}, CAMERA_FOV_Y_RAD, static_cast(Defaults::Game::WIDTH), static_cast(Defaults::Game::HEIGHT)); starfield_ = std::make_unique( sdl_.getRenderer(), camera_.get(), 200); starfield_->setColor(Defaults::Title::Colors::STARFIELD); if (current_state_ == TitleState::MAIN) { starfield_->setBrightness(BRIGHTNESS_STARFIELD); } else { starfield_->setBrightness(0.0F); } ship_animator_ = std::make_unique(sdl_.getRenderer(), camera_.get()); ship_animator_->init(); // Les naus comencen invisibles; updateMainState() les dispara al moment // correcte de la intro coreografiada (també quan venim de JUMP_TO_TITLE_MAIN). ship_animator_->setVisible(false); // Flash que tapa el "pop" final de la nau al VP. Es spawneja al centre // de pantalla (= projecció del VP) quan ship_animator avisa. flash_shape_ = Graphics::ShapeLoader::load("title_flash.shp"); ship_animator_->setOnShipDisappear([this](int /*player_id*/) { triggerFlash(Vec2{ .x = static_cast(Defaults::Window::WIDTH) / 2.0F, .y = static_cast(Defaults::Window::HEIGHT) / 2.0F}); }); initTitle(); inicialitzarJailgames(); if (Audio::getMusicState() != Audio::MusicState::PLAYING) { Audio::get()->playMusic("title.ogg"); } } TitleScene::~TitleScene() { Audio::get()->stopMusic(); } void TitleScene::initTitle() { using namespace Graphics; const std::vector FITXERS_ORNI = { "title/letra_o.shp", "title/letra_r.shp", "title/letra_n.shp", "title/letra_i.shp"}; float total_width_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; } 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->getPrimitives()) { 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 WIDTH = (max_x - min_x) * Defaults::Title::Layout::LOGO_SCALE; const float HEIGHT = (max_y - min_y) * Defaults::Title::Layout::LOGO_SCALE; const float CENTER_OFFSET = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE; letters_orni_.push_back({shape, {.x = 0.0F, .y = 0.0F}, WIDTH, HEIGHT, CENTER_OFFSET}); total_width_orni += WIDTH; } total_width_orni += LETTER_SPACING * static_cast(letters_orni_.size() - 1); float x_actual = (Defaults::Game::WIDTH - total_width_orni) / 2.0F; for (auto& letter : letters_orni_) { letter.position.x = x_actual + letter.center_offset; letter.position.y = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS; x_actual += letter.width + LETTER_SPACING; } const float ORNI_HEIGHT = letters_orni_.empty() ? 50.0F : letters_orni_[0].height; const float Y_ORNI = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS; const float SEPARACION = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_LINE_SPACING; dynamic_attack_y_ = Y_ORNI + ORNI_HEIGHT + SEPARACION; const std::vector FITXERS_ATTACK = { "title/letra_a.shp", "title/letra_t.shp", "title/letra_t.shp", "title/letra_a.shp", "title/letra_c.shp", "title/letra_k.shp", "title/letra_exclamacion.shp"}; float total_width_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; } 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->getPrimitives()) { 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 WIDTH = (max_x - min_x) * Defaults::Title::Layout::LOGO_SCALE; const float HEIGHT = (max_y - min_y) * Defaults::Title::Layout::LOGO_SCALE; const float CENTER_OFFSET = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE; letters_attack_.push_back({shape, {.x = 0.0F, .y = 0.0F}, WIDTH, HEIGHT, CENTER_OFFSET}); total_width_attack += WIDTH; } total_width_attack += LETTER_SPACING * static_cast(letters_attack_.size() - 1); x_actual = (Defaults::Game::WIDTH - total_width_attack) / 2.0F; for (auto& letter : letters_attack_) { letter.position.x = x_actual + letter.center_offset; letter.position.y = dynamic_attack_y_; x_actual += letter.width + LETTER_SPACING; } original_positions_orni_.clear(); for (const auto& letter : letters_orni_) { original_positions_orni_.push_back(letter.position); } original_positions_attack_.clear(); for (const auto& letter : letters_attack_) { original_positions_attack_.push_back(letter.position); } } void TitleScene::inicialitzarJailgames() { using namespace Graphics; 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; float total_width = 0.0F; float max_height = 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->getPrimitives()) { 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 WIDTH = (max_x - min_x) * SCALE; const float HEIGHT = (max_y - min_y) * SCALE; const float CENTER_OFFSET = (shape->getCenter().x - min_x) * SCALE; letters_jailgames_.push_back({shape, {.x = 0.0F, .y = 0.0F}, WIDTH, HEIGHT, CENTER_OFFSET}); total_width += WIDTH; max_height = std::max(max_height, HEIGHT); } constexpr float JAILGAMES_SPACING = LETTER_SPACING * SCALE; if (!letters_jailgames_.empty()) { total_width += JAILGAMES_SPACING * static_cast(letters_jailgames_.size() - 1); } const float Y_COPY = 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_COPY - GAP - (max_height / 2.0F); const float X_INICIAL = (Defaults::Game::WIDTH - total_width) / 2.0F; float x_actual = X_INICIAL; for (auto& letter : letters_jailgames_) { letter.position.x = x_actual + letter.center_offset; letter.position.y = Y_CENTRE; x_actual += letter.width + JAILGAMES_SPACING; } } void TitleScene::dibuixarPeuTitol(float spacing) const { namespace S = Defaults::Title::Sequence; // Pivot al centre de pantalla (= projecció VP). Cada element s'expandeix // des d'aquí mentre s_factor passa de SCALE_START (gran, prop de l'usuari) // a 1.0 (a la mida i posició finals, "lluny" al VP). const float SCREEN_CENTRE_X = Defaults::Game::WIDTH / 2.0F; const float SCREEN_CENTRE_Y = Defaults::Game::HEIGHT / 2.0F; const float JAILGAMES_S = std::lerp(S::FOOTER_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_jailgames_progress_)); const float COPYRIGHT_S = std::lerp(S::FOOTER_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_copyright_progress_)); const float JAILGAMES_RENDER_SCALE = Defaults::Title::Layout::JAILGAMES_SCALE * JAILGAMES_S; for (const auto& letter : letters_jailgames_) { const Vec2 POS{ .x = SCREEN_CENTRE_X + (JAILGAMES_S * (letter.position.x - SCREEN_CENTRE_X)), .y = SCREEN_CENTRE_Y + (JAILGAMES_S * (letter.position.y - SCREEN_CENTRE_Y)), }; Rendering::renderShape(sdl_.getRenderer(), letter.shape, POS, 0.0F, JAILGAMES_RENDER_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::JAILGAMES_LOGO); } std::string copyright = Project::COPYRIGHT; for (char& c : copyright) { if (c >= 'a' && c <= 'z') { c = static_cast(c - 32); } } const float Y_COPY_FINAL = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS; const float COPY_X = SCREEN_CENTRE_X; // ja al centre const float COPY_Y = SCREEN_CENTRE_Y + (COPYRIGHT_S * (Y_COPY_FINAL - SCREEN_CENTRE_Y)); const float COPY_RENDER_SCALE = Defaults::Title::Layout::COPYRIGHT_SCALE * COPYRIGHT_S; text_.renderCentered(copyright, {.x = COPY_X, .y = COPY_Y}, COPY_RENDER_SCALE, spacing, 1.0F, Defaults::Title::Colors::COPYRIGHT); } auto TitleScene::isFinished() const -> bool { return context_.nextScene() != SceneType::TITLE; } void TitleScene::update(float delta_time) { if (starfield_) { starfield_->update(delta_time); } if (ship_animator_ && (current_state_ == TitleState::STARFIELD_FADE_IN || current_state_ == TitleState::STARFIELD || current_state_ == TitleState::MAIN || current_state_ == TitleState::PLAYER_JOIN_PHASE)) { ship_animator_->update(delta_time); } updateFlashes(delta_time); switch (current_state_) { case TitleState::STARFIELD_FADE_IN: updateStarfieldFadeInState(delta_time); break; case TitleState::STARFIELD: updateStarfieldState(delta_time); break; case TitleState::MAIN: updateMainState(delta_time); break; case TitleState::PLAYER_JOIN_PHASE: updatePlayerJoinPhaseState(delta_time); break; case TitleState::BLACK_SCREEN: updateBlackScreenState(delta_time); break; } handleSkipInput(); handleStartInput(); } void TitleScene::updateStarfieldFadeInState(float delta_time) { temps_acumulat_ += delta_time; const float PROGRESS = std::min(1.0F, temps_acumulat_ / DURATION_FADE_IN); starfield_->setBrightness(PROGRESS * BRIGHTNESS_STARFIELD); if (temps_acumulat_ >= DURATION_FADE_IN) { current_state_ = TitleState::STARFIELD; temps_acumulat_ = 0.0F; starfield_->setBrightness(BRIGHTNESS_STARFIELD); } } void TitleScene::updateStarfieldState(float delta_time) { temps_acumulat_ += delta_time; if (temps_acumulat_ >= DURATION_INIT) { current_state_ = TitleState::MAIN; state_time_main_ = 0.0F; animation_active_ = false; lerp_factor_ = 0.0F; } } void TitleScene::updateMainState(float delta_time) { state_time_main_ += delta_time; namespace S = Defaults::Title::Sequence; namespace Sh = Defaults::Title::Ships; // Thresholds derivats de la coreografia (vegeu Defaults::Title::Sequence). constexpr float T_FOOTER_START = S::LOGO_ENTRY_DURATION; constexpr float T_COPY_START = T_FOOTER_START + S::COPYRIGHT_STAGGER; constexpr float T_JAILGAMES_END = T_FOOTER_START + S::JAILGAMES_ENTRY_DURATION; constexpr float T_COPYRIGHT_END = T_COPY_START + S::COPYRIGHT_ENTRY_DURATION; constexpr float T_FOOTER_END = std::max(T_JAILGAMES_END, T_COPYRIGHT_END); constexpr float T_SHIPS_START = T_FOOTER_END + S::SHIPS_DELAY_AFTER_FOOTER; constexpr float T_SHIPS_LANDED = T_SHIPS_START + Sh::ENTRY_DURATION + Sh::P2_ENTRY_DELAY; constexpr float T_PRESS_START_VISIBLE = T_SHIPS_LANDED + S::PRESS_START_DELAY_AFTER_SHIPS; intro_logo_progress_ = std::clamp(state_time_main_ / S::LOGO_ENTRY_DURATION, 0.0F, 1.0F); intro_jailgames_progress_ = std::clamp( (state_time_main_ - T_FOOTER_START) / S::JAILGAMES_ENTRY_DURATION, 0.0F, 1.0F); intro_copyright_progress_ = std::clamp( (state_time_main_ - T_COPY_START) / S::COPYRIGHT_ENTRY_DURATION, 0.0F, 1.0F); if (!ships_intro_launched_ && state_time_main_ >= T_SHIPS_START && ship_animator_ != nullptr) { ship_animator_->setVisible(true); ship_animator_->startEntryAnimation(); ships_intro_launched_ = true; } if (!press_start_visible_ && state_time_main_ >= T_PRESS_START_VISIBLE) { press_start_visible_ = true; } // L'oscil·lació suau del logo arrenca quan el logo ha aterrat. Així // l'amplitud creix gradualment (lerp) durant la resta de la intro. if (state_time_main_ < S::LOGO_ENTRY_DURATION) { lerp_factor_ = 0.0F; animation_active_ = false; } else if (state_time_main_ < S::LOGO_ENTRY_DURATION + DURATION_LERP) { lerp_factor_ = (state_time_main_ - S::LOGO_ENTRY_DURATION) / DURATION_LERP; animation_active_ = true; } else { lerp_factor_ = 1.0F; animation_active_ = true; } updateLogoAnimation(delta_time); } void TitleScene::updatePlayerJoinPhaseState(float delta_time) { temps_acumulat_ += delta_time; updateLogoAnimation(delta_time); const bool P1_ABANS = match_config_.player1_active; const bool P2_ABANS = match_config_.player2_active; if (checkStartGameButtonPressed()) { context_.setMatchConfig(match_config_); triggerExitForJoinedPlayers(P1_ABANS, P2_ABANS, "late join - "); Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME); temps_acumulat_ = 0.0F; } if (temps_acumulat_ >= DURATION_TRANSITION) { current_state_ = TitleState::BLACK_SCREEN; temps_acumulat_ = 0.0F; } } void TitleScene::updateBlackScreenState(float delta_time) { temps_acumulat_ += delta_time; if (temps_acumulat_ >= DURATION_BLACK_SCREEN) { context_.setNextScene(SceneType::GAME); } } void TitleScene::handleSkipInput() { if (current_state_ != TitleState::STARFIELD_FADE_IN && current_state_ != TitleState::STARFIELD) { return; } if (!checkSkipButtonPressed()) { return; } current_state_ = TitleState::MAIN; starfield_->setBrightness(BRIGHTNESS_STARFIELD); // Saltar la intro coreografiada: deixar tots els elements ja in-place. namespace S = Defaults::Title::Sequence; namespace Sh = Defaults::Title::Ships; constexpr float T_FOOTER_END = std::max( S::LOGO_ENTRY_DURATION + S::JAILGAMES_ENTRY_DURATION, S::LOGO_ENTRY_DURATION + S::COPYRIGHT_STAGGER + S::COPYRIGHT_ENTRY_DURATION); constexpr float T_PRESS_START_VISIBLE = T_FOOTER_END + S::SHIPS_DELAY_AFTER_FOOTER + Sh::ENTRY_DURATION + Sh::P2_ENTRY_DELAY + S::PRESS_START_DELAY_AFTER_SHIPS; state_time_main_ = T_PRESS_START_VISIBLE; intro_logo_progress_ = 1.0F; intro_jailgames_progress_ = 1.0F; intro_copyright_progress_ = 1.0F; press_start_visible_ = true; ships_intro_launched_ = true; if (ship_animator_ != nullptr) { ship_animator_->setVisible(true); ship_animator_->startEntryAnimation(); } } void TitleScene::handleStartInput() { if (current_state_ != TitleState::MAIN) { return; } // No acceptar START fins que la intro coreografiada haja conclòs i el // text "PRESS START TO PLAY" siga visible. if (!press_start_visible_) { return; } const bool P1_ABANS = match_config_.player1_active; const bool P2_ABANS = match_config_.player2_active; if (!checkStartGameButtonPressed()) { return; } if (ship_animator_ && !ship_animator_->isVisible()) { ship_animator_->setVisible(true); ship_animator_->skipToFloatingState(); } context_.setMatchConfig(match_config_); current_state_ = TitleState::PLAYER_JOIN_PHASE; temps_acumulat_ = 0.0F; triggerExitForJoinedPlayers(P1_ABANS, P2_ABANS, ""); Audio::get()->fadeOutMusic(MUSIC_FADE); Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME); } void TitleScene::triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, const char* log_prefix) { if (ship_animator_ == nullptr) { return; } if (match_config_.player1_active && !p1_was_active) { ship_animator_->triggerExitAnimationForPlayer(1); std::cout << "[TitleScene] P1 " << log_prefix << "ship exiting\n"; } if (match_config_.player2_active && !p2_was_active) { ship_animator_->triggerExitAnimationForPlayer(2); std::cout << "[TitleScene] P2 " << log_prefix << "ship exiting\n"; } } void TitleScene::updateLogoAnimation(float delta_time) { if (!animation_active_) { return; } animation_time_ += delta_time * lerp_factor_; const float TWO_PI = 2.0F * Defaults::Math::PI; const float OFFSET_X = ORBIT_AMPLITUDE_X * std::sin(TWO_PI * ORBIT_FREQUENCY_X * animation_time_); const float OFFSET_Y = ORBIT_AMPLITUDE_Y * std::sin((TWO_PI * ORBIT_FREQUENCY_Y * animation_time_) + ORBIT_PHASE_OFFSET); for (std::size_t i = 0; i < letters_orni_.size(); ++i) { letters_orni_[i].position.x = original_positions_orni_[i].x + std::round(OFFSET_X); letters_orni_[i].position.y = original_positions_orni_[i].y + std::round(OFFSET_Y); } for (std::size_t i = 0; i < letters_attack_.size(); ++i) { letters_attack_[i].position.x = original_positions_attack_[i].x + std::round(OFFSET_X); letters_attack_[i].position.y = original_positions_attack_[i].y + std::round(OFFSET_Y); } } void TitleScene::draw() { if (starfield_ && current_state_ != TitleState::BLACK_SCREEN) { starfield_->draw(); } if (ship_animator_ && (current_state_ == TitleState::STARFIELD_FADE_IN || current_state_ == TitleState::STARFIELD || current_state_ == TitleState::MAIN || current_state_ == TitleState::PLAYER_JOIN_PHASE)) { ship_animator_->draw(); } drawFlashes(); if (current_state_ == TitleState::STARFIELD_FADE_IN || current_state_ == TitleState::STARFIELD) { return; } if (current_state_ != TitleState::MAIN && current_state_ != TitleState::PLAYER_JOIN_PHASE) { return; } // Factor d'escala+posició per simular un moviment 3D des de l'usuari (prop, // sprite gran i posició projectada extrema) cap al VP (lluny, sprite a la // seva mida i posició finals). Pivot: centre de pantalla (= projecció VP). const float LOGO_S = std::lerp(Defaults::Title::Sequence::LOGO_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_logo_progress_)); const float SCREEN_CENTRE_X = Defaults::Game::WIDTH / 2.0F; const float SCREEN_CENTRE_Y = Defaults::Game::HEIGHT / 2.0F; const float LOGO_RENDER_SCALE = Defaults::Title::Layout::LOGO_SCALE * LOGO_S; if (animation_active_) { float temps_shadow = std::max(0.0F, animation_time_ - SHADOW_DELAY); const float TWO_PI = 2.0F * Defaults::Math::PI; const float SHADOW_OX = (ORBIT_AMPLITUDE_X * std::sin(TWO_PI * ORBIT_FREQUENCY_X * temps_shadow)) + SHADOW_OFFSET_X; const float SHADOW_OY = (ORBIT_AMPLITUDE_Y * std::sin((TWO_PI * ORBIT_FREQUENCY_Y * temps_shadow) + ORBIT_PHASE_OFFSET)) + SHADOW_OFFSET_Y; for (std::size_t i = 0; i < letters_orni_.size(); ++i) { const float BASE_X = original_positions_orni_[i].x + std::round(SHADOW_OX); const float BASE_Y = original_positions_orni_[i].y + std::round(SHADOW_OY); const Vec2 POS_SHADOW{ .x = SCREEN_CENTRE_X + (LOGO_S * (BASE_X - SCREEN_CENTRE_X)), .y = SCREEN_CENTRE_Y + (LOGO_S * (BASE_Y - SCREEN_CENTRE_Y)), }; Rendering::renderShape(sdl_.getRenderer(), letters_orni_[i].shape, POS_SHADOW, 0.0F, LOGO_RENDER_SCALE, 1.0F, SHADOW_BRIGHTNESS, Defaults::Title::Colors::LOGO_SHADOW); } for (std::size_t i = 0; i < letters_attack_.size(); ++i) { const float BASE_X = original_positions_attack_[i].x + std::round(SHADOW_OX); const float BASE_Y = original_positions_attack_[i].y + std::round(SHADOW_OY); const Vec2 POS_SHADOW{ .x = SCREEN_CENTRE_X + (LOGO_S * (BASE_X - SCREEN_CENTRE_X)), .y = SCREEN_CENTRE_Y + (LOGO_S * (BASE_Y - SCREEN_CENTRE_Y)), }; Rendering::renderShape(sdl_.getRenderer(), letters_attack_[i].shape, POS_SHADOW, 0.0F, LOGO_RENDER_SCALE, 1.0F, SHADOW_BRIGHTNESS, Defaults::Title::Colors::LOGO_SHADOW); } } for (const auto& letter : letters_orni_) { const Vec2 POS{ .x = SCREEN_CENTRE_X + (LOGO_S * (letter.position.x - SCREEN_CENTRE_X)), .y = SCREEN_CENTRE_Y + (LOGO_S * (letter.position.y - SCREEN_CENTRE_Y)), }; Rendering::renderShape(sdl_.getRenderer(), letter.shape, POS, 0.0F, LOGO_RENDER_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::LOGO_MAIN); } for (const auto& letter : letters_attack_) { const Vec2 POS{ .x = SCREEN_CENTRE_X + (LOGO_S * (letter.position.x - SCREEN_CENTRE_X)), .y = SCREEN_CENTRE_Y + (LOGO_S * (letter.position.y - SCREEN_CENTRE_Y)), }; Rendering::renderShape(sdl_.getRenderer(), letter.shape, POS, 0.0F, LOGO_RENDER_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::LOGO_MAIN); } const float SPACING = Defaults::Title::Layout::TEXT_SPACING; if (press_start_visible_) { bool mostrar_text = true; if (current_state_ == TitleState::PLAYER_JOIN_PHASE) { const float FASE = temps_acumulat_ * BLINK_FREQUENCY * 2.0F * std::numbers::pi_v; 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; const float CENTRE_X = Defaults::Game::WIDTH / 2.0F; const 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, 1.0F, Defaults::Title::Colors::PRESS_START); } } 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_.player1_active) { match_config_.player1_active = true; any_pressed = true; } } if (input->checkActionPlayer2(action, Input::DO_NOT_ALLOW_REPEAT)) { if (!match_config_.player2_active) { match_config_.player2_active = true; any_pressed = true; } } } return any_pressed; } void TitleScene::handleEvent(const SDL_Event& event) { (void)event; } namespace { constexpr float FLASH_DURATION = 0.40F; constexpr float FLASH_MAX_SCALE = 2.5F; constexpr SDL_Color FLASH_COLOR = {.r = 255, .g = 255, .b = 255, .a = 255}; } // namespace void TitleScene::triggerFlash(Vec2 pos) { for (auto& f : flashes_) { if (!f.active) { f.active = true; f.position = pos; f.timer = 0.0F; return; } } } void TitleScene::updateFlashes(float delta_time) { for (auto& f : flashes_) { if (!f.active) { continue; } f.timer += delta_time; if (f.timer >= FLASH_DURATION) { f.active = false; } } } void TitleScene::drawFlashes() { if (!flash_shape_) { return; } for (const auto& f : flashes_) { if (!f.active) { continue; } // Escala 0 → max al midpoint → 0. Sinus simètric. const float T_NORM = f.timer / FLASH_DURATION; const float SCALE = FLASH_MAX_SCALE * std::sin(T_NORM * Defaults::Math::PI); Rendering::renderShape(sdl_.getRenderer(), flash_shape_, f.position, 0.0F, SCALE, 1.0F, 1.0F, FLASH_COLOR); } }