refactor(scenes): flux centralitzat amb SceneId+ExitRequest; TitleScene attract → LOGO amb fade-to-black

This commit is contained in:
JailDesigner committed 2026-05-06 18:33:31 +02:00
1 parent 25ce524470
commit b6d6ee471c
17 files changed
+368 -121

No files matched your search

@@ -0,0 +1,39 @@
#include "core/rendering/transitions/fade_to_black.hpp"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include "core/rendering/screen.hpp"
namespace Transitions {
void FadeToBlack::begin(int duration_ms) {
duration_ms_ = duration_ms;
elapsed_ms_ = 0;
}
void FadeToBlack::tick(int delta_ms) {
if (duration_ms_ <= 0) { return; }
elapsed_ms_ = std::min(elapsed_ms_ + delta_ms, duration_ms_);
}
auto FadeToBlack::progress() const -> float {
if (duration_ms_ <= 0) { return 0.0F; }
return std::clamp(static_cast<float>(elapsed_ms_) / static_cast<float>(duration_ms_), 0.0F, 1.0F);
}
void FadeToBlack::render() const {
if (duration_ms_ <= 0) { return; }
const float P = progress();
// Alpha lineal — sense corba quadràtica (això és prerogativa del
// dissolve mosaic, on volem que la mascara fosca arribe tard
// perquè es vegen els blocs). Per al fade net, lineal 0..255.
const auto ALPHA = static_cast<std::uint8_t>(std::round(255.0F * P));
if (ALPHA == 0) { return; }
auto& screen = Screen::get();
screen.blendRect(0, 0, screen.getCanvasWidth(), screen.getCanvasHeight(), 0x00000000U, ALPHA);
}
} // namespace Transitions
@@ -0,0 +1,37 @@
#pragma once
namespace Transitions {
// Fade-out lineal a negre. A diferència de `Mosaic`, no rebina cap
// píxel; només posa una capa negra amb alpha creixent (0 → 255) al
// damunt del canvas. És la transició "neta" per defecte; ideal per a
// sortides on no convé l'estètica chunky de l'arcade dissolve.
//
// Mateixa interfície que `Mosaic` (begin/tick/render/isActive/
// isFinished/progress) perquè els llocs de crida puguen escollir-ne
// una o l'altra sense canviar el patró d'ús.
class FadeToBlack {
public:
FadeToBlack() = default;
// Arrenca un fade nou. Si ja n'hi havia un actiu, el reemplaça.
// `duration_ms <= 0` deixa l'efecte inactiu.
void begin(int duration_ms);
void tick(int delta_ms);
[[nodiscard]] auto isActive() const -> bool { return duration_ms_ > 0 && elapsed_ms_ < duration_ms_; }
[[nodiscard]] auto isFinished() const -> bool { return duration_ms_ > 0 && elapsed_ms_ >= duration_ms_; }
[[nodiscard]] auto progress() const -> float;
// Pinta la capa negra amb alpha proporcional al `progress()`.
// Cridar després del render normal de l'escena, abans de
// `Screen::present()`.
void render() const;
private:
int duration_ms_{0};
int elapsed_ms_{0};
};
} // namespace Transitions
+9 -3
View File
@@ -1,7 +1,13 @@
#pragma once
#include <memory>
#include <any>
#include <utility>
// Base de totes les escenes. La transició entre escenes va per
// `exit_request_`: l'escena hi posa un `ExitRequest` (definit a
// `game/scenes/scene_flow.hpp`) amagat dins d'un `std::any` perquè
// `core/scene/scene.hpp` no ha de saber dels tipus del joc. `Director`
// fa el `std::any_cast` al canvi de frame i construeix la nova escena.
class Scene {
public:
virtual ~Scene() = default;
@@ -10,9 +16,9 @@ class Scene {
virtual void render() = 0;
[[nodiscard]] auto wantsQuit() const -> bool { return wants_quit_; }
[[nodiscard]] auto nextScene() -> std::unique_ptr<Scene> { return std::move(next_scene_); }
[[nodiscard]] auto takeExitRequest() -> std::any { return std::exchange(exit_request_, {}); }
protected:
bool wants_quit_{false};
std::unique_ptr<Scene> next_scene_;
std::any exit_request_;
};
+47 -25
View File
@@ -3,6 +3,7 @@
#include <SDL3/SDL.h>
#include <algorithm>
#include <any>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
@@ -10,6 +11,7 @@
#include <memory>
#include <string>
#include <utility>
#include <variant>
#include "core/audio/audio.hpp"
#include "core/audio/sound_effects_config.hpp"
@@ -196,6 +198,7 @@ Director::Director() {
auto placeholder_font_surface = Resource::Cache::get().getSurface(PLACEHOLDER_FONT_GIF_KEY);
auto placeholder_text = std::make_shared<Text>(placeholder_font_surface, UiConfig::get().placeholderFont().fnt);
placeholder_text->setDefaultKerning(UiConfig::get().placeholderFont().kerning);
placeholder_text_ = placeholder_text;
// Font del menú de servei (F9). El títol va amb la variant `display`
// i els items amb `condensed-display` (més compacte perquè caben més
@@ -210,6 +213,7 @@ Director::Director() {
};
auto menu_title_text = load_menu_font(UiConfig::get().menuTitleFont());
auto menu_items_text = load_menu_font(UiConfig::get().menuItemsFont());
menu_title_text_ = menu_title_text;
// Mini-logo del peu de la TITLE. Es preloda ací per consistència
// amb la resta d'assets d'UI; manté la seua paleta original (no la
@@ -218,12 +222,14 @@ Director::Director() {
constexpr const char* MINI_LOGO_KEY = "logo_jailgames_mini.gif";
Resource::Cache::get().loadSurface(MINI_LOGO_KEY, "data/logo/logo_jailgames_mini.gif");
auto mini_logo_surface = Resource::Cache::get().getSurface(MINI_LOGO_KEY);
mini_logo_surface_ = mini_logo_surface;
// Fons sencer de la TITLE (320×240). Igual que el minilogo, conserva
// la seua paleta nadiua perquè el GIF ja porta els colors finals.
constexpr const char* TITLE_BG_KEY = "title_bg.gif";
Resource::Cache::get().loadSurface(TITLE_BG_KEY, "data/title/title_bg.gif");
auto title_bg_surface = Resource::Cache::get().getSurface(TITLE_BG_KEY);
title_bg_surface_ = title_bg_surface;
ServiceMenu::get().init(menu_title_text, menu_items_text, ui_palette);
// Overlay hook: Screen::present() cridarà aquesta lambda al principi,
@@ -258,35 +264,18 @@ Director::Director() {
std::cout << "Director: start_scene=" << DebugConfig::toString(START)
<< " starting_players=" << DebugConfig::toString(STARTING_PLAYERS) << '\n';
// `make_game` i `make_intro` reben `StartingPlayers` com a paràmetre
// perquè la TitleScene tria al moment quins jugadors entren (P1, P2,
// BOTH) segons la pulsació. La IntroScene ho reenvia a la GameScene.
// El valor de `STARTING_PLAYERS` només s'usa com a punt d'entrada en
// mode Debug quan saltem directament a INTRO o GAME (saltant TITLE).
auto make_game = [](DebugConfig::StartingPlayers players) -> std::unique_ptr<Scene> {
auto cfg = GameScene::defaultConfig();
cfg.starting_players = players;
return std::make_unique<GameScene>(std::move(cfg));
};
auto make_intro = [placeholder_text, make_game](DebugConfig::StartingPlayers players) -> std::unique_ptr<Scene> {
return std::make_unique<IntroScene>(placeholder_text, players, make_game);
};
auto make_title = [menu_title_text, placeholder_text, title_bg_surface, mini_logo_surface, make_intro]() -> std::unique_ptr<Scene> {
return std::make_unique<TitleScene>(menu_title_text, placeholder_text, title_bg_surface, mini_logo_surface, make_intro);
};
switch (START) {
case DebugConfig::StartScene::LOGO:
current_scene_ = std::make_unique<LogoScene>(placeholder_text, make_title);
current_scene_ = buildScene(SceneId::LOGO, LogoParams{});
break;
case DebugConfig::StartScene::TITLE:
current_scene_ = make_title();
current_scene_ = buildScene(SceneId::TITLE, TitleParams{});
break;
case DebugConfig::StartScene::INTRO:
current_scene_ = make_intro(STARTING_PLAYERS);
current_scene_ = buildScene(SceneId::INTRO, IntroParams{.starting_players = STARTING_PLAYERS});
break;
case DebugConfig::StartScene::GAME:
current_scene_ = make_game(STARTING_PLAYERS);
current_scene_ = buildScene(SceneId::GAME, GameParams{.starting_players = STARTING_PLAYERS});
break;
}
last_ticks_ms_ = SDL_GetTicks();
@@ -404,13 +393,46 @@ void Director::processMenuInput() {
void Director::switchSceneIfRequested() {
if (!current_scene_) { return; }
if (auto next = current_scene_->nextScene()) {
// TODO: migrar a logger unificat (INC-03 a docs/audit-core-rendering.md).
std::cout << "Director: scene transition\n";
current_scene_ = std::move(next);
// L'escena ha publicat un `ExitRequest` dins de `exit_request_`.
// Director el desempaqueta i construeix la nova escena via
// `buildScene`. Si la `std::any_cast` falla (tipus inesperat), el
// log avisa i no canviem d'escena.
auto request = current_scene_->takeExitRequest();
if (!request.has_value()) { return; }
try {
const auto& req = std::any_cast<const ExitRequest&>(request);
std::cout << "Director: scene transition → " << toString(req.next) << '\n';
current_scene_ = buildScene(req.next, req.params);
} catch (const std::bad_any_cast&) {
std::cerr << "Director: exit_request_ amb tipus inesperat — ignorant\n";
}
}
auto Director::buildScene(SceneId id, const SceneParams& params) -> std::unique_ptr<Scene> {
// Durant la migració, les escenes que encara fan servir factories
// legacy reben lambdes primes que tornen a delegar a `buildScene`.
// Així el motor d'escena ja és Director-driven encara que els
// ctors antics no l'utilitzen directament. Quan totes estiguen
// migrades a `ExitRequest`, aquestes lambdes desapareixeran.
switch (id) {
case SceneId::LOGO:
return std::make_unique<LogoScene>(placeholder_text_);
case SceneId::TITLE:
return std::make_unique<TitleScene>(menu_title_text_, placeholder_text_, title_bg_surface_, mini_logo_surface_);
case SceneId::INTRO: {
const auto& p = std::get<IntroParams>(params);
return std::make_unique<IntroScene>(placeholder_text_, p.starting_players);
}
case SceneId::GAME: {
const auto& p = std::get<GameParams>(params);
auto cfg = GameScene::defaultConfig();
cfg.starting_players = p.starting_players;
return std::make_unique<GameScene>(std::move(cfg));
}
}
return nullptr;
}
auto Director::iterate() -> SDL_AppResult {
// Ocultació automàtica del cursor: observem els events abans que els
// altres steps els consumeixin; Mouse no els modifica, només reacciona
+20
View File
@@ -6,7 +6,11 @@
#include <memory>
#include <string>
#include "game/scenes/scene_flow.hpp"
class Scene;
class Surface;
class Text;
class Director {
public:
@@ -20,9 +24,25 @@ class Director {
std::unique_ptr<Scene> current_scene_;
std::uint64_t last_ticks_ms_{0};
// Recursos compartits per la fabricació d'escenes. S'instancien una
// vegada al ctor (parsejant fnt's, retenint shared_ptrs del
// Resource::Cache) i `buildScene` els passa pel valor a cada nova
// escena. No són `static`/Meyers perquè depenen de l'inici d'SDL i
// de Resource::Cache.
std::shared_ptr<Text> placeholder_text_;
std::shared_ptr<Text> menu_title_text_;
std::shared_ptr<Surface> title_bg_surface_;
std::shared_ptr<Surface> mini_logo_surface_;
void createSystemFolder();
void processEvents();
static void processGamepadEvents();
static void processMenuInput();
void switchSceneIfRequested();
// Fabrica una escena nova a partir d'un identificador i els seus
// paràmetres. És l'únic punt del codi que coneix com construir cada
// tipus concret d'escena, així les escenes només declaren `ExitRequest`
// i no s'enllacen entre si.
[[nodiscard]] auto buildScene(SceneId id, const SceneParams& params) -> std::unique_ptr<Scene>;
};
+2
View File
@@ -107,6 +107,7 @@ UiConfig::UiConfig()
title_fade_out_ms_(Defaults::UI::Title::FADE_OUT_MS),
title_fade_out_max_block_(Defaults::UI::Title::FADE_OUT_MAX_BLOCK),
title_music_fade_out_ms_(Defaults::UI::Title::MUSIC_FADE_OUT_MS),
title_attract_fade_to_black_ms_(Defaults::UI::Title::ATTRACT_FADE_TO_BLACK_MS),
title_heat_haze_{
.enabled = Defaults::UI::Title::HeatHaze::ENABLED,
.amplitude_px = Defaults::UI::Title::HeatHaze::AMPLITUDE_PX,
@@ -236,6 +237,7 @@ void UiConfig::load(const std::string& file_path) {
readField(t, "fade_out_ms", title_fade_out_ms_);
readField(t, "fade_out_max_block", title_fade_out_max_block_);
readField(t, "music_fade_out_ms", title_music_fade_out_ms_);
readField(t, "attract_fade_to_black_ms", title_attract_fade_to_black_ms_);
if (t.contains("heat_haze")) {
const auto& hh = t["heat_haze"];
readField(hh, "enabled", title_heat_haze_.enabled);
+2
View File
@@ -100,6 +100,7 @@ class UiConfig {
[[nodiscard]] auto titleFadeOutMs() const -> int { return title_fade_out_ms_; }
[[nodiscard]] auto titleFadeOutMaxBlock() const -> int { return title_fade_out_max_block_; }
[[nodiscard]] auto titleMusicFadeOutMs() const -> int { return title_music_fade_out_ms_; }
[[nodiscard]] auto titleAttractFadeToBlackMs() const -> int { return title_attract_fade_to_black_ms_; }
[[nodiscard]] auto titleHeatHaze() const -> const TitleHeatHaze& { return title_heat_haze_; }
// Corbes d'easing triades al YAML. Si el nom no és conegut, s'usa
@@ -178,6 +179,7 @@ class UiConfig {
int title_fade_out_ms_;
int title_fade_out_max_block_;
int title_music_fade_out_ms_;
int title_attract_fade_to_black_ms_;
TitleHeatHaze title_heat_haze_;
std::string menu_select_sound_;