Files
orni-attack/source/game/scenes/title_scene.cpp
T
JailDesigner 64a6599e81 fix(title): manten animacions amb menu obert, bloqueja nomes els polls d'input
El fix anterior pausava tot el title quan el menu de servei estava obert,
trencant l'efecte d'animacio de fons. Ara title segueix animant-se i
nomes guardem handleSkipInput/handleStartInput mentre el menu o el modal
de rebind estan actius, per evitar START fantasma sense congelar el render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:54:04 +02:00

702 lines
27 KiB
C++

// title_scene.cpp - Implementació de l'escena de títol 3D real
// © 2026 JailDesigner
#include "title_scene.hpp"
#include <algorithm>
#include <cfloat>
#include <cmath>
#include <iostream>
#include <numbers>
#include <string>
#include "core/audio/audio.hpp"
#include "core/defaults.hpp"
#include "core/graphics/shape_loader.hpp"
#include "core/input/define_inputs.hpp"
#include "core/input/input.hpp"
#include "core/locale/locale.hpp"
#include "core/math/easing.hpp"
#include "core/rendering/shape_renderer.hpp"
#include "core/system/scene_context.hpp"
#include "core/system/service_menu.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<InputAction, 1> 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<Graphics::Camera3D>(
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<float>(Defaults::Game::WIDTH),
static_cast<float>(Defaults::Game::HEIGHT));
starfield_ = std::make_unique<Graphics::Starfield>(
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<Title::ShipAnimator>(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<float>(Defaults::Window::WIDTH) / 2.0F,
.y = static_cast<float>(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<std::string> 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<float>(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<std::string> 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<float>(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<std::string> 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<float>(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<char>(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;
}
// Les animacions segueixen pero els inputs es bloquegen mentre el menu
// de servei o l'overlay de redefinicio estiguin actius: en cas contrari,
// SDL_GetKeyboardState i SDL_GetGamepadButton segueixen veient les tecles
// pulsades i podrien disparar handleSkipInput/handleStartInput sense
// intencio. Mateixa logica: per a GameScene tota la pausa es global,
// pero a TitleScene nomes guardem els polls d'input.
const auto* menu = System::ServiceMenu::get();
const auto* di = System::DefineInputs::get();
const bool INPUT_BLOCKED = (menu != nullptr && menu->isOpen()) ||
(di != nullptr && di->isActive());
if (!INPUT_BLOCKED) {
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<float>;
mostrar_text = (std::sin(FASE) > 0.0F);
}
if (mostrar_text) {
const std::string MAIN_TEXT = Locale::get().text("title.press_start");
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);
}
}