Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea5762265e | ||
|
|
78732042cb | ||
|
|
f10671d9c0 |
No files matched your search
@@ -50,6 +50,8 @@ namespace Effects {
|
|||||||
burst_remaining_ms_ = 0;
|
burst_remaining_ms_ = 0;
|
||||||
burst_y_norm_ = 0.0F;
|
burst_y_norm_ = 0.0F;
|
||||||
burst_dx_ = 0;
|
burst_dx_ = 0;
|
||||||
|
tail_started_ = false;
|
||||||
|
tail_remaining_ms_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScanReveal::tick(int delta_ms) {
|
void ScanReveal::tick(int delta_ms) {
|
||||||
@@ -63,9 +65,13 @@ namespace Effects {
|
|||||||
burst_remaining_ms_ = std::max(0, burst_remaining_ms_ - delta_ms);
|
burst_remaining_ms_ = std::max(0, burst_remaining_ms_ - delta_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Spawn d'un nou burst (només durant el scan, no al flash).
|
// 3) Spawn d'un nou burst. Permès durant el scan i durant el
|
||||||
|
// glitch tail post-flash; bloquejat exactament durant el
|
||||||
|
// flash perquè aquell és un punt visual de tancament net.
|
||||||
// Probabilitat per frame ≈ (per_sec/1000) · delta_ms.
|
// Probabilitat per frame ≈ (per_sec/1000) · delta_ms.
|
||||||
if (burst_remaining_ms_ == 0 && elapsed_ms_ < params_.duration_ms && params_.vhs_burst_per_sec > 0 && params_.vhs_burst_duration_ms > 0) {
|
const bool TAIL_ACTIVE = tail_started_ && tail_remaining_ms_ > 0;
|
||||||
|
const bool SCAN_ONGOING = elapsed_ms_ < params_.duration_ms;
|
||||||
|
if (burst_remaining_ms_ == 0 && (SCAN_ONGOING || TAIL_ACTIVE) && params_.vhs_burst_per_sec > 0 && params_.vhs_burst_duration_ms > 0) {
|
||||||
const float P = (static_cast<float>(params_.vhs_burst_per_sec) / 1000.0F) * static_cast<float>(delta_ms);
|
const float P = (static_cast<float>(params_.vhs_burst_per_sec) / 1000.0F) * static_cast<float>(delta_ms);
|
||||||
std::uniform_real_distribution<float> dist01(0.0F, 1.0F);
|
std::uniform_real_distribution<float> dist01(0.0F, 1.0F);
|
||||||
if (dist01(rng_) < P) {
|
if (dist01(rng_) < P) {
|
||||||
@@ -86,6 +92,17 @@ namespace Effects {
|
|||||||
if (flash_remaining_ms_ > 0) {
|
if (flash_remaining_ms_ > 0) {
|
||||||
flash_remaining_ms_ = std::max(0, flash_remaining_ms_ - delta_ms);
|
flash_remaining_ms_ = std::max(0, flash_remaining_ms_ - delta_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5) Glitch tail: arranca un sol cop quan el scan ha acabat i el
|
||||||
|
// flash s'ha esgotat. Si no hi havia flash configurat, també
|
||||||
|
// arranca tot just acabar el scan.
|
||||||
|
if (!tail_started_ && elapsed_ms_ >= params_.duration_ms && flash_remaining_ms_ <= 0 && params_.tail_glitch_ms > 0) {
|
||||||
|
tail_started_ = true;
|
||||||
|
tail_remaining_ms_ = params_.tail_glitch_ms;
|
||||||
|
}
|
||||||
|
if (tail_remaining_ms_ > 0) {
|
||||||
|
tail_remaining_ms_ = std::max(0, tail_remaining_ms_ - delta_ms);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ScanReveal::isActive() const -> bool {
|
auto ScanReveal::isActive() const -> bool {
|
||||||
@@ -94,8 +111,12 @@ namespace Effects {
|
|||||||
|
|
||||||
auto ScanReveal::isFinished() const -> bool {
|
auto ScanReveal::isFinished() const -> bool {
|
||||||
if (params_.duration_ms <= 0) { return true; }
|
if (params_.duration_ms <= 0) { return true; }
|
||||||
// Acabat = scan al final + flash exhaurit (o sense flash).
|
// Acabat = scan al final + flash exhaurit + glitch tail
|
||||||
return elapsed_ms_ >= params_.duration_ms && flash_remaining_ms_ <= 0;
|
// exhaurit (o sense tail configurat).
|
||||||
|
const bool SCAN_DONE = elapsed_ms_ >= params_.duration_ms;
|
||||||
|
const bool FLASH_DONE = flash_remaining_ms_ <= 0;
|
||||||
|
const bool TAIL_DONE = (params_.tail_glitch_ms <= 0) || (tail_started_ && tail_remaining_ms_ <= 0);
|
||||||
|
return SCAN_DONE && FLASH_DONE && TAIL_DONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScanReveal::render(const Surface& src, int x, int y) const {
|
void ScanReveal::render(const Surface& src, int x, int y) const {
|
||||||
@@ -118,15 +139,21 @@ namespace Effects {
|
|||||||
const int TIME_BUCKET = elapsed_ms_ / JITTER_TIME_BUCKET_MS;
|
const int TIME_BUCKET = elapsed_ms_ / JITTER_TIME_BUCKET_MS;
|
||||||
const int BURST_Y_PX = static_cast<int>(burst_y_norm_ * static_cast<float>(H));
|
const int BURST_Y_PX = static_cast<int>(burst_y_norm_ * static_cast<float>(H));
|
||||||
const bool BURST_ACTIVE = burst_remaining_ms_ > 0;
|
const bool BURST_ACTIVE = burst_remaining_ms_ > 0;
|
||||||
|
// Durant el glitch tail post-flash, BAND_TOP queda just damunt
|
||||||
|
// del peu del logo, fet que (amb el codi de jitter que segueix)
|
||||||
|
// generaria jitter residual a les últimes files. Aquí el
|
||||||
|
// suprimim explícitament: el tail només viu de bursts VHS.
|
||||||
|
const bool TAIL_ACTIVE = tail_started_ && tail_remaining_ms_ > 0;
|
||||||
|
|
||||||
// Render fila a fila (només files revelades).
|
// Render fila a fila (només files revelades).
|
||||||
for (int row = 0; row <= BAND_BOT && row < H; ++row) {
|
for (int row = 0; row <= BAND_BOT && row < H; ++row) {
|
||||||
int dx = 0;
|
int dx = 0;
|
||||||
|
|
||||||
// Jitter de banda: amplitud màxima a la banda, falloff
|
// Jitter de banda: amplitud màxima a la banda, falloff
|
||||||
// lineal cap als marges del radi de glitch.
|
// lineal cap als marges del radi de glitch. Inactiu durant
|
||||||
|
// el glitch tail (no hi ha banda).
|
||||||
const int DIST_TO_BAND = std::abs(row - BAND_TOP);
|
const int DIST_TO_BAND = std::abs(row - BAND_TOP);
|
||||||
if (params_.glitch_band_radius_px > 0 && DIST_TO_BAND <= params_.glitch_band_radius_px && params_.glitch_amplitude_px > 0) {
|
if (!TAIL_ACTIVE && params_.glitch_band_radius_px > 0 && DIST_TO_BAND <= params_.glitch_band_radius_px && params_.glitch_amplitude_px > 0) {
|
||||||
const float FALLOFF = 1.0F - (static_cast<float>(DIST_TO_BAND) / static_cast<float>(params_.glitch_band_radius_px));
|
const float FALLOFF = 1.0F - (static_cast<float>(DIST_TO_BAND) / static_cast<float>(params_.glitch_band_radius_px));
|
||||||
const float JIT = rowJitter(row, TIME_BUCKET);
|
const float JIT = rowJitter(row, TIME_BUCKET);
|
||||||
dx += static_cast<int>(std::lround(
|
dx += static_cast<int>(std::lround(
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ namespace Effects {
|
|||||||
// demoscene/VHS deteriorat.
|
// demoscene/VHS deteriorat.
|
||||||
//
|
//
|
||||||
// Quan la banda surt per sota del logo i s'esgota `duration_ms`,
|
// Quan la banda surt per sota del logo i s'esgota `duration_ms`,
|
||||||
// dispara un flash blanc breu i passa a `isFinished()=true`. El
|
// dispara un flash blanc breu. Si `tail_glitch_ms > 0`, després del
|
||||||
// caller decideix què fa amb el control de fase a partir d'aquí.
|
// flash el logo queda totalment visible mentre els bursts VHS
|
||||||
|
// continuen saltant durant aquest temps (glitch tail). Quan el tail
|
||||||
|
// s'exhaureix `isFinished()=true`. El caller decideix què fa amb el
|
||||||
|
// control de fase a partir d'aquí.
|
||||||
//
|
//
|
||||||
// Ús:
|
// Ús:
|
||||||
// ScanReveal sr;
|
// ScanReveal sr;
|
||||||
@@ -43,6 +46,10 @@ namespace Effects {
|
|||||||
int vhs_burst_height_px;
|
int vhs_burst_height_px;
|
||||||
int vhs_burst_amplitude_px;
|
int vhs_burst_amplitude_px;
|
||||||
int flash_ms;
|
int flash_ms;
|
||||||
|
// Glitch tail post-flash: el logo queda visible sencer i els
|
||||||
|
// bursts VHS continuen saltant durant aquest temps. 0 ho
|
||||||
|
// desactiva (efecte acaba al flash).
|
||||||
|
int tail_glitch_ms;
|
||||||
};
|
};
|
||||||
|
|
||||||
ScanReveal();
|
ScanReveal();
|
||||||
@@ -75,6 +82,12 @@ namespace Effects {
|
|||||||
float burst_y_norm_{0.0F};
|
float burst_y_norm_{0.0F};
|
||||||
int burst_dx_{0};
|
int burst_dx_{0};
|
||||||
|
|
||||||
|
// Glitch tail post-flash: arranca quan el flash s'esgota i
|
||||||
|
// permet que els bursts continuen saltant amb el logo ja del tot
|
||||||
|
// visible. `isFinished()` espera al final d'aquest tail.
|
||||||
|
bool tail_started_{false};
|
||||||
|
int tail_remaining_ms_{0};
|
||||||
|
|
||||||
// RNG per als bursts. Seed des de random_device a `begin()`
|
// RNG per als bursts. Seed des de random_device a `begin()`
|
||||||
// perquè cada execució sigui visualment diferent.
|
// perquè cada execució sigui visualment diferent.
|
||||||
std::minstd_rand rng_;
|
std::minstd_rand rng_;
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ UiConfig::UiConfig()
|
|||||||
},
|
},
|
||||||
logo_scan_{
|
logo_scan_{
|
||||||
.duration_ms = Defaults::UI::Logo::Scan::DURATION_MS,
|
.duration_ms = Defaults::UI::Logo::Scan::DURATION_MS,
|
||||||
|
.tail_glitch_ms = Defaults::UI::Logo::Scan::TAIL_GLITCH_MS,
|
||||||
.band_height_px = Defaults::UI::Logo::Scan::BAND_HEIGHT_PX,
|
.band_height_px = Defaults::UI::Logo::Scan::BAND_HEIGHT_PX,
|
||||||
.band_alpha = Defaults::UI::Logo::Scan::BAND_ALPHA,
|
.band_alpha = Defaults::UI::Logo::Scan::BAND_ALPHA,
|
||||||
.band_pulse_hz = Defaults::UI::Logo::Scan::BAND_PULSE_HZ,
|
.band_pulse_hz = Defaults::UI::Logo::Scan::BAND_PULSE_HZ,
|
||||||
@@ -143,6 +144,13 @@ UiConfig::UiConfig()
|
|||||||
.vhs_burst_amplitude_px = Defaults::UI::Logo::Scan::VHS_BURST_AMPLITUDE_PX,
|
.vhs_burst_amplitude_px = Defaults::UI::Logo::Scan::VHS_BURST_AMPLITUDE_PX,
|
||||||
.flash_ms = Defaults::UI::Logo::Scan::FLASH_MS,
|
.flash_ms = Defaults::UI::Logo::Scan::FLASH_MS,
|
||||||
},
|
},
|
||||||
|
logo_timeline_{
|
||||||
|
.black_intro_ms = Defaults::UI::Logo::Timeline::BLACK_INTRO_MS,
|
||||||
|
.hold_silent_ms = Defaults::UI::Logo::Timeline::HOLD_SILENT_MS,
|
||||||
|
.hold_with_music_ms = Defaults::UI::Logo::Timeline::HOLD_WITH_MUSIC_MS,
|
||||||
|
.fade_out_ms = Defaults::UI::Logo::Timeline::FADE_OUT_MS,
|
||||||
|
.black_outro_ms = Defaults::UI::Logo::Timeline::BLACK_OUTRO_MS,
|
||||||
|
},
|
||||||
menu_select_sound_(Defaults::UI::MENU_SELECT_SOUND),
|
menu_select_sound_(Defaults::UI::MENU_SELECT_SOUND),
|
||||||
menu_accept_sound_(Defaults::UI::MENU_ACCEPT_SOUND),
|
menu_accept_sound_(Defaults::UI::MENU_ACCEPT_SOUND),
|
||||||
player_start_sound_(Defaults::UI::PLAYER_START_SOUND),
|
player_start_sound_(Defaults::UI::PLAYER_START_SOUND),
|
||||||
@@ -301,6 +309,7 @@ void UiConfig::load(const std::string& file_path) {
|
|||||||
if (l.contains("scan")) {
|
if (l.contains("scan")) {
|
||||||
const auto& s = l["scan"];
|
const auto& s = l["scan"];
|
||||||
readField(s, "duration_ms", logo_scan_.duration_ms);
|
readField(s, "duration_ms", logo_scan_.duration_ms);
|
||||||
|
readField(s, "tail_glitch_ms", logo_scan_.tail_glitch_ms);
|
||||||
readField(s, "band_height_px", logo_scan_.band_height_px);
|
readField(s, "band_height_px", logo_scan_.band_height_px);
|
||||||
readField(s, "band_alpha", logo_scan_.band_alpha);
|
readField(s, "band_alpha", logo_scan_.band_alpha);
|
||||||
readField(s, "band_pulse_hz", logo_scan_.band_pulse_hz);
|
readField(s, "band_pulse_hz", logo_scan_.band_pulse_hz);
|
||||||
@@ -313,6 +322,14 @@ void UiConfig::load(const std::string& file_path) {
|
|||||||
readField(s, "vhs_burst_amplitude_px", logo_scan_.vhs_burst_amplitude_px);
|
readField(s, "vhs_burst_amplitude_px", logo_scan_.vhs_burst_amplitude_px);
|
||||||
readField(s, "flash_ms", logo_scan_.flash_ms);
|
readField(s, "flash_ms", logo_scan_.flash_ms);
|
||||||
}
|
}
|
||||||
|
if (l.contains("timeline")) {
|
||||||
|
const auto& tl = l["timeline"];
|
||||||
|
readField(tl, "black_intro_ms", logo_timeline_.black_intro_ms);
|
||||||
|
readField(tl, "hold_silent_ms", logo_timeline_.hold_silent_ms);
|
||||||
|
readField(tl, "hold_with_music_ms", logo_timeline_.hold_with_music_ms);
|
||||||
|
readField(tl, "fade_out_ms", logo_timeline_.fade_out_ms);
|
||||||
|
readField(tl, "black_outro_ms", logo_timeline_.black_outro_ms);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (yaml.contains("notifier")) {
|
if (yaml.contains("notifier")) {
|
||||||
|
|||||||
@@ -124,9 +124,12 @@ class UiConfig {
|
|||||||
[[nodiscard]] auto titleHeatHaze() const -> const TitleHeatHaze& { return title_heat_haze_; }
|
[[nodiscard]] auto titleHeatHaze() const -> const TitleHeatHaze& { return title_heat_haze_; }
|
||||||
|
|
||||||
// Escena LOGO — paràmetres del scanline reveal del logo de l'editora.
|
// Escena LOGO — paràmetres del scanline reveal del logo de l'editora.
|
||||||
// Tot s'aplica només a la fase SCAN (substitueix l'antic FADE_IN).
|
// `duration_ms` és la durada de la línia descendent; `tail_glitch_ms`
|
||||||
|
// és el temps post-flash en què el logo queda totalment visible
|
||||||
|
// però els bursts VHS encara hi salten (glitch tail).
|
||||||
struct LogoScan {
|
struct LogoScan {
|
||||||
int duration_ms;
|
int duration_ms;
|
||||||
|
int tail_glitch_ms;
|
||||||
int band_height_px;
|
int band_height_px;
|
||||||
int band_alpha;
|
int band_alpha;
|
||||||
float band_pulse_hz;
|
float band_pulse_hz;
|
||||||
@@ -142,6 +145,19 @@ class UiConfig {
|
|||||||
|
|
||||||
[[nodiscard]] auto logoScan() const -> const LogoScan& { return logo_scan_; }
|
[[nodiscard]] auto logoScan() const -> const LogoScan& { return logo_scan_; }
|
||||||
|
|
||||||
|
// Timeline lineal de la LogoScene. Cada camp és la durada (ms) d'una
|
||||||
|
// fase. La SCAN no apareix aquí perquè la marca el propi
|
||||||
|
// `LogoScan` (duration_ms + tail_glitch_ms + flash_ms).
|
||||||
|
struct LogoTimeline {
|
||||||
|
int black_intro_ms;
|
||||||
|
int hold_silent_ms;
|
||||||
|
int hold_with_music_ms;
|
||||||
|
int fade_out_ms;
|
||||||
|
int black_outro_ms;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] auto logoTimeline() const -> const LogoTimeline& { return logo_timeline_; }
|
||||||
|
|
||||||
// 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
|
||||||
// Easing::linear com a fallback.
|
// Easing::linear com a fallback.
|
||||||
[[nodiscard]] auto pauseEnterCurve() const -> CurveFn { return pause_enter_curve_; }
|
[[nodiscard]] auto pauseEnterCurve() const -> CurveFn { return pause_enter_curve_; }
|
||||||
@@ -229,6 +245,7 @@ class UiConfig {
|
|||||||
TitleIntro title_intro_;
|
TitleIntro title_intro_;
|
||||||
TitleHeatHaze title_heat_haze_;
|
TitleHeatHaze title_heat_haze_;
|
||||||
LogoScan logo_scan_;
|
LogoScan logo_scan_;
|
||||||
|
LogoTimeline logo_timeline_;
|
||||||
|
|
||||||
std::string menu_select_sound_;
|
std::string menu_select_sound_;
|
||||||
std::string menu_accept_sound_;
|
std::string menu_accept_sound_;
|
||||||
|
|||||||
@@ -38,6 +38,21 @@ LogoScene::LogoScene(std::shared_ptr<Surface> logo, bool reset)
|
|||||||
audio->stopAllSounds();
|
audio->stopAllSounds();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Arrenquem en BLACK_INTRO: ni so ni efecte. El glitch sound i el
|
||||||
|
// begin() de scan_ es disparen quan entrem a SCAN.
|
||||||
|
}
|
||||||
|
|
||||||
|
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::enterScan() {
|
||||||
|
phase_ = Phase::SCAN;
|
||||||
|
phase_elapsed_ms_ = 0;
|
||||||
// So del scanline reveal: una sola pulsació al començament de la
|
// So del scanline reveal: una sola pulsació al començament de la
|
||||||
// fase SCAN, en grup INTERFACE perquè no es vegi afectat pel volum
|
// fase SCAN, en grup INTERFACE perquè no es vegi afectat pel volum
|
||||||
// de sons del joc i quedi sempre audible.
|
// de sons del joc i quedi sempre audible.
|
||||||
@@ -60,26 +75,36 @@ LogoScene::LogoScene(std::shared_ptr<Surface> logo, bool reset)
|
|||||||
.vhs_burst_height_px = s.vhs_burst_height_px,
|
.vhs_burst_height_px = s.vhs_burst_height_px,
|
||||||
.vhs_burst_amplitude_px = s.vhs_burst_amplitude_px,
|
.vhs_burst_amplitude_px = s.vhs_burst_amplitude_px,
|
||||||
.flash_ms = s.flash_ms,
|
.flash_ms = s.flash_ms,
|
||||||
|
.tail_glitch_ms = s.tail_glitch_ms,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
LogoScene::~LogoScene() = default;
|
void LogoScene::enterHoldSilent() {
|
||||||
|
phase_ = Phase::HOLD_SILENT;
|
||||||
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;
|
phase_elapsed_ms_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LogoScene::enterHoldMusic() {
|
||||||
|
phase_ = Phase::HOLD_MUSIC;
|
||||||
|
phase_elapsed_ms_ = 0;
|
||||||
|
// Música del títol al començament d'aquesta fase. 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 durant el FADE_OUT i el BLACK_OUTRO.
|
||||||
|
if (auto* audio = Audio::get(); audio != nullptr) {
|
||||||
|
audio->playMusic("title.ogg", -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void LogoScene::enterFadeOut() {
|
void LogoScene::enterFadeOut() {
|
||||||
phase_ = Phase::FADE_OUT;
|
phase_ = Phase::FADE_OUT;
|
||||||
phase_elapsed_ms_ = 0;
|
phase_elapsed_ms_ = 0;
|
||||||
fade_.begin(FADE_OUT_MS, Transitions::FadeToBlack::Direction::FADE_OUT);
|
fade_.begin(UiConfig::get().logoTimeline().fade_out_ms, Transitions::FadeToBlack::Direction::FADE_OUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LogoScene::enterBlackOutro() {
|
||||||
|
phase_ = Phase::BLACK_OUTRO;
|
||||||
|
phase_elapsed_ms_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void LogoScene::enterDone() {
|
void LogoScene::enterDone() {
|
||||||
@@ -103,32 +128,32 @@ void LogoScene::update(int delta_ms) {
|
|||||||
|
|
||||||
if (phase_ != Phase::DONE) {
|
if (phase_ != Phase::DONE) {
|
||||||
phase_elapsed_ms_ += delta_ms;
|
phase_elapsed_ms_ += delta_ms;
|
||||||
// Tick dels efectes actius. `scan_` només fa feina visible
|
// Tick dels efectes actius. Tots dos són no-op si el seu
|
||||||
// durant Phase::SCAN; `fade_` només durant Phase::FADE_OUT. Tots
|
// duration_ms intern està a 0; només cal cridar-los si la fase
|
||||||
// dos són no-op si el seu duration_ms intern està a 0.
|
// corresponent està viva o just acabada.
|
||||||
scan_.tick(delta_ms);
|
scan_.tick(delta_ms);
|
||||||
fade_.tick(delta_ms);
|
fade_.tick(delta_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto& tl = UiConfig::get().logoTimeline();
|
||||||
switch (phase_) {
|
switch (phase_) {
|
||||||
case Phase::SCAN:
|
case Phase::BLACK_INTRO:
|
||||||
if (scan_.isFinished()) { enterMiddle(); }
|
if (phase_elapsed_ms_ >= tl.black_intro_ms) { enterScan(); }
|
||||||
break;
|
break;
|
||||||
case Phase::MIDDLE:
|
case Phase::SCAN:
|
||||||
// Música del títol al cor de la fase central. La crida és
|
if (scan_.isFinished()) { enterHoldSilent(); }
|
||||||
// idempotent (Audio detecta same-track + same-loop i no
|
break;
|
||||||
// reinicia), així que TitleScene podrà fer el seu propi
|
case Phase::HOLD_SILENT:
|
||||||
// playMusic sense interrompre la pista.
|
if (phase_elapsed_ms_ >= tl.hold_silent_ms) { enterHoldMusic(); }
|
||||||
if (!music_started_ && phase_elapsed_ms_ >= MUSIC_TRIGGER_MS) {
|
break;
|
||||||
music_started_ = true;
|
case Phase::HOLD_MUSIC:
|
||||||
if (auto* audio = Audio::get(); audio != nullptr) {
|
if (phase_elapsed_ms_ >= tl.hold_with_music_ms) { enterFadeOut(); }
|
||||||
audio->playMusic("title.ogg", -1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (phase_elapsed_ms_ >= MIDDLE_MS) { enterFadeOut(); }
|
|
||||||
break;
|
break;
|
||||||
case Phase::FADE_OUT:
|
case Phase::FADE_OUT:
|
||||||
if (phase_elapsed_ms_ >= FADE_OUT_MS) { enterDone(); }
|
if (phase_elapsed_ms_ >= tl.fade_out_ms) { enterBlackOutro(); }
|
||||||
|
break;
|
||||||
|
case Phase::BLACK_OUTRO:
|
||||||
|
if (phase_elapsed_ms_ >= tl.black_outro_ms) { enterDone(); }
|
||||||
break;
|
break;
|
||||||
case Phase::DONE:
|
case Phase::DONE:
|
||||||
break;
|
break;
|
||||||
@@ -142,25 +167,28 @@ void LogoScene::render() {
|
|||||||
constexpr std::uint32_t BLACK_ARGB = 0xFF000000U;
|
constexpr std::uint32_t BLACK_ARGB = 0xFF000000U;
|
||||||
Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, BLACK_ARGB);
|
Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, BLACK_ARGB);
|
||||||
|
|
||||||
if (logo_) {
|
// BLACK_INTRO i BLACK_OUTRO no pinten logo: només el negre del
|
||||||
|
// fillRect d'aquí dalt. La resta de fases sí.
|
||||||
|
const bool SHOW_LOGO = phase_ != Phase::BLACK_INTRO && phase_ != Phase::BLACK_OUTRO;
|
||||||
|
if (logo_ && SHOW_LOGO) {
|
||||||
const int X = (GameCanvas::WIDTH - logo_->getWidth()) / 2;
|
const int X = (GameCanvas::WIDTH - logo_->getWidth()) / 2;
|
||||||
const int Y = (GameCanvas::HEIGHT - logo_->getHeight()) / 2;
|
const int Y = (GameCanvas::HEIGHT - logo_->getHeight()) / 2;
|
||||||
if (phase_ == Phase::SCAN) {
|
if (phase_ == Phase::SCAN) {
|
||||||
// Revelat CRT: la pròpia ScanReveal s'encarrega del blit
|
// Revelat CRT + glitch tail: la pròpia ScanReveal s'encarrega
|
||||||
// fila a fila, de la banda lluminosa, dels glitches i del
|
// del blit fila a fila, de la banda lluminosa, dels glitches,
|
||||||
// flash final.
|
// del flash final i dels bursts del tail.
|
||||||
scan_.render(*logo_, X, Y);
|
scan_.render(*logo_, X, Y);
|
||||||
} else {
|
} else {
|
||||||
// MIDDLE i FADE_OUT: render normal, el logo ja està a
|
// HOLD_SILENT, HOLD_MUSIC, FADE_OUT: render normal, el logo
|
||||||
// pantalla complet i estable.
|
// ja està a pantalla complet i estable.
|
||||||
logo_->render(X, Y);
|
logo_->render(X, Y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Notifier::get().render();
|
Notifier::get().render();
|
||||||
// Capa de fosa: només FADE_OUT pinta negre semitransparent (el
|
// Capa de fosa: només pinta durant FADE_OUT (alpha creixent). En
|
||||||
// FADE_IN s'ha substituït per `scan_`). Va per damunt del logo i
|
// BLACK_OUTRO ja deixem la pantalla negra del fillRect, així que no
|
||||||
// del Notifier, però per sota de RenderInfo (overlay de debug).
|
// cal ni que `fade_` segueixi viu.
|
||||||
fade_.render();
|
fade_.render();
|
||||||
RenderInfo::get().render();
|
RenderInfo::get().render();
|
||||||
Screen::get().present();
|
Screen::get().present();
|
||||||
|
|||||||
@@ -9,14 +9,22 @@
|
|||||||
|
|
||||||
class Surface;
|
class Surface;
|
||||||
|
|
||||||
// Escena del logotip de l'editora. FSM amb tres fases visibles:
|
// Escena del logotip de l'editora. Timeline lineal en 7 fases visibles
|
||||||
// `SCAN` — revelat tipus scanline CRT (durada YAML, ~1500 ms)
|
// (durades configurables des de `logo.timeline.*` al YAML, excepte SCAN
|
||||||
// `MIDDLE` — logo estable (`MIDDLE_MS`, 5 s)
|
// que la marca `logo.scan.duration_ms` + `tail_glitch_ms` + `flash_ms`):
|
||||||
// `FADE_OUT` — fade lineal a negre (`FADE_OUT_MS`, 500 ms)
|
//
|
||||||
// I una terminal `DONE` que dispara la transició cap a TITLE. A 2,5 s
|
// `BLACK_INTRO` — negre pur abans d'arrencar res.
|
||||||
// dins de `MIDDLE` arrenca la música del títol perquè entri al cor de
|
// `SCAN` — revelat scanline CRT amb glitch tail. So
|
||||||
// l'escena i continue cap a TitleScene sense talls (Audio ja és
|
// `logoGlitchSound` es dispara just al començar
|
||||||
// idempotent quan es demana la mateixa pista que ja sona).
|
// aquesta fase (no al constructor).
|
||||||
|
// `HOLD_SILENT` — logo estable, en silenci, marge entre el final
|
||||||
|
// del glitch i l'arrencada de la música.
|
||||||
|
// `HOLD_MUSIC` — logo estable amb la música del títol sonant.
|
||||||
|
// `FADE_OUT` — fade lineal a negre.
|
||||||
|
// `BLACK_OUTRO` — negre amb la música encara sonant. TitleScene
|
||||||
|
// continuarà la pista de manera idempotent (Audio
|
||||||
|
// detecta same-track + same-loop i no reinicia).
|
||||||
|
// `DONE` — terminal; dispara la transició cap a TITLE.
|
||||||
//
|
//
|
||||||
// Skip: qualsevol tecla no reservada salta directament a la transició
|
// Skip: qualsevol tecla no reservada salta directament a la transició
|
||||||
// (sense fade-out) — el TitleScene té el seu propi `intro.fade_in_ms`,
|
// (sense fade-out) — el TitleScene té el seu propi `intro.fade_in_ms`,
|
||||||
@@ -27,14 +35,6 @@ class Surface;
|
|||||||
// silenciós encara que veníssem del joc en marxa.
|
// silenciós encara que veníssem del joc en marxa.
|
||||||
class LogoScene : public Scene {
|
class LogoScene : public Scene {
|
||||||
public:
|
public:
|
||||||
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<Surface> logo, bool reset);
|
LogoScene(std::shared_ptr<Surface> logo, bool reset);
|
||||||
~LogoScene() override;
|
~LogoScene() override;
|
||||||
|
|
||||||
@@ -42,22 +42,25 @@ class LogoScene : public Scene {
|
|||||||
void render() override;
|
void render() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class Phase : std::uint8_t { SCAN,
|
enum class Phase : std::uint8_t { BLACK_INTRO,
|
||||||
MIDDLE,
|
SCAN,
|
||||||
|
HOLD_SILENT,
|
||||||
|
HOLD_MUSIC,
|
||||||
FADE_OUT,
|
FADE_OUT,
|
||||||
|
BLACK_OUTRO,
|
||||||
DONE };
|
DONE };
|
||||||
|
|
||||||
void enterMiddle();
|
void enterScan();
|
||||||
|
void enterHoldSilent();
|
||||||
|
void enterHoldMusic();
|
||||||
void enterFadeOut();
|
void enterFadeOut();
|
||||||
|
void enterBlackOutro();
|
||||||
void enterDone();
|
void enterDone();
|
||||||
[[nodiscard]] static auto wantsSkip() -> bool;
|
[[nodiscard]] static auto wantsSkip() -> bool;
|
||||||
|
|
||||||
std::shared_ptr<Surface> logo_;
|
std::shared_ptr<Surface> logo_;
|
||||||
Phase phase_{Phase::SCAN};
|
Phase phase_{Phase::BLACK_INTRO};
|
||||||
int phase_elapsed_ms_{0};
|
int phase_elapsed_ms_{0};
|
||||||
bool music_started_{false};
|
|
||||||
// Capa de fosa per al FADE_OUT cap a TITLE. El FADE_IN inicial s'ha
|
|
||||||
// substituït per `scan_` (revelat CRT amb glitch).
|
|
||||||
Transitions::FadeToBlack fade_;
|
Transitions::FadeToBlack fade_;
|
||||||
Effects::ScanReveal scan_;
|
Effects::ScanReveal scan_;
|
||||||
};
|
};
|
||||||
@@ -360,14 +360,37 @@ namespace Defaults {
|
|||||||
} // namespace HeatHaze
|
} // namespace HeatHaze
|
||||||
} // namespace Title
|
} // namespace Title
|
||||||
|
|
||||||
// Escena LOGO (logotip de l'editora). El revelat del logo a la
|
// Escena LOGO (logotip de l'editora). La timeline és lineal i
|
||||||
// fase SCAN imita un escaneig CRT amb una banda lluminosa que
|
// composta per fases independents amb durades configurables des
|
||||||
// baixa de dalt a baix, glitch al voltant de la banda i bursts
|
// del YAML (`logo.timeline.*`). L'efecte CRT de revelat (scan
|
||||||
// VHS ocasionals damunt la part ja revelada. Quan la banda toca
|
// line + glitch tail) ho gestiona `Effects::ScanReveal` i les
|
||||||
// el peu del logo, un flash blanc breu remata l'efecte i s'entra
|
// seves constants viuen al sub-namespace `Scan` d'aquí baix.
|
||||||
// a MIDDLE (logo estable).
|
namespace Logo::Timeline {
|
||||||
|
// Negre inicial abans que comence el revelat. Permet entrar
|
||||||
|
// a l'escena des d'una transició/fade extern sense talls.
|
||||||
|
constexpr int BLACK_INTRO_MS = 2000;
|
||||||
|
// Silenci amb logo estable abans d'arrencar la música del
|
||||||
|
// títol — espai per "respirar" entre el glitch i la pista.
|
||||||
|
constexpr int HOLD_SILENT_MS = 1000;
|
||||||
|
// Logo estable amb música del títol sonant. Quan acaba,
|
||||||
|
// comença el fade-out.
|
||||||
|
constexpr int HOLD_WITH_MUSIC_MS = 2500;
|
||||||
|
// Fade lineal a negre.
|
||||||
|
constexpr int FADE_OUT_MS = 500;
|
||||||
|
// Negre final amb música encara sonant — la TitleScene
|
||||||
|
// continuarà la pista de manera idempotent.
|
||||||
|
constexpr int BLACK_OUTRO_MS = 1000;
|
||||||
|
} // namespace Logo::Timeline
|
||||||
|
|
||||||
|
// Constants del revelat CRT pròpiament dit. La banda lluminosa
|
||||||
|
// baixa de dalt a baix amb glitch al seu voltant i bursts VHS
|
||||||
|
// ocasionals damunt la part ja revelada. Quan la banda toca el
|
||||||
|
// peu del logo, un flash blanc remata l'efecte; després un
|
||||||
|
// "tail" manté el logo visible amb bursts durant `TAIL_GLITCH_MS`
|
||||||
|
// abans que `isFinished()` retorne true.
|
||||||
namespace Logo::Scan {
|
namespace Logo::Scan {
|
||||||
constexpr int DURATION_MS = 1500;
|
constexpr int DURATION_MS = 1500;
|
||||||
|
constexpr int TAIL_GLITCH_MS = 1500;
|
||||||
constexpr int BAND_HEIGHT_PX = 4;
|
constexpr int BAND_HEIGHT_PX = 4;
|
||||||
constexpr int BAND_ALPHA = 200;
|
constexpr int BAND_ALPHA = 200;
|
||||||
// Pulse de brillantor sinusoidal sobre l'alpha base de la
|
// Pulse de brillantor sinusoidal sobre l'alpha base de la
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace Texts {
|
namespace Texts {
|
||||||
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte ARCADE — JailDesigner";
|
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte ARCADE — JailDesigner";
|
||||||
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
||||||
constexpr const char* VERSION = "0.35.0"; // Single source of truth — Makefile el grep
|
constexpr const char* VERSION = "0.36.0"; // Single source of truth — Makefile el grep
|
||||||
} // namespace Texts
|
} // namespace Texts
|
||||||
|
|
||||||
namespace GameCanvas {
|
namespace GameCanvas {
|
||||||
|
|||||||
Reference in new issue
Block a user