diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 1ef5319..170311b 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -531,12 +531,87 @@ void Player::snapToLane( const float LANE = base + (static_cast(IDX) * pitch); const float DELTA = LANE - (this->*get_pos)(); - if (std::abs(DELTA) > 0.0F && std::abs(DELTA) <= THRESHOLD) { - const float STEP = std::min(std::abs(DELTA), SPEED_PX_PER_S * delta_time); - const float SAVED = (this->*get_pos)(); - (this->*set_pos)(SAVED + std::copysign(STEP, DELTA)); - if (map_.collides(baseBox())) { (this->*set_pos)(SAVED); } + if (std::abs(DELTA) <= 0.0F || std::abs(DELTA) > THRESHOLD) { return; } + + const float FULL_STEP = std::min(std::abs(DELTA), SPEED_PX_PER_S * delta_time); + const float SAVED = (this->*get_pos)(); + + // Fase 1: snap a velocitat plena. Si no xoca amb res, commit — és el + // cas habitual (carril lliure) i no ha de patir la friccó de l'empenta. + (this->*set_pos)(SAVED + std::copysign(FULL_STEP, DELTA)); + if (!collidesWithWorld(baseBox())) { return; } + + // Mur al carril objectiu: no hi ha ningú a qui empentar, revert directe. + if (map_.collides(baseBox())) { + (this->*set_pos)(SAVED); + return; } + + // Fase 2: sols un altre jugador al carril. Reintentem amb step escalat + // per `push_factor_` (mateixa friccó que el push horitzontal del + // resolver) i mirem d'arrossegar els blockers. Si la pushada no cap + // (contra mur o tercer jugador), el snap es revertix sencer. + const float PUSH_STEP = std::min(std::abs(DELTA), SPEED_PX_PER_S * push_factor_ * delta_time); + const float SIGNED_PUSH = std::copysign(PUSH_STEP, DELTA); + (this->*set_pos)(SAVED + SIGNED_PUSH); + if (collidesWithWorld(baseBox())) { + if (map_.collides(baseBox()) || !tryPushSnapBlockers(SIGNED_PUSH, get_pos, set_pos)) { + (this->*set_pos)(SAVED); + } + } +} + +auto Player::tryPushSnapBlockers( + float signed_step, + float (Player::*get_pos)() const, + void (Player::*set_pos)(float)) -> bool { + if (all_players_ == nullptr) { return false; } + + // Blockers = jugadors que ara mateix solapen la nostra hit-box (ja + // amb el snap aplicat). Mateix criteri actor-actor que `collidesWithWorld`. + std::vector blockers; + const SDL_FRect MY_HIT = getHitBox(); + for (Player* p : *all_players_) { + if (p == nullptr || p == this) { continue; } + const SDL_FRect OTHER_HIT = p->getHitBox(); + if (SDL_HasRectIntersectionFloat(&MY_HIT, &OTHER_HIT)) { + blockers.push_back(p); + } + } + if (blockers.empty()) { return false; } + + // Desem la posició original de cada blocker i el desplacem el mateix + // step que s'ha aplicat a `this`: com que els dos es mouen igual en el + // mateix eix, la distància relativa es conserva i, per tant, si abans + // del snap no es solapaven, després de l'empenta tampoc. + std::vector saved; + saved.reserve(blockers.size()); + for (Player* p : blockers) { + const float S = (p->*get_pos)(); + saved.push_back(S); + (p->*set_pos)(S + signed_step); + } + + // Validació: `this` ha de quedar net (ni mur ni cap jugador), i cada + // blocker també (cobreix el cas de 3+ jugadors on un blocker podria + // acabar dins d'un tercer o contra un mur). + const bool OK = !collidesWithWorld(baseBox()) && std::ranges::none_of(blockers, [](const Player* p) { + return p->collidesWithWorld(p->getBaseBox()); + }); + + if (!OK) { + for (size_t i = 0; i < blockers.size(); ++i) { + (blockers[i]->*set_pos)(saved[i]); + } + return false; + } + + // Els blockers s'han desplaçat sense input propi: mateixa convenció + // d'animació que el push horitzontal del resolver (stand_*, no walk). + for (Player* p : blockers) { + p->setBeingPushed(true); + } + return true; } void Player::snapToHorizontalLane(float delta_time) { diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index 3ed3c6f..e4b95c9 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -117,6 +117,14 @@ class Player : public AnimatedSprite { // posar cada frame que el push s'haja de mantenir. void setBeingPushed(bool pushed) { being_pushed_ = pushed; } + // Factor d'escala d'empenta jugador↔jugador (viu al YAML del mapa). + // L'usa el resolver de GameScene per al push horitzontal, i també + // `snapToLane` quan descobreix que un altre jugador li bloqueja el + // carril objectiu: per coherència, la pushada del snap va al mateix + // ritme que l'horitzontal. Si no es crida, el valor per defecte 1.0 + // manté el snap a velocitat plena (comportament pre-multiplayer). + void setPushFactor(float factor) { push_factor_ = factor; } + // Y de la base del jugador (cara inferior de la caixa de colisió). Usada // per al Z-order contra els sarcs: peus per sobre d'un sarc → darrere; // per sota → davant. @@ -191,6 +199,18 @@ class Player : public AnimatedSprite { float (Player::*get_pos)() const, void (Player::*set_pos)(float)); + // Quan el snap lateral trobaria un altre jugador al carril objectiu, + // l'empentem el mateix step en el mateix eix. Així `this` pot entrar + // al passadís encara que l'altre estiga estacionat al mitj — mateix + // contracte que el push horitzontal però disparat pel snap. Retorna + // true si la push ha quedat neta (ningú xoca amb el mapa ni amb un + // tercer jugador); false fa que el caller revertisca el propi snap. + // Marca els blockers amb `being_pushed_` per a la seua animació. + auto tryPushSnapBlockers( + float signed_step, + float (Player::*get_pos)() const, + void (Player::*set_pos)(float)) -> bool; + int id_{-1}; Controls controls_; @@ -225,6 +245,11 @@ class Player : public AnimatedSprite { // Serveix només per a l'elecció d'animació — la posició ja la // gestiona `intended_vx_/vy_` escalats pel resolver. bool being_pushed_{false}; + + // Escala d'empenta usada per `snapToLane` quan descobreix un blocker + // (mateixa del resolver). 1.0 = sense escalar (comportament si + // GameScene no ha cridat setPushFactor). + float push_factor_{1.0F}; // Còpia plana de la geometria del mapa + punters a `collides`/ // `inside_playfield`. POD trivial; copiada al ctor i no muta durant // la vida del Player. Vol dir que aquest Player només és vàlid per diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index d8acb12..9f8362e 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -303,6 +303,9 @@ GameScene::GameScene(Config config) players_view_.reserve(players_.size()); std::ranges::transform(players_, std::back_inserter(players_view_), [](const auto& p) { return p.get(); }); for (auto& p : players_) { p->setOtherPlayers(&players_view_); } + // 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_); } // Marcador (HUD) de la franja superior. Reutilitza la font 8bithud i // la paleta "ui" ja precarregades pel Director — no toca disc des