// 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/input/input.hpp" // Para Input, InputAction #include "core/rendering/surface_animated_sprite.hpp" // Para SAnimatedSprite #include "core/resources/resource.hpp" // Para Resource #include "external/jail_audio.h" // Para JA_PlaySound #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 // Constructor Player::Player(const Data& player) : room_(player.room) { // Inicializa algunas variables 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_); } // Actualiza las variables del objeto void Player::update(float delta_time) { if (!is_paused_) { checkInput(); // Comprueba las entradas y establece las velocidades y el flip move(delta_time); // Recalcula la posición del jugador animate(delta_time); // Establece la animación del jugador updateState(delta_time); // Comprueba el estado del jugador checkBorders(); // Comprueba si está situado en alguno de los cuatro bordes de la habitación checkJumpEnd(); // Comprueba si ha finalizado el salto al alcanzar la altura de inicio checkKillingTiles(); // Comprueba que el jugador no toque ningun tile de los que matan} setColor(); } } // Comprueba las entradas y establece las velocidades y el flip void Player::checkInput() { // Solo comprueba las entradas de dirección cuando está sobre una superficie if (state_ != State::STANDING) { return; } if (!auto_movement_) { // Comprueba las entradas de desplazamiento lateral solo en el caso de no estar enganchado a una superficie automatica if (Input::get()->checkInput(InputAction::LEFT)) { vx_ = -HORIZONTAL_VELOCITY; sprite_->setFlip(SDL_FLIP_HORIZONTAL); left_or_right_is_pressed_ = true; } else if (Input::get()->checkInput(InputAction::RIGHT)) { vx_ = HORIZONTAL_VELOCITY; sprite_->setFlip(SDL_FLIP_NONE); left_or_right_is_pressed_ = true; } else { // No se pulsa ninguna dirección vx_ = 0.0F; left_or_right_is_pressed_ = false; } } else { // El movimiento lo proporciona la superficie vx_ = HORIZONTAL_VELOCITY * room_->getAutoSurfaceDirection(); sprite_->setFlip(vx_ > 0.0F ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL); } if (Input::get()->checkInput(InputAction::JUMP)) { /* Solo puede saltar si ademas de estar (State::STANDING) Esta sobre el suelo, rampa o suelo que se mueve Esto es para evitar el salto desde el vacio al cambiar de pantalla verticalmente Ya que se coloca el estado State::STANDING al cambiar de pantalla */ if (isOnFloor() || isOnAutoSurface()) { setState(State::JUMPING); } } } // Comprueba si está situado en alguno de los cuatro bordes de la habitación void Player::checkBorders() { if (x_ < PLAY_AREA_LEFT) { border_ = Room::Border::LEFT; is_on_border_ = true; } else if (x_ + WIDTH > PLAY_AREA_RIGHT) { border_ = Room::Border::RIGHT; is_on_border_ = true; } else if (y_ < PLAY_AREA_TOP) { border_ = Room::Border::TOP; is_on_border_ = true; } else if (y_ + HEIGHT > PLAY_AREA_BOTTOM) { border_ = Room::Border::BOTTOM; is_on_border_ = true; } else { is_on_border_ = false; } } // Comprueba el estado del jugador void Player::updateState(float delta_time) { // Actualiza las variables en función del estado switch (state_) { case State::STANDING: { // Calcula la distancia de caída en pixels (velocidad * tiempo) const int FALLING_DISTANCE = static_cast(y_) - last_grounded_position_; if (previous_state_ == State::FALLING && FALLING_DISTANCE > MAX_FALLING_HEIGHT) { // Si cae de muy alto, el jugador muere is_alive_ = false; } vy_ = 0.0F; jumping_time_ = 0.0F; last_grounded_position_ = static_cast(y_); auto_movement_ = !isOnFloor() && isOnAutoSurface(); if (!isOnFloor() && !isOnAutoSurface() && !isOnDownSlope()) { setState(State::FALLING); } break; } case State::FALLING: { vx_ = 0.0F; vy_ = MAX_VY; playFallSound(); break; } case State::JUMPING: { last_grounded_position_ = static_cast(y_); jumping_time_ += delta_time; playJumpSound(); break; } default: break; } } // Cambia al jugador de un borde al opuesto. Util para el cambio de pantalla void Player::switchBorders() { switch (border_) { case Room::Border::TOP: y_ = PLAY_AREA_BOTTOM - HEIGHT - TILE_SIZE; setState(State::STANDING); break; case Room::Border::BOTTOM: y_ = PLAY_AREA_TOP; setState(State::STANDING); break; case Room::Border::RIGHT: x_ = PLAY_AREA_LEFT; break; case Room::Border::LEFT: x_ = PLAY_AREA_RIGHT - WIDTH; break; default: break; } is_on_border_ = false; placeSprite(); } // Aplica gravedad al jugador void Player::applyGravity(float delta_time) { // La gravedad solo se aplica cuando el jugador esta saltando // Nunca mientras cae o esta de pie if (state_ == State::JUMPING) { vy_ += GRAVITY_FORCE * delta_time; vy_ = std::min(vy_, MAX_VY); } } // Maneja el movimiento horizontal hacia la izquierda void Player::moveHorizontalLeft(float delta_time) { // Crea el rectangulo de proyección en el eje X para ver si colisiona const float DISPLACEMENT = vx_ * delta_time; SDL_FRect proj = { .x = x_ + DISPLACEMENT, .y = y_, .w = std::ceil(std::fabs(DISPLACEMENT)), // Para evitar que tenga un ancho de 0 pixels .h = HEIGHT}; // Comprueba la colisión con las superficies const int POS = room_->checkRightSurfaces(&proj); // Calcula la nueva posición if (POS == -1) { // Si no hay colisión x_ += DISPLACEMENT; } else { // Si hay colisión lo mueve hasta donde no colisiona x_ = POS + 1; std::cout << "LEFT\n"; } // Si ha tocado alguna rampa mientras camina (sin saltar) if (state_ == State::STANDING) { // Si está bajando la rampa, recoloca al jugador if (isOnDownSlope() && state_ == State::STANDING) { y_ += 1; std::cout << "RAMP DOWN LEFT\n"; } // Asciende else { const LineVertical LEFT_SIDE = {.x = static_cast(x_), .y1 = static_cast(y_) + static_cast(HEIGHT) - 2, .y2 = static_cast(y_) + static_cast(HEIGHT) - 1}; // Comprueba solo los dos pixels de abajo const int LY = room_->checkLeftSlopes(&LEFT_SIDE); if (LY > -1) { y_ = LY - HEIGHT; std::cout << "RAMP UP LEFT\n"; } } } } // Maneja el movimiento horizontal hacia la derecha void Player::moveHorizontalRight(float delta_time) { // Crea el rectangulo de proyección en el eje X para ver si colisiona const float DISPLACEMENT = vx_ * delta_time; SDL_FRect proj = { .x = x_ + WIDTH, .y = y_, .w = std::ceil(DISPLACEMENT), // Para evitar que tenga un ancho de 0 pixels .h = HEIGHT}; // Comprueba la colisión const int POS = room_->checkLeftSurfaces(&proj); // Calcula la nueva posición if (POS == -1) { // Si no hay colisión x_ += DISPLACEMENT; } else { // Si hay colisión lo mueve hasta donde no colisiona x_ = POS - WIDTH; std::cout << "RIGHT\n"; } // Si ha tocado alguna rampa mientras camina (sin saltar) if (state_ == State::STANDING) { // Si está bajando la rampa, recoloca al jugador if (isOnDownSlope() && state_ == State::STANDING) { y_ += 1; std::cout << "RAMP DOWN RIGHT\n"; } // Asciende else { const LineVertical RIGHT_SIDE = {.x = static_cast(x_) + static_cast(WIDTH) - 1, .y1 = static_cast(y_) + static_cast(HEIGHT) - 2, .y2 = static_cast(y_) + static_cast(HEIGHT) - 1}; // Comprueba solo los dos pixels de abajo const int RY = room_->checkRightSlopes(&RIGHT_SIDE); if (RY > -1) { y_ = RY - HEIGHT; std::cout << "RAMP UP RIGHT\n"; } } } } // Maneja el movimiento vertical hacia arriba void Player::moveVerticalUp(float delta_time) { // Crea el rectangulo de proyección en el eje Y para ver si colisiona const float DISPLACEMENT = vy_ * delta_time; SDL_FRect proj = { .x = x_, .y = y_ + DISPLACEMENT, .w = WIDTH, .h = std::ceil(std::fabs(DISPLACEMENT)) // Para evitar que tenga una altura de 0 pixels }; // Comprueba la colisión const int POS = room_->checkBottomSurfaces(&proj); // Calcula la nueva posición if (POS == -1) { // Si no hay colisión y_ += DISPLACEMENT; } else { // Si hay colisión lo mueve hasta donde no colisiona y entra en caída y_ = POS + 1; std::cout << "TOP\n"; setState(State::FALLING); } } // Maneja el movimiento vertical hacia abajo void Player::moveVerticalDown(float delta_time) { // Crea el rectangulo de proyección en el eje Y para ver si colisiona const float DISPLACEMENT = vy_ * delta_time; SDL_FRect proj = { .x = x_, .y = y_ + HEIGHT, .w = WIDTH, .h = std::ceil(DISPLACEMENT) // Para evitar que tenga una altura de 0 pixels }; // Comprueba la colisión con las superficies normales y las automáticas const float POS = std::max(room_->checkTopSurfaces(&proj), room_->checkAutoSurfaces(&proj)); if (POS > -1) { // Si hay colisión lo mueve hasta donde no colisiona y pasa a estar sobre la superficie y_ = POS - HEIGHT; std::cout << "BOTTOM\n"; setState(State::STANDING); } else { // Si no hay colisión con los muros, comprueba la colisión con las rampas if (state_ != State::JUMPING) { // Las rampas no se miran si se está saltando auto frect = getRect(); auto rect = toSDLRect(frect); const LineVertical LEFT_SIDE = {.x = rect.x, .y1 = rect.y, .y2 = rect.y + rect.h - 1}; const LineVertical RIGHT_SIDE = {.x = rect.x + rect.w - 1, .y1 = rect.y, .y2 = rect.y + rect.h - 1}; std::cout << RIGHT_SIDE.x << " " << RIGHT_SIDE.y1 << " " << RIGHT_SIDE.y2 << "\n"; const float POINT = std::max(room_->checkRightSlopes(&RIGHT_SIDE), room_->checkLeftSlopes(&LEFT_SIDE)); if (POINT > -1) { // No está saltando y hay colisión con una rampa // Calcula la nueva posición y_ = POINT - HEIGHT; std::cout << "BOTTOM SLOPE\n"; setState(State::STANDING); } else { // No está saltando y no hay colisón con una rampa // Calcula la nueva posición y_ += DISPLACEMENT; } } else { // Esta saltando y no hay colisión con los muros // Calcula la nueva posición y_ += DISPLACEMENT; } } } // Recalcula la posición del jugador y su animación void Player::move(float delta_time) { applyGravity(delta_time); // Aplica gravedad al jugador // Movimiento horizontal if (vx_ < 0.0F) { moveHorizontalLeft(delta_time); } else if (vx_ > 0.0F) { moveHorizontalRight(delta_time); } // Movimiento vertical if (vy_ < 0.0F) { moveVerticalUp(delta_time); } else if (vy_ > 0.0F) { moveVerticalDown(delta_time); } placeSprite(); // Coloca el sprite en la nueva posición } // Establece la animación del jugador void Player::animate(float delta_time) { if (vx_ != 0) { sprite_->update(delta_time); } } // Comprueba si ha finalizado el salto al alcanzar la altura de inicio void Player::checkJumpEnd() { if (state_ == State::JUMPING && vy_ > 0.0F && static_cast(y_) >= jump_init_pos_) { // Si alcanza la altura de salto inicial, pasa al estado de caída setState(State::FALLING); } } // Calcula y reproduce el sonido de salto void Player::playJumpSound() { const int SOUND_INDEX = static_cast(jumping_time_ / SOUND_INTERVAL); const int PREVIOUS_INDEX = static_cast((jumping_time_ - SOUND_INTERVAL) / SOUND_INTERVAL); // Solo reproduce el sonido cuando cambia de índice if (SOUND_INDEX != PREVIOUS_INDEX && SOUND_INDEX < static_cast(jumping_sound_.size())) { JA_PlaySound(jumping_sound_[SOUND_INDEX]); } } // Calcula y reproduce el sonido de caer void Player::playFallSound() { return; /* const int SOUND_INDEX = static_cast(falling_time_ / SOUND_INTERVAL); const int PREVIOUS_INDEX = static_cast((falling_time_ - SOUND_INTERVAL) / SOUND_INTERVAL); // Solo reproduce el sonido cuando cambia de índice if (SOUND_INDEX != PREVIOUS_INDEX) { const int CLAMPED_INDEX = std::min(SOUND_INDEX, static_cast(falling_sound_.size()) - 1); JA_PlaySound(falling_sound_[CLAMPED_INDEX]); #ifdef _DEBUG Debug::get()->add("FALL: " + std::to_string(CLAMPED_INDEX)); #endif } */ } // Comprueba si el jugador tiene suelo debajo de los pies auto Player::isOnFloor() -> bool { bool on_floor = false; bool on_slope_l = false; bool on_slope_r = false; updateFeet(); // Comprueba las superficies for (auto f : under_feet_) { on_floor |= room_->checkTopSurfaces(&f); on_floor |= room_->checkAutoSurfaces(&f); } // Comprueba las rampas on_slope_l = room_->checkLeftSlopes(&under_feet_[0]); on_slope_r = room_->checkRightSlopes(&under_feet_[1]); return on_floor || on_slope_l || on_slope_r; } // Comprueba si el jugador esta sobre una superficie automática auto Player::isOnAutoSurface() -> bool { bool on_auto_surface = false; updateFeet(); // Comprueba las superficies for (auto f : under_feet_) { on_auto_surface |= room_->checkAutoSurfaces(&f); } return on_auto_surface; } // Comprueba si el jugador está sobre una rampa hacia abajo auto Player::isOnDownSlope() -> bool { bool on_slope = false; updateFeet(); // Cuando el jugador baja una escalera, se queda volando // Hay que mirar otro pixel más por debajo SDL_FPoint foot0 = under_feet_[0]; SDL_FPoint foot1 = under_feet_[1]; foot0.y += 1.0f; foot1.y += 1.0f; // Comprueba las rampas on_slope |= room_->checkLeftSlopes(&foot0); on_slope |= room_->checkRightSlopes(&foot1); return on_slope; } // Comprueba que el jugador no toque ningun tile de los que matan auto Player::checkKillingTiles() -> 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; })) { is_alive_ = false; // 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 void Player::setColor() { /* if (Options::cheats.invincible == Options::Cheat::State::ENABLED) { color_ = static_cast(PaletteColor::CYAN); } else if (Options::cheats.infinite_lives == Options::Cheat::State::ENABLED) { color_ = static_cast(PaletteColor::YELLOW); } else { color_ = static_cast(PaletteColor::WHITE); } */ switch (state_) { case State::STANDING: color_ = static_cast(PaletteColor::YELLOW); break; case State::JUMPING: color_ = static_cast(PaletteColor::GREEN); break; case State::FALLING: color_ = static_cast(PaletteColor::RED); break; default: break; } } // 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() { const SDL_FPoint P = {x_, y_}; under_feet_[0] = {.x = P.x, .y = P.y + HEIGHT}; under_feet_[1] = {.x = P.x + 7, .y = P.y + HEIGHT}; feet_[0] = {.x = P.x, .y = P.y + HEIGHT - 1}; feet_[1] = {.x = P.x + 7, .y = P.y + HEIGHT - 1}; } // Cambia el estado del jugador void Player::setState(State value) { previous_state_ = state_; state_ = value; switch (state_) { case State::STANDING: vy_ = 0.0F; last_grounded_position_ = static_cast(y_); std::cout << "SET STANDING\n"; break; case State::JUMPING: last_grounded_position_ = static_cast(y_); vy_ = JUMP_VELOCITY; jump_init_pos_ = y_; jumping_time_ = 0.0F; std::cout << "SET JUMPING\n"; break; case State::FALLING: vx_ = 0.0F; vy_ = MAX_VY; playFallSound(); if (previous_state_ == State::STANDING) { last_grounded_position_ = static_cast(y_); } auto_movement_ = false; jumping_time_ = 0.0F; std::cout << "SET FALLING\n"; break; default: break; } // Llama a checkState con delta_time 0 porque esto es un cambio de estado inmediato updateState(0.0F); } // Inicializa los sonidos de salto y caida void Player::initSounds() { jumping_sound_.clear(); falling_sound_.clear(); for (int i = 1; i <= 24; ++i) { std::string sound_file = "jump" + std::to_string(i) + ".wav"; jumping_sound_.push_back(Resource::get()->getSound(sound_file)); if (i >= 11) { falling_sound_.push_back(Resource::get()->getSound(sound_file)); } } } // Aplica los valores de spawn al jugador void Player::applySpawnValues(const SpawnData& spawn) { x_ = spawn.x; y_ = spawn.y; vx_ = spawn.vx; vy_ = spawn.vy; jump_init_pos_ = spawn.jump_init_pos; state_ = spawn.state; sprite_->setFlip(spawn.flip); } // Inicializa el sprite del jugador void Player::initSprite(const std::string& animations_path) { auto animations = Resource::get()->getAnimations(animations_path); sprite_ = std::make_unique(animations); sprite_->setWidth(WIDTH); sprite_->setHeight(HEIGHT); sprite_->setCurrentAnimation("walk"); } // Coloca el sprite en la posición del jugador void Player::placeSprite() { sprite_->setPos(x_, y_); // Recoloca el sprite collider_box_ = getRect(); // Actualiza el rectangulo de colisión updateColliderPoints(); // Actualiza los puntos de colisión }