fix(game): HUD lives desde YAML, prompt P1 en single-P2, countdown 9..0 configurable
This commit is contained in:
1 parent
78e164fce4
commit
5e862ceadc
9 files changed
+66
-29
No files matched your search
@@ -130,7 +130,7 @@ whip:
|
|||||||
# anar a negatius (no hi ha game over).
|
# anar a negatius (no hi ha game over).
|
||||||
death:
|
death:
|
||||||
respawn_delay_ms: 500
|
respawn_delay_ms: 500
|
||||||
lives: 3
|
lives: 1
|
||||||
|
|
||||||
# Immunitat post-spawn: durant `duration_ms` ms, els hazards no
|
# Immunitat post-spawn: durant `duration_ms` ms, els hazards no
|
||||||
# poden ferir ni matar el jugador (els forats sí que el maten — la
|
# poden ferir ni matar el jugador (els forats sí que el maten — la
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ whip:
|
|||||||
# anar a negatius (no hi ha game over).
|
# anar a negatius (no hi ha game over).
|
||||||
death:
|
death:
|
||||||
respawn_delay_ms: 500
|
respawn_delay_ms: 500
|
||||||
lives: 3
|
lives: 1
|
||||||
|
|
||||||
# Immunitat post-spawn: durant `duration_ms` ms, els hazards no
|
# Immunitat post-spawn: durant `duration_ms` ms, els hazards no
|
||||||
# poden ferir ni matar el jugador (els forats sí que el maten — la
|
# poden ferir ni matar el jugador (els forats sí que el maten — la
|
||||||
|
|||||||
@@ -56,9 +56,12 @@ text_panel:
|
|||||||
game_over:
|
game_over:
|
||||||
# CONTINUE? — animació d'entrada i sortida idèntica al room text.
|
# CONTINUE? — animació d'entrada i sortida idèntica al room text.
|
||||||
continue_text_enter_ms: 400
|
continue_text_enter_ms: 400
|
||||||
continue_text_hold_ms: 5000 # 5 s = `continue_count_steps` graons d'1 s
|
# Hold del countdown: 10 s = (continue_count_start + 1) graons d'1 s.
|
||||||
|
# La sèrie visible és [continue_count_start, ..., 1, 0]; el "0" es
|
||||||
|
# manté l'últim segon abans del salt a GAME OVER.
|
||||||
|
continue_text_hold_ms: 10000
|
||||||
continue_text_exit_ms: 400
|
continue_text_exit_ms: 400
|
||||||
continue_count_steps: 5
|
continue_count_start: 9
|
||||||
# GAME OVER — apareix sense enter anim (panel ja amunt del CONTINUE?),
|
# GAME OVER — apareix sense enter anim (panel ja amunt del CONTINUE?),
|
||||||
# es manté un temps i marxa amb la mateixa exit anim del room text.
|
# es manté un temps i marxa amb la mateixa exit anim del room text.
|
||||||
game_over_text_hold_ms: 1500
|
game_over_text_hold_ms: 1500
|
||||||
|
|||||||
@@ -127,6 +127,11 @@ class Player : public AnimatedSprite {
|
|||||||
// Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor.
|
// Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor.
|
||||||
static void loadFrom(const std::string& yaml_path);
|
static void loadFrom(const std::string& yaml_path);
|
||||||
|
|
||||||
|
// Vides amb què arrenca un jugador (camp `lives:` del YAML del
|
||||||
|
// jugador). El consumeix el HUD per inicialitzar el comptador del
|
||||||
|
// marcador i `Player::resetForJoin` per a re-entrades mid-partida.
|
||||||
|
[[nodiscard]] static auto getInitialLives() -> int { return death_config.initial_lives; }
|
||||||
|
|
||||||
Player(const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1);
|
Player(const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1);
|
||||||
|
|
||||||
// Update en una sola crida: manté l'API anterior per als tests i
|
// Update en una sola crida: manté l'API anterior per als tests i
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ namespace GameOverFsm {
|
|||||||
cfg.continue_text_enter_ms = readChildInt(n, "continue_text_enter_ms", cfg.continue_text_enter_ms);
|
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_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_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.continue_count_start = readChildInt(n, "continue_count_start", cfg.continue_count_start);
|
||||||
cfg.game_over_text_hold_ms = readChildInt(n, "game_over_text_hold_ms", cfg.game_over_text_hold_ms);
|
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_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.game_over_wait_ms = readChildInt(n, "game_over_wait_ms", cfg.game_over_wait_ms);
|
||||||
@@ -112,10 +112,16 @@ namespace GameOverFsm {
|
|||||||
auto countdownSecondsRemaining(State current, const Transition& t, const Config& cfg) -> int {
|
auto countdownSecondsRemaining(State current, const Transition& t, const Config& cfg) -> int {
|
||||||
if (current != State::CONTINUE_PROMPT) { return 0; }
|
if (current != State::CONTINUE_PROMPT) { return 0; }
|
||||||
if (t.phase != 1) { return 0; }
|
if (t.phase != 1) { return 0; }
|
||||||
const int STEPS = std::max(1, cfg.continue_count_steps);
|
// La sèrie és [start, start-1, ..., 1, 0]: en total `start+1`
|
||||||
const int STEP_MS = std::max(1, cfg.continue_text_hold_ms / STEPS);
|
// graons. El primer número visible és `start` i l'últim 0 es
|
||||||
|
// manté durant el seu graó abans que la fase 1 acabi i la FSM
|
||||||
|
// salti a GAME_OVER. Si `start == 0` només es mostra "0"
|
||||||
|
// durant tot el hold.
|
||||||
|
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 CONSUMED = t.elapsed_ms / STEP_MS;
|
||||||
return std::clamp(STEPS - CONSUMED, 0, STEPS);
|
return std::clamp(START - CONSUMED, 0, START);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto stateName(State s) -> const char* {
|
auto stateName(State s) -> const char* {
|
||||||
|
|||||||
@@ -51,12 +51,16 @@ namespace GameOverFsm {
|
|||||||
struct Config {
|
struct Config {
|
||||||
// CONTINUE? — animació d'entrada/sortida idèntica al room text.
|
// CONTINUE? — animació d'entrada/sortida idèntica al room text.
|
||||||
int continue_text_enter_ms{500};
|
int continue_text_enter_ms{500};
|
||||||
// Hold = duració total del countdown (es divideix en
|
// Hold = duració total del countdown. Es divideix en
|
||||||
// `continue_count_steps` graons d'1 segon visualment).
|
// (continue_count_start + 1) graons iguals; el cas canònic és
|
||||||
int continue_text_hold_ms{5000};
|
// 10000 ms / 10 graons d'1 s cadascun.
|
||||||
|
int continue_text_hold_ms{10000};
|
||||||
int continue_text_exit_ms{500};
|
int continue_text_exit_ms{500};
|
||||||
// Quants "tics" mostra el countdown de CONTINUE?. 5 → "5,4,3,2,1".
|
// Primer número visible al countdown. La sèrie és
|
||||||
int continue_count_steps{5};
|
// [continue_count_start, ..., 1, 0] — sempre acaba en 0 i el
|
||||||
|
// valor 0 es manté visible durant l'últim graó abans del salt
|
||||||
|
// a GAME_OVER. 9 → "9, 8, ..., 0" (10 graons).
|
||||||
|
int continue_count_start{9};
|
||||||
|
|
||||||
// GAME OVER — apareix sense enter anim (el panel ja és amunt
|
// GAME OVER — apareix sense enter anim (el panel ja és amunt
|
||||||
// del CONTINUE?) i s'esvaeix amb la mateixa exit anim que el
|
// del CONTINUE?) i s'esvaeix amb la mateixa exit anim que el
|
||||||
|
|||||||
@@ -456,10 +456,17 @@ GameScene::GameScene(Config config)
|
|||||||
auto hud_font_surface = Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY);
|
auto hud_font_surface = Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY);
|
||||||
auto hud_font = std::make_shared<Text>(hud_font_surface, UiConfig::get().hudFont().fnt);
|
auto hud_font = std::make_shared<Text>(hud_font_surface, UiConfig::get().hudFont().fnt);
|
||||||
hud_font->setDefaultKerning(UiConfig::get().hudFont().kerning);
|
hud_font->setDefaultKerning(UiConfig::get().hudFont().kerning);
|
||||||
hud_ = std::make_unique<Hud>(entity_manager_.getBus(), hud_font);
|
// Vides inicials del HUD = `initial_lives` del YAML del jugador
|
||||||
// Si hi ha P2 actiu, activem el seu bloc (per defecte el HUD naix amb
|
// (`Player::loadFrom` ja ha omplert `death_config` abans d'aquí).
|
||||||
// només P1). Mentre els events del bus no porten `player_id`, els hits
|
// Sense això, el marcador es queda fixe a 3 i el YAML perd efecte.
|
||||||
// segueixen aplicant-se a P1 (veure TODO a Hud::onPlayerHit).
|
hud_ = std::make_unique<Hud>(entity_manager_.getBus(), hud_font, Player::getInitialLives());
|
||||||
|
// Activem el bloc HUD per als slots que arranquen en partida. Els
|
||||||
|
// inactius queden mostrant el prompt "POLSA START / PER JUGAR" via
|
||||||
|
// `refreshJoinPrompt` cada frame. Tots dos s'haurien de poder
|
||||||
|
// activar (cas BOTH) o un dels dos (cas P1 o P2 sols) — el HUD
|
||||||
|
// naix amb tots dos slots inactius i és aquesta crida la que dóna
|
||||||
|
// l'estat real.
|
||||||
|
if (USE_P1) { hud_->activatePlayer(1); }
|
||||||
if (USE_P2) { hud_->activatePlayer(2); }
|
if (USE_P2) { hud_->activatePlayer(2); }
|
||||||
// Sincronització inicial dels comptadors de piràmide/sala del HUD amb
|
// Sincronització inicial dels comptadors de piràmide/sala del HUD amb
|
||||||
// el LevelManager (després de cada `loadRoom` també es refresca; ací
|
// el LevelManager (després de cada `loadRoom` també es refresca; ací
|
||||||
|
|||||||
+14
-6
@@ -69,9 +69,10 @@ namespace {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Hud::Hud(EventBus& bus, std::shared_ptr<Text> font)
|
Hud::Hud(EventBus& bus, std::shared_ptr<Text> font, int initial_lives)
|
||||||
: bus_(bus),
|
: bus_(bus),
|
||||||
font_(std::move(font)) {
|
font_(std::move(font)),
|
||||||
|
initial_lives_(initial_lives) {
|
||||||
// Geometria fixa dels tres blocs. Sumen exactament GameCanvas::WIDTH.
|
// Geometria fixa dels tres blocs. Sumen exactament GameCanvas::WIDTH.
|
||||||
// La durada de l'animació d'entrada ve de data/config/ui.yaml via UiConfig.
|
// La durada de l'animació d'entrada ve de data/config/ui.yaml via UiConfig.
|
||||||
const int ENTRY_MS = UiConfig::get().hudEntryMs();
|
const int ENTRY_MS = UiConfig::get().hudEntryMs();
|
||||||
@@ -103,10 +104,17 @@ Hud::Hud(EventBus& bus, std::shared_ptr<Text> font)
|
|||||||
.entry_offset_y = 0,
|
.entry_offset_y = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Per defecte, P1 actiu (sempre n'hi ha al menys un); P2 inactiu fins
|
// Naixem amb tots dos slots inactius. El GameScene crida
|
||||||
// que un segon Player s'uneix mitjançant `activatePlayer(2)`.
|
// `activatePlayer(...)` per als que arrenquen en partida segons
|
||||||
players_[0].active = true;
|
// `starting_players` (un, l'altre, o tots dos). Els no activats
|
||||||
|
// mostraran el prompt "POLSA START / PER JUGAR" via `setJoinPrompt`
|
||||||
|
// fins que el jugador faci join. Les vides s'inicialitzen al valor
|
||||||
|
// del YAML del jugador — sense això, canviar `lives:` no es
|
||||||
|
// reflectiria al marcador.
|
||||||
|
players_[0].active = false;
|
||||||
|
players_[0].lives = initial_lives_;
|
||||||
players_[1].active = false;
|
players_[1].active = false;
|
||||||
|
players_[1].lives = initial_lives_;
|
||||||
|
|
||||||
// Subscripcions al bus de l'EntityManager. De moment només `player_hit`
|
// Subscripcions al bus de l'EntityManager. De moment només `player_hit`
|
||||||
// afecta el HUD visualment: decrementa vides, dispara flash i shake al
|
// afecta el HUD visualment: decrementa vides, dispara flash i shake al
|
||||||
@@ -176,7 +184,7 @@ void Hud::activatePlayer(int player_one_based) {
|
|||||||
if (player_one_based < 1 || player_one_based > 2) { return; }
|
if (player_one_based < 1 || player_one_based > 2) { return; }
|
||||||
auto& p = players_[static_cast<std::size_t>(player_one_based - 1)];
|
auto& p = players_[static_cast<std::size_t>(player_one_based - 1)];
|
||||||
p.active = true;
|
p.active = true;
|
||||||
p.lives = DEFAULT_LIVES;
|
p.lives = initial_lives_;
|
||||||
p.score = 0;
|
p.score = 0;
|
||||||
p.treasures = 0;
|
p.treasures = 0;
|
||||||
p.join_prompt = JoinPromptMode::NONE;
|
p.join_prompt = JoinPromptMode::NONE;
|
||||||
|
|||||||
+10
-6
@@ -55,7 +55,11 @@ class Hud {
|
|||||||
CAN_JOIN,
|
CAN_JOIN,
|
||||||
WAIT };
|
WAIT };
|
||||||
|
|
||||||
Hud(EventBus& bus, std::shared_ptr<Text> font);
|
// `initial_lives` es la cantidad de vides amb què s'activa un slot
|
||||||
|
// (P1 al ctor i, més tard, els que arriben via "join in progress").
|
||||||
|
// Ve del YAML del jugador (`Player::death_config.initial_lives`),
|
||||||
|
// així el marcador segueix automàticament la configuració del joc.
|
||||||
|
Hud(EventBus& bus, std::shared_ptr<Text> font, int initial_lives);
|
||||||
~Hud();
|
~Hud();
|
||||||
|
|
||||||
Hud(const Hud&) = delete;
|
Hud(const Hud&) = delete;
|
||||||
@@ -111,10 +115,6 @@ class Hud {
|
|||||||
static constexpr int LINE1_Y = 4;
|
static constexpr int LINE1_Y = 4;
|
||||||
static constexpr int LINE2_Y = 13;
|
static constexpr int LINE2_Y = 13;
|
||||||
|
|
||||||
// Vides inicials per jugador (placeholder fins que GameScene ho
|
|
||||||
// configure des del Config del nivell).
|
|
||||||
static constexpr int DEFAULT_LIVES = 3;
|
|
||||||
|
|
||||||
struct Block {
|
struct Block {
|
||||||
Mode mode{Mode::IDLE};
|
Mode mode{Mode::IDLE};
|
||||||
// Coordenades de pantalla on viu el bloc (constants durant la
|
// Coordenades de pantalla on viu el bloc (constants durant la
|
||||||
@@ -143,7 +143,7 @@ class Hud {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct PlayerStats {
|
struct PlayerStats {
|
||||||
int lives{DEFAULT_LIVES};
|
int lives{0};
|
||||||
int score{0};
|
int score{0};
|
||||||
int treasures{0};
|
int treasures{0};
|
||||||
// `false` → el bloc mostra el prompt actiu (`join_prompt`) o
|
// `false` → el bloc mostra el prompt actiu (`join_prompt`) o
|
||||||
@@ -179,4 +179,8 @@ class Hud {
|
|||||||
std::array<PlayerStats, 2> players_;
|
std::array<PlayerStats, 2> players_;
|
||||||
int pyramid_{1};
|
int pyramid_{1};
|
||||||
int room_{1};
|
int room_{1};
|
||||||
|
// Vides amb què s'arrenca un slot (`activatePlayer`). Cachejat al
|
||||||
|
// ctor des de `Player::death_config.initial_lives` per no llegir el
|
||||||
|
// singleton estàtic en el camí calent del bus de events.
|
||||||
|
int initial_lives_{0};
|
||||||
};
|
};
|
||||||
Reference in new issue
Block a user