diff --git a/data/locale/ca.yaml b/data/locale/ca.yaml index 21e1767..3202ff7 100644 --- a/data/locale/ca.yaml +++ b/data/locale/ca.yaml @@ -27,6 +27,9 @@ hud: title: press_start: "PREMEU START PER JUGAR" +demo: + banner: "MODE DEMO - PREMEU START" + game_screen: game_over: "FI DEL JOC" continue: "CONTINUAR" diff --git a/data/locale/en.yaml b/data/locale/en.yaml index 111c936..9be2fdb 100644 --- a/data/locale/en.yaml +++ b/data/locale/en.yaml @@ -26,6 +26,9 @@ hud: title: press_start: "PRESS START TO PLAY" +demo: + banner: "DEMO MODE - PRESS START" + game_screen: game_over: "GAME OVER" continue: "CONTINUE" diff --git a/source/core/defaults/game.hpp b/source/core/defaults/game.hpp index 55ed3dc..76463dd 100644 --- a/source/core/defaults/game.hpp +++ b/source/core/defaults/game.hpp @@ -39,6 +39,19 @@ namespace Defaults::Game { constexpr float LEVEL_COMPLETED_DURATION = 3.0F; // Duración total constexpr float LEVEL_COMPLETED_TYPING_RATIO = 0.05F; // ~150ms de typewriter (escan ràpid però visible) + // Attract mode: transició TÍTOL → DEMO. Primer un "dive" de càmera cap al + // punt de fuga (deixa enrere títol/naus/logo) i després una cortinilla negra + // que cau per tapar; a la demo, la cortinilla segueix caient i destapa. + namespace Dive { + constexpr float DURATION = 0.55F; // Durada del dive (s), amb acceleració (ease-in) + constexpr float CAMERA_DISTANCE = 450.0F; // Avanç de la càmera en +Z (passa les naus, a Z≈323) + constexpr float ZOOM_MAX = 7.0F; // Zoom final dels elements 2D (logo + peu) en travessar-los + } // namespace Dive + namespace Curtain { + constexpr float COVER_DURATION = 0.35F; // TÍTOL: la tela cau i tapa + constexpr float REVEAL_DURATION = 0.45F; // DEMO: la tela segueix caient i destapa + } // namespace Curtain + // Transición INIT_HUD (animación inicial del HUD) constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado diff --git a/source/core/graphics/curtain.cpp b/source/core/graphics/curtain.cpp new file mode 100644 index 0000000..843f93e --- /dev/null +++ b/source/core/graphics/curtain.cpp @@ -0,0 +1,71 @@ +// curtain.cpp - Implementació de la cortinilla negra +// © 2026 JailDesigner + +#include "core/graphics/curtain.hpp" + +#include + +#include "core/defaults/game.hpp" +#include "core/math/easing.hpp" + +namespace Graphics { + + namespace { + constexpr float SCREEN_H = static_cast(Defaults::Game::HEIGHT); + constexpr float SCREEN_W = static_cast(Defaults::Game::WIDTH); + } // namespace + + Curtain::Curtain(Rendering::Renderer* renderer) + : renderer_(renderer) {} + + void Curtain::cover(float duration) { + // Caire superior de -H (fora, a dalt) fins a 0 (tela tapant tota la pantalla). + from_ = -SCREEN_H; + to_ = 0.0F; + duration_ = duration; + elapsed_ = 0.0F; + active_ = true; + } + + void Curtain::reveal(float duration) { + // Caire superior de 0 (tapant) fins a +H (tela fora per baix). + from_ = 0.0F; + to_ = SCREEN_H; + duration_ = duration; + elapsed_ = 0.0F; + active_ = true; + } + + void Curtain::update(float delta_time) { + if (!active_) { + return; + } + elapsed_ += delta_time; + } + + auto Curtain::topY() const -> float { + if (duration_ <= 0.0F) { + return to_; + } + const float T = std::clamp(elapsed_ / duration_, 0.0F, 1.0F); + // Ease-in: la tela "cau" accelerant, com per gravetat. + return Easing::lerp(from_, to_, Easing::easeInQuad(T)); + } + + auto Curtain::isDone() const -> bool { + return !active_ || elapsed_ >= duration_; + } + + void Curtain::draw() const { + if (!active_) { + return; + } + const float TOP = topY(); + // Si la tela ja ha sortit completament per baix, no hi ha res a pintar. + if (TOP >= SCREEN_H) { + return; + } + renderer_->pushRect(0.0F, TOP, SCREEN_W, SCREEN_H, 0.0F, 0.0F, 0.0F, 1.0F); + } + +} // namespace Graphics diff --git a/source/core/graphics/curtain.hpp b/source/core/graphics/curtain.hpp new file mode 100644 index 0000000..1e91e1e --- /dev/null +++ b/source/core/graphics/curtain.hpp @@ -0,0 +1,48 @@ +// curtain.hpp - Cortinilla negra per a transicions d'escena +// © 2026 JailDesigner +// +// Tela negra a pantalla completa que es mou SEMPRE cap avall: +// - cover(): cau des de dalt fins a tapar-ho tot (queda negre). +// - reveal(): segueix caient i surt per baix, deixant veure l'escena. +// Una escena la posseeix, l'actualitza cada frame i la dibuixa l'ÚLTIM (per +// damunt de tot). En repòs (no arrencada o reveal acabada) el draw() és no-op. + +#pragma once + +#include "core/rendering/render_context.hpp" + +namespace Graphics { + + class Curtain { + public: + explicit Curtain(Rendering::Renderer* renderer); + + // Tela que cau des de dalt fins a tapar tota la pantalla en 'duration' s. + void cover(float duration); + + // Tela que segueix caient i surt per baix (destapa) en 'duration' s. + void reveal(float duration); + + // Avança el temporitzador intern. + void update(float delta_time); + + // Dibuixa la tela negra a la seva posició vertical actual. No-op si no + // queda res visible. + void draw() const; + + // Cert quan el moviment actual ha acabat (o no s'ha arrencat mai). + [[nodiscard]] auto isDone() const -> bool; + + private: + // Posició actual del caire superior de la tela (píxels lògics). + [[nodiscard]] auto topY() const -> float; + + Rendering::Renderer* renderer_; + float from_{0.0F}; + float to_{0.0F}; + float duration_{0.0F}; + float elapsed_{0.0F}; + bool active_{false}; + }; + +} // namespace Graphics diff --git a/source/core/graphics/playfield.cpp b/source/core/graphics/playfield.cpp index 2c87e60..78b8763 100644 --- a/source/core/graphics/playfield.cpp +++ b/source/core/graphics/playfield.cpp @@ -62,6 +62,12 @@ namespace Graphics { buildLines(); } + void Playfield::completeBuild() { + // Avançar el rellotge intern més enllà de tota la finestra d'spawn + el + // creixement de l'última línia: computeLineProgress() retorna 1.0 per a totes. + elapsed_s_ = Defaults::Playfield::TOTAL_ANIMATION_DURATION_S; + } + void Playfield::update(float delta_time) { elapsed_s_ += delta_time; for (auto& ripple : ripples_) { diff --git a/source/core/graphics/playfield.hpp b/source/core/graphics/playfield.hpp index ba0216b..4194fda 100644 --- a/source/core/graphics/playfield.hpp +++ b/source/core/graphics/playfield.hpp @@ -30,6 +30,11 @@ namespace Graphics { // Avança timers interns (creació + ripples). void update(float delta_time); + // Completa instantàniament l'animació de creació de la graella (totes les + // línies al 100%). Útil per a la demo (attract), que arrenca amb la + // partida "ja començada" i no ha de mostrar el muntatge del fons. + void completeBuild(); + // Pinta la graella. La porció dibuixada de cada línia depèn del timer intern, // i s'aplica deformació radial per cada ripple activa que afecti la línia. void draw() const; diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 7e2b40f..173ec8e 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -55,7 +55,8 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) text_(sdl.getRenderer()), starfield_parallax_(sdl.getRenderer()), playfield_(sdl.getRenderer()), - border_(sdl.getRenderer()) { + border_(sdl.getRenderer()), + curtain_(sdl.getRenderer()) { // Recuperar configuración de match des del context match_config_ = context_.getMatchConfig(); @@ -164,6 +165,15 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) context_.advanceDemoScenario(); stage_manager_->initDemo(SC.stage); demo_timer_ = DEMO_DURATION; + // Silenciar els SFX durant la demo (la música segueix). Guardem l'estat + // previ per restaurar-lo al destructor sense xafar la preferència de l'usuari. + sound_was_enabled_ = Audio::get()->isSoundEnabled(); + Audio::get()->enableSound(false); + // El fons (graella) ha d'aparèixer ja muntat: la demo és una partida en marxa. + playfield_.completeBuild(); + // La cortinilla arrenca tapant i cau per destapar la demo (continua el + // moviment iniciat al títol, que va acabar amb la pantalla negra). + curtain_.reveal(Defaults::Game::Curtain::REVEAL_DURATION); } else { stage_manager_->init(); } @@ -231,6 +241,13 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) init_hud_rect_sound_played_ = false; } +GameScene::~GameScene() { + // Si la demo havia silenciat els SFX, restaurar l'estat previ en sortir. + if (match_config_.mode == GameConfig::Mode::DEMO) { + Audio::get()->enableSound(sound_was_enabled_); + } +} + auto GameScene::isFinished() const -> bool { return context_.nextScene() != SceneType::GAME; } @@ -350,6 +367,8 @@ void GameScene::updateShipsControl(float delta_time) { } auto GameScene::stepDemo(float delta_time) -> bool { + curtain_.update(delta_time); // cortinilla que destapa la demo + // Qualsevol input trenca la demo i torna al títol (música intacta). if (Input::get()->checkAnyPlayerAction(DEMO_EXIT_ACTIONS)) { context_.setNextScene(SceneType::TITLE, Option::JUMP_TO_TITLE_MAIN); @@ -695,6 +714,10 @@ void GameScene::draw() { drawLevelCompletedState(); break; } + + // Cortinilla d'entrada de la demo: per damunt de tot. No-op fora del mode + // DEMO (curtain_ mai s'arrenca) i quan ja ha sortit per baix. + curtain_.draw(); } void GameScene::drawEnemies() const { @@ -904,6 +927,12 @@ void GameScene::drawScoreboard() { .x = scoreboard_zone.w / 2.0F, .y = scoreboard_zone.y + (scoreboard_zone.h / 2.0F), }; + // En mode demo (attract) el marcador no té sentit: substituïm puntuacions i + // vides per un rètol que indica que és una demo i convida a jugar. + if (match_config_.mode == GameConfig::Mode::DEMO) { + text_.renderCentered(Locale::get().text("demo.banner"), CENTER, SCALE, SPACING); + return; + } Systems::InitHud::drawScoreboardSegmentsAt(text_, buildScoreboardSegments(), CENTER, SCALE, SPACING); } diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index e2d2cc4..717b186 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -9,6 +9,7 @@ #include #include "core/graphics/border.hpp" +#include "core/graphics/curtain.hpp" #include "core/graphics/playfield.hpp" #include "core/graphics/starfield_parallax.hpp" #include "core/graphics/vector_text.hpp" @@ -43,7 +44,7 @@ enum class GameOverState : uint8_t { class GameScene final : public Scene { public: explicit GameScene(SDLManager& sdl, SceneManager::SceneContext& context); - ~GameScene() override = default; + ~GameScene() override; // Restaura l'estat dels SFX si la demo els ha silenciat // Scene interface void handleEvent(const SDL_Event& event) override; @@ -95,6 +96,9 @@ class GameScene final : public Scene { // Border del playfield (4 línies amb desplaçaments i flash per impactes) Graphics::Border border_; + // Cortinilla que destapa en entrar a la demo (només mode DEMO; inactiva en partida normal). + Graphics::Curtain curtain_; + // [NEW] Stage system std::unique_ptr stage_config_; std::unique_ptr stage_manager_; @@ -106,6 +110,7 @@ class GameScene final : public Scene { std::array demo_pilots_; std::array demo_ctrls_{}; // Control per nau al frame actual float demo_timer_{0.0F}; // Temps restant de la demo (→ LOGO) + bool sound_was_enabled_{true}; // Estat dels SFX abans de la demo (per restaurar-lo) // Funciones privades // bullet_velocity: velocitat de la bala que ha causat la mort (Vec2{} si no diff --git a/source/game/scenes/logo_scene.cpp b/source/game/scenes/logo_scene.cpp index ea1d787..55640ab 100644 --- a/source/game/scenes/logo_scene.cpp +++ b/source/game/scenes/logo_scene.cpp @@ -55,6 +55,11 @@ LogoScene::LogoScene(SDLManager& sdl, SceneContext& context) (void)option; // Suprimir warning sound_played_.fill(false); // Inicialitzar seguiment de sons + + // Si ja sona música (venim de la demo del cicle d'atracció), operar en + // silenci: ni sons de lletres ni reinici de title.ogg. + attract_silent_ = (Audio::getMusicState() == Audio::MusicState::PLAYING); + initLetters(); } @@ -171,7 +176,10 @@ void LogoScene::changeState(AnimationState nou_estat) { std::mt19937 g(rd()); std::shuffle(explosion_order_.begin(), explosion_order_.end(), g); } else if (nou_estat == AnimationState::POST_EXPLOSION) { - Audio::get()->playMusic("title.ogg"); + // En el cicle d'atracció la música ja sona; no la reiniciem. + if (!attract_silent_) { + Audio::get()->playMusic("title.ogg"); + } } std::cout << "[LogoScene] Canvi a state: " << static_cast(nou_estat) @@ -237,9 +245,13 @@ void LogoScene::update(float delta_time) { global_progress, LETTER_THRESHOLD); - // Reproduir so cuando la letter comença a aparèixer (progress > 0) + // Reproduir so cuando la letter comença a aparèixer (progress > 0). + // En mode silenciós (cicle d'atracció) saltem el so però igualment + // marquem la letter per no acumular pendents. if (letter_progress > 0.0F) { - Audio::get()->playSound(Defaults::Sound::LOGO, Audio::Group::GAME); + if (!attract_silent_) { + Audio::get()->playSound(Defaults::Sound::LOGO, Audio::Group::GAME); + } sound_played_[i] = true; } } diff --git a/source/game/scenes/logo_scene.hpp b/source/game/scenes/logo_scene.hpp index ca75643..aae6236 100644 --- a/source/game/scenes/logo_scene.hpp +++ b/source/game/scenes/logo_scene.hpp @@ -67,6 +67,11 @@ class LogoScene final : public Scene { // Seguiment de sons de lletres (evitar reproduccions repetides) std::array sound_played_; // Track si cada letter ya ha reproduit el so + // Cicle d'atracció: si en entrar al logo ja hi ha música sonant (venim de la + // demo amb title.ogg en marxa), el logo no ha d'emetre sons ni reiniciar la + // música — només repintar-se en silenci. + bool attract_silent_{false}; + // Constants de animación static constexpr float DURATION_PRE = 1.5F; // Duració PRE_ANIMATION (pantalla negra) static constexpr float DURATION_ZOOM = 4.0F; // Duració del zoom (segons) diff --git a/source/game/scenes/title_scene.cpp b/source/game/scenes/title_scene.cpp index d8a1968..25e292f 100644 --- a/source/game/scenes/title_scene.cpp +++ b/source/game/scenes/title_scene.cpp @@ -37,7 +37,8 @@ namespace { TitleScene::TitleScene(SDLManager& sdl, SceneContext& context) : sdl_(sdl), context_(context), - text_(sdl.getRenderer()) { + text_(sdl.getRenderer()), + curtain_(sdl.getRenderer()) { std::cout << "SceneType Titol: Inicialitzant...\n"; match_config_.player1_active = false; @@ -276,8 +277,9 @@ void TitleScene::dibuixarPeuTitol(float spacing) const { // a 1.0 (a la mida i posició finals, "lluny" al VP). const float SCREEN_CENTRE_X = Defaults::Game::WIDTH / 2.0F; const float SCREEN_CENTRE_Y = Defaults::Game::HEIGHT / 2.0F; - const float JAILGAMES_S = std::lerp(S::FOOTER_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_jailgames_progress_)); - const float COPYRIGHT_S = std::lerp(S::FOOTER_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_copyright_progress_)); + // dive_zoom_ (attract) afegeix el zoom del dive per travessar el peu. + const float JAILGAMES_S = dive_zoom_ * std::lerp(S::FOOTER_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_jailgames_progress_)); + const float COPYRIGHT_S = dive_zoom_ * std::lerp(S::FOOTER_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_copyright_progress_)); const float JAILGAMES_RENDER_SCALE = Defaults::Title::Layout::JAILGAMES_SCALE * JAILGAMES_S; for (const auto& letter : letters_jailgames_) { @@ -333,6 +335,12 @@ void TitleScene::update(float delta_time) { case TitleState::BLACK_SCREEN: updateBlackScreenState(delta_time); break; + case TitleState::DEMO_DIVE: + updateDemoDiveState(delta_time); + break; + case TitleState::DEMO_CURTAIN: + updateDemoCurtainState(delta_time); + break; } // Les animacions segueixen pero els inputs es bloquegen mentre el menu @@ -368,7 +376,15 @@ void TitleScene::update(float delta_time) { demo_cfg.player2_active = (SC.players >= 2); demo_cfg.mode = GameConfig::Mode::DEMO; context_.setMatchConfig(demo_cfg); - context_.setNextScene(SceneType::GAME); + // No saltem en sec: primer un "dive" de càmera cap al punt de fuga + // (deixa enrere títol/naus/logo) i després la cortinilla. L'estat + // deixa de ser MAIN, així que ni es re-dispara la demo ni s'accepta + // START durant la transició. Amaguem ja el "PREMEU START". + press_start_visible_ = false; + current_state_ = TitleState::DEMO_DIVE; + dive_time_ = 0.0F; + dive_zoom_ = 1.0F; + temps_acumulat_ = 0.0F; } } } @@ -473,6 +489,38 @@ void TitleScene::updateBlackScreenState(float delta_time) { } } +void TitleScene::updateDemoDiveState(float delta_time) { + namespace D = Defaults::Game::Dive; + dive_time_ += delta_time; + const float T = std::min(dive_time_ / D::DURATION, 1.0F); + const float EASED = Easing::easeInQuad(T); // acceleració cap al punt de fuga + + // Càmera 3D real cap a +Z: starfield i naus es projecten amb la càmera, així + // que les estrelles es rasguen i les naus creixen i s'escapen pels costats. + // IMPORTANT: cal moure posició I target alhora; si només es mou la posició, + // forward = (target - position) s'inverteix en passar el target i la càmera + // gira cap enrere (tot apareix invertit i mirant a l'espectador). + if (camera_ != nullptr) { + const float Z = EASED * D::CAMERA_DISTANCE; + camera_->setPosition(Vec3{.x = 0.0F, .y = 0.0F, .z = Z}); + camera_->setTarget(Vec3{.x = 0.0F, .y = 0.0F, .z = Z + 1.0F}); + } + // Logo i peu són 2D: els fakegem el dive amb un zoom des del centre. + dive_zoom_ = std::lerp(1.0F, D::ZOOM_MAX, EASED); + + if (T >= 1.0F) { + current_state_ = TitleState::DEMO_CURTAIN; + curtain_.cover(Defaults::Game::Curtain::COVER_DURATION); + } +} + +void TitleScene::updateDemoCurtainState(float delta_time) { + curtain_.update(delta_time); + if (curtain_.isDone()) { + context_.setNextScene(SceneType::GAME); + } +} + void TitleScene::handleSkipInput() { if (current_state_ != TitleState::STARFIELD_FADE_IN && current_state_ != TitleState::STARFIELD) { return; @@ -577,7 +625,9 @@ void TitleScene::draw() { (current_state_ == TitleState::STARFIELD_FADE_IN || current_state_ == TitleState::STARFIELD || current_state_ == TitleState::MAIN || - current_state_ == TitleState::PLAYER_JOIN_PHASE)) { + current_state_ == TitleState::PLAYER_JOIN_PHASE || + current_state_ == TitleState::DEMO_DIVE || + current_state_ == TitleState::DEMO_CURTAIN)) { ship_animator_->draw(); } drawFlashes(); @@ -586,14 +636,21 @@ void TitleScene::draw() { return; } - if (current_state_ != TitleState::MAIN && current_state_ != TitleState::PLAYER_JOIN_PHASE) { + // DEMO_DIVE/DEMO_CURTAIN es pinten com MAIN (logo + peu) perquè el títol + // segueixi visible sota el dive i la cortinilla. + if (current_state_ != TitleState::MAIN && + current_state_ != TitleState::PLAYER_JOIN_PHASE && + current_state_ != TitleState::DEMO_DIVE && + current_state_ != TitleState::DEMO_CURTAIN) { + curtain_.draw(); // BLACK_SCREEN i altres: només la cortinilla (si activa) return; } // Factor d'escala+posició per simular un moviment 3D des de l'usuari (prop, // sprite gran i posició projectada extrema) cap al VP (lluny, sprite a la // seva mida i posició finals). Pivot: centre de pantalla (= projecció VP). - const float LOGO_S = std::lerp(Defaults::Title::Sequence::LOGO_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_logo_progress_)); + // El dive (attract) hi afegeix un zoom extra (dive_zoom_) per travessar-los. + const float LOGO_S = dive_zoom_ * std::lerp(Defaults::Title::Sequence::LOGO_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_logo_progress_)); const float SCREEN_CENTRE_X = Defaults::Game::WIDTH / 2.0F; const float SCREEN_CENTRE_Y = Defaults::Game::HEIGHT / 2.0F; const float LOGO_RENDER_SCALE = Defaults::Title::Layout::LOGO_SCALE * LOGO_S; @@ -657,6 +714,9 @@ void TitleScene::draw() { } dibuixarPeuTitol(SPACING); + + // Cortinilla negra (attract): per damunt de tot. No-op si no està activa. + curtain_.draw(); } auto TitleScene::checkSkipButtonPressed() -> bool { diff --git a/source/game/scenes/title_scene.hpp b/source/game/scenes/title_scene.hpp index 9dafdb8..28f9c74 100644 --- a/source/game/scenes/title_scene.hpp +++ b/source/game/scenes/title_scene.hpp @@ -18,6 +18,7 @@ #include #include "core/graphics/camera3d.hpp" +#include "core/graphics/curtain.hpp" #include "core/graphics/shape.hpp" #include "core/graphics/starfield.hpp" #include "core/graphics/vector_text.hpp" @@ -46,6 +47,8 @@ class TitleScene final : public Scene { MAIN, PLAYER_JOIN_PHASE, BLACK_SCREEN, + DEMO_DIVE, // Attract: dive de càmera cap al punt de fuga + DEMO_CURTAIN, // Attract: cortinilla negra que cau i tapa abans del salt }; struct LogoLetter { @@ -60,6 +63,7 @@ class TitleScene final : public Scene { SceneManager::SceneContext& context_; GameConfig::MatchConfig match_config_; Graphics::VectorText text_; + Graphics::Curtain curtain_; // Cortinilla negra en saltar a la demo (attract) std::unique_ptr camera_; std::unique_ptr starfield_; std::unique_ptr ship_animator_; @@ -81,6 +85,11 @@ class TitleScene final : public Scene { float temps_acumulat_{0.0F}; float idle_timer_{0.0F}; // Attract mode: inactivitat acumulada al state MAIN + // Dive (attract): temps transcorregut i zoom aplicat als elements 2D + // (logo + peu) mentre la càmera avança cap al punt de fuga. + float dive_time_{0.0F}; + float dive_zoom_{1.0F}; + std::vector letters_orni_; std::vector letters_attack_; float dynamic_attack_y_{0.0F}; @@ -143,6 +152,8 @@ class TitleScene final : public Scene { void updateMainState(float delta_time); void updatePlayerJoinPhaseState(float delta_time); void updateBlackScreenState(float delta_time); + void updateDemoDiveState(float delta_time); + void updateDemoCurtainState(float delta_time); void handleSkipInput(); void handleStartInput(); void triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, const char* log_prefix);