// IWYU pragma: no_include #include "game/entities/player.hpp" #include // Para max, min #include // Para ceil, abs #include #include // Para std::ranges::any_of #include "core/audio/audio.hpp" // Para Audio #include "core/input/input.hpp" // Para Input, InputAction #include "core/rendering/sprite/animated_sprite.hpp" // Para SAnimatedSprite #include "core/resources/resource_cache.hpp" // Para Resource #include "game/gameplay/room.hpp" // Para Room, TileType #include "game/options.hpp" // Para Cheat, Options, options #include "utils/defines.hpp" // Para RoomBorder::BOTTOM, RoomBorder::LEFT, RoomBorder::RIGHT #ifdef _DEBUG #include "core/system/debug.hpp" // Para Debug #endif // Constructor Player::Player(const Data& player) : room_(player.room) { initSprite(player.animations_path); setColor(); applySpawnValues(player.spawn_data); placeSprite(); initSounds(); previous_state_ = state_; } // Pinta el jugador en pantalla void Player::render() { sprite_->render(1, color_); #ifdef _DEBUG if (Debug::get()->isEnabled()) { Screen::get()->getRendererSurface()->putPixel(under_right_foot_.x, under_right_foot_.y, 8); Screen::get()->getRendererSurface()->putPixel(under_left_foot_.x, under_left_foot_.y, 8); } #endif } // Actualiza las variables del objeto void Player::update(float delta_time) { if (!is_paused_) { handleInput(); updateState(delta_time); move(delta_time); handleKillingTiles(); // Los collider_points_ están actualizados por syncSpriteAndCollider() dentro de move() animate(delta_time); border_ = handleBorders(); } } // Comprueba las entradas y modifica variables void Player::handleInput() { if (ignore_input_) { return; } if (Input::get()->checkAction(InputAction::LEFT)) { wanna_go_ = Direction::LEFT; } else if (Input::get()->checkAction(InputAction::RIGHT)) { wanna_go_ = Direction::RIGHT; } else { wanna_go_ = Direction::NONE; } const bool JUMP_PRESSED = Input::get()->checkAction(InputAction::JUMP); wanna_jump_ = JUMP_PRESSED && !jump_held_; // Solo en el flanco de pulsación jump_held_ = JUMP_PRESSED; wanna_down_ = Input::get()->checkAction(InputAction::DOWN); } // La lógica de movimiento está distribuida en move void Player::move(float delta_time) { switch (state_) { case State::ON_GROUND: moveOnGround(delta_time); break; case State::ON_SLOPE: moveOnSlope(delta_time); break; case State::ON_AIR: moveOnAir(delta_time); break; } syncSpriteAndCollider(); // Actualiza la posición del sprite y las colisiones #ifdef _DEBUG Debug::get()->set("P.X", std::to_string(static_cast(x_))); Debug::get()->set("P.Y", std::to_string(static_cast(y_))); Debug::get()->set("P.LGP", std::to_string(last_grounded_position_)); switch (state_) { case State::ON_GROUND: Debug::get()->set("P.STATE", "ON_GROUND"); break; case State::ON_SLOPE: Debug::get()->set("P.STATE", "ON_SLOPE"); break; case State::ON_AIR: Debug::get()->set("P.STATE", "ON_AIR"); break; } #endif } void Player::handleConveyorBelts() { if (!auto_movement_ and isOnConveyorBelt() and wanna_go_ == Direction::NONE) { auto_movement_ = true; } if (auto_movement_ and !isOnConveyorBelt()) { auto_movement_ = false; } } void Player::handleShouldFall() { if (!isOnFloor() and (state_ == State::ON_GROUND || state_ == State::ON_SLOPE)) { vy_ = 0.0F; transitionToState(State::ON_AIR); } } void Player::transitionToState(State state) { previous_state_ = state_; state_ = state; switch (state) { case State::ON_GROUND: vy_ = 0; if (previous_state_ == State::ON_AIR) { Audio::get()->playSound(land_sound_, Audio::Group::GAME); } current_slope_ = nullptr; break; case State::ON_SLOPE: vy_ = 0; if (previous_state_ == State::ON_AIR) { Audio::get()->playSound(land_sound_, Audio::Group::GAME); } updateCurrentSlope(); if (current_slope_ == nullptr) { // Los pies no coinciden con ninguna rampa: tratar como suelo plano state_ = State::ON_GROUND; } break; case State::ON_AIR: last_grounded_position_ = static_cast(y_); current_slope_ = nullptr; break; } } void Player::updateState(float delta_time) { switch (state_) { case State::ON_GROUND: updateOnGround(delta_time); break; case State::ON_SLOPE: updateOnSlope(delta_time); break; case State::ON_AIR: updateOnAir(delta_time); break; } } // Inicia un salto desde ON_GROUND/ON_SLOPE: velocidad inicial hacia arriba + sonido + transición void Player::startJump() { vy_ = JUMP_VELOCITY; last_grounded_position_ = y_; Audio::get()->playSound(jump_sound_, Audio::Group::GAME); transitionToState(State::ON_AIR); } // Actualización lógica del estado ON_GROUND void Player::updateOnGround(float delta_time) { (void)delta_time; // No usado en este método, pero se mantiene por consistencia handleConveyorBelts(); // Gestiona las cintas transportadoras // El salto tiene prioridad sobre la caída por falta de suelo if (wanna_jump_) { startJump(); return; } handleShouldFall(); // Verifica si debe caer (no tiene suelo) } // Actualización lógica del estado ON_SLOPE void Player::updateOnSlope(float delta_time) { (void)delta_time; // No usado en este método, pero se mantiene por consistencia if (wanna_jump_) { startJump(); return; } // DOWN: dejarse caer atravesando la rampa if (wanna_down_) { y_ += 1.0F; vy_ = 0.0F; transitionToState(State::ON_AIR); return; } handleShouldFall(); } // Actualización lógica del estado ON_AIR void Player::updateOnAir(float delta_time) { (void)delta_time; auto_movement_ = false; // Desactiva el movimiento automático en el aire } // Movimiento físico del estado ON_GROUND void Player::moveOnGround(float delta_time) { // Determinama cuál debe ser la velocidad a partir de automovement o de wanna_go_ updateVelocity(delta_time); if (vx_ == 0.0F) { return; } // Movimiento horizontal y colision con muros applyHorizontalMovement(delta_time); // Comprueba colision con rampas, corrige y cambia estado const int SIDE_X = vx_ < 0.0F ? static_cast(x_) : static_cast(x_) + WIDTH - 1; const LineVertical SIDE = { .x = SIDE_X, .y1 = static_cast(y_) + HEIGHT - 2, .y2 = static_cast(y_) + HEIGHT - 1}; // Comprueba la rampa correspondiente según la dirección const int SLOPE_Y = vx_ < 0.0F ? room_->checkLeftSlopes(SIDE) : room_->checkRightSlopes(SIDE); if (SLOPE_Y != Collision::NONE) { // Hay rampa: sube al jugador para pegarlo a la rampa y_ = SLOPE_Y - HEIGHT; transitionToState(State::ON_SLOPE); } #ifdef _DEBUG Debug::get()->set("sl.detect_y", SLOPE_Y != Collision::NONE ? std::to_string(SLOPE_Y) : "-"); #endif // Comprueba si está sobre una rampa if (isOnSlope()) { transitionToState(State::ON_SLOPE); } } // Movimiento físico del estado ON_SLOPE void Player::moveOnSlope(float delta_time) { // Determinama cuál debe ser la velocidad a partir de automovement o de wanna_go_ updateVelocity(delta_time); // Verificar rampa válida antes de comprobar velocidad: si no hay rampa siempre caer, // independientemente de si hay o no input (evita bloqueo con vx_=0 y slope null) if (current_slope_ == nullptr) { vy_ = 0.0F; transitionToState(State::ON_AIR); return; } if (vx_ == 0.0F) { return; } // Determinar el tipo de rampa const bool IS_LEFT_SLOPE = isLeftSlope(); // Movimiento horizontal con colisión lateral applyHorizontalMovement(delta_time); // Seleccionar el pie apropiado según el tipo de rampa // Left slopes (forma \) colisionan con el pie izquierdo // Right slopes (forma /) colisionan con el pie derecho const int X = IS_LEFT_SLOPE ? x_ : x_ + WIDTH - 1; // Calcular la Y basada en la ecuación de la rampa (45 grados) // Left slope (\): y aumenta con x -> y = y1 + (x - x1) // Right slope (/): y disminuye con x -> y = y1 - (x - x1) if (IS_LEFT_SLOPE) { y_ = current_slope_->y1 + (X - current_slope_->x1) - HEIGHT; } else { y_ = current_slope_->y1 - (X - current_slope_->x1) - HEIGHT; } // Verificar si el pie ha salido de los límites horizontales de la rampa // Usar min/max porque LEFT slopes tienen x1x2 const int MIN_X = std::min(current_slope_->x1, current_slope_->x2); const int MAX_X = std::max(current_slope_->x1, current_slope_->x2); const bool OUT_OF_BOUNDS = (X < MIN_X) || (X > MAX_X); #ifdef _DEBUG Debug::get()->set("sl.foot", std::to_string(X)); Debug::get()->set("sl.y_c", std::to_string(static_cast(y_))); Debug::get()->set("sl.oob", OUT_OF_BOUNDS ? "YES" : "ok"); #endif if (OUT_OF_BOUNDS) { // Determinar si estamos saliendo por arriba o por abajo de la rampa const bool EXITING_DOWNWARD = (X > current_slope_->x2 && IS_LEFT_SLOPE) || (X < current_slope_->x1 && !IS_LEFT_SLOPE); const bool EXITING_UPWARD = (X < current_slope_->x1 && IS_LEFT_SLOPE) || (X > current_slope_->x2 && !IS_LEFT_SLOPE); #ifdef _DEBUG Debug::get()->set("sl.oob", EXITING_DOWNWARD ? "DOWN" : "UP"); #endif if (EXITING_DOWNWARD) { // Salida por abajo: no hacer nada // y_ += 1.0F; } if (EXITING_UPWARD) { // Salida por arriba: bajar un pixel ya que ha subido 1 de mas al salirse de la recta y_ += 1.0F; } // Verificar si hay soporte debajo (suelo plano o conveyor belt) if (isOnTopSurface() || isOnConveyorBelt()) { // Hay soporte: transición a ON_GROUND (podría ser superficie o conveyor belt) transitionToState(State::ON_GROUND); } else { // Sin soporte: empezar a caer vy_ = 0.0F; transitionToState(State::ON_AIR); } return; } // Verificar transición a superficie plana /*if (isOnTopSurface()) { transitionToState(State::ON_GROUND); return; }*/ } // Movimiento físico del estado ON_AIR // El jugador puede moverse horizontalmente en el aire y la gravedad siempre actúa. void Player::moveOnAir(float delta_time) { // Movimiento horizontal libre según wanna_go_ (permite girar en el aire) updateVelocity(delta_time); applyHorizontalMovement(delta_time); // Gravedad applyGravity(delta_time); const float DISPLACEMENT_Y = vy_ * delta_time; // Subiendo: comprobar techo if (vy_ < 0.0F) { const SDL_FRect PROJECTION = getProjection(Direction::UP, DISPLACEMENT_Y); const int POS = room_->checkBottomSurfaces(PROJECTION); if (POS == Collision::NONE) { y_ += DISPLACEMENT_Y; } else { // Choque con techo: se pega por debajo y empieza a caer y_ = POS + 1; vy_ = 0.0F; } return; } // Bajando: comprobar aterrizaje en superficies y rampas if (vy_ > 0.0F) { const SDL_FRect PROJECTION = getProjection(Direction::DOWN, DISPLACEMENT_Y); handleLandingFromAir(DISPLACEMENT_Y, PROJECTION); } } // Comprueba si está situado en alguno de los cuatro bordes de la habitación auto Player::handleBorders() -> Room::Border { if (x_ < PlayArea::LEFT) { return Room::Border::LEFT; } if (x_ + WIDTH > PlayArea::RIGHT) { return Room::Border::RIGHT; } if (y_ < PlayArea::TOP) { return Room::Border::TOP; } if (y_ + HEIGHT > PlayArea::BOTTOM) { // Restricción de muerte por altura de caída desactivada return Room::Border::BOTTOM; } return Room::Border::NONE; } // Cambia al jugador de un borde al opuesto. Util para el cambio de pantalla void Player::switchBorders() { switch (border_) { case Room::Border::TOP: y_ = PlayArea::BOTTOM - HEIGHT - Tile::SIZE; // CRÍTICO: Resetear last_grounded_position_ para evitar muerte falsa por diferencia de Y entre pantallas last_grounded_position_ = static_cast(y_); transitionToState(State::ON_GROUND); // TODO: Detectar si debe ser ON_SLOPE break; case Room::Border::BOTTOM: y_ = PlayArea::TOP; // CRÍTICO: Resetear last_grounded_position_ para evitar muerte falsa por diferencia de Y entre pantallas last_grounded_position_ = static_cast(y_); transitionToState(State::ON_GROUND); // TODO: Detectar si debe ser ON_SLOPE break; case Room::Border::RIGHT: x_ = PlayArea::LEFT; break; case Room::Border::LEFT: x_ = PlayArea::RIGHT - WIDTH; break; default: break; } border_ = Room::Border::NONE; syncSpriteAndCollider(); } // Aplica gravedad al jugador void Player::applyGravity(float delta_time) { if (state_ == State::ON_AIR) { // Si está subiendo y ha soltado el botón de salto, gravedad aumentada para cortar el salto const float GRAVITY = (vy_ < 0.0F && !jump_held_) ? GRAVITY_FORCE * LOW_JUMP_GRAVITY_MULT : GRAVITY_FORCE; vy_ += GRAVITY * delta_time; vy_ = std::min(vy_, MAX_VY); } } // Establece la animación del jugador void Player::animate(float delta_time) { // NOLINT(readability-make-member-function-const) if (state_ == State::ON_AIR) { sprite_->setCurrentAnimation("jump"); } else if (vx_ != 0) { sprite_->setCurrentAnimation("default"); sprite_->update(delta_time); } else { sprite_->setCurrentAnimation("stand"); } } // Comprueba si el jugador tiene suelo debajo de los pies auto Player::isOnFloor() -> bool { bool on_top_surface = false; bool on_conveyor_belt = false; updateFeet(); // Comprueba las superficies on_top_surface |= room_->checkTopSurfaces(under_left_foot_); on_top_surface |= room_->checkTopSurfaces(under_right_foot_); // Comprueba las cintas transportadoras on_conveyor_belt |= room_->checkConveyorBelts(under_left_foot_); on_conveyor_belt |= room_->checkConveyorBelts(under_right_foot_); // Comprueba las rampas auto on_slope_l = room_->checkLeftSlopes(under_left_foot_); auto on_slope_r = room_->checkRightSlopes(under_right_foot_); return on_top_surface || on_conveyor_belt || on_slope_l || on_slope_r; } // Comprueba si el jugador está sobre una superficie auto Player::isOnTopSurface() -> bool { bool on_top_surface = false; updateFeet(); // Comprueba las superficies on_top_surface |= room_->checkTopSurfaces(under_left_foot_); on_top_surface |= room_->checkTopSurfaces(under_right_foot_); return on_top_surface; } // Comprueba si el jugador esta sobre una cinta transportadora auto Player::isOnConveyorBelt() -> bool { bool on_conveyor_belt = false; updateFeet(); // Comprueba las superficies on_conveyor_belt |= room_->checkConveyorBelts(under_left_foot_); on_conveyor_belt |= room_->checkConveyorBelts(under_right_foot_); return on_conveyor_belt; } // Comprueba si el jugador está sobre una rampa // Retorna true SOLO si un pie está en rampa Y el otro pie está volando (sin soporte) auto Player::isOnSlope() -> bool { updateFeet(); // Verificar qué pie está en qué tipo de rampa const bool LEFT_FOOT_ON_LEFT_SLOPE = room_->checkLeftSlopes(under_left_foot_); const bool RIGHT_FOOT_ON_RIGHT_SLOPE = room_->checkRightSlopes(under_right_foot_); // Verificar si cada pie está "volando" (sin soporte: ni top surface ni conveyor belt) const bool LEFT_FOOT_FLYING = !(room_->checkTopSurfaces(under_left_foot_) || room_->checkConveyorBelts(under_left_foot_)); const bool RIGHT_FOOT_FLYING = !(room_->checkTopSurfaces(under_right_foot_) || room_->checkConveyorBelts(under_right_foot_)); // Retornar true si UN pie en rampa Y el OTRO volando return (LEFT_FOOT_ON_LEFT_SLOPE && RIGHT_FOOT_FLYING) || (RIGHT_FOOT_ON_RIGHT_SLOPE && LEFT_FOOT_FLYING); } // Comprueba si current_slope_ es una rampa izquierda (ascendente a la izquierda) // Las rampas izquierdas tienen forma \ con x1 < x2 (x aumenta de izq a der) auto Player::isLeftSlope() -> bool { if (current_slope_ == nullptr) { return false; } // Left slopes (\): x1 < x2 (x aumenta de izquierda a derecha) // Right slopes (/): x1 > x2 (x decrece de izquierda a derecha) return current_slope_->x1 < current_slope_->x2; } // Actualiza current_slope_ con la rampa correcta según el pie que toca void Player::updateCurrentSlope() { updateFeet(); // Left slopes (\) ascendentes a izquierda tocan el pie izquierdo if (room_->checkLeftSlopes(under_left_foot_)) { current_slope_ = room_->getSlopeAtPoint(under_left_foot_); } // Right slopes (/) ascendentes a derecha tocan el pie derecho else if (room_->checkRightSlopes(under_right_foot_)) { current_slope_ = room_->getSlopeAtPoint(under_right_foot_); } // Fallback para casos edge else { current_slope_ = room_->getSlopeAtPoint(under_left_foot_); if (current_slope_ == nullptr) { current_slope_ = room_->getSlopeAtPoint(under_right_foot_); } } #ifdef _DEBUG if (current_slope_ != nullptr) { Debug::get()->set("sl.type", isLeftSlope() ? "L\\" : "R/"); Debug::get()->set("sl.p1", std::to_string(current_slope_->x1) + "," + std::to_string(current_slope_->y1)); Debug::get()->set("sl.p2", std::to_string(current_slope_->x2) + "," + std::to_string(current_slope_->y2)); } else { Debug::get()->set("sl.type", "null"); Debug::get()->unset("sl.p1"); Debug::get()->unset("sl.p2"); } #endif } // Comprueba que el jugador no toque ningun tile de los que matan auto Player::handleKillingTiles() -> bool { // Comprueba si hay contacto con algún tile que mata if (std::ranges::any_of(collider_points_, [this](const auto& c) -> bool { return room_->getTile(c) == Room::Tile::KILL; })) { markAsDead(); // Mata al jugador inmediatamente return true; // Retorna en cuanto se detecta una colisión } return false; // No se encontró ninguna colisión } // Establece el color del jugador (0 = automático según options) void Player::setColor(Uint8 color) { if (color != 0) { color_ = color; return; } // Color personalizado desde opciones if (Options::game.player_color >= 0) { color_ = static_cast(Options::game.player_color); } else { color_ = 14; } // Si el color coincide con el fondo de la habitación, usar fallback if (room_ != nullptr && color_ == room_->getBGColor()) { color_ = (room_->getBGColor() != 14) ? 14 : 1; } } // Actualiza los puntos de colisión void Player::updateColliderPoints() { // 3 columnas × 4 filas: garantiza que cada tile de 8×8 que solape el jugador tenga al menos un punto const float L = x_; const float M = x_ + (WIDTH / 2); const float R = x_ + WIDTH - 1; const float Y0 = y_; const float Y1 = y_ + 8; const float Y2 = y_ + 16; const float Y3 = y_ + HEIGHT - 1; collider_points_[0] = {.x = L, .y = Y0}; collider_points_[1] = {.x = M, .y = Y0}; collider_points_[2] = {.x = R, .y = Y0}; collider_points_[3] = {.x = L, .y = Y1}; collider_points_[4] = {.x = M, .y = Y1}; collider_points_[5] = {.x = R, .y = Y1}; collider_points_[6] = {.x = L, .y = Y2}; collider_points_[7] = {.x = M, .y = Y2}; collider_points_[8] = {.x = R, .y = Y2}; collider_points_[9] = {.x = L, .y = Y3}; collider_points_[10] = {.x = M, .y = Y3}; collider_points_[11] = {.x = R, .y = Y3}; } // Actualiza los puntos de los pies void Player::updateFeet() { under_left_foot_ = { .x = x_, .y = y_ + HEIGHT}; under_right_foot_ = { .x = x_ + WIDTH - 1, .y = y_ + HEIGHT}; } // Inicializa los sonidos de salto y aterrizaje void Player::initSounds() { // NOLINT(readability-convert-member-functions-to-static) jump_sound_ = Resource::Cache::get()->getSound("jump.wav"); land_sound_ = Resource::Cache::get()->getSound("land.wav"); } // Aplica los valores de spawn al jugador void Player::applySpawnValues(const SpawnData& spawn) { x_ = spawn.x; y_ = spawn.y; y_prev_ = spawn.y; // Inicializar y_prev_ igual a y_ para evitar saltos en primer frame vx_ = spawn.vx; vy_ = spawn.vy; last_grounded_position_ = spawn.last_grounded_position; state_ = spawn.state; sprite_->setFlip(spawn.flip); } // Resuelve nombre de skin a fichero de animación auto Player::skinToAnimationPath(const std::string& skin_name) -> std::string { if (skin_name == "default") { return "player.yaml"; } return skin_name + ".yaml"; } // Cambia la skin del jugador en caliente preservando la orientación actual void Player::setSkin(const std::string& skin_name) { const auto FLIP = sprite_->getFlip(); initSprite(skinToAnimationPath(skin_name)); sprite_->setFlip(FLIP); } // Inicializa el sprite del jugador void Player::initSprite(const std::string& animations_path) { // NOLINT(readability-convert-member-functions-to-static) const auto& animation_data = Resource::Cache::get()->getAnimationData(animations_path); sprite_ = std::make_unique(animation_data); sprite_->setWidth(WIDTH); sprite_->setHeight(HEIGHT); sprite_->setCurrentAnimation("default"); } // Actualiza la posición del sprite y las colisiones void Player::syncSpriteAndCollider() { placeSprite(); // Coloca el sprite en la posición del jugador collider_box_ = getRect(); // Actualiza el rectangulo de colisión updateColliderPoints(); // Actualiza los puntos de colisión #ifdef _DEBUG updateFeet(); #endif } // Coloca el sprite en la posición del jugador void Player::placeSprite() { sprite_->setPos(x_, y_); } // Calcula la velocidad en x con inercia ligera (interpolación hacia vel. objetivo) void Player::updateVelocity(float delta_time) { float target = 0.0F; if (auto_movement_) { // La cinta transportadora tiene el control target = HORIZONTAL_VELOCITY * room_->getConveyorBeltDirection(); } else { switch (wanna_go_) { case Direction::LEFT: target = -HORIZONTAL_VELOCITY; break; case Direction::RIGHT: target = HORIZONTAL_VELOCITY; break; default: target = 0.0F; break; } } // Orientación del sprite según la dirección deseada (sin cambiar cuando target=0) if (target > 0.0F) { sprite_->setFlip(Flip::RIGHT); } else if (target < 0.0F) { sprite_->setFlip(Flip::LEFT); } // Inercia: // - En el aire: inercia completa (arranque y frenada graduales) // - En suelo/rampa: arranque instantáneo, frenada gradual const float STEP = HORIZONTAL_ACCEL * delta_time; if (state_ == State::ON_AIR) { if (vx_ < target) { vx_ = std::min(vx_ + STEP, target); } else if (vx_ > target) { vx_ = std::max(vx_ - STEP, target); } } else { if (target != 0.0F) { vx_ = target; // Arranque instantáneo } else if (vx_ > 0.0F) { vx_ = std::max(vx_ - STEP, 0.0F); // Frenada gradual } else if (vx_ < 0.0F) { vx_ = std::min(vx_ + STEP, 0.0F); // Frenada gradual } } } // Aplica movimiento horizontal con colisión de muros void Player::applyHorizontalMovement(float delta_time) { if (vx_ == 0.0F) { return; } const float DISPLACEMENT = vx_ * delta_time; if (vx_ < 0.0F) { const SDL_FRect PROJECTION = getProjection(Direction::LEFT, DISPLACEMENT); const int POS = room_->checkRightSurfaces(PROJECTION); if (POS == Collision::NONE) { x_ += DISPLACEMENT; } else { x_ = POS + 1; } } else { const SDL_FRect PROJECTION = getProjection(Direction::RIGHT, DISPLACEMENT); const int POS = room_->checkLeftSurfaces(PROJECTION); if (POS == Collision::NONE) { x_ += DISPLACEMENT; } else { x_ = POS - WIDTH; } } } // Detecta aterrizaje en superficies y rampas auto Player::handleLandingFromAir(float displacement, const SDL_FRect& projection) -> bool { // Comprueba la colisión con las superficies y las cintas transportadoras const float POS = std::max(room_->checkTopSurfaces(projection), room_->checkAutoSurfaces(projection)); if (POS != Collision::NONE) { // Si hay colisión lo mueve hasta donde no colisiona y pasa a estar sobre la superficie y_ = POS - HEIGHT; transitionToState(State::ON_GROUND); return true; } // Comprueba la colisión con las rampas auto rect = toSDLRect(projection); const LineVertical LEFT_SIDE = {.x = rect.x, .y1 = rect.y, .y2 = rect.y + rect.h}; const LineVertical RIGHT_SIDE = {.x = rect.x + rect.w - 1, .y1 = rect.y, .y2 = rect.y + rect.h}; const float POINT = std::max(room_->checkRightSlopes(RIGHT_SIDE), room_->checkLeftSlopes(LEFT_SIDE)); if (POINT != Collision::NONE) { y_ = POINT - HEIGHT; transitionToState(State::ON_SLOPE); return true; } // No hay colisión y_ += displacement; #ifdef _DEBUG // Guarda por si en debug el jugador se sale de la pantalla, para que no esté cayendo infinitamente if (y_ > PlayArea::BOTTOM + HEIGHT) { y_ = PlayArea::TOP + 2; } #endif return false; } // Devuelve el rectangulo de proyeccion auto Player::getProjection(Direction direction, float displacement) -> SDL_FRect { // NOLINT(readability-convert-member-functions-to-static) switch (direction) { case Direction::LEFT: return { .x = x_ + displacement, .y = y_, .w = std::ceil(std::fabs(displacement)), // Para evitar que tenga una anchura de 0 pixels .h = HEIGHT - 1}; // -1 para dar ventana de 2px en aperturas de altura exacta case Direction::RIGHT: return { .x = x_ + WIDTH, .y = y_, .w = std::ceil(displacement), // Para evitar que tenga una anchura de 0 pixels .h = HEIGHT - 1}; // -1 para dar ventana de 2px en aperturas de altura exacta case Direction::UP: return { .x = x_, .y = y_ + displacement, .w = WIDTH, .h = std::ceil(std::fabs(displacement)) // Para evitar que tenga una altura de 0 pixels }; case Direction::DOWN: return { .x = x_, .y = y_ + HEIGHT, .w = WIDTH, .h = std::ceil(displacement) // Para evitar que tenga una altura de 0 pixels }; default: return { .x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; } } // Marca al jugador como muerto void Player::markAsDead() { is_alive_ = (Options::cheats.invincible == Options::Cheat::State::ENABLED); } #ifdef _DEBUG // Establece la posición del jugador directamente (debug) void Player::setDebugPosition(float x, float y) { x_ = x; y_ = y; syncSpriteAndCollider(); } // Fija estado ON_GROUND, velocidades a 0, actualiza last_grounded_position_ (debug) void Player::finalizeDebugTeleport() { vx_ = 0.0F; vy_ = 0.0F; last_grounded_position_ = static_cast(y_); transitionToState(State::ON_GROUND); syncSpriteAndCollider(); } #endif