Merge branch 'feature/attract-polish': polish de l'attract mode (logo silenciós, demo sense SFX, fons ja muntat, rètol de demo i transició dive+cortinilla títol→demo)
This commit is contained in:
@@ -27,6 +27,9 @@ hud:
|
|||||||
title:
|
title:
|
||||||
press_start: "PREMEU START PER JUGAR"
|
press_start: "PREMEU START PER JUGAR"
|
||||||
|
|
||||||
|
demo:
|
||||||
|
banner: "MODE DEMO - PREMEU START"
|
||||||
|
|
||||||
game_screen:
|
game_screen:
|
||||||
game_over: "FI DEL JOC"
|
game_over: "FI DEL JOC"
|
||||||
continue: "CONTINUAR"
|
continue: "CONTINUAR"
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ hud:
|
|||||||
title:
|
title:
|
||||||
press_start: "PRESS START TO PLAY"
|
press_start: "PRESS START TO PLAY"
|
||||||
|
|
||||||
|
demo:
|
||||||
|
banner: "DEMO MODE - PRESS START"
|
||||||
|
|
||||||
game_screen:
|
game_screen:
|
||||||
game_over: "GAME OVER"
|
game_over: "GAME OVER"
|
||||||
continue: "CONTINUE"
|
continue: "CONTINUE"
|
||||||
|
|||||||
@@ -39,6 +39,19 @@ namespace Defaults::Game {
|
|||||||
constexpr float LEVEL_COMPLETED_DURATION = 3.0F; // Duración total
|
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)
|
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)
|
// Transición INIT_HUD (animación inicial del HUD)
|
||||||
constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado
|
constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// curtain.cpp - Implementació de la cortinilla negra
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#include "core/graphics/curtain.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "core/defaults/game.hpp"
|
||||||
|
#include "core/math/easing.hpp"
|
||||||
|
|
||||||
|
namespace Graphics {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr float SCREEN_H = static_cast<float>(Defaults::Game::HEIGHT);
|
||||||
|
constexpr float SCREEN_W = static_cast<float>(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
|
||||||
@@ -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
|
||||||
@@ -62,6 +62,12 @@ namespace Graphics {
|
|||||||
buildLines();
|
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) {
|
void Playfield::update(float delta_time) {
|
||||||
elapsed_s_ += delta_time;
|
elapsed_s_ += delta_time;
|
||||||
for (auto& ripple : ripples_) {
|
for (auto& ripple : ripples_) {
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ namespace Graphics {
|
|||||||
// Avança timers interns (creació + ripples).
|
// Avança timers interns (creació + ripples).
|
||||||
void update(float delta_time);
|
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,
|
// 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.
|
// i s'aplica deformació radial per cada ripple activa que afecti la línia.
|
||||||
void draw() const;
|
void draw() const;
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
|||||||
text_(sdl.getRenderer()),
|
text_(sdl.getRenderer()),
|
||||||
starfield_parallax_(sdl.getRenderer()),
|
starfield_parallax_(sdl.getRenderer()),
|
||||||
playfield_(sdl.getRenderer()),
|
playfield_(sdl.getRenderer()),
|
||||||
border_(sdl.getRenderer()) {
|
border_(sdl.getRenderer()),
|
||||||
|
curtain_(sdl.getRenderer()) {
|
||||||
// Recuperar configuración de match des del context
|
// Recuperar configuración de match des del context
|
||||||
match_config_ = context_.getMatchConfig();
|
match_config_ = context_.getMatchConfig();
|
||||||
|
|
||||||
@@ -164,6 +165,15 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
|||||||
context_.advanceDemoScenario();
|
context_.advanceDemoScenario();
|
||||||
stage_manager_->initDemo(SC.stage);
|
stage_manager_->initDemo(SC.stage);
|
||||||
demo_timer_ = DEMO_DURATION;
|
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 {
|
} else {
|
||||||
stage_manager_->init();
|
stage_manager_->init();
|
||||||
}
|
}
|
||||||
@@ -231,6 +241,13 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
|||||||
init_hud_rect_sound_played_ = false;
|
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 {
|
auto GameScene::isFinished() const -> bool {
|
||||||
return context_.nextScene() != SceneType::GAME;
|
return context_.nextScene() != SceneType::GAME;
|
||||||
}
|
}
|
||||||
@@ -350,6 +367,8 @@ void GameScene::updateShipsControl(float delta_time) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto GameScene::stepDemo(float delta_time) -> bool {
|
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).
|
// Qualsevol input trenca la demo i torna al títol (música intacta).
|
||||||
if (Input::get()->checkAnyPlayerAction(DEMO_EXIT_ACTIONS)) {
|
if (Input::get()->checkAnyPlayerAction(DEMO_EXIT_ACTIONS)) {
|
||||||
context_.setNextScene(SceneType::TITLE, Option::JUMP_TO_TITLE_MAIN);
|
context_.setNextScene(SceneType::TITLE, Option::JUMP_TO_TITLE_MAIN);
|
||||||
@@ -695,6 +714,10 @@ void GameScene::draw() {
|
|||||||
drawLevelCompletedState();
|
drawLevelCompletedState();
|
||||||
break;
|
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 {
|
void GameScene::drawEnemies() const {
|
||||||
@@ -904,6 +927,12 @@ void GameScene::drawScoreboard() {
|
|||||||
.x = scoreboard_zone.w / 2.0F,
|
.x = scoreboard_zone.w / 2.0F,
|
||||||
.y = scoreboard_zone.y + (scoreboard_zone.h / 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);
|
Systems::InitHud::drawScoreboardSegmentsAt(text_, buildScoreboardSegments(), CENTER, SCALE, SPACING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "core/graphics/border.hpp"
|
#include "core/graphics/border.hpp"
|
||||||
|
#include "core/graphics/curtain.hpp"
|
||||||
#include "core/graphics/playfield.hpp"
|
#include "core/graphics/playfield.hpp"
|
||||||
#include "core/graphics/starfield_parallax.hpp"
|
#include "core/graphics/starfield_parallax.hpp"
|
||||||
#include "core/graphics/vector_text.hpp"
|
#include "core/graphics/vector_text.hpp"
|
||||||
@@ -43,7 +44,7 @@ enum class GameOverState : uint8_t {
|
|||||||
class GameScene final : public Scene {
|
class GameScene final : public Scene {
|
||||||
public:
|
public:
|
||||||
explicit GameScene(SDLManager& sdl, SceneManager::SceneContext& context);
|
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
|
// Scene interface
|
||||||
void handleEvent(const SDL_Event& event) override;
|
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)
|
// Border del playfield (4 línies amb desplaçaments i flash per impactes)
|
||||||
Graphics::Border border_;
|
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
|
// [NEW] Stage system
|
||||||
std::unique_ptr<StageSystem::StageSystemConfig> stage_config_;
|
std::unique_ptr<StageSystem::StageSystemConfig> stage_config_;
|
||||||
std::unique_ptr<StageSystem::StageManager> stage_manager_;
|
std::unique_ptr<StageSystem::StageManager> stage_manager_;
|
||||||
@@ -106,6 +110,7 @@ class GameScene final : public Scene {
|
|||||||
std::array<Systems::Demo::DemoPilot, 2> demo_pilots_;
|
std::array<Systems::Demo::DemoPilot, 2> demo_pilots_;
|
||||||
std::array<Systems::Demo::Control, 2> demo_ctrls_{}; // Control per nau al frame actual
|
std::array<Systems::Demo::Control, 2> demo_ctrls_{}; // Control per nau al frame actual
|
||||||
float demo_timer_{0.0F}; // Temps restant de la demo (→ LOGO)
|
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
|
// Funciones privades
|
||||||
// bullet_velocity: velocitat de la bala que ha causat la mort (Vec2{} si no
|
// bullet_velocity: velocitat de la bala que ha causat la mort (Vec2{} si no
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ LogoScene::LogoScene(SDLManager& sdl, SceneContext& context)
|
|||||||
(void)option; // Suprimir warning
|
(void)option; // Suprimir warning
|
||||||
|
|
||||||
sound_played_.fill(false); // Inicialitzar seguiment de sons
|
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();
|
initLetters();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +176,10 @@ void LogoScene::changeState(AnimationState nou_estat) {
|
|||||||
std::mt19937 g(rd());
|
std::mt19937 g(rd());
|
||||||
std::shuffle(explosion_order_.begin(), explosion_order_.end(), g);
|
std::shuffle(explosion_order_.begin(), explosion_order_.end(), g);
|
||||||
} else if (nou_estat == AnimationState::POST_EXPLOSION) {
|
} 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<int>(nou_estat)
|
std::cout << "[LogoScene] Canvi a state: " << static_cast<int>(nou_estat)
|
||||||
@@ -237,9 +245,13 @@ void LogoScene::update(float delta_time) {
|
|||||||
global_progress,
|
global_progress,
|
||||||
LETTER_THRESHOLD);
|
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) {
|
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;
|
sound_played_[i] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ class LogoScene final : public Scene {
|
|||||||
// Seguiment de sons de lletres (evitar reproduccions repetides)
|
// Seguiment de sons de lletres (evitar reproduccions repetides)
|
||||||
std::array<bool, 9> sound_played_; // Track si cada letter ya ha reproduit el so
|
std::array<bool, 9> 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
|
// Constants de animación
|
||||||
static constexpr float DURATION_PRE = 1.5F; // Duració PRE_ANIMATION (pantalla negra)
|
static constexpr float DURATION_PRE = 1.5F; // Duració PRE_ANIMATION (pantalla negra)
|
||||||
static constexpr float DURATION_ZOOM = 4.0F; // Duració del zoom (segons)
|
static constexpr float DURATION_ZOOM = 4.0F; // Duració del zoom (segons)
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ namespace {
|
|||||||
TitleScene::TitleScene(SDLManager& sdl, SceneContext& context)
|
TitleScene::TitleScene(SDLManager& sdl, SceneContext& context)
|
||||||
: sdl_(sdl),
|
: sdl_(sdl),
|
||||||
context_(context),
|
context_(context),
|
||||||
text_(sdl.getRenderer()) {
|
text_(sdl.getRenderer()),
|
||||||
|
curtain_(sdl.getRenderer()) {
|
||||||
std::cout << "SceneType Titol: Inicialitzant...\n";
|
std::cout << "SceneType Titol: Inicialitzant...\n";
|
||||||
|
|
||||||
match_config_.player1_active = false;
|
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).
|
// 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_X = Defaults::Game::WIDTH / 2.0F;
|
||||||
const float SCREEN_CENTRE_Y = Defaults::Game::HEIGHT / 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_));
|
// dive_zoom_ (attract) afegeix el zoom del dive per travessar el peu.
|
||||||
const float COPYRIGHT_S = std::lerp(S::FOOTER_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_copyright_progress_));
|
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;
|
const float JAILGAMES_RENDER_SCALE = Defaults::Title::Layout::JAILGAMES_SCALE * JAILGAMES_S;
|
||||||
for (const auto& letter : letters_jailgames_) {
|
for (const auto& letter : letters_jailgames_) {
|
||||||
@@ -333,6 +335,12 @@ void TitleScene::update(float delta_time) {
|
|||||||
case TitleState::BLACK_SCREEN:
|
case TitleState::BLACK_SCREEN:
|
||||||
updateBlackScreenState(delta_time);
|
updateBlackScreenState(delta_time);
|
||||||
break;
|
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
|
// 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.player2_active = (SC.players >= 2);
|
||||||
demo_cfg.mode = GameConfig::Mode::DEMO;
|
demo_cfg.mode = GameConfig::Mode::DEMO;
|
||||||
context_.setMatchConfig(demo_cfg);
|
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() {
|
void TitleScene::handleSkipInput() {
|
||||||
if (current_state_ != TitleState::STARFIELD_FADE_IN && current_state_ != TitleState::STARFIELD) {
|
if (current_state_ != TitleState::STARFIELD_FADE_IN && current_state_ != TitleState::STARFIELD) {
|
||||||
return;
|
return;
|
||||||
@@ -577,7 +625,9 @@ void TitleScene::draw() {
|
|||||||
(current_state_ == TitleState::STARFIELD_FADE_IN ||
|
(current_state_ == TitleState::STARFIELD_FADE_IN ||
|
||||||
current_state_ == TitleState::STARFIELD ||
|
current_state_ == TitleState::STARFIELD ||
|
||||||
current_state_ == TitleState::MAIN ||
|
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();
|
ship_animator_->draw();
|
||||||
}
|
}
|
||||||
drawFlashes();
|
drawFlashes();
|
||||||
@@ -586,14 +636,21 @@ void TitleScene::draw() {
|
|||||||
return;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Factor d'escala+posició per simular un moviment 3D des de l'usuari (prop,
|
// 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
|
// sprite gran i posició projectada extrema) cap al VP (lluny, sprite a la
|
||||||
// seva mida i posició finals). Pivot: centre de pantalla (= projecció VP).
|
// 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_X = Defaults::Game::WIDTH / 2.0F;
|
||||||
const float SCREEN_CENTRE_Y = Defaults::Game::HEIGHT / 2.0F;
|
const float SCREEN_CENTRE_Y = Defaults::Game::HEIGHT / 2.0F;
|
||||||
const float LOGO_RENDER_SCALE = Defaults::Title::Layout::LOGO_SCALE * LOGO_S;
|
const float LOGO_RENDER_SCALE = Defaults::Title::Layout::LOGO_SCALE * LOGO_S;
|
||||||
@@ -657,6 +714,9 @@ void TitleScene::draw() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dibuixarPeuTitol(SPACING);
|
dibuixarPeuTitol(SPACING);
|
||||||
|
|
||||||
|
// Cortinilla negra (attract): per damunt de tot. No-op si no està activa.
|
||||||
|
curtain_.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto TitleScene::checkSkipButtonPressed() -> bool {
|
auto TitleScene::checkSkipButtonPressed() -> bool {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "core/graphics/camera3d.hpp"
|
#include "core/graphics/camera3d.hpp"
|
||||||
|
#include "core/graphics/curtain.hpp"
|
||||||
#include "core/graphics/shape.hpp"
|
#include "core/graphics/shape.hpp"
|
||||||
#include "core/graphics/starfield.hpp"
|
#include "core/graphics/starfield.hpp"
|
||||||
#include "core/graphics/vector_text.hpp"
|
#include "core/graphics/vector_text.hpp"
|
||||||
@@ -46,6 +47,8 @@ class TitleScene final : public Scene {
|
|||||||
MAIN,
|
MAIN,
|
||||||
PLAYER_JOIN_PHASE,
|
PLAYER_JOIN_PHASE,
|
||||||
BLACK_SCREEN,
|
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 {
|
struct LogoLetter {
|
||||||
@@ -60,6 +63,7 @@ class TitleScene final : public Scene {
|
|||||||
SceneManager::SceneContext& context_;
|
SceneManager::SceneContext& context_;
|
||||||
GameConfig::MatchConfig match_config_;
|
GameConfig::MatchConfig match_config_;
|
||||||
Graphics::VectorText text_;
|
Graphics::VectorText text_;
|
||||||
|
Graphics::Curtain curtain_; // Cortinilla negra en saltar a la demo (attract)
|
||||||
std::unique_ptr<Graphics::Camera3D> camera_;
|
std::unique_ptr<Graphics::Camera3D> camera_;
|
||||||
std::unique_ptr<Graphics::Starfield> starfield_;
|
std::unique_ptr<Graphics::Starfield> starfield_;
|
||||||
std::unique_ptr<Title::ShipAnimator> ship_animator_;
|
std::unique_ptr<Title::ShipAnimator> ship_animator_;
|
||||||
@@ -81,6 +85,11 @@ class TitleScene final : public Scene {
|
|||||||
float temps_acumulat_{0.0F};
|
float temps_acumulat_{0.0F};
|
||||||
float idle_timer_{0.0F}; // Attract mode: inactivitat acumulada al state MAIN
|
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<LogoLetter> letters_orni_;
|
std::vector<LogoLetter> letters_orni_;
|
||||||
std::vector<LogoLetter> letters_attack_;
|
std::vector<LogoLetter> letters_attack_;
|
||||||
float dynamic_attack_y_{0.0F};
|
float dynamic_attack_y_{0.0F};
|
||||||
@@ -143,6 +152,8 @@ class TitleScene final : public Scene {
|
|||||||
void updateMainState(float delta_time);
|
void updateMainState(float delta_time);
|
||||||
void updatePlayerJoinPhaseState(float delta_time);
|
void updatePlayerJoinPhaseState(float delta_time);
|
||||||
void updateBlackScreenState(float delta_time);
|
void updateBlackScreenState(float delta_time);
|
||||||
|
void updateDemoDiveState(float delta_time);
|
||||||
|
void updateDemoCurtainState(float delta_time);
|
||||||
void handleSkipInput();
|
void handleSkipInput();
|
||||||
void handleStartInput();
|
void handleStartInput();
|
||||||
void triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, const char* log_prefix);
|
void triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, const char* log_prefix);
|
||||||
|
|||||||
Reference in New Issue
Block a user