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
@@ -36,6 +36,7 @@
|
||||
#include "game/levels/level_manager.hpp"
|
||||
#include "game/map/map.hpp"
|
||||
#include "game/options.hpp"
|
||||
#include "game/scenes/start_input.hpp"
|
||||
#include "game/ui/hud.hpp"
|
||||
#include "game/ui/service_menu.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_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
|
||||
// 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) {
|
||||
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è
|
||||
// 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
|
||||
// permanents (single-player) NO compten — d'altra manera la sala mai
|
||||
// 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);
|
||||
// El snap també empeny lateralment si l'altre jugador ocupa el carril:
|
||||
// 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); }
|
||||
// Tornem a fixar el comptador de "esperats" amb només els slots
|
||||
// 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);
|
||||
|
||||
// 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)`
|
||||
// (teleport instantani) en lloc de `spawnFromAir(x, y)`.
|
||||
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);
|
||||
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;
|
||||
if (BOTH_ACTIVE) { x = (i == 0) ? SPAWN.p1_x : SPAWN.p2_x; }
|
||||
players_[i]->setSpawnPoint(x, SPAWN.y);
|
||||
@@ -647,14 +655,14 @@ void GameScene::respawnPlayersInstant() {
|
||||
|
||||
void GameScene::triggerPlayerSpawn(std::size_t slot_index) {
|
||||
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; }
|
||||
players_spawned_in_intro_[slot_index] = true;
|
||||
|
||||
// Recalcula spawn per si door_col canvia entre sales (avui no, però
|
||||
// la lògica ja queda preparada per a l'itinerari).
|
||||
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
|
||||
// valors de SPAWN.p1_x/p2_x estan fets per al cas BOTH (offset ±tile);
|
||||
// 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(); }
|
||||
}
|
||||
|
||||
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() {
|
||||
if (intro_completed_) { return; }
|
||||
intro_completed_ = true;
|
||||
@@ -700,9 +773,9 @@ void GameScene::completeIntro() {
|
||||
|
||||
void GameScene::spawnDuePlayers() {
|
||||
if (!intro_completed_) { return; }
|
||||
const bool BOTH_ACTIVE = players_starts_active_[0] && players_starts_active_[1];
|
||||
for (std::size_t i = 0; i < players_starts_active_.size(); ++i) {
|
||||
if (!players_starts_active_[i]) { continue; }
|
||||
const bool BOTH_ACTIVE = players_active_[0] && players_active_[1];
|
||||
for (std::size_t i = 0; i < players_active_.size(); ++i) {
|
||||
if (!players_active_[i]) { continue; }
|
||||
if (players_spawned_in_intro_[i]) { continue; }
|
||||
// Single-player: el slot actiu (P1 o P2) sempre fa servir
|
||||
// `player_spawn_delay_ms`. Mode BOTH: P1 fa player_spawn_delay_ms,
|
||||
@@ -1219,6 +1292,20 @@ void GameScene::update(int delta_ms) {
|
||||
}
|
||||
#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);
|
||||
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()));
|
||||
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() {
|
||||
|
||||
Reference in new issue
Block a user