// IWYU pragma: no_include #include "player.h" #include // Para rand #include // Para max, min #include // Para ceil, abs #include "animated_sprite.h" // Para AnimatedSprite #include "asset.h" // Para Asset #include "const.h" // Para BORDER_TOP, BLOCK, BORDER_BOTTOM, BORDER... #include "debug.h" // Para Debug #include "input.h" // Para Input, inputs_e #include "jail_audio.h" // Para JA_LoadSound, JA_Sound_t, JA_PlaySound #include "resource.h" // Para Resource #include "room.h" // Para Room, tile_e #include "texture.h" // Para Texture #include "options.h" #include "screen.h" // Constructor Player::Player(player_t player) : renderer_(Screen::get()->getRenderer()), input_(Input::get()), resource_(Resource::get()), asset_(Asset::get()), room_(player.room), debug_(Debug::get()) { // Crea objetos sprite_ = std::make_shared(resource_->getAnimation(player.animation)); // Inicializa variables reLoadPalette(); is_on_border_ = false; border_ = BORDER_TOP; auto_movement_ = false; is_alive_ = true; is_paused_ = false; jump_init_pos_ = player.spawn.jump_init_pos; state_ = player.spawn.state; previous_state_ = state_; x_ = player.spawn.x; y_ = player.spawn.y; vx_ = player.spawn.vx; vy_ = player.spawn.vy; w_ = 8; h_ = 16; sprite_->setPosX(player.spawn.x); sprite_->setPosY(player.spawn.y); sprite_->setWidth(8); sprite_->setHeight(16); sprite_->setFlip(player.spawn.flip); sprite_->setCurrentAnimation("walk"); sprite_->update(); last_position_ = getRect(); collider_box_ = getRect(); const SDL_Point p = {0, 0}; collider_points_.insert(collider_points_.end(), {p, p, p, p, p, p, p, p}); under_feet_.insert(under_feet_.end(), {p, p}); feet_.insert(feet_.end(), {p, p}); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump1.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump2.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump3.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump4.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump5.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump6.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump7.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump8.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump9.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump10.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump11.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump12.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump13.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump14.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump15.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump16.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump17.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump18.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump19.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump20.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump21.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump22.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump23.wav").c_str())); jumping_sound_.push_back(JA_LoadSound(asset_->get("jump24.wav").c_str())); falling_sound_.push_back(JA_LoadSound(asset_->get("jump11.wav").c_str())); falling_sound_.push_back(JA_LoadSound(asset_->get("jump12.wav").c_str())); falling_sound_.push_back(JA_LoadSound(asset_->get("jump13.wav").c_str())); falling_sound_.push_back(JA_LoadSound(asset_->get("jump14.wav").c_str())); falling_sound_.push_back(JA_LoadSound(asset_->get("jump15.wav").c_str())); falling_sound_.push_back(JA_LoadSound(asset_->get("jump16.wav").c_str())); falling_sound_.push_back(JA_LoadSound(asset_->get("jump17.wav").c_str())); falling_sound_.push_back(JA_LoadSound(asset_->get("jump18.wav").c_str())); falling_sound_.push_back(JA_LoadSound(asset_->get("jump19.wav").c_str())); falling_sound_.push_back(JA_LoadSound(asset_->get("jump20.wav").c_str())); falling_sound_.push_back(JA_LoadSound(asset_->get("jump21.wav").c_str())); falling_sound_.push_back(JA_LoadSound(asset_->get("jump22.wav").c_str())); falling_sound_.push_back(JA_LoadSound(asset_->get("jump23.wav").c_str())); falling_sound_.push_back(JA_LoadSound(asset_->get("jump24.wav").c_str())); jumping_counter_ = 0; falling_counter_ = 0; #ifdef DEBUG rx = {0, 0, 0, 0}; ry = {0, 0, 0, 0}; debugColor = {0, 255, 0}; debugPoint = {0, 0}; #endif } // Pinta el jugador en pantalla void Player::render() { sprite_->getTexture()->setColor(color_.r, color_.g, color_.b); sprite_->render(); #ifdef DEBUG if (debug_->getEnabled()) { // Pinta los underfeet SDL_SetRenderDrawColor(renderer_, 255, 0, 255, 255); SDL_RenderDrawPoint(renderer_, underFeet[0].x, underFeet[0].y); SDL_RenderDrawPoint(renderer_, underFeet[1].x, underFeet[1].y); // Pinta rectangulo del jugador SDL_SetRenderDrawColor(renderer_, debugColor.r, debugColor.g, debugColor.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_, &rx); } if (vy != 0.0f) { SDL_RenderFillRect(renderer_, &ry); } // Pinta el punto de debug SDL_SetRenderDrawColor(renderer_, rand() % 256, rand() % 256, rand() % 256, 255); SDL_RenderDrawPoint(renderer_, debugPoint.x, debugPoint.y); } #endif } // Actualiza las variables del objeto void Player::update() { if (is_paused_) { // Si está en modo pausa no se actualiza nada return; } 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_ != s_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_->checkInput(input_left)) { vx_ = -0.6f; sprite_->setFlip(SDL_FLIP_HORIZONTAL); } else if (input_->checkInput(input_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_->checkInput(input_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(s_jumping); vy_ = -MAX_VY_; jump_init_pos_ = y_; jumping_counter_ = 0; } } } // Indica si el jugador esta en uno de los cuatro bordes de la pantalla bool Player::getOnBorder() { return is_on_border_; } // Indica en cual de los cuatro bordes se encuentra int Player::getBorder() { return border_; } // 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_ + w_ > 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_ + h_ > 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_ == s_falling) { vx_ = 0.0f; vy_ = MAX_VY_; falling_counter_++; playFallSound(); } else if (state_ == s_standing) { if (previous_state_ == s_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(s_falling); vx_ = 0.0f; vy_ = MAX_VY_; falling_counter_++; playFallSound(); } } else if (state_ == s_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 - h_ - 0 - BLOCK; setState(s_standing); } else if (border_ == BORDER_BOTTOM) { y_ = PLAY_AREA_TOP + 0; setState(s_standing); } else if (border_ == BORDER_RIGHT) { x_ = PLAY_AREA_LEFT + 0; } if (border_ == BORDER_LEFT) { x_ = PLAY_AREA_RIGHT - w_ - 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_ == s_jumping) { vy_ += GF; if (vy_ > MAX_VY_) { vy_ = MAX_VY_; } } } // Obtiene el rectangulo que delimita al jugador SDL_Rect Player::getRect() { return {static_cast(x_), static_cast(y_), w_, h_}; } // Obtiene el rectangulo de colision del jugador SDL_Rect &Player::getCollider() { collider_box_ = getRect(); return collider_box_; } // 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 debugColor = {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 = h_; proj.w = ceil(abs(vx_)); // Para evitar que tenga un ancho de 0 pixels #ifdef DEBUG rx = 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_ != s_jumping) { v_line_t leftSide = {static_cast(x_), static_cast(y_) + h_ - 2, static_cast(y_) + h_ - 1}; // Comprueba solo los dos pixels de abajo const int ly = room_->checkLeftSlopes(&leftSide); if (ly > -1) { y_ = ly - h_; } } // Si está bajando la rampa, recoloca al jugador if (isOnDownSlope() && state_ != s_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_) + w_; proj.y = static_cast(y_); proj.h = h_; proj.w = ceil(vx_); // Para evitar que tenga un ancho de 0 pixels #ifdef DEBUG rx = 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 - w_; } // Si ha tocado alguna rampa mientras camina (sin saltar), asciende if (state_ != s_jumping) { v_line_t rightSide = {static_cast(x_) + w_ - 1, static_cast(y_) + h_ - 2, static_cast(y_) + h_ - 1}; // Comprueba solo los dos pixels de abajo const int ry = room_->checkRightSlopes(&rightSide); if (ry > -1) { y_ = ry - h_; } } // Si está bajando la rampa, recoloca al jugador if (isOnDownSlope() && state_ != s_jumping) { y_ += 1; } } // Si ha salido del suelo, el jugador cae if (state_ == s_standing && !isOnFloor()) { setState(s_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_ == s_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 = w_; #ifdef DEBUG ry = 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(s_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_) + h_; proj.h = ceil(vy_); // Para evitar que tenga una altura de 0 pixels proj.w = w_; #ifdef DEBUG ry = 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 - h_; setState(s_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_ != s_jumping) { // Las rampas no se miran si se está saltando v_line_t leftSide = {proj.x, proj.y, proj.y + proj.h - 1}; v_line_t 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 - h_; setState(s_standing); #ifdef DEBUG debugColor = {255, 255, 0}; debugPoint = {(int)x + (w / 2), p}; #endif } else { // No está saltando y no hay colisón con una rampa // Calcula la nueva posición y_ += vy_; #ifdef DEBUG debugColor = {255, 0, 0}; #endif } } else { // Esta saltando y no hay colisión con los muros // Calcula la nueva posición y_ += vy_; } } } // Actualiza la posición del sprite sprite_->setPosX(x_); sprite_->setPosY(y_); #ifdef DEBUG debug_->add("RECT_X: " + std::to_string(rx.x) + "," + std::to_string(rx.y) + "," + std::to_string(rx.w) + "," + std::to_string(rx.h)); debug_->add("RECT_Y: " + std::to_string(ry.x) + "," + std::to_string(ry.y) + "," + std::to_string(ry.w) + "," + std::to_string(ry.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_ == s_jumping) { if (vy_ > 0) { if (y_ >= jump_init_pos_) { // Si alcanza la altura de salto inicial, pasa al estado de caída setState(s_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_->add("JUMP: " + std::to_string(jumpCounter / 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_->add("FALL: " + std::to_string(fallCounter / 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_->add("ON_FLOOR"); } if (onSlopeL) { debug_->add("ON_SLOPE_L: " + std::to_string(underFeet[0].x) + "," + std::to_string(underFeet[0].y)); } if (onSlopeR) { debug_->add("ON_SLOPE_R: " + std::to_string(underFeet[1].x) + "," + std::to_string(underFeet[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_->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_->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 bool check = false; for (auto c : collider_points_) { check |= (room_->getTile(c) == t_kill); } // Mata al jugador si hay colisión if (check) { is_alive_ = false; } return check; } // Obtiene algunos parametros del jugador playerSpawn_t Player::getSpawnParams() { playerSpawn_t params; params.x = x_; params.y = y_; params.vx = vx_; params.vy = vy_; params.jump_init_pos = jump_init_pos_; params.state = state_; params.flip = sprite_->getFlip(); return params; } // Recarga la textura void Player::reLoadTexture() { sprite_->getTexture()->reLoad(); } // Recarga la paleta void Player::reLoadPalette() { color_ = stringToColor(options.video.palette, "white"); if (options.cheats.infinite_lives == Cheat::CheatState::ENABLED) { color_ = stringToColor(options.video.palette, "yellow"); } if (options.cheats.invincible == Cheat::CheatState::ENABLED) { color_ = stringToColor(options.video.palette, "cyan"); } } // Establece el valor de la variable void Player::setRoom(std::shared_ptr room) { room_ = room; } // 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 + h_}; under_feet_[1] = {p.x + 7, p.y + h_}; feet_[0] = {p.x, p.y + h_ - 1}; feet_[1] = {p.x + 7, p.y + h_ - 1}; } // Cambia el estado del jugador void Player::setState(state_e value) { previous_state_ = state_; state_ = value; checkState(); } // Comprueba si el jugador esta vivo bool Player::isAlive() { return is_alive_; } // Pone el jugador en modo pausa void Player::pause() { is_paused_ = true; } // Quita el modo pausa del jugador void Player::resume() { is_paused_ = false; }