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

+96 -55
View File
@@ -22,6 +22,7 @@
#include "core/system/event_buffer.hpp"
#include "core/ui/ui_config.hpp"
#include "game/options.hpp"
#include "game/scenes/scene_flow.hpp"
#include "utils/defaults.hpp"
#include "utils/defines.hpp"
@@ -81,12 +82,11 @@ 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)),
condensed_font_(std::move(condensed_font)),
background_(std::move(background)),
mini_logo_(std::move(mini_logo)),
next_factory_(std::move(next_factory)),
timer_ms_(UiConfig::get().titleDurationMs()) {
std::cout << "[scene] " << SCENE_NAME << " start\n";
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{
.max_block_size = UiConfig::get().titleFadeOutMaxBlock(),
});
attract_fade_.emplace();
// Música de fons en bucle. Es preloda al Director, així que la
// 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) {
audio->playMusic("title.ogg", -1);
}
@@ -131,53 +133,15 @@ void TitleScene::update(int delta_ms) {
const bool P2_START = pressedStart(1);
switch (phase_) {
case Phase::IDLE: {
timer_ms_ -= delta_ms;
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);
}
case Phase::IDLE:
tickIdle(delta_ms, P1_START, P2_START);
break;
}
case Phase::STARTING: {
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) {
advanceTo(pending_players_);
}
case Phase::STARTING:
tickStarting(delta_ms, P1_START, P2_START);
break;
case Phase::ATTRACT_OUT:
tickAttract(delta_ms);
break;
}
}
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";
}
void TitleScene::advanceTo(DebugConfig::StartingPlayers players) {
if (next_factory_) {
next_scene_ = next_factory_(players);
void TitleScene::tickIdle(int delta_ms, bool p1_start, bool p2_start) {
timer_ms_ -= delta_ms;
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() {
const auto& ui_config = UiConfig::get();
const auto& palette = Resource::Cache::get().getPalette("ui");
@@ -299,10 +336,14 @@ void TitleScene::render() {
Notifier::get().render();
// El fade-out cobreix l'escena i el Notifier (forma part de la
// transició), però NO el RenderInfo (overlay de debug, sempre
// visible).
if (fade_out_ && (fade_out_->isActive() || fade_out_->isFinished())) {
// Capa de transició final segons la fase. Sempre cobreix l'escena
// i el Notifier, mai el RenderInfo (overlay de debug, sempre
// visible). STARTING usa mosaic dissolve; ATTRACT_OUT, fade-to-black
// 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();
}