#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(float x, float y, const Controls& controls) : AnimatedSprite(Resource::Cache::get().getAnimationData("player.yaml")), 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); } // 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::snapToHorizontalLane(float delta_time) { // Lanes en posY: base + i·pitch_y per a i ∈ [0, grid_rows]. Correspon a // la caixa de col·lisió ocupant exactament un passadís horitzontal. const auto BASE = static_cast(Map::playfield_y - collision_y_offset); const auto PITCH = static_cast(Map::pitch_y); const auto THRESHOLD = static_cast(collision_size) / 2.0F; const auto IDX = std::clamp( static_cast(std::round((getPosY() - BASE) / PITCH)), 0, Map::grid_rows); const float LANE = BASE + (static_cast(IDX) * PITCH); const float DY = LANE - getPosY(); if (std::abs(DY) > 0.0F && std::abs(DY) <= THRESHOLD) { const float STEP = std::min(std::abs(DY), SPEED_PX_PER_S * delta_time); const float SAVED = getPosY(); setPosY(getPosY() + std::copysign(STEP, DY)); if (Map::collides(baseBox())) { setPosY(SAVED); } } } void Player::snapToVerticalLane(float delta_time) { const auto BASE = static_cast(Map::playfield_x - collision_x_offset); const auto PITCH = static_cast(Map::pitch_x); const auto THRESHOLD = static_cast(collision_size) / 2.0F; const auto IDX = std::clamp( static_cast(std::round((getPosX() - BASE) / PITCH)), 0, Map::grid_cols); const float LANE = BASE + (static_cast(IDX) * PITCH); const float DX = LANE - getPosX(); if (std::abs(DX) > 0.0F && std::abs(DX) <= THRESHOLD) { const float STEP = std::min(std::abs(DX), SPEED_PX_PER_S * delta_time); const float SAVED = getPosX(); setPosX(getPosX() + std::copysign(STEP, DX)); if (Map::collides(baseBox())) { setPosX(SAVED); } } }