diff --git a/source/core/defaults/game.hpp b/source/core/defaults/game.hpp index 63de213..76463dd 100644 --- a/source/core/defaults/game.hpp +++ b/source/core/defaults/game.hpp @@ -39,11 +39,18 @@ 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 + // Attract mode: transició TÍTOL → DEMO. Primer un "dive" de càmera cap al + // punt de fuga (deixa enrere títol/naus/logo) i després una cortinilla negra + // que cau per tapar; a la demo, la cortinilla segueix caient i destapa. + namespace Dive { + constexpr float DURATION = 0.55F; // Durada del dive (s), amb acceleració (ease-in) + constexpr float CAMERA_DISTANCE = 450.0F; // Avanç de la càmera en +Z (passa les naus, a Z≈323) + constexpr float ZOOM_MAX = 7.0F; // Zoom final dels elements 2D (logo + peu) en travessar-los + } // namespace Dive + namespace Curtain { + constexpr float COVER_DURATION = 0.35F; // TÍTOL: la tela cau i tapa + constexpr float REVEAL_DURATION = 0.45F; // DEMO: la tela segueix caient i destapa + } // namespace Curtain // 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/curtain.cpp b/source/core/graphics/curtain.cpp new file mode 100644 index 0000000..843f93e --- /dev/null +++ b/source/core/graphics/curtain.cpp @@ -0,0 +1,71 @@ +// curtain.cpp - Implementació de la cortinilla negra +// © 2026 JailDesigner + +#include "core/graphics/curtain.hpp" + +#include + +#include "core/defaults/game.hpp" +#include "core/math/easing.hpp" + +namespace Graphics { + + namespace { + constexpr float SCREEN_H = static_cast(Defaults::Game::HEIGHT); + constexpr float SCREEN_W = static_cast(Defaults::Game::WIDTH); + } // namespace + + Curtain::Curtain(Rendering::Renderer* renderer) + : renderer_(renderer) {} + + void Curtain::cover(float duration) { + // Caire superior de -H (fora, a dalt) fins a 0 (tela tapant tota la pantalla). + from_ = -SCREEN_H; + to_ = 0.0F; + duration_ = duration; + elapsed_ = 0.0F; + active_ = true; + } + + void Curtain::reveal(float duration) { + // Caire superior de 0 (tapant) fins a +H (tela fora per baix). + from_ = 0.0F; + to_ = SCREEN_H; + duration_ = duration; + elapsed_ = 0.0F; + active_ = true; + } + + void Curtain::update(float delta_time) { + if (!active_) { + return; + } + elapsed_ += delta_time; + } + + auto Curtain::topY() const -> float { + if (duration_ <= 0.0F) { + return to_; + } + const float T = std::clamp(elapsed_ / duration_, 0.0F, 1.0F); + // Ease-in: la tela "cau" accelerant, com per gravetat. + return Easing::lerp(from_, to_, Easing::easeInQuad(T)); + } + + auto Curtain::isDone() const -> bool { + return !active_ || elapsed_ >= duration_; + } + + void Curtain::draw() const { + if (!active_) { + return; + } + const float TOP = topY(); + // Si la tela ja ha sortit completament per baix, no hi ha res a pintar. + if (TOP >= SCREEN_H) { + return; + } + renderer_->pushRect(0.0F, TOP, SCREEN_W, SCREEN_H, 0.0F, 0.0F, 0.0F, 1.0F); + } + +} // namespace Graphics diff --git a/source/core/graphics/curtain.hpp b/source/core/graphics/curtain.hpp new file mode 100644 index 0000000..1e91e1e --- /dev/null +++ b/source/core/graphics/curtain.hpp @@ -0,0 +1,48 @@ +// curtain.hpp - Cortinilla negra per a transicions d'escena +// © 2026 JailDesigner +// +// Tela negra a pantalla completa que es mou SEMPRE cap avall: +// - cover(): cau des de dalt fins a tapar-ho tot (queda negre). +// - reveal(): segueix caient i surt per baix, deixant veure l'escena. +// Una escena la posseeix, l'actualitza cada frame i la dibuixa l'ÚLTIM (per +// damunt de tot). En repòs (no arrencada o reveal acabada) el draw() és no-op. + +#pragma once + +#include "core/rendering/render_context.hpp" + +namespace Graphics { + + class Curtain { + public: + explicit Curtain(Rendering::Renderer* renderer); + + // Tela que cau des de dalt fins a tapar tota la pantalla en 'duration' s. + void cover(float duration); + + // Tela que segueix caient i surt per baix (destapa) en 'duration' s. + void reveal(float duration); + + // Avança el temporitzador intern. + void update(float delta_time); + + // Dibuixa la tela negra a la seva posició vertical actual. No-op si no + // queda res visible. + void draw() const; + + // Cert quan el moviment actual ha acabat (o no s'ha arrencat mai). + [[nodiscard]] auto isDone() const -> bool; + + private: + // Posició actual del caire superior de la tela (píxels lògics). + [[nodiscard]] auto topY() const -> float; + + 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/core/graphics/screen_fade.cpp b/source/core/graphics/screen_fade.cpp deleted file mode 100644 index 76ec391..0000000 --- a/source/core/graphics/screen_fade.cpp +++ /dev/null @@ -1,62 +0,0 @@ -// 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 deleted file mode 100644 index 620a7bf..0000000 --- a/source/core/graphics/screen_fade.hpp +++ /dev/null @@ -1,43 +0,0 @@ -// 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 576feaf..173ec8e 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -56,7 +56,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) starfield_parallax_(sdl.getRenderer()), playfield_(sdl.getRenderer()), border_(sdl.getRenderer()), - fade_(sdl.getRenderer()) { + curtain_(sdl.getRenderer()) { // Recuperar configuración de match des del context match_config_ = context_.getMatchConfig(); @@ -171,8 +171,9 @@ 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); + // La cortinilla arrenca tapant i cau per destapar la demo (continua el + // moviment iniciat al títol, que va acabar amb la pantalla negra). + curtain_.reveal(Defaults::Game::Curtain::REVEAL_DURATION); } else { stage_manager_->init(); } @@ -366,7 +367,7 @@ void GameScene::updateShipsControl(float delta_time) { } auto GameScene::stepDemo(float delta_time) -> bool { - fade_.update(delta_time); // fosa d'entrada des de negre + curtain_.update(delta_time); // cortinilla que destapa la demo // Qualsevol input trenca la demo i torna al títol (música intacta). if (Input::get()->checkAnyPlayerAction(DEMO_EXIT_ACTIONS)) { @@ -714,9 +715,9 @@ void GameScene::draw() { 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(); + // Cortinilla d'entrada de la demo: per damunt de tot. No-op fora del mode + // DEMO (curtain_ mai s'arrenca) i quan ja ha sortit per baix. + curtain_.draw(); } void GameScene::drawEnemies() const { diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index 4561039..717b186 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -9,8 +9,8 @@ #include #include "core/graphics/border.hpp" +#include "core/graphics/curtain.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" @@ -96,8 +96,8 @@ 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_; + // Cortinilla que destapa en entrar a la demo (només mode DEMO; inactiva en partida normal). + Graphics::Curtain curtain_; // [NEW] Stage system std::unique_ptr stage_config_; diff --git a/source/game/scenes/title_scene.cpp b/source/game/scenes/title_scene.cpp index 3174956..5c20fe4 100644 --- a/source/game/scenes/title_scene.cpp +++ b/source/game/scenes/title_scene.cpp @@ -38,7 +38,7 @@ TitleScene::TitleScene(SDLManager& sdl, SceneContext& context) : sdl_(sdl), context_(context), text_(sdl.getRenderer()), - fade_(sdl.getRenderer()) { + curtain_(sdl.getRenderer()) { std::cout << "SceneType Titol: Inicialitzant...\n"; match_config_.player1_active = false; @@ -277,8 +277,9 @@ void TitleScene::dibuixarPeuTitol(float spacing) const { // a 1.0 (a la mida i posició finals, "lluny" al VP). const float SCREEN_CENTRE_X = Defaults::Game::WIDTH / 2.0F; const float SCREEN_CENTRE_Y = Defaults::Game::HEIGHT / 2.0F; - const float JAILGAMES_S = std::lerp(S::FOOTER_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_jailgames_progress_)); - const float COPYRIGHT_S = std::lerp(S::FOOTER_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_copyright_progress_)); + // dive_zoom_ (attract) afegeix el zoom del dive per travessar el peu. + const float JAILGAMES_S = dive_zoom_ * std::lerp(S::FOOTER_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_jailgames_progress_)); + const float COPYRIGHT_S = dive_zoom_ * std::lerp(S::FOOTER_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_copyright_progress_)); const float JAILGAMES_RENDER_SCALE = Defaults::Title::Layout::JAILGAMES_SCALE * JAILGAMES_S; for (const auto& letter : letters_jailgames_) { @@ -334,8 +335,11 @@ void TitleScene::update(float delta_time) { case TitleState::BLACK_SCREEN: updateBlackScreenState(delta_time); break; - case TitleState::DEMO_FADE_OUT: - updateDemoFadeOutState(delta_time); + case TitleState::DEMO_DIVE: + updateDemoDiveState(delta_time); + break; + case TitleState::DEMO_CURTAIN: + updateDemoCurtainState(delta_time); break; } @@ -372,11 +376,14 @@ void TitleScene::update(float delta_time) { demo_cfg.player2_active = (SC.players >= 2); demo_cfg.mode = GameConfig::Mode::DEMO; context_.setMatchConfig(demo_cfg); - // 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); + // No saltem en sec: primer un "dive" de càmera cap al punt de fuga + // (deixa enrere títol/naus/logo) i després la cortinilla. L'estat + // deixa de ser MAIN, així que ni es re-dispara la demo ni s'accepta + // START durant la transició. Amaguem ja el "PREMEU START". + press_start_visible_ = false; + current_state_ = TitleState::DEMO_DIVE; + dive_time_ = 0.0F; + dive_zoom_ = 1.0F; temps_acumulat_ = 0.0F; } } @@ -482,9 +489,29 @@ void TitleScene::updateBlackScreenState(float delta_time) { } } -void TitleScene::updateDemoFadeOutState(float delta_time) { - fade_.update(delta_time); - if (fade_.isDone()) { +void TitleScene::updateDemoDiveState(float delta_time) { + namespace D = Defaults::Game::Dive; + dive_time_ += delta_time; + const float T = std::min(dive_time_ / D::DURATION, 1.0F); + const float EASED = Easing::easeInQuad(T); // acceleració cap al punt de fuga + + // Càmera 3D real cap a +Z: starfield i naus es projecten amb la càmera, així + // que les estrelles es rasguen i les naus creixen i s'escapen pels costats. + if (camera_ != nullptr) { + camera_->setPosition(Vec3{.x = 0.0F, .y = 0.0F, .z = EASED * D::CAMERA_DISTANCE}); + } + // Logo i peu són 2D: els fakegem el dive amb un zoom des del centre. + dive_zoom_ = std::lerp(1.0F, D::ZOOM_MAX, EASED); + + if (T >= 1.0F) { + current_state_ = TitleState::DEMO_CURTAIN; + curtain_.cover(Defaults::Game::Curtain::COVER_DURATION); + } +} + +void TitleScene::updateDemoCurtainState(float delta_time) { + curtain_.update(delta_time); + if (curtain_.isDone()) { context_.setNextScene(SceneType::GAME); } } @@ -594,7 +621,8 @@ void TitleScene::draw() { current_state_ == TitleState::STARFIELD || current_state_ == TitleState::MAIN || current_state_ == TitleState::PLAYER_JOIN_PHASE || - current_state_ == TitleState::DEMO_FADE_OUT)) { + current_state_ == TitleState::DEMO_DIVE || + current_state_ == TitleState::DEMO_CURTAIN)) { ship_animator_->draw(); } drawFlashes(); @@ -603,19 +631,21 @@ void TitleScene::draw() { return; } - // DEMO_FADE_OUT es pinta com MAIN (logo + text) perquè el títol segueixi - // visible sota la fosa a negre. + // DEMO_DIVE/DEMO_CURTAIN es pinten com MAIN (logo + peu) perquè el títol + // segueixi visible sota el dive i la cortinilla. 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) + current_state_ != TitleState::DEMO_DIVE && + current_state_ != TitleState::DEMO_CURTAIN) { + curtain_.draw(); // BLACK_SCREEN i altres: només la cortinilla (si activa) return; } // Factor d'escala+posició per simular un moviment 3D des de l'usuari (prop, // sprite gran i posició projectada extrema) cap al VP (lluny, sprite a la // seva mida i posició finals). Pivot: centre de pantalla (= projecció VP). - const float LOGO_S = std::lerp(Defaults::Title::Sequence::LOGO_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_logo_progress_)); + // El dive (attract) hi afegeix un zoom extra (dive_zoom_) per travessar-los. + const float LOGO_S = dive_zoom_ * std::lerp(Defaults::Title::Sequence::LOGO_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_logo_progress_)); const float SCREEN_CENTRE_X = Defaults::Game::WIDTH / 2.0F; const float SCREEN_CENTRE_Y = Defaults::Game::HEIGHT / 2.0F; const float LOGO_RENDER_SCALE = Defaults::Title::Layout::LOGO_SCALE * LOGO_S; @@ -680,8 +710,8 @@ void TitleScene::draw() { dibuixarPeuTitol(SPACING); - // Fosa a negre (attract): per damunt de tot. No-op si no està activa. - fade_.draw(); + // Cortinilla negra (attract): per damunt de tot. No-op si no està activa. + curtain_.draw(); } auto TitleScene::checkSkipButtonPressed() -> bool { diff --git a/source/game/scenes/title_scene.hpp b/source/game/scenes/title_scene.hpp index c735b7b..28f9c74 100644 --- a/source/game/scenes/title_scene.hpp +++ b/source/game/scenes/title_scene.hpp @@ -18,7 +18,7 @@ #include #include "core/graphics/camera3d.hpp" -#include "core/graphics/screen_fade.hpp" +#include "core/graphics/curtain.hpp" #include "core/graphics/shape.hpp" #include "core/graphics/starfield.hpp" #include "core/graphics/vector_text.hpp" @@ -47,7 +47,8 @@ class TitleScene final : public Scene { MAIN, PLAYER_JOIN_PHASE, BLACK_SCREEN, - DEMO_FADE_OUT, // Attract: fosa a negre abans de saltar a la demo + DEMO_DIVE, // Attract: dive de càmera cap al punt de fuga + DEMO_CURTAIN, // Attract: cortinilla negra que cau i tapa abans del salt }; struct LogoLetter { @@ -62,7 +63,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) + Graphics::Curtain curtain_; // Cortinilla negra en saltar a la demo (attract) std::unique_ptr camera_; std::unique_ptr starfield_; std::unique_ptr ship_animator_; @@ -84,6 +85,11 @@ class TitleScene final : public Scene { float temps_acumulat_{0.0F}; float idle_timer_{0.0F}; // Attract mode: inactivitat acumulada al state MAIN + // Dive (attract): temps transcorregut i zoom aplicat als elements 2D + // (logo + peu) mentre la càmera avança cap al punt de fuga. + float dive_time_{0.0F}; + float dive_zoom_{1.0F}; + std::vector letters_orni_; std::vector letters_attack_; float dynamic_attack_y_{0.0F}; @@ -146,7 +152,8 @@ 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 updateDemoDiveState(float delta_time); + void updateDemoCurtainState(float delta_time); void handleSkipInput(); void handleStartInput(); void triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, const char* log_prefix);