#include "game/entities/player.hpp" #include #include #include #include #include #include #include "core/resources/resource_cache.hpp" #include "external/fkyaml_node.hpp" #include "game/entities/event_bus.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(); // Detecció de canvi de tile. Es calcula *després* del clamp i de les // col·lisions perquè la posició aquí ja és la definitiva del frame; // així els sarcòfags no reben falsos positius mentre el Player rellisca // contra una paret. Si el tile ha canviat, publiquem `on_tile_entered` // al bus; l'EntityManager farà fan-out a notifyTileEntered. if (bus_ != nullptr) { const auto [COL, ROW] = getCurrentTile(); if (COL != last_tile_col_ || ROW != last_tile_row_) { last_tile_col_ = COL; last_tile_row_ = ROW; bus_->publish("on_tile_entered", TilePos{.col = COL, .row = ROW}); } } } auto Player::getCurrentTile() const -> std::pair { // El grid del tilemap és de `Map::tile_size` píxels i està 16-alineat // respecte a l'origen del tilemap (pot ser negatiu per overscan). El // centre de la caixa de col·lisió ens dóna el tile on "està" el Player. const auto BOX = baseBox(); const float CX = BOX.x + (BOX.w * 0.5F); const float CY = BOX.y + (BOX.h * 0.5F); const int COL = static_cast(std::floor( (CX - static_cast(Map::tilemap_origin_x)) / static_cast(Map::tile_size))); const int ROW = static_cast(std::floor( (CY - static_cast(Map::tilemap_origin_y)) / static_cast(Map::tile_size))); return {COL, ROW}; } 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); }