diff --git a/source/game/scenes/logo_scene.cpp b/source/game/scenes/logo_scene.cpp index 3d14719..172173f 100644 --- a/source/game/scenes/logo_scene.cpp +++ b/source/game/scenes/logo_scene.cpp @@ -32,20 +32,41 @@ namespace { LogoScene::LogoScene(std::shared_ptr logo, bool reset) : logo_(std::move(logo)) { std::cout << "[scene] " << SCENE_NAME << (reset ? " start (reset)\n" : " start\n"); - // RESET = "Reiniciar" del menú: tornem al punt zero del joc, així que - // qualsevol música o so en marxa (la del joc, els sons d'UI, etc.) ha - // d'apagar-se abans que el LOGO comenci. En el flux normal no toquem - // res — la música de TITLE pot seguir sonant durant l'attract-out. if (reset) { if (auto* audio = Audio::get(); audio != nullptr) { audio->stopMusic(); audio->stopAllSounds(); } } + // Arranca el fade-in: el primer frame ja pinta negre opac que + // s'esvaeix cap a la imatge durant FADE_IN_MS. + fade_.begin(FADE_IN_MS, Transitions::FadeToBlack::Direction::FADE_IN); } LogoScene::~LogoScene() = default; +auto LogoScene::wantsSkip() -> bool { + return std::ranges::any_of(EventBuffer::events, [](const SDL_Event& ev) { + return ev.type == SDL_EVENT_KEY_DOWN && !Placeholder::isReservedScancode(ev.key.scancode); + }); +} + +void LogoScene::enterMiddle() { + phase_ = Phase::MIDDLE; + phase_elapsed_ms_ = 0; +} + +void LogoScene::enterFadeOut() { + phase_ = Phase::FADE_OUT; + phase_elapsed_ms_ = 0; + fade_.begin(FADE_OUT_MS, Transitions::FadeToBlack::Direction::FADE_OUT); +} + +void LogoScene::enterDone() { + phase_ = Phase::DONE; + exit_request_ = ExitRequest{.next = SceneId::TITLE, .params = TitleParams{}}; +} + void LogoScene::update(int delta_ms) { GlobalInputs::update(delta_ms); if (GlobalInputs::wantsQuit()) { @@ -53,14 +74,40 @@ void LogoScene::update(int delta_ms) { return; } - timer_ms_ -= delta_ms; + // Skip: qualsevol tecla no reservada salta directament a la + // transició cap a TITLE. El propi TitleScene fa el seu fade-in + // inicial des de negre, així que la sortida no queda abrupta. + if (phase_ != Phase::DONE && wantsSkip()) { + enterDone(); + } - const bool SKIPPED = std::ranges::any_of(EventBuffer::events, [](const SDL_Event& ev) { - return ev.type == SDL_EVENT_KEY_DOWN && !Placeholder::isReservedScancode(ev.key.scancode); - }); + if (phase_ != Phase::DONE) { + phase_elapsed_ms_ += delta_ms; + fade_.tick(delta_ms); + } - if (timer_ms_ <= 0 || SKIPPED) { - exit_request_ = ExitRequest{.next = SceneId::TITLE, .params = TitleParams{}}; + switch (phase_) { + case Phase::FADE_IN: + if (phase_elapsed_ms_ >= FADE_IN_MS) { enterMiddle(); } + break; + case Phase::MIDDLE: + // Música del títol al cor de la fase central. La crida és + // idempotent (Audio detecta same-track + same-loop i no + // reinicia), així que TitleScene podrà fer el seu propi + // playMusic sense interrompre la pista. + if (!music_started_ && phase_elapsed_ms_ >= MUSIC_TRIGGER_MS) { + music_started_ = true; + if (auto* audio = Audio::get(); audio != nullptr) { + audio->playMusic("title.ogg", -1); + } + } + if (phase_elapsed_ms_ >= MIDDLE_MS) { enterFadeOut(); } + break; + case Phase::FADE_OUT: + if (phase_elapsed_ms_ >= FADE_OUT_MS) { enterDone(); } + break; + case Phase::DONE: + break; } Notifier::get().update(delta_ms); @@ -78,6 +125,10 @@ void LogoScene::render() { } Notifier::get().render(); + // Capa de fosa: durant FADE_IN i FADE_OUT pinta el negre semitransparent + // per damunt del logo i del Notifier (no del RenderInfo, que és overlay + // de debug i ha de mantenir-se llegible). + fade_.render(); RenderInfo::get().render(); Screen::get().present(); } diff --git a/source/game/scenes/logo_scene.hpp b/source/game/scenes/logo_scene.hpp index 4b57a95..6621dce 100644 --- a/source/game/scenes/logo_scene.hpp +++ b/source/game/scenes/logo_scene.hpp @@ -1,24 +1,37 @@ #pragma once +#include #include +#include "core/rendering/transitions/fade_to_black.hpp" #include "core/scene/scene.hpp" class Surface; -// Escena del logotip de l'editora. Durada fixa (5 s) o skip amb qualsevol -// tecla no reservada. Quan expira, publica un `ExitRequest` cap a TITLE; -// Director és qui construeix la nova escena (l'escena no coneix els altres -// tipus i es pot exercir en tests sense arrossegar dependències). +// Escena del logotip de l'editora. FSM amb tres fases visibles +// (`FADE_IN` 500 ms → `MIDDLE` 5 s → `FADE_OUT` 500 ms) i una terminal +// (`DONE`) que dispara la transició cap a TITLE. A 2,5 s dins de +// `MIDDLE` arrenca la música del títol perquè entri al cor de l'escena +// i continue cap a TitleScene sense talls (Audio ja és idempotent quan +// es demana la mateixa pista que ja sona). // -// Pinta el GIF `data/logo/logo_jailgames.gif` centrat dins del canvas -// lògic, amb la seua paleta nadiua (no s'aplica la paleta UI). Si arriba -// amb `reset=true` (acció "Reiniciar" del menú de servei), atura la -// música i tots els sons al constructor — així el següent LOGO arrenca +// Skip: qualsevol tecla no reservada salta directament a la transició +// (sense fade-out) — el TitleScene té el seu propi `intro_fade_in_ms`, +// així que la transició queda visualment continua igualment. +// +// Si arriba amb `reset=true` (acció "Reiniciar" del menú de servei), +// atura música i tots els sons al constructor — el següent LOGO arrenca // silenciós encara que veníssem del joc en marxa. class LogoScene : public Scene { public: - static constexpr int DURATION_MS = 5000; + static constexpr int FADE_IN_MS = 500; + static constexpr int MIDDLE_MS = 5000; + static constexpr int FADE_OUT_MS = 500; + // Offset dins de MIDDLE en què comença a sonar la pista del títol; + // triat perquè quedi clarament al cor de la fase central (logo + // estable) i prou abans del fade-out per donar continuïtat sonora a + // TitleScene. + static constexpr int MUSIC_TRIGGER_MS = 2500; LogoScene(std::shared_ptr logo, bool reset); ~LogoScene() override; @@ -27,6 +40,21 @@ class LogoScene : public Scene { void render() override; private: + enum class Phase : std::uint8_t { FADE_IN, + MIDDLE, + FADE_OUT, + DONE }; + + void enterMiddle(); + void enterFadeOut(); + void enterDone(); + [[nodiscard]] static auto wantsSkip() -> bool; + std::shared_ptr logo_; - int timer_ms_{DURATION_MS}; + Phase phase_{Phase::FADE_IN}; + int phase_elapsed_ms_{0}; + bool music_started_{false}; + // Capa de fosa reutilitzada: comença amb FADE_IN al ctor i es + // reaprofita amb FADE_OUT en entrar a la fase corresponent. + Transitions::FadeToBlack fade_; };