// IWYU pragma: no_include #include "player.h" #include // Para max, min #include // Para ceil, abs #include "debug.h" // Para Debug #include "defines.h" // Para BORDER_BOTTOM, BORDER_LEFT, BORDER_RIGHT #include "input.h" // Para Input, InputAction #include "jail_audio.h" // Para JA_PlaySound #include "options.h" // Para Cheat, Options, options #include "resource.h" // Para Resource #include "room.h" // Para Room, TileType #include "s_animated_sprite.h" // Para SAnimatedSprite // 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_ = {0, 0, 0, 0}; debug_rect_y_ = {0, 0, 0, 0}; debug_color_ = {0, 255, 0}; debug_point_ = {0, 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_ = BORDER_LEFT; is_on_border_ = true; } else if (x_ + WIDTH_ > PLAY_AREA_RIGHT) { border_ = BORDER_RIGHT; is_on_border_ = true; } else if (y_ < PLAY_AREA_TOP) { border_ = BORDER_TOP; is_on_border_ = true; } else if (y_ + HEIGHT_ > PLAY_AREA_BOTTOM) { border_ = BORDER_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() { if (border_ == BORDER_TOP) { y_ = PLAY_AREA_BOTTOM - HEIGHT_ - 0 - BLOCK; setState(PlayerState::STANDING); } else if (border_ == BORDER_BOTTOM) { y_ = PLAY_AREA_TOP + 0; setState(PlayerState::STANDING); } else if (border_ == BORDER_RIGHT) { x_ = PLAY_AREA_LEFT + 0; } if (border_ == BORDER_LEFT) { x_ = PLAY_AREA_RIGHT - WIDTH_ - 0; } is_on_border_ = false; } // Aplica gravedad al jugador void Player::applyGravity() { constexpr float GF = 0.035f; // La gravedad solo se aplica cuando el jugador esta saltando // Nunca mientras cae o esta de pie if (state_ == PlayerState::JUMPING) { vy_ += GF; if (vy_ > MAX_VY_) { vy_ = MAX_VY_; } } } // Recalcula la posición del jugador y su animación void Player::move() { last_position_ = {static_cast(x_), static_cast(y_)}; // Guarda la posicion actual antes de modificarla applyGravity(); // Aplica gravedad al jugador checkState(); // Comprueba el estado del jugador #ifdef DEBUG debug_color_ = {0, 255, 0}; #endif // Se mueve hacia la izquierda if (vx_ < 0.0f) { // Crea el rectangulo de proyección en el eje X para ver si colisiona SDL_Rect proj; proj.x = static_cast(x_ + vx_); proj.y = static_cast(y_); proj.h = HEIGHT_; proj.w = ceil(abs(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) { LineVertical leftSide = {static_cast(x_), static_cast(y_) + HEIGHT_ - 2, static_cast(y_) + HEIGHT_ - 1}; // Comprueba solo los dos pixels de abajo const int ly = room_->checkLeftSlopes(&leftSide); if (ly > -1) { y_ = ly - HEIGHT_; } } // Si está bajando la rampa, recoloca al jugador if (isOnDownSlope() && state_ != PlayerState::JUMPING) { y_ += 1; } } // Se mueve hacia la derecha else if (vx_ > 0.0f) { // Crea el rectangulo de proyección en el eje X para ver si colisiona SDL_Rect proj; proj.x = static_cast(x_) + WIDTH_; proj.y = static_cast(y_); proj.h = HEIGHT_; proj.w = 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) { LineVertical rightSide = {static_cast(x_) + WIDTH_ - 1, static_cast(y_) + HEIGHT_ - 2, static_cast(y_) + HEIGHT_ - 1}; // Comprueba solo los dos pixels de abajo const int ry = room_->checkRightSlopes(&rightSide); if (ry > -1) { y_ = ry - HEIGHT_; } } // Si está bajando la rampa, recoloca al jugador if (isOnDownSlope() && state_ != PlayerState::JUMPING) { y_ += 1; } } // Si ha salido del suelo, el jugador cae if (state_ == PlayerState::STANDING && !isOnFloor()) { setState(PlayerState::FALLING); // Deja de estar enganchado a la superficie automatica auto_movement_ = false; } // Si ha salido de una superficie automatica, detiene el movimiento automatico if (state_ == PlayerState::STANDING && isOnFloor() && !isOnAutoSurface()) { // Deja de estar enganchado a la superficie automatica auto_movement_ = false; } // Se mueve hacia arriba if (vy_ < 0.0f) { // Crea el rectangulo de proyección en el eje Y para ver si colisiona SDL_Rect proj; proj.x = static_cast(x_); proj.y = static_cast(y_ + vy_); proj.h = ceil(abs(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); } } // Se mueve hacia abajo else if (vy_ > 0.0f) { // Crea el rectangulo de proyección en el eje Y para ver si colisiona SDL_Rect proj; proj.x = static_cast(x_); proj.y = static_cast(y_) + HEIGHT_; proj.h = 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 int 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 LineVertical leftSide = {proj.x, proj.y, proj.y + proj.h - 1}; LineVertical rightSide = {proj.x + proj.w - 1, proj.y, proj.y + proj.h - 1}; const int p = std::max(room_->checkRightSlopes(&rightSide), room_->checkLeftSlopes(&leftSide)); if (p > -1) { // No está saltando y hay colisión con una rampa // Calcula la nueva posición y_ = p - HEIGHT_; setState(PlayerState::STANDING); #ifdef DEBUG debug_color_ = {255, 255, 0}; debug_point_ = {(int)x_ + (WIDTH_ / 2), p}; #endif } else { // No está saltando y no hay colisón con una rampa // Calcula la nueva posición y_ += vy_; #ifdef DEBUG debug_color_ = {255, 0, 0}; #endif } } else { // Esta saltando y no hay colisión con los muros // Calcula la nueva posición y_ += vy_; } } } 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 bool Player::isOnFloor() { bool onFloor = false; bool onSlopeL = false; bool onSlopeR = false; updateFeet(); // Comprueba las superficies for (auto f : under_feet_) { onFloor |= room_->checkTopSurfaces(&f); onFloor |= room_->checkAutoSurfaces(&f); } // Comprueba las rampas onSlopeL = room_->checkLeftSlopes(&under_feet_[0]); onSlopeR = room_->checkRightSlopes(&under_feet_[1]); #ifdef DEBUG if (onFloor) { Debug::get()->add("ON_FLOOR"); } if (onSlopeL) { Debug::get()->add("ON_SLOPE_L: " + std::to_string(under_feet_[0].x) + "," + std::to_string(under_feet_[0].y)); } if (onSlopeR) { Debug::get()->add("ON_SLOPE_R: " + std::to_string(under_feet_[1].x) + "," + std::to_string(under_feet_[1].y)); } #endif return onFloor || onSlopeL || onSlopeR; } // Comprueba si el jugador esta sobre una superficie automática bool Player::isOnAutoSurface() { bool onAutoSurface = false; updateFeet(); // Comprueba las superficies for (auto f : under_feet_) { onAutoSurface |= room_->checkAutoSurfaces(&f); } #ifdef DEBUG if (onAutoSurface) { Debug::get()->add("ON_AUTO_SURFACE"); } #endif return onAutoSurface; } // Comprueba si el jugador está sobre una rampa hacia abajo bool Player::isOnDownSlope() { bool onSlope = 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 onSlope |= room_->checkLeftSlopes(&under_feet_[0]); onSlope |= room_->checkRightSlopes(&under_feet_[1]); #ifdef DEBUG if (onSlope) { Debug::get()->add("ON_DOWN_SLOPE"); } #endif return onSlope; } // Comprueba que el jugador no toque ningun tile de los que matan bool Player::checkKillingTiles() { // Actualiza los puntos de colisión updateColliderPoints(); // Comprueba si hay contacto y retorna en cuanto se encuentra colisión for (const auto &c : collider_points_) { if (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 == Cheat::CheatState::ENABLED) { color_ = static_cast(PaletteColor::CYAN); } else if (options.cheats.infinite_lives == Cheat::CheatState::ENABLED) { color_ = static_cast(PaletteColor::YELLOW); } else { color_ = static_cast(PaletteColor::WHITE); } } // Actualiza los puntos de colisión void Player::updateColliderPoints() { const SDL_Rect rect = getRect(); collider_points_[0] = {rect.x, rect.y}; collider_points_[1] = {rect.x + 7, rect.y}; collider_points_[2] = {rect.x + 7, rect.y + 7}; collider_points_[3] = {rect.x, rect.y + 7}; collider_points_[4] = {rect.x, rect.y + 8}; collider_points_[5] = {rect.x + 7, rect.y + 8}; collider_points_[6] = {rect.x + 7, rect.y + 15}; collider_points_[7] = {rect.x, rect.y + 15}; } // Actualiza los puntos de los pies void Player::updateFeet() { const SDL_Point p = {static_cast(x_), static_cast(y_)}; under_feet_[0] = {p.x, p.y + HEIGHT_}; under_feet_[1] = {p.x + 7, p.y + HEIGHT_}; feet_[0] = {p.x, p.y + HEIGHT_ - 1}; feet_[1] = {p.x + 7, 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 soundFile = "jump" + std::to_string(i) + ".wav"; jumping_sound_.push_back(Resource::get()->getSound(soundFile)); if (i >= 11) { falling_sound_.push_back(Resource::get()->getSound(soundFile)); } } } // 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 renderer = Screen::get()->getRenderer(); // Pinta los underfeet SDL_SetRenderDrawColor(renderer, 255, 0, 255, 255); SDL_RenderDrawPoint(renderer, under_feet_[0].x, under_feet_[0].y); SDL_RenderDrawPoint(renderer, under_feet_[1].x, under_feet_[1].y); // Pinta rectangulo del jugador SDL_SetRenderDrawColor(renderer, debug_color_.r, debug_color_.g, debug_color_.b, 192); SDL_Rect rect = getRect(); SDL_RenderFillRect(renderer, &rect); SDL_SetRenderDrawColor(renderer, 0, 255, 255, 255); SDL_RenderDrawRect(renderer, &rect); // Pinta el rectangulo de movimiento SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255); if (vx_ != 0.0f) { SDL_RenderFillRect(renderer, &debug_rect_x_); } if (vy_ != 0.0f) { SDL_RenderFillRect(renderer, &debug_rect_y_); } // Pinta el punto de debug SDL_SetRenderDrawColor(renderer, rand() % 256, rand() % 256, rand() % 256, 255); SDL_RenderDrawPoint(renderer, debug_point_.x, debug_point_.y); } */ } #endif