From b363efd1f0c81f8f29d152fbe34826c45b6234e4 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 29 May 2026 08:54:56 +0200 Subject: [PATCH 1/7] =?UTF-8?q?feat(demo):=20logo=20silenci=C3=B3s=20dins?= =?UTF-8?q?=20el=20cicle=20d'atracci=C3=B3=20(no=20sons=20ni=20reinici=20d?= =?UTF-8?q?e=20m=C3=BAsica=20si=20ja=20sona)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/game/scenes/logo_scene.cpp | 18 +++++++++++++++--- source/game/scenes/logo_scene.hpp | 5 +++++ 2 files changed, 20 insertions(+), 3 deletions(-) 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) From 1e63d3ae9d8a7fa3e40fe5757cd34cdd25b151fd Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 29 May 2026 09:09:37 +0200 Subject: [PATCH 2/7] =?UTF-8?q?feat(demo):=20silenciar=20els=20efectes=20d?= =?UTF-8?q?e=20so=20durant=20la=20demo=20(m=C3=BAsica=20intacta)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/game/scenes/game_scene.cpp | 11 +++++++++++ source/game/scenes/game_scene.hpp | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 7e2b40f..4cf33ea 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -164,6 +164,10 @@ 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); } else { stage_manager_->init(); } @@ -231,6 +235,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; } diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index e2d2cc4..40ac85e 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -43,7 +43,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; @@ -106,6 +106,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 From 4e67a67acede8e1140bfbaf55fdb024be7a295f0 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 29 May 2026 09:12:30 +0200 Subject: [PATCH 3/7] feat(demo): la graella del fons apareix ja muntada en entrar a la demo --- source/core/graphics/playfield.cpp | 6 ++++++ source/core/graphics/playfield.hpp | 5 +++++ source/game/scenes/game_scene.cpp | 2 ++ 3 files changed, 13 insertions(+) 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 4cf33ea..bf20482 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -168,6 +168,8 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) // 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(); } else { stage_manager_->init(); } From 472c543c7ba4ce6e2cec30ea12cdf4cd71b80af4 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 29 May 2026 09:16:55 +0200 Subject: [PATCH 4/7] =?UTF-8?q?feat(demo):=20el=20marcador=20mostra=20el?= =?UTF-8?q?=20r=C3=A8tol=20de=20demo=20en=20lloc=20de=20puntuacions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/locale/ca.yaml | 3 +++ data/locale/en.yaml | 3 +++ source/game/scenes/game_scene.cpp | 6 ++++++ 3 files changed, 12 insertions(+) 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/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index bf20482..28365e5 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -917,6 +917,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); } From 068f42782b8a18af318de1aaebbc3e523aebd9f2 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 29 May 2026 09:21:02 +0200 Subject: [PATCH 5/7] =?UTF-8?q?feat(demo):=20transici=C3=B3=20per=20fosa?= =?UTF-8?q?=20a/desde=20negre=20en=20el=20salt=20t=C3=ADtol=E2=86=92demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/core/defaults/game.hpp | 6 +++ source/core/graphics/screen_fade.cpp | 62 ++++++++++++++++++++++++++++ source/core/graphics/screen_fade.hpp | 43 +++++++++++++++++++ source/game/scenes/game_scene.cpp | 11 ++++- source/game/scenes/game_scene.hpp | 4 ++ source/game/scenes/title_scene.cpp | 33 +++++++++++++-- source/game/scenes/title_scene.hpp | 4 ++ 7 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 source/core/graphics/screen_fade.cpp create mode 100644 source/core/graphics/screen_fade.hpp diff --git a/source/core/defaults/game.hpp b/source/core/defaults/game.hpp index 55ed3dc..63de213 100644 --- a/source/core/defaults/game.hpp +++ b/source/core/defaults/game.hpp @@ -39,6 +39,12 @@ 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: durada de la fosa a/desde negre a les transicions de demo. + namespace Fade { + constexpr float DEMO_OUT_DURATION = 0.6F; // TÍTOL → DEMO (fosa a negre abans del salt) + constexpr float DEMO_IN_DURATION = 0.6F; // DEMO: fosa des de negre sobre el joc ja en marxa + } // namespace Fade + // 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/screen_fade.cpp b/source/core/graphics/screen_fade.cpp new file mode 100644 index 0000000..76ec391 --- /dev/null +++ b/source/core/graphics/screen_fade.cpp @@ -0,0 +1,62 @@ +// screen_fade.cpp - Implementació de la fosa a/desde negre +// © 2026 JailDesigner + +#include "core/graphics/screen_fade.hpp" + +#include +#include + +#include "core/defaults/game.hpp" + +namespace Graphics { + + ScreenFade::ScreenFade(Rendering::Renderer* renderer) + : renderer_(renderer) {} + + void ScreenFade::start(float from, float to, float duration) { + from_ = from; + to_ = to; + duration_ = duration; + elapsed_ = 0.0F; + active_ = true; + } + + void ScreenFade::update(float delta_time) { + if (!active_) { + return; + } + elapsed_ += delta_time; + } + + auto ScreenFade::alpha() const -> float { + if (!active_) { + return 0.0F; + } + if (duration_ <= 0.0F) { + return to_; + } + const float T = std::clamp(elapsed_ / duration_, 0.0F, 1.0F); + return std::lerp(from_, to_, T); + } + + auto ScreenFade::isDone() const -> bool { + return !active_ || elapsed_ >= duration_; + } + + void ScreenFade::draw() const { + const float A = alpha(); + if (A <= 0.0F) { + return; + } + renderer_->pushRect( + 0.0F, + 0.0F, + static_cast(Defaults::Game::WIDTH), + static_cast(Defaults::Game::HEIGHT), + 0.0F, + 0.0F, + 0.0F, + A); + } + +} // namespace Graphics diff --git a/source/core/graphics/screen_fade.hpp b/source/core/graphics/screen_fade.hpp new file mode 100644 index 0000000..620a7bf --- /dev/null +++ b/source/core/graphics/screen_fade.hpp @@ -0,0 +1,43 @@ +// screen_fade.hpp - Fosa a/desde negre per a transicions d'escena +// © 2026 JailDesigner +// +// Rect negre a pantalla completa amb l'alpha animat linealment. L'escena que el +// posseeix l'actualitza cada frame i el dibuixa l'ÚLTIM (per damunt de tot). En +// repòs (default-construït o fosa acabada amb alpha 0) el draw() és un no-op. + +#pragma once + +#include "core/rendering/render_context.hpp" + +namespace Graphics { + + class ScreenFade { + public: + explicit ScreenFade(Rendering::Renderer* renderer); + + // Arrenca una fosa lineal d'alpha 'from' a 'to' en 'duration' segons. + void start(float from, float to, float duration); + + // Avança el temporitzador intern. + void update(float delta_time); + + // Dibuixa el rect negre a pantalla completa amb l'alpha actual. + // No fa res si l'alpha és ~0 (estalvia el pushRect). + void draw() const; + + // Cert quan la fosa ha acabat (o no s'ha arrencat mai). + [[nodiscard]] auto isDone() const -> bool; + + // Alpha actual ∈ [0, 1]. + [[nodiscard]] auto alpha() const -> float; + + private: + 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/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 28365e5..576feaf 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()), + fade_(sdl.getRenderer()) { // Recuperar configuración de match des del context match_config_ = context_.getMatchConfig(); @@ -170,6 +171,8 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) Audio::get()->enableSound(false); // El fons (graella) ha d'aparèixer ja muntat: la demo és una partida en marxa. playfield_.completeBuild(); + // Fosa des de negre sobre el joc ja en marxa (transició suau des del títol). + fade_.start(1.0F, 0.0F, Defaults::Game::Fade::DEMO_IN_DURATION); } else { stage_manager_->init(); } @@ -363,6 +366,8 @@ void GameScene::updateShipsControl(float delta_time) { } auto GameScene::stepDemo(float delta_time) -> bool { + fade_.update(delta_time); // fosa d'entrada des de negre + // 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); @@ -708,6 +713,10 @@ void GameScene::draw() { drawLevelCompletedState(); break; } + + // Fosa d'entrada de la demo: per damunt de tot. No-op fora del mode DEMO + // (fade_ mai s'arrenca) i quan ja ha acabat (alpha 0). + fade_.draw(); } void GameScene::drawEnemies() const { diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index 40ac85e..4561039 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -10,6 +10,7 @@ #include "core/graphics/border.hpp" #include "core/graphics/playfield.hpp" +#include "core/graphics/screen_fade.hpp" #include "core/graphics/starfield_parallax.hpp" #include "core/graphics/vector_text.hpp" #include "core/physics/physics_world.hpp" @@ -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_; + // Fosa des de negre en entrar a la demo (només mode DEMO; inactiu en partida normal). + Graphics::ScreenFade fade_; + // [NEW] Stage system std::unique_ptr stage_config_; std::unique_ptr stage_manager_; diff --git a/source/game/scenes/title_scene.cpp b/source/game/scenes/title_scene.cpp index d8a1968..3174956 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()), + fade_(sdl.getRenderer()) { std::cout << "SceneType Titol: Inicialitzant...\n"; match_config_.player1_active = false; @@ -333,6 +334,9 @@ void TitleScene::update(float delta_time) { case TitleState::BLACK_SCREEN: updateBlackScreenState(delta_time); break; + case TitleState::DEMO_FADE_OUT: + updateDemoFadeOutState(delta_time); + break; } // Les animacions segueixen pero els inputs es bloquegen mentre el menu @@ -368,7 +372,12 @@ 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: fosa a negre i, en acabar, canvi a GAME (el salt + // real el fa updateDemoFadeOutState). L'estat deixa de ser MAIN, així + // que ni es re-dispara la demo ni s'accepta START durant la fosa. + current_state_ = TitleState::DEMO_FADE_OUT; + fade_.start(0.0F, 1.0F, Defaults::Game::Fade::DEMO_OUT_DURATION); + temps_acumulat_ = 0.0F; } } } @@ -473,6 +482,13 @@ void TitleScene::updateBlackScreenState(float delta_time) { } } +void TitleScene::updateDemoFadeOutState(float delta_time) { + fade_.update(delta_time); + if (fade_.isDone()) { + context_.setNextScene(SceneType::GAME); + } +} + void TitleScene::handleSkipInput() { if (current_state_ != TitleState::STARFIELD_FADE_IN && current_state_ != TitleState::STARFIELD) { return; @@ -577,7 +593,8 @@ 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_FADE_OUT)) { ship_animator_->draw(); } drawFlashes(); @@ -586,7 +603,12 @@ void TitleScene::draw() { return; } - if (current_state_ != TitleState::MAIN && current_state_ != TitleState::PLAYER_JOIN_PHASE) { + // DEMO_FADE_OUT es pinta com MAIN (logo + text) perquè el títol segueixi + // visible sota la fosa a negre. + if (current_state_ != TitleState::MAIN && + current_state_ != TitleState::PLAYER_JOIN_PHASE && + current_state_ != TitleState::DEMO_FADE_OUT) { + fade_.draw(); // BLACK_SCREEN i altres: només la fosa (si activa) return; } @@ -657,6 +679,9 @@ void TitleScene::draw() { } dibuixarPeuTitol(SPACING); + + // Fosa a negre (attract): per damunt de tot. No-op si no està activa. + fade_.draw(); } auto TitleScene::checkSkipButtonPressed() -> bool { diff --git a/source/game/scenes/title_scene.hpp b/source/game/scenes/title_scene.hpp index 9dafdb8..c735b7b 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/screen_fade.hpp" #include "core/graphics/shape.hpp" #include "core/graphics/starfield.hpp" #include "core/graphics/vector_text.hpp" @@ -46,6 +47,7 @@ class TitleScene final : public Scene { MAIN, PLAYER_JOIN_PHASE, BLACK_SCREEN, + DEMO_FADE_OUT, // Attract: fosa a negre abans de saltar a la demo }; struct LogoLetter { @@ -60,6 +62,7 @@ class TitleScene final : public Scene { SceneManager::SceneContext& context_; GameConfig::MatchConfig match_config_; Graphics::VectorText text_; + Graphics::ScreenFade fade_; // Fosa a negre en saltar a la demo (attract) std::unique_ptr camera_; std::unique_ptr starfield_; std::unique_ptr ship_animator_; @@ -143,6 +146,7 @@ class TitleScene final : public Scene { void updateMainState(float delta_time); void updatePlayerJoinPhaseState(float delta_time); void updateBlackScreenState(float delta_time); + void updateDemoFadeOutState(float delta_time); void handleSkipInput(); void handleStartInput(); void triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, const char* log_prefix); From 2f6d6c405fb36968cde99b0b034dea8345b8e74a Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 29 May 2026 10:03:17 +0200 Subject: [PATCH 6/7] =?UTF-8?q?feat(demo):=20transici=C3=B3=20t=C3=ADtol?= =?UTF-8?q?=E2=86=92demo=20amb=20dive=20de=20c=C3=A0mera=20+=20cortinilla?= =?UTF-8?q?=20negra=20(substitueix=20el=20fundido)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/core/defaults/game.hpp | 17 +++++-- source/core/graphics/curtain.cpp | 71 +++++++++++++++++++++++++++ source/core/graphics/curtain.hpp | 48 +++++++++++++++++++ source/core/graphics/screen_fade.cpp | 62 ------------------------ source/core/graphics/screen_fade.hpp | 43 ----------------- source/game/scenes/game_scene.cpp | 15 +++--- source/game/scenes/game_scene.hpp | 6 +-- source/game/scenes/title_scene.cpp | 72 ++++++++++++++++++++-------- source/game/scenes/title_scene.hpp | 15 ++++-- 9 files changed, 204 insertions(+), 145 deletions(-) create mode 100644 source/core/graphics/curtain.cpp create mode 100644 source/core/graphics/curtain.hpp delete mode 100644 source/core/graphics/screen_fade.cpp delete mode 100644 source/core/graphics/screen_fade.hpp diff --git a/source/core/defaults/game.hpp b/source/core/defaults/game.hpp index 63de213..76463dd 100644 --- a/source/core/defaults/game.hpp +++ b/source/core/defaults/game.hpp @@ -39,11 +39,18 @@ 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: durada de la fosa a/desde negre a les transicions de demo. - namespace Fade { - constexpr float DEMO_OUT_DURATION = 0.6F; // TÍTOL → DEMO (fosa a negre abans del salt) - constexpr float DEMO_IN_DURATION = 0.6F; // DEMO: fosa des de negre sobre el joc ja en marxa - } // namespace Fade + // 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/screen_fade.cpp b/source/core/graphics/screen_fade.cpp deleted file mode 100644 index 76ec391..0000000 --- a/source/core/graphics/screen_fade.cpp +++ /dev/null @@ -1,62 +0,0 @@ -// screen_fade.cpp - Implementació de la fosa a/desde negre -// © 2026 JailDesigner - -#include "core/graphics/screen_fade.hpp" - -#include -#include - -#include "core/defaults/game.hpp" - -namespace Graphics { - - ScreenFade::ScreenFade(Rendering::Renderer* renderer) - : renderer_(renderer) {} - - void ScreenFade::start(float from, float to, float duration) { - from_ = from; - to_ = to; - duration_ = duration; - elapsed_ = 0.0F; - active_ = true; - } - - void ScreenFade::update(float delta_time) { - if (!active_) { - return; - } - elapsed_ += delta_time; - } - - auto ScreenFade::alpha() const -> float { - if (!active_) { - return 0.0F; - } - if (duration_ <= 0.0F) { - return to_; - } - const float T = std::clamp(elapsed_ / duration_, 0.0F, 1.0F); - return std::lerp(from_, to_, T); - } - - auto ScreenFade::isDone() const -> bool { - return !active_ || elapsed_ >= duration_; - } - - void ScreenFade::draw() const { - const float A = alpha(); - if (A <= 0.0F) { - return; - } - renderer_->pushRect( - 0.0F, - 0.0F, - static_cast(Defaults::Game::WIDTH), - static_cast(Defaults::Game::HEIGHT), - 0.0F, - 0.0F, - 0.0F, - A); - } - -} // namespace Graphics diff --git a/source/core/graphics/screen_fade.hpp b/source/core/graphics/screen_fade.hpp deleted file mode 100644 index 620a7bf..0000000 --- a/source/core/graphics/screen_fade.hpp +++ /dev/null @@ -1,43 +0,0 @@ -// screen_fade.hpp - Fosa a/desde negre per a transicions d'escena -// © 2026 JailDesigner -// -// Rect negre a pantalla completa amb l'alpha animat linealment. L'escena que el -// posseeix l'actualitza cada frame i el dibuixa l'ÚLTIM (per damunt de tot). En -// repòs (default-construït o fosa acabada amb alpha 0) el draw() és un no-op. - -#pragma once - -#include "core/rendering/render_context.hpp" - -namespace Graphics { - - class ScreenFade { - public: - explicit ScreenFade(Rendering::Renderer* renderer); - - // Arrenca una fosa lineal d'alpha 'from' a 'to' en 'duration' segons. - void start(float from, float to, float duration); - - // Avança el temporitzador intern. - void update(float delta_time); - - // Dibuixa el rect negre a pantalla completa amb l'alpha actual. - // No fa res si l'alpha és ~0 (estalvia el pushRect). - void draw() const; - - // Cert quan la fosa ha acabat (o no s'ha arrencat mai). - [[nodiscard]] auto isDone() const -> bool; - - // Alpha actual ∈ [0, 1]. - [[nodiscard]] auto alpha() const -> float; - - private: - 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/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 576feaf..173ec8e 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -56,7 +56,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) starfield_parallax_(sdl.getRenderer()), playfield_(sdl.getRenderer()), border_(sdl.getRenderer()), - fade_(sdl.getRenderer()) { + curtain_(sdl.getRenderer()) { // Recuperar configuración de match des del context match_config_ = context_.getMatchConfig(); @@ -171,8 +171,9 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context) Audio::get()->enableSound(false); // El fons (graella) ha d'aparèixer ja muntat: la demo és una partida en marxa. playfield_.completeBuild(); - // Fosa des de negre sobre el joc ja en marxa (transició suau des del títol). - fade_.start(1.0F, 0.0F, Defaults::Game::Fade::DEMO_IN_DURATION); + // 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(); } @@ -366,7 +367,7 @@ void GameScene::updateShipsControl(float delta_time) { } auto GameScene::stepDemo(float delta_time) -> bool { - fade_.update(delta_time); // fosa d'entrada des de negre + 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)) { @@ -714,9 +715,9 @@ void GameScene::draw() { break; } - // Fosa d'entrada de la demo: per damunt de tot. No-op fora del mode DEMO - // (fade_ mai s'arrenca) i quan ja ha acabat (alpha 0). - fade_.draw(); + // 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 { diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index 4561039..717b186 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -9,8 +9,8 @@ #include #include "core/graphics/border.hpp" +#include "core/graphics/curtain.hpp" #include "core/graphics/playfield.hpp" -#include "core/graphics/screen_fade.hpp" #include "core/graphics/starfield_parallax.hpp" #include "core/graphics/vector_text.hpp" #include "core/physics/physics_world.hpp" @@ -96,8 +96,8 @@ class GameScene final : public Scene { // Border del playfield (4 línies amb desplaçaments i flash per impactes) Graphics::Border border_; - // Fosa des de negre en entrar a la demo (només mode DEMO; inactiu en partida normal). - Graphics::ScreenFade fade_; + // 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_; diff --git a/source/game/scenes/title_scene.cpp b/source/game/scenes/title_scene.cpp index 3174956..5c20fe4 100644 --- a/source/game/scenes/title_scene.cpp +++ b/source/game/scenes/title_scene.cpp @@ -38,7 +38,7 @@ TitleScene::TitleScene(SDLManager& sdl, SceneContext& context) : sdl_(sdl), context_(context), text_(sdl.getRenderer()), - fade_(sdl.getRenderer()) { + curtain_(sdl.getRenderer()) { std::cout << "SceneType Titol: Inicialitzant...\n"; match_config_.player1_active = false; @@ -277,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_) { @@ -334,8 +335,11 @@ void TitleScene::update(float delta_time) { case TitleState::BLACK_SCREEN: updateBlackScreenState(delta_time); break; - case TitleState::DEMO_FADE_OUT: - updateDemoFadeOutState(delta_time); + case TitleState::DEMO_DIVE: + updateDemoDiveState(delta_time); + break; + case TitleState::DEMO_CURTAIN: + updateDemoCurtainState(delta_time); break; } @@ -372,11 +376,14 @@ void TitleScene::update(float delta_time) { demo_cfg.player2_active = (SC.players >= 2); demo_cfg.mode = GameConfig::Mode::DEMO; context_.setMatchConfig(demo_cfg); - // No saltem en sec: fosa a negre i, en acabar, canvi a GAME (el salt - // real el fa updateDemoFadeOutState). L'estat deixa de ser MAIN, així - // que ni es re-dispara la demo ni s'accepta START durant la fosa. - current_state_ = TitleState::DEMO_FADE_OUT; - fade_.start(0.0F, 1.0F, Defaults::Game::Fade::DEMO_OUT_DURATION); + // 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; } } @@ -482,9 +489,29 @@ void TitleScene::updateBlackScreenState(float delta_time) { } } -void TitleScene::updateDemoFadeOutState(float delta_time) { - fade_.update(delta_time); - if (fade_.isDone()) { +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. + if (camera_ != nullptr) { + camera_->setPosition(Vec3{.x = 0.0F, .y = 0.0F, .z = EASED * D::CAMERA_DISTANCE}); + } + // 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); } } @@ -594,7 +621,8 @@ void TitleScene::draw() { current_state_ == TitleState::STARFIELD || current_state_ == TitleState::MAIN || current_state_ == TitleState::PLAYER_JOIN_PHASE || - current_state_ == TitleState::DEMO_FADE_OUT)) { + current_state_ == TitleState::DEMO_DIVE || + current_state_ == TitleState::DEMO_CURTAIN)) { ship_animator_->draw(); } drawFlashes(); @@ -603,19 +631,21 @@ void TitleScene::draw() { return; } - // DEMO_FADE_OUT es pinta com MAIN (logo + text) perquè el títol segueixi - // visible sota la fosa a negre. + // 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_FADE_OUT) { - fade_.draw(); // BLACK_SCREEN i altres: només la fosa (si activa) + 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; @@ -680,8 +710,8 @@ void TitleScene::draw() { dibuixarPeuTitol(SPACING); - // Fosa a negre (attract): per damunt de tot. No-op si no està activa. - fade_.draw(); + // 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 c735b7b..28f9c74 100644 --- a/source/game/scenes/title_scene.hpp +++ b/source/game/scenes/title_scene.hpp @@ -18,7 +18,7 @@ #include #include "core/graphics/camera3d.hpp" -#include "core/graphics/screen_fade.hpp" +#include "core/graphics/curtain.hpp" #include "core/graphics/shape.hpp" #include "core/graphics/starfield.hpp" #include "core/graphics/vector_text.hpp" @@ -47,7 +47,8 @@ class TitleScene final : public Scene { MAIN, PLAYER_JOIN_PHASE, BLACK_SCREEN, - DEMO_FADE_OUT, // Attract: fosa a negre abans de saltar a la demo + 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 { @@ -62,7 +63,7 @@ class TitleScene final : public Scene { SceneManager::SceneContext& context_; GameConfig::MatchConfig match_config_; Graphics::VectorText text_; - Graphics::ScreenFade fade_; // Fosa a negre en saltar a la demo (attract) + Graphics::Curtain curtain_; // Cortinilla negra en saltar a la demo (attract) std::unique_ptr camera_; std::unique_ptr starfield_; std::unique_ptr ship_animator_; @@ -84,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}; @@ -146,7 +152,8 @@ class TitleScene final : public Scene { void updateMainState(float delta_time); void updatePlayerJoinPhaseState(float delta_time); void updateBlackScreenState(float delta_time); - void updateDemoFadeOutState(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); From 30bbb37bff735dc843acfd88429a8e6bc2569fd3 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 29 May 2026 10:13:30 +0200 Subject: [PATCH 7/7] =?UTF-8?q?fix(demo):=20el=20dive=20movia=20nom=C3=A9s?= =?UTF-8?q?=20la=20posici=C3=B3=20de=20la=20c=C3=A0mera=20i=20s'invertia?= =?UTF-8?q?=20el=20forward;=20ara=20mou=20posici=C3=B3=20i=20target=20alho?= =?UTF-8?q?ra?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/game/scenes/title_scene.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/source/game/scenes/title_scene.cpp b/source/game/scenes/title_scene.cpp index 5c20fe4..25e292f 100644 --- a/source/game/scenes/title_scene.cpp +++ b/source/game/scenes/title_scene.cpp @@ -497,8 +497,13 @@ void TitleScene::updateDemoDiveState(float delta_time) { // 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) { - camera_->setPosition(Vec3{.x = 0.0F, .y = 0.0F, .z = EASED * D::CAMERA_DISTANCE}); + 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);