Files
orni-attack/source/game/scenes/title_scene.cpp
T
JailDesigner ed98ef612e Fase 3: import del subsistema de audio desde AEEA
Reemplaza el audio antiguo de orni_attack (singleton con new/delete
raw, sin efectos, sin crossfade) por el subsistema moderno de AEEA
(unique_ptr, RAII, crossfade nativo, echo/reverb, pitch-shift,
callbacks de fin de pista, getMusicDurationMs para timelines
deterministas).

Eliminados:
- source/core/audio/audio_cache.{hpp,cpp} (1 cache por subsistema)
- source/core/audio/jail_audio.hpp viejo (motor inline globals)
- source/external/stb_vorbis.h (v1.20)

Añadidos (copiados de AEEA, traducidos comentarios al castellano):
- source/core/audio/audio.{hpp,cpp} — singleton con Audio::Config inyectada
- source/core/audio/audio_adapter.{hpp,cpp} — adapter para getMusic/getSound
- source/core/audio/audio_effects.{hpp,cpp} — Schroeder reverb + echo DSP
- source/core/audio/jail_audio.{hpp,cpp} — Ja::Engine class-based, streaming
- source/core/audio/sound_effects_config.{hpp,cpp} — presets YAML (opcional)
- source/external/stb_vorbis.c (v1.22) — OGG decoder, versión más reciente
- source/external/stb_vorbis_impl.cpp — TU aislada para evitar clang-tidy

Adaptaciones:
- audio_adapter.cpp implementado a medida para orni: usa
  Resource::Helper::loadFile (no Resource::Cache de AEEA que orni no
  tiene). Cache local con unique_ptr<Ja::Music> / unique_ptr<Ja::Sound>.
- Includes: utils/defaults.hpp -> core/defaults.hpp, utils/log.hpp
  reemplazado por iostream con std::cerr/std::cout.

API breaking changes (callsites migrados):
- Audio::init() -> Audio::init(Config); el Director construye la Config
  desde Defaults::Audio::* (ENABLED, VOLUME, MUSIC_*, SOUND_*).
- Audio::get()->getMusicState() -> Audio::getMusicState() (ahora static).
- AudioCache::getMusic/getSound -> AudioResource::getMusic/getSound.

Defaults::Audio consolidado: ahora aglutina las constantes que antes
estaban repartidas entre namespace Audio (VOLUME, ENABLED), namespace
Music (VOLUME, ENABLED), namespace Sound (VOLUME, ENABLED). Las pistas
y rutas de efectos siguen en Music::* / Sound::*. Añadidas FREQUENCY,
FORMAT, CHANNELS, CROSSFADE_MS, VOLUME_STEP para el motor.

Beneficios para fases siguientes:
- Crossfade en transiciones de escena (uso: playMusic(name, -1, 1500)).
- Pitch-shift para variaciones de SFX (Audio::playSound(name, group, 0.95)).
- Echo/reverb DSP via playSoundWithEcho/Reverb (sounds.yaml presets).
- Callbacks setOnMusicEnded para sincronizar eventos con el fin de pista.

Compila y enlaza. Pendiente: test runtime del usuario.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:43:01 +02:00

731 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
// © 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/input/mouse.hpp"
#include "core/rendering/shape_renderer.hpp"
#include "core/system/scene_context.hpp"
#include "core/system/global_events.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();
// 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::run() {
SDL_Event event;
Uint64 last_time = SDL_GetTicks();
while (SceneManager::actual == SceneType::TITLE) {
// Calcular delta_time real
Uint64 current_time = SDL_GetTicks();
float delta_time = (current_time - last_time) / 1000.0F;
last_time = current_time;
// Limitar delta_time per evitar grandes salts
delta_time = std::min(delta_time, 0.05F);
// Actualitzar counter de FPS
sdl_.updateFPS(delta_time);
// Actualitzar visibilitat del cursor (auto-ocultar)
Mouse::updateCursorVisibility();
// Actualitzar sistema de input ABANS del event loop
Input::get()->update();
// Processar events SDL
while (SDL_PollEvent(&event)) {
// Manejo de finestra
if (sdl_.handleWindowEvent(event)) {
continue;
}
// Events globals (F1/F2/F3/F4/ESC/QUIT)
if (GlobalEvents::handle(event, sdl_, context_)) {
continue;
}
// Processar events de l'escena
processar_events(event);
}
// Actualitzar lógica
update(delta_time);
// Actualitzar sistema de audio
Audio::update();
// Actualitzar colors oscil·lats
sdl_.updateColors(delta_time);
// Netejar pantalla
sdl_.clear(0, 0, 0);
// Actualitzar context de renderizado (factor de scale global)
sdl_.updateRenderingContext();
// Dibuixar
draw();
// Presentar renderer (swap buffers)
sdl_.present();
}
std::cout << "SceneType Titol: Finalitzant...\n";
}
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
SceneManager::actual = 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';
context_.setNextScene(SceneType::GAME);
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);
}
// === Copyright a la part inferior (centrat horitzontalment, dues línies) ===
const float escala_copy = Defaults::Title::Layout::COPYRIGHT_SCALE;
const float copy_height = text_.get_text_height(escala_copy);
const float line_spacing = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT_LINE_SPACING;
// Línea 1: Original (© 1999 Visente i Sergi)
std::string copyright_original = Project::COPYRIGHT_ORIGINAL;
for (char& c : copyright_original) {
if (c >= 'a' && c <= 'z') {
c = c - 32; // Uppercase
}
}
// Línea 2: Port (© 2025 jaildesigner)
std::string copyright_port = Project::COPYRIGHT_PORT;
for (char& c : copyright_port) {
if (c >= 'a' && c <= 'z') {
c = c - 32; // Uppercase
}
}
// Calcular posicions (anclatge des del top + separació)
float y_line1 = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS;
float y_line2 = y_line1 + copy_height + line_spacing; // Línea 2 debajo de línea 1
// Renderizar línees centrades
float centre_x = Defaults::Game::WIDTH / 2.0F;
text_.renderCentered(copyright_original, {.x = centre_x, .y = y_line1}, escala_copy, spacing);
text_.renderCentered(copyright_port, {.x = centre_x, .y = y_line2}, escala_copy, 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::processar_events(const SDL_Event& event) {
// No procesar eventos genéricos aquí - la lógica se movió a update()
}