feat(game): CONTINUE? + GAME OVER + fade-to-LOGO (Fase 2)
Sub-FSM nova `GameOverFsm` (CONTINUE_PROMPT → GAME_OVER → FADE_OUT → DONE) que viu en paral·lel a la `RoomFsm`. Es dispara quan tots els jugadors queden DISABLED durant PLAYING, mostra el rètol "Continuar?" amb countdown reusant el panel del room text, i si ningú pulsa START acaba en "Fi del Joc" + fade negre i sol·licita la transició a LOGO. Pulsar START durant CONTINUE? cancel·la el countdown i fa join (revive) del jugador. Després de GAME OVER, el HUD passa a "ESPERA / PER JUGAR" i el START queda bloquejat fins a la transició d'escena. Els helpers d'animació del room text (X i altura del panel) ara reben phase + elapsed_ms per paràmetre per poder ser reaprofitats per les dues sub-FSMs sense duplicar la lògica visual. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c13b11c501
commit
78e164fce4
8 files changed
+562
-43
No files matched your search
@@ -0,0 +1,137 @@
|
||||
#include "game/scenes/game_over_fsm.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "utils/log.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Llegeix un sub-camp sencer d'un node YAML. Si manca, retorna el
|
||||
// fallback. Mateix patró que `RoomFsm::loadConfig`.
|
||||
auto readChildInt(const fkyaml::node& parent, const char* key, int fallback) -> int {
|
||||
if (!parent.contains(key)) { return fallback; }
|
||||
return parent[key].get_value<int>();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace GameOverFsm {
|
||||
|
||||
auto loadConfig(const std::string& yaml_path) -> Config {
|
||||
Config cfg{};
|
||||
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||
if (bytes.empty()) {
|
||||
Log::warn("GameOverFsm", "{} absent — uso fallbacks compilats", yaml_path);
|
||||
return cfg;
|
||||
}
|
||||
const auto* begin = reinterpret_cast<const char*>(bytes.data());
|
||||
const auto* end = begin + bytes.size();
|
||||
auto yaml = fkyaml::node::deserialize(begin, end);
|
||||
|
||||
if (yaml.contains("game_over")) {
|
||||
const auto& n = yaml["game_over"];
|
||||
cfg.continue_text_enter_ms = readChildInt(n, "continue_text_enter_ms", cfg.continue_text_enter_ms);
|
||||
cfg.continue_text_hold_ms = readChildInt(n, "continue_text_hold_ms", cfg.continue_text_hold_ms);
|
||||
cfg.continue_text_exit_ms = readChildInt(n, "continue_text_exit_ms", cfg.continue_text_exit_ms);
|
||||
cfg.continue_count_steps = readChildInt(n, "continue_count_steps", cfg.continue_count_steps);
|
||||
cfg.game_over_text_hold_ms = readChildInt(n, "game_over_text_hold_ms", cfg.game_over_text_hold_ms);
|
||||
cfg.game_over_text_exit_ms = readChildInt(n, "game_over_text_exit_ms", cfg.game_over_text_exit_ms);
|
||||
cfg.game_over_wait_ms = readChildInt(n, "game_over_wait_ms", cfg.game_over_wait_ms);
|
||||
cfg.fade_out_ms = readChildInt(n, "fade_out_ms", cfg.fade_out_ms);
|
||||
cfg.black_hold_ms = readChildInt(n, "black_hold_ms", cfg.black_hold_ms);
|
||||
}
|
||||
|
||||
Log::info("GameOverFsm", "timings carregats des de {} (continue hold {}ms, fade {}ms)", yaml_path, cfg.continue_text_hold_ms, cfg.fade_out_ms);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
auto advance(State current, Transition& t, const Config& cfg, int delta_ms) -> State {
|
||||
if (current == State::OFF || current == State::DONE) { return current; }
|
||||
|
||||
t.elapsed_ms += delta_ms;
|
||||
|
||||
if (current == State::CONTINUE_PROMPT) {
|
||||
const std::array<int, 3> DURS = {
|
||||
cfg.continue_text_enter_ms,
|
||||
cfg.continue_text_hold_ms,
|
||||
cfg.continue_text_exit_ms,
|
||||
};
|
||||
while (t.phase < static_cast<int>(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) {
|
||||
t.elapsed_ms -= DURS[t.phase];
|
||||
// Si acabem la fase 1 (hold) sense cancel·lació, saltem
|
||||
// directament a GAME_OVER — el panel queda amunt i no
|
||||
// es reprodueix l'exit anim. La fase 2 (exit) només
|
||||
// s'executa si el GameScene l'ha forçada amb
|
||||
// `cancelContinuePrompt`.
|
||||
if (t.phase == 1) {
|
||||
return State::GAME_OVER;
|
||||
}
|
||||
++t.phase;
|
||||
}
|
||||
if (t.phase >= static_cast<int>(DURS.size())) { return State::OFF; }
|
||||
return current;
|
||||
}
|
||||
|
||||
if (current == State::GAME_OVER) {
|
||||
const std::array<int, 3> DURS = {
|
||||
cfg.game_over_text_hold_ms,
|
||||
cfg.game_over_text_exit_ms,
|
||||
cfg.game_over_wait_ms,
|
||||
};
|
||||
while (t.phase < static_cast<int>(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) {
|
||||
t.elapsed_ms -= DURS[t.phase];
|
||||
++t.phase;
|
||||
}
|
||||
if (t.phase >= static_cast<int>(DURS.size())) { return State::FADE_OUT; }
|
||||
return current;
|
||||
}
|
||||
|
||||
// FADE_OUT
|
||||
const std::array<int, 2> DURS = {
|
||||
cfg.fade_out_ms,
|
||||
cfg.black_hold_ms,
|
||||
};
|
||||
while (t.phase < static_cast<int>(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) {
|
||||
t.elapsed_ms -= DURS[t.phase];
|
||||
++t.phase;
|
||||
}
|
||||
if (t.phase >= static_cast<int>(DURS.size())) { return State::DONE; }
|
||||
return current;
|
||||
}
|
||||
|
||||
void cancelContinuePrompt(State current, Transition& t) {
|
||||
if (current != State::CONTINUE_PROMPT) { return; }
|
||||
if (t.phase >= 2) { return; } // ja en exit anim, no-op
|
||||
t.phase = 2;
|
||||
t.elapsed_ms = 0;
|
||||
}
|
||||
|
||||
auto countdownSecondsRemaining(State current, const Transition& t, const Config& cfg) -> int {
|
||||
if (current != State::CONTINUE_PROMPT) { return 0; }
|
||||
if (t.phase != 1) { return 0; }
|
||||
const int STEPS = std::max(1, cfg.continue_count_steps);
|
||||
const int STEP_MS = std::max(1, cfg.continue_text_hold_ms / STEPS);
|
||||
const int CONSUMED = t.elapsed_ms / STEP_MS;
|
||||
return std::clamp(STEPS - CONSUMED, 0, STEPS);
|
||||
}
|
||||
|
||||
auto stateName(State s) -> const char* {
|
||||
switch (s) {
|
||||
case State::OFF:
|
||||
return "OFF";
|
||||
case State::CONTINUE_PROMPT:
|
||||
return "CONTINUE_PROMPT";
|
||||
case State::GAME_OVER:
|
||||
return "GAME_OVER";
|
||||
case State::FADE_OUT:
|
||||
return "FADE_OUT";
|
||||
case State::DONE:
|
||||
return "DONE";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
} // namespace GameOverFsm
|
||||
Reference in new issue
Block a user