diff --git a/source/core/defaults/title.hpp b/source/core/defaults/title.hpp index aa5d851..02c1af8 100644 --- a/source/core/defaults/title.hpp +++ b/source/core/defaults/title.hpp @@ -81,7 +81,7 @@ namespace Defaults::Title { // Durades de animación constexpr float ENTRY_DURATION = 2.0F; // Entrada (segons) - constexpr float EXIT_DURATION = 1.0F; // Salida (segons) + constexpr float EXIT_DURATION = 1.5F; // Salida (segons) // Flotació (oscil·lació reduïda y diferenciada per ship) constexpr float FLOAT_AMPLITUDE_X = 4.0F; // Amplitud X (píxels) @@ -96,9 +96,6 @@ namespace Defaults::Title { constexpr float P1_ENTRY_DELAY = 0.0F; // P1 entra immediatament constexpr float P2_ENTRY_DELAY = 0.5F; // P2 entra 0.5s después - // Delay global antes de start l'animación de entrada al state MAIN - constexpr float ENTRANCE_DELAY = 5.0F; // Temps de espera antes que las naves entrin - // Multiplicadors de freqüència para cada ship (variació sutil ±12%) constexpr float P1_FREQUENCY_MULTIPLIER = 0.88F; // 12% més lenta constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; // 12% més ràpida @@ -128,6 +125,34 @@ namespace Defaults::Title { constexpr float TEXT_SPACING = 2.0F; } // namespace Layout + // Coreografia de la seqüència d'entrada al state MAIN. + // Tots els elements (logo, footer, naus, press start) entren ordenadament + // segons aquests thresholds. Vegeu title_scene.cpp/updateMainState. + // + // Per al logo i el footer, l'efecte simula un moviment 3D des de l'usuari + // cap al VP: el text arrenca gran i a la posició projectada extrema (com + // si estigués prop de la càmera, fora de pantalla) i acaba a la seva + // posició final amb escala normal (com si hagués aterrat al VP). Pivot: + // centre de pantalla (= projecció del VP 3D). + namespace Sequence { + // Factor d'escala inicial. >1 = sprite gran a l'inici (prop de l'usuari). + // La posició inicial es deriva: pivot=centre, delta multiplicat per aquest factor. + constexpr float LOGO_INTRO_SCALE_START = 2.5F; + constexpr float FOOTER_INTRO_SCALE_START = 2.5F; + + // Durades de les animacions d'entrada (segons). + constexpr float LOGO_ENTRY_DURATION = 1.2F; + constexpr float JAILGAMES_ENTRY_DURATION = 0.7F; + constexpr float COPYRIGHT_ENTRY_DURATION = 0.7F; + + // Stagger "pam-pam" entre l'arrencada de JAILGAMES i la de COPYRIGHT. + constexpr float COPYRIGHT_STAGGER = 0.18F; + + // Delays entre etapes. + constexpr float SHIPS_DELAY_AFTER_FOOTER = 0.20F; + constexpr float PRESS_START_DELAY_AFTER_SHIPS = 0.40F; + } // namespace Sequence + // Paleta neon de l'escena de títol (cian + magenta synthwave). // alpha = 255 (sentinela "color vàlid") fa que el pipeline ignori // el color global de l'oscil·lador per a aquesta crida. diff --git a/source/game/scenes/title_scene.cpp b/source/game/scenes/title_scene.cpp index 9712f3e..4d0fd03 100644 --- a/source/game/scenes/title_scene.cpp +++ b/source/game/scenes/title_scene.cpp @@ -14,6 +14,7 @@ #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" @@ -68,13 +69,9 @@ TitleScene::TitleScene(SDLManager& sdl, SceneContext& context) ship_animator_ = std::make_unique(sdl_.getRenderer(), camera_.get()); ship_animator_->init(); - - if (estat_actual_ == TitleState::MAIN) { - ship_animator_->setVisible(true); - ship_animator_->startEntryAnimation(); - } else { - ship_animator_->setVisible(false); - } + // 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); initTitle(); inicialitzarJailgames(); @@ -251,8 +248,23 @@ void TitleScene::inicialitzarJailgames() { } 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& lletra : lletres_jailgames_) { - Rendering::renderShape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::JAILGAMES_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::JAILGAMES_LOGO); + const Vec2 POS{ + .x = SCREEN_CENTRE_X + (JAILGAMES_S * (lletra.position.x - SCREEN_CENTRE_X)), + .y = SCREEN_CENTRE_Y + (JAILGAMES_S * (lletra.position.y - SCREEN_CENTRE_Y)), + }; + Rendering::renderShape(sdl_.getRenderer(), lletra.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) { @@ -260,9 +272,11 @@ void TitleScene::dibuixarPeuTitol(float spacing) const { 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, 1.0F, Defaults::Title::Colors::COPYRIGHT); + 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 { @@ -326,18 +340,48 @@ void TitleScene::updateStarfieldState(float delta_time) { void TitleScene::updateMainState(float delta_time) { temps_estat_main_ += delta_time; - if (temps_estat_main_ >= Defaults::Title::Ships::ENTRANCE_DELAY && - ship_animator_ && !ship_animator_->isVisible()) { + + 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(temps_estat_main_ / S::LOGO_ENTRY_DURATION, 0.0F, 1.0F); + intro_jailgames_progress_ = std::clamp( + (temps_estat_main_ - T_FOOTER_START) / S::JAILGAMES_ENTRY_DURATION, + 0.0F, + 1.0F); + intro_copyright_progress_ = std::clamp( + (temps_estat_main_ - T_COPY_START) / S::COPYRIGHT_ENTRY_DURATION, + 0.0F, + 1.0F); + + if (!ships_intro_launched_ && temps_estat_main_ >= T_SHIPS_START && + ship_animator_ != nullptr) { ship_animator_->setVisible(true); ship_animator_->startEntryAnimation(); + ships_intro_launched_ = true; } - if (temps_estat_main_ < DELAY_INICI_ANIMACIO) { + if (!press_start_visible_ && temps_estat_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 (temps_estat_main_ < S::LOGO_ENTRY_DURATION) { factor_lerp_ = 0.0F; animacio_activa_ = false; - } else if (temps_estat_main_ < DELAY_INICI_ANIMACIO + DURACIO_LERP) { - const float TEMPS_LERP = temps_estat_main_ - DELAY_INICI_ANIMACIO; - factor_lerp_ = TEMPS_LERP / DURACIO_LERP; + } else if (temps_estat_main_ < S::LOGO_ENTRY_DURATION + DURACIO_LERP) { + factor_lerp_ = (temps_estat_main_ - S::LOGO_ENTRY_DURATION) / DURACIO_LERP; animacio_activa_ = true; } else { factor_lerp_ = 1.0F; @@ -382,13 +426,36 @@ void TitleScene::handleSkipInput() { } estat_actual_ = TitleState::MAIN; starfield_->setBrightness(BRIGHTNESS_STARFIELD); - temps_estat_main_ = 0.0F; + + // 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; + temps_estat_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 (estat_actual_ != 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_.jugador1_actiu; const bool P2_ABANS = match_config_.jugador2_actiu; @@ -466,6 +533,14 @@ void TitleScene::draw() { 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 (animacio_activa_) { float temps_shadow = std::max(0.0F, temps_animacio_ - SHADOW_DELAY); const float TWO_PI = 2.0F * Defaults::Math::PI; @@ -473,41 +548,55 @@ void TitleScene::draw() { 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 < lletres_orni_.size(); ++i) { + const float BASE_X = posicions_originals_orni_[i].x + std::round(SHADOW_OX); + const float BASE_Y = posicions_originals_orni_[i].y + std::round(SHADOW_OY); const Vec2 POS_SHADOW{ - .x = posicions_originals_orni_[i].x + std::round(SHADOW_OX), - .y = posicions_originals_orni_[i].y + std::round(SHADOW_OY), + .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(), lletres_orni_[i].shape, POS_SHADOW, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F, SHADOW_BRIGHTNESS, Defaults::Title::Colors::LOGO_SHADOW); + Rendering::renderShape(sdl_.getRenderer(), lletres_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 < lletres_attack_.size(); ++i) { + const float BASE_X = posicions_originals_attack_[i].x + std::round(SHADOW_OX); + const float BASE_Y = posicions_originals_attack_[i].y + std::round(SHADOW_OY); const Vec2 POS_SHADOW{ - .x = posicions_originals_attack_[i].x + std::round(SHADOW_OX), - .y = posicions_originals_attack_[i].y + std::round(SHADOW_OY), + .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(), lletres_attack_[i].shape, POS_SHADOW, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F, SHADOW_BRIGHTNESS, Defaults::Title::Colors::LOGO_SHADOW); + Rendering::renderShape(sdl_.getRenderer(), lletres_attack_[i].shape, POS_SHADOW, 0.0F, LOGO_RENDER_SCALE, 1.0F, SHADOW_BRIGHTNESS, Defaults::Title::Colors::LOGO_SHADOW); } } for (const auto& lletra : lletres_orni_) { - Rendering::renderShape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::LOGO_MAIN); + const Vec2 POS{ + .x = SCREEN_CENTRE_X + (LOGO_S * (lletra.position.x - SCREEN_CENTRE_X)), + .y = SCREEN_CENTRE_Y + (LOGO_S * (lletra.position.y - SCREEN_CENTRE_Y)), + }; + Rendering::renderShape(sdl_.getRenderer(), lletra.shape, POS, 0.0F, LOGO_RENDER_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::LOGO_MAIN); } for (const auto& lletra : lletres_attack_) { - Rendering::renderShape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::LOGO_MAIN); + const Vec2 POS{ + .x = SCREEN_CENTRE_X + (LOGO_S * (lletra.position.x - SCREEN_CENTRE_X)), + .y = SCREEN_CENTRE_Y + (LOGO_S * (lletra.position.y - SCREEN_CENTRE_Y)), + }; + Rendering::renderShape(sdl_.getRenderer(), lletra.shape, POS, 0.0F, LOGO_RENDER_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::LOGO_MAIN); } const float SPACING = Defaults::Title::Layout::TEXT_SPACING; - bool mostrar_text = true; - if (estat_actual_ == 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); + if (press_start_visible_) { + bool mostrar_text = true; + if (estat_actual_ == 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); diff --git a/source/game/scenes/title_scene.hpp b/source/game/scenes/title_scene.hpp index 82457ef..1191f46 100644 --- a/source/game/scenes/title_scene.hpp +++ b/source/game/scenes/title_scene.hpp @@ -80,6 +80,13 @@ class TitleScene final : public Scene { bool animacio_activa_{false}; float factor_lerp_{0.0F}; + // Progresos de la intro coreografiada al state MAIN. + float intro_logo_progress_{0.0F}; + float intro_jailgames_progress_{0.0F}; + float intro_copyright_progress_{0.0F}; + bool press_start_visible_{false}; + bool ships_intro_launched_{false}; + static constexpr float BRIGHTNESS_STARFIELD = 1.2F; static constexpr float DURACIO_FADE_IN = 3.0F; static constexpr float DURACIO_INIT = 4.0F; @@ -100,7 +107,6 @@ class TitleScene final : public Scene { static constexpr float SHADOW_OFFSET_X = 2.0F; static constexpr float SHADOW_OFFSET_Y = 2.0F; - static constexpr float DELAY_INICI_ANIMACIO = 10.0F; static constexpr float DURACIO_LERP = 2.0F; // Càmera 3D: FOV vertical en radians. diff --git a/source/game/title/ship_animator.cpp b/source/game/title/ship_animator.cpp index 5a02ab2..a81acca 100644 --- a/source/game/title/ship_animator.cpp +++ b/source/game/title/ship_animator.cpp @@ -88,8 +88,10 @@ namespace Title { // Mida visual i animació. constexpr float SHIP_FLOAT_SCALE = 2.0F; constexpr float SHIP_ENTRY_SCALE = 2.0F; // Mida mundial idèntica; la perspectiva fa la resta - constexpr float ENTRY_DURATION = 2.0F; - constexpr float EXIT_DURATION = 1.5F; + // ENTRY_DURATION viu a Defaults::Title::Ships::ENTRY_DURATION (compartit + // amb title_scene.cpp per calcular el threshold T_SHIPS_LANDED). + constexpr float ENTRY_DURATION = Defaults::Title::Ships::ENTRY_DURATION; + constexpr float EXIT_DURATION = Defaults::Title::Ships::EXIT_DURATION; // Oscil·lació en unitats mundials (al voltant del target_position). constexpr float FLOAT_AMPLITUDE_X = 1.5F;