Files
orni-attack/source/game/scenes/title_scene.cpp
T
JailDesigner b746578bc8 Cabeceras: unificar copyright a "© 2026 JailDesigner" en todo source/
Sustituye en bloque las cabeceras de los archivos por una sola línea de
copyright. Cero rastro de "Visente", "Sergi" o "1999" en el árbol del
proyecto. Se eliminan también las variantes "© 2025 Port a C++20", "© 2025
Port a C++20 con SDL3" y "© 2025 Orni Attack" (con todas sus colas
descriptivas como "Arquitectura de entidades" o "Sistema de física"), que
en este punto eran ruido histórico.

Aplicado con un par de sed (find -type f, excluyendo source/external y
source/legacy):

  1. \|^// © 1999 Visente i Sergi (versión Pascal)$|d
  2. s|^// © 2025 (Port a C++20.*|Orni Attack.*)$|// © 2026 JailDesigner|

Verificado: la única variante de cabecera tras el sweep es
"// © 2026 JailDesigner".

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

740 lines
28 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// title_scene.cpp - Implementació de l'escena de título
// © 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/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;
}