diff --git a/source/core/defaults/game.hpp b/source/core/defaults/game.hpp index 55ed3dc..63de213 100644 --- a/source/core/defaults/game.hpp +++ b/source/core/defaults/game.hpp @@ -39,6 +39,12 @@ namespace Defaults::Game { constexpr float LEVEL_COMPLETED_DURATION = 3.0F; // Duración total constexpr float LEVEL_COMPLETED_TYPING_RATIO = 0.05F; // ~150ms de typewriter (escan ràpid però visible) + // Attract mode: durada de la fosa a/desde negre a les transicions de demo. + namespace Fade { + constexpr float DEMO_OUT_DURATION = 0.6F; // TÍTOL → DEMO (fosa a negre abans del salt) + constexpr float DEMO_IN_DURATION = 0.6F; // DEMO: fosa des de negre sobre el joc ja en marxa + } // namespace Fade + // Transición INIT_HUD (animación inicial del HUD) constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado diff --git a/source/core/graphics/screen_fade.cpp b/source/core/graphics/screen_fade.cpp new file mode 100644 index 0000000..76ec391 --- /dev/null +++ b/source/core/graphics/screen_fade.cpp @@ -0,0 +1,62 @@ +// screen_fade.cpp - Implementació de la fosa a/desde negre +// © 2026 JailDesigner + +#include "core/graphics/screen_fade.hpp" + +#include +#include + +#include "core/defaults/game.hpp" + +namespace Graphics { + + ScreenFade::ScreenFade(Rendering::Renderer* renderer) + : renderer_(renderer) {} + + void ScreenFade::start(float from, float to, float duration) { + from_ = from; + to_ = to; + duration_ = duration; + elapsed_ = 0.0F; + active_ = true; + } + + void ScreenFade::update(float delta_time) { + if (!active_) { + return; + } + elapsed_ += delta_time; + } + + auto ScreenFade::alpha() const -> float { + if (!active_) { + return 0.0F; + } + if (duration_ <= 0.0F) { + return to_; + } + const float T = std::clamp(elapsed_ / duration_, 0.0F, 1.0F); + return std::lerp(from_, to_, T); + } + + auto ScreenFade::isDone() const -> bool { + return !active_ || elapsed_ >= duration_; + } + + void ScreenFade::draw() const { + const float A = alpha(); + if (A <= 0.0F) { + return; + } + renderer_->pushRect( + 0.0F, + 0.0F, + static_cast(Defaults::Game::WIDTH), + static_cast(Defaults::Game::HEIGHT), + 0.0F, + 0.0F, + 0.0F, + A); + } + +} // namespace Graphics diff --git a/source/core/graphics/screen_fade.hpp b/source/core/graphics/screen_fade.hpp new file mode 100644 index 0000000..620a7bf --- /dev/null +++ b/source/core/graphics/screen_fade.hpp @@ -0,0 +1,43 @@ +// screen_fade.hpp - Fosa a/desde negre per a transicions d'escena +// © 2026 JailDesigner +// +// Rect negre a pantalla completa amb l'alpha animat linealment. L'escena que el +// posseeix l'actualitza cada frame i el dibuixa l'ÚLTIM (per damunt de tot). En +// repòs (default-construït o fosa acabada amb alpha 0) el draw() és un no-op. + +#pragma once + +#include "core/rendering/render_context.hpp" + +namespace Graphics { + + class ScreenFade { + public: + explicit ScreenFade(Rendering::Renderer* renderer); + + // Arrenca una fosa lineal d'alpha 'from' a 'to' en 'duration' segons. + void start(float from, float to, float duration); + + // Avança el temporitzador intern. + void update(float delta_time); + + // Dibuixa el rect negre a pantalla completa amb l'alpha actual. + // No fa res si l'alpha és ~0 (estalvia el pushRect). + void draw() const; + + // Cert quan la fosa ha acabat (o no s'ha arrencat mai). + [[nodiscard]] auto isDone() const -> bool; + + // Alpha actual ∈ [0, 1]. + [[nodiscard]] auto alpha() const -> float; + + private: + Rendering::Renderer* renderer_; + float from_{0.0F}; + float to_{0.0F}; + float duration_{0.0F}; + float elapsed_{0.0F}; + bool active_{false}; + }; + +} // namespace Graphics diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 28365e5..576feaf 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -55,7 +55,8 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) text_(sdl.getRenderer()), starfield_parallax_(sdl.getRenderer()), playfield_(sdl.getRenderer()), - border_(sdl.getRenderer()) { + border_(sdl.getRenderer()), + fade_(sdl.getRenderer()) { // Recuperar configuración de match des del context match_config_ = context_.getMatchConfig(); @@ -170,6 +171,8 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) Audio::get()->enableSound(false); // El fons (graella) ha d'aparèixer ja muntat: la demo és una partida en marxa. playfield_.completeBuild(); + // Fosa des de negre sobre el joc ja en marxa (transició suau des del títol). + fade_.start(1.0F, 0.0F, Defaults::Game::Fade::DEMO_IN_DURATION); } else { stage_manager_->init(); } @@ -363,6 +366,8 @@ void GameScene::updateShipsControl(float delta_time) { } auto GameScene::stepDemo(float delta_time) -> bool { + fade_.update(delta_time); // fosa d'entrada des de negre + // Qualsevol input trenca la demo i torna al títol (música intacta). if (Input::get()->checkAnyPlayerAction(DEMO_EXIT_ACTIONS)) { context_.setNextScene(SceneType::TITLE, Option::JUMP_TO_TITLE_MAIN); @@ -708,6 +713,10 @@ void GameScene::draw() { drawLevelCompletedState(); break; } + + // Fosa d'entrada de la demo: per damunt de tot. No-op fora del mode DEMO + // (fade_ mai s'arrenca) i quan ja ha acabat (alpha 0). + fade_.draw(); } void GameScene::drawEnemies() const { diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index 40ac85e..4561039 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -10,6 +10,7 @@ #include "core/graphics/border.hpp" #include "core/graphics/playfield.hpp" +#include "core/graphics/screen_fade.hpp" #include "core/graphics/starfield_parallax.hpp" #include "core/graphics/vector_text.hpp" #include "core/physics/physics_world.hpp" @@ -95,6 +96,9 @@ class GameScene final : public Scene { // Border del playfield (4 línies amb desplaçaments i flash per impactes) Graphics::Border border_; + // Fosa des de negre en entrar a la demo (només mode DEMO; inactiu en partida normal). + Graphics::ScreenFade fade_; + // [NEW] Stage system std::unique_ptr stage_config_; std::unique_ptr stage_manager_; diff --git a/source/game/scenes/title_scene.cpp b/source/game/scenes/title_scene.cpp index d8a1968..3174956 100644 --- a/source/game/scenes/title_scene.cpp +++ b/source/game/scenes/title_scene.cpp @@ -37,7 +37,8 @@ namespace { TitleScene::TitleScene(SDLManager& sdl, SceneContext& context) : sdl_(sdl), context_(context), - text_(sdl.getRenderer()) { + text_(sdl.getRenderer()), + fade_(sdl.getRenderer()) { std::cout << "SceneType Titol: Inicialitzant...\n"; match_config_.player1_active = false; @@ -333,6 +334,9 @@ void TitleScene::update(float delta_time) { case TitleState::BLACK_SCREEN: updateBlackScreenState(delta_time); break; + case TitleState::DEMO_FADE_OUT: + updateDemoFadeOutState(delta_time); + break; } // Les animacions segueixen pero els inputs es bloquegen mentre el menu @@ -368,7 +372,12 @@ void TitleScene::update(float delta_time) { demo_cfg.player2_active = (SC.players >= 2); demo_cfg.mode = GameConfig::Mode::DEMO; context_.setMatchConfig(demo_cfg); - context_.setNextScene(SceneType::GAME); + // No saltem en sec: fosa a negre i, en acabar, canvi a GAME (el salt + // real el fa updateDemoFadeOutState). L'estat deixa de ser MAIN, així + // que ni es re-dispara la demo ni s'accepta START durant la fosa. + current_state_ = TitleState::DEMO_FADE_OUT; + fade_.start(0.0F, 1.0F, Defaults::Game::Fade::DEMO_OUT_DURATION); + temps_acumulat_ = 0.0F; } } } @@ -473,6 +482,13 @@ void TitleScene::updateBlackScreenState(float delta_time) { } } +void TitleScene::updateDemoFadeOutState(float delta_time) { + fade_.update(delta_time); + if (fade_.isDone()) { + context_.setNextScene(SceneType::GAME); + } +} + void TitleScene::handleSkipInput() { if (current_state_ != TitleState::STARFIELD_FADE_IN && current_state_ != TitleState::STARFIELD) { return; @@ -577,7 +593,8 @@ void TitleScene::draw() { (current_state_ == TitleState::STARFIELD_FADE_IN || current_state_ == TitleState::STARFIELD || current_state_ == TitleState::MAIN || - current_state_ == TitleState::PLAYER_JOIN_PHASE)) { + current_state_ == TitleState::PLAYER_JOIN_PHASE || + current_state_ == TitleState::DEMO_FADE_OUT)) { ship_animator_->draw(); } drawFlashes(); @@ -586,7 +603,12 @@ void TitleScene::draw() { return; } - if (current_state_ != TitleState::MAIN && current_state_ != TitleState::PLAYER_JOIN_PHASE) { + // DEMO_FADE_OUT es pinta com MAIN (logo + text) perquè el títol segueixi + // visible sota la fosa a negre. + if (current_state_ != TitleState::MAIN && + current_state_ != TitleState::PLAYER_JOIN_PHASE && + current_state_ != TitleState::DEMO_FADE_OUT) { + fade_.draw(); // BLACK_SCREEN i altres: només la fosa (si activa) return; } @@ -657,6 +679,9 @@ void TitleScene::draw() { } dibuixarPeuTitol(SPACING); + + // Fosa a negre (attract): per damunt de tot. No-op si no està activa. + fade_.draw(); } auto TitleScene::checkSkipButtonPressed() -> bool { diff --git a/source/game/scenes/title_scene.hpp b/source/game/scenes/title_scene.hpp index 9dafdb8..c735b7b 100644 --- a/source/game/scenes/title_scene.hpp +++ b/source/game/scenes/title_scene.hpp @@ -18,6 +18,7 @@ #include #include "core/graphics/camera3d.hpp" +#include "core/graphics/screen_fade.hpp" #include "core/graphics/shape.hpp" #include "core/graphics/starfield.hpp" #include "core/graphics/vector_text.hpp" @@ -46,6 +47,7 @@ class TitleScene final : public Scene { MAIN, PLAYER_JOIN_PHASE, BLACK_SCREEN, + DEMO_FADE_OUT, // Attract: fosa a negre abans de saltar a la demo }; struct LogoLetter { @@ -60,6 +62,7 @@ class TitleScene final : public Scene { SceneManager::SceneContext& context_; GameConfig::MatchConfig match_config_; Graphics::VectorText text_; + Graphics::ScreenFade fade_; // Fosa a negre en saltar a la demo (attract) std::unique_ptr camera_; std::unique_ptr starfield_; std::unique_ptr ship_animator_; @@ -143,6 +146,7 @@ class TitleScene final : public Scene { void updateMainState(float delta_time); void updatePlayerJoinPhaseState(float delta_time); void updateBlackScreenState(float delta_time); + void updateDemoFadeOutState(float delta_time); void handleSkipInput(); void handleStartInput(); void triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, const char* log_prefix);