diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 6faa501..a1ba91a 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -241,6 +241,21 @@ Player::Player(const std::string& anim_key, int id, float x, float y, const Cont } void Player::update(float delta_time) { + tick(delta_time); + applyMovement(delta_time); +} + +auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool { + if (map_.collides(box)) { return true; } + if (all_players_ == nullptr) { return false; } + return std::ranges::any_of(*all_players_, [this, &box](const Player* p) { + if (p == nullptr || p == this) { return false; } + const auto OTHER = p->getBaseBox(); + return SDL_HasRectIntersectionFloat(&box, &OTHER); + }); +} + +void Player::tick(float delta_time) { // Via `InputState` en lloc de `SDL_GetKeyboardState` directe: quan el // service menu (o qualsevol UI modal) està obert, el wrapper retorna // un buffer tot-a-zero perquè el jugador no es mogui mentre l'usuari @@ -253,31 +268,32 @@ void Player::update(float delta_time) { if (keys[controls_.up]) { vy -= SPEED_PX_PER_S; } if (keys[controls_.down]) { vy += SPEED_PX_PER_S; } - // Cruceta i stick esquerre del primer gamepad (si n'hi ha). També + // Cruceta i stick esquerre del mando assignat (si n'hi ha). També // respecten el bloqueig d'`InputState`, així que amb el menú obert // els botons tornen false/0 i no contribueixen a la velocitat. - // Direccions oposades (teclat left + d-pad right) es cancel·len de - // forma natural; premuda simultània en la mateixa direcció no duplica - // la velocitat perquè cada branca només resta/suma una vegada. accumulateGamepadMovement(id_, vx, vy); + intended_vx_ = vx; + intended_vy_ = vy; setVelX(vx); setVelY(vy); updateFacingAndTurn(delta_time, vx, vy); +} + +void Player::applyMovement(float delta_time) { + // El resolver del GameScene pot haver escalat `intended_vx_/vy_` + // (cas push: velocitat * push_factor) o haver-les anul·lat (cas + // head-on). Consumim-les aquí com a velocitat definitiva del frame. + const float VX = intended_vx_; + const float VY = intended_vy_; // Snap al passadís: si només hi ha moviment horitzontal (o vertical) i // la caixa està prou prop d'una lane perpendicular (≤ mitja caixa), la - // Y (o X) es corregeix gradualment per enfilar el passadís. Amb - // moviment diagonal no es fa snap — l'usuari pilota manualment. - const bool ONLY_H = (vx != 0.0F) && (vy == 0.0F); - const bool ONLY_V = (vy != 0.0F) && (vx == 0.0F); + // Y (o X) es corregeix gradualment per enfilar el passadís. + const bool ONLY_H = (VX != 0.0F) && (VY == 0.0F); + const bool ONLY_V = (VY != 0.0F) && (VX == 0.0F); - // Pos d'inici de frame (abans de snap i integració): si en acabar el - // frame coincideix amb la posició final, el jugador no s'ha mogut - // realment (ni per input, ni per snap, ni ha superat la col·lisió) i cal - // forçar l'animació `stand` corresponent al facing — així s'atura el - // caminat quan xoca contra un mur o un sarcòfag. const float FRAME_START_X = getPosX(); const float FRAME_START_Y = getPosY(); @@ -288,20 +304,19 @@ void Player::update(float delta_time) { } // Animació del frame + moviment col·lisió-aware. `tryMove` aplica els - // deltes per eix i reverteix l'eix que col·lisiona, així el jugador - // llisca contra sarcòfags i murs en compte de quedar-se bloquejat en - // sec. La caixa que s'envia al callback és `collisionBox()` (override - // a Player retornant `getBaseBox()`), no el sprite sencer. + // deltes per eix i reverteix l'eix que col·lisiona; així el jugador + // llisca contra sarcòfags, murs i altres jugadors en compte de quedar + // bloquejat en sec. La callback tracta els altres jugadors com a + // sòlids per evitar overlaps (el resolver del GameScene deixa les + // velocitats "segures", i aquesta comprovació és la xarxa de + // seguretat davant de casos d'ordre d'aplicació). animate(delta_time); - tryMove(vx * delta_time, vy * delta_time, [this](const SDL_FRect& box) { return map_.collides(box); }); + tryMove(VX * delta_time, VY * delta_time, [this](const SDL_FRect& box) { + return collidesWithWorld(box); + }); clampToPlayfield(); - // Animació basada en moviment real: si la col·lisió (o l'absència - // d'input) ha deixat el jugador al mateix lloc que al principi del - // frame, queda en `stand` del facing actual. Durant un gir de 180° - // forcem també `stand` encara que el jugador s'estiga movent — la - // pose intermèdia és sempre estàtica per disseny. const bool MOVED = (getPosX() != FRAME_START_X) || (getPosY() != FRAME_START_Y); const bool ANIM_AS_MOVING = MOVED && (turning_remaining_ms_ <= 0.0F); setCurrentAnimation(animationName(ANIM_AS_MOVING)); diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index 7109eb7..00de35b 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "core/rendering/sprite/animated_sprite.hpp" #include "game/map/map.hpp" @@ -44,14 +45,48 @@ class Player : public AnimatedSprite { Player(const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1); + // Update en una sola crida: manté l'API anterior per als tests i + // escenaris amb un sol jugador. Internament és equivalent a cridar + // `tick(dt)` seguit d'`applyMovement(dt)`. void update(float delta_time) override; + // Fase 1: llig l'input d'aquest frame i emmagatzema la velocitat + // intencionada a `intended_vx_` / `intended_vy_`. No mou el jugador. + // Separada d'`applyMovement` perquè el GameScene puga resoldre + // interaccions entre jugadors (empentes, neutralitzacions head-on) + // abans que ningú consume la posició. + void tick(float delta_time); + + // Fase 2: consumeix `intended_vx_`/`intended_vy_` (possiblement + // modificats pel resolver), crida `tryMove` contra el mapa i els + // altres jugadors, fa snap al passadís, clamp al playfield, avança + // l'animació i publica `on_tile_entered` si ha canviat de tile. + void applyMovement(float delta_time); + // Identificador local (0 = P1, 1 = P2…). S'usa als events del bus // perquè els consumidors (HUD, futures regles de puntuació) sàpiguen // quin jugador ha originat l'event. No té cap vinculació amb la // xarxa — és únic per dins del procés. [[nodiscard]] auto getId() const -> int { return id_; } + // Accés i mutació de la velocitat intencionada d'aquest frame. El + // GameScene els toca entre `tick` i `applyMovement` per implementar + // col·lisions jugador↔jugador (escalar a push_factor, anul·lar en + // head-on). Expressades en píxels/segon, no en píxels/frame. + [[nodiscard]] auto getIntendedVX() const -> float { return intended_vx_; } + [[nodiscard]] auto getIntendedVY() const -> float { return intended_vy_; } + void setIntendedVelocity(float vx, float vy) { + intended_vx_ = vx; + intended_vy_ = vy; + } + + // Llista de tots els jugadors del nivell (incloent-se ell mateix — + // la callback ignora `this`). La caixa del jugador es considera un + // sòlid a la callback de col·lisió: així `tryMove` no permet que un + // jugador travesse l'altre, i els desplaçaments parcials (push + // contra mur) s'emparen automàticament. + void setOtherPlayers(const std::vector* all) { all_players_ = all; } + // 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. @@ -87,6 +122,11 @@ class Player : public AnimatedSprite { // (snapToLane, clamp). Els consumidors externs usen `getBaseBox()`. [[nodiscard]] auto baseBox() const -> SDL_FRect { return getBaseBox(); } + // Callback de col·lisió per a `tryMove`: true si la caixa proposada + // xoca amb el mapa o amb un altre jugador. Membre per mantenir la + // complexitat cognitiva d'`applyMovement` sota el llindar. + [[nodiscard]] auto collidesWithWorld(const SDL_FRect& box) const -> bool; + // Clamp de la caixa de col·lisió al playfield (no al sprite sencer: // el cap pot sobresortir pel mur superior sense afectar el joc). void clampToPlayfield(); @@ -112,6 +152,16 @@ class Player : public AnimatedSprite { int id_{-1}; Controls controls_; + + // Velocitat intencionada calculada a `tick` i aplicada a + // `applyMovement`. El GameScene pot sobreescriure-la entremig per + // resoldre interaccions jugador↔jugador (empentes, head-on). + float intended_vx_{0.0F}; + float intended_vy_{0.0F}; + + // Punter a la llista de tots els jugadors del nivell (propietat del + // GameScene). `nullptr` = jugador aïllat, sense col·lisió inter-P. + const std::vector* all_players_{nullptr}; // 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