Files
orni-attack/source/game/scenes/title_scene.cpp
T
JailDesigner bbbb8d47ae Lint: rename públicos al inglés + refactor cognitive-complexity + unused-includes
Identifier-naming: rename de métodos públicos y cross-file al inglés
(camelBack), traducción de campos y locales en el proceso (TitleShip,
StageManager, SpawnController, ShipAnimator, helpers de PlayArea, etc.).

Refactor por cognitive-complexity (>25): GameScene::draw (59→3) con 9
helpers de estado, PhysicsWorld::resolveBodyCollisions (35→5) extrayendo
resolveBodyPair, Options::load{Window,Physics,Audio}ConfigFromYaml
(32/49/57→5/2/3) con templates readField, TitleScene::update (68→4) con
5 sub-pasos por estado + handleSkipInput/handleStartInput +
triggerExitForJoinedPlayers, DebrisManager::explode (39→3) con
extractSegments/spawnDebris/applyAngularVelocity/applyVisualRotation.

use-anyofallof: bucles → std::ranges::any_of/all_of en Input,
ShipAnimator y SpawnController.

readability-static-accessed-through-instance: Director::run y
VectorText::getTextWidth/Height invocados por clase.

readability-convert-member-functions-to-static: ResourcePack::decryptData.

unused-includes: eliminación de <utility>, <cstdint>, <vector>,
<iostream>, defaults.hpp y otros no usados directamente en headers y
unidades de traducción. Restablecido core/defaults.hpp en title_scene.cpp
(falsa "unused" del header).

Bug fix: eliminado isActive() duplicado en Bullet (redeclaración tras
rename de esta_activa→isActive que chocaba con el override de Entity).

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

756 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/defaults.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())
{
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_->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_->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 = {
"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->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);
}
}
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->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);
}
}
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->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 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::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') {
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:
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;
// 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
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
}
}
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í)
}
}
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
animacio_activa_ = true;
}
// Fase 3: Animación completa (12s+)
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;
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
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;
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, "");
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_.jugador1_actiu && !p1_was_active) {
ship_animator_->triggerExitAnimationForPlayer(1);
std::cout << "[TitleScene] P1 " << log_prefix << "ship exiting\n";
}
if (match_config_.jugador2_actiu && !p2_was_active) {
ship_animator_->triggerExitAnimationForPlayer(2);
std::cout << "[TitleScene] P2 " << log_prefix << "ship exiting\n";
}
}
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_;
// 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::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);
}
}
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;
}