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
+256
-80
No files matched your search
@@ -65,7 +65,7 @@ void EntityManager::checkPlayerCollisions(const std::vector<Player*>& players) {
|
|||||||
const auto BOX = entity->getCollisionRect();
|
const auto BOX = entity->getCollisionRect();
|
||||||
const int E_HEIGHT = entity->getHeight();
|
const int E_HEIGHT = entity->getHeight();
|
||||||
for (auto* p : players) {
|
for (auto* p : players) {
|
||||||
if (p == nullptr || p->isExited()) { continue; }
|
if (p == nullptr || p->isOutOfPlay()) { continue; }
|
||||||
// Filtre per altura: si el jugador està en l'aire i està per
|
// Filtre per altura: si el jugador està en l'aire i està per
|
||||||
// damunt de l'alçada del cos de l'entitat, no hi ha contacte.
|
// damunt de l'alçada del cos de l'entitat, no hi ha contacte.
|
||||||
// És el mecanisme del salt per esquivar momies, roques, etc.
|
// És el mecanisme del salt per esquivar momies, roques, etc.
|
||||||
|
|||||||
@@ -328,10 +328,10 @@ void Player::update(float delta_time) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
||||||
// EXITED: jugador despawnat. No xoca amb res ni res xoca amb ell.
|
// EXITED/DISABLED: jugador fora del joc. No xoca amb res ni res
|
||||||
// També salta jugadors EXITED del check actor↔actor més avall via
|
// xoca amb ell. També es filtren els altres jugadors fora de joc
|
||||||
// el filtre de `isExited` (vegeu el predicat any_of).
|
// al check actor↔actor més avall via el predicat `isOutOfPlay`.
|
||||||
if (state_ == State::EXITED) { return false; }
|
if (isOutOfPlay()) { return false; }
|
||||||
if (map_.collides(box)) { return true; }
|
if (map_.collides(box)) { return true; }
|
||||||
if (all_players_ == nullptr) { return false; }
|
if (all_players_ == nullptr) { return false; }
|
||||||
// Per al contacte actor-actor usem la hit-box petita a banda i banda:
|
// Per al contacte actor-actor usem la hit-box petita a banda i banda:
|
||||||
@@ -357,7 +357,7 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
|||||||
const float MY_Z = jump_z_;
|
const float MY_Z = jump_z_;
|
||||||
return std::ranges::any_of(*all_players_, [this, &PROPOSED_HIT, MY_AIR, MY_HURT, MY_Z](const Player* p) {
|
return std::ranges::any_of(*all_players_, [this, &PROPOSED_HIT, MY_AIR, MY_HURT, MY_Z](const Player* p) {
|
||||||
if (p == nullptr || p == this) { return false; }
|
if (p == nullptr || p == this) { return false; }
|
||||||
if (p->isExited()) { return false; }
|
if (p->isOutOfPlay()) { return false; }
|
||||||
if (MY_HURT || p->isHurt()) { return false; }
|
if (MY_HURT || p->isHurt()) { return false; }
|
||||||
if (MY_AIR != p->isAirborne()) { return false; }
|
if (MY_AIR != p->isAirborne()) { return false; }
|
||||||
// Aquí els dos estats coincideixen. Si tots dos volen, el filtre
|
// Aquí els dos estats coincideixen. Si tots dos volen, el filtre
|
||||||
@@ -407,10 +407,11 @@ void Player::integrateAxis(float& phys, float input, float delta_time, float mot
|
|||||||
// llegibilitat. La complexitat puja 1 per la guarda EXITED.
|
// llegibilitat. La complexitat puja 1 per la guarda EXITED.
|
||||||
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
|
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
|
||||||
void Player::tick(float delta_time) {
|
void Player::tick(float delta_time) {
|
||||||
// EXITED: jugador despawnat, no fa res. Cap input, cap física, cap
|
// EXITED/DISABLED: jugador fora del joc, no fa res. Cap input, cap
|
||||||
// bus event. Continua sent al vector de jugadors per simplicitat
|
// física, cap bus event. Continua sent al vector de jugadors per
|
||||||
// (no s'esborra) però queda completament inert fins al respawn.
|
// simplicitat (no s'esborra) però queda completament inert fins
|
||||||
if (state_ == State::EXITED) { return; }
|
// que GameScene el reactiva (respawn / spawnFromAir).
|
||||||
|
if (isOutOfPlay()) { return; }
|
||||||
|
|
||||||
// Reset per frame: el resolver del GameScene el tornarà a posar a
|
// Reset per frame: el resolver del GameScene el tornarà a posar a
|
||||||
// true si aquest frame hi ha push actiu. Així quan el push acaba
|
// true si aquest frame hi ha push actiu. Així quan el push acaba
|
||||||
@@ -719,6 +720,9 @@ void Player::resolveLandingOverlap() {
|
|||||||
if (all_players_ == nullptr) { return; }
|
if (all_players_ == nullptr) { return; }
|
||||||
for (Player* other : *all_players_) {
|
for (Player* other : *all_players_) {
|
||||||
if (other == nullptr || other == this) { continue; }
|
if (other == nullptr || other == this) { continue; }
|
||||||
|
// Salta jugadors fora del joc: un caigut sobre un slot DISABLED
|
||||||
|
// (o sobre un EXITED encara al vector) no els ha de projectar.
|
||||||
|
if (other->isOutOfPlay()) { continue; }
|
||||||
// Test amb la hit-box (8×8 de contacte precís), no la base-box.
|
// Test amb la hit-box (8×8 de contacte precís), no la base-box.
|
||||||
// Coherent amb la resta de col·lisions actor-actor del joc:
|
// Coherent amb la resta de col·lisions actor-actor del joc:
|
||||||
// momies, fuetades futures, resolver PvP… tots usen hit-box. Un
|
// momies, fuetades futures, resolver PvP… tots usen hit-box. Un
|
||||||
@@ -753,8 +757,8 @@ void Player::resolveLandingOverlap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Player::applyMovement(float delta_time) {
|
void Player::applyMovement(float delta_time) {
|
||||||
// EXITED: cap moviment ni col·lisió. Idèntic al gating de `tick`.
|
// EXITED/DISABLED: cap moviment ni col·lisió. Idèntic al gating de `tick`.
|
||||||
if (state_ == State::EXITED) { return; }
|
if (isOutOfPlay()) { return; }
|
||||||
|
|
||||||
// Integració del salt: si estem AIRBORNE, avança jump_z_/jump_vz_.
|
// Integració del salt: si estem AIRBORNE, avança jump_z_/jump_vz_.
|
||||||
// El facing i intended_v* es mantenen del moment del despegue (no es
|
// El facing i intended_v* es mantenen del moment del despegue (no es
|
||||||
@@ -875,9 +879,10 @@ void Player::publishTileEnteredIfChanged() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Player::render() {
|
void Player::render() {
|
||||||
// EXITED: invisible. La crida al render base es salta sencera; el
|
// EXITED/DISABLED: invisible. La crida al render base es salta
|
||||||
// jugador no apareix al z-sort tot i seguir vivint al vector.
|
// sencera; el jugador no apareix al z-sort tot i seguir vivint al
|
||||||
if (state_ == State::EXITED) { return; }
|
// vector.
|
||||||
|
if (isOutOfPlay()) { return; }
|
||||||
|
|
||||||
// Mentre el jugador està enlaire, el sprite es desplaça visualment
|
// Mentre el jugador està enlaire, el sprite es desplaça visualment
|
||||||
// cap amunt `jump_z_` píxels. Modifiquem `pos_.y` (la posició entera
|
// cap amunt `jump_z_` píxels. Modifiquem `pos_.y` (la posició entera
|
||||||
@@ -1054,6 +1059,54 @@ void Player::respawn(float x, float y) {
|
|||||||
setPosY(y);
|
setPosY(y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Player::disable() {
|
||||||
|
if (state_ == State::DISABLED) { return; }
|
||||||
|
state_ = State::DISABLED;
|
||||||
|
intended_vx_ = 0.0F;
|
||||||
|
intended_vy_ = 0.0F;
|
||||||
|
input_vx_ = 0.0F;
|
||||||
|
input_vy_ = 0.0F;
|
||||||
|
being_pushed_ = false;
|
||||||
|
jump_z_ = 0.0F;
|
||||||
|
jump_vz_ = 0.0F;
|
||||||
|
landing_stun_remaining_ms_ = 0.0F;
|
||||||
|
turning_remaining_ms_ = 0.0F;
|
||||||
|
pending_cardinal_ = Cardinal::NONE;
|
||||||
|
intermediate_cardinal_ = Cardinal::NONE;
|
||||||
|
last_input_cardinal_ = Cardinal::NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Player::spawnFromAir(float x, float y) {
|
||||||
|
// Net total: equival a un `respawn` seguit d'una entrada en l'aire.
|
||||||
|
// Així el flag de gir, l'animació, els flancs del salt i les
|
||||||
|
// velocitats residuals queden a un estat conegut abans de promocionar
|
||||||
|
// a AIRBORNE. Si hi havia un cardinal d'input penjat de la sessió
|
||||||
|
// anterior, no contamina la pròxima caiguda.
|
||||||
|
respawn(x, y);
|
||||||
|
|
||||||
|
// Altura derivada de la geometria: prou per arrencar amb el sprite
|
||||||
|
// sencer per damunt del bord superior del canvas (recordem que
|
||||||
|
// visualment es pinta a `pos_.y - jump_z_`). Afegim un xicotet marge
|
||||||
|
// perquè el primer frame ja siga clarament off-screen, no just al ras.
|
||||||
|
constexpr float OFFSCREEN_MARGIN_PX = 8.0F;
|
||||||
|
const float SPAWN_HEIGHT = y + static_cast<float>(sprite_h) + OFFSCREEN_MARGIN_PX;
|
||||||
|
|
||||||
|
state_ = State::AIRBORNE;
|
||||||
|
jump_z_ = SPAWN_HEIGHT;
|
||||||
|
// Caiguda recta: sense impuls inicial, deixem que la gravetat (la
|
||||||
|
// mateixa que la del salt normal) faça la feina. Velocitat horitzontal
|
||||||
|
// a zero perquè no es desviï del passadís d'spawn.
|
||||||
|
jump_vz_ = 0.0F;
|
||||||
|
intended_vx_ = 0.0F;
|
||||||
|
intended_vy_ = 0.0F;
|
||||||
|
|
||||||
|
// Mirant cap avall durant la caiguda — `animationName(AIRBORNE)` amb
|
||||||
|
// facing DOWN ja resol a `jump_down`. `respawn` ja ha posat facing a
|
||||||
|
// DOWN; ho deixem explícit ací per claredat de la cinemàtica.
|
||||||
|
facing_ = Facing::DOWN;
|
||||||
|
setCurrentAnimation("jump_down");
|
||||||
|
}
|
||||||
|
|
||||||
auto Player::getBaseBox() const -> SDL_FRect {
|
auto Player::getBaseBox() const -> SDL_FRect {
|
||||||
return {
|
return {
|
||||||
.x = getPosX() + static_cast<float>(collision_x_offset),
|
.x = getPosX() + static_cast<float>(collision_x_offset),
|
||||||
@@ -1144,6 +1197,7 @@ auto Player::tryPushSnapBlockers(
|
|||||||
const SDL_FRect MY_HIT = getHitBox();
|
const SDL_FRect MY_HIT = getHitBox();
|
||||||
for (Player* p : *all_players_) {
|
for (Player* p : *all_players_) {
|
||||||
if (p == nullptr || p == this) { continue; }
|
if (p == nullptr || p == this) { continue; }
|
||||||
|
if (p->isOutOfPlay()) { continue; }
|
||||||
const SDL_FRect OTHER_HIT = p->getHitBox();
|
const SDL_FRect OTHER_HIT = p->getHitBox();
|
||||||
if (SDL_HasRectIntersectionFloat(&MY_HIT, &OTHER_HIT)) {
|
if (SDL_HasRectIntersectionFloat(&MY_HIT, &OTHER_HIT)) {
|
||||||
blockers.push_back(p);
|
blockers.push_back(p);
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ class Player : public AnimatedSprite {
|
|||||||
// posteriors a `exit()` són no-op. El reset el porta `respawn()`
|
// posteriors a `exit()` són no-op. El reset el porta `respawn()`
|
||||||
// (cridada per GameScene en transició OUTRO → INTRO).
|
// (cridada per GameScene en transició OUTRO → INTRO).
|
||||||
EXITED,
|
EXITED,
|
||||||
|
// Slot de jugador inactiu: instanciat però fora de partida. Mateix
|
||||||
|
// perfil físic que EXITED (invisible, sense input, sense col·lisió,
|
||||||
|
// ignorat pel resolver PvP), però amb semàntica diferent: no
|
||||||
|
// compta com a "ha completat la sala" ni dispara `level_complete`.
|
||||||
|
// Usat per (a) el slot del segon jugador en single-player, (b)
|
||||||
|
// tots els slots durant les transicions de sala abans del spawn
|
||||||
|
// aeri, i (c) en el futur, l'estat post-mort abans del respawn.
|
||||||
|
DISABLED,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Geometria carregada des del YAML del player (p.ex. `data/player/sam.yaml`).
|
// Geometria carregada des del YAML del player (p.ex. `data/player/sam.yaml`).
|
||||||
@@ -184,6 +192,15 @@ class Player : public AnimatedSprite {
|
|||||||
[[nodiscard]] auto isAirborne() const -> bool { return state_ == State::AIRBORNE; }
|
[[nodiscard]] auto isAirborne() const -> bool { return state_ == State::AIRBORNE; }
|
||||||
[[nodiscard]] auto isHurt() const -> bool { return state_ == State::HURT; }
|
[[nodiscard]] auto isHurt() const -> bool { return state_ == State::HURT; }
|
||||||
[[nodiscard]] auto isExited() const -> bool { return state_ == State::EXITED; }
|
[[nodiscard]] auto isExited() const -> bool { return state_ == State::EXITED; }
|
||||||
|
[[nodiscard]] auto isDisabled() const -> bool { return state_ == State::DISABLED; }
|
||||||
|
// Resum dels estats "fora del món": EXITED i DISABLED comparteixen el
|
||||||
|
// mateix perfil físic (no es pinten, no llegeixen input, no
|
||||||
|
// col·lisionen, no participen al resolver PvP). Tots dos gates de
|
||||||
|
// l'update i el render comproven aquesta combinació amb una sola
|
||||||
|
// crida per evitar oblits si demà afegim un tercer estat-fora-del-món.
|
||||||
|
[[nodiscard]] auto isOutOfPlay() const -> bool {
|
||||||
|
return state_ == State::EXITED || state_ == State::DISABLED;
|
||||||
|
}
|
||||||
// Tangible per a contactes jugador↔jugador: només els estats a terra
|
// Tangible per a contactes jugador↔jugador: només els estats a terra
|
||||||
// amb cos sòlid. AIRBORNE i HURT són intangibles (es travessen).
|
// amb cos sòlid. AIRBORNE i HURT són intangibles (es travessen).
|
||||||
// EXITED tampoc — el jugador ha eixit lògicament i ja no és al món.
|
// EXITED tampoc — el jugador ha eixit lògicament i ja no és al món.
|
||||||
@@ -216,9 +233,32 @@ class Player : public AnimatedSprite {
|
|||||||
// Reset complet a l'estat inicial: posició a (x, y), velocitats a 0,
|
// Reset complet a l'estat inicial: posició a (x, y), velocitats a 0,
|
||||||
// estat GROUNDED, qualsevol flag de control reinicialitzat. Cridat
|
// estat GROUNDED, qualsevol flag de control reinicialitzat. Cridat
|
||||||
// per GameScene a la transició ROOM_OUTRO → ROOM_INTRO. Idempotent
|
// per GameScene a la transició ROOM_OUTRO → ROOM_INTRO. Idempotent
|
||||||
// (es pot cridar des de qualsevol estat).
|
// (es pot cridar des de qualsevol estat). Es manté disponible per a
|
||||||
|
// qualsevol cas futur on calgui aparèixer el jugador ja a terra
|
||||||
|
// sense la cinemàtica de caiguda; el flux estàndard d'arrencada de
|
||||||
|
// sala usa `disable()` + `spawnFromAir()`.
|
||||||
void respawn(float x, float y);
|
void respawn(float x, float y);
|
||||||
|
|
||||||
|
// Posa el jugador en el slot inactiu (DISABLED). Idempotent: la
|
||||||
|
// segona crida és no-op. Mateix patró que `exit()` per simetria.
|
||||||
|
// No publica res al bus — el GameScene decideix quan disparar el
|
||||||
|
// `spawnFromAir` posterior, i la transició DISABLED↔jugant és
|
||||||
|
// ortogonal a `level_complete`. Útil per:
|
||||||
|
// - el slot del segon jugador en single-player,
|
||||||
|
// - el període entre setState(ROOM_INTRO) i el trigger del spawn,
|
||||||
|
// - el futur estat post-mort (abans del respawn temporitzat).
|
||||||
|
void disable();
|
||||||
|
|
||||||
|
// Cinemàtica d'aparició: posiciona el jugador al seu tile final
|
||||||
|
// (x, y al sistema de coordenades del sprite) i el deixa en
|
||||||
|
// AIRBORNE a una altura derivada de la geometria del playfield —
|
||||||
|
// prou per arrencar fora del marc i caure recte fins al sòl. La
|
||||||
|
// velocitat horitzontal queda a zero (caiguda perfectament
|
||||||
|
// vertical). En tocar terra, el flux normal d'AIRBORNE→
|
||||||
|
// LANDING_STUN→GROUNDED el reactiva amb input. Reutilitzable per
|
||||||
|
// al respawn post-mort: mateixa entrada cinemàtica.
|
||||||
|
void spawnFromAir(float x, float y);
|
||||||
|
|
||||||
// Alçada actual del salt (píxels visuals per damunt del sòl).
|
// Alçada actual del salt (píxels visuals per damunt del sòl).
|
||||||
// 0 quan està a terra. L'usa el render i el sistema de col·lisions
|
// 0 quan està a terra. L'usa el render i el sistema de col·lisions
|
||||||
// per altura.
|
// per altura.
|
||||||
|
|||||||
@@ -113,12 +113,12 @@ namespace {
|
|||||||
// surt projectat sense control i els dos es travessen. Coincideix
|
// surt projectat sense control i els dos es travessen. Coincideix
|
||||||
// amb la regla d'`Player::collidesWithWorld`.
|
// amb la regla d'`Player::collidesWithWorld`.
|
||||||
if (a->isHurt() || b->isHurt()) { return; }
|
if (a->isHurt() || b->isHurt()) { return; }
|
||||||
// EXITED: el jugador despawnat no és tangible. Si entrés al
|
// EXITED/DISABLED: jugadors fora del joc, no participen al resolver.
|
||||||
// resolver, la seua presència física frenaria l'altre (per
|
// 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
|
// overlap predictiu) tot i que `applyMovement` és no-op. Sortir
|
||||||
// d'aquí garanteix que el segon jugador pot creuar la porta
|
// d'aquí garanteix que el jugador actiu pot creuar la porta o
|
||||||
// sense interferència del primer ja eixit.
|
// moure's lliurement sense interferència d'un slot inactiu.
|
||||||
if (a->isExited() || b->isExited()) { return; }
|
if (a->isOutOfPlay() || b->isOutOfPlay()) { return; }
|
||||||
|
|
||||||
// Aire ↔ tierra: el saltador es travessa net del caminant. Sense
|
// Aire ↔ tierra: el saltador es travessa net del caminant. Sense
|
||||||
// resolver, sense head-on, sense push. Els dos continuen amb la
|
// 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_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};
|
||||||
|
|
||||||
std::string anim_p1;
|
// Carreguem assets per als DOS jugadors sempre, encara que algun no
|
||||||
std::string anim_p2;
|
// arranque actiu: l'slot existeix igualment al vector com a DISABLED
|
||||||
if (USE_P1) { anim_p1 = load_player_assets(config_.player_sprite, config_.player_yaml); }
|
// i podria reactivar-se amb un futur "join in progress". El cost de
|
||||||
if (USE_P2) { anim_p2 = load_player_assets(config_.player2_sprite, config_.player2_yaml); }
|
// 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
|
// 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
|
// del mateix YAML que l'animació. Ha d'estar feta abans d'instanciar
|
||||||
// Player perquè el spawn consulta `collision_*`. Els membres de Player
|
// Player perquè el spawn consulta `collision_*`. Els membres de Player
|
||||||
// són `static inline`, compartits per tots els jugadors: cridem
|
// són `static inline`, compartits per tots els jugadors; carregar des
|
||||||
// `loadFrom` amb el YAML del primer jugador actiu (P1 o P2). Els dos
|
// del YAML de P1 és suficient (els dos personatges comparteixen
|
||||||
// personatges usen la mateixa geometria (morcus és un repaletat de sam).
|
// geometria — morcus és un repaletat de sam).
|
||||||
Player::loadFrom(USE_P1 ? config_.player_yaml : config_.player2_yaml);
|
Player::loadFrom(config_.player_yaml);
|
||||||
|
|
||||||
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
|
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
|
||||||
// etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa
|
// 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
|
// `Map::door_col` perquè canviar la columna de la porta al YAML
|
||||||
// moga automàticament l'spawn — no hi ha cap valor hardcoded.
|
// moga automàticament l'spawn — no hi ha cap valor hardcoded.
|
||||||
const auto SPAWN = computeSpawn();
|
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
|
// Índex de tile amb què pinta cada jugador — llegit del YAML del mapa
|
||||||
// (camps opcionals `player1_paint_tile` / `player2_paint_tile`). Si no
|
// (camps opcionals `player1_paint_tile` / `player2_paint_tile`). Si no
|
||||||
@@ -331,16 +334,29 @@ GameScene::GameScene(Config config)
|
|||||||
// puntuació (veure `Event::player_id`).
|
// puntuació (veure `Event::player_id`).
|
||||||
constexpr int ID_P1 = 0;
|
constexpr int ID_P1 = 0;
|
||||||
constexpr int ID_P2 = 1;
|
constexpr int ID_P2 = 1;
|
||||||
if (USE_P1) {
|
|
||||||
const int P1_PAINT = readOptionalInt(config_.map_yaml, "player1_paint_tile", -1);
|
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 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));
|
// 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
|
// PaintLayer: font única del pintat del nivell. Es crea després de
|
||||||
// `loadFromLevel` perquè l'EntityManager ja ha de tenir els sarcòfags
|
// `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_); }
|
for (auto& p : players_) { p->setOtherPlayers(&players_view_); }
|
||||||
|
|
||||||
// Comptador d'EXITED a EntityManager: publica `level_complete` quan
|
// Comptador d'EXITED a EntityManager: publica `level_complete` quan
|
||||||
// tots els jugadors actius han travessat la porta. Cap setExpectedPlayerCount
|
// tots els jugadors *actius* han travessat la porta. Els slots DISABLED
|
||||||
// ha d'inicialitzar-se abans del primer `player_exited`; ho fem aquí
|
// permanents (single-player) NO compten — d'altra manera la sala mai
|
||||||
// perquè ja sabem el nombre real d'instàncies.
|
// es completaria, perquè un slot DISABLED no pot eixir per la porta.
|
||||||
entity_manager_.setExpectedPlayerCount(static_cast<int>(players_.size()));
|
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:
|
// 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.
|
||||||
for (auto& p : players_) { p->setPushFactor(push_factor_); }
|
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.
|
// CLOSED amb visited buit, momies i altres actors a la posició inicial.
|
||||||
Map::loadFrom(config_.map_yaml);
|
Map::loadFrom(config_.map_yaml);
|
||||||
entity_manager_.reload(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(); }
|
if (paint_layer_) { paint_layer_->clear(); }
|
||||||
|
|
||||||
// Respawn dels jugadors. Recalcular spawn per si door_col canvia
|
// Tornem tots els slots a DISABLED. Els actius els reactivarà el
|
||||||
// (avui el mapa no canvia entre cicles, però la lògica ja queda
|
// trigger d'spawn aeri dins de `updateRoomTransition` quan torni a
|
||||||
// preparada per a l'itinerari futur).
|
// disparar-se el delay del nou cicle d'INTRO. Així cada cicle de
|
||||||
const auto SPAWN = computeSpawn();
|
// sala repeteix la cinemàtica de caiguda de la mateixa manera que
|
||||||
const bool BOTH = (players_.size() >= 2);
|
// el primer.
|
||||||
// Mode single-player: l'únic jugador queda centrat (= SPAWN.p1_x +
|
for (auto& p : players_) { p->disable(); }
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << "[ROOM] reset: entities reloaded, paint cleared, "
|
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) {
|
void GameScene::setState(RoomFsm::State new_state) {
|
||||||
@@ -471,6 +507,12 @@ void GameScene::setState(RoomFsm::State new_state) {
|
|||||||
const auto PREV = state_;
|
const auto PREV = state_;
|
||||||
state_ = new_state;
|
state_ = new_state;
|
||||||
transition_ = {};
|
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
|
// En tornar a ROOM_INTRO des de ROOM_OUTRO (placeholder de
|
||||||
// "torna a la mateixa sala"), reseteja completament l'estat del
|
// "torna a la mateixa sala"), reseteja completament l'estat del
|
||||||
// nivell. Si la transició és la inicial (PREV també és ROOM_INTRO,
|
// 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) {
|
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);
|
const auto NEXT = RoomFsm::advance(state_, transition_, transition_config_, delta_ms);
|
||||||
if (NEXT != state_) { setState(NEXT); }
|
if (NEXT != state_) { setState(NEXT); }
|
||||||
}
|
}
|
||||||
@@ -725,15 +778,9 @@ void GameScene::update(int delta_ms) {
|
|||||||
constexpr bool EDITOR_FROZEN = false;
|
constexpr bool EDITOR_FROZEN = false;
|
||||||
#endif
|
#endif
|
||||||
const int SCALED_DELTA_MS = static_cast<int>(static_cast<float>(delta_ms) * pause_.scale);
|
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) {
|
if (!EDITOR_FROZEN && SCALED_DELTA_MS > 0) {
|
||||||
const float DT = static_cast<float>(SCALED_DELTA_MS) / 1000.0F;
|
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
|
// Pipeline de moviment en tres passes per poder intercalar la
|
||||||
// resolució de contactes jugador↔jugador entre input i posició:
|
// resolució de contactes jugador↔jugador entre input i posició:
|
||||||
// 1) tick: cada jugador calcula la velocitat intencionada
|
// 1) tick: cada jugador calcula la velocitat intencionada
|
||||||
@@ -744,10 +791,14 @@ void GameScene::update(int delta_ms) {
|
|||||||
// pusher faça `tryMove`, el pushed ja estiga fora de la
|
// pusher faça `tryMove`, el pushed ja estiga fora de la
|
||||||
// seua caixa. La callback també tracta l'altre jugador com
|
// seua caixa. La callback també tracta l'altre jugador com
|
||||||
// a sòlid: xarxa de seguretat davant de qualsevol inexactitud.
|
// 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); }
|
for (auto& player : players_) { player->tick(DT); }
|
||||||
resolvePlayerVsPlayer(players_view_, DT, push_factor_);
|
resolvePlayerVsPlayer(players_view_, DT, push_factor_);
|
||||||
for (auto* player : orderedForMovement(players_view_)) { player->applyMovement(DT); }
|
for (auto* player : orderedForMovement(players_view_)) { player->applyMovement(DT); }
|
||||||
}
|
|
||||||
|
|
||||||
// El tick d'entitats DESPRÉS del tick dels jugadors: si el jugador ha
|
// 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
|
// emès `on_tile_entered` aquest frame, l'EntityManager ja l'ha encaminat
|
||||||
@@ -755,13 +806,12 @@ void GameScene::update(int delta_ms) {
|
|||||||
// aplicar la transició OPENING → OPEN en el mateix frame.
|
// aplicar la transició OPENING → OPEN en el mateix frame.
|
||||||
entity_manager_.update(DT);
|
entity_manager_.update(DT);
|
||||||
|
|
||||||
// Detecció de contacte player↔entitat després que totes les entitats
|
// Detecció de contacte player↔entitat. `checkPlayerCollisions` ja
|
||||||
// hagin mogut aquest frame. Reutilitzem la vista plana que ja
|
// filtra jugadors fora del joc (`isOutOfPlay`), així que durant
|
||||||
// manté el GameScene per al resolver player↔player.
|
// l'INTRO previ a l'spawn no es disparen contactes amb les
|
||||||
if (GAMEPLAY_ACTIVE) {
|
// entitats que ja s'animen al fons.
|
||||||
entity_manager_.checkPlayerCollisions(players_view_);
|
entity_manager_.checkPlayerCollisions(players_view_);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
editor_.update(delta_ms, tilemap_);
|
editor_.update(delta_ms, tilemap_);
|
||||||
@@ -799,19 +849,14 @@ void GameScene::render() {
|
|||||||
// explícita: l'editor edita el tilemap, no la capa de pintat).
|
// explícita: l'editor edita el tilemap, no la capa de pintat).
|
||||||
if (!SKIP_ACTORS && paint_layer_) { paint_layer_->render(); }
|
if (!SKIP_ACTORS && paint_layer_) { paint_layer_->render(); }
|
||||||
if (!SKIP_ACTORS) {
|
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
|
// Passada de sombres dels jugadors saltant. Va per davant del
|
||||||
// pintat (al damunt) però per darrere de TOTS els actors —
|
// pintat (al damunt) però per darrere de TOTS els actors —
|
||||||
// entitats i jugadors, inclòs el saltador mateix— així la sombra
|
// entitats i jugadors, inclòs el saltador mateix— així la sombra
|
||||||
// queda al sòl sense tapar res. `renderShadow` és no-op si el
|
// queda al sòl sense tapar res. `renderShadow` és no-op si el
|
||||||
// jugador està a terra, així que no cal filtrar al callsite.
|
// 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(); }
|
for (const auto& p : players_) { p->renderShadow(); }
|
||||||
}
|
|
||||||
// Z-order interleaving entitats ↔ players per `getFeetY()` ascendent:
|
// Z-order interleaving entitats ↔ players per `getFeetY()` ascendent:
|
||||||
// els actors amb els peus més amunt queden dibuixats primer (al fons),
|
// 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
|
// 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`.
|
Player* player; // nullable; si és nullptr, s'usa `entity`.
|
||||||
};
|
};
|
||||||
std::vector<Renderable> order;
|
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();
|
const auto ENTITIES = entity_manager_.getEntities();
|
||||||
std::ranges::transform(ENTITIES, std::back_inserter(order), [](Entity* e) {
|
std::ranges::transform(ENTITIES, std::back_inserter(order), [](Entity* e) {
|
||||||
return Renderable{.feet_y = e->getFeetY(), .entity = e, .player = nullptr};
|
return Renderable{.feet_y = e->getFeetY(), .entity = e, .player = nullptr};
|
||||||
});
|
});
|
||||||
if (RENDER_PLAYERS) {
|
// 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) {
|
std::ranges::transform(players_, std::back_inserter(order), [](const auto& p) {
|
||||||
return Renderable{.feet_y = p->getFeetY(), .entity = nullptr, .player = p.get()};
|
return Renderable{.feet_y = p->getFeetY(), .entity = nullptr, .player = p.get()};
|
||||||
});
|
});
|
||||||
}
|
|
||||||
std::ranges::stable_sort(order, [](const Renderable& a, const Renderable& b) {
|
std::ranges::stable_sort(order, [](const Renderable& a, const Renderable& b) {
|
||||||
return a.feet_y < b.feet_y;
|
return a.feet_y < b.feet_y;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -84,8 +85,34 @@ 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
|
||||||
|
// 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_;
|
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
|
// 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
|
// 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
|
// 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_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_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.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")) {
|
if (yaml.contains("room_outro")) {
|
||||||
const auto& n = yaml["room_outro"];
|
const auto& n = yaml["room_outro"];
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ namespace RoomFsm {
|
|||||||
int outro_text_exit_ms{500};
|
int outro_text_exit_ms{500};
|
||||||
int outro_fade_out_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_enter_curve{nullptr};
|
||||||
UiConfig::CurveFn text_exit_curve{nullptr};
|
UiConfig::CurveFn text_exit_curve{nullptr};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace Texts {
|
namespace Texts {
|
||||||
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
||||||
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
||||||
constexpr const char* VERSION = "0.24.0"; // Single source of truth — Makefile el grep
|
constexpr const char* VERSION = "0.25.0"; // Single source of truth — Makefile el grep
|
||||||
} // namespace Texts
|
} // namespace Texts
|
||||||
|
|
||||||
namespace GameCanvas {
|
namespace GameCanvas {
|
||||||
|
|||||||
Reference in new issue
Block a user