8c251d2246
Sustituye las dos líneas de copyright (Pascal original 1999 + port 2025) por una sola línea "© 2026 JAILDESIGNER" centrada en la posición de la antigua primera línea. Encima, en el espacio liberado, se muestra el logo vectorial JAILGAMES en pequeño (escala 0.25, las mismas letras que usa LogoScene). Cambios: - CMakeLists.txt: PROJECT_COPYRIGHT pasa a "© 2026 JailDesigner". Eliminadas las variables intermedias PROJECT_COPYRIGHT_ORIGINAL y PROJECT_COPYRIGHT_PORT (ya no se referenciaban en otro sitio). - project.h.in: fuera Project::COPYRIGHT_ORIGINAL y Project::COPYRIGHT_PORT. - Defaults::Title::Layout: nuevas constantes JAILGAMES_SCALE (0.25) y JAILGAMES_COPYRIGHT_GAP (1.5% de la altura lógica) para el espaciado. - TitleScene: nuevo helper inicialitzarJailgames() que carga las 9 letras y las posiciona centradas justo encima de la línea de copyright. El bloque del pie del título sale del draw() a un dibuixarPeuTitol() para mantener la complejidad cognitiva por debajo del umbral del linter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
740 lines
28 KiB
C++
740 lines
28 KiB
C++
// title_scene.cpp - Implementació de l'escena de título
|
||
// © 2025 Port a C++20
|
||
|
||
#include "title_scene.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <cfloat>
|
||
#include <cmath>
|
||
#include <iostream>
|
||
#include <numbers>
|
||
#include <string>
|
||
|
||
#include "core/audio/audio.hpp"
|
||
#include "core/graphics/shape_loader.hpp"
|
||
#include "core/input/input.hpp"
|
||
#include "core/rendering/shape_renderer.hpp"
|
||
#include "core/system/scene_context.hpp"
|
||
#include "project.h"
|
||
|
||
// Using declarations per simplificar el codi
|
||
using SceneManager::SceneContext;
|
||
using SceneType = SceneContext::SceneType;
|
||
using Option = SceneContext::Option;
|
||
|
||
TitleScene::TitleScene(SDLManager& sdl, SceneContext& context)
|
||
: sdl_(sdl),
|
||
context_(context),
|
||
text_(sdl.getRenderer()),
|
||
estat_actual_(TitleState::STARFIELD_FADE_IN),
|
||
temps_acumulat_(0.0F),
|
||
temps_animacio_(0.0F),
|
||
temps_estat_main_(0.0F),
|
||
animacio_activa_(false),
|
||
factor_lerp_(0.0F) {
|
||
std::cout << "SceneType Titol: Inicialitzant...\n";
|
||
|
||
// Inicialitzar configuración de match (sin player active per defecte)
|
||
match_config_.jugador1_actiu = false;
|
||
match_config_.jugador2_actiu = false;
|
||
match_config_.mode = GameConfig::Mode::NORMAL;
|
||
|
||
// Processar opción del context
|
||
auto option = context_.consumeOption();
|
||
|
||
if (option == Option::JUMP_TO_TITLE_MAIN) {
|
||
std::cout << "SceneType Titol: Opción JUMP_TO_TITLE_MAIN activada\n";
|
||
estat_actual_ = TitleState::MAIN;
|
||
temps_estat_main_ = 0.0F;
|
||
}
|
||
|
||
// Crear starfield de fons
|
||
Vec2 centre_pantalla{
|
||
.x = Defaults::Game::WIDTH / 2.0F,
|
||
.y = Defaults::Game::HEIGHT / 2.0F};
|
||
|
||
SDL_FRect area_completa{
|
||
0,
|
||
0,
|
||
static_cast<float>(Defaults::Game::WIDTH),
|
||
static_cast<float>(Defaults::Game::HEIGHT)};
|
||
|
||
starfield_ = std::make_unique<Graphics::Starfield>(
|
||
sdl_.getRenderer(),
|
||
centre_pantalla,
|
||
area_completa,
|
||
150 // densitat: 150 estrelles (50 per capa)
|
||
);
|
||
|
||
// Brightness depèn de l'opción
|
||
if (estat_actual_ == TitleState::MAIN) {
|
||
// Si saltem a MAIN, starfield instantàniament brillant
|
||
starfield_->set_brightness(BRIGHTNESS_STARFIELD);
|
||
} else {
|
||
// Flux normal: comença con brightness 0.0 per fade-in
|
||
starfield_->set_brightness(0.0F);
|
||
}
|
||
|
||
// Inicialitzar animador de naves 3D
|
||
ship_animator_ = std::make_unique<Title::ShipAnimator>(sdl_.getRenderer());
|
||
ship_animator_->init();
|
||
|
||
if (estat_actual_ == TitleState::MAIN) {
|
||
// Jump to MAIN: empezar entrada inmediatamente
|
||
ship_animator_->set_visible(true);
|
||
ship_animator_->start_entry_animation();
|
||
} else {
|
||
// Flux normal: NO empezar entrada todavía (esperaran a MAIN)
|
||
ship_animator_->set_visible(false);
|
||
}
|
||
|
||
// Inicialitzar lletres del título "ORNI ATTACK!"
|
||
inicialitzar_titol();
|
||
|
||
// Logo JAILGAMES pequeño sobre el copyright inferior.
|
||
inicialitzarJailgames();
|
||
|
||
// Iniciar música de título si no está sonant
|
||
if (Audio::getMusicState() != Audio::MusicState::PLAYING) {
|
||
Audio::get()->playMusic("title.ogg");
|
||
}
|
||
}
|
||
|
||
TitleScene::~TitleScene() {
|
||
// Aturar música de título cuando es destrueix l'escena
|
||
Audio::get()->stopMusic();
|
||
}
|
||
|
||
void TitleScene::inicialitzar_titol() {
|
||
using namespace Graphics;
|
||
|
||
// === LÍNIA 1: "ORNI" ===
|
||
std::vector<std::string> fitxers_orni = {
|
||
"title/letra_o.shp",
|
||
"title/letra_r.shp",
|
||
"title/letra_n.shp",
|
||
"title/letra_i.shp"};
|
||
|
||
// Pas 1: Carregar formes i calcular amplades per "ORNI"
|
||
float ancho_total_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;
|
||
}
|
||
|
||
// Calcular bounding box de la shape (trobar ancho i altura)
|
||
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->get_primitives()) {
|
||
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);
|
||
}
|
||
}
|
||
|
||
float ancho_sin_escalar = max_x - min_x;
|
||
float altura_sin_escalar = max_y - min_y;
|
||
|
||
// Escalar ancho, altura i offset con LOGO_SCALE
|
||
float ancho = ancho_sin_escalar * Defaults::Title::Layout::LOGO_SCALE;
|
||
float altura = altura_sin_escalar * Defaults::Title::Layout::LOGO_SCALE;
|
||
float offset_centre = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
|
||
|
||
lletres_orni_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ancho, altura, offset_centre});
|
||
|
||
ancho_total_orni += ancho;
|
||
}
|
||
|
||
// Añadir espaiat entre lletres
|
||
ancho_total_orni += ESPAI_ENTRE_LLETRES * (lletres_orni_.size() - 1);
|
||
|
||
// Calcular posición inicial (centrat horitzontal) per "ORNI"
|
||
float x_inicial_orni = (Defaults::Game::WIDTH - ancho_total_orni) / 2.0F;
|
||
float x_actual = x_inicial_orni;
|
||
|
||
for (auto& lletra : lletres_orni_) {
|
||
lletra.position.x = x_actual + lletra.offset_centre;
|
||
lletra.position.y = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS;
|
||
x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES;
|
||
}
|
||
|
||
std::cout << "[TitleScene] Línia 1 (ORNI): " << lletres_orni_.size()
|
||
<< " lletres, ancho total: " << ancho_total_orni << " px\n";
|
||
|
||
// === Calcular posición Y dinàmica per "ATTACK!" ===
|
||
// Todas las lletres ORNI tenen la misma altura, utilitzem la primera
|
||
float altura_orni = lletres_orni_.empty() ? 50.0F : lletres_orni_[0].altura;
|
||
float y_orni = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS;
|
||
float separacion_lineas = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_LINE_SPACING;
|
||
y_attack_dinamica_ = y_orni + altura_orni + separacion_lineas;
|
||
|
||
std::cout << "[TitleScene] Altura ORNI: " << altura_orni
|
||
<< " px, Y_ATTACK dinàmica: " << y_attack_dinamica_ << " px\n";
|
||
|
||
// === LÍNIA 2: "ATTACK!" ===
|
||
std::vector<std::string> fitxers_attack = {
|
||
"title/letra_a.shp",
|
||
"title/letra_t.shp",
|
||
"title/letra_t.shp", // T repetida
|
||
"title/letra_a.shp", // A repetida
|
||
"title/letra_c.shp",
|
||
"title/letra_k.shp",
|
||
"title/letra_exclamacion.shp"};
|
||
|
||
// Pas 1: Carregar formes i calcular amplades per "ATTACK!"
|
||
float ancho_total_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;
|
||
}
|
||
|
||
// Calcular bounding box de la shape (trobar ancho i altura)
|
||
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->get_primitives()) {
|
||
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);
|
||
}
|
||
}
|
||
|
||
float ancho_sin_escalar = max_x - min_x;
|
||
float altura_sin_escalar = max_y - min_y;
|
||
|
||
// Escalar ancho, altura i offset con LOGO_SCALE
|
||
float ancho = ancho_sin_escalar * Defaults::Title::Layout::LOGO_SCALE;
|
||
float altura = altura_sin_escalar * Defaults::Title::Layout::LOGO_SCALE;
|
||
float offset_centre = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
|
||
|
||
lletres_attack_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ancho, altura, offset_centre});
|
||
|
||
ancho_total_attack += ancho;
|
||
}
|
||
|
||
// Añadir espaiat entre lletres
|
||
ancho_total_attack += ESPAI_ENTRE_LLETRES * (lletres_attack_.size() - 1);
|
||
|
||
// Calcular posición inicial (centrat horitzontal) per "ATTACK!"
|
||
float x_inicial_attack = (Defaults::Game::WIDTH - ancho_total_attack) / 2.0F;
|
||
x_actual = x_inicial_attack;
|
||
|
||
for (auto& lletra : lletres_attack_) {
|
||
lletra.position.x = x_actual + lletra.offset_centre;
|
||
lletra.position.y = y_attack_dinamica_; // Usar posición dinàmica
|
||
x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES;
|
||
}
|
||
|
||
std::cout << "[TitleScene] Línia 2 (ATTACK!): " << lletres_attack_.size()
|
||
<< " lletres, ancho total: " << ancho_total_attack << " px\n";
|
||
|
||
// Guardar posicions originals per l'animación orbital
|
||
posicions_originals_orni_.clear();
|
||
for (const auto& lletra : lletres_orni_) {
|
||
posicions_originals_orni_.push_back(lletra.position);
|
||
}
|
||
|
||
posicions_originals_attack_.clear();
|
||
for (const auto& lletra : lletres_attack_) {
|
||
posicions_originals_attack_.push_back(lletra.position);
|
||
}
|
||
|
||
std::cout << "[TitleScene] Animación: Posicions originals guardades\n";
|
||
}
|
||
|
||
void TitleScene::inicialitzarJailgames() {
|
||
using namespace Graphics;
|
||
|
||
// Mismas letras que la LogoScene, mismo orden (J-A-I-L-G-A-M-E-S).
|
||
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;
|
||
|
||
// Pas 1: carregar formes i calcular amplada/altura escalades.
|
||
float ancho_total = 0.0F;
|
||
float altura_max = 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->get_primitives()) {
|
||
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 ANCHO = (max_x - min_x) * SCALE;
|
||
const float ALTURA = (max_y - min_y) * SCALE;
|
||
const float OFFSET_CENTRE = (shape->getCenter().x - min_x) * SCALE;
|
||
|
||
lletres_jailgames_.push_back({shape, {.x = 0.0F, .y = 0.0F},
|
||
ANCHO, ALTURA, OFFSET_CENTRE});
|
||
|
||
ancho_total += ANCHO;
|
||
altura_max = std::max(altura_max, ALTURA);
|
||
}
|
||
|
||
// Espaiat entre lletres (proporcional a la escala, para que no quede pegado).
|
||
constexpr float ESPAI_JAILGAMES = ESPAI_ENTRE_LLETRES * SCALE;
|
||
if (!lletres_jailgames_.empty()) {
|
||
ancho_total += ESPAI_JAILGAMES * static_cast<float>(lletres_jailgames_.size() - 1);
|
||
}
|
||
|
||
// Pas 2: centrar horizontalmente y colocar JUST encima de la línea de copyright.
|
||
const float Y_COPYRIGHT = 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_COPYRIGHT - GAP - (altura_max / 2.0F);
|
||
const float X_INICIAL = (Defaults::Game::WIDTH - ancho_total) / 2.0F;
|
||
|
||
float x_actual = X_INICIAL;
|
||
for (auto& lletra : lletres_jailgames_) {
|
||
lletra.position.x = x_actual + lletra.offset_centre;
|
||
lletra.position.y = Y_CENTRE;
|
||
x_actual += lletra.ancho + ESPAI_JAILGAMES;
|
||
}
|
||
}
|
||
|
||
void TitleScene::dibuixarPeuTitol(float spacing) const {
|
||
// Logo JAILGAMES pequeño sobre el copyright.
|
||
for (const auto& lletra : lletres_jailgames_) {
|
||
Rendering::render_shape(sdl_.getRenderer(), lletra.shape,
|
||
lletra.position, 0.0F,
|
||
Defaults::Title::Layout::JAILGAMES_SCALE,
|
||
1.0F);
|
||
}
|
||
|
||
// Copyright en una sola línea, centrado, en mayúsculas.
|
||
std::string copyright = Project::COPYRIGHT;
|
||
for (char& c : copyright) {
|
||
if (c >= 'a' && c <= 'z') {
|
||
c = static_cast<char>(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);
|
||
}
|
||
|
||
auto TitleScene::isFinished() const -> bool {
|
||
return context_.nextScene() != SceneType::TITLE;
|
||
}
|
||
|
||
void TitleScene::update(float delta_time) {
|
||
// Actualitzar starfield (siempre active)
|
||
if (starfield_) {
|
||
starfield_->update(delta_time);
|
||
}
|
||
|
||
// Actualitzar naves (cuando visibles)
|
||
if (ship_animator_ &&
|
||
(estat_actual_ == TitleState::STARFIELD_FADE_IN ||
|
||
estat_actual_ == TitleState::STARFIELD ||
|
||
estat_actual_ == TitleState::MAIN ||
|
||
estat_actual_ == TitleState::PLAYER_JOIN_PHASE)) {
|
||
ship_animator_->update(delta_time);
|
||
}
|
||
|
||
switch (estat_actual_) {
|
||
case TitleState::STARFIELD_FADE_IN: {
|
||
temps_acumulat_ += delta_time;
|
||
|
||
// Calcular progrés del fade (0.0 → 1.0)
|
||
float progress = std::min(1.0F, temps_acumulat_ / DURACIO_FADE_IN);
|
||
|
||
// Lerp brightness de 0.0 a BRIGHTNESS_STARFIELD
|
||
float brightness_actual = progress * BRIGHTNESS_STARFIELD;
|
||
starfield_->set_brightness(brightness_actual);
|
||
|
||
// Transición a STARFIELD cuando el fade es completa
|
||
if (temps_acumulat_ >= DURACIO_FADE_IN) {
|
||
estat_actual_ = TitleState::STARFIELD;
|
||
temps_acumulat_ = 0.0F; // Reset timer per al següent state
|
||
starfield_->set_brightness(BRIGHTNESS_STARFIELD); // Assegurar value final
|
||
}
|
||
break;
|
||
}
|
||
|
||
case TitleState::STARFIELD:
|
||
temps_acumulat_ += delta_time;
|
||
if (temps_acumulat_ >= DURACIO_INIT) {
|
||
estat_actual_ = TitleState::MAIN;
|
||
temps_estat_main_ = 0.0F; // Reset timer al entrar a MAIN
|
||
animacio_activa_ = false; // Comença estàtic
|
||
factor_lerp_ = 0.0F; // Sin animación aún
|
||
|
||
// Naves esperaran ENTRANCE_DELAY antes de entrar (no start aquí)
|
||
}
|
||
break;
|
||
|
||
case TitleState::MAIN: {
|
||
temps_estat_main_ += delta_time;
|
||
|
||
// Iniciar animación de entrada de naves después del delay
|
||
if (temps_estat_main_ >= Defaults::Title::Ships::ENTRANCE_DELAY) {
|
||
if (ship_animator_ && !ship_animator_->is_visible()) {
|
||
ship_animator_->set_visible(true);
|
||
ship_animator_->start_entry_animation();
|
||
}
|
||
}
|
||
|
||
// Fase 1: Estàtic (0-10s)
|
||
if (temps_estat_main_ < DELAY_INICI_ANIMACIO) {
|
||
factor_lerp_ = 0.0F;
|
||
animacio_activa_ = false;
|
||
}
|
||
// Fase 2: Lerp (10-12s)
|
||
else if (temps_estat_main_ < DELAY_INICI_ANIMACIO + DURACIO_LERP) {
|
||
float temps_lerp = temps_estat_main_ - DELAY_INICI_ANIMACIO;
|
||
factor_lerp_ = temps_lerp / DURACIO_LERP; // 0.0 → 1.0 linealment
|
||
animacio_activa_ = true;
|
||
}
|
||
// Fase 3: Animación completa (12s+)
|
||
else {
|
||
factor_lerp_ = 1.0F;
|
||
animacio_activa_ = true;
|
||
}
|
||
|
||
// Actualitzar animación del logo
|
||
actualitzar_animacio_logo(delta_time);
|
||
break;
|
||
}
|
||
|
||
case TitleState::PLAYER_JOIN_PHASE:
|
||
temps_acumulat_ += delta_time;
|
||
|
||
// Continuar animación orbital durante la transición
|
||
actualitzar_animacio_logo(delta_time);
|
||
|
||
// [NOU] Continuar comprovant si l'altre player quiere unir-se durante la transición ("late join")
|
||
{
|
||
bool p1_actiu_abans = match_config_.jugador1_actiu;
|
||
bool p2_actiu_abans = match_config_.jugador2_actiu;
|
||
|
||
if (checkStartGameButtonPressed()) {
|
||
// Updates match_config_ if pressed, logs are in the method
|
||
context_.setMatchConfig(match_config_);
|
||
|
||
// Trigger animación de salida per la ship que acaba de unir-se
|
||
if (ship_animator_) {
|
||
if (match_config_.jugador1_actiu && !p1_actiu_abans) {
|
||
ship_animator_->trigger_exit_animation_for_player(1);
|
||
std::cout << "[TitleScene] P1 late join - ship exiting\n";
|
||
}
|
||
if (match_config_.jugador2_actiu && !p2_actiu_abans) {
|
||
ship_animator_->trigger_exit_animation_for_player(2);
|
||
std::cout << "[TitleScene] P2 late join - ship exiting\n";
|
||
}
|
||
}
|
||
|
||
// Reproducir so de START cuando el segon player s'uneix
|
||
Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME);
|
||
|
||
// Reiniciar el timer per allargar el time de transición
|
||
temps_acumulat_ = 0.0F;
|
||
|
||
std::cout << "[TitleScene] Segon player s'ha unit - so i timer reiniciats\n";
|
||
}
|
||
}
|
||
|
||
if (temps_acumulat_ >= DURACIO_TRANSITION) {
|
||
// Transición a pantalla negra
|
||
estat_actual_ = TitleState::BLACK_SCREEN;
|
||
temps_acumulat_ = 0.0F;
|
||
std::cout << "[TitleScene] Passant a BLACK_SCREEN\n";
|
||
}
|
||
break;
|
||
|
||
case TitleState::BLACK_SCREEN:
|
||
temps_acumulat_ += delta_time;
|
||
|
||
// No animation, no input checking - just wait
|
||
if (temps_acumulat_ >= DURACIO_BLACK_SCREEN) {
|
||
// Transición a escena GAME (el Director detecta isFinished()).
|
||
context_.setNextScene(SceneType::GAME);
|
||
std::cout << "[TitleScene] Canviant a escena GAME\n";
|
||
}
|
||
break;
|
||
}
|
||
|
||
// Verificar botones de skip (FIRE/THRUST/START) para saltar escenas ANTES de MAIN
|
||
if (estat_actual_ == TitleState::STARFIELD_FADE_IN || estat_actual_ == TitleState::STARFIELD) {
|
||
if (checkSkipButtonPressed()) {
|
||
// Saltar a MAIN
|
||
estat_actual_ = TitleState::MAIN;
|
||
starfield_->set_brightness(BRIGHTNESS_STARFIELD);
|
||
temps_estat_main_ = 0.0F;
|
||
|
||
// Naves esperaran ENTRANCE_DELAY antes de entrar (no start aquí)
|
||
}
|
||
}
|
||
|
||
// Verificar boton START para start match desde MAIN
|
||
if (estat_actual_ == TitleState::MAIN) {
|
||
// Guardar state anterior per detectar qui ha premut START AQUEST frame
|
||
bool p1_actiu_abans = match_config_.jugador1_actiu;
|
||
bool p2_actiu_abans = match_config_.jugador2_actiu;
|
||
|
||
if (checkStartGameButtonPressed()) {
|
||
// Si START es prem durante el delay (naves aún invisibles), saltar-las a FLOATING
|
||
if (ship_animator_ && !ship_animator_->is_visible()) {
|
||
ship_animator_->set_visible(true);
|
||
ship_animator_->skip_to_floating_state();
|
||
}
|
||
|
||
// Configurar match antes de canviar de escena
|
||
context_.setMatchConfig(match_config_);
|
||
std::cout << "[TitleScene] Configuración de match - P1: "
|
||
<< (match_config_.jugador1_actiu ? "ACTIU" : "INACTIU")
|
||
<< ", P2: "
|
||
<< (match_config_.jugador2_actiu ? "ACTIU" : "INACTIU")
|
||
<< '\n';
|
||
|
||
// El setNextScene a GAME se hace al final de BLACK_SCREEN para no
|
||
// saltar la animación de salida (isFinished() lo recoge entonces).
|
||
estat_actual_ = TitleState::PLAYER_JOIN_PHASE;
|
||
temps_acumulat_ = 0.0F;
|
||
|
||
// Trigger animación de salida NOMÉS per las naves que han premut START
|
||
if (ship_animator_) {
|
||
if (match_config_.jugador1_actiu && !p1_actiu_abans) {
|
||
ship_animator_->trigger_exit_animation_for_player(1);
|
||
std::cout << "[TitleScene] P1 ship exiting\n";
|
||
}
|
||
if (match_config_.jugador2_actiu && !p2_actiu_abans) {
|
||
ship_animator_->trigger_exit_animation_for_player(2);
|
||
std::cout << "[TitleScene] P2 ship exiting\n";
|
||
}
|
||
}
|
||
|
||
Audio::get()->fadeOutMusic(MUSIC_FADE);
|
||
Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME);
|
||
}
|
||
}
|
||
}
|
||
|
||
void TitleScene::actualitzar_animacio_logo(float delta_time) {
|
||
// Solo calcular i aplicar offsets si l'animación está activa
|
||
if (animacio_activa_) {
|
||
// Acumular time escalat
|
||
temps_animacio_ += delta_time * factor_lerp_;
|
||
|
||
// Usar amplituds i freqüències completes
|
||
float amplitude_x_actual = ORBIT_AMPLITUDE_X;
|
||
float amplitude_y_actual = ORBIT_AMPLITUDE_Y;
|
||
float frequency_x_actual = ORBIT_FREQUENCY_X;
|
||
float frequency_y_actual = ORBIT_FREQUENCY_Y;
|
||
|
||
// Calcular offset orbital
|
||
float offset_x = amplitude_x_actual * std::sin(2.0F * Defaults::Math::PI * frequency_x_actual * temps_animacio_);
|
||
float offset_y = amplitude_y_actual * std::sin((2.0F * Defaults::Math::PI * frequency_y_actual * temps_animacio_) + ORBIT_PHASE_OFFSET);
|
||
|
||
// Aplicar offset a todas las lletres de "ORNI"
|
||
for (size_t i = 0; i < lletres_orni_.size(); ++i) {
|
||
lletres_orni_[i].position.x = posicions_originals_orni_[i].x + static_cast<int>(std::round(offset_x));
|
||
lletres_orni_[i].position.y = posicions_originals_orni_[i].y + static_cast<int>(std::round(offset_y));
|
||
}
|
||
|
||
// Aplicar offset a todas las lletres de "ATTACK!"
|
||
for (size_t i = 0; i < lletres_attack_.size(); ++i) {
|
||
lletres_attack_[i].position.x = posicions_originals_attack_[i].x + static_cast<int>(std::round(offset_x));
|
||
lletres_attack_[i].position.y = posicions_originals_attack_[i].y + static_cast<int>(std::round(offset_y));
|
||
}
|
||
}
|
||
}
|
||
|
||
void TitleScene::draw() {
|
||
// Dibuixar starfield de fons (en todos los estats excepte BLACK_SCREEN)
|
||
if (starfield_ && estat_actual_ != TitleState::BLACK_SCREEN) {
|
||
starfield_->draw();
|
||
}
|
||
|
||
// Dibuixar naves (después starfield, antes logo)
|
||
if (ship_animator_ &&
|
||
(estat_actual_ == TitleState::STARFIELD_FADE_IN ||
|
||
estat_actual_ == TitleState::STARFIELD ||
|
||
estat_actual_ == TitleState::MAIN ||
|
||
estat_actual_ == TitleState::PLAYER_JOIN_PHASE)) {
|
||
ship_animator_->draw();
|
||
}
|
||
|
||
// En los estats STARFIELD_FADE_IN i STARFIELD, solo mostrar starfield (sin text)
|
||
if (estat_actual_ == TitleState::STARFIELD_FADE_IN || estat_actual_ == TitleState::STARFIELD) {
|
||
return;
|
||
}
|
||
|
||
// Estat MAIN i PLAYER_JOIN_PHASE: Dibuixar título i text (sobre el starfield)
|
||
// BLACK_SCREEN: no draw res (fons negre ya está netejat)
|
||
if (estat_actual_ == TitleState::MAIN || estat_actual_ == TitleState::PLAYER_JOIN_PHASE) {
|
||
// === Calcular i renderizar ombra (solo si animación activa) ===
|
||
if (animacio_activa_) {
|
||
float temps_shadow = temps_animacio_ - SHADOW_DELAY;
|
||
temps_shadow = std::max(temps_shadow, 0.0F); // Evitar time negatiu
|
||
|
||
// Usar amplituds i freqüències completes per l'ombra
|
||
float amplitude_x_shadow = ORBIT_AMPLITUDE_X;
|
||
float amplitude_y_shadow = ORBIT_AMPLITUDE_Y;
|
||
float frequency_x_shadow = ORBIT_FREQUENCY_X;
|
||
float frequency_y_shadow = ORBIT_FREQUENCY_Y;
|
||
|
||
// Calcular offset de l'ombra
|
||
float shadow_offset_x = (amplitude_x_shadow * std::sin(2.0F * Defaults::Math::PI * frequency_x_shadow * temps_shadow)) + SHADOW_OFFSET_X;
|
||
float shadow_offset_y = (amplitude_y_shadow * std::sin((2.0F * Defaults::Math::PI * frequency_y_shadow * temps_shadow) + ORBIT_PHASE_OFFSET)) + SHADOW_OFFSET_Y;
|
||
|
||
// === RENDERITZAR OMBRA PRIMER (darrera del logo principal) ===
|
||
|
||
// Ombra "ORNI"
|
||
for (size_t i = 0; i < lletres_orni_.size(); ++i) {
|
||
Vec2 pos_shadow;
|
||
pos_shadow.x = posicions_originals_orni_[i].x + static_cast<int>(std::round(shadow_offset_x));
|
||
pos_shadow.y = posicions_originals_orni_[i].y + static_cast<int>(std::round(shadow_offset_y));
|
||
|
||
Rendering::render_shape(
|
||
sdl_.getRenderer(),
|
||
lletres_orni_[i].shape,
|
||
pos_shadow,
|
||
0.0F,
|
||
Defaults::Title::Layout::LOGO_SCALE,
|
||
1.0F, // progress = 1.0 (totalment visible)
|
||
SHADOW_BRIGHTNESS // brightness = 0.4 (brightness reduïda)
|
||
);
|
||
}
|
||
|
||
// Ombra "ATTACK!"
|
||
for (size_t i = 0; i < lletres_attack_.size(); ++i) {
|
||
Vec2 pos_shadow;
|
||
pos_shadow.x = posicions_originals_attack_[i].x + static_cast<int>(std::round(shadow_offset_x));
|
||
pos_shadow.y = posicions_originals_attack_[i].y + static_cast<int>(std::round(shadow_offset_y));
|
||
|
||
Rendering::render_shape(
|
||
sdl_.getRenderer(),
|
||
lletres_attack_[i].shape,
|
||
pos_shadow,
|
||
0.0F,
|
||
Defaults::Title::Layout::LOGO_SCALE,
|
||
1.0F, // progress = 1.0 (totalment visible)
|
||
SHADOW_BRIGHTNESS);
|
||
}
|
||
}
|
||
|
||
// === RENDERITZAR LOGO PRINCIPAL (damunt) ===
|
||
|
||
// Dibuixar "ORNI" (línia 1)
|
||
for (const auto& lletra : lletres_orni_) {
|
||
Rendering::render_shape(
|
||
sdl_.getRenderer(),
|
||
lletra.shape,
|
||
lletra.position,
|
||
0.0F,
|
||
Defaults::Title::Layout::LOGO_SCALE,
|
||
1.0F // Brillantor completa
|
||
);
|
||
}
|
||
|
||
// Dibuixar "ATTACK!" (línia 2)
|
||
for (const auto& lletra : lletres_attack_) {
|
||
Rendering::render_shape(
|
||
sdl_.getRenderer(),
|
||
lletra.shape,
|
||
lletra.position,
|
||
0.0F,
|
||
Defaults::Title::Layout::LOGO_SCALE,
|
||
1.0F // Brillantor completa
|
||
);
|
||
}
|
||
|
||
// === Text "PRESS START TO PLAY" ===
|
||
// En state MAIN: siempre visible
|
||
// En state TRANSITION: parpellejant (blink con sinusoide)
|
||
|
||
const float spacing = Defaults::Title::Layout::TEXT_SPACING;
|
||
|
||
bool mostrar_text = true;
|
||
if (estat_actual_ == TitleState::PLAYER_JOIN_PHASE) {
|
||
// Parpelleig: sin oscil·la entre -1 i 1, volem ON cuando > 0
|
||
float fase = temps_acumulat_ * BLINK_FREQUENCY * 2.0F * std::numbers::pi_v<float>; // 2π × freq × time
|
||
mostrar_text = (std::sin(fase) > 0.0F);
|
||
}
|
||
|
||
if (mostrar_text) {
|
||
const std::string main_text = "PRESS START TO PLAY";
|
||
const float escala_main = Defaults::Title::Layout::PRESS_START_SCALE;
|
||
|
||
float centre_x = Defaults::Game::WIDTH / 2.0F;
|
||
float centre_y = Defaults::Game::HEIGHT * Defaults::Title::Layout::PRESS_START_POS;
|
||
|
||
text_.renderCentered(main_text, {.x = centre_x, .y = centre_y}, escala_main, spacing);
|
||
}
|
||
|
||
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_.jugador1_actiu) {
|
||
match_config_.jugador1_actiu = true;
|
||
any_pressed = true;
|
||
std::cout << "[TitleScene] P1 pressed START\n";
|
||
}
|
||
}
|
||
if (input->checkActionPlayer2(action, Input::DO_NOT_ALLOW_REPEAT)) {
|
||
if (!match_config_.jugador2_actiu) {
|
||
match_config_.jugador2_actiu = true;
|
||
any_pressed = true;
|
||
std::cout << "[TitleScene] P2 pressed START\n";
|
||
}
|
||
}
|
||
}
|
||
|
||
return any_pressed;
|
||
}
|
||
|
||
void TitleScene::handleEvent(const SDL_Event& event) {
|
||
// La lógica de input se decide en update() consultando Input::checkAction;
|
||
// aquí no hay eventos puntuales que procesar.
|
||
(void)event;
|
||
}
|