feat(title): intro coreografiada amb logo, footer i naus escalonats

Logo cau des de dalt; quan aterra, JAILGAMES i COPYRIGHT pugen des de
baix amb stagger pam-pam; després arrenquen les naus i, en aterrar
elles, apareix PRESS START. Magic numbers a Defaults::Title::Sequence.
This commit is contained in:
2026-05-22 13:51:09 +02:00
parent 03209ee23b
commit 2ca2062011
4 changed files with 132 additions and 38 deletions
+22 -4
View File
@@ -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,27 @@ 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.
namespace Sequence {
// Offsets fora-pantalla per a l'animació d'entrada (additius a la posició final).
constexpr float LOGO_OFFSCREEN_OFFSET_Y = -240.0F; // logo entra des de dalt
constexpr float FOOTER_OFFSCREEN_OFFSET_Y = +160.0F; // jailgames/copyright entren des de baix
// 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.
+99 -31
View File
@@ -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<Title::ShipAnimator>(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,16 @@ void TitleScene::inicialitzarJailgames() {
}
void TitleScene::dibuixarPeuTitol(float spacing) const {
namespace S = Defaults::Title::Sequence;
const float JAILGAMES_Y_INTRO = (1.0F - Easing::easeOutQuad(intro_jailgames_progress_)) *
S::FOOTER_OFFSCREEN_OFFSET_Y;
const float COPYRIGHT_Y_INTRO = (1.0F - Easing::easeOutQuad(intro_copyright_progress_)) *
S::FOOTER_OFFSCREEN_OFFSET_Y;
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 = lletra.position.x, .y = lletra.position.y + JAILGAMES_Y_INTRO};
Rendering::renderShape(sdl_.getRenderer(), lletra.shape, POS, 0.0F, Defaults::Title::Layout::JAILGAMES_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::JAILGAMES_LOGO);
}
std::string copyright = Project::COPYRIGHT;
for (char& c : copyright) {
@@ -260,7 +265,7 @@ void TitleScene::dibuixarPeuTitol(float spacing) const {
c = static_cast<char>(c - 32);
}
}
const float Y_COPY = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS;
const float Y_COPY = (Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS) + COPYRIGHT_Y_INTRO;
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);
}
@@ -326,18 +331,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 +417,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 +524,12 @@ void TitleScene::draw() {
return;
}
// Offset additiu de la intro del logo (cau des de dalt). Quan progress=1,
// el logo està al seu lloc i aquest offset val 0 — l'oscil·lació suau
// (updateLogoAnimation) continua treballant sobre la posició base.
const float LOGO_Y_INTRO = (1.0F - Easing::easeOutQuad(intro_logo_progress_)) *
Defaults::Title::Sequence::LOGO_OFFSCREEN_OFFSET_Y;
if (animacio_activa_) {
float temps_shadow = std::max(0.0F, temps_animacio_ - SHADOW_DELAY);
const float TWO_PI = 2.0F * Defaults::Math::PI;
@@ -475,39 +539,43 @@ void TitleScene::draw() {
for (std::size_t i = 0; i < lletres_orni_.size(); ++i) {
const Vec2 POS_SHADOW{
.x = posicions_originals_orni_[i].x + std::round(SHADOW_OX),
.y = posicions_originals_orni_[i].y + std::round(SHADOW_OY),
.y = posicions_originals_orni_[i].y + std::round(SHADOW_OY) + LOGO_Y_INTRO,
};
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);
}
for (std::size_t i = 0; i < lletres_attack_.size(); ++i) {
const Vec2 POS_SHADOW{
.x = posicions_originals_attack_[i].x + std::round(SHADOW_OX),
.y = posicions_originals_attack_[i].y + std::round(SHADOW_OY),
.y = posicions_originals_attack_[i].y + std::round(SHADOW_OY) + LOGO_Y_INTRO,
};
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);
}
}
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 = lletra.position.x, .y = lletra.position.y + LOGO_Y_INTRO};
Rendering::renderShape(sdl_.getRenderer(), lletra.shape, POS, 0.0F, Defaults::Title::Layout::LOGO_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 = lletra.position.x, .y = lletra.position.y + LOGO_Y_INTRO};
Rendering::renderShape(sdl_.getRenderer(), lletra.shape, POS, 0.0F, Defaults::Title::Layout::LOGO_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<float>;
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<float>;
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);
+7 -1
View File
@@ -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.
+4 -2
View File
@@ -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;