feat(player): saltar travessa el caminant; rebot mid-aire entre dos saltadors

This commit is contained in:
JailDesigner committed 2026-04-25 14:20:38 +02:00
1 parent 3646e6a4aa
commit 188f035333
4 files changed
+78 -32

No files matched your search

+11 -6
View File
@@ -15,11 +15,16 @@ collision:
# `size` es reserva per a la col·lisió amb murs/sarcòfags i per enfilar # `size` es reserva per a la col·lisió amb murs/sarcòfags i per enfilar
# els passadissos. Centrada en el mateix punt que la caixa base. # els passadissos. Centrada en el mateix punt que la caixa base.
hit_size: 8 hit_size: 8
# Alçada del cos en píxels per al filtre de col·lisió per altura: un # Alçada del cos en píxels. Funció DOBLE segons amb què col·lisione:
# actor saltant amb jump_z > height passa per damunt sense col·lisió. # · Contra entitats (mummy, rolling_rock, sarc): si `jump_z > height`
# Mateix camp i semàntica que als YAML d'entitats (mummy, rolling_rock, # d'aquella entitat, no hi ha col·lisió. Igual que als YAMLs
# etc.) — així el filtre queda uniforme per a tots els colls·lisionables. # d'entitats.
height: 24 # · Contra altres jugadors: la col·lisió és binària per estat aeri.
# Si un està en l'aire i l'altre no → cap col·lisió. Si tots dos
# estan en l'aire, llavors `body_height` decideix si els rangs
# verticals solapen i s'aplica el rebot mid-aire. `0` = sense cos
# (ni rebot quan tots dos volen).
height: 12
# Animacions en mapa (clau = nom). Camps per animació: # Animacions en mapa (clau = nom). Camps per animació:
# frames → llista d'índexs dins de l'spritesheet. # frames → llista d'índexs dins de l'spritesheet.
@@ -92,7 +97,7 @@ turn:
# d'entitats: és una propietat de col·lisió, no de la física del salt. # d'entitats: és una propietat de col·lisió, no de la física del salt.
# Amb impulse=240 i gravity=600: pic ≈ 48 px, temps total en l'aire ≈ 0.8 s. # Amb impulse=240 i gravity=600: pic ≈ 48 px, temps total en l'aire ≈ 0.8 s.
jump: jump:
impulse: 240 impulse: 200
gravity: 600 gravity: 600
landing_stun_ms: 200 landing_stun_ms: 200
shadow_y_offset: 3 shadow_y_offset: 3
+11 -6
View File
@@ -15,11 +15,16 @@ collision:
# `size` es reserva per a la col·lisió amb murs/sarcòfags i per enfilar # `size` es reserva per a la col·lisió amb murs/sarcòfags i per enfilar
# els passadissos. Centrada en el mateix punt que la caixa base. # els passadissos. Centrada en el mateix punt que la caixa base.
hit_size: 8 hit_size: 8
# Alçada del cos en píxels per al filtre de col·lisió per altura: un # Alçada del cos en píxels. Funció DOBLE segons amb què col·lisione:
# actor saltant amb jump_z > height passa per damunt sense col·lisió. # · Contra entitats (mummy, rolling_rock, sarc): si `jump_z > height`
# Mateix camp i semàntica que als YAML d'entitats (mummy, rolling_rock, # d'aquella entitat, no hi ha col·lisió. Igual que als YAMLs
# etc.) — així el filtre queda uniforme per a tots els colls·lisionables. # d'entitats.
height: 24 # · Contra altres jugadors: la col·lisió és binària per estat aeri.
# Si un està en l'aire i l'altre no → cap col·lisió. Si tots dos
# estan en l'aire, llavors `body_height` decideix si els rangs
# verticals solapen i s'aplica el rebot mid-aire. `0` = sense cos
# (ni rebot quan tots dos volen).
height: 12
# Animacions en mapa (clau = nom). Camps per animació: # Animacions en mapa (clau = nom). Camps per animació:
# frames → llista d'índexs dins de l'spritesheet. # frames → llista d'índexs dins de l'spritesheet.
@@ -92,7 +97,7 @@ turn:
# d'entitats: és una propietat de col·lisió, no de la física del salt. # d'entitats: és una propietat de col·lisió, no de la física del salt.
# Amb impulse=240 i gravity=600: pic ≈ 48 px, temps total en l'aire ≈ 0.8 s. # Amb impulse=240 i gravity=600: pic ≈ 48 px, temps total en l'aire ≈ 0.8 s.
jump: jump:
impulse: 240 impulse: 200
gravity: 600 gravity: 600
landing_stun_ms: 200 landing_stun_ms: 200
shadow_y_offset: 3 shadow_y_offset: 3
+19 -9
View File
@@ -322,17 +322,27 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
// (16×16) segueix topant amb murs/sarcs però els jugadors poden // (16×16) segueix topant amb murs/sarcs però els jugadors poden
// acostar-se fins al contacte real. // acostar-se fins al contacte real.
// //
// Filtre per altura: si aquest jugador (this) està en l'aire i passa // Regla de col·lisió jugador↔jugador (segons el feel triat):
// per damunt del cos de l'altre, no hi ha col·lisió — saltar permet // - Tots dos a terra → col·lisió normal.
// sobrevolar l'altre. `body_height` és estàtic (compartit entre // - Un en l'aire i l'altre no → SENSE col·lisió. El saltador es
// jugadors), però per al filtre val la pena consultar-lo per actor // travessa net del caminant; ningú actua de "muro" durant el
// perquè en el futur cada character podria tenir-ne un de propi. // vol, ni a la pujada ni a la baixada.
// - Tots dos en l'aire → col·lisió només si els rangs verticals
// [jump_z, jump_z + body_height] solapen. Així permetem el xoc
// mid-aire quan tots dos van un contra l'altre i estan a la
// mateixa franja d'alçada.
const SDL_FRect PROPOSED_HIT = hitBoxFromBase(box); const SDL_FRect PROPOSED_HIT = hitBoxFromBase(box);
const float Z = jump_z_; const bool MY_AIR = (state_ == State::AIRBORNE);
const bool AIRBORNE = (state_ == State::AIRBORNE); const float MY_Z = jump_z_;
return std::ranges::any_of(*all_players_, [this, &PROPOSED_HIT, AIRBORNE, Z](const Player* p) { return std::ranges::any_of(*all_players_, [this, &PROPOSED_HIT, MY_AIR, MY_Z](const Player* p) {
if (p == nullptr || p == this) { return false; } if (p == nullptr || p == this) { return false; }
if (AIRBORNE && body_height > 0 && Z > static_cast<float>(body_height)) { return false; } if (MY_AIR != p->isAirborne()) { return false; }
// Aquí els dos estats coincideixen. Si tots dos volen, el filtre
// per altura mira que els rangs verticals encara solapin; si no,
// estan a prou distància en Y "real" i no es toquen.
if (MY_AIR && body_height > 0 && std::fabs(MY_Z - p->jump_z_) >= static_cast<float>(body_height)) {
return false;
}
const auto OTHER = p->getHitBox(); const auto OTHER = p->getHitBox();
return SDL_HasRectIntersectionFloat(&PROPOSED_HIT, &OTHER); return SDL_HasRectIntersectionFloat(&PROPOSED_HIT, &OTHER);
}); });
+37 -11
View File
@@ -103,22 +103,22 @@ namespace {
auto* b = ps[1]; auto* b = ps[1];
if (a == nullptr || b == nullptr) { return; } if (a == nullptr || b == nullptr) { return; }
// Filtre per altura: si un dels dos està en l'aire per damunt const bool A_AIR = a->isAirborne();
// del cos de l'altre, no hi ha contacte possible — el saltador const bool B_AIR = b->isAirborne();
// sobrevola. Saltar fa de "moviment lliure" per al resolver.
// `body_height` és estàtic carregat des del YAML; cppcheck el llig // Aire ↔ tierra: el saltador es travessa net del caminant. Sense
// com a sempre-zero perquè no veu la inicialització en runtime. // resolver, sense head-on, sense push. Els dos continuen amb la
const auto BODY_H = static_cast<float>(Player::body_height); // velocitat que portaven. Important: això NO és el filtre per
// cppcheck-suppress knownConditionTrueFalse // altura del cos — és binari. Coincideix amb la regla d'`Player::
if (BODY_H > 0.0F && a->isAirborne() && a->getJumpZ() > BODY_H) { return; } // collidesWithWorld`: durant un salt no hi ha mai col·lisió amb
// cppcheck-suppress knownConditionTrueFalse // un caminant, ni baix ni alt.
if (BODY_H > 0.0F && b->isAirborne() && b->getJumpZ() > BODY_H) { return; } if (A_AIR != B_AIR) { return; }
// Calculem l'overlap previst en l'espai de la hit-box petita: // Calculem l'overlap previst en l'espai de la hit-box petita:
// els jugadors han de poder acostar-se molt (caixes de moviment // els jugadors han de poder acostar-se molt (caixes de moviment
// 16×16 quasi tocant) sense disparar el resolver; només quan el // 16×16 quasi tocant) sense disparar el resolver; només quan el
// contacte real (hit-box 8×8) s'activaria, neutralitzem o apliquem // contacte real (hit-box 8×8) s'activaria, neutralitzem o apliquem
// push. // push (a terra) o invertim velocitats (en l'aire).
const SDL_FRect BOX_A = a->getHitBox(); const SDL_FRect BOX_A = a->getHitBox();
const SDL_FRect BOX_B = b->getHitBox(); const SDL_FRect BOX_B = b->getHitBox();
const SDL_FRect NEXT_A{ const SDL_FRect NEXT_A{
@@ -135,6 +135,32 @@ namespace {
}; };
if (!rectsOverlap(NEXT_A, NEXT_B)) { return; } if (!rectsOverlap(NEXT_A, NEXT_B)) { return; }
// Tots dos en l'aire (A_AIR == B_AIR == true aquí, ja descartat
// l'altre cas a dalt): rebot mid-aire si els rangs verticals
// [z, z+body_height] solapen. Si la diferència d'alçades és
// massa gran, ja no hi ha contacte real (un està molt més amunt
// que l'altre) i no fem res. Si solapen, invertim ambdues
// velocitats horitzontals i els deixem rebotar fins a aterrar
// — els dos cauen en stun, com a càstig per abusar del salt.
if (A_AIR) {
// cppcheck-suppress knownConditionTrueFalse
const auto BODY_H = static_cast<float>(Player::body_height);
// cppcheck-suppress knownConditionTrueFalse
if (BODY_H <= 0.0F) { return; }
if (std::fabs(a->getJumpZ() - b->getJumpZ()) >= BODY_H) { return; }
// Saltadors estàtics horitzontalment (cas degenerat: tots dos
// jumpejant verticals al mateix x): no hi ha velocitat a
// invertir, deixem-los — el predictiu no tornarà a disparar
// perquè en el següent frame ja no hi haurà velocitat
// d'acostament.
const float REL_VX = a->getIntendedVX() - b->getIntendedVX();
const float REL_VY = a->getIntendedVY() - b->getIntendedVY();
if (REL_VX == 0.0F && REL_VY == 0.0F) { return; }
a->setIntendedVelocity(-a->getIntendedVX(), -a->getIntendedVY());
b->setIntendedVelocity(-b->getIntendedVX(), -b->getIntendedVY());
return;
}
const bool MOVING_A = (a->getInputVX() != 0.0F) || (a->getInputVY() != 0.0F); const bool MOVING_A = (a->getInputVX() != 0.0F) || (a->getInputVY() != 0.0F);
const bool MOVING_B = (b->getInputVX() != 0.0F) || (b->getInputVY() != 0.0F); const bool MOVING_B = (b->getInputVX() != 0.0F) || (b->getInputVY() != 0.0F);