#include "game/entities/player.hpp" #include #include #include #include #include #include "core/resources/resource_cache.hpp" #include "external/fkyaml_node.hpp" #include "game/map/map.hpp" #include "utils/defaults.hpp" namespace { // Alias local per reduir soroll; la constant viu a Defaults::Player. constexpr float SPEED_PX_PER_S = Defaults::Player::SPEED_PX_PER_S; } // namespace void Player::loadFrom(const std::string& yaml_path) { std::ifstream file(yaml_path); if (!file.is_open()) { throw std::runtime_error("Player: no s'ha pogut obrir " + yaml_path); } auto yaml = fkyaml::node::deserialize(file); sprite_w = yaml["frameWidth"].get_value(); sprite_h = yaml["frameHeight"].get_value(); if (!yaml.contains("collision")) { throw std::runtime_error("Player: manca bloc `collision:` a " + yaml_path); } const auto& c = yaml["collision"]; collision_size = c["size"].get_value(); base_size = c["base_size"].get_value(); // Col·lisió centrada en el quadrat inferior del sprite. Si la caixa és // més gran que la base (cas habitual, 16 vs 12) el padding és simètric // a cada costat; si són iguals, padding=0 i la caixa coincideix amb la // base sense offsets. collision_padding = (collision_size - base_size) / 2; collision_x_offset = -collision_padding; collision_y_offset = (sprite_h - base_size) - collision_padding; } Player::Player(const std::string& anim_key, float x, float y, const Controls& controls) : AnimatedSprite(Resource::Cache::get().getAnimationData(anim_key)), controls_(controls) { setPos(x, y); setCurrentAnimation("stand"); } void Player::update(float delta_time) { const bool* keys = SDL_GetKeyboardState(nullptr); float vx = 0.0F; float vy = 0.0F; if (keys[controls_.left]) { vx -= SPEED_PX_PER_S; } if (keys[controls_.right]) { vx += SPEED_PX_PER_S; } if (keys[controls_.up]) { vy -= SPEED_PX_PER_S; } if (keys[controls_.down]) { vy += SPEED_PX_PER_S; } setVelX(vx); setVelY(vy); // El sprite original mira a la dreta; flipem només si es mou a l'esquerra // i restaurem si torna a la dreta. Moviments purament verticals conserven // l'últim flip perquè el jugador no "gire" sense motiu. if (vx < 0.0F) { setFlip(SDL_FLIP_HORIZONTAL); } else if (vx > 0.0F) { setFlip(SDL_FLIP_NONE); } const bool MOVING = vx != 0.0F || vy != 0.0F; setCurrentAnimation(MOVING ? "walk" : "stand"); // 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); if (ONLY_H) { snapToHorizontalLane(delta_time); } else if (ONLY_V) { snapToVerticalLane(delta_time); } // Guardem la posició prèvia i deixem que AnimatedSprite::update aplique // la velocitat com sempre; així no dupliquem la integració temporal ni // l'animació. Després, axis-separated collision: revertim i re-apliquem // cada eix comprovant el mapa de colisions, de manera que el jugador // llisca contra els sarcòfags en compte de quedar-se bloquejat en sec. const float OLD_X = getPosX(); const float OLD_Y = getPosY(); AnimatedSprite::update(delta_time); const float NEW_X = getPosX(); const float NEW_Y = getPosY(); setPos(OLD_X, OLD_Y); setPosX(NEW_X); if (Map::collides(baseBox())) { setPosX(OLD_X); } setPosY(NEW_Y); if (Map::collides(baseBox())) { setPosY(OLD_Y); } clampToPlayfield(); } void Player::clampToPlayfield() { // Límits de la zona jugable: la caixa de col·lisió no pot eixir del // playfield (muros). El cap del sprite sí pot sobresortir per damunt. const auto MIN_X = static_cast(Map::playfield_x - collision_x_offset); const auto MAX_X = static_cast( Map::playfield_x + Map::playfield_w - collision_x_offset - collision_size); const auto MIN_Y = static_cast(Map::playfield_y - collision_y_offset); const auto MAX_Y = static_cast( Map::playfield_y + Map::playfield_h - collision_y_offset - collision_size); setPosX(std::clamp(getPosX(), MIN_X, MAX_X)); setPosY(std::clamp(getPosY(), MIN_Y, MAX_Y)); } auto Player::baseBox() const -> SDL_FRect { return { .x = getPosX() + static_cast(collision_x_offset), .y = getPosY() + static_cast(collision_y_offset), .w = static_cast(collision_size), .h = static_cast(collision_size), }; } void Player::snapToLane( float delta_time, float base, float pitch, int max_idx, float (Player::*get_pos)() const, void (Player::*set_pos)(float)) { // Lanes en pos_axis: base + i·pitch per a i ∈ [0, max_idx]. Correspon a // la caixa de col·lisió ocupant exactament un passadís perpendicular. const auto THRESHOLD = static_cast(collision_size) / 2.0F; const auto IDX = std::clamp( static_cast(std::round(((this->*get_pos)() - base) / pitch)), 0, max_idx); 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); } } } void Player::snapToHorizontalLane(float delta_time) { snapToLane(delta_time, static_cast(Map::playfield_y - collision_y_offset), static_cast(Map::pitch_y), Map::grid_rows, &Player::getPosY, &Player::setPosY); } void Player::snapToVerticalLane(float delta_time) { snapToLane(delta_time, static_cast(Map::playfield_x - collision_x_offset), static_cast(Map::pitch_x), Map::grid_cols, &Player::getPosX, &Player::setPosX); }