#pragma once #include #include #include #include #include #include "core/rendering/sprite/animated_sprite.hpp" #include "game/map/map.hpp" class EventBus; class Player : public AnimatedSprite { public: // Mapeig de tecles per al moviment del jugador. Cada jugador local en pot // tenir unes diferents; així podem afegir-ne més sense tocar el codi del // Player, només instanciant amb Controls distints. struct Controls { SDL_Scancode up; SDL_Scancode down; SDL_Scancode left; SDL_Scancode right; }; // Geometria carregada des del YAML del player (p.ex. `data/player/sam.yaml`). // `sprite_*` són // la mida del frame (sprite 12×24 amb perspectiva falsa: visualment, els // 12 píxels superiors són el cap i els 12 inferiors la base). // La caixa de col·lisió és un quadrat `collision_size` concèntric amb el // quadrat inferior del sprite (`base_size`). Els offsets els calculem // al load a partir d'aquests tres valors; el mapa de col·lisions i el // de dibuix són independents (el sprite pot sobresortir sense afectar). inline static int sprite_w = 0; inline static int sprite_h = 0; inline static int base_size = 0; inline static int collision_size = 0; inline static int collision_padding = 0; inline static int collision_x_offset = 0; inline static int collision_y_offset = 0; // Caixa de "contacte" més petita que la caixa de col·lisió: s'usa per // a les interaccions actor-actor (player↔player, player↔momia/trampa), // on volem que el contacte sigui precís i no dispari a "distància" // de la vora del sprite. La caixa de col·lisió gran continua usant-se // per xocar amb murs i sarcòfags (per mantenir el carril del // passadís). Sense `hit_size` al YAML, s'iguala a `collision_size` i // el comportament és idèntic a l'anterior. inline static int hit_size = 0; inline static int hit_x_offset = 0; inline static int hit_y_offset = 0; // Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor. static void loadFrom(const std::string& yaml_path); 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. // Amb inèrcia activada, `intended_v*` pot ser no-zero encara que // l'usuari no estiga prement res (coast residual). [[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; } // Velocitat crua d'input d'aquest frame (-SPEED, 0 o +SPEED per eix). // A diferència de `intended_v*`, no té en compte la inèrcia: un jugador // que coasta sense prémer res té `input_v* == 0`. El resolver PvP del // GameScene l'usa per decidir "parat vs en moviment" (criteri: un // coast sense input compta com parat — l'empenta i el head-on només // es disparen quan hi ha intenció explícita del jugador). [[nodiscard]] auto getInputVX() const -> float { return input_vx_; } [[nodiscard]] auto getInputVY() const -> float { return input_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; } // Marcat pel resolver jugador↔jugador quan aquest frame el jugador // és arrossegat per una empenta (no té input propi; es desplaça // perquè l'altre l'empeny). Determina que l'animació del frame siga // `stand_*` encara que la posició canvie. El flag es reseteja a // l'inici de cada `tick`, així que el resolver l'ha de tornar a // 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. [[nodiscard]] auto getFeetY() const -> float { return getPosY() + static_cast(collision_y_offset + collision_size); } // Injecta el bus del nivell. El Player publica `on_tile_entered` cada // vegada que el centre de la seua caixa de col·lisió entra en una // cel·la nova del tilemap, amb payload `PaintedTile{col,row,paint_tile_index_}`. // El PaintLayer filtra pintabilitat i re-publica `tile_painted` a qui // correspongui (sarcòfags). Si el bus no s'injecta, no emet res. void setEventBus(EventBus* bus) { bus_ = bus; } // Tile actual del jugador (centre de `baseBox()` convertit a // coordenades de tile del tilemap, usant Map::tile_size i // Map::tilemap_origin_*). Retorna (col, row). [[nodiscard]] auto getCurrentTile() const -> std::pair; // Rectangle de col·lisió del jugador, en coordenades del canvas. Usat // pel mapa de col·lisions (tryMove contra murs/sarcòfags). [[nodiscard]] auto getBaseBox() const -> SDL_FRect; // Rectangle de contacte actor-actor (hit-box), més petit que la caixa // base i concèntric amb ella. L'usen `checkPlayerCollisions` // (momies, trampes, porta) i el resolver jugador↔jugador per // decidir quan es toquen. [[nodiscard]] auto getHitBox() const -> SDL_FRect; // Versió estàtica: deriva la hit-box a partir d'una caixa base // arbitrària (p. ex. la posició proposada per `tryMove` abans // d'aplicar-la). Evita recalcular `hit_x_offset`/`hit_y_offset` fora // de Player. [[nodiscard]] static auto hitBoxFromBase(const SDL_FRect& base) -> SDL_FRect; // Override del hook de Sprite: `MovingSprite::tryMove` envia aquesta // caixa al callback de col·lisió. La base del Player és més petita // que el sprite (el cap pot sobresortir per murs/sarcòfags sense // bloquejar el moviment). [[nodiscard]] auto collisionBox() const -> SDL_FRect override { return getBaseBox(); } private: // Alias privat històric. Mantingut perquè viu dins de l'implementació // (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(); // Snap cap al passadís perpendicular a la direcció premuda. Si la caixa // de col·lisió està a ≤ collision_size/2 d'una lane, s'hi aproxima // gradualment (a velocitat de moviment). Permet canviar de direcció // sense haver d'alinear-se píxel-perfect en l'encreuament. Els dos // wrappers deleguen a `snapToLane` només canviant l'eix actiu. void snapToHorizontalLane(float delta_time); void snapToVerticalLane(float delta_time); // Nom d'animació per al `facing_` actual. Si `moving` és cert s'usa // la variant `walk*`, altrament la variant `stand*`. [[nodiscard]] auto animationName(bool moving) const -> const char*; void snapToLane( float delta_time, float base, float pitch, int max_idx, 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_; // Velocitat intencionada calculada a `tick` i aplicada a // `applyMovement`. El GameScene pot sobreescriure-la entremig per // resoldre interaccions jugador↔jugador (empentes, head-on). // També actua com a estat físic persistent entre frames: amb inèrcia // activada, si l'usuari solta l'input, aquí queda la velocitat // residual que `tick` anirà decaient linealment cap a 0. float intended_vx_{0.0F}; float intended_vy_{0.0F}; // Velocitat desitjada per input aquest frame (teclat + gamepad). Cada // eix és -SPEED, 0 o +SPEED. És la "intenció del jugador"; la física // resultant viu a `intended_v*` i pot diferir (coast sense input). float input_vx_{0.0F}; float input_vy_{0.0F}; // Acumulador de temps en moviment continu. Creix cada frame amb // `|input_v*| > 0` o `|intended_v*| > 0`, i es reseteja a 0 només // quan NO hi ha ni input ni velocitat residual. Activa la inèrcia // quan passa el llindar configurat al YAML (`threshold_ms`); no es // buida mai enmig d'un coast, ni tan sols en canvis de direcció, // així un toc curt en sentit oposat no "frena en sec" un coast llarg. float motion_time_ms_{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}; // Frame actual el jugador està sent empentat (no té input propi). // 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 // a un mapa concret — si el mapa canvia, la Scene ha de // reinstanciar-lo. Map::Info map_; // Direcció "cap on mira" el jugador. Governa quin conjunt d'animacions // s'aplica (lateral, amunt, avall) i si el sprite es flipeja o no. Els // moviments purament horitzontals o diagonals prenen `SIDE` (prevalença // lateral), els purament verticals fan `UP`/`DOWN`. En aturar-se es // preserva l'última orientació perquè la versió stand coincideixi. enum class Facing : std::uint8_t { SIDE, UP, DOWN }; Facing facing_{Facing::SIDE}; // Cardinal complet (N/S/E/W) usat per a la detecció de girs de 180°. // El parell (Facing, flip) només té 3+1 estats; aquesta enum n'afegeix // la 4a sense reorganitzar el sistema d'animacions. enum class Cardinal : std::uint8_t { NONE, NORTH, SOUTH, EAST, WEST }; // Configuració d'inèrcia llegida del bloc `inertia:` del YAML. Amb // `brake_time_ms == 0` (o bloc absent) la inèrcia està desactivada i // el comportament és instantani: soltar input para el jugador en sec. struct InertiaConfig { int brake_time_ms{0}; // Temps en ms perquè la velocitat caiga de ±SPEED a 0. int threshold_ms{0}; // Temps mínim en moviment continu per activar la inèrcia. }; static InertiaConfig inertia_config; // Integra un eix. Si hi ha input en aquest eix, la velocitat física // salta a aquest valor (acceleració instantània). Si no, i hi ha // input en l'altre eix (`any_input == true`), la velocitat d'aquest // eix es posa a 0 en sec: el jugador està fent un gir de 90°, no // soltant l'input — no volem que arrossegue en diagonal. Només quan // l'usuari deixa anar tot l'input (`any_input == false`) s'activa la // inèrcia de frenada (si `motion_time_ms` ha passat el llindar). static void integrateAxis(float& phys, float input, float delta_time, float motion_time_ms, bool any_input); // Configuració del gir de 180°, llegida del bloc `turn:` del YAML del // player. Si `duration_ms` és 0 (o el bloc no existeix) la mecànica // queda desactivada i els canvis de direcció són instantanis. struct TurnConfig { int duration_ms{0}; Cardinal w_to_e{Cardinal::SOUTH}; Cardinal e_to_w{Cardinal::NORTH}; Cardinal n_to_s{Cardinal::EAST}; Cardinal s_to_n{Cardinal::WEST}; }; // Definida out-of-class al .cpp: dins de la classe, els initializers // per defecte de TurnConfig encara no són visibles per al compilador. static TurnConfig turn_config; // Estat del gir en curs. Mentre `turning_remaining_ms_ > 0`, el facing/ // flip es manté forçat a `intermediate_cardinal_` i l'animació es força // a `stand` encara que el moviment continuï. En esgotar-se el timer, // s'aplica `pending_cardinal_`. `last_input_cardinal_` recorda l'últim // cardinal d'input no nul: serveix per disparar el gir només quan // l'usuari canvia d'opinió, no la primera vegada que prem una tecla // des de l'estat idle. float turning_remaining_ms_{0.0F}; Cardinal pending_cardinal_{Cardinal::NONE}; Cardinal intermediate_cardinal_{Cardinal::NONE}; Cardinal last_input_cardinal_{Cardinal::NONE}; // Aplica un cardinal a (`facing_`, flip). NONE no fa res — preserva el // facing actual (cas idle: l'última orientació es manté). void applyCardinal(Cardinal c); // Actualitza facing_/flip i l'estat del gir a partir de l'input del frame. // Extret de `update` per acotar la complexitat cognitiva (clang-tidy). void updateFacingAndTurn(float delta_time, float vx, float vy); static auto cardinalFromInput(float vx, float vy) -> Cardinal; static auto cardinalFromString(const std::string& s) -> Cardinal; static auto opposite(Cardinal c) -> Cardinal; static auto intermediateFor(Cardinal target) -> Cardinal; // Bus del nivell (opcional). Propietat externa (EntityManager); el // Player només en guarda un punter per publicar events. EventBus* bus_{nullptr}; // Últim tile publicat. Inicialitzats a un sentinel fora de rang perquè // el primer frame en què el bus estigui connectat ja emeti un event. int last_tile_col_{-9999}; int last_tile_row_{-9999}; // Índex de tile del tileset amb què aquest jugador pinta. Llegit del // YAML del mapa (`player1_paint_tile` / `player2_paint_tile`) i passat // al ctor per GameScene. -1 desactiva el pintat (p.ex. al test // d'arrencada o en un escenari que no vulgui feedback visual). int paint_tile_index_{-1}; };