#include "game/entities/player.hpp" #include #include #include #include "core/resources/resource_cache.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 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); constexpr 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); constexpr 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); } } }