// 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/surface_animated_sprite.hpp" // Para SAnimatedSprite #include "core/resources/resource_cache.hpp" // Para Resource #include "game/defaults.hpp" // Para Defaults::Sound #include "game/gameplay/room.hpp" // Para Room, TileType #include "game/options.hpp" // Para Cheat, Options, options #include "utils/color.hpp" // Para Color #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(); previous_state_ = state_; } // Pinta el jugador en pantalla void Player::render() { sprite_->render(); #ifdef _DEBUG if (Debug::get()->isEnabled()) { Screen::get()->getRendererSurface()->putPixel(under_right_foot_.x, under_right_foot_.y, Color::index(Color::Cpc::GREEN)); Screen::get()->getRendererSurface()->putPixel(under_left_foot_.x, under_left_foot_.y, Color::index(Color::Cpc::GREEN)); } #endif } // Actualiza las variables del objeto void Player::update(float delta_time) { if (!is_paused_) { handleInput(); updateState(delta_time); move(delta_time); animate(delta_time); border_ = handleBorders(); } } // Comprueba las entradas y modifica variables void Player::handleInput() { 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; } wanna_jump_ = Input::get()->checkAction(InputAction::JUMP); } // 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_AIR: moveOnAir(delta_time); break; } syncSpriteAndCollider(); // Actualiza la posición del sprite y las colisiones #ifdef _DEBUG Debug::get()->add(std::string("X : " + std::to_string(static_cast(x_)))); Debug::get()->add(std::string("Y : " + std::to_string(static_cast(y_)))); Debug::get()->add(std::string("LGP: " + std::to_string(last_grounded_position_))); switch (state_) { case State::ON_GROUND: Debug::get()->add(std::string("ON_GROUND")); break; case State::ON_AIR: Debug::get()->add(std::string("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) { // Pasar a ON_AIR sin impulso de salto (caída) previous_state_ = state_; state_ = State::ON_AIR; last_grounded_position_ = static_cast(y_); vy_ = 0.0F; // Sin impulso inicial, la gravedad lo acelerará } } void Player::transitionToState(State state) { previous_state_ = state_; state_ = state; switch (state) { case State::ON_GROUND: vy_ = 0; break; case State::ON_AIR: if (previous_state_ == State::ON_GROUND) { vy_ = JUMP_VELOCITY; // Impulso de salto last_grounded_position_ = y_; Audio::get()->playSound(Defaults::Sound::JUMP, Audio::Group::GAME); } break; } } void Player::updateState(float delta_time) { switch (state_) { case State::ON_GROUND: updateOnGround(delta_time); break; case State::ON_AIR: updateOnAir(delta_time); break; } } // 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 handleShouldFall(); // Verifica si debe caer (no tiene suelo) // Verifica si el jugador quiere saltar if (wanna_jump_) { transitionToState(State::ON_AIR); } } // Actualización lógica del estado ON_AIR void Player::updateOnAir(float delta_time) { (void)delta_time; // No usado, pero se mantiene por consistencia de interfaz auto_movement_ = false; // Desactiva el movimiento automático mientras está en el aire } // Movimiento físico del estado ON_GROUND void Player::moveOnGround(float delta_time) { // Determina 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); } // Movimiento físico del estado ON_AIR void Player::moveOnAir(float delta_time) { // Movimiento horizontal (el jugador puede moverse en el aire) updateVelocity(delta_time); applyHorizontalMovement(delta_time); // Aplicar gravedad applyGravity(delta_time); const float DISPLACEMENT_Y = vy_ * delta_time; // Movimiento vertical hacia arriba 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 { // Colisión con el techo: detener ascenso y_ = POS + 1; vy_ = 0.0F; } } // Movimiento vertical hacia abajo else 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) { 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); 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); 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) { // La gravedad se aplica siempre que el jugador está en el aire if (state_ == State::ON_AIR) { vy_ += GRAVITY_FORCE * delta_time; } } // Establece la animación del jugador void Player::animate(float delta_time) { if (vx_ != 0) { sprite_->update(delta_time); } } // 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_); return on_top_surface || on_conveyor_belt; } // 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 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) { 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 cheats) void Player::setColor(Uint8 color) { if (color != 0) { color_ = color; return; } if (Options::cheats.invincible == Options::Cheat::State::ENABLED) { color_ = Color::index(Color::Cpc::CYAN); } else if (Options::cheats.infinite_lives == Options::Cheat::State::ENABLED) { color_ = Color::index(Color::Cpc::YELLOW); } else { color_ = Color::index(Color::Cpc::WHITE); } } // Actualiza los puntos de colisión void Player::updateColliderPoints() { const SDL_FRect RECT = getRect(); collider_points_[0] = {.x = RECT.x, .y = RECT.y}; collider_points_[1] = {.x = RECT.x + 7, .y = RECT.y}; collider_points_[2] = {.x = RECT.x + 7, .y = RECT.y + 7}; collider_points_[3] = {.x = RECT.x, .y = RECT.y + 7}; collider_points_[4] = {.x = RECT.x, .y = RECT.y + 8}; collider_points_[5] = {.x = RECT.x + 7, .y = RECT.y + 8}; collider_points_[6] = {.x = RECT.x + 7, .y = RECT.y + 15}; collider_points_[7] = {.x = RECT.x, .y = RECT.y + 15}; } // 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}; } // 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); } // Inicializa el sprite del jugador void Player::initSprite(const std::string& animations_path) { const auto& animation_data = Resource::Cache::get()->getAnimationData(animations_path); sprite_ = std::make_unique(animation_data); sprite_->setWidth(WIDTH); sprite_->setHeight(HEIGHT); sprite_->setCurrentAnimation("walk"); } // 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 sistema caminar/correr y momentum void Player::updateVelocity(float delta_time) { if (auto_movement_) { // La cinta transportadora tiene el control (velocidad fija) vx_ = RUN_VELOCITY * room_->getConveyorBeltDirection(); sprite_->setFlip(vx_ < 0.0F ? Flip::LEFT : Flip::RIGHT); movement_time_ = 0.0F; } else { // El jugador tiene el control switch (wanna_go_) { case Direction::LEFT: movement_time_ += delta_time; if (movement_time_ < TIME_TO_RUN) { // Caminando: velocidad fija inmediata vx_ = -WALK_VELOCITY; } else { // Corriendo: acelerar hacia RUN_VELOCITY vx_ -= RUN_ACCELERATION * delta_time; vx_ = std::max(vx_, -RUN_VELOCITY); } sprite_->setFlip(Flip::LEFT); break; case Direction::RIGHT: movement_time_ += delta_time; if (movement_time_ < TIME_TO_RUN) { // Caminando: velocidad fija inmediata vx_ = WALK_VELOCITY; } else { // Corriendo: acelerar hacia RUN_VELOCITY vx_ += RUN_ACCELERATION * delta_time; vx_ = std::min(vx_, RUN_VELOCITY); } sprite_->setFlip(Flip::RIGHT); break; case Direction::NONE: movement_time_ = 0.0F; // Desacelerar gradualmente (momentum) if (vx_ > 0.0F) { vx_ -= HORIZONTAL_DECELERATION * delta_time; vx_ = std::max(vx_, 0.0F); } else if (vx_ < 0.0F) { vx_ += HORIZONTAL_DECELERATION * delta_time; vx_ = std::min(vx_, 0.0F); } break; default: break; } } } // 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 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); Audio::get()->playSound(Defaults::Sound::LAND, Audio::Group::GAME); return true; } // No hay colisión y_ += displacement; return false; } // Devuelve el rectangulo de proyeccion auto Player::getProjection(Direction direction, float displacement) -> SDL_FRect { 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}; case Direction::RIGHT: return { .x = x_ + WIDTH, .y = y_, .w = std::ceil(displacement), // Para evitar que tenga una anchura de 0 pixels .h = HEIGHT}; 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() { if (Options::cheats.invincible == Options::Cheat::State::ENABLED) { is_alive_ = true; // No puede morir } else { is_alive_ = false; // Muere } } #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