refactor(title): la 3D és l'única — elimina backup 2D i renomena als noms canònics
This commit is contained in:
1 parent
a29c2b9cc2
commit
54d3e683a1
16 files changed
+680
-2321
No files matched your search
+131
-344
@@ -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;
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
// title_scene.hpp - Pantalla de título del juego
|
||||
// Muestra message "PRESS BUTTON TO PLAY" y copyright
|
||||
// title_scene.hpp - Escena de títol en 3D real
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Clon de `TitleScene` (2D) que substitueix `Graphics::Starfield` per
|
||||
// `Graphics::Starfield` i `Title::ShipAnimator` per `Title::ShipAnimator`,
|
||||
// afegint una `Graphics::Camera3D` que projecta l'escena en perspectiva real.
|
||||
// Tot el bloc d'overlay 2D (logo "ORNI ATTACK!", "PRESS START TO PLAY", peu
|
||||
// "JAILGAMES + copyright") es manté idèntic.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -11,125 +17,108 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "core/graphics/camera3d.hpp"
|
||||
#include "core/graphics/shape.hpp"
|
||||
#include "core/graphics/starfield.hpp"
|
||||
#include "core/graphics/vector_text.hpp"
|
||||
#include "core/input/input_types.hpp"
|
||||
#include "core/rendering/sdl_manager.hpp"
|
||||
#include "core/system/game_config.hpp"
|
||||
#include "core/system/scene.hpp"
|
||||
#include "core/system/scene_context.hpp"
|
||||
#include "core/system/game_config.hpp"
|
||||
#include "core/types.hpp"
|
||||
#include "game/title/ship_animator.hpp"
|
||||
|
||||
// Botones para INICIAR PARTIDA desde MAIN (solo START)
|
||||
static constexpr std::array<InputAction, 1> START_GAME_BUTTONS = {
|
||||
InputAction::START};
|
||||
|
||||
class TitleScene final : public Scene {
|
||||
public:
|
||||
explicit TitleScene(SDLManager& sdl, SceneManager::SceneContext& context);
|
||||
~TitleScene() override; // Destructor per aturar música
|
||||
public:
|
||||
explicit TitleScene(SDLManager& sdl, SceneManager::SceneContext& context);
|
||||
~TitleScene() override;
|
||||
|
||||
// Scene interface
|
||||
void handleEvent(const SDL_Event& event) override;
|
||||
void update(float delta_time) override;
|
||||
void draw() override;
|
||||
[[nodiscard]] auto isFinished() const -> bool override;
|
||||
void handleEvent(const SDL_Event& event) override;
|
||||
void update(float delta_time) override;
|
||||
void draw() override;
|
||||
[[nodiscard]] auto isFinished() const -> bool override;
|
||||
|
||||
private:
|
||||
// Màquina de estats per la pantalla de título
|
||||
enum class TitleState : std::uint8_t {
|
||||
STARFIELD_FADE_IN, // Fade-in del starfield (3.0s)
|
||||
STARFIELD, // Pantalla con camp de estrelles (4.0s)
|
||||
MAIN, // Pantalla de título con text (indefinit, hasta START)
|
||||
PLAYER_JOIN_PHASE, // Fase de unió de jugadors: fade-out música + text parpellejant (2.5s)
|
||||
BLACK_SCREEN // Pantalla negra de transición (2.0s)
|
||||
};
|
||||
private:
|
||||
enum class TitleState : std::uint8_t {
|
||||
STARFIELD_FADE_IN,
|
||||
STARFIELD,
|
||||
MAIN,
|
||||
PLAYER_JOIN_PHASE,
|
||||
BLACK_SCREEN,
|
||||
};
|
||||
|
||||
// Estructura per emmagatzemar informació de cada lletra del título
|
||||
struct LetraLogo {
|
||||
std::shared_ptr<Graphics::Shape> shape; // Forma vectorial de la lletra
|
||||
Vec2 position; // Posición en pantalla
|
||||
float ancho; // Amplada scaled
|
||||
float altura; // Altura scaled
|
||||
float offset_centre; // Offset del centro per posicionament
|
||||
};
|
||||
struct LetraLogo {
|
||||
std::shared_ptr<Graphics::Shape> shape;
|
||||
Vec2 position;
|
||||
float ancho;
|
||||
float altura;
|
||||
float offset_centre;
|
||||
};
|
||||
|
||||
SDLManager& sdl_;
|
||||
SceneManager::SceneContext& context_;
|
||||
GameConfig::MatchConfig match_config_; // Configuración de jugadors active
|
||||
Graphics::VectorText text_; // Sistema de text vectorial
|
||||
std::unique_ptr<Graphics::Starfield> starfield_; // Camp de estrelles de fons
|
||||
std::unique_ptr<Title::ShipAnimator> ship_animator_; // Naves 3D flotantes
|
||||
TitleState estat_actual_{TitleState::STARFIELD_FADE_IN}; // Estat actual de la màquina
|
||||
float temps_acumulat_{0.0F}; // Temps acumulat per l'state INIT
|
||||
SDLManager& sdl_;
|
||||
SceneManager::SceneContext& context_;
|
||||
GameConfig::MatchConfig match_config_;
|
||||
Graphics::VectorText text_;
|
||||
std::unique_ptr<Graphics::Camera3D> camera_;
|
||||
std::unique_ptr<Graphics::Starfield> starfield_;
|
||||
std::unique_ptr<Title::ShipAnimator> ship_animator_;
|
||||
TitleState estat_actual_{TitleState::STARFIELD_FADE_IN};
|
||||
float temps_acumulat_{0.0F};
|
||||
|
||||
// Lletres del título "ORNI ATTACK!"
|
||||
std::vector<LetraLogo> lletres_orni_; // Lletres de "ORNI" (línia 1)
|
||||
std::vector<LetraLogo> lletres_attack_; // Lletres de "ATTACK!" (línia 2)
|
||||
float y_attack_dinamica_; // Posición Y calculada dinàmicament per "ATTACK!"
|
||||
std::vector<LetraLogo> lletres_orni_;
|
||||
std::vector<LetraLogo> lletres_attack_;
|
||||
float y_attack_dinamica_{0.0F};
|
||||
|
||||
// Logo "JAILGAMES" pequeño sobre el copyright (esquinas inferiores del título).
|
||||
std::vector<LetraLogo> lletres_jailgames_;
|
||||
std::vector<LetraLogo> lletres_jailgames_;
|
||||
|
||||
// Estat de animación del logo
|
||||
float temps_animacio_{0.0F}; // Temps acumulat per animación orbital
|
||||
std::vector<Vec2> posicions_originals_orni_; // Posicions originals de "ORNI"
|
||||
std::vector<Vec2> posicions_originals_attack_; // Posicions originals de "ATTACK!"
|
||||
float temps_animacio_{0.0F};
|
||||
std::vector<Vec2> posicions_originals_orni_;
|
||||
std::vector<Vec2> posicions_originals_attack_;
|
||||
|
||||
// Estat de arrencada de l'animación
|
||||
float temps_estat_main_{0.0F}; // Temps acumulat en state MAIN
|
||||
bool animacio_activa_{false}; // Flag: true cuando animación está activa
|
||||
float factor_lerp_{0.0F}; // Factor de lerp actual (0.0 → 1.0)
|
||||
float temps_estat_main_{0.0F};
|
||||
bool animacio_activa_{false};
|
||||
float factor_lerp_{0.0F};
|
||||
|
||||
// Constants
|
||||
static constexpr float BRIGHTNESS_STARFIELD = 1.2F; // Brightness del starfield (>1.0 = més brillant)
|
||||
static constexpr float DURACIO_FADE_IN = 3.0F; // Duració del fade-in del starfield (1.5 segons)
|
||||
static constexpr float DURACIO_INIT = 4.0F; // Duració de l'state INIT (2 segons)
|
||||
static constexpr float DURACIO_TRANSITION = 2.5F; // Duració de la transición (1.5 segons)
|
||||
static constexpr float ESPAI_ENTRE_LLETRES = 10.0F; // Espai entre lletres
|
||||
static constexpr float BLINK_FREQUENCY = 3.0F; // Freqüència de parpelleig (3 Hz)
|
||||
static constexpr float DURACIO_BLACK_SCREEN = 2.0F; // Duració pantalla negra (2 segons)
|
||||
static constexpr int MUSIC_FADE = 1500; // Duracio del fade de la musica del titol al començar a jugar
|
||||
static constexpr float BRIGHTNESS_STARFIELD = 1.2F;
|
||||
static constexpr float DURACIO_FADE_IN = 3.0F;
|
||||
static constexpr float DURACIO_INIT = 4.0F;
|
||||
static constexpr float DURACIO_TRANSITION = 2.5F;
|
||||
static constexpr float ESPAI_ENTRE_LLETRES = 10.0F;
|
||||
static constexpr float BLINK_FREQUENCY = 3.0F;
|
||||
static constexpr float DURACIO_BLACK_SCREEN = 2.0F;
|
||||
static constexpr int MUSIC_FADE = 1500;
|
||||
|
||||
// Constants de animación del logo
|
||||
static constexpr float ORBIT_AMPLITUDE_X = 4.0F; // Amplitud oscil·lació horitzontal (píxels)
|
||||
static constexpr float ORBIT_AMPLITUDE_Y = 3.0F; // Amplitud oscil·lació vertical (píxels)
|
||||
static constexpr float ORBIT_FREQUENCY_X = 0.8F; // Velocidad oscil·lació horitzontal (Hz)
|
||||
static constexpr float ORBIT_FREQUENCY_Y = 1.2F; // Velocidad oscil·lació vertical (Hz)
|
||||
static constexpr float ORBIT_PHASE_OFFSET = 1.57F; // Desfasament entre X i Y (90° per circular)
|
||||
static constexpr float ORBIT_AMPLITUDE_X = 4.0F;
|
||||
static constexpr float ORBIT_AMPLITUDE_Y = 3.0F;
|
||||
static constexpr float ORBIT_FREQUENCY_X = 0.8F;
|
||||
static constexpr float ORBIT_FREQUENCY_Y = 1.2F;
|
||||
static constexpr float ORBIT_PHASE_OFFSET = 1.57F;
|
||||
|
||||
// Constants de ombra del logo
|
||||
static constexpr float SHADOW_DELAY = 0.5F; // Retard temporal de l'ombra (segons)
|
||||
static constexpr float SHADOW_BRIGHTNESS = 0.4F; // Multiplicador de brightness de l'ombra (0.0-1.0)
|
||||
static constexpr float SHADOW_OFFSET_X = 2.0F; // Offset espacial X fix (píxels)
|
||||
static constexpr float SHADOW_OFFSET_Y = 2.0F; // Offset espacial Y fix (píxels)
|
||||
static constexpr float SHADOW_DELAY = 0.5F;
|
||||
static constexpr float SHADOW_BRIGHTNESS = 0.4F;
|
||||
static constexpr float SHADOW_OFFSET_X = 2.0F;
|
||||
static constexpr float SHADOW_OFFSET_Y = 2.0F;
|
||||
|
||||
// Temporització de l'arrencada de l'animación
|
||||
static constexpr float DELAY_INICI_ANIMACIO = 10.0F; // 10s estàtic antes de animar
|
||||
static constexpr float DURACIO_LERP = 2.0F; // 2s per arribar a amplitud completa
|
||||
static constexpr float DELAY_INICI_ANIMACIO = 10.0F;
|
||||
static constexpr float DURACIO_LERP = 2.0F;
|
||||
|
||||
// Métodos privats
|
||||
void updateLogoAnimation(float delta_time); // Actualitza l'animación orbital del logo
|
||||
// Estático: solo consulta Input (singleton), no estado de la escena.
|
||||
static auto checkSkipButtonPressed() -> bool;
|
||||
auto checkStartGameButtonPressed() -> bool;
|
||||
void initTitle(); // Carrega i posiciona las lletres del título
|
||||
void inicialitzarJailgames(); // Carrega i posiciona el logo JAILGAMES pequeño
|
||||
void dibuixarPeuTitol(float spacing) const; // Logo JAILGAMES + línia de copyright
|
||||
// Càmera 3D: FOV vertical en radians.
|
||||
static constexpr float CAMERA_FOV_Y_RAD = 1.0472F; // 60°
|
||||
|
||||
// Sub-pasos de update() (extreure cada state per reduir complexitat).
|
||||
void updateStarfieldFadeInState(float delta_time);
|
||||
void updateStarfieldState(float delta_time);
|
||||
void updateMainState(float delta_time);
|
||||
void updatePlayerJoinPhaseState(float delta_time);
|
||||
void updateBlackScreenState(float delta_time);
|
||||
// Handlers de input globals (independents de l'state actual).
|
||||
void handleSkipInput();
|
||||
void handleStartInput();
|
||||
// Helper compartit: dispara l'animación de salida per las naves del player que
|
||||
// acaba de fer un join "en aquest frame" (jugadorX_actiu == true && !prev).
|
||||
void triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active,
|
||||
const char* log_prefix);
|
||||
void updateLogoAnimation(float delta_time);
|
||||
static auto checkSkipButtonPressed() -> bool;
|
||||
auto checkStartGameButtonPressed() -> bool;
|
||||
void initTitle();
|
||||
void inicialitzarJailgames();
|
||||
void dibuixarPeuTitol(float spacing) const;
|
||||
|
||||
void updateStarfieldFadeInState(float delta_time);
|
||||
void updateStarfieldState(float delta_time);
|
||||
void updateMainState(float delta_time);
|
||||
void updatePlayerJoinPhaseState(float delta_time);
|
||||
void updateBlackScreenState(float delta_time);
|
||||
void handleSkipInput();
|
||||
void handleStartInput();
|
||||
void triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, const char* log_prefix);
|
||||
};
|
||||
@@ -1,547 +0,0 @@
|
||||
// title_scene_3d.cpp - Implementació de l'escena de títol 3D real
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "title_scene_3d.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 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
|
||||
// al `title_scene.hpp` perquè no volem un acoblament entre la versió 2D i la
|
||||
// 3D mentre conviuen.
|
||||
constexpr std::array<InputAction, 1> START_GAME_BUTTONS_3D = {InputAction::START};
|
||||
|
||||
} // namespace
|
||||
|
||||
TitleScene3D::TitleScene3D(SDLManager& sdl, SceneContext& context)
|
||||
: sdl_(sdl),
|
||||
context_(context),
|
||||
text_(sdl.getRenderer()) {
|
||||
std::cout << "SceneType Titol3D: Inicialitzant...\n";
|
||||
|
||||
match_config_.jugador1_actiu = false;
|
||||
match_config_.jugador2_actiu = false;
|
||||
match_config_.mode = GameConfig::Mode::NORMAL;
|
||||
|
||||
auto option = context_.consumeOption();
|
||||
if (option == Option::JUMP_TO_TITLE_MAIN) {
|
||||
std::cout << "SceneType Titol3D: Opció JUMP_TO_TITLE_MAIN activada\n";
|
||||
estat_actual_ = TitleState::MAIN;
|
||||
temps_estat_main_ = 0.0F;
|
||||
}
|
||||
|
||||
// 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));
|
||||
|
||||
starfield_ = std::make_unique<Graphics::Starfield3D>(
|
||||
sdl_.getRenderer(),
|
||||
camera_.get(),
|
||||
200);
|
||||
if (estat_actual_ == TitleState::MAIN) {
|
||||
starfield_->setBrightness(BRIGHTNESS_STARFIELD);
|
||||
} else {
|
||||
starfield_->setBrightness(0.0F);
|
||||
}
|
||||
|
||||
ship_animator_ = std::make_unique<Title::ShipAnimator3D>(sdl_.getRenderer(), camera_.get());
|
||||
ship_animator_->init();
|
||||
|
||||
if (estat_actual_ == TitleState::MAIN) {
|
||||
ship_animator_->setVisible(true);
|
||||
ship_animator_->startEntryAnimation();
|
||||
} else {
|
||||
ship_animator_->setVisible(false);
|
||||
}
|
||||
|
||||
initTitle();
|
||||
inicialitzarJailgames();
|
||||
|
||||
if (Audio::getMusicState() != Audio::MusicState::PLAYING) {
|
||||
Audio::get()->playMusic("title.ogg");
|
||||
}
|
||||
}
|
||||
|
||||
TitleScene3D::~TitleScene3D() {
|
||||
Audio::get()->stopMusic();
|
||||
}
|
||||
|
||||
void TitleScene3D::initTitle() {
|
||||
using namespace Graphics;
|
||||
|
||||
const std::vector<std::string> FITXERS_ORNI = {
|
||||
"title/letra_o.shp",
|
||||
"title/letra_r.shp",
|
||||
"title/letra_n.shp",
|
||||
"title/letra_i.shp"};
|
||||
|
||||
float ancho_total_orni = 0.0F;
|
||||
for (const auto& file : FITXERS_ORNI) {
|
||||
auto shape = ShapeLoader::load(file);
|
||||
if (!shape || !shape->isValid()) {
|
||||
std::cerr << "[TitleScene3D] 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) * 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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const std::vector<std::string> FITXERS_ATTACK = {
|
||||
"title/letra_a.shp",
|
||||
"title/letra_t.shp",
|
||||
"title/letra_t.shp",
|
||||
"title/letra_a.shp",
|
||||
"title/letra_c.shp",
|
||||
"title/letra_k.shp",
|
||||
"title/letra_exclamacion.shp"};
|
||||
|
||||
float ancho_total_attack = 0.0F;
|
||||
for (const auto& file : FITXERS_ATTACK) {
|
||||
auto shape = ShapeLoader::load(file);
|
||||
if (!shape || !shape->isValid()) {
|
||||
std::cerr << "[TitleScene3D] 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) * 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);
|
||||
|
||||
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_;
|
||||
x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene3D::inicialitzarJailgames() {
|
||||
using namespace Graphics;
|
||||
|
||||
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;
|
||||
|
||||
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 << "[TitleScene3D] 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);
|
||||
}
|
||||
constexpr float ESPAI_JAILGAMES = ESPAI_ENTRE_LLETRES * SCALE;
|
||||
if (!lletres_jailgames_.empty()) {
|
||||
ancho_total += ESPAI_JAILGAMES * static_cast<float>(lletres_jailgames_.size() - 1);
|
||||
}
|
||||
|
||||
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_COPY - 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 TitleScene3D::dibuixarPeuTitol(float spacing) const {
|
||||
for (const auto& lletra : lletres_jailgames_) {
|
||||
Rendering::renderShape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::JAILGAMES_SCALE, 1.0F);
|
||||
}
|
||||
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 TitleScene3D::isFinished() const -> bool {
|
||||
// Aquesta escena és la destinació d'un setNextScene(TITLE) quan ORNI_TITLE_3D
|
||||
// està activat; mentre el context continue marcant TITLE com a destí actual,
|
||||
// l'escena resta viva. També accepta TITLE_3D explícit.
|
||||
const SceneType NEXT = context_.nextScene();
|
||||
return NEXT != SceneType::TITLE && NEXT != SceneType::TITLE_3D;
|
||||
}
|
||||
|
||||
void TitleScene3D::update(float delta_time) {
|
||||
if (starfield_) {
|
||||
starfield_->update(delta_time);
|
||||
}
|
||||
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 TitleScene3D::updateStarfieldFadeInState(float delta_time) {
|
||||
temps_acumulat_ += delta_time;
|
||||
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;
|
||||
starfield_->setBrightness(BRIGHTNESS_STARFIELD);
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene3D::updateStarfieldState(float delta_time) {
|
||||
temps_acumulat_ += delta_time;
|
||||
if (temps_acumulat_ >= DURACIO_INIT) {
|
||||
estat_actual_ = TitleState::MAIN;
|
||||
temps_estat_main_ = 0.0F;
|
||||
animacio_activa_ = false;
|
||||
factor_lerp_ = 0.0F;
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene3D::updateMainState(float delta_time) {
|
||||
temps_estat_main_ += delta_time;
|
||||
if (temps_estat_main_ >= Defaults::Title::Ships::ENTRANCE_DELAY &&
|
||||
ship_animator_ && !ship_animator_->isVisible()) {
|
||||
ship_animator_->setVisible(true);
|
||||
ship_animator_->startEntryAnimation();
|
||||
}
|
||||
|
||||
if (temps_estat_main_ < DELAY_INICI_ANIMACIO) {
|
||||
factor_lerp_ = 0.0F;
|
||||
animacio_activa_ = false;
|
||||
} 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;
|
||||
} else {
|
||||
factor_lerp_ = 1.0F;
|
||||
animacio_activa_ = true;
|
||||
}
|
||||
updateLogoAnimation(delta_time);
|
||||
}
|
||||
|
||||
void TitleScene3D::updatePlayerJoinPhaseState(float delta_time) {
|
||||
temps_acumulat_ += delta_time;
|
||||
updateLogoAnimation(delta_time);
|
||||
|
||||
const bool P1_ABANS = match_config_.jugador1_actiu;
|
||||
const bool P2_ABANS = match_config_.jugador2_actiu;
|
||||
|
||||
if (checkStartGameButtonPressed()) {
|
||||
context_.setMatchConfig(match_config_);
|
||||
triggerExitForJoinedPlayers(P1_ABANS, P2_ABANS, "late join - ");
|
||||
Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME);
|
||||
temps_acumulat_ = 0.0F;
|
||||
}
|
||||
|
||||
if (temps_acumulat_ >= DURACIO_TRANSITION) {
|
||||
estat_actual_ = TitleState::BLACK_SCREEN;
|
||||
temps_acumulat_ = 0.0F;
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene3D::updateBlackScreenState(float delta_time) {
|
||||
temps_acumulat_ += delta_time;
|
||||
if (temps_acumulat_ >= DURACIO_BLACK_SCREEN) {
|
||||
context_.setNextScene(SceneType::GAME);
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene3D::handleSkipInput() {
|
||||
if (estat_actual_ != TitleState::STARFIELD_FADE_IN && estat_actual_ != TitleState::STARFIELD) {
|
||||
return;
|
||||
}
|
||||
if (!checkSkipButtonPressed()) {
|
||||
return;
|
||||
}
|
||||
estat_actual_ = TitleState::MAIN;
|
||||
starfield_->setBrightness(BRIGHTNESS_STARFIELD);
|
||||
temps_estat_main_ = 0.0F;
|
||||
}
|
||||
|
||||
void TitleScene3D::handleStartInput() {
|
||||
if (estat_actual_ != TitleState::MAIN) {
|
||||
return;
|
||||
}
|
||||
const bool P1_ABANS = match_config_.jugador1_actiu;
|
||||
const bool P2_ABANS = match_config_.jugador2_actiu;
|
||||
|
||||
if (!checkStartGameButtonPressed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ship_animator_ && !ship_animator_->isVisible()) {
|
||||
ship_animator_->setVisible(true);
|
||||
ship_animator_->skipToFloatingState();
|
||||
}
|
||||
|
||||
context_.setMatchConfig(match_config_);
|
||||
estat_actual_ = TitleState::PLAYER_JOIN_PHASE;
|
||||
temps_acumulat_ = 0.0F;
|
||||
|
||||
triggerExitForJoinedPlayers(P1_ABANS, P2_ABANS, "");
|
||||
|
||||
Audio::get()->fadeOutMusic(MUSIC_FADE);
|
||||
Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME);
|
||||
}
|
||||
|
||||
void TitleScene3D::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 << "[TitleScene3D] P1 " << log_prefix << "ship exiting\n";
|
||||
}
|
||||
if (match_config_.jugador2_actiu && !p2_was_active) {
|
||||
ship_animator_->triggerExitAnimationForPlayer(2);
|
||||
std::cout << "[TitleScene3D] P2 " << log_prefix << "ship exiting\n";
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene3D::updateLogoAnimation(float delta_time) {
|
||||
if (!animacio_activa_) {
|
||||
return;
|
||||
}
|
||||
temps_animacio_ += delta_time * factor_lerp_;
|
||||
|
||||
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);
|
||||
|
||||
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 TitleScene3D::draw() {
|
||||
if (starfield_ && estat_actual_ != TitleState::BLACK_SCREEN) {
|
||||
starfield_->draw();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
if (estat_actual_ == TitleState::STARFIELD_FADE_IN || estat_actual_ == TitleState::STARFIELD) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 TitleScene3D::checkSkipButtonPressed() -> bool {
|
||||
return Input::get()->checkAnyPlayerAction(ARCADE_BUTTONS);
|
||||
}
|
||||
|
||||
auto TitleScene3D::checkStartGameButtonPressed() -> bool {
|
||||
auto* input = Input::get();
|
||||
bool any_pressed = false;
|
||||
for (auto action : START_GAME_BUTTONS_3D) {
|
||||
if (input->checkActionPlayer1(action, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
if (!match_config_.jugador1_actiu) {
|
||||
match_config_.jugador1_actiu = true;
|
||||
any_pressed = true;
|
||||
}
|
||||
}
|
||||
if (input->checkActionPlayer2(action, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
if (!match_config_.jugador2_actiu) {
|
||||
match_config_.jugador2_actiu = true;
|
||||
any_pressed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return any_pressed;
|
||||
}
|
||||
|
||||
void TitleScene3D::handleEvent(const SDL_Event& event) {
|
||||
(void)event;
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
// title_scene_3d.hpp - Variant 3D real de l'escena de títol
|
||||
// © 2026 JailDesigner
|
||||
//
|
||||
// Clon de `TitleScene` (2D) que substitueix `Graphics::Starfield` per
|
||||
// `Graphics::Starfield3D` i `Title::ShipAnimator` per `Title::ShipAnimator3D`,
|
||||
// afegint una `Graphics::Camera3D` que projecta l'escena en perspectiva real.
|
||||
// Tot el bloc d'overlay 2D (logo "ORNI ATTACK!", "PRESS START TO PLAY", peu
|
||||
// "JAILGAMES + copyright") es manté idèntic.
|
||||
//
|
||||
// Trigger: env var `ORNI_TITLE_3D=1` interceptada al `Director::buildScene`,
|
||||
// o transicions explícites a `SceneType::TITLE_3D`.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "core/graphics/camera3d.hpp"
|
||||
#include "core/graphics/shape.hpp"
|
||||
#include "core/graphics/starfield3d.hpp"
|
||||
#include "core/graphics/vector_text.hpp"
|
||||
#include "core/input/input_types.hpp"
|
||||
#include "core/rendering/sdl_manager.hpp"
|
||||
#include "core/system/game_config.hpp"
|
||||
#include "core/system/scene.hpp"
|
||||
#include "core/system/scene_context.hpp"
|
||||
#include "core/types.hpp"
|
||||
#include "game/title/ship_animator3d.hpp"
|
||||
|
||||
class TitleScene3D final : public Scene {
|
||||
public:
|
||||
explicit TitleScene3D(SDLManager& sdl, SceneManager::SceneContext& context);
|
||||
~TitleScene3D() override;
|
||||
|
||||
void handleEvent(const SDL_Event& event) override;
|
||||
void update(float delta_time) override;
|
||||
void draw() override;
|
||||
[[nodiscard]] auto isFinished() const -> bool override;
|
||||
|
||||
private:
|
||||
enum class TitleState : std::uint8_t {
|
||||
STARFIELD_FADE_IN,
|
||||
STARFIELD,
|
||||
MAIN,
|
||||
PLAYER_JOIN_PHASE,
|
||||
BLACK_SCREEN,
|
||||
};
|
||||
|
||||
struct LetraLogo {
|
||||
std::shared_ptr<Graphics::Shape> shape;
|
||||
Vec2 position;
|
||||
float ancho;
|
||||
float altura;
|
||||
float offset_centre;
|
||||
};
|
||||
|
||||
SDLManager& sdl_;
|
||||
SceneManager::SceneContext& context_;
|
||||
GameConfig::MatchConfig match_config_;
|
||||
Graphics::VectorText text_;
|
||||
std::unique_ptr<Graphics::Camera3D> camera_;
|
||||
std::unique_ptr<Graphics::Starfield3D> starfield_;
|
||||
std::unique_ptr<Title::ShipAnimator3D> ship_animator_;
|
||||
TitleState estat_actual_{TitleState::STARFIELD_FADE_IN};
|
||||
float temps_acumulat_{0.0F};
|
||||
|
||||
std::vector<LetraLogo> lletres_orni_;
|
||||
std::vector<LetraLogo> lletres_attack_;
|
||||
float y_attack_dinamica_{0.0F};
|
||||
|
||||
std::vector<LetraLogo> lletres_jailgames_;
|
||||
|
||||
float temps_animacio_{0.0F};
|
||||
std::vector<Vec2> posicions_originals_orni_;
|
||||
std::vector<Vec2> posicions_originals_attack_;
|
||||
|
||||
float temps_estat_main_{0.0F};
|
||||
bool animacio_activa_{false};
|
||||
float factor_lerp_{0.0F};
|
||||
|
||||
static constexpr float BRIGHTNESS_STARFIELD = 1.2F;
|
||||
static constexpr float DURACIO_FADE_IN = 3.0F;
|
||||
static constexpr float DURACIO_INIT = 4.0F;
|
||||
static constexpr float DURACIO_TRANSITION = 2.5F;
|
||||
static constexpr float ESPAI_ENTRE_LLETRES = 10.0F;
|
||||
static constexpr float BLINK_FREQUENCY = 3.0F;
|
||||
static constexpr float DURACIO_BLACK_SCREEN = 2.0F;
|
||||
static constexpr int MUSIC_FADE = 1500;
|
||||
|
||||
static constexpr float ORBIT_AMPLITUDE_X = 4.0F;
|
||||
static constexpr float ORBIT_AMPLITUDE_Y = 3.0F;
|
||||
static constexpr float ORBIT_FREQUENCY_X = 0.8F;
|
||||
static constexpr float ORBIT_FREQUENCY_Y = 1.2F;
|
||||
static constexpr float ORBIT_PHASE_OFFSET = 1.57F;
|
||||
|
||||
static constexpr float SHADOW_DELAY = 0.5F;
|
||||
static constexpr float SHADOW_BRIGHTNESS = 0.4F;
|
||||
static constexpr float SHADOW_OFFSET_X = 2.0F;
|
||||
static constexpr float SHADOW_OFFSET_Y = 2.0F;
|
||||
|
||||
static constexpr float DELAY_INICI_ANIMACIO = 10.0F;
|
||||
static constexpr float DURACIO_LERP = 2.0F;
|
||||
|
||||
// Càmera 3D: FOV vertical en radians.
|
||||
static constexpr float CAMERA_FOV_Y_RAD = 1.0472F; // 60°
|
||||
|
||||
void updateLogoAnimation(float delta_time);
|
||||
static auto checkSkipButtonPressed() -> bool;
|
||||
auto checkStartGameButtonPressed() -> bool;
|
||||
void initTitle();
|
||||
void inicialitzarJailgames();
|
||||
void dibuixarPeuTitol(float spacing) const;
|
||||
|
||||
void updateStarfieldFadeInState(float delta_time);
|
||||
void updateStarfieldState(float delta_time);
|
||||
void updateMainState(float delta_time);
|
||||
void updatePlayerJoinPhaseState(float delta_time);
|
||||
void updateBlackScreenState(float delta_time);
|
||||
void handleSkipInput();
|
||||
void handleStartInput();
|
||||
void triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, const char* log_prefix);
|
||||
};
|
||||
Reference in new issue
Block a user