refactor(title): la 3D és l'única — elimina backup 2D i renomena als noms canònics

This commit is contained in:
2026-05-22 12:04:16 +02:00
parent a29c2b9cc2
commit 54d3e683a1
16 changed files with 680 additions and 2321 deletions
+131 -344
View File
@@ -1,4 +1,4 @@
// title_scene.cpp - Implementació de l'escena de título
// title_scene.cpp - Implementació de l'escena de títol 3D real
// © 2026 JailDesigner
#include "title_scene.hpp"
@@ -18,115 +18,96 @@
#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;
namespace {
// Botons per iniciar partida des de MAIN (només START). Duplicat del que viu
constexpr std::array<InputAction, 1> START_GAME_BUTTONS = {InputAction::START};
} // namespace
TitleScene::TitleScene(SDLManager& sdl, SceneContext& context)
: sdl_(sdl),
context_(context),
text_(sdl.getRenderer())
{
text_(sdl.getRenderer()) {
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";
std::cout << "SceneType Titol: Opció 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,
// 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)};
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
camera_.get(),
200);
if (estat_actual_ == TitleState::MAIN) {
// Si saltem a MAIN, starfield instantàniament brillant
starfield_->setBrightness(BRIGHTNESS_STARFIELD);
} else {
// Flux normal: comença con brightness 0.0 per fade-in
starfield_->setBrightness(0.0F);
}
// Inicialitzar animador de naves 3D
ship_animator_ = std::make_unique<Title::ShipAnimator>(sdl_.getRenderer());
ship_animator_ = std::make_unique<Title::ShipAnimator>(sdl_.getRenderer(), camera_.get());
ship_animator_->init();
if (estat_actual_ == TitleState::MAIN) {
// Jump to MAIN: empezar entrada inmediatamente
ship_animator_->setVisible(true);
ship_animator_->startEntryAnimation();
} else {
// Flux normal: NO empezar entrada todavía (esperaran a MAIN)
ship_animator_->setVisible(false);
}
// Inicialitzar lletres del título "ORNI ATTACK!"
initTitle();
// 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::initTitle() {
using namespace Graphics;
// === LÍNIA 1: "ORNI" ===
std::vector<std::string> fitxers_orni = {
const 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) {
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->getPrimitives()) {
for (const auto& point : prim.points) {
min_x = std::min(min_x, point.x);
@@ -135,72 +116,46 @@ void TitleScene::initTitle() {
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;
const float ANCHO = (max_x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
const float ALTURA = (max_y - min_y) * Defaults::Title::Layout::LOGO_SCALE;
const 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;
}
ancho_total_orni += ESPAI_ENTRE_LLETRES * static_cast<float>(lletres_orni_.size() - 1);
// 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;
float x_actual = (Defaults::Game::WIDTH - ancho_total_orni) / 2.0F;
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";
const float ALTURA_ORNI = lletres_orni_.empty() ? 50.0F : lletres_orni_[0].altura;
const float Y_ORNI = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS;
const float SEPARACION = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_LINE_SPACING;
y_attack_dinamica_ = Y_ORNI + ALTURA_ORNI + SEPARACION;
// === 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 = {
const 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_t.shp",
"title/letra_a.shp",
"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) {
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->getPrimitives()) {
for (const auto& point : prim.points) {
min_x = std::min(min_x, point.x);
@@ -209,54 +164,34 @@ void TitleScene::initTitle() {
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;
const float ANCHO = (max_x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
const float ALTURA = (max_y - min_y) * Defaults::Title::Layout::LOGO_SCALE;
const 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;
}
ancho_total_attack += ESPAI_ENTRE_LLETRES * static_cast<float>(lletres_attack_.size() - 1);
// 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;
x_actual = (Defaults::Game::WIDTH - ancho_total_attack) / 2.0F;
for (auto& lletra : lletres_attack_) {
lletra.position.x = x_actual + lletra.offset_centre;
lletra.position.y = y_attack_dinamica_; // Usar posición dinàmica
lletra.position.y = y_attack_dinamica_;
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",
@@ -270,17 +205,14 @@ void TitleScene::inicialitzarJailgames() {
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;
@@ -296,24 +228,18 @@ void TitleScene::inicialitzarJailgames() {
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});
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 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_COPYRIGHT - GAP - (altura_max / 2.0F);
const float Y_CENTRE = Y_COPY - GAP - (altura_max / 2.0F);
const float X_INICIAL = (Defaults::Game::WIDTH - ancho_total) / 2.0F;
float x_actual = X_INICIAL;
@@ -325,15 +251,9 @@ void TitleScene::inicialitzarJailgames() {
}
void TitleScene::dibuixarPeuTitol(float spacing) const {
// Logo JAILGAMES pequeño sobre el copyright.
for (const auto& lletra : lletres_jailgames_) {
Rendering::renderShape(sdl_.getRenderer(), lletra.shape,
lletra.position, 0.0F,
Defaults::Title::Layout::JAILGAMES_SCALE,
1.0F);
Rendering::renderShape(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') {
@@ -342,8 +262,7 @@ void TitleScene::dibuixarPeuTitol(float spacing) const {
}
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);
text_.renderCentered(copyright, {.x = CENTRE_X, .y = Y_COPY}, Defaults::Title::Layout::COPYRIGHT_SCALE, spacing);
}
auto TitleScene::isFinished() const -> bool {
@@ -351,12 +270,9 @@ auto TitleScene::isFinished() const -> bool {
}
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 ||
@@ -389,19 +305,12 @@ void TitleScene::update(float delta_time) {
void TitleScene::updateStarfieldFadeInState(float delta_time) {
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_->setBrightness(brightness_actual);
// Transición a STARFIELD cuando el fade es completa
const float PROGRESS = std::min(1.0F, temps_acumulat_ / DURACIO_FADE_IN);
starfield_->setBrightness(PROGRESS * BRIGHTNESS_STARFIELD);
if (temps_acumulat_ >= DURACIO_FADE_IN) {
estat_actual_ = TitleState::STARFIELD;
temps_acumulat_ = 0.0F; // Reset timer per al següent state
starfield_->setBrightness(BRIGHTNESS_STARFIELD); // Assegurar value final
temps_acumulat_ = 0.0F;
starfield_->setBrightness(BRIGHTNESS_STARFIELD);
}
}
@@ -409,147 +318,100 @@ void TitleScene::updateStarfieldState(float delta_time) {
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í)
temps_estat_main_ = 0.0F;
animacio_activa_ = false;
factor_lerp_ = 0.0F;
}
}
void TitleScene::updateMainState(float delta_time) {
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 &&
ship_animator_ && !ship_animator_->isVisible()) {
ship_animator_->setVisible(true);
ship_animator_->startEntryAnimation();
}
// 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
} 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;
animacio_activa_ = true;
}
// Fase 3: Animación completa (12s+)
else {
} else {
factor_lerp_ = 1.0F;
animacio_activa_ = true;
}
// Actualitzar animación del logo
updateLogoAnimation(delta_time);
}
void TitleScene::updatePlayerJoinPhaseState(float delta_time) {
temps_acumulat_ += delta_time;
// Continuar animación orbital durante la transición
updateLogoAnimation(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;
const bool P1_ABANS = match_config_.jugador1_actiu;
const bool P2_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
triggerExitForJoinedPlayers(p1_actiu_abans, p2_actiu_abans, "late join - ");
// Reproducir so de START cuando el segon player s'uneix
triggerExitForJoinedPlayers(P1_ABANS, P2_ABANS, "late join - ");
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";
}
}
void TitleScene::updateBlackScreenState(float delta_time) {
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";
}
}
void TitleScene::handleSkipInput() {
// Verificar botones de skip (FIRE/THRUST/START) para saltar escenas ANTES de MAIN
if (estat_actual_ != TitleState::STARFIELD_FADE_IN && estat_actual_ != TitleState::STARFIELD) {
return;
}
if (!checkSkipButtonPressed()) {
return;
}
// Saltar a MAIN
estat_actual_ = TitleState::MAIN;
starfield_->setBrightness(BRIGHTNESS_STARFIELD);
temps_estat_main_ = 0.0F;
// Naves esperaran ENTRANCE_DELAY antes de entrar (no start aquí)
}
void TitleScene::handleStartInput() {
// Verificar boton START para start match desde MAIN
if (estat_actual_ != TitleState::MAIN) {
return;
}
// 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;
const bool P1_ABANS = match_config_.jugador1_actiu;
const bool P2_ABANS = match_config_.jugador2_actiu;
if (!checkStartGameButtonPressed()) {
return;
}
// Si START es prem durante el delay (naves aún invisibles), saltar-las a FLOATING
if (ship_animator_ && !ship_animator_->isVisible()) {
ship_animator_->setVisible(true);
ship_animator_->skipToFloatingState();
}
// 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
triggerExitForJoinedPlayers(p1_actiu_abans, p2_actiu_abans, "");
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) {
void TitleScene::triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, const char* log_prefix) {
if (ship_animator_ == nullptr) {
return;
}
@@ -564,42 +426,30 @@ void TitleScene::triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_act
}
void TitleScene::updateLogoAnimation(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_;
if (!animacio_activa_) {
return;
}
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;
const float TWO_PI = 2.0F * Defaults::Math::PI;
const float OFFSET_X = ORBIT_AMPLITUDE_X * std::sin(TWO_PI * ORBIT_FREQUENCY_X * temps_animacio_);
const float OFFSET_Y = ORBIT_AMPLITUDE_Y * std::sin((TWO_PI * ORBIT_FREQUENCY_Y * temps_animacio_) + ORBIT_PHASE_OFFSET);
// 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));
}
for (std::size_t i = 0; i < lletres_orni_.size(); ++i) {
lletres_orni_[i].position.x = posicions_originals_orni_[i].x + std::round(OFFSET_X);
lletres_orni_[i].position.y = posicions_originals_orni_[i].y + std::round(OFFSET_Y);
}
for (std::size_t i = 0; i < lletres_attack_.size(); ++i) {
lletres_attack_[i].position.x = posicions_originals_attack_[i].x + std::round(OFFSET_X);
lletres_attack_[i].position.y = posicions_originals_attack_[i].y + 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 ||
@@ -608,116 +458,59 @@ void TitleScene::draw() {
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::renderShape(
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::renderShape(
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::renderShape(
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::renderShape(
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 MAIN_SCALE = 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}, MAIN_SCALE, SPACING);
}
dibuixarPeuTitol(SPACING);
if (estat_actual_ != TitleState::MAIN && estat_actual_ != TitleState::PLAYER_JOIN_PHASE) {
return;
}
if (animacio_activa_) {
float temps_shadow = std::max(0.0F, temps_animacio_ - 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 < 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),
};
Rendering::renderShape(sdl_.getRenderer(), lletres_orni_[i].shape, POS_SHADOW, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F, SHADOW_BRIGHTNESS);
}
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),
};
Rendering::renderShape(sdl_.getRenderer(), lletres_attack_[i].shape, POS_SHADOW, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F, SHADOW_BRIGHTNESS);
}
}
for (const auto& lletra : lletres_orni_) {
Rendering::renderShape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F);
}
for (const auto& lletra : lletres_attack_) {
Rendering::renderShape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F);
}
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);
}
dibuixarPeuTitol(SPACING);
}
auto TitleScene::checkSkipButtonPressed() -> bool {
@@ -727,29 +520,23 @@ auto TitleScene::checkSkipButtonPressed() -> bool {
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;
}