feat(player): join in progress + DISABLED en agotar vidas (Fase 1)
Habilita la regla 1 de la nova FSM de joc: durant PLAYING, qualsevol
slot DISABLED pot pulsar START per entrar a partida amb spawn aeri al
seu costat de la porta i vides reseteades. Quan un jugador esgota les
vides, `respawnFromDying` el deixa a DISABLED i publica `player_disabled`
al bus; GameScene i HUD reaccionen, deixant el slot disponible per a un
nou join. Afegeix prompts al HUD ("PULSA START / PER JUGAR" o
"ESPERA / PER JUGAR") segons l'estat de la RoomFsm.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8921b9fd0c
commit
c13b11c501
12 files changed
+325
-56
No files matched your search
@@ -113,6 +113,7 @@ set(APP_SOURCES
|
|||||||
source/game/ui/service_menu.cpp
|
source/game/ui/service_menu.cpp
|
||||||
source/game/ui/hud.cpp
|
source/game/ui/hud.cpp
|
||||||
source/game/scenes/placeholder_input.cpp
|
source/game/scenes/placeholder_input.cpp
|
||||||
|
source/game/scenes/start_input.cpp
|
||||||
source/game/scenes/logo_scene.cpp
|
source/game/scenes/logo_scene.cpp
|
||||||
source/game/scenes/title_scene.cpp
|
source/game/scenes/title_scene.cpp
|
||||||
source/game/scenes/intro_scene.cpp
|
source/game/scenes/intro_scene.cpp
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ hud:
|
|||||||
treasures: "Tres"
|
treasures: "Tres"
|
||||||
pyramid: "Piramide"
|
pyramid: "Piramide"
|
||||||
room: "Sala"
|
room: "Sala"
|
||||||
|
# Prompts mostrats al bloc d'un slot inactiu (en lloc dels zeros)
|
||||||
|
# quan un jugador encara no ha pulsat START o ha esgotat vides.
|
||||||
|
# Dues línies per fer-los caber al bloc lateral (~106 px).
|
||||||
|
join_prompt_can_join_l1: "POLSA START"
|
||||||
|
join_prompt_can_join_l2: "PER JUGAR"
|
||||||
|
join_prompt_wait_l1: "ESPERA"
|
||||||
|
join_prompt_wait_l2: "PER JUGAR"
|
||||||
|
|
||||||
# Textos de l'escena TITLE.
|
# Textos de l'escena TITLE.
|
||||||
title:
|
title:
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ hud:
|
|||||||
treasures: "Tre"
|
treasures: "Tre"
|
||||||
pyramid: "Pyramid"
|
pyramid: "Pyramid"
|
||||||
room: "Room"
|
room: "Room"
|
||||||
|
# Prompts shown in an inactive slot's block (instead of zeros) while
|
||||||
|
# a player hasn't pressed START yet or has run out of lives. Two
|
||||||
|
# short lines so they fit in the side block (~106 px wide).
|
||||||
|
join_prompt_can_join_l1: "PRESS START"
|
||||||
|
join_prompt_can_join_l2: "TO PLAY"
|
||||||
|
join_prompt_wait_l1: "PLEASE"
|
||||||
|
join_prompt_wait_l2: "WAIT"
|
||||||
|
|
||||||
# Texts of the TITLE scene.
|
# Texts of the TITLE scene.
|
||||||
title:
|
title:
|
||||||
|
|||||||
@@ -1270,11 +1270,30 @@ void Player::applyDyingMovement(float delta_time) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Player::resetForJoin() {
|
||||||
|
lives_remaining_ = death_config.initial_lives;
|
||||||
|
}
|
||||||
|
|
||||||
void Player::respawnFromDying() {
|
void Player::respawnFromDying() {
|
||||||
// Vides: decreix sempre. Pot anar a negatius — no hi ha game over.
|
// Vides: decreix una unitat. Si el comptador arriba a 0 el jugador
|
||||||
|
// surt de partida (DISABLED) i la GameScene/HUD reaccionen via bus
|
||||||
|
// — això habilita el flux de "join in progress" (l'altre jugador
|
||||||
|
// pot pulsar START per entrar) i obrirà el camí al CONTINUE? +
|
||||||
|
// GAME OVER en una fase posterior.
|
||||||
--lives_remaining_;
|
--lives_remaining_;
|
||||||
death_animation_complete_ = false;
|
death_animation_complete_ = false;
|
||||||
respawn_remaining_ms_ = 0.0F;
|
respawn_remaining_ms_ = 0.0F;
|
||||||
|
if (lives_remaining_ <= 0) {
|
||||||
|
disable();
|
||||||
|
if (bus_ != nullptr) {
|
||||||
|
bus_->publish(Event{
|
||||||
|
.name = "player_disabled",
|
||||||
|
.payload = std::monostate{},
|
||||||
|
.player_id = id_,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Cinemàtica idèntica a la d'INTRO de sala: el jugador cau des de
|
// Cinemàtica idèntica a la d'INTRO de sala: el jugador cau des de
|
||||||
// dalt fins al passadís d'spawn (`spawnFromAir` reseteja primer i
|
// dalt fins al passadís d'spawn (`spawnFromAir` reseteja primer i
|
||||||
// després posa AIRBORNE amb l'altura derivada de la geometria).
|
// després posa AIRBORNE amb l'altura derivada de la geometria).
|
||||||
|
|||||||
@@ -296,8 +296,11 @@ class Player : public AnimatedSprite {
|
|||||||
// `death_spinning`, posició congelada, input ignorat. Idempotent —
|
// `death_spinning`, posició congelada, input ignorat. Idempotent —
|
||||||
// si ja està DYING, EXITED o DISABLED, és no-op (un segon toc de
|
// si ja està DYING, EXITED o DISABLED, és no-op (un segon toc de
|
||||||
// momia mentre la roda no la reinicia). Quan l'animació acaba i
|
// momia mentre la roda no la reinicia). Quan l'animació acaba i
|
||||||
// `respawn_delay_ms` expira, `respawn(spawn_x_, spawn_y_)` torna a
|
// `respawn_delay_ms` expira, `respawnFromDying()` decrementa
|
||||||
// GROUNDED i `lives_remaining_` decreix en 1 (pot anar a negatius).
|
// `lives_remaining_`: si encara queden vides, fa `spawnFromAir`
|
||||||
|
// al spawn point; si arriba a 0, fa `disable()` i publica al bus
|
||||||
|
// `player_disabled` perquè GameScene/HUD reaccionin (slot fora de
|
||||||
|
// partida, esperant un nou START per re-entrar via "join in progress").
|
||||||
void enterDying(DeathCause cause = DeathCause::GENERIC);
|
void enterDying(DeathCause cause = DeathCause::GENERIC);
|
||||||
|
|
||||||
// Coordenades del spawn de respawn (cantó superior-esquerra del
|
// Coordenades del spawn de respawn (cantó superior-esquerra del
|
||||||
@@ -309,9 +312,11 @@ class Player : public AnimatedSprite {
|
|||||||
spawn_y_ = y;
|
spawn_y_ = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comptador d'intents (estil Mario, NO HP). Decreix a cada respawn;
|
// Comptador d'intents (estil Mario, NO HP). Decreix a cada respawn
|
||||||
// no es clampa a 0 — permet anar a negatius perquè avui no hi ha
|
// post-mort. En arribar a 0 el jugador passa a DISABLED i publica
|
||||||
// game over. Públic per a HUD i tests.
|
// `player_disabled` al bus; pot tornar a partida via "join in
|
||||||
|
// progress" (GameScene::joinPlayer crida `resetForJoin`). Públic
|
||||||
|
// per a HUD i tests.
|
||||||
[[nodiscard]] auto getLivesRemaining() const -> int { return lives_remaining_; }
|
[[nodiscard]] auto getLivesRemaining() const -> int { return lives_remaining_; }
|
||||||
|
|
||||||
// Activa la immunitat per `duration_ms` ms. Mentre `isImmune()` és
|
// Activa la immunitat per `duration_ms` ms. Mentre `isImmune()` és
|
||||||
@@ -342,6 +347,14 @@ class Player : public AnimatedSprite {
|
|||||||
// sala usa `disable()` + `spawnFromAir()`.
|
// sala usa `disable()` + `spawnFromAir()`.
|
||||||
void respawn(float x, float y);
|
void respawn(float x, float y);
|
||||||
|
|
||||||
|
// Restaura el comptador de vides al valor inicial configurat
|
||||||
|
// (`death_config.initial_lives`). Cridat per la GameScene quan un
|
||||||
|
// slot inactiu pulsa START per entrar mid-partida (regla 6 de la
|
||||||
|
// FSM de joc): el jugador entra amb les vides plenes. La resta de
|
||||||
|
// l'estat (posició, animació, flags) la reseteja `spawnFromAir`
|
||||||
|
// que GameScene crida tot seguit.
|
||||||
|
void resetForJoin();
|
||||||
|
|
||||||
// Posa el jugador en el slot inactiu (DISABLED). Idempotent: la
|
// Posa el jugador en el slot inactiu (DISABLED). Idempotent: la
|
||||||
// segona crida és no-op. Mateix patró que `exit()` per simetria.
|
// segona crida és no-op. Mateix patró que `exit()` per simetria.
|
||||||
// No publica res al bus — el GameScene decideix quan disparar el
|
// No publica res al bus — el GameScene decideix quan disparar el
|
||||||
@@ -829,8 +842,10 @@ class Player : public AnimatedSprite {
|
|||||||
|
|
||||||
// Comptador d'intents (estil Mario). Inicialitzat a `death_config.
|
// Comptador d'intents (estil Mario). Inicialitzat a `death_config.
|
||||||
// initial_lives` al ctor; decreix una vegada per cada respawn
|
// initial_lives` al ctor; decreix una vegada per cada respawn
|
||||||
// post-mort (no per respawns de canvi de sala). Pot anar a negatius:
|
// post-mort (no per respawns de canvi de sala). En arribar a 0,
|
||||||
// avui no hi ha game over.
|
// `respawnFromDying` posa el jugador a DISABLED i publica
|
||||||
|
// `player_disabled` al bus. `resetForJoin()` el restaura al valor
|
||||||
|
// inicial quan el jugador re-entra mid-partida.
|
||||||
int lives_remaining_{0};
|
int lives_remaining_{0};
|
||||||
|
|
||||||
// Coordenades del spawn de respawn (cantó superior-esquerra del
|
// Coordenades del spawn de respawn (cantó superior-esquerra del
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
#include "game/levels/level_manager.hpp"
|
#include "game/levels/level_manager.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
|
#include "game/scenes/start_input.hpp"
|
||||||
#include "game/ui/hud.hpp"
|
#include "game/ui/hud.hpp"
|
||||||
#include "game/ui/service_menu.hpp"
|
#include "game/ui/service_menu.hpp"
|
||||||
#include "utils/defaults.hpp"
|
#include "utils/defaults.hpp"
|
||||||
@@ -268,7 +269,7 @@ GameScene::GameScene(Config config)
|
|||||||
|
|
||||||
const bool USE_P1 = (config_.starting_players == DebugConfig::StartingPlayers::P1 || config_.starting_players == DebugConfig::StartingPlayers::BOTH);
|
const bool USE_P1 = (config_.starting_players == DebugConfig::StartingPlayers::P1 || config_.starting_players == DebugConfig::StartingPlayers::BOTH);
|
||||||
const bool USE_P2 = (config_.starting_players == DebugConfig::StartingPlayers::P2 || config_.starting_players == DebugConfig::StartingPlayers::BOTH);
|
const bool USE_P2 = (config_.starting_players == DebugConfig::StartingPlayers::P2 || config_.starting_players == DebugConfig::StartingPlayers::BOTH);
|
||||||
players_starts_active_ = {USE_P1, USE_P2};
|
players_active_ = {USE_P1, USE_P2};
|
||||||
|
|
||||||
// Carreguem assets per als DOS jugadors sempre, encara que algun no
|
// Carreguem assets per als DOS jugadors sempre, encara que algun no
|
||||||
// arranque actiu: l'slot existeix igualment al vector com a DISABLED
|
// arranque actiu: l'slot existeix igualment al vector com a DISABLED
|
||||||
@@ -311,6 +312,13 @@ GameScene::GameScene(Config config)
|
|||||||
entity_manager_.getBus().subscribe("player_killed", [this](const Event& ev) {
|
entity_manager_.getBus().subscribe("player_killed", [this](const Event& ev) {
|
||||||
dispatchPlayerKilled(ev.player_id);
|
dispatchPlayerKilled(ev.player_id);
|
||||||
});
|
});
|
||||||
|
// `player_disabled` el publica el Player al final del cicle DYING
|
||||||
|
// quan `lives_remaining_` arriba a 0. La feina viu a
|
||||||
|
// `onPlayerDisabled` per evitar pujar la complexitat del ctor; el
|
||||||
|
// HUD ja s'apunta al mateix event per banda.
|
||||||
|
entity_manager_.getBus().subscribe("player_disabled", [this](const Event& ev) {
|
||||||
|
onPlayerDisabled(ev.player_id);
|
||||||
|
});
|
||||||
|
|
||||||
// Polvet: precarreguem el surface del Dust una sola vegada perquè
|
// Polvet: precarreguem el surface del Dust una sola vegada perquè
|
||||||
// els spawns runtime de l'entitat no facin disc al primer salt.
|
// els spawns runtime de l'entitat no facin disc al primer salt.
|
||||||
@@ -433,7 +441,7 @@ GameScene::GameScene(Config config)
|
|||||||
// tots els jugadors *actius* han travessat la porta. Els slots DISABLED
|
// tots els jugadors *actius* han travessat la porta. Els slots DISABLED
|
||||||
// permanents (single-player) NO compten — d'altra manera la sala mai
|
// permanents (single-player) NO compten — d'altra manera la sala mai
|
||||||
// es completaria, perquè un slot DISABLED no pot eixir per la porta.
|
// es completaria, perquè un slot DISABLED no pot eixir per la porta.
|
||||||
const int ACTIVE_COUNT = std::ranges::count(players_starts_active_, true);
|
const int ACTIVE_COUNT = std::ranges::count(players_active_, true);
|
||||||
entity_manager_.setExpectedPlayerCount(ACTIVE_COUNT);
|
entity_manager_.setExpectedPlayerCount(ACTIVE_COUNT);
|
||||||
// El snap també empeny lateralment si l'altre jugador ocupa el carril:
|
// El snap també empeny lateralment si l'altre jugador ocupa el carril:
|
||||||
// passem el mateix factor perquè vaja al mateix ritme que el resolver.
|
// passem el mateix factor perquè vaja al mateix ritme que el resolver.
|
||||||
@@ -542,7 +550,7 @@ void GameScene::loadRoom(const std::string& yaml_path) {
|
|||||||
for (auto& p : players_) { p->setMapInfo(INFO); }
|
for (auto& p : players_) { p->setMapInfo(INFO); }
|
||||||
// Tornem a fixar el comptador de "esperats" amb només els slots
|
// Tornem a fixar el comptador de "esperats" amb només els slots
|
||||||
// actius — un slot DISABLED permanent (single-player) no compta.
|
// actius — un slot DISABLED permanent (single-player) no compta.
|
||||||
const int ACTIVE_COUNT = std::ranges::count(players_starts_active_, true);
|
const int ACTIVE_COUNT = std::ranges::count(players_active_, true);
|
||||||
entity_manager_.setExpectedPlayerCount(ACTIVE_COUNT);
|
entity_manager_.setExpectedPlayerCount(ACTIVE_COUNT);
|
||||||
|
|
||||||
// Tornem tots els slots a DISABLED. Els actius els reactivarà el
|
// Tornem tots els slots a DISABLED. Els actius els reactivarà el
|
||||||
@@ -629,10 +637,10 @@ void GameScene::respawnPlayersInstant() {
|
|||||||
// mateix patró de `triggerPlayerSpawn` però cridem `respawn(x, y)`
|
// mateix patró de `triggerPlayerSpawn` però cridem `respawn(x, y)`
|
||||||
// (teleport instantani) en lloc de `spawnFromAir(x, y)`.
|
// (teleport instantani) en lloc de `spawnFromAir(x, y)`.
|
||||||
const auto SPAWN = computeSpawn();
|
const auto SPAWN = computeSpawn();
|
||||||
const bool BOTH_ACTIVE = (players_starts_active_[0] && players_starts_active_[1]);
|
const bool BOTH_ACTIVE = (players_active_[0] && players_active_[1]);
|
||||||
const float SINGLE_X = SPAWN.p1_x + static_cast<float>(Map::tile_size);
|
const float SINGLE_X = SPAWN.p1_x + static_cast<float>(Map::tile_size);
|
||||||
for (std::size_t i = 0; i < players_.size(); ++i) {
|
for (std::size_t i = 0; i < players_.size(); ++i) {
|
||||||
if (!players_starts_active_[i]) { continue; }
|
if (!players_active_[i]) { continue; }
|
||||||
float x = SINGLE_X;
|
float x = SINGLE_X;
|
||||||
if (BOTH_ACTIVE) { x = (i == 0) ? SPAWN.p1_x : SPAWN.p2_x; }
|
if (BOTH_ACTIVE) { x = (i == 0) ? SPAWN.p1_x : SPAWN.p2_x; }
|
||||||
players_[i]->setSpawnPoint(x, SPAWN.y);
|
players_[i]->setSpawnPoint(x, SPAWN.y);
|
||||||
@@ -647,14 +655,14 @@ void GameScene::respawnPlayersInstant() {
|
|||||||
|
|
||||||
void GameScene::triggerPlayerSpawn(std::size_t slot_index) {
|
void GameScene::triggerPlayerSpawn(std::size_t slot_index) {
|
||||||
if (slot_index >= players_.size()) { return; }
|
if (slot_index >= players_.size()) { return; }
|
||||||
if (!players_starts_active_[slot_index]) { return; }
|
if (!players_active_[slot_index]) { return; }
|
||||||
if (players_spawned_in_intro_[slot_index]) { return; }
|
if (players_spawned_in_intro_[slot_index]) { return; }
|
||||||
players_spawned_in_intro_[slot_index] = true;
|
players_spawned_in_intro_[slot_index] = true;
|
||||||
|
|
||||||
// Recalcula spawn per si door_col canvia entre sales (avui no, però
|
// Recalcula spawn per si door_col canvia entre sales (avui no, però
|
||||||
// la lògica ja queda preparada per a l'itinerari).
|
// la lògica ja queda preparada per a l'itinerari).
|
||||||
const auto SPAWN = computeSpawn();
|
const auto SPAWN = computeSpawn();
|
||||||
const bool BOTH_ACTIVE = (players_starts_active_[0] && players_starts_active_[1]);
|
const bool BOTH_ACTIVE = (players_active_[0] && players_active_[1]);
|
||||||
// Mode single-player: l'únic actiu queda centrat sota la porta. Els
|
// Mode single-player: l'únic actiu queda centrat sota la porta. Els
|
||||||
// valors de SPAWN.p1_x/p2_x estan fets per al cas BOTH (offset ±tile);
|
// valors de SPAWN.p1_x/p2_x estan fets per al cas BOTH (offset ±tile);
|
||||||
// amb un sol actiu, el reposicionem al centre exacte.
|
// amb un sol actiu, el reposicionem al centre exacte.
|
||||||
@@ -685,6 +693,71 @@ void GameScene::dispatchPlayerKilled(int player_id) {
|
|||||||
if (IT != players_.end()) { (*IT)->enterDying(); }
|
if (IT != players_.end()) { (*IT)->enterDying(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameScene::onPlayerDisabled(int player_id) {
|
||||||
|
Log::info("PLAYER DISABLED", "player_id={}", player_id);
|
||||||
|
if (player_id < 0 || player_id >= static_cast<int>(players_active_.size())) { return; }
|
||||||
|
players_active_[player_id] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::handleJoinInput() {
|
||||||
|
// El "join in progress" només es processa durant PLAYING. Durant
|
||||||
|
// INTRO la cinemàtica de spawn ja té la seva pròpia coreografia
|
||||||
|
// (vegeu `spawnDuePlayers`); durant OUTRO el cicle de sala ja està
|
||||||
|
// tancant. En tots dos casos el HUD mostra "ESPERA / PARA JUGAR"
|
||||||
|
// perquè quede clar que la pulsació encara no té efecte.
|
||||||
|
if (state_ != RoomFsm::State::PLAYING) { return; }
|
||||||
|
for (std::size_t i = 0; i < players_.size(); ++i) {
|
||||||
|
if (players_active_[i]) { continue; }
|
||||||
|
if (!players_[i]->isDisabled()) { continue; }
|
||||||
|
if (!Input::Detect::pressedStart(static_cast<int>(i))) { continue; }
|
||||||
|
joinPlayer(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::joinPlayer(std::size_t slot_index) {
|
||||||
|
if (slot_index >= players_.size()) { return; }
|
||||||
|
// Spawn lateral SEMPRE: cada jugador entra pel seu costat de la
|
||||||
|
// porta de sortida. Mai el centre (en single-player la cinemàtica
|
||||||
|
// inicial sí que centra el slot actiu, però en un join mid-partida
|
||||||
|
// volem el costat per coherència visual amb la regla 2).
|
||||||
|
const auto SPAWN = computeSpawn();
|
||||||
|
const float X = (slot_index == 0) ? SPAWN.p1_x : SPAWN.p2_x;
|
||||||
|
players_[slot_index]->setSpawnPoint(X, SPAWN.y);
|
||||||
|
players_[slot_index]->resetForJoin();
|
||||||
|
players_[slot_index]->spawnFromAir(X, SPAWN.y);
|
||||||
|
players_active_[slot_index] = true;
|
||||||
|
if (hud_) { hud_->activatePlayer(static_cast<int>(slot_index) + 1); }
|
||||||
|
Log::info("ROOM", "join mid-game P{} -> spawn aerial at ({}, {})", slot_index + 1, X, SPAWN.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GameScene::activePlayerCount() const -> int {
|
||||||
|
int n = 0;
|
||||||
|
for (const auto& p : players_) {
|
||||||
|
if (!p) { continue; }
|
||||||
|
if (!p->isOutOfPlay() || p->isDying()) { ++n; }
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::refreshJoinPrompt() {
|
||||||
|
if (!hud_) { return; }
|
||||||
|
for (std::size_t i = 0; i < players_.size(); ++i) {
|
||||||
|
Hud::JoinPromptMode mode = Hud::JoinPromptMode::NONE;
|
||||||
|
if (!players_active_[i]) {
|
||||||
|
switch (state_) {
|
||||||
|
case RoomFsm::State::PLAYING:
|
||||||
|
mode = Hud::JoinPromptMode::CAN_JOIN;
|
||||||
|
break;
|
||||||
|
case RoomFsm::State::ROOM_INTRO:
|
||||||
|
case RoomFsm::State::ROOM_OUTRO:
|
||||||
|
mode = Hud::JoinPromptMode::WAIT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hud_->setJoinPrompt(static_cast<int>(i) + 1, mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GameScene::completeIntro() {
|
void GameScene::completeIntro() {
|
||||||
if (intro_completed_) { return; }
|
if (intro_completed_) { return; }
|
||||||
intro_completed_ = true;
|
intro_completed_ = true;
|
||||||
@@ -700,9 +773,9 @@ void GameScene::completeIntro() {
|
|||||||
|
|
||||||
void GameScene::spawnDuePlayers() {
|
void GameScene::spawnDuePlayers() {
|
||||||
if (!intro_completed_) { return; }
|
if (!intro_completed_) { return; }
|
||||||
const bool BOTH_ACTIVE = players_starts_active_[0] && players_starts_active_[1];
|
const bool BOTH_ACTIVE = players_active_[0] && players_active_[1];
|
||||||
for (std::size_t i = 0; i < players_starts_active_.size(); ++i) {
|
for (std::size_t i = 0; i < players_active_.size(); ++i) {
|
||||||
if (!players_starts_active_[i]) { continue; }
|
if (!players_active_[i]) { continue; }
|
||||||
if (players_spawned_in_intro_[i]) { continue; }
|
if (players_spawned_in_intro_[i]) { continue; }
|
||||||
// Single-player: el slot actiu (P1 o P2) sempre fa servir
|
// Single-player: el slot actiu (P1 o P2) sempre fa servir
|
||||||
// `player_spawn_delay_ms`. Mode BOTH: P1 fa player_spawn_delay_ms,
|
// `player_spawn_delay_ms`. Mode BOTH: P1 fa player_spawn_delay_ms,
|
||||||
@@ -1219,6 +1292,20 @@ void GameScene::update(int delta_ms) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Join in progress (regla 1): un slot DISABLED pot entrar a partida
|
||||||
|
// pulsant START durant PLAYING. Es processa abans del bloc de món
|
||||||
|
// per que el spawn aeri ja sigui efectiu en aquest mateix frame.
|
||||||
|
// Gateat per pausa/menú/editor: no es vol que un START accidental
|
||||||
|
// mentre l'usuari navega per un menú trigueri un join.
|
||||||
|
#ifdef _DEBUG
|
||||||
|
const bool EDITOR_OPEN = editor_.active();
|
||||||
|
#else
|
||||||
|
constexpr bool EDITOR_OPEN = false;
|
||||||
|
#endif
|
||||||
|
if (pause_.state == PauseState::IDLE && !MENU_FROZEN && !EDITOR_OPEN) {
|
||||||
|
handleJoinInput();
|
||||||
|
}
|
||||||
|
|
||||||
GlobalInputs::update(delta_ms);
|
GlobalInputs::update(delta_ms);
|
||||||
if (GlobalInputs::wantsQuit()) { wants_quit_ = true; }
|
if (GlobalInputs::wantsQuit()) { wants_quit_ = true; }
|
||||||
|
|
||||||
@@ -1279,6 +1366,11 @@ void GameScene::update(int delta_ms) {
|
|||||||
|
|
||||||
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiConfig::get().renderInfoText(), UiConfig::get().renderInfoShadow()));
|
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiConfig::get().renderInfoText(), UiConfig::get().renderInfoShadow()));
|
||||||
Notifier::get().update(delta_ms);
|
Notifier::get().update(delta_ms);
|
||||||
|
|
||||||
|
// Refresc del prompt al HUD per a slots inactius. Es fa al final per
|
||||||
|
// recollir els canvis d'aquest frame (joins, deaths, transició de
|
||||||
|
// RoomFsm) abans del proper render.
|
||||||
|
refreshJoinPrompt();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::render() {
|
void GameScene::render() {
|
||||||
|
|||||||
@@ -95,17 +95,19 @@ class GameScene : public Scene {
|
|||||||
// Vector des del principi: AEE suportarà multijugador local; cada Player
|
// Vector des del principi: AEE suportarà multijugador local; cada Player
|
||||||
// té els seus propis Controls i entra al vector amb una crida extra.
|
// té els seus propis Controls i entra al vector amb una crida extra.
|
||||||
// Sempre conté **dos** entrades (P1 a [0], P2 a [1]) tant en mode BOTH
|
// Sempre conté **dos** entrades (P1 a [0], P2 a [1]) tant en mode BOTH
|
||||||
// com en single-player: l'slot inactiu es queda en estat DISABLED de
|
// com en single-player: l'slot inactiu es queda en estat DISABLED i
|
||||||
// manera permanent, deixant el camí preparat per al "join in progress"
|
// pot entrar mid-partida via "join in progress" (`handleJoinInput`).
|
||||||
// futur (el segon jugador podrà entrar a meitat de partida sense
|
|
||||||
// recrear estructures). Veure `players_starts_active_`.
|
|
||||||
std::vector<std::unique_ptr<Player>> players_;
|
std::vector<std::unique_ptr<Player>> players_;
|
||||||
|
|
||||||
// Quins slots juguen aquesta partida segons `config_.starting_players`.
|
// Quins slots estan en partida ARA MATEIX (jugant o DYING). Mutable
|
||||||
// Index = id del jugador (0 = P1, 1 = P2). Determina a qui aplica el
|
// durant la sala: el ctor el sembra des de `config_.starting_players`,
|
||||||
// spawn aeri al final de la fase 0 d'INTRO; un slot amb `false` es
|
// `joinPlayer` el posa a true quan un slot inactiu pulsa START, i la
|
||||||
// queda DISABLED tot el cicle de sala.
|
// subscripció a `player_disabled` el torna a false quan un slot
|
||||||
std::array<bool, 2> players_starts_active_{false, false};
|
// esgota vides. `loadRoom` recalcula `setExpectedPlayerCount` a
|
||||||
|
// partir d'aquesta vista — un slot DISABLED al final d'una sala no
|
||||||
|
// entra automàticament a la següent (el jugador ha de re-pulsar
|
||||||
|
// START si vol re-entrar).
|
||||||
|
std::array<bool, 2> players_active_{false, false};
|
||||||
|
|
||||||
// Cinemàtica de spawn d'un slot concret. Posa el slot indicat en
|
// Cinemàtica de spawn d'un slot concret. Posa el slot indicat en
|
||||||
// AIRBORNE des d'una altura fora de pantalla. Cridat per
|
// AIRBORNE des d'una altura fora de pantalla. Cridat per
|
||||||
@@ -130,6 +132,13 @@ class GameScene : public Scene {
|
|||||||
// Extret a un mètode per acotar la complexitat cognitiva del ctor.
|
// Extret a un mètode per acotar la complexitat cognitiva del ctor.
|
||||||
void dispatchPlayerKilled(int player_id);
|
void dispatchPlayerKilled(int player_id);
|
||||||
|
|
||||||
|
// Handler de l'event `player_disabled` (un slot acaba d'esgotar
|
||||||
|
// vides). Apaga `players_active_[id]` perquè el `loadRoom` següent
|
||||||
|
// no compti aquest slot i perquè el "join in progress" del frame
|
||||||
|
// següent torni a oferir-li `setJoinPrompt(CAN_JOIN)`. Extret a
|
||||||
|
// mètode per acotar la complexitat cognitiva del subscriber/ctor.
|
||||||
|
void onPlayerDisabled(int player_id);
|
||||||
|
|
||||||
// Itera els slots actius i, si el delay configurat ha expirat des
|
// Itera els slots actius i, si el delay configurat ha expirat des
|
||||||
// del callback de fi de música d'INTRO, dispara `triggerPlayerSpawn`.
|
// del callback de fi de música d'INTRO, dispara `triggerPlayerSpawn`.
|
||||||
// Cridada cada frame durant la fase d'spawn escalonat (entre el
|
// Cridada cada frame durant la fase d'spawn escalonat (entre el
|
||||||
@@ -137,6 +146,33 @@ class GameScene : public Scene {
|
|||||||
// caigut tots o si encara no estem dins la finestra (intro_completed_).
|
// caigut tots o si encara no estem dins la finestra (intro_completed_).
|
||||||
void spawnDuePlayers();
|
void spawnDuePlayers();
|
||||||
|
|
||||||
|
// Atén el "join in progress" (regla 1 de la FSM de joc): si un slot
|
||||||
|
// està DISABLED i el jugador corresponent pulsa START dins de PLAYING,
|
||||||
|
// dispara `joinPlayer`. La detecció de tecla/botó la fa
|
||||||
|
// `Input::detect::pressedStart`. Ignorat fora de PLAYING (durant
|
||||||
|
// INTRO/OUTRO el slot inactiu mostra "ESPERA / PARA JUGAR" al HUD).
|
||||||
|
void handleJoinInput();
|
||||||
|
|
||||||
|
// Materialitza l'entrada d'un slot inactiu: spawn aeri al costat
|
||||||
|
// corresponent de la porta (`computeSpawn().pX_x`), reset de vides
|
||||||
|
// al valor inicial via `Player::resetForJoin`, marcatge de
|
||||||
|
// `players_active_[i]=true` i activació del bloc del HUD. La
|
||||||
|
// cinemàtica reaprofita `spawnFromAir` (mateixa caiguda visual que
|
||||||
|
// l'INTRO de sala i el respawn post-mort).
|
||||||
|
void joinPlayer(std::size_t slot_index);
|
||||||
|
|
||||||
|
// Recompte de jugadors "en partida" (no DISABLED ni EXITED, o bé
|
||||||
|
// DYING — DYING encara compta com a jugant fins que es resol). Hook
|
||||||
|
// exposat per a la futura sub-FSM CONTINUE? + GAME OVER, que vigila
|
||||||
|
// la transició a 0 jugadors actius. No s'usa dins de la Fase 1.
|
||||||
|
[[nodiscard]] auto activePlayerCount() const -> int;
|
||||||
|
|
||||||
|
// Refresca el prompt mostrat al bloc HUD de cada slot inactiu. Es
|
||||||
|
// crida cada frame des d'`update()`. La decisió viu ací (que coneix
|
||||||
|
// `state_` de la RoomFsm); el HUD només pinta. Per a slots actius,
|
||||||
|
// posa el prompt a NONE per ser defensiu davant de canvis d'estat.
|
||||||
|
void refreshJoinPrompt();
|
||||||
|
|
||||||
// Sentinel que marca el moment "música d'INTRO acabada" dins del
|
// Sentinel que marca el moment "música d'INTRO acabada" dins del
|
||||||
// timeline. Ja no ve d'un callback d'àudio: el setja el GameScene
|
// timeline. Ja no ve d'un callback d'àudio: el setja el GameScene
|
||||||
// quan la FSM ha consumit l'última fase d'INTRO (la `music_tail`,
|
// quan la FSM ha consumit l'última fase d'INTRO (la `music_tail`,
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#include "game/scenes/start_input.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "core/input/gamepad_bindings.hpp"
|
||||||
|
#include "core/input/gamepad_manager.hpp"
|
||||||
|
#include "core/system/event_buffer.hpp"
|
||||||
|
#include "game/options.hpp"
|
||||||
|
#include "utils/defaults.hpp"
|
||||||
|
|
||||||
|
namespace Input::Detect {
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Input::Detect
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Helper compartit per detectar pulsacions del botó START dins del frame
|
||||||
|
// actual per a un jugador concret. Originalment vivia al namespace anònim
|
||||||
|
// de `title_scene.cpp` perquè era l'únic consumidor; ara la GameScene
|
||||||
|
// també l'usa per al "join in progress" mid-partida (regla 1 de la FSM
|
||||||
|
// del jugador), així que extraiem la funció a un namespace propi per
|
||||||
|
// evitar duplicar la pila d'input (Defaults::Keys + GamepadManager +
|
||||||
|
// Options + GamepadBindings + EventBuffer) en dos llocs.
|
||||||
|
namespace Input::Detect {
|
||||||
|
|
||||||
|
// Cert si dins dels events SDL d'aquest frame hi ha una pulsació de
|
||||||
|
// START per al jugador `player_id` (0 = P1, 1 = P2). Combina events
|
||||||
|
// de teclat (scancode hardcoded a `Defaults::KeysGame[P2]::START`) i
|
||||||
|
// de gamepad (botó remapejable via `GamepadBindings`, lligat al pad
|
||||||
|
// assignat al jugador via `Options::controls()`).
|
||||||
|
[[nodiscard]] auto pressedStart(int player_id) -> bool;
|
||||||
|
|
||||||
|
} // namespace Input::Detect
|
||||||
@@ -8,8 +8,6 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "core/audio/audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
#include "core/input/gamepad_bindings.hpp"
|
|
||||||
#include "core/input/gamepad_manager.hpp"
|
|
||||||
#include "core/input/global_inputs.hpp"
|
#include "core/input/global_inputs.hpp"
|
||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
#include "core/rendering/notifier.hpp"
|
#include "core/rendering/notifier.hpp"
|
||||||
@@ -18,11 +16,10 @@
|
|||||||
#include "core/rendering/surface.hpp"
|
#include "core/rendering/surface.hpp"
|
||||||
#include "core/rendering/text.hpp"
|
#include "core/rendering/text.hpp"
|
||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
#include "core/system/event_buffer.hpp"
|
|
||||||
#include "core/ui/ui_config.hpp"
|
#include "core/ui/ui_config.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
#include "game/scenes/scene_flow.hpp"
|
#include "game/scenes/scene_flow.hpp"
|
||||||
#include "utils/defaults.hpp"
|
#include "game/scenes/start_input.hpp"
|
||||||
#include "utils/defines.hpp"
|
#include "utils/defines.hpp"
|
||||||
#include "utils/easing.hpp"
|
#include "utils/easing.hpp"
|
||||||
#include "utils/log.hpp"
|
#include "utils/log.hpp"
|
||||||
@@ -33,27 +30,6 @@ namespace {
|
|||||||
// docs/audit-core-rendering.md).
|
// docs/audit-core-rendering.md).
|
||||||
const char* const SCENE_NAME = "TITLE";
|
const char* const SCENE_NAME = "TITLE";
|
||||||
|
|
||||||
// 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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reproducció del so de START. Centralitzat ací per disparar-lo des
|
// Reproducció del so de START. Centralitzat ací per disparar-lo des
|
||||||
// dels punts on una pulsació té efecte real (entrada a STARTING o
|
// dels punts on una pulsació té efecte real (entrada a STARTING o
|
||||||
// upgrade a BOTH); l'attract timeout no l'ha de tocar.
|
// upgrade a BOTH); l'attract timeout no l'ha de tocar.
|
||||||
@@ -139,8 +115,8 @@ void TitleScene::update(int delta_ms) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool P1_START = pressedStart(0);
|
const bool P1_START = Input::Detect::pressedStart(0);
|
||||||
const bool P2_START = pressedStart(1);
|
const bool P2_START = Input::Detect::pressedStart(1);
|
||||||
|
|
||||||
switch (phase_) {
|
switch (phase_) {
|
||||||
case Phase::IDLE:
|
case Phase::IDLE:
|
||||||
|
|||||||
@@ -136,6 +136,18 @@ Hud::Hud(EventBus& bus, std::shared_ptr<Text> font)
|
|||||||
Log::info("HUD", "level_complete (mode Win pendent)");
|
Log::info("HUD", "level_complete (mode Win pendent)");
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// `player_disabled`: el jugador acaba d'esgotar les seves vides al
|
||||||
|
// final del cicle DYING. Tornem el bloc al render inactiu — el
|
||||||
|
// prompt textual el fixa GameScene cada frame via `setJoinPrompt`,
|
||||||
|
// així que ací només cal apagar `active`. Els camps lives/score/
|
||||||
|
// treasures es preserven per si en una versió futura volem mostrar
|
||||||
|
// el resultat final abans de tornar a zero.
|
||||||
|
subs_.push_back(bus_.subscribe("player_disabled", [this](const Event& ev) {
|
||||||
|
const std::size_t IDX = (ev.player_id == 1) ? 1U : 0U;
|
||||||
|
players_[IDX].active = false;
|
||||||
|
Log::info("HUD", "player_disabled p{}", IDX + 1);
|
||||||
|
}));
|
||||||
|
|
||||||
Log::info("HUD", "init: 3 blocks ready (entry {} ms)", ENTRY_MS);
|
Log::info("HUD", "init: 3 blocks ready (entry {} ms)", ENTRY_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +179,12 @@ void Hud::activatePlayer(int player_one_based) {
|
|||||||
p.lives = DEFAULT_LIVES;
|
p.lives = DEFAULT_LIVES;
|
||||||
p.score = 0;
|
p.score = 0;
|
||||||
p.treasures = 0;
|
p.treasures = 0;
|
||||||
|
p.join_prompt = JoinPromptMode::NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Hud::setJoinPrompt(int player_one_based, JoinPromptMode mode) {
|
||||||
|
if (player_one_based < 1 || player_one_based > 2) { return; }
|
||||||
|
players_[static_cast<std::size_t>(player_one_based - 1)].join_prompt = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Hud::computeOffset(const Block& b) -> SDL_FPoint {
|
auto Hud::computeOffset(const Block& b) -> SDL_FPoint {
|
||||||
@@ -234,6 +252,26 @@ void Hud::renderBlock(BlockId id) const {
|
|||||||
|
|
||||||
void Hud::renderIdleSide(int x, int y, int w, const Block& blk, int player_index, const std::string& label) const {
|
void Hud::renderIdleSide(int x, int y, int w, const Block& blk, int player_index, const std::string& label) const {
|
||||||
const auto& p = players_[static_cast<std::size_t>(player_index)];
|
const auto& p = players_[static_cast<std::size_t>(player_index)];
|
||||||
|
|
||||||
|
// Slot inactiu amb prompt actiu: substituïm el render de zeros pel
|
||||||
|
// missatge "PULSA START / PARA JUGAR" o "ESPERA / PARA JUGAR" segons
|
||||||
|
// el mode fixat per la GameScene cada frame. El render de zeros
|
||||||
|
// queda com a fallback (mode NONE o slot actiu).
|
||||||
|
if (!p.active && p.join_prompt != JoinPromptMode::NONE) {
|
||||||
|
const bool CAN_JOIN = (p.join_prompt == JoinPromptMode::CAN_JOIN);
|
||||||
|
const std::string L1 = Locale::get().text(
|
||||||
|
CAN_JOIN ? "hud.join_prompt_can_join_l1" : "hud.join_prompt_wait_l1");
|
||||||
|
const std::string L2 = Locale::get().text(
|
||||||
|
CAN_JOIN ? "hud.join_prompt_can_join_l2" : "hud.join_prompt_wait_l2");
|
||||||
|
const int L1_W = font_->length(L1);
|
||||||
|
const int L2_W = font_->length(L2);
|
||||||
|
const int X1 = x + ((w - L1_W) / 2);
|
||||||
|
const int X2 = x + ((w - L2_W) / 2);
|
||||||
|
font_->writeColored(X1, y + LINE1_Y, L1, UiConfig::get().hudLabel());
|
||||||
|
font_->writeColored(X2, y + LINE2_Y, L2, UiConfig::get().hudValue());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const Uint8 SCORE_C = (blk.flash_ms_left > 0) ? UiConfig::get().hudFlash() : UiConfig::get().hudValue();
|
const Uint8 SCORE_C = (blk.flash_ms_left > 0) ? UiConfig::get().hudFlash() : UiConfig::get().hudValue();
|
||||||
|
|
||||||
// --- Línia 1: "<label>-000000" centrada ---
|
// --- Línia 1: "<label>-000000" centrada ---
|
||||||
|
|||||||
+29
-2
@@ -41,6 +41,20 @@ class Hud {
|
|||||||
WIN,
|
WIN,
|
||||||
EMPTY };
|
EMPTY };
|
||||||
|
|
||||||
|
// Prompt mostrat al bloc d'un slot inactiu (en lloc dels zeros). El
|
||||||
|
// GameScene fixa aquest valor cada frame segons el `RoomFsm::State`
|
||||||
|
// perquè el missatge sigui coherent amb si el jugador pot pulsar
|
||||||
|
// START per entrar ara mateix o si ha d'esperar al canvi de fase.
|
||||||
|
// - NONE → render històric (zeros). Per a slots actius o per
|
||||||
|
// quan el GameScene no vol ensenyar prompt.
|
||||||
|
// - CAN_JOIN → "PULSA START / PARA JUGAR". Slot inactiu en PLAYING.
|
||||||
|
// - WAIT → "ESPERA / PARA JUGAR". Slot inactiu durant
|
||||||
|
// ROOM_INTRO/ROOM_OUTRO i, en fases futures, després
|
||||||
|
// de GAME OVER.
|
||||||
|
enum class JoinPromptMode : std::uint8_t { NONE,
|
||||||
|
CAN_JOIN,
|
||||||
|
WAIT };
|
||||||
|
|
||||||
Hud(EventBus& bus, std::shared_ptr<Text> font);
|
Hud(EventBus& bus, std::shared_ptr<Text> font);
|
||||||
~Hud();
|
~Hud();
|
||||||
|
|
||||||
@@ -71,6 +85,14 @@ class Hud {
|
|||||||
// arrenca amb les vides per defecte.
|
// arrenca amb les vides per defecte.
|
||||||
void activatePlayer(int player_one_based);
|
void activatePlayer(int player_one_based);
|
||||||
|
|
||||||
|
// Fixa el prompt visible al bloc d'un slot inactiu (1-based: 1 → P1,
|
||||||
|
// 2 → P2). Cridat des de `GameScene::update()` cada frame perquè el
|
||||||
|
// missatge depèn de l'estat de la `RoomFsm` (vegeu enum
|
||||||
|
// `JoinPromptMode`). Sobre un slot actiu el prompt no es renderitza
|
||||||
|
// — el camp existeix per simetria i per evitar branques al caller.
|
||||||
|
// Idempotent: cridar amb el mateix mode és no-op.
|
||||||
|
void setJoinPrompt(int player_one_based, JoinPromptMode mode);
|
||||||
|
|
||||||
// Geometria pública perquè la GameScene puga calcular si el HUD
|
// Geometria pública perquè la GameScene puga calcular si el HUD
|
||||||
// s'està movent encara (p. ex. retardar l'spawn fins que estiga al
|
// s'està movent encara (p. ex. retardar l'spawn fins que estiga al
|
||||||
// seu lloc) — opcional, no obligatori.
|
// seu lloc) — opcional, no obligatori.
|
||||||
@@ -124,9 +146,14 @@ class Hud {
|
|||||||
int lives{DEFAULT_LIVES};
|
int lives{DEFAULT_LIVES};
|
||||||
int score{0};
|
int score{0};
|
||||||
int treasures{0};
|
int treasures{0};
|
||||||
// `false` → el bloc mostra `000000`/`0`/`00` (jugador inactiu).
|
// `false` → el bloc mostra el prompt actiu (`join_prompt`) o
|
||||||
// Sempre es renderitza per mantenir el layout fix.
|
// bé zeros si el prompt és NONE. Sempre es renderitza per
|
||||||
|
// mantenir el layout fix.
|
||||||
bool active{false};
|
bool active{false};
|
||||||
|
// Missatge a mostrar quan `active==false`. El GameScene el
|
||||||
|
// refresca cada frame segons l'estat de la RoomFsm (vegeu
|
||||||
|
// `Hud::setJoinPrompt`).
|
||||||
|
JoinPromptMode join_prompt{JoinPromptMode::NONE};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pinta el bloc indicat amb el seu fons + el contingut del seu mode
|
// Pinta el bloc indicat amb el seu fons + el contingut del seu mode
|
||||||
|
|||||||
Reference in new issue
Block a user