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
@@ -65,7 +65,7 @@ void EntityManager::checkPlayerCollisions(const std::vector<Player*>& players) {
|
||||
const auto BOX = entity->getCollisionRect();
|
||||
const int E_HEIGHT = entity->getHeight();
|
||||
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
|
||||
// damunt de l'alçada del cos de l'entitat, no hi ha contacte.
|
||||
// É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 {
|
||||
// EXITED: jugador despawnat. No xoca amb res ni res xoca amb ell.
|
||||
// També salta jugadors EXITED del check actor↔actor més avall via
|
||||
// el filtre de `isExited` (vegeu el predicat any_of).
|
||||
if (state_ == State::EXITED) { return false; }
|
||||
// EXITED/DISABLED: jugador fora del joc. No xoca amb res ni res
|
||||
// xoca amb ell. També es filtren els altres jugadors fora de joc
|
||||
// al check actor↔actor més avall via el predicat `isOutOfPlay`.
|
||||
if (isOutOfPlay()) { return false; }
|
||||
if (map_.collides(box)) { return true; }
|
||||
if (all_players_ == nullptr) { return false; }
|
||||
// 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_;
|
||||
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->isExited()) { return false; }
|
||||
if (p->isOutOfPlay()) { return false; }
|
||||
if (MY_HURT || p->isHurt()) { return false; }
|
||||
if (MY_AIR != p->isAirborne()) { return false; }
|
||||
// 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.
|
||||
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
|
||||
void Player::tick(float delta_time) {
|
||||
// EXITED: jugador despawnat, no fa res. Cap input, cap física, cap
|
||||
// bus event. Continua sent al vector de jugadors per simplicitat
|
||||
// (no s'esborra) però queda completament inert fins al respawn.
|
||||
if (state_ == State::EXITED) { return; }
|
||||
// EXITED/DISABLED: jugador fora del joc, no fa res. Cap input, cap
|
||||
// física, cap bus event. Continua sent al vector de jugadors per
|
||||
// simplicitat (no s'esborra) però queda completament inert fins
|
||||
// que GameScene el reactiva (respawn / spawnFromAir).
|
||||
if (isOutOfPlay()) { return; }
|
||||
|
||||
// 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
|
||||
@@ -719,6 +720,9 @@ void Player::resolveLandingOverlap() {
|
||||
if (all_players_ == nullptr) { return; }
|
||||
for (Player* other : *all_players_) {
|
||||
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.
|
||||
// Coherent amb la resta de col·lisions actor-actor del joc:
|
||||
// momies, fuetades futures, resolver PvP… tots usen hit-box. Un
|
||||
@@ -753,8 +757,8 @@ void Player::resolveLandingOverlap() {
|
||||
}
|
||||
|
||||
void Player::applyMovement(float delta_time) {
|
||||
// EXITED: cap moviment ni col·lisió. Idèntic al gating de `tick`.
|
||||
if (state_ == State::EXITED) { return; }
|
||||
// EXITED/DISABLED: cap moviment ni col·lisió. Idèntic al gating de `tick`.
|
||||
if (isOutOfPlay()) { return; }
|
||||
|
||||
// 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
|
||||
@@ -875,9 +879,10 @@ void Player::publishTileEnteredIfChanged() {
|
||||
}
|
||||
|
||||
void Player::render() {
|
||||
// EXITED: invisible. La crida al render base es salta sencera; el
|
||||
// jugador no apareix al z-sort tot i seguir vivint al vector.
|
||||
if (state_ == State::EXITED) { return; }
|
||||
// EXITED/DISABLED: invisible. La crida al render base es salta
|
||||
// sencera; el jugador no apareix al z-sort tot i seguir vivint al
|
||||
// vector.
|
||||
if (isOutOfPlay()) { return; }
|
||||
|
||||
// Mentre el jugador està enlaire, el sprite es desplaça visualment
|
||||
// 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);
|
||||
}
|
||||
|
||||
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 {
|
||||
return {
|
||||
.x = getPosX() + static_cast<float>(collision_x_offset),
|
||||
@@ -1144,6 +1197,7 @@ auto Player::tryPushSnapBlockers(
|
||||
const SDL_FRect MY_HIT = getHitBox();
|
||||
for (Player* p : *all_players_) {
|
||||
if (p == nullptr || p == this) { continue; }
|
||||
if (p->isOutOfPlay()) { continue; }
|
||||
const SDL_FRect OTHER_HIT = p->getHitBox();
|
||||
if (SDL_HasRectIntersectionFloat(&MY_HIT, &OTHER_HIT)) {
|
||||
blockers.push_back(p);
|
||||
|
||||
@@ -50,6 +50,14 @@ class Player : public AnimatedSprite {
|
||||
// posteriors a `exit()` són no-op. El reset el porta `respawn()`
|
||||
// (cridada per GameScene en transició OUTRO → INTRO).
|
||||
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`).
|
||||
@@ -184,6 +192,15 @@ class Player : public AnimatedSprite {
|
||||
[[nodiscard]] auto isAirborne() const -> bool { return state_ == State::AIRBORNE; }
|
||||
[[nodiscard]] auto isHurt() const -> bool { return state_ == State::HURT; }
|
||||
[[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
|
||||
// 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.
|
||||
@@ -216,9 +233,32 @@ class Player : public AnimatedSprite {
|
||||
// Reset complet a l'estat inicial: posició a (x, y), velocitats a 0,
|
||||
// estat GROUNDED, qualsevol flag de control reinicialitzat. Cridat
|
||||
// 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);
|
||||
|
||||
// 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).
|
||||
// 0 quan està a terra. L'usa el render i el sistema de col·lisions
|
||||
// per altura.
|
||||
|
||||
Reference in new issue
Block a user