diff --git a/data/entities/player/morcus.yaml b/data/entities/player/morcus.yaml index d9e2b66..67dbf14 100644 --- a/data/entities/player/morcus.yaml +++ b/data/entities/player/morcus.yaml @@ -130,7 +130,7 @@ whip: # anar a negatius (no hi ha game over). death: respawn_delay_ms: 500 - lives: 3 + lives: 1 # Immunitat post-spawn: durant `duration_ms` ms, els hazards no # poden ferir ni matar el jugador (els forats sí que el maten — la diff --git a/data/entities/player/sam.yaml b/data/entities/player/sam.yaml index a1c20b0..c369bc6 100644 --- a/data/entities/player/sam.yaml +++ b/data/entities/player/sam.yaml @@ -130,7 +130,7 @@ whip: # anar a negatius (no hi ha game over). death: respawn_delay_ms: 500 - lives: 3 + lives: 1 # Immunitat post-spawn: durant `duration_ms` ms, els hazards no # poden ferir ni matar el jugador (els forats sí que el maten — la diff --git a/data/scenes/game/game.yaml b/data/scenes/game/game.yaml index 80a138e..1f11ad7 100644 --- a/data/scenes/game/game.yaml +++ b/data/scenes/game/game.yaml @@ -56,9 +56,12 @@ text_panel: game_over: # CONTINUE? — animació d'entrada i sortida idèntica al room text. 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_count_steps: 5 + continue_count_start: 9 # 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. game_over_text_hold_ms: 1500 diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index e0b3f2c..4c1611a 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -127,6 +127,11 @@ class Player : public AnimatedSprite { // Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor. 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); // Update en una sola crida: manté l'API anterior per als tests i diff --git a/source/game/scenes/game_over_fsm.cpp b/source/game/scenes/game_over_fsm.cpp index f55678b..8c21f48 100644 --- a/source/game/scenes/game_over_fsm.cpp +++ b/source/game/scenes/game_over_fsm.cpp @@ -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_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.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_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); @@ -112,10 +112,16 @@ namespace GameOverFsm { 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); + // La sèrie és [start, start-1, ..., 1, 0]: en total `start+1` + // 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; - return std::clamp(STEPS - CONSUMED, 0, STEPS); + return std::clamp(START - CONSUMED, 0, START); } auto stateName(State s) -> const char* { diff --git a/source/game/scenes/game_over_fsm.hpp b/source/game/scenes/game_over_fsm.hpp index 30f5176..c738bad 100644 --- a/source/game/scenes/game_over_fsm.hpp +++ b/source/game/scenes/game_over_fsm.hpp @@ -51,12 +51,16 @@ namespace GameOverFsm { struct Config { // CONTINUE? — animació d'entrada/sortida idèntica al room text. int continue_text_enter_ms{500}; - // Hold = duració total del countdown (es divideix en - // `continue_count_steps` graons d'1 segon visualment). - int continue_text_hold_ms{5000}; + // Hold = duració total del countdown. Es divideix en + // (continue_count_start + 1) graons iguals; el cas canònic és + // 10000 ms / 10 graons d'1 s cadascun. + int continue_text_hold_ms{10000}; int continue_text_exit_ms{500}; - // Quants "tics" mostra el countdown de CONTINUE?. 5 → "5,4,3,2,1". - int continue_count_steps{5}; + // Primer número visible al countdown. La sèrie és + // [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 // del CONTINUE?) i s'esvaeix amb la mateixa exit anim que el diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 5265354..a8317a5 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -456,10 +456,17 @@ GameScene::GameScene(Config config) auto hud_font_surface = Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY); auto hud_font = std::make_shared(hud_font_surface, UiConfig::get().hudFont().fnt); hud_font->setDefaultKerning(UiConfig::get().hudFont().kerning); - hud_ = std::make_unique(entity_manager_.getBus(), hud_font); - // Si hi ha P2 actiu, activem el seu bloc (per defecte el HUD naix amb - // només P1). Mentre els events del bus no porten `player_id`, els hits - // segueixen aplicant-se a P1 (veure TODO a Hud::onPlayerHit). + // Vides inicials del HUD = `initial_lives` del YAML del jugador + // (`Player::loadFrom` ja ha omplert `death_config` abans d'aquí). + // Sense això, el marcador es queda fixe a 3 i el YAML perd efecte. + hud_ = std::make_unique(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); } // Sincronització inicial dels comptadors de piràmide/sala del HUD amb // el LevelManager (després de cada `loadRoom` també es refresca; ací diff --git a/source/game/ui/hud.cpp b/source/game/ui/hud.cpp index 6070d59..455858c 100644 --- a/source/game/ui/hud.cpp +++ b/source/game/ui/hud.cpp @@ -69,9 +69,10 @@ namespace { } // namespace -Hud::Hud(EventBus& bus, std::shared_ptr font) +Hud::Hud(EventBus& bus, std::shared_ptr font, int initial_lives) : bus_(bus), - font_(std::move(font)) { + font_(std::move(font)), + initial_lives_(initial_lives) { // Geometria fixa dels tres blocs. Sumen exactament GameCanvas::WIDTH. // La durada de l'animació d'entrada ve de data/config/ui.yaml via UiConfig. const int ENTRY_MS = UiConfig::get().hudEntryMs(); @@ -103,10 +104,17 @@ Hud::Hud(EventBus& bus, std::shared_ptr font) .entry_offset_y = 0, }; - // Per defecte, P1 actiu (sempre n'hi ha al menys un); P2 inactiu fins - // que un segon Player s'uneix mitjançant `activatePlayer(2)`. - players_[0].active = true; + // Naixem amb tots dos slots inactius. El GameScene crida + // `activatePlayer(...)` per als que arrenquen en partida segons + // `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].lives = initial_lives_; // Subscripcions al bus de l'EntityManager. De moment només `player_hit` // 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; } auto& p = players_[static_cast(player_one_based - 1)]; p.active = true; - p.lives = DEFAULT_LIVES; + p.lives = initial_lives_; p.score = 0; p.treasures = 0; p.join_prompt = JoinPromptMode::NONE; diff --git a/source/game/ui/hud.hpp b/source/game/ui/hud.hpp index 62b09ac..c38220b 100644 --- a/source/game/ui/hud.hpp +++ b/source/game/ui/hud.hpp @@ -55,7 +55,11 @@ class Hud { CAN_JOIN, WAIT }; - Hud(EventBus& bus, std::shared_ptr 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 font, int initial_lives); ~Hud(); Hud(const Hud&) = delete; @@ -111,10 +115,6 @@ class Hud { static constexpr int LINE1_Y = 4; 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 { Mode mode{Mode::IDLE}; // Coordenades de pantalla on viu el bloc (constants durant la @@ -143,7 +143,7 @@ class Hud { }; struct PlayerStats { - int lives{DEFAULT_LIVES}; + int lives{0}; int score{0}; int treasures{0}; // `false` → el bloc mostra el prompt actiu (`join_prompt`) o @@ -179,4 +179,8 @@ class Hud { std::array players_; int pyramid_{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}; };