diff --git a/data/ui.yaml b/data/ui.yaml index 5f9e12a..c034648 100644 --- a/data/ui.yaml +++ b/data/ui.yaml @@ -113,6 +113,13 @@ title: copyright_color: 246 blink_visible_ms: 800 blink_hidden_ms: 200 + # Fase STARTING (entre la primera pulsació de START i el salt a la + # següent escena). `starting_ms` és la durada fixa; durant aquesta + # fase el parpadeig accelera per `starting_blink_speedup` (2.0 = doble + # de freqüència). Si l'altre jugador prem START dins d'aquesta + # finestra, el comptador es reseteja i s'arrenca amb BOTH. + starting_ms: 3000 + starting_blink_speedup: 2.0 # Distorsió sinusoidal del fons (efecte de calor del desert). # `amplitude_px`: desplaçament màxim de cada fila en píxels. # `wavelength_px`: distància vertical entre dos pics de l'ona. diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index c13afad..cccc406 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -251,19 +251,18 @@ Director::Director() { std::cout << "Director: start_scene=" << DebugConfig::toString(START) << " starting_players=" << DebugConfig::toString(STARTING_PLAYERS) << '\n'; - // El branch Release inicialitza STARTING_PLAYERS amb una constant - // literal i clang considera la captura redundant; en Debug el valor és - // runtime i la captura sí és imprescindible. La lambda viu dins de - // IntroScene, més enllà del scope d'aquest ctor — capturar per - // referència seria UB. - // NOLINTNEXTLINE(clang-diagnostic-unused-lambda-capture) - auto make_game = [STARTING_PLAYERS]() -> std::unique_ptr { + // `make_game` i `make_intro` reben `StartingPlayers` com a paràmetre + // perquè la TitleScene tria al moment quins jugadors entren (P1, P2, + // BOTH) segons la pulsació. La IntroScene ho reenvia a la GameScene. + // El valor de `STARTING_PLAYERS` només s'usa com a punt d'entrada en + // mode Debug quan saltem directament a INTRO o GAME (saltant TITLE). + auto make_game = [](DebugConfig::StartingPlayers players) -> std::unique_ptr { auto cfg = GameScene::defaultConfig(); - cfg.starting_players = STARTING_PLAYERS; + cfg.starting_players = players; return std::make_unique(std::move(cfg)); }; - auto make_intro = [placeholder_text, make_game]() -> std::unique_ptr { - return std::make_unique(placeholder_text, make_game); + auto make_intro = [placeholder_text, make_game](DebugConfig::StartingPlayers players) -> std::unique_ptr { + return std::make_unique(placeholder_text, players, make_game); }; auto make_title = [menu_title_text, placeholder_text, title_bg_surface, mini_logo_surface, make_intro]() -> std::unique_ptr { return std::make_unique(menu_title_text, placeholder_text, title_bg_surface, mini_logo_surface, make_intro); @@ -277,10 +276,10 @@ Director::Director() { current_scene_ = make_title(); break; case DebugConfig::StartScene::INTRO: - current_scene_ = make_intro(); + current_scene_ = make_intro(STARTING_PLAYERS); break; case DebugConfig::StartScene::GAME: - current_scene_ = make_game(); + current_scene_ = make_game(STARTING_PLAYERS); break; } last_ticks_ms_ = SDL_GetTicks(); diff --git a/source/core/ui/ui_config.cpp b/source/core/ui/ui_config.cpp index 09e44c9..c543ef2 100644 --- a/source/core/ui/ui_config.cpp +++ b/source/core/ui/ui_config.cpp @@ -102,6 +102,8 @@ UiConfig::UiConfig() title_copyright_color_(Defaults::UI::Title::COPYRIGHT_COLOR), title_blink_visible_ms_(Defaults::UI::Title::BLINK_VISIBLE_MS), title_blink_hidden_ms_(Defaults::UI::Title::BLINK_HIDDEN_MS), + title_starting_ms_(Defaults::UI::Title::STARTING_MS), + title_starting_blink_speedup_(Defaults::UI::Title::STARTING_BLINK_SPEEDUP), title_heat_haze_{ .enabled = Defaults::UI::Title::HeatHaze::ENABLED, .amplitude_px = Defaults::UI::Title::HeatHaze::AMPLITUDE_PX, @@ -224,6 +226,8 @@ void UiConfig::load(const std::string& file_path) { readColorIndex(t, "copyright_color", title_copyright_color_); readField(t, "blink_visible_ms", title_blink_visible_ms_); readField(t, "blink_hidden_ms", title_blink_hidden_ms_); + readField(t, "starting_ms", title_starting_ms_); + readField(t, "starting_blink_speedup", title_starting_blink_speedup_); if (t.contains("heat_haze")) { const auto& hh = t["heat_haze"]; readField(hh, "enabled", title_heat_haze_.enabled); diff --git a/source/core/ui/ui_config.hpp b/source/core/ui/ui_config.hpp index e9df296..1426697 100644 --- a/source/core/ui/ui_config.hpp +++ b/source/core/ui/ui_config.hpp @@ -95,6 +95,8 @@ class UiConfig { [[nodiscard]] auto titleCopyrightColor() const -> Uint8 { return title_copyright_color_; } [[nodiscard]] auto titleBlinkVisibleMs() const -> int { return title_blink_visible_ms_; } [[nodiscard]] auto titleBlinkHiddenMs() const -> int { return title_blink_hidden_ms_; } + [[nodiscard]] auto titleStartingMs() const -> int { return title_starting_ms_; } + [[nodiscard]] auto titleStartingBlinkSpeedup() const -> float { return title_starting_blink_speedup_; } [[nodiscard]] auto titleHeatHaze() const -> const TitleHeatHaze& { return title_heat_haze_; } // Corbes d'easing triades al YAML. Si el nom no és conegut, s'usa @@ -167,6 +169,8 @@ class UiConfig { Uint8 title_copyright_color_; int title_blink_visible_ms_; int title_blink_hidden_ms_; + int title_starting_ms_; + float title_starting_blink_speedup_; TitleHeatHaze title_heat_haze_; std::string menu_select_sound_; diff --git a/source/game/scenes/intro_scene.cpp b/source/game/scenes/intro_scene.cpp index d912b44..6f318b6 100644 --- a/source/game/scenes/intro_scene.cpp +++ b/source/game/scenes/intro_scene.cpp @@ -30,10 +30,11 @@ namespace { } // namespace -IntroScene::IntroScene(std::shared_ptr font, SceneFactory next_factory) +IntroScene::IntroScene(std::shared_ptr font, DebugConfig::StartingPlayers starting_players, SceneFactory next_factory) : font_(std::move(font)), + starting_players_(starting_players), next_factory_(std::move(next_factory)) { - std::cout << "[scene] " << SCENE_NAME << " start\n"; + std::cout << "[scene] " << SCENE_NAME << " start (players=" << DebugConfig::toString(starting_players_) << ")\n"; } IntroScene::~IntroScene() = default; @@ -52,7 +53,7 @@ void IntroScene::update(int delta_ms) { }); if ((timer_ms_ <= 0 || SKIPPED) && next_factory_) { - next_scene_ = next_factory_(); + next_scene_ = next_factory_(starting_players_); } Notifier::get().update(delta_ms); diff --git a/source/game/scenes/intro_scene.hpp b/source/game/scenes/intro_scene.hpp index 859b6ea..ec941bb 100644 --- a/source/game/scenes/intro_scene.hpp +++ b/source/game/scenes/intro_scene.hpp @@ -4,19 +4,24 @@ #include #include "core/scene/scene.hpp" +#include "game/debug_config.hpp" class Text; // Escena placeholder de la introducció. Com la resta de placeholders, durada // fixa i skip amb tecla no reservada; quan expira encadena a la factory // injectada (en producció: GameScene). +// +// `starting_players` és el resultat triat a la TitleScene (P1, P2 o BOTH). +// L'IntroScene només el guarda i el reenvia a la seva factory; quan demà +// l'intro tinga contingut real podrà variar segons qui jugue. class IntroScene : public Scene { public: - using SceneFactory = std::function()>; + using SceneFactory = std::function(DebugConfig::StartingPlayers)>; static constexpr int DURATION_MS = 5000; - IntroScene(std::shared_ptr font, SceneFactory next_factory); + IntroScene(std::shared_ptr font, DebugConfig::StartingPlayers starting_players, SceneFactory next_factory); ~IntroScene() override; void update(int delta_ms) override; @@ -24,6 +29,7 @@ class IntroScene : public Scene { private: std::shared_ptr font_; + DebugConfig::StartingPlayers starting_players_; SceneFactory next_factory_; int timer_ms_{DURATION_MS}; }; diff --git a/source/game/scenes/title_scene.cpp b/source/game/scenes/title_scene.cpp index cfe1d2e..55b498f 100644 --- a/source/game/scenes/title_scene.cpp +++ b/source/game/scenes/title_scene.cpp @@ -8,6 +8,8 @@ #include #include +#include "core/input/gamepad_bindings.hpp" +#include "core/input/gamepad_manager.hpp" #include "core/input/global_inputs.hpp" #include "core/locale/locale.hpp" #include "core/rendering/notifier.hpp" @@ -19,7 +21,7 @@ #include "core/system/event_buffer.hpp" #include "core/ui/ui_config.hpp" #include "game/options.hpp" -#include "game/scenes/placeholder_input.hpp" +#include "utils/defaults.hpp" #include "utils/defines.hpp" namespace { @@ -38,6 +40,27 @@ namespace { constexpr int COPYRIGHT_Y_PERCENT = 90; constexpr int LOGO_GAP_PX = 2; // Separació vertical entre el minilogo i el text de copyright. + // Detecta una pulsació de START dins el frame actual per al jugador + // indicat. Combina events de teclat (scancode hardcoded a Defaults:: + // KeysGame[P2]) i events de gamepad (botó remapejable via + // GamepadBindings, lligat al pad assignat al jugador via Options). + auto pressedStart(int player_id) -> bool { + const SDL_Scancode KB = (player_id == 0) ? Defaults::KeysGame::START : Defaults::KeysGameP2::START; + SDL_Gamepad* pad = GamepadManager::get().getForPlayer(player_id); + const SDL_JoystickID PAD_ID = (pad != nullptr) ? SDL_GetGamepadID(pad) : 0; + const auto& slot = (player_id == 0) ? Options::controls().gamepad_p1 + : Options::controls().gamepad_p2; + const int START_BTN = GamepadBindings::get().getButton(slot.name, slot.path, GamepadBindings::Action::START); + + return std::ranges::any_of(EventBuffer::events, [&](const SDL_Event& ev) { + if (ev.type == SDL_EVENT_KEY_DOWN && ev.key.scancode == KB) { return true; } + if (ev.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN && pad != nullptr && ev.gbutton.which == PAD_ID && static_cast(ev.gbutton.button) == START_BTN) { + return true; + } + return false; + }); + } + // Separació vertical entre la primera i segona línia del títol. Una // alçada de línia és l'opció més llegible al canvas de 240 px sense // entrar a configuració extra; si demà cal afinar-ho, mou-se a @@ -75,22 +98,70 @@ void TitleScene::update(int delta_ms) { return; } - timer_ms_ -= delta_ms; - blink_elapsed_ms_ += delta_ms; if (heat_haze_) { heat_haze_->tick(delta_ms); } - const bool SKIPPED = std::ranges::any_of(EventBuffer::events, [](const SDL_Event& ev) { - return ev.type == SDL_EVENT_KEY_DOWN && !Placeholder::isReservedScancode(ev.key.scancode); - }); + // Avanç del comptador del parpadeig: durant STARTING, accelera per + // `starting_blink_speedup` (típicament 2.0). Es manté en enter perquè + // tota la lògica del cicle (visible_ms / hidden_ms) treballa amb int. + const float BLINK_RATE = (phase_ == Phase::STARTING) + ? UiConfig::get().titleStartingBlinkSpeedup() + : 1.0F; + blink_elapsed_ms_ += static_cast(static_cast(delta_ms) * BLINK_RATE); - if ((timer_ms_ <= 0 || SKIPPED) && next_factory_) { - next_scene_ = next_factory_(); + const bool P1_START = pressedStart(0); + const bool P2_START = pressedStart(1); + + switch (phase_) { + case Phase::IDLE: { + timer_ms_ -= delta_ms; + if (P1_START) { + enterStarting(DebugConfig::StartingPlayers::P1); + } else if (P2_START) { + enterStarting(DebugConfig::StartingPlayers::P2); + } else if (timer_ms_ <= 0) { + // Attract: ningú ha premut. Saltem a la següent escena + // amb BOTH per defecte. Demà, quan tinguem una DemoScene, + // ací anirà la transició a demo en lloc de a intro. + advanceTo(DebugConfig::StartingPlayers::BOTH); + } + break; + } + case Phase::STARTING: { + starting_timer_ms_ -= delta_ms; + // Si l'altre jugador prem START dins de la finestra, pugem + // a BOTH i resetegem el comptador. Pulsacions repetides del + // mateix jugador ja "starting" no fan res. + const bool OTHER_PRESSED = + (pending_players_ == DebugConfig::StartingPlayers::P1 && P2_START) || + (pending_players_ == DebugConfig::StartingPlayers::P2 && P1_START); + if (OTHER_PRESSED) { + pending_players_ = DebugConfig::StartingPlayers::BOTH; + starting_timer_ms_ = UiConfig::get().titleStartingMs(); + } + if (starting_timer_ms_ <= 0) { + advanceTo(pending_players_); + } + break; + } } Notifier::get().update(delta_ms); RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiConfig::get().renderInfoText(), UiConfig::get().renderInfoShadow())); } +void TitleScene::enterStarting(DebugConfig::StartingPlayers players) { + phase_ = Phase::STARTING; + pending_players_ = players; + starting_timer_ms_ = UiConfig::get().titleStartingMs(); + std::cout << "[scene] " << SCENE_NAME << " STARTING (players=" << DebugConfig::toString(players) << ")\n"; +} + +void TitleScene::advanceTo(DebugConfig::StartingPlayers players) { + if (next_factory_) { + next_scene_ = next_factory_(players); + } +} + void TitleScene::render() { const auto& ui_config = UiConfig::get(); const auto& palette = Resource::Cache::get().getPalette("ui"); diff --git a/source/game/scenes/title_scene.hpp b/source/game/scenes/title_scene.hpp index 062dcff..a040fab 100644 --- a/source/game/scenes/title_scene.hpp +++ b/source/game/scenes/title_scene.hpp @@ -1,11 +1,13 @@ #pragma once +#include #include #include #include #include "core/rendering/effects/heat_haze.hpp" #include "core/scene/scene.hpp" +#include "game/debug_config.hpp" class Surface; class Text; @@ -16,7 +18,17 @@ class Text; // data/ui.yaml secció `title:` per poder afinar-la sense recompilar. class TitleScene : public Scene { public: - using SceneFactory = std::function()>; + using SceneFactory = std::function(DebugConfig::StartingPlayers)>; + + // Fases de l'escena. + // - IDLE: animació atràctil. Cap tecla salta; només P1 START o + // P2 START fan transició. El timeout (`title.duration_ms`) + // salta a la següent escena amb BOTH (modus attract). + // - STARTING: comptador fix entre la primera pulsació de START i + // la transició. Si l'altre jugador prem START dins d'aquesta + // finestra, el comptador es reseteja amb `pending_players_=BOTH`. + enum class Phase : std::uint8_t { IDLE, + STARTING }; TitleScene(std::shared_ptr display_font, std::shared_ptr condensed_font, std::shared_ptr background, std::shared_ptr mini_logo, SceneFactory next_factory); ~TitleScene() override; @@ -25,6 +37,9 @@ class TitleScene : public Scene { void render() override; private: + void enterStarting(DebugConfig::StartingPlayers players); + void advanceTo(DebugConfig::StartingPlayers players); + std::shared_ptr display_font_; std::shared_ptr condensed_font_; std::shared_ptr background_; @@ -33,4 +48,7 @@ class TitleScene : public Scene { int timer_ms_; int blink_elapsed_ms_{0}; std::optional heat_haze_; + Phase phase_{Phase::IDLE}; + int starting_timer_ms_{0}; + DebugConfig::StartingPlayers pending_players_{DebugConfig::StartingPlayers::BOTH}; }; diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index 0ef540c..411f4d2 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -259,6 +259,13 @@ namespace Defaults { constexpr int BLINK_VISIBLE_MS = 800; constexpr int BLINK_HIDDEN_MS = 200; + // Fase STARTING: durada fixa entre la primera pulsació de + // START i la transició a la següent escena. Durant aquesta + // fase, el parpadeig del prompt va a `BLINK_SPEEDUP` × la + // freqüència normal (2.0 = doble; 1.5 = un 50% més ràpid). + constexpr int STARTING_MS = 3000; + constexpr float STARTING_BLINK_SPEEDUP = 2.0F; + // Distorsió sinusoidal horitzontal de l'imatge de fons // (efecte de calor del desert). `amplitude_px` és el // desplaçament màxim per fila; `wavelength_px` la distància