feat(title): FSM IDLE/STARTING amb START per jugador i propagació de StartingPlayers

This commit is contained in:
JailDesigner committed 2026-05-06 13:28:34 +02:00
1 parent 1fc3ab8e31
commit ca64115bfe
9 files changed
+143 -26

No files matched your search

+4 -3
View File
@@ -30,10 +30,11 @@ namespace {
} // namespace
IntroScene::IntroScene(std::shared_ptr<Text> font, SceneFactory next_factory)
IntroScene::IntroScene(std::shared_ptr<Text> 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);
+8 -2
View File
@@ -4,19 +4,24 @@
#include <memory>
#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<std::unique_ptr<Scene>()>;
using SceneFactory = std::function<std::unique_ptr<Scene>(DebugConfig::StartingPlayers)>;
static constexpr int DURATION_MS = 5000;
IntroScene(std::shared_ptr<Text> font, SceneFactory next_factory);
IntroScene(std::shared_ptr<Text> 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<Text> font_;
DebugConfig::StartingPlayers starting_players_;
SceneFactory next_factory_;
int timer_ms_{DURATION_MS};
};
+79 -8
View File
@@ -8,6 +8,8 @@
#include <string>
#include <utility>
#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<int>(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<int>(static_cast<float>(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");
+19 -1
View File
@@ -1,11 +1,13 @@
#pragma once
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#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<std::unique_ptr<Scene>()>;
using SceneFactory = std::function<std::unique_ptr<Scene>(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<Text> display_font, std::shared_ptr<Text> condensed_font, std::shared_ptr<Surface> background, std::shared_ptr<Surface> 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<Text> display_font_;
std::shared_ptr<Text> condensed_font_;
std::shared_ptr<Surface> background_;
@@ -33,4 +48,7 @@ class TitleScene : public Scene {
int timer_ms_;
int blink_elapsed_ms_{0};
std::optional<Effects::HeatHaze> heat_haze_;
Phase phase_{Phase::IDLE};
int starting_timer_ms_{0};
DebugConfig::StartingPlayers pending_players_{DebugConfig::StartingPlayers::BOTH};
};