refactor(scenes): flux centralitzat amb SceneId+ExitRequest; TitleScene attract → LOGO amb fade-to-black
This commit is contained in:
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
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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_;
|
||||
|
||||
Reference in new issue
Block a user