feat: estat DISABLED i spawn aeri al començar la sala — v0.25.0
This commit is contained in:
1 parent
645db217fa
commit
580c13e854
8 files changed
+280
-104
No files matched your search
@@ -113,12 +113,12 @@ namespace {
|
||||
// surt projectat sense control i els dos es travessen. Coincideix
|
||||
// amb la regla d'`Player::collidesWithWorld`.
|
||||
if (a->isHurt() || b->isHurt()) { return; }
|
||||
// EXITED: el jugador despawnat no és tangible. Si entrés al
|
||||
// resolver, la seua presència física frenaria l'altre (per
|
||||
// EXITED/DISABLED: jugadors fora del joc, no participen al resolver.
|
||||
// Si hi entrés un, la seua presència física frenaria l'altre (per
|
||||
// overlap predictiu) tot i que `applyMovement` és no-op. Sortir
|
||||
// d'aquí garanteix que el segon jugador pot creuar la porta
|
||||
// sense interferència del primer ja eixit.
|
||||
if (a->isExited() || b->isExited()) { return; }
|
||||
// d'aquí garanteix que el jugador actiu pot creuar la porta o
|
||||
// moure's lliurement sense interferència d'un slot inactiu.
|
||||
if (a->isOutOfPlay() || b->isOutOfPlay()) { return; }
|
||||
|
||||
// Aire ↔ tierra: el saltador es travessa net del caminant. Sense
|
||||
// resolver, sense head-on, sense push. Els dos continuen amb la
|
||||
@@ -250,19 +250,22 @@ 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};
|
||||
|
||||
std::string anim_p1;
|
||||
std::string anim_p2;
|
||||
if (USE_P1) { anim_p1 = load_player_assets(config_.player_sprite, config_.player_yaml); }
|
||||
if (USE_P2) { anim_p2 = load_player_assets(config_.player2_sprite, config_.player2_yaml); }
|
||||
// Carreguem assets per als DOS jugadors sempre, encara que algun no
|
||||
// arranque actiu: l'slot existeix igualment al vector com a DISABLED
|
||||
// i podria reactivar-se amb un futur "join in progress". El cost de
|
||||
// tenir el sprite cachejat sense usar-se és negligible (cap render).
|
||||
const std::string ANIM_P1 = load_player_assets(config_.player_sprite, config_.player_yaml);
|
||||
const std::string ANIM_P2 = load_player_assets(config_.player2_sprite, config_.player2_yaml);
|
||||
|
||||
// Geometria de col·lisió del jugador (caixa 16×16, base 12): carregada
|
||||
// del mateix YAML que l'animació. Ha d'estar feta abans d'instanciar
|
||||
// Player perquè el spawn consulta `collision_*`. Els membres de Player
|
||||
// són `static inline`, compartits per tots els jugadors: cridem
|
||||
// `loadFrom` amb el YAML del primer jugador actiu (P1 o P2). Els dos
|
||||
// personatges usen la mateixa geometria (morcus és un repaletat de sam).
|
||||
Player::loadFrom(USE_P1 ? config_.player_yaml : config_.player2_yaml);
|
||||
// són `static inline`, compartits per tots els jugadors; carregar des
|
||||
// del YAML de P1 és suficient (els dos personatges comparteixen
|
||||
// geometria — morcus és un repaletat de sam).
|
||||
Player::loadFrom(config_.player_yaml);
|
||||
|
||||
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
|
||||
// etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa
|
||||
@@ -321,7 +324,7 @@ GameScene::GameScene(Config config)
|
||||
// `Map::door_col` perquè canviar la columna de la porta al YAML
|
||||
// moga automàticament l'spawn — no hi ha cap valor hardcoded.
|
||||
const auto SPAWN = computeSpawn();
|
||||
const bool BOTH = (USE_P1 && USE_P2);
|
||||
const bool BOTH_ACTIVE = (USE_P1 && USE_P2);
|
||||
|
||||
// Índex de tile amb què pinta cada jugador — llegit del YAML del mapa
|
||||
// (camps opcionals `player1_paint_tile` / `player2_paint_tile`). Si no
|
||||
@@ -331,16 +334,29 @@ GameScene::GameScene(Config config)
|
||||
// puntuació (veure `Event::player_id`).
|
||||
constexpr int ID_P1 = 0;
|
||||
constexpr int ID_P2 = 1;
|
||||
if (USE_P1) {
|
||||
const int P1_PAINT = readOptionalInt(config_.map_yaml, "player1_paint_tile", -1);
|
||||
const float X = BOTH ? SPAWN.p1_x : SPAWN.p1_x + static_cast<float>(Map::tile_size);
|
||||
players_.emplace_back(std::make_unique<Player>(anim_p1, ID_P1, X, SPAWN.y, CONTROLS_P1, Map::makeInfo(), P1_PAINT));
|
||||
}
|
||||
if (USE_P2) {
|
||||
const int P2_PAINT = readOptionalInt(config_.map_yaml, "player2_paint_tile", -1);
|
||||
const float X = BOTH ? SPAWN.p2_x : SPAWN.p2_x - static_cast<float>(Map::tile_size);
|
||||
players_.emplace_back(std::make_unique<Player>(anim_p2, ID_P2, X, SPAWN.y, CONTROLS_P2, Map::makeInfo(), P2_PAINT));
|
||||
const int P1_PAINT = readOptionalInt(config_.map_yaml, "player1_paint_tile", -1);
|
||||
const int P2_PAINT = readOptionalInt(config_.map_yaml, "player2_paint_tile", -1);
|
||||
|
||||
// Sempre instanciem els dos slots. Mode single-player: l'únic actiu
|
||||
// queda centrat sota la porta (= SPAWN.p1_x + tile_size); l'inactiu
|
||||
// ocupa la posició BOTH per simetria, però com es queda DISABLED tot
|
||||
// el cicle no es nota. Així `players_[0]` sempre és P1 i `players_[1]`
|
||||
// sempre és P2, independentment de quins juguen.
|
||||
const auto TILE_F = static_cast<float>(Map::tile_size);
|
||||
float p1_x = SPAWN.p1_x;
|
||||
float p2_x = SPAWN.p2_x;
|
||||
if (!BOTH_ACTIVE) {
|
||||
if (USE_P1) { p1_x = SPAWN.p1_x + TILE_F; }
|
||||
if (USE_P2) { p2_x = SPAWN.p2_x - TILE_F; }
|
||||
}
|
||||
players_.emplace_back(std::make_unique<Player>(ANIM_P1, ID_P1, p1_x, SPAWN.y, CONTROLS_P1, Map::makeInfo(), P1_PAINT));
|
||||
players_.emplace_back(std::make_unique<Player>(ANIM_P2, ID_P2, p2_x, SPAWN.y, CONTROLS_P2, Map::makeInfo(), P2_PAINT));
|
||||
|
||||
// Tots els slots arrenquen DISABLED. Els actius els reactivarà
|
||||
// `updateRoomTransition` quan s'esgote `player_spawn_delay_ms_` dins
|
||||
// de ROOM_INTRO (cinemàtica de caiguda); els inactius es queden
|
||||
// DISABLED tot el cicle de sala.
|
||||
for (auto& p : players_) { p->disable(); }
|
||||
|
||||
// PaintLayer: font única del pintat del nivell. Es crea després de
|
||||
// `loadFromLevel` perquè l'EntityManager ja ha de tenir els sarcòfags
|
||||
@@ -366,10 +382,11 @@ GameScene::GameScene(Config config)
|
||||
for (auto& p : players_) { p->setOtherPlayers(&players_view_); }
|
||||
|
||||
// Comptador d'EXITED a EntityManager: publica `level_complete` quan
|
||||
// tots els jugadors actius han travessat la porta. Cap setExpectedPlayerCount
|
||||
// ha d'inicialitzar-se abans del primer `player_exited`; ho fem aquí
|
||||
// perquè ja sabem el nombre real d'instàncies.
|
||||
entity_manager_.setExpectedPlayerCount(static_cast<int>(players_.size()));
|
||||
// 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);
|
||||
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.
|
||||
for (auto& p : players_) { p->setPushFactor(push_factor_); }
|
||||
@@ -444,25 +461,44 @@ void GameScene::resetRoom() {
|
||||
// CLOSED amb visited buit, momies i altres actors a la posició inicial.
|
||||
Map::loadFrom(config_.map_yaml);
|
||||
entity_manager_.reload(config_.map_yaml);
|
||||
entity_manager_.setExpectedPlayerCount(static_cast<int>(players_.size()));
|
||||
// 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);
|
||||
entity_manager_.setExpectedPlayerCount(ACTIVE_COUNT);
|
||||
if (paint_layer_) { paint_layer_->clear(); }
|
||||
|
||||
// Respawn dels jugadors. Recalcular spawn per si door_col canvia
|
||||
// (avui el mapa no canvia entre cicles, però la lògica ja queda
|
||||
// preparada per a l'itinerari futur).
|
||||
const auto SPAWN = computeSpawn();
|
||||
const bool BOTH = (players_.size() >= 2);
|
||||
// Mode single-player: l'únic jugador queda centrat (= SPAWN.p1_x +
|
||||
// tile_size, perquè SPAWN.p1_x és el centre menys un tile).
|
||||
const float SINGLE_X = SPAWN.p1_x + static_cast<float>(Map::tile_size);
|
||||
for (std::size_t i = 0; i < players_.size(); ++i) {
|
||||
float x = SINGLE_X;
|
||||
if (BOTH) { x = (i == 0) ? SPAWN.p1_x : SPAWN.p2_x; }
|
||||
players_[i]->respawn(x, SPAWN.y);
|
||||
}
|
||||
// Tornem tots els slots a DISABLED. Els actius els reactivarà el
|
||||
// trigger d'spawn aeri dins de `updateRoomTransition` quan torni a
|
||||
// disparar-se el delay del nou cicle d'INTRO. Així cada cicle de
|
||||
// sala repeteix la cinemàtica de caiguda de la mateixa manera que
|
||||
// el primer.
|
||||
for (auto& p : players_) { p->disable(); }
|
||||
|
||||
std::cout << "[ROOM] reset: entities reloaded, paint cleared, "
|
||||
<< players_.size() << " player(s) respawned\n";
|
||||
<< ACTIVE_COUNT << " active player(s) disabled (awaiting spawn cinematic)\n";
|
||||
}
|
||||
|
||||
void GameScene::triggerActivePlayerSpawn() {
|
||||
if (players_spawned_in_intro_) { return; }
|
||||
players_spawned_in_intro_ = 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]);
|
||||
// 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.
|
||||
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; }
|
||||
float x = SINGLE_X;
|
||||
if (BOTH_ACTIVE) { x = (i == 0) ? SPAWN.p1_x : SPAWN.p2_x; }
|
||||
players_[i]->spawnFromAir(x, SPAWN.y);
|
||||
}
|
||||
|
||||
std::cout << "[ROOM] spawn cinematic: airborne entry for active players\n";
|
||||
}
|
||||
|
||||
void GameScene::setState(RoomFsm::State new_state) {
|
||||
@@ -471,6 +507,12 @@ void GameScene::setState(RoomFsm::State new_state) {
|
||||
const auto PREV = state_;
|
||||
state_ = new_state;
|
||||
transition_ = {};
|
||||
// En entrar a un nou cicle d'INTRO, resetegem el timer i el flag de
|
||||
// l'spawn aeri perquè es torne a disparar al delay configurat.
|
||||
if (new_state == RoomFsm::State::ROOM_INTRO) {
|
||||
room_intro_elapsed_ms_ = 0;
|
||||
players_spawned_in_intro_ = false;
|
||||
}
|
||||
// En tornar a ROOM_INTRO des de ROOM_OUTRO (placeholder de
|
||||
// "torna a la mateixa sala"), reseteja completament l'estat del
|
||||
// nivell. Si la transició és la inicial (PREV també és ROOM_INTRO,
|
||||
@@ -481,6 +523,17 @@ void GameScene::setState(RoomFsm::State new_state) {
|
||||
}
|
||||
|
||||
void GameScene::updateRoomTransition(int delta_ms) {
|
||||
// Disparador d'spawn aeri: timer propi (independent de les fases de
|
||||
// la FSM) que arrenca a l'entrada de ROOM_INTRO. La separació deixa
|
||||
// el delay desacoblat del fade-in i, demà, reutilitzable per al
|
||||
// respawn post-mort sense que la FSM de sala hi haja d'intervenir.
|
||||
if (state_ == RoomFsm::State::ROOM_INTRO) {
|
||||
room_intro_elapsed_ms_ += delta_ms;
|
||||
if (!players_spawned_in_intro_ && room_intro_elapsed_ms_ >= transition_config_.player_spawn_delay_ms) {
|
||||
triggerActivePlayerSpawn();
|
||||
}
|
||||
}
|
||||
|
||||
const auto NEXT = RoomFsm::advance(state_, transition_, transition_config_, delta_ms);
|
||||
if (NEXT != state_) { setState(NEXT); }
|
||||
}
|
||||
@@ -725,29 +778,27 @@ void GameScene::update(int delta_ms) {
|
||||
constexpr bool EDITOR_FROZEN = false;
|
||||
#endif
|
||||
const int SCALED_DELTA_MS = static_cast<int>(static_cast<float>(delta_ms) * pause_.scale);
|
||||
const bool GAMEPLAY_ACTIVE = (state_ == RoomFsm::State::PLAYING);
|
||||
if (!EDITOR_FROZEN && SCALED_DELTA_MS > 0) {
|
||||
const float DT = static_cast<float>(SCALED_DELTA_MS) / 1000.0F;
|
||||
|
||||
// Durant ROOM_INTRO/ROOM_OUTRO els jugadors no s'actualitzen ni
|
||||
// poden col·lisionar amb entitats. Les entitats sí, perquè el
|
||||
// disseny demana que el món continui animant-se durant les
|
||||
// transicions de sala.
|
||||
if (GAMEPLAY_ACTIVE) {
|
||||
// Pipeline de moviment en tres passes per poder intercalar la
|
||||
// resolució de contactes jugador↔jugador entre input i posició:
|
||||
// 1) tick: cada jugador calcula la velocitat intencionada
|
||||
// (teclat + mando + facing).
|
||||
// 2) resolve: si els dos es trobarien, anul·lem velocitats
|
||||
// (head-on) o escalem a push_factor (empenta).
|
||||
// 3) applyMovement: en l'ordre "pushed primer" perquè quan el
|
||||
// pusher faça `tryMove`, el pushed ja estiga fora de la
|
||||
// seua caixa. La callback també tracta l'altre jugador com
|
||||
// a sòlid: xarxa de seguretat davant de qualsevol inexactitud.
|
||||
for (auto& player : players_) { player->tick(DT); }
|
||||
resolvePlayerVsPlayer(players_view_, DT, push_factor_);
|
||||
for (auto* player : orderedForMovement(players_view_)) { player->applyMovement(DT); }
|
||||
}
|
||||
// Pipeline de moviment en tres passes per poder intercalar la
|
||||
// resolució de contactes jugador↔jugador entre input i posició:
|
||||
// 1) tick: cada jugador calcula la velocitat intencionada
|
||||
// (teclat + mando + facing).
|
||||
// 2) resolve: si els dos es trobarien, anul·lem velocitats
|
||||
// (head-on) o escalem a push_factor (empenta).
|
||||
// 3) applyMovement: en l'ordre "pushed primer" perquè quan el
|
||||
// pusher faça `tryMove`, el pushed ja estiga fora de la
|
||||
// seua caixa. La callback també tracta l'altre jugador com
|
||||
// a sòlid: xarxa de seguretat davant de qualsevol inexactitud.
|
||||
// Cap gate per estat de sala: els jugadors fora del joc (DISABLED
|
||||
// durant l'INTRO abans del trigger d'spawn, o EXITED post-porta)
|
||||
// s'auto-salten a `tick`/`applyMovement`/`render`. Així la
|
||||
// cinemàtica de spawn aeri pot transcorrer mentre la fase d'INTRO
|
||||
// encara es mou.
|
||||
for (auto& player : players_) { player->tick(DT); }
|
||||
resolvePlayerVsPlayer(players_view_, DT, push_factor_);
|
||||
for (auto* player : orderedForMovement(players_view_)) { player->applyMovement(DT); }
|
||||
|
||||
// El tick d'entitats DESPRÉS del tick dels jugadors: si el jugador ha
|
||||
// emès `on_tile_entered` aquest frame, l'EntityManager ja l'ha encaminat
|
||||
@@ -755,12 +806,11 @@ void GameScene::update(int delta_ms) {
|
||||
// aplicar la transició OPENING → OPEN en el mateix frame.
|
||||
entity_manager_.update(DT);
|
||||
|
||||
// Detecció de contacte player↔entitat després que totes les entitats
|
||||
// hagin mogut aquest frame. Reutilitzem la vista plana que ja
|
||||
// manté el GameScene per al resolver player↔player.
|
||||
if (GAMEPLAY_ACTIVE) {
|
||||
entity_manager_.checkPlayerCollisions(players_view_);
|
||||
}
|
||||
// Detecció de contacte player↔entitat. `checkPlayerCollisions` ja
|
||||
// filtra jugadors fora del joc (`isOutOfPlay`), així que durant
|
||||
// l'INTRO previ a l'spawn no es disparen contactes amb les
|
||||
// entitats que ja s'animen al fons.
|
||||
entity_manager_.checkPlayerCollisions(players_view_);
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
@@ -799,19 +849,14 @@ void GameScene::render() {
|
||||
// explícita: l'editor edita el tilemap, no la capa de pintat).
|
||||
if (!SKIP_ACTORS && paint_layer_) { paint_layer_->render(); }
|
||||
if (!SKIP_ACTORS) {
|
||||
// Durant ROOM_INTRO/ROOM_OUTRO els jugadors no es veuen ni es mouen.
|
||||
// Les entitats sí que es renderitzen perquè el disseny demana que
|
||||
// el món continui animant-se durant el fade i el text.
|
||||
const bool RENDER_PLAYERS = (state_ == RoomFsm::State::PLAYING);
|
||||
|
||||
if (RENDER_PLAYERS) {
|
||||
// Passada de sombres dels jugadors saltant. Va per davant del
|
||||
// pintat (al damunt) però per darrere de TOTS els actors —
|
||||
// entitats i jugadors, inclòs el saltador mateix— així la sombra
|
||||
// queda al sòl sense tapar res. `renderShadow` és no-op si el
|
||||
// jugador està a terra, així que no cal filtrar al callsite.
|
||||
for (const auto& p : players_) { p->renderShadow(); }
|
||||
}
|
||||
// Passada de sombres dels jugadors saltant. Va per davant del
|
||||
// pintat (al damunt) però per darrere de TOTS els actors —
|
||||
// entitats i jugadors, inclòs el saltador mateix— així la sombra
|
||||
// queda al sòl sense tapar res. `renderShadow` és no-op si el
|
||||
// jugador no està a l'aire (i, com `render()`, també no-op si
|
||||
// `isOutOfPlay`). Cap gate per estat de sala: durant la
|
||||
// cinemàtica d'spawn aeri ja volem que la sombra es vegi.
|
||||
for (const auto& p : players_) { p->renderShadow(); }
|
||||
// Z-order interleaving entitats ↔ players per `getFeetY()` ascendent:
|
||||
// els actors amb els peus més amunt queden dibuixats primer (al fons),
|
||||
// els més avall al davant. Així un sarc situat per darrere d'un player
|
||||
@@ -822,16 +867,18 @@ void GameScene::render() {
|
||||
Player* player; // nullable; si és nullptr, s'usa `entity`.
|
||||
};
|
||||
std::vector<Renderable> order;
|
||||
order.reserve(entity_manager_.entityCount() + (RENDER_PLAYERS ? players_.size() : 0));
|
||||
order.reserve(entity_manager_.entityCount() + players_.size());
|
||||
const auto ENTITIES = entity_manager_.getEntities();
|
||||
std::ranges::transform(ENTITIES, std::back_inserter(order), [](Entity* e) {
|
||||
return Renderable{.feet_y = e->getFeetY(), .entity = e, .player = nullptr};
|
||||
});
|
||||
if (RENDER_PLAYERS) {
|
||||
std::ranges::transform(players_, std::back_inserter(order), [](const auto& p) {
|
||||
return Renderable{.feet_y = p->getFeetY(), .entity = nullptr, .player = p.get()};
|
||||
});
|
||||
}
|
||||
// Tots els jugadors entren al z-sort. Els fora de joc fan no-op a
|
||||
// `render()`; els actius (inclosos AIRBORNE durant l'spawn aeri)
|
||||
// es pinten amb el seu feet_y normal. La sombra que afecta la
|
||||
// sombra del salt s'ha pintat ja a la passada anterior.
|
||||
std::ranges::transform(players_, std::back_inserter(order), [](const auto& p) {
|
||||
return Renderable{.feet_y = p->getFeetY(), .entity = nullptr, .player = p.get()};
|
||||
});
|
||||
std::ranges::stable_sort(order, [](const Renderable& a, const Renderable& b) {
|
||||
return a.feet_y < b.feet_y;
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -84,8 +85,34 @@ class GameScene : public Scene {
|
||||
|
||||
// Vector des del principi: AEE suportarà multijugador local; cada Player
|
||||
// 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
|
||||
// com en single-player: l'slot inactiu es queda en estat DISABLED de
|
||||
// manera permanent, deixant el camí preparat per al "join in progress"
|
||||
// futur (el segon jugador podrà entrar a meitat de partida sense
|
||||
// recrear estructures). Veure `players_starts_active_`.
|
||||
std::vector<std::unique_ptr<Player>> players_;
|
||||
|
||||
// Quins slots juguen aquesta partida segons `config_.starting_players`.
|
||||
// Index = id del jugador (0 = P1, 1 = P2). Determina a qui aplica el
|
||||
// spawn aeri al final de la fase 0 d'INTRO; un slot amb `false` es
|
||||
// queda DISABLED tot el cicle de sala.
|
||||
std::array<bool, 2> players_starts_active_{false, false};
|
||||
|
||||
// Mil·lisegons des de l'entrada en ROOM_INTRO, per disparar la
|
||||
// cinemàtica de spawn aeri quan s'arriba a
|
||||
// `transition_config_.player_spawn_delay_ms`. Es reseteja a
|
||||
// `setState(ROOM_INTRO)`. `players_spawned_in_intro_` garanteix
|
||||
// que el trigger només es dispara una vegada per cicle.
|
||||
int room_intro_elapsed_ms_{0};
|
||||
bool players_spawned_in_intro_{false};
|
||||
|
||||
// Cinemàtica de spawn: posa els slots actius en AIRBORNE des d'una
|
||||
// altura fora de pantalla. Cridat per `updateRoomTransition` quan
|
||||
// `room_intro_elapsed_ms_` supera el delay configurat. Idempotent
|
||||
// (consulta `players_spawned_in_intro_`). També és el punt al qual
|
||||
// es connectarà el respawn post-mort en el futur.
|
||||
void triggerActivePlayerSpawn();
|
||||
|
||||
// Vista plana de punters crus — la fa servir Player com a llista
|
||||
// d'obstacles mòbils a la callback de col·lisió. Es construeix un sol
|
||||
// cop al ctor (els punters són estables perquè `players_` no es
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace RoomFsm {
|
||||
cfg.intro_text_enter_ms = readChildInt(n, "text_enter_ms", cfg.intro_text_enter_ms);
|
||||
cfg.intro_text_hold_ms = readChildInt(n, "text_hold_ms", cfg.intro_text_hold_ms);
|
||||
cfg.intro_text_exit_ms = readChildInt(n, "text_exit_ms", cfg.intro_text_exit_ms);
|
||||
cfg.player_spawn_delay_ms = readChildInt(n, "player_spawn_delay_ms", cfg.player_spawn_delay_ms);
|
||||
}
|
||||
if (yaml.contains("room_outro")) {
|
||||
const auto& n = yaml["room_outro"];
|
||||
|
||||
@@ -42,6 +42,13 @@ namespace RoomFsm {
|
||||
int outro_text_exit_ms{500};
|
||||
int outro_fade_out_ms{500};
|
||||
|
||||
// Mil·lisegons des de l'inici de ROOM_INTRO en què cau la
|
||||
// cinemàtica d'spawn aeri dels jugadors actius. Independent del
|
||||
// fade-in perquè el mateix valor servirà per al respawn post-mort
|
||||
// (que no té fade que el sincronitze). 500 ms = típicament el
|
||||
// moment en què el fade-in acaba i el text encara no ha entrat.
|
||||
int player_spawn_delay_ms{500};
|
||||
|
||||
UiConfig::CurveFn text_enter_curve{nullptr};
|
||||
UiConfig::CurveFn text_exit_curve{nullptr};
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user