diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 4eea305..d4a1193 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -24,6 +24,7 @@ #include "game/scenes/game_scene.hpp" #include "game/scenes/logo_scene.hpp" #include "game/scenes/title_scene.hpp" +#include "game/scenes/title_scene_3d.hpp" #include "global_events.hpp" #include "project.h" #include "scene.hpp" @@ -291,8 +292,17 @@ auto Director::buildScene(SceneType type, SDLManager& sdl, SceneContext& context switch (type) { case SceneType::LOGO: return std::make_unique(sdl, context); - case SceneType::TITLE: + case SceneType::TITLE: { + // Env var ORNI_TITLE_3D=1 redirigeix la TITLE clàssica cap a la + // variant 3D real en proves; en qualsevol altre cas, la 2D. + const char* env = std::getenv("ORNI_TITLE_3D"); + if (env != nullptr && env[0] == '1' && env[1] == '\0') { + return std::make_unique(sdl, context); + } return std::make_unique(sdl, context); + } + case SceneType::TITLE_3D: + return std::make_unique(sdl, context); case SceneType::GAME: return std::make_unique(sdl, context); case SceneType::EXIT: diff --git a/source/core/system/scene_context.hpp b/source/core/system/scene_context.hpp index bd466e3..7d8377d 100644 --- a/source/core/system/scene_context.hpp +++ b/source/core/system/scene_context.hpp @@ -9,16 +9,19 @@ namespace SceneManager { -// Context de transición entre escenes -// Conté l'escena destinació i opciones específiques per aquella escena -class SceneContext { - public: + // Context de transición entre escenes + // Conté l'escena destinació i opciones específiques per aquella escena + class SceneContext { + public: // Tipo de escena del juego enum class SceneType : std::uint8_t { - LOGO, // Pantalla de start (logo JAILGAMES) - TITLE, // Pantalla de título con menú - GAME, // Juego principal (Asteroids) - EXIT // Salir del programa + LOGO, // Pantalla de start (logo JAILGAMES) + TITLE, // Pantalla de título (versió 2D actual). Si l'env var + // ORNI_TITLE_3D=1 està activa, Director::buildScene + // redirigeix aquest valor a TitleScene3D. + TITLE_3D, // Pantalla de títol 3D real (variant en proves) + GAME, // Juego principal (Asteroids) + EXIT // Salir del programa }; // Opciones específiques para cada escena @@ -70,14 +73,14 @@ class SceneContext { return match_config_; } - private: - SceneType next_scene_{SceneType::LOGO}; // SceneType a la qual transicionar - Option option_{Option::NONE}; // Opción específica per l'escena - GameConfig::MatchConfig match_config_; // Configuración de match (jugadors active, mode) -}; + private: + SceneType next_scene_{SceneType::LOGO}; // SceneType a la qual transicionar + Option option_{Option::NONE}; // Opción específica per l'escena + GameConfig::MatchConfig match_config_; // Configuración de match (jugadors active, mode) + }; -// Variable global inline per gestionar l'escena actual (backward compatibility) -// Sincronitzada con context.nextScene() por el Director -inline SceneContext::SceneType actual = SceneContext::SceneType::LOGO; + // Variable global inline per gestionar l'escena actual (backward compatibility) + // Sincronitzada con context.nextScene() por el Director + inline SceneContext::SceneType actual = SceneContext::SceneType::LOGO; } // namespace SceneManager diff --git a/source/game/scenes/title_scene_3d.cpp b/source/game/scenes/title_scene_3d.cpp new file mode 100644 index 0000000..338e9be --- /dev/null +++ b/source/game/scenes/title_scene_3d.cpp @@ -0,0 +1,547 @@ +// title_scene_3d.cpp - Implementació de l'escena de títol 3D real +// © 2026 JailDesigner + +#include "title_scene_3d.hpp" + +#include +#include +#include +#include +#include +#include + +#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 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( + 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(Defaults::Game::WIDTH), + static_cast(Defaults::Game::HEIGHT)); + + starfield_ = std::make_unique( + sdl_.getRenderer(), + camera_.get(), + 200); + if (estat_actual_ == TitleState::MAIN) { + starfield_->setBrightness(BRIGHTNESS_STARFIELD); + } else { + starfield_->setBrightness(0.0F); + } + + ship_animator_ = std::make_unique(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 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(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 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(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 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(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(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; + 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; +} diff --git a/source/game/scenes/title_scene_3d.hpp b/source/game/scenes/title_scene_3d.hpp new file mode 100644 index 0000000..840f802 --- /dev/null +++ b/source/game/scenes/title_scene_3d.hpp @@ -0,0 +1,126 @@ +// 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 + +#include +#include +#include +#include + +#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 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 camera_; + std::unique_ptr starfield_; + std::unique_ptr ship_animator_; + TitleState estat_actual_{TitleState::STARFIELD_FADE_IN}; + float temps_acumulat_{0.0F}; + + std::vector lletres_orni_; + std::vector lletres_attack_; + float y_attack_dinamica_{0.0F}; + + std::vector lletres_jailgames_; + + float temps_animacio_{0.0F}; + std::vector posicions_originals_orni_; + std::vector 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); +};