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
@@ -94,6 +94,7 @@ set(APP_SOURCES
|
|||||||
source/core/rendering/sprite/animated_sprite_parser.cpp
|
source/core/rendering/sprite/animated_sprite_parser.cpp
|
||||||
source/core/rendering/effects/heat_haze.cpp
|
source/core/rendering/effects/heat_haze.cpp
|
||||||
source/core/rendering/transitions/mosaic.cpp
|
source/core/rendering/transitions/mosaic.cpp
|
||||||
|
source/core/rendering/transitions/fade_to_black.cpp
|
||||||
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
|
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
|
||||||
source/core/rendering/sdl3gpu/viewport.cpp
|
source/core/rendering/sdl3gpu/viewport.cpp
|
||||||
source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp
|
source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp
|
||||||
|
|||||||
@@ -135,6 +135,10 @@ title:
|
|||||||
# Fade-out de `title.ogg` en entrar a STARTING. Independent del visual:
|
# Fade-out de `title.ogg` en entrar a STARTING. Independent del visual:
|
||||||
# la música pot acabar abans o després del mosaic dissolve.
|
# la música pot acabar abans o després del mosaic dissolve.
|
||||||
music_fade_out_ms: 3000
|
music_fade_out_ms: 3000
|
||||||
|
# Fade-to-black per a la sortida d'attract (timeout cap a LOGO). NO és
|
||||||
|
# mosaic — capa negra lineal damunt del canvas. La música no es toca:
|
||||||
|
# `title.ogg` continua sonant cap a LOGO, que ja veurà què en fa.
|
||||||
|
attract_fade_to_black_ms: 800
|
||||||
# Distorsió sinusoidal del fons (efecte de calor del desert).
|
# Distorsió sinusoidal del fons (efecte de calor del desert).
|
||||||
# `amplitude_px`: desplaçament màxim de cada fila en píxels.
|
# `amplitude_px`: desplaçament màxim de cada fila en píxels.
|
||||||
# `wavelength_px`: distància vertical entre dos pics de l'ona.
|
# `wavelength_px`: distància vertical entre dos pics de l'ona.
|
||||||
|
|||||||
@@ -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
|
#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 {
|
class Scene {
|
||||||
public:
|
public:
|
||||||
virtual ~Scene() = default;
|
virtual ~Scene() = default;
|
||||||
@@ -10,9 +16,9 @@ class Scene {
|
|||||||
virtual void render() = 0;
|
virtual void render() = 0;
|
||||||
|
|
||||||
[[nodiscard]] auto wantsQuit() const -> bool { return wants_quit_; }
|
[[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:
|
protected:
|
||||||
bool wants_quit_{false};
|
bool wants_quit_{false};
|
||||||
std::unique_ptr<Scene> next_scene_;
|
std::any exit_request_;
|
||||||
};
|
};
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <any>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
@@ -10,6 +11,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
#include "core/audio/audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
#include "core/audio/sound_effects_config.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_font_surface = Resource::Cache::get().getSurface(PLACEHOLDER_FONT_GIF_KEY);
|
||||||
auto placeholder_text = std::make_shared<Text>(placeholder_font_surface, UiConfig::get().placeholderFont().fnt);
|
auto placeholder_text = std::make_shared<Text>(placeholder_font_surface, UiConfig::get().placeholderFont().fnt);
|
||||||
placeholder_text->setDefaultKerning(UiConfig::get().placeholderFont().kerning);
|
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`
|
// 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
|
// 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_title_text = load_menu_font(UiConfig::get().menuTitleFont());
|
||||||
auto menu_items_text = load_menu_font(UiConfig::get().menuItemsFont());
|
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
|
// 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
|
// 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";
|
constexpr const char* MINI_LOGO_KEY = "logo_jailgames_mini.gif";
|
||||||
Resource::Cache::get().loadSurface(MINI_LOGO_KEY, "data/logo/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);
|
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
|
// Fons sencer de la TITLE (320×240). Igual que el minilogo, conserva
|
||||||
// la seua paleta nadiua perquè el GIF ja porta els colors finals.
|
// la seua paleta nadiua perquè el GIF ja porta els colors finals.
|
||||||
constexpr const char* TITLE_BG_KEY = "title_bg.gif";
|
constexpr const char* TITLE_BG_KEY = "title_bg.gif";
|
||||||
Resource::Cache::get().loadSurface(TITLE_BG_KEY, "data/title/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);
|
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);
|
ServiceMenu::get().init(menu_title_text, menu_items_text, ui_palette);
|
||||||
|
|
||||||
// Overlay hook: Screen::present() cridarà aquesta lambda al principi,
|
// Overlay hook: Screen::present() cridarà aquesta lambda al principi,
|
||||||
@@ -258,35 +264,18 @@ Director::Director() {
|
|||||||
std::cout << "Director: start_scene=" << DebugConfig::toString(START)
|
std::cout << "Director: start_scene=" << DebugConfig::toString(START)
|
||||||
<< " starting_players=" << DebugConfig::toString(STARTING_PLAYERS) << '\n';
|
<< " 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) {
|
switch (START) {
|
||||||
case DebugConfig::StartScene::LOGO:
|
case DebugConfig::StartScene::LOGO:
|
||||||
current_scene_ = std::make_unique<LogoScene>(placeholder_text, make_title);
|
current_scene_ = buildScene(SceneId::LOGO, LogoParams{});
|
||||||
break;
|
break;
|
||||||
case DebugConfig::StartScene::TITLE:
|
case DebugConfig::StartScene::TITLE:
|
||||||
current_scene_ = make_title();
|
current_scene_ = buildScene(SceneId::TITLE, TitleParams{});
|
||||||
break;
|
break;
|
||||||
case DebugConfig::StartScene::INTRO:
|
case DebugConfig::StartScene::INTRO:
|
||||||
current_scene_ = make_intro(STARTING_PLAYERS);
|
current_scene_ = buildScene(SceneId::INTRO, IntroParams{.starting_players = STARTING_PLAYERS});
|
||||||
break;
|
break;
|
||||||
case DebugConfig::StartScene::GAME:
|
case DebugConfig::StartScene::GAME:
|
||||||
current_scene_ = make_game(STARTING_PLAYERS);
|
current_scene_ = buildScene(SceneId::GAME, GameParams{.starting_players = STARTING_PLAYERS});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
last_ticks_ms_ = SDL_GetTicks();
|
last_ticks_ms_ = SDL_GetTicks();
|
||||||
@@ -404,13 +393,46 @@ void Director::processMenuInput() {
|
|||||||
|
|
||||||
void Director::switchSceneIfRequested() {
|
void Director::switchSceneIfRequested() {
|
||||||
if (!current_scene_) { return; }
|
if (!current_scene_) { return; }
|
||||||
if (auto next = current_scene_->nextScene()) {
|
// L'escena ha publicat un `ExitRequest` dins de `exit_request_`.
|
||||||
// TODO: migrar a logger unificat (INC-03 a docs/audit-core-rendering.md).
|
// Director el desempaqueta i construeix la nova escena via
|
||||||
std::cout << "Director: scene transition\n";
|
// `buildScene`. Si la `std::any_cast` falla (tipus inesperat), el
|
||||||
current_scene_ = std::move(next);
|
// 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 {
|
auto Director::iterate() -> SDL_AppResult {
|
||||||
// Ocultació automàtica del cursor: observem els events abans que els
|
// Ocultació automàtica del cursor: observem els events abans que els
|
||||||
// altres steps els consumeixin; Mouse no els modifica, només reacciona
|
// altres steps els consumeixin; Mouse no els modifica, només reacciona
|
||||||
|
|||||||
@@ -6,7 +6,11 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "game/scenes/scene_flow.hpp"
|
||||||
|
|
||||||
class Scene;
|
class Scene;
|
||||||
|
class Surface;
|
||||||
|
class Text;
|
||||||
|
|
||||||
class Director {
|
class Director {
|
||||||
public:
|
public:
|
||||||
@@ -20,9 +24,25 @@ class Director {
|
|||||||
std::unique_ptr<Scene> current_scene_;
|
std::unique_ptr<Scene> current_scene_;
|
||||||
std::uint64_t last_ticks_ms_{0};
|
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 createSystemFolder();
|
||||||
void processEvents();
|
void processEvents();
|
||||||
static void processGamepadEvents();
|
static void processGamepadEvents();
|
||||||
static void processMenuInput();
|
static void processMenuInput();
|
||||||
void switchSceneIfRequested();
|
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_ms_(Defaults::UI::Title::FADE_OUT_MS),
|
||||||
title_fade_out_max_block_(Defaults::UI::Title::FADE_OUT_MAX_BLOCK),
|
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_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_{
|
title_heat_haze_{
|
||||||
.enabled = Defaults::UI::Title::HeatHaze::ENABLED,
|
.enabled = Defaults::UI::Title::HeatHaze::ENABLED,
|
||||||
.amplitude_px = Defaults::UI::Title::HeatHaze::AMPLITUDE_PX,
|
.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_ms", title_fade_out_ms_);
|
||||||
readField(t, "fade_out_max_block", title_fade_out_max_block_);
|
readField(t, "fade_out_max_block", title_fade_out_max_block_);
|
||||||
readField(t, "music_fade_out_ms", title_music_fade_out_ms_);
|
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")) {
|
if (t.contains("heat_haze")) {
|
||||||
const auto& hh = t["heat_haze"];
|
const auto& hh = t["heat_haze"];
|
||||||
readField(hh, "enabled", title_heat_haze_.enabled);
|
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 titleFadeOutMs() const -> int { return title_fade_out_ms_; }
|
||||||
[[nodiscard]] auto titleFadeOutMaxBlock() const -> int { return title_fade_out_max_block_; }
|
[[nodiscard]] auto titleFadeOutMaxBlock() const -> int { return title_fade_out_max_block_; }
|
||||||
[[nodiscard]] auto titleMusicFadeOutMs() const -> int { return title_music_fade_out_ms_; }
|
[[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_; }
|
[[nodiscard]] auto titleHeatHaze() const -> const TitleHeatHaze& { return title_heat_haze_; }
|
||||||
|
|
||||||
// Corbes d'easing triades al YAML. Si el nom no és conegut, s'usa
|
// 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_ms_;
|
||||||
int title_fade_out_max_block_;
|
int title_fade_out_max_block_;
|
||||||
int title_music_fade_out_ms_;
|
int title_music_fade_out_ms_;
|
||||||
|
int title_attract_fade_to_black_ms_;
|
||||||
TitleHeatHaze title_heat_haze_;
|
TitleHeatHaze title_heat_haze_;
|
||||||
|
|
||||||
std::string menu_select_sound_;
|
std::string menu_select_sound_;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "core/ui/ui_config.hpp"
|
#include "core/ui/ui_config.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
#include "game/scenes/placeholder_input.hpp"
|
#include "game/scenes/placeholder_input.hpp"
|
||||||
|
#include "game/scenes/scene_flow.hpp"
|
||||||
#include "utils/defaults.hpp"
|
#include "utils/defaults.hpp"
|
||||||
#include "utils/defines.hpp"
|
#include "utils/defines.hpp"
|
||||||
|
|
||||||
@@ -30,10 +31,9 @@ namespace {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
IntroScene::IntroScene(std::shared_ptr<Text> font, DebugConfig::StartingPlayers starting_players, SceneFactory next_factory)
|
IntroScene::IntroScene(std::shared_ptr<Text> font, DebugConfig::StartingPlayers starting_players)
|
||||||
: font_(std::move(font)),
|
: font_(std::move(font)),
|
||||||
starting_players_(starting_players),
|
starting_players_(starting_players) {
|
||||||
next_factory_(std::move(next_factory)) {
|
|
||||||
std::cout << "[scene] " << SCENE_NAME << " start (players=" << DebugConfig::toString(starting_players_) << ")\n";
|
std::cout << "[scene] " << SCENE_NAME << " start (players=" << DebugConfig::toString(starting_players_) << ")\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,8 +52,11 @@ void IntroScene::update(int delta_ms) {
|
|||||||
return ev.type == SDL_EVENT_KEY_DOWN && !Placeholder::isReservedScancode(ev.key.scancode);
|
return ev.type == SDL_EVENT_KEY_DOWN && !Placeholder::isReservedScancode(ev.key.scancode);
|
||||||
});
|
});
|
||||||
|
|
||||||
if ((timer_ms_ <= 0 || SKIPPED) && next_factory_) {
|
if (timer_ms_ <= 0 || SKIPPED) {
|
||||||
next_scene_ = next_factory_(starting_players_);
|
exit_request_ = ExitRequest{
|
||||||
|
.next = SceneId::GAME,
|
||||||
|
.params = GameParams{.starting_players = starting_players_},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Notifier::get().update(delta_ms);
|
Notifier::get().update(delta_ms);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "core/scene/scene.hpp"
|
#include "core/scene/scene.hpp"
|
||||||
@@ -9,19 +8,17 @@
|
|||||||
class Text;
|
class Text;
|
||||||
|
|
||||||
// Escena placeholder de la introducció. Com la resta de placeholders, durada
|
// Escena placeholder de la introducció. Com la resta de placeholders, durada
|
||||||
// fixa i skip amb tecla no reservada; quan expira encadena a la factory
|
// fixa i skip amb tecla no reservada; quan expira publica un `ExitRequest`
|
||||||
// injectada (en producció: GameScene).
|
// cap a GAME amb el `starting_players` rebut de TitleScene.
|
||||||
//
|
//
|
||||||
// `starting_players` és el resultat triat a la TitleScene (P1, P2 o BOTH).
|
// `starting_players` és el resultat triat a la TitleScene (P1, P2 o BOTH).
|
||||||
// L'IntroScene només el guarda i el reenvia a la seva factory; quan demà
|
// L'IntroScene només el guarda i el reenvia per `ExitRequest`; quan demà
|
||||||
// l'intro tinga contingut real podrà variar segons qui jugue.
|
// l'intro tinga contingut real podrà variar segons qui jugue.
|
||||||
class IntroScene : public Scene {
|
class IntroScene : public Scene {
|
||||||
public:
|
public:
|
||||||
using SceneFactory = std::function<std::unique_ptr<Scene>(DebugConfig::StartingPlayers)>;
|
|
||||||
|
|
||||||
static constexpr int DURATION_MS = 5000;
|
static constexpr int DURATION_MS = 5000;
|
||||||
|
|
||||||
IntroScene(std::shared_ptr<Text> font, DebugConfig::StartingPlayers starting_players, SceneFactory next_factory);
|
IntroScene(std::shared_ptr<Text> font, DebugConfig::StartingPlayers starting_players);
|
||||||
~IntroScene() override;
|
~IntroScene() override;
|
||||||
|
|
||||||
void update(int delta_ms) override;
|
void update(int delta_ms) override;
|
||||||
@@ -30,6 +27,5 @@ class IntroScene : public Scene {
|
|||||||
private:
|
private:
|
||||||
std::shared_ptr<Text> font_;
|
std::shared_ptr<Text> font_;
|
||||||
DebugConfig::StartingPlayers starting_players_;
|
DebugConfig::StartingPlayers starting_players_;
|
||||||
SceneFactory next_factory_;
|
|
||||||
int timer_ms_{DURATION_MS};
|
int timer_ms_{DURATION_MS};
|
||||||
};
|
};
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "core/ui/ui_config.hpp"
|
#include "core/ui/ui_config.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
#include "game/scenes/placeholder_input.hpp"
|
#include "game/scenes/placeholder_input.hpp"
|
||||||
|
#include "game/scenes/scene_flow.hpp"
|
||||||
#include "utils/defaults.hpp"
|
#include "utils/defaults.hpp"
|
||||||
#include "utils/defines.hpp"
|
#include "utils/defines.hpp"
|
||||||
|
|
||||||
@@ -31,9 +32,8 @@ namespace {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
LogoScene::LogoScene(std::shared_ptr<Text> font, SceneFactory next_factory)
|
LogoScene::LogoScene(std::shared_ptr<Text> font)
|
||||||
: font_(std::move(font)),
|
: font_(std::move(font)) {
|
||||||
next_factory_(std::move(next_factory)) {
|
|
||||||
std::cout << "[scene] " << SCENE_NAME << " start\n";
|
std::cout << "[scene] " << SCENE_NAME << " start\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,8 +52,8 @@ void LogoScene::update(int delta_ms) {
|
|||||||
return ev.type == SDL_EVENT_KEY_DOWN && !Placeholder::isReservedScancode(ev.key.scancode);
|
return ev.type == SDL_EVENT_KEY_DOWN && !Placeholder::isReservedScancode(ev.key.scancode);
|
||||||
});
|
});
|
||||||
|
|
||||||
if ((timer_ms_ <= 0 || SKIPPED) && next_factory_) {
|
if (timer_ms_ <= 0 || SKIPPED) {
|
||||||
next_scene_ = next_factory_();
|
exit_request_ = ExitRequest{.next = SceneId::TITLE, .params = TitleParams{}};
|
||||||
}
|
}
|
||||||
|
|
||||||
Notifier::get().update(delta_ms);
|
Notifier::get().update(delta_ms);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "core/scene/scene.hpp"
|
#include "core/scene/scene.hpp"
|
||||||
@@ -8,16 +7,14 @@
|
|||||||
class Text;
|
class Text;
|
||||||
|
|
||||||
// Escena placeholder del logotip. Durada fixa (5 s) o skip amb qualsevol tecla
|
// Escena placeholder del logotip. Durada fixa (5 s) o skip amb qualsevol tecla
|
||||||
// no reservada. Quan expira, encadena amb la `SceneFactory` que se li injecta
|
// no reservada. Quan expira, publica un `ExitRequest` cap a TITLE; Director
|
||||||
// al ctor — així l'escena no coneix el tipus concret de la següent, i es pot
|
// és qui construeix la nova escena (l'escena no coneix els altres tipus i
|
||||||
// exercir en tests sense arrossegar GameScene.
|
// es pot exercir en tests sense arrossegar dependències).
|
||||||
class LogoScene : public Scene {
|
class LogoScene : public Scene {
|
||||||
public:
|
public:
|
||||||
using SceneFactory = std::function<std::unique_ptr<Scene>()>;
|
|
||||||
|
|
||||||
static constexpr int DURATION_MS = 5000;
|
static constexpr int DURATION_MS = 5000;
|
||||||
|
|
||||||
LogoScene(std::shared_ptr<Text> font, SceneFactory next_factory);
|
explicit LogoScene(std::shared_ptr<Text> font);
|
||||||
~LogoScene() override;
|
~LogoScene() override;
|
||||||
|
|
||||||
void update(int delta_ms) override;
|
void update(int delta_ms) override;
|
||||||
@@ -25,6 +22,5 @@ class LogoScene : public Scene {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
std::shared_ptr<Text> font_;
|
std::shared_ptr<Text> font_;
|
||||||
SceneFactory next_factory_;
|
|
||||||
int timer_ms_{DURATION_MS};
|
int timer_ms_{DURATION_MS};
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
#include "game/debug_config.hpp"
|
||||||
|
|
||||||
|
// Sistema centralitzat de transicions entre escenes. Cada escena, en lloc
|
||||||
|
// de construir directament la següent, declara una `ExitRequest` amb un
|
||||||
|
// identificador i els paràmetres que la nova escena necessite per
|
||||||
|
// arrencar; `Director::buildScene` és l'únic punt que sap com instanciar
|
||||||
|
// cada tipus i té tots els recursos a mà.
|
||||||
|
//
|
||||||
|
// Afegir una escena nova:
|
||||||
|
// 1. Una entrada al `enum SceneId`.
|
||||||
|
// 2. Si necessita paràmetres, un struct `*Params` i afegir-lo al variant.
|
||||||
|
// 3. Un cas a `Director::buildScene` que el construïsca.
|
||||||
|
//
|
||||||
|
// Una escena només pot demanar transicionar a un altre `SceneId` que
|
||||||
|
// haja estat enregistrat al variant — el compilador ja descarta noms
|
||||||
|
// erronis.
|
||||||
|
enum class SceneId : std::uint8_t {
|
||||||
|
LOGO,
|
||||||
|
TITLE,
|
||||||
|
INTRO,
|
||||||
|
GAME,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Estructures de paràmetres per escena. Buides quan la nova escena no
|
||||||
|
// rep cap dada de l'anterior; en cas contrari, només els camps que
|
||||||
|
// fluixen pel canal de transició (els recursos, defaults i config
|
||||||
|
// interna els resol Director quan construeix).
|
||||||
|
struct LogoParams {};
|
||||||
|
struct TitleParams {};
|
||||||
|
struct IntroParams {
|
||||||
|
DebugConfig::StartingPlayers starting_players{DebugConfig::StartingPlayers::BOTH};
|
||||||
|
};
|
||||||
|
struct GameParams {
|
||||||
|
DebugConfig::StartingPlayers starting_players{DebugConfig::StartingPlayers::BOTH};
|
||||||
|
};
|
||||||
|
|
||||||
|
using SceneParams = std::variant<LogoParams, TitleParams, IntroParams, GameParams>;
|
||||||
|
|
||||||
|
struct ExitRequest {
|
||||||
|
SceneId next;
|
||||||
|
SceneParams params;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper per a logs / debug. Implementat inline per evitar un .cpp per a
|
||||||
|
// una funció de tres línies.
|
||||||
|
[[nodiscard]] inline auto toString(SceneId id) -> const char* {
|
||||||
|
switch (id) {
|
||||||
|
case SceneId::LOGO:
|
||||||
|
return "LOGO";
|
||||||
|
case SceneId::TITLE:
|
||||||
|
return "TITLE";
|
||||||
|
case SceneId::INTRO:
|
||||||
|
return "INTRO";
|
||||||
|
case SceneId::GAME:
|
||||||
|
return "GAME";
|
||||||
|
}
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
#include "core/system/event_buffer.hpp"
|
#include "core/system/event_buffer.hpp"
|
||||||
#include "core/ui/ui_config.hpp"
|
#include "core/ui/ui_config.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
|
#include "game/scenes/scene_flow.hpp"
|
||||||
#include "utils/defaults.hpp"
|
#include "utils/defaults.hpp"
|
||||||
#include "utils/defines.hpp"
|
#include "utils/defines.hpp"
|
||||||
|
|
||||||
@@ -81,12 +82,11 @@ namespace {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TitleScene::TitleScene(std::shared_ptr<Text> display_font, std::shared_ptr<Text> condensed_font, std::shared_ptr<Surface> background, std::shared_ptr<Surface> mini_logo, SceneFactory next_factory)
|
TitleScene::TitleScene(std::shared_ptr<Text> display_font, std::shared_ptr<Text> condensed_font, std::shared_ptr<Surface> background, std::shared_ptr<Surface> mini_logo)
|
||||||
: display_font_(std::move(display_font)),
|
: display_font_(std::move(display_font)),
|
||||||
condensed_font_(std::move(condensed_font)),
|
condensed_font_(std::move(condensed_font)),
|
||||||
background_(std::move(background)),
|
background_(std::move(background)),
|
||||||
mini_logo_(std::move(mini_logo)),
|
mini_logo_(std::move(mini_logo)),
|
||||||
next_factory_(std::move(next_factory)),
|
|
||||||
timer_ms_(UiConfig::get().titleDurationMs()) {
|
timer_ms_(UiConfig::get().titleDurationMs()) {
|
||||||
std::cout << "[scene] " << SCENE_NAME << " start\n";
|
std::cout << "[scene] " << SCENE_NAME << " start\n";
|
||||||
const auto& hh = UiConfig::get().titleHeatHaze();
|
const auto& hh = UiConfig::get().titleHeatHaze();
|
||||||
@@ -100,10 +100,12 @@ TitleScene::TitleScene(std::shared_ptr<Text> display_font, std::shared_ptr<Text>
|
|||||||
fade_out_.emplace(Transitions::Mosaic::Params{
|
fade_out_.emplace(Transitions::Mosaic::Params{
|
||||||
.max_block_size = UiConfig::get().titleFadeOutMaxBlock(),
|
.max_block_size = UiConfig::get().titleFadeOutMaxBlock(),
|
||||||
});
|
});
|
||||||
|
attract_fade_.emplace();
|
||||||
|
|
||||||
// Música de fons en bucle. Es preloda al Director, així que la
|
// Música de fons en bucle. Es preloda al Director, així que la
|
||||||
// primera crida és immediata. El fade-out arrenca en entrar a
|
// primera crida és immediata. El fade-out arrenca en entrar a
|
||||||
// STARTING (vore enterStarting).
|
// STARTING (vore enterStarting); en ATTRACT_OUT NO es toca, la
|
||||||
|
// música segueix sonant cap a LOGO.
|
||||||
if (auto* audio = Audio::get(); audio != nullptr) {
|
if (auto* audio = Audio::get(); audio != nullptr) {
|
||||||
audio->playMusic("title.ogg", -1);
|
audio->playMusic("title.ogg", -1);
|
||||||
}
|
}
|
||||||
@@ -131,53 +133,15 @@ void TitleScene::update(int delta_ms) {
|
|||||||
const bool P2_START = pressedStart(1);
|
const bool P2_START = pressedStart(1);
|
||||||
|
|
||||||
switch (phase_) {
|
switch (phase_) {
|
||||||
case Phase::IDLE: {
|
case Phase::IDLE:
|
||||||
timer_ms_ -= delta_ms;
|
tickIdle(delta_ms, P1_START, P2_START);
|
||||||
if (P1_START) {
|
|
||||||
playStartSound();
|
|
||||||
enterStarting(DebugConfig::StartingPlayers::P1);
|
|
||||||
} else if (P2_START) {
|
|
||||||
playStartSound();
|
|
||||||
enterStarting(DebugConfig::StartingPlayers::P2);
|
|
||||||
} else if (timer_ms_ <= 0) {
|
|
||||||
// Attract: ningú ha premut. Entrem a STARTING amb BOTH
|
|
||||||
// perquè el fade-out de transició també hi compti.
|
|
||||||
// Demà, quan tinguem una DemoScene, ací anirà la
|
|
||||||
// transició a demo en lloc de a intro.
|
|
||||||
enterStarting(DebugConfig::StartingPlayers::BOTH);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
case Phase::STARTING:
|
||||||
case Phase::STARTING: {
|
tickStarting(delta_ms, P1_START, P2_START);
|
||||||
starting_timer_ms_ -= delta_ms;
|
break;
|
||||||
// Si l'altre jugador prem START dins de la finestra (i el
|
case Phase::ATTRACT_OUT:
|
||||||
// fade encara no ha arrencat), pugem a BOTH i resetegem el
|
tickAttract(delta_ms);
|
||||||
// comptador. Una vegada el fade ja està actiu, ignorem
|
|
||||||
// pulsacions per no saltar visualment la transició.
|
|
||||||
const bool OTHER_PRESSED =
|
|
||||||
(pending_players_ == DebugConfig::StartingPlayers::P1 && P2_START) ||
|
|
||||||
(pending_players_ == DebugConfig::StartingPlayers::P2 && P1_START);
|
|
||||||
const bool FADE_LOCKED = fade_out_ && (fade_out_->isActive() || fade_out_->isFinished());
|
|
||||||
if (OTHER_PRESSED && !FADE_LOCKED) {
|
|
||||||
playStartSound();
|
|
||||||
pending_players_ = DebugConfig::StartingPlayers::BOTH;
|
|
||||||
starting_timer_ms_ = UiConfig::get().titleStartingMs();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arrenca el fade quan al comptador li queden <= fade_out_ms,
|
|
||||||
// i només una vegada (ni en repeticions ni si l'altre jugador
|
|
||||||
// ha resetejat el timer).
|
|
||||||
const int FADE_MS = UiConfig::get().titleFadeOutMs();
|
|
||||||
if (fade_out_ && !fade_out_->isActive() && !fade_out_->isFinished() && starting_timer_ms_ <= FADE_MS && FADE_MS > 0) {
|
|
||||||
fade_out_->begin(FADE_MS);
|
|
||||||
}
|
|
||||||
if (fade_out_ && fade_out_->isActive()) { fade_out_->tick(delta_ms); }
|
|
||||||
|
|
||||||
if (starting_timer_ms_ <= 0) {
|
|
||||||
advanceTo(pending_players_);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Notifier::get().update(delta_ms);
|
Notifier::get().update(delta_ms);
|
||||||
@@ -201,12 +165,85 @@ void TitleScene::enterStarting(DebugConfig::StartingPlayers players) {
|
|||||||
std::cout << "[scene] " << SCENE_NAME << " STARTING (players=" << DebugConfig::toString(players) << ")\n";
|
std::cout << "[scene] " << SCENE_NAME << " STARTING (players=" << DebugConfig::toString(players) << ")\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
void TitleScene::advanceTo(DebugConfig::StartingPlayers players) {
|
void TitleScene::tickIdle(int delta_ms, bool p1_start, bool p2_start) {
|
||||||
if (next_factory_) {
|
timer_ms_ -= delta_ms;
|
||||||
next_scene_ = next_factory_(players);
|
if (p1_start) {
|
||||||
|
playStartSound();
|
||||||
|
enterStarting(DebugConfig::StartingPlayers::P1);
|
||||||
|
} else if (p2_start) {
|
||||||
|
playStartSound();
|
||||||
|
enterStarting(DebugConfig::StartingPlayers::P2);
|
||||||
|
} else if (timer_ms_ <= 0) {
|
||||||
|
// Attract timeout: ningú ha premut. Sortida silenciosa cap a
|
||||||
|
// LOGO amb fade-to-black; música i parpadeig intactes.
|
||||||
|
enterAttract();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TitleScene::tickStarting(int delta_ms, bool p1_start, bool p2_start) {
|
||||||
|
starting_timer_ms_ -= delta_ms;
|
||||||
|
// Si l'altre jugador prem START dins de la finestra (i el fade
|
||||||
|
// encara no ha arrencat), pugem a BOTH i resetegem el comptador.
|
||||||
|
// Una vegada el fade ja està actiu, ignorem pulsacions per no
|
||||||
|
// saltar visualment la transició.
|
||||||
|
const bool OTHER_PRESSED =
|
||||||
|
(pending_players_ == DebugConfig::StartingPlayers::P1 && p2_start) ||
|
||||||
|
(pending_players_ == DebugConfig::StartingPlayers::P2 && p1_start);
|
||||||
|
const bool FADE_LOCKED = fade_out_ && (fade_out_->isActive() || fade_out_->isFinished());
|
||||||
|
if (OTHER_PRESSED && !FADE_LOCKED) {
|
||||||
|
playStartSound();
|
||||||
|
pending_players_ = DebugConfig::StartingPlayers::BOTH;
|
||||||
|
starting_timer_ms_ = UiConfig::get().titleStartingMs();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrenca el fade quan al comptador li queden <= fade_out_ms, i
|
||||||
|
// només una vegada (ni en repeticions ni si l'altre jugador ha
|
||||||
|
// resetejat el timer).
|
||||||
|
const int FADE_MS = UiConfig::get().titleFadeOutMs();
|
||||||
|
if (fade_out_ && !fade_out_->isActive() && !fade_out_->isFinished() && starting_timer_ms_ <= FADE_MS && FADE_MS > 0) {
|
||||||
|
fade_out_->begin(FADE_MS);
|
||||||
|
}
|
||||||
|
if (fade_out_ && fade_out_->isActive()) { fade_out_->tick(delta_ms); }
|
||||||
|
|
||||||
|
if (starting_timer_ms_ <= 0) {
|
||||||
|
advanceToIntro(pending_players_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TitleScene::tickAttract(int delta_ms) {
|
||||||
|
// Sortida d'attract: només avancem el fade-to-black. Quan s'acaba
|
||||||
|
// demanem transició a LOGO. Cap input ni música ni canvi de
|
||||||
|
// freqüència de parpadeig — el text "press start" segueix amb el
|
||||||
|
// cicle d'IDLE per damunt del fade.
|
||||||
|
if (!attract_fade_) { return; }
|
||||||
|
attract_fade_->tick(delta_ms);
|
||||||
|
if (attract_fade_->isFinished()) {
|
||||||
|
advanceToLogo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TitleScene::enterAttract() {
|
||||||
|
phase_ = Phase::ATTRACT_OUT;
|
||||||
|
if (attract_fade_) {
|
||||||
|
attract_fade_->begin(UiConfig::get().titleAttractFadeToBlackMs());
|
||||||
|
}
|
||||||
|
std::cout << "[scene] " << SCENE_NAME << " ATTRACT_OUT\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void TitleScene::advanceToIntro(DebugConfig::StartingPlayers players) {
|
||||||
|
exit_request_ = ExitRequest{
|
||||||
|
.next = SceneId::INTRO,
|
||||||
|
.params = IntroParams{.starting_players = players},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void TitleScene::advanceToLogo() {
|
||||||
|
exit_request_ = ExitRequest{
|
||||||
|
.next = SceneId::LOGO,
|
||||||
|
.params = LogoParams{},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
void TitleScene::render() {
|
void TitleScene::render() {
|
||||||
const auto& ui_config = UiConfig::get();
|
const auto& ui_config = UiConfig::get();
|
||||||
const auto& palette = Resource::Cache::get().getPalette("ui");
|
const auto& palette = Resource::Cache::get().getPalette("ui");
|
||||||
@@ -299,10 +336,14 @@ void TitleScene::render() {
|
|||||||
|
|
||||||
Notifier::get().render();
|
Notifier::get().render();
|
||||||
|
|
||||||
// El fade-out cobreix l'escena i el Notifier (forma part de la
|
// Capa de transició final segons la fase. Sempre cobreix l'escena
|
||||||
// transició), però NO el RenderInfo (overlay de debug, sempre
|
// i el Notifier, mai el RenderInfo (overlay de debug, sempre
|
||||||
// visible).
|
// visible). STARTING usa mosaic dissolve; ATTRACT_OUT, fade-to-black
|
||||||
if (fade_out_ && (fade_out_->isActive() || fade_out_->isFinished())) {
|
// lineal (sense efecte chunky perquè la sortida silenciosa s'integre
|
||||||
|
// amb la pista visual del LOGO).
|
||||||
|
if (phase_ == Phase::ATTRACT_OUT && attract_fade_) {
|
||||||
|
attract_fade_->render();
|
||||||
|
} else if (fade_out_ && (fade_out_->isActive() || fade_out_->isFinished())) {
|
||||||
fade_out_->render();
|
fade_out_->render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <functional>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
#include "core/rendering/effects/heat_haze.hpp"
|
#include "core/rendering/effects/heat_haze.hpp"
|
||||||
|
#include "core/rendering/transitions/fade_to_black.hpp"
|
||||||
#include "core/rendering/transitions/mosaic.hpp"
|
#include "core/rendering/transitions/mosaic.hpp"
|
||||||
#include "core/scene/scene.hpp"
|
#include "core/scene/scene.hpp"
|
||||||
#include "game/debug_config.hpp"
|
#include "game/debug_config.hpp"
|
||||||
@@ -19,33 +19,40 @@ class Text;
|
|||||||
// data/ui.yaml secció `title:` per poder afinar-la sense recompilar.
|
// data/ui.yaml secció `title:` per poder afinar-la sense recompilar.
|
||||||
class TitleScene : public Scene {
|
class TitleScene : public Scene {
|
||||||
public:
|
public:
|
||||||
using SceneFactory = std::function<std::unique_ptr<Scene>(DebugConfig::StartingPlayers)>;
|
|
||||||
|
|
||||||
// Fases de l'escena.
|
// Fases de l'escena.
|
||||||
// - IDLE: animació atràctil. Cap tecla salta; només P1 START o
|
// - IDLE: animació atràctil. Només P1 START o P2 START hi fan
|
||||||
// P2 START fan transició. El timeout (`title.duration_ms`)
|
// transició; un timeout `title.duration_ms` porta a ATTRACT_OUT.
|
||||||
// salta a la següent escena amb BOTH (modus attract).
|
|
||||||
// - STARTING: comptador fix entre la primera pulsació de START i
|
// - STARTING: comptador fix entre la primera pulsació de START i
|
||||||
// la transició. Si l'altre jugador prem START dins d'aquesta
|
// la transició cap a INTRO. Si l'altre jugador prem START dins
|
||||||
// finestra, el comptador es reseteja amb `pending_players_=BOTH`.
|
// d'aquesta finestra, el comptador es reseteja amb
|
||||||
|
// `pending_players_=BOTH`. Visual: mosaic dissolve + fade-out
|
||||||
|
// de música.
|
||||||
|
// - ATTRACT_OUT: fase de sortida silenciosa cap a LOGO quan
|
||||||
|
// expira el timeout sense pulsacions. Visual: fade-to-black net.
|
||||||
|
// NO toca la música ni canvia la freqüència del parpadeig.
|
||||||
enum class Phase : std::uint8_t { IDLE,
|
enum class Phase : std::uint8_t { IDLE,
|
||||||
STARTING };
|
STARTING,
|
||||||
|
ATTRACT_OUT };
|
||||||
|
|
||||||
TitleScene(std::shared_ptr<Text> display_font, std::shared_ptr<Text> condensed_font, std::shared_ptr<Surface> background, std::shared_ptr<Surface> mini_logo, SceneFactory next_factory);
|
TitleScene(std::shared_ptr<Text> display_font, std::shared_ptr<Text> condensed_font, std::shared_ptr<Surface> background, std::shared_ptr<Surface> mini_logo);
|
||||||
~TitleScene() override;
|
~TitleScene() override;
|
||||||
|
|
||||||
void update(int delta_ms) override;
|
void update(int delta_ms) override;
|
||||||
void render() override;
|
void render() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void tickIdle(int delta_ms, bool p1_start, bool p2_start);
|
||||||
|
void tickStarting(int delta_ms, bool p1_start, bool p2_start);
|
||||||
|
void tickAttract(int delta_ms);
|
||||||
void enterStarting(DebugConfig::StartingPlayers players);
|
void enterStarting(DebugConfig::StartingPlayers players);
|
||||||
void advanceTo(DebugConfig::StartingPlayers players);
|
void enterAttract();
|
||||||
|
void advanceToIntro(DebugConfig::StartingPlayers players);
|
||||||
|
void advanceToLogo();
|
||||||
|
|
||||||
std::shared_ptr<Text> display_font_;
|
std::shared_ptr<Text> display_font_;
|
||||||
std::shared_ptr<Text> condensed_font_;
|
std::shared_ptr<Text> condensed_font_;
|
||||||
std::shared_ptr<Surface> background_;
|
std::shared_ptr<Surface> background_;
|
||||||
std::shared_ptr<Surface> mini_logo_;
|
std::shared_ptr<Surface> mini_logo_;
|
||||||
SceneFactory next_factory_;
|
|
||||||
int timer_ms_;
|
int timer_ms_;
|
||||||
int blink_elapsed_ms_{0};
|
int blink_elapsed_ms_{0};
|
||||||
std::optional<Effects::HeatHaze> heat_haze_;
|
std::optional<Effects::HeatHaze> heat_haze_;
|
||||||
@@ -53,4 +60,5 @@ class TitleScene : public Scene {
|
|||||||
int starting_timer_ms_{0};
|
int starting_timer_ms_{0};
|
||||||
DebugConfig::StartingPlayers pending_players_{DebugConfig::StartingPlayers::BOTH};
|
DebugConfig::StartingPlayers pending_players_{DebugConfig::StartingPlayers::BOTH};
|
||||||
std::optional<Transitions::Mosaic> fade_out_;
|
std::optional<Transitions::Mosaic> fade_out_;
|
||||||
|
std::optional<Transitions::FadeToBlack> attract_fade_;
|
||||||
};
|
};
|
||||||
@@ -287,6 +287,13 @@ namespace Defaults {
|
|||||||
// (la música pot acabar abans/després del mosaic).
|
// (la música pot acabar abans/després del mosaic).
|
||||||
constexpr int MUSIC_FADE_OUT_MS = 2000;
|
constexpr int MUSIC_FADE_OUT_MS = 2000;
|
||||||
|
|
||||||
|
// Fade-to-black per a la sortida d'attract (timeout). És una
|
||||||
|
// capa negra lineal sobre el canvas — sense mosaic — perquè
|
||||||
|
// l'eixida silenciosa cap a LOGO siga neta. NO toca la música:
|
||||||
|
// el title.ogg continua sonant durant la transició; LOGO ja
|
||||||
|
// veurà què en fa.
|
||||||
|
constexpr int ATTRACT_FADE_TO_BLACK_MS = 800;
|
||||||
|
|
||||||
// Distorsió sinusoidal horitzontal de l'imatge de fons
|
// Distorsió sinusoidal horitzontal de l'imatge de fons
|
||||||
// (efecte de calor del desert). `amplitude_px` és el
|
// (efecte de calor del desert). `amplitude_px` és el
|
||||||
// desplaçament màxim per fila; `wavelength_px` la distància
|
// desplaçament màxim per fila; `wavelength_px` la distància
|
||||||
|
|||||||
Reference in new issue
Block a user