// IWYU pragma: no_include #include "game/entities/player.hpp" #include // Para max, min #include // Para ceil, abs #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 "core/system/debug.hpp" // Para Debug #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 PlayerData& player) : room_(player.room) { // Inicializa algunas variables initSprite(player.texture_path, player.animations_path); setColor(); applySpawnValues(player.spawn); placeSprite(); initSounds(); previous_state_ = state_; last_position_ = getRect(); collider_box_ = getRect(); collider_points_.resize(collider_points_.size() + 8, {0, 0}); under_feet_.resize(under_feet_.size() + 2, {0, 0}); feet_.resize(feet_.size() + 2, {0, 0}); #ifdef _DEBUG debug_rect_x_ = {.x = 0, .y = 0, .w = 0, .h = 0}; debug_rect_y_ = {.x = 0, .y = 0, .w = 0, .h = 0}; debug_color_ = static_cast(PaletteColor::GREEN); debug_point_ = {.x = 0, .y = 0}; #endif } // Pinta el jugador en pantalla void Player::render() { sprite_->render(1, color_); #ifdef _DEBUG renderDebugInfo(); #endif } // Actualiza las variables del objeto void Player::update() { if (!is_paused_) { checkInput(); // Comprueba las entradas y modifica variables move(); // Recalcula la posición del jugador animate(); // Establece la animación 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} } } // Comprueba las entradas y modifica variables void Player::checkInput() { // Solo comprueba las entradas de dirección cuando está sobre una superficie if (state_ != PlayerState::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_ = -0.6F; sprite_->setFlip(SDL_FLIP_HORIZONTAL); } else if (Input::get()->checkInput(InputAction::RIGHT)) { vx_ = 0.6F; sprite_->setFlip(SDL_FLIP_NONE); } else { // No se pulsa ninguna dirección vx_ = 0.0F; if (isOnAutoSurface()) { // Si deja de moverse sobre una superficie se engancha auto_movement_ = true; } } } else { // El movimiento lo proporciona la superficie vx_ = 0.6F * room_->getAutoSurfaceDirection(); if (vx_ > 0.0F) { sprite_->setFlip(SDL_FLIP_NONE); } else { sprite_->setFlip(SDL_FLIP_HORIZONTAL); } } if (Input::get()->checkInput(InputAction::JUMP)) { // Solo puede saltar si ademas de estar (state == s_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 s_standing al cambiar de pantalla if (isOnFloor() || isOnAutoSurface()) { setState(PlayerState::JUMPING); vy_ = -MAX_VY; jump_init_pos_ = y_; jumping_counter_ = 0; } } } // Comprueba si está situado en alguno de los cuatro bordes de la habitación void Player::checkBorders() { if (x_ < PLAY_AREA_LEFT) { border_ = RoomBorder::LEFT; is_on_border_ = true; } else if (x_ + WIDTH > PLAY_AREA_RIGHT) { border_ = RoomBorder::RIGHT; is_on_border_ = true; } else if (y_ < PLAY_AREA_TOP) { border_ = RoomBorder::TOP; is_on_border_ = true; } else if (y_ + HEIGHT > PLAY_AREA_BOTTOM) { border_ = RoomBorder::BOTTOM; is_on_border_ = true; } else { is_on_border_ = false; } } // Comprueba el estado del jugador void Player::checkState() { // Actualiza las variables en función del estado if (state_ == PlayerState::FALLING) { vx_ = 0.0F; vy_ = MAX_VY; falling_counter_++; playFallSound(); } else if (state_ == PlayerState::STANDING) { if (previous_state_ == PlayerState::FALLING && falling_counter_ > MAX_FALLING_HEIGHT) { // Si cae de muy alto, el jugador muere is_alive_ = false; } vy_ = 0.0F; jumping_counter_ = 0; falling_counter_ = 0; if (!isOnFloor() && !isOnAutoSurface() && !isOnDownSlope()) { setState(PlayerState::FALLING); vx_ = 0.0F; vy_ = MAX_VY; falling_counter_++; playFallSound(); } } else if (state_ == PlayerState::JUMPING) { falling_counter_ = 0; jumping_counter_++; playJumpSound(); } } // Cambia al jugador de un borde al opuesto. Util para el cambio de pantalla void Player::switchBorders() { switch (border_) { case RoomBorder::TOP: y_ = PLAY_AREA_BOTTOM - HEIGHT - BLOCK; setState(PlayerState::STANDING); break; case RoomBorder::BOTTOM: y_ = PLAY_AREA_TOP; setState(PlayerState::STANDING); break; case RoomBorder::RIGHT: x_ = PLAY_AREA_LEFT; break; case RoomBorder::LEFT: x_ = PLAY_AREA_RIGHT - WIDTH; break; default: break; } is_on_border_ = false; placeSprite(); collider_box_ = getRect(); } // Aplica gravedad al jugador void Player::applyGravity() { constexpr float GRAVITY_FORCE = 0.035F; // La gravedad solo se aplica cuando el jugador esta saltando // Nunca mientras cae o esta de pie if (state_ == PlayerState::JUMPING) { vy_ += GRAVITY_FORCE; vy_ = std::min(vy_, MAX_VY); } } // Maneja el movimiento horizontal hacia la izquierda void Player::moveHorizontalLeft() { // Crea el rectangulo de proyección en el eje X para ver si colisiona SDL_FRect proj; proj.x = static_cast(x_ + vx_); proj.y = static_cast(y_); proj.h = HEIGHT; proj.w = static_cast(std::ceil(std::fabs(vx_))); // Para evitar que tenga un ancho de 0 pixels #ifdef _DEBUG debug_rect_x_ = proj; #endif // 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_ += vx_; } else { // Si hay colisión lo mueve hasta donde no colisiona x_ = POS + 1; } // Si ha tocado alguna rampa mientras camina (sin saltar), asciende if (state_ != PlayerState::JUMPING) { 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; } } // Si está bajando la rampa, recoloca al jugador if (isOnDownSlope() && state_ != PlayerState::JUMPING) { y_ += 1; } } // Maneja el movimiento horizontal hacia la derecha void Player::moveHorizontalRight() { // Crea el rectangulo de proyección en el eje X para ver si colisiona SDL_FRect proj; proj.x = x_ + WIDTH; proj.y = y_; proj.h = HEIGHT; proj.w = std::ceil(vx_); // Para evitar que tenga un ancho de 0 pixels #ifdef _DEBUG debug_rect_x_ = proj; #endif // Comprueba la colisión const int POS = room_->checkLeftSurfaces(&proj); // Calcula la nueva posición if (POS == -1) { // Si no hay colisión x_ += vx_; } else { // Si hay colisión lo mueve hasta donde no colisiona x_ = POS - WIDTH; } // Si ha tocado alguna rampa mientras camina (sin saltar), asciende if (state_ != PlayerState::JUMPING) { 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; } } // Si está bajando la rampa, recoloca al jugador if (isOnDownSlope() && state_ != PlayerState::JUMPING) { y_ += 1; } } // Maneja el movimiento vertical hacia arriba void Player::moveVerticalUp() { // Crea el rectangulo de proyección en el eje Y para ver si colisiona SDL_FRect proj; proj.x = static_cast(x_); proj.y = static_cast(y_ + vy_); proj.h = static_cast(std::ceil(std::fabs(vy_))); // Para evitar que tenga una altura de 0 pixels proj.w = WIDTH; #ifdef _DEBUG debug_rect_y_ = proj; #endif // Comprueba la colisión const int POS = room_->checkBottomSurfaces(&proj); // Calcula la nueva posición if (POS == -1) { // Si no hay colisión y_ += vy_; } else { // Si hay colisión lo mueve hasta donde no colisiona y entra en caída y_ = POS + 1; setState(PlayerState::FALLING); } } // Maneja el movimiento vertical hacia abajo void Player::moveVerticalDown() { // Crea el rectangulo de proyección en el eje Y para ver si colisiona SDL_FRect proj; proj.x = x_; proj.y = y_ + HEIGHT; proj.h = std::ceil(vy_); // Para evitar que tenga una altura de 0 pixels proj.w = WIDTH; #ifdef _DEBUG debug_rect_y_ = proj; #endif // 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; setState(PlayerState::STANDING); // Deja de estar enganchado a la superficie automatica auto_movement_ = false; } else { // Si no hay colisión con los muros, comprueba la colisión con las rampas if (state_ != PlayerState::JUMPING) { // Las rampas no se miran si se está saltando auto rect = toSDLRect(proj); 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}; 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; setState(PlayerState::STANDING); #ifdef _DEBUG debug_color_ = static_cast(PaletteColor::YELLOW); debug_point_ = {.x = x_ + (WIDTH / 2), .y = POINT}; #endif } else { // No está saltando y no hay colisón con una rampa // Calcula la nueva posición y_ += vy_; #ifdef _DEBUG debug_color_ = static_cast(PaletteColor::RED); #endif } } else { // Esta saltando y no hay colisión con los muros // Calcula la nueva posición y_ += vy_; } } } // Recalcula la posición del jugador y su animación void Player::move() { last_position_ = {.x = x_, .y = y_}; // Guarda la posicion actual antes de modificarla applyGravity(); // Aplica gravedad al jugador checkState(); // Comprueba el estado del jugador #ifdef _DEBUG debug_color_ = static_cast(PaletteColor::GREEN); #endif // Movimiento horizontal if (vx_ < 0.0F) { moveHorizontalLeft(); } else if (vx_ > 0.0F) { moveHorizontalRight(); } // Si ha salido del suelo, el jugador cae if (state_ == PlayerState::STANDING && !isOnFloor()) { setState(PlayerState::FALLING); auto_movement_ = false; } // Si ha salido de una superficie automatica, detiene el movimiento automatico if (state_ == PlayerState::STANDING && isOnFloor() && !isOnAutoSurface()) { auto_movement_ = false; } // Movimiento vertical if (vy_ < 0.0F) { moveVerticalUp(); } else if (vy_ > 0.0F) { moveVerticalDown(); } placeSprite(); // Coloca el sprite en la nueva posición collider_box_ = getRect(); // Actualiza el rectangulo de colisión #ifdef _DEBUG Debug::get()->add("RECT_X: " + std::to_string(debug_rect_x_.x) + "," + std::to_string(debug_rect_x_.y) + "," + std::to_string(debug_rect_x_.w) + "," + std::to_string(debug_rect_x_.h)); Debug::get()->add("RECT_Y: " + std::to_string(debug_rect_y_.x) + "," + std::to_string(debug_rect_y_.y) + "," + std::to_string(debug_rect_y_.w) + "," + std::to_string(debug_rect_y_.h)); #endif } // Establece la animación del jugador void Player::animate() { if (vx_ != 0) { sprite_->update(); } } // Comprueba si ha finalizado el salto al alcanzar la altura de inicio void Player::checkJumpEnd() { if (state_ == PlayerState::JUMPING) { if (vy_ > 0) { if (y_ >= jump_init_pos_) { // Si alcanza la altura de salto inicial, pasa al estado de caída setState(PlayerState::FALLING); vy_ = MAX_VY; jumping_counter_ = 0; } } } } // Calcula y reproduce el sonido de salto void Player::playJumpSound() { if (jumping_counter_ % 4 == 0) { JA_PlaySound(jumping_sound_[jumping_counter_ / 4]); } #ifdef _DEBUG Debug::get()->add("JUMP: " + std::to_string(jumping_counter_ / 4)); #endif } // Calcula y reproduce el sonido de caer void Player::playFallSound() { if (falling_counter_ % 4 == 0) { JA_PlaySound(falling_sound_[std::min((falling_counter_ / 4), (int)falling_sound_.size() - 1)]); } #ifdef _DEBUG Debug::get()->add("FALL: " + std::to_string(falling_counter_ / 4)); #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_.data()); on_slope_r = room_->checkRightSlopes(&under_feet_[1]); #ifdef _DEBUG if (on_floor) { Debug::get()->add("ON_FLOOR"); } if (on_slope_l) { Debug::get()->add("ON_SLOPE_L: " + std::to_string(under_feet_[0].x) + "," + std::to_string(under_feet_[0].y)); } if (on_slope_r) { Debug::get()->add("ON_SLOPE_R: " + std::to_string(under_feet_[1].x) + "," + std::to_string(under_feet_[1].y)); } #endif 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); } #ifdef _DEBUG if (on_auto_surface) { Debug::get()->add("ON_AUTO_SURFACE"); } #endif 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 under_feet_[0].y += 1; under_feet_[1].y += 1; // Comprueba las rampas on_slope |= room_->checkLeftSlopes(under_feet_.data()); on_slope |= room_->checkRightSlopes(&under_feet_[1]); #ifdef _DEBUG if (on_slope) { Debug::get()->add("ON_DOWN_SLOPE"); } #endif return on_slope; } // Comprueba que el jugador no toque ningun tile de los que matan auto Player::checkKillingTiles() -> bool { // Actualiza los puntos de colisión updateColliderPoints(); // 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) == TileType::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); } } // 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(PlayerState value) { previous_state_ = state_; state_ = value; checkState(); } // 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 PlayerSpawn& 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& surface_path, const std::string& animations_path) { auto surface = Resource::get()->getSurface(surface_path); auto animations = Resource::get()->getAnimations(animations_path); sprite_ = std::make_shared(surface, animations); sprite_->setWidth(WIDTH); sprite_->setHeight(HEIGHT); sprite_->setCurrentAnimation("walk"); } #ifdef _DEBUG // Pinta la información de debug del jugador void Player::renderDebugInfo() { if (Debug::get()->getEnabled()) { auto surface = Screen::get()->getRendererSurface(); // Pinta los underfeet surface->putPixel(under_feet_[0].x, under_feet_[0].y, static_cast(PaletteColor::BRIGHT_MAGENTA)); surface->putPixel(under_feet_[1].x, under_feet_[1].y, static_cast(PaletteColor::BRIGHT_MAGENTA)); // Pinta rectangulo del jugador SDL_FRect rect = getRect(); surface->drawRectBorder(&rect, static_cast(PaletteColor::BRIGHT_CYAN)); // Pinta el rectangulo de movimiento if (vx_ != 0.0F) { surface->fillRect(&debug_rect_x_, static_cast(PaletteColor::BRIGHT_RED)); } if (vy_ != 0.0F) { surface->fillRect(&debug_rect_y_, static_cast(PaletteColor::BRIGHT_RED)); } // Pinta el punto de debug surface->putPixel(debug_point_.x, debug_point_.y, rand() % 16); } } #endif // _DEBUG