c4933875dd
El menu de servei queda obert per sota de l'overlay DefineInputs durant tot el rebind (en lloc de tancar-se al activar la accio), de manera que absorbeix qualsevol KEY_DOWN que arribi un cop l'overlay s'auto-cancela. La pantalla de titol tambe pausa la seua logica mentre el menu de servei esta obert, igual que GameScene, per evitar que detecti un START fantasma si l'usuari encara te una tecla pulsada al moment de tancar-se el modal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
696 lines
27 KiB
C++
696 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/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) {
|
|
// Pausa global: mentre el menu de servei esta obert (i, per tant, el
|
|
// sub-overlay de rebind tambe, si esta actiu), congelem la logica de la
|
|
// pantalla de titol per no consumir un START fantasma quan l'overlay
|
|
// s'auto-tanca i les tecles encara s'estan pulsant.
|
|
if (const auto* menu = System::ServiceMenu::get(); menu != nullptr && menu->isOpen()) {
|
|
return;
|
|
}
|
|
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<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);
|
|
}
|
|
}
|