From 486a440668c640514f9e3400c64bfce1f75b1ea2 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 8 May 2026 22:20:39 +0200 Subject: [PATCH] feat(game): skip de countdown amb JUMP/ATTACK quan numero <= 7 --- data/scenes/game/game.yaml | 6 +++ source/game/scenes/game_over_fsm.cpp | 15 ++++++++ source/game/scenes/game_over_fsm.hpp | 17 +++++++++ source/game/scenes/game_scene.cpp | 57 ++++++++++++++++++---------- source/game/scenes/game_scene.hpp | 12 ++++++ source/game/scenes/start_input.cpp | 27 +++++++++++++ source/game/scenes/start_input.hpp | 9 +++++ 7 files changed, 123 insertions(+), 20 deletions(-) diff --git a/data/scenes/game/game.yaml b/data/scenes/game/game.yaml index 811e875..5bb349b 100644 --- a/data/scenes/game/game.yaml +++ b/data/scenes/game/game.yaml @@ -85,3 +85,9 @@ game_over: # (al primer cop que es mostra `continue_count_start`, a cada canvi, # i quan apareix el 0 — però NO al saltar a GAME OVER). Buit = silenci. countdown_sound_key: "countdown.wav" + # Llindar a partir del qual qualsevol botó de salt o atac de + # qualsevol jugador avança al següent número del countdown. + # 7 → skip habilitat mentre el número visible és 7..1 (no a 0). + # -1 = mai. Útil per accelerar la decisió un cop la urgència ja + # és evident al jugador. + skip_below_or_equal: 7 diff --git a/source/game/scenes/game_over_fsm.cpp b/source/game/scenes/game_over_fsm.cpp index 9157ac0..db2e808 100644 --- a/source/game/scenes/game_over_fsm.cpp +++ b/source/game/scenes/game_over_fsm.cpp @@ -51,6 +51,7 @@ namespace GameOverFsm { if (n.contains("countdown_sound_key")) { cfg.countdown_sound_key = n["countdown_sound_key"].get_value(); } + cfg.skip_below_or_equal = readChildInt(n, "skip_below_or_equal", cfg.skip_below_or_equal); } Log::info("GameOverFsm", "timings carregats des de {} (continue hold {}ms, fade {}ms)", yaml_path, cfg.continue_text_hold_ms, cfg.fade_out_ms); @@ -118,6 +119,20 @@ namespace GameOverFsm { t.elapsed_ms = 0; } + void skipToNextCountdown(State current, Transition& t, const Config& cfg) { + if (current != State::CONTINUE_PROMPT) { return; } + if (t.phase != 1) { return; } + const int START = std::max(0, cfg.continue_count_start); + const int STEP_COUNT = START + 1; + const int STEP_MS = std::max(1, cfg.continue_text_hold_ms / STEP_COUNT); + const int CONSUMED = t.elapsed_ms / STEP_MS; + const int CURRENT = std::clamp(START - CONSUMED, 0, START); + // No saltem si ja som al 0: el següent "número" és la transició + // a GAME_OVER, que `advance` ja gestiona quan el hold s'esgota. + if (CURRENT <= 0) { return; } + t.elapsed_ms = (CONSUMED + 1) * STEP_MS; + } + auto countdownSecondsRemaining(State current, const Transition& t, const Config& cfg) -> int { if (current != State::CONTINUE_PROMPT) { return 0; } if (t.phase != 1) { return 0; } diff --git a/source/game/scenes/game_over_fsm.hpp b/source/game/scenes/game_over_fsm.hpp index ab23d59..1239340 100644 --- a/source/game/scenes/game_over_fsm.hpp +++ b/source/game/scenes/game_over_fsm.hpp @@ -92,6 +92,14 @@ namespace GameOverFsm { // canvi de número, i quan apareix el 0). NO sona en la // transició posterior cap a GAME_OVER. Buit = sense so. std::string countdown_sound_key{"countdown.wav"}; + + // Llindar a partir del qual qualsevol botó de salt o atac (de + // qualsevol jugador) salta al següent número del countdown. + // Exemple: 7 → mentre el número visible és 7, 6, 5, ..., 1 + // (no a 0, que ja no té "següent número") un botó d'acció + // avança al següent. Si el valor és més gran que `start` el + // skip queda desactivat efectivament. -1 = mai. + int skip_below_or_equal{7}; }; // Llegeix la configuració des del YAML. Si el fitxer manca o falta @@ -127,6 +135,15 @@ namespace GameOverFsm { // CONTINUE_PROMPT phase 1, retorna 0 (no s'hauria de pintar). [[nodiscard]] auto countdownSecondsRemaining(State current, const Transition& t, const Config& cfg) -> int; + // Salta al següent número del countdown (decrementa el valor + // visible en una unitat). Només actua dins de CONTINUE_PROMPT + // phase 1 i quan el valor actual és > 0 — al 0 ja no hi ha + // "següent número", la fase ha d'esgotar-se de manera natural i + // el caller es recolza en `advance` per al salt a GAME_OVER. La + // GameScene comprova també el llindar `skip_below_or_equal` + // abans de cridar; aquesta funció, per simplicitat, no en sap. + void skipToNextCountdown(State current, Transition& t, const Config& cfg); + // Nom textual per a logs i tests. [[nodiscard]] auto stateName(State s) -> const char*; diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 092ba2c..a59f496 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -1231,26 +1231,43 @@ void GameScene::updateGameOverFlow(int delta_ms) { } } - // Tick del so del countdown: cada vegada que el número visible - // canvia (incloent l'aparició del primer valor i del 0), reproduïm - // el so configurat. La detecció es fa post-`advance` perquè el - // canvi de fase 0 → 1 arribi a aparèixer com a transició -1 → start. - // En la transició cap a GAME_OVER el state ja no és CONTINUE_PROMPT - // i la branca s'omet — així no sona un "tick" extra al saltar. - if (game_over_state_ == GameOverFsm::State::CONTINUE_PROMPT) { - const int CURRENT = GameOverFsm::countdownSecondsRemaining( - game_over_state_, - game_over_transition_, - game_over_config_); - if (game_over_transition_.phase == 1 && CURRENT != last_countdown_value_) { - last_countdown_value_ = CURRENT; - const auto& key = game_over_config_.countdown_sound_key; - if (!key.empty()) { - if (auto* audio = Audio::get(); audio != nullptr) { - audio->playSound(key, Audio::Group::INTERFACE); - } - } - } + // Skip i tick del so del countdown viuen en helpers per acotar + // la complexitat cognitiva d'aquesta funció. Skip primer perquè + // el tick del so soni amb el valor nou si la pulsació ha avançat. + handleCountdownSkip(); + emitCountdownTickIfChanged(); +} + +void GameScene::handleCountdownSkip() { + if (game_over_state_ != GameOverFsm::State::CONTINUE_PROMPT) { return; } + if (game_over_transition_.phase != 1) { return; } + const int CURRENT = GameOverFsm::countdownSecondsRemaining( + game_over_state_, + game_over_transition_, + game_over_config_); + if (CURRENT <= 0) { return; } + if (CURRENT > game_over_config_.skip_below_or_equal) { return; } + const bool ANY_ACTION = Input::Detect::pressedActionButton(0) || Input::Detect::pressedActionButton(1); + if (!ANY_ACTION) { return; } + GameOverFsm::skipToNextCountdown( + game_over_state_, + game_over_transition_, + game_over_config_); +} + +void GameScene::emitCountdownTickIfChanged() { + if (game_over_state_ != GameOverFsm::State::CONTINUE_PROMPT) { return; } + if (game_over_transition_.phase != 1) { return; } + const int CURRENT = GameOverFsm::countdownSecondsRemaining( + game_over_state_, + game_over_transition_, + game_over_config_); + if (CURRENT == last_countdown_value_) { return; } + last_countdown_value_ = CURRENT; + const auto& key = game_over_config_.countdown_sound_key; + if (key.empty()) { return; } + if (auto* audio = Audio::get(); audio != nullptr) { + audio->playSound(key, Audio::Group::INTERFACE); } } diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index 8cf5341..95ec21f 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -341,6 +341,18 @@ class GameScene : public Scene { // marcador. void renderGameOverFade(); + // Comprova si dins de CONTINUE_PROMPT phase 1 el número visible + // està al rang de skip i, si un jugador pulsa salt o atac, fa + // saltar la fase al següent número via `GameOverFsm::skipToNextCountdown`. + // Extret a mètode separat per acotar la complexitat cognitiva + // d'`updateGameOverFlow`. + void handleCountdownSkip(); + + // Detecta canvis de número del countdown i dispara el so + // configurat un cop per cada nou valor. També extret per + // mantenir `updateGameOverFlow` per sota del llindar de tidy. + void emitCountdownTickIfChanged(); + // Helpers d'animació del rètol de transició. Reutilitzats pel // rètol de sala (`renderRoomText`) i pel de game over // (`renderGameOverText`). Reben `phase` i `elapsed_ms` per diff --git a/source/game/scenes/start_input.cpp b/source/game/scenes/start_input.cpp index 3b047b4..1fc82e3 100644 --- a/source/game/scenes/start_input.cpp +++ b/source/game/scenes/start_input.cpp @@ -29,4 +29,31 @@ namespace Input::Detect { }); } + auto pressedActionButton(int player_id) -> bool { + // Teclat: les bindings actives venen d'`Options::controls()` (les + // que el menú de servei pot reescriure), no de `Defaults` cru — + // així respectem el remapping de l'usuari igual que fa el Player. + const auto& kb = (player_id == 0) ? Options::controls().keyboard_p1 + : Options::controls().keyboard_p2; + const SDL_Scancode KB_JUMP = kb.jump; + const SDL_Scancode KB_ATTACK = kb.attack; + 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 JUMP_BTN = GamepadBindings::get().getButton(slot.name, slot.path, GamepadBindings::Action::JUMP); + const int ATTACK_BTN = GamepadBindings::get().getButton(slot.name, slot.path, GamepadBindings::Action::ATTACK); + + return std::ranges::any_of(EventBuffer::events, [&](const SDL_Event& ev) { + if (ev.type == SDL_EVENT_KEY_DOWN && (ev.key.scancode == KB_JUMP || ev.key.scancode == KB_ATTACK)) { + return true; + } + if (ev.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN && pad != nullptr && ev.gbutton.which == PAD_ID) { + const int BTN = static_cast(ev.gbutton.button); + if (BTN == JUMP_BTN || BTN == ATTACK_BTN) { return true; } + } + return false; + }); + } + } // namespace Input::Detect diff --git a/source/game/scenes/start_input.hpp b/source/game/scenes/start_input.hpp index 42fa0eb..5fce6fb 100644 --- a/source/game/scenes/start_input.hpp +++ b/source/game/scenes/start_input.hpp @@ -16,4 +16,13 @@ namespace Input::Detect { // assignat al jugador via `Options::controls()`). [[nodiscard]] auto pressedStart(int player_id) -> bool; + // Cert si dins dels events SDL d'aquest frame el jugador + // `player_id` (0 = P1, 1 = P2) ha pulsat el botó de salt o el + // d'atac (qualsevol dels dos). Usat per la GameScene per acceptar + // qualsevol "botó d'acció" com a senyal de skip del countdown de + // CONTINUE? un cop el número visible cau a un llindar configurable. + // Mateix patró que `pressedStart`: combina teclat (Defaults::Keys) + // i gamepad (GamepadBindings). + [[nodiscard]] auto pressedActionButton(int player_id) -> bool; + } // namespace Input::Detect