// 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.hpp" // Para Resource #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_) { /* handleInput(); // Comprueba las entradas y modifica variables move(delta_time); // Recalcula la posición del jugador animate(delta_time); // Establece la animación del jugador handleBorders(); // Comprueba si está situado en alguno de los cuatro bordes de la habitación handleJumpEnd(); // Comprueba si ha finalizado el salto al alcanzar la altura de inicio handleKillingTiles(); // Comprueba que el jugador no toque ningun tile de los que matan setColor(); // Establece el color del jugador */ handleInput(); updateState(); move(delta_time); animate(delta_time); handleBorders(); setColor(); } } // Comprueba las entradas y modifica variables void Player::handleInput() { if (Input::get()->checkAction(InputAction::LEFT)) { wannaGo = Direction::LEFT; } else if (Input::get()->checkAction(InputAction::RIGHT)) { wannaGo = Direction::RIGHT; } else { wannaGo = Direction::STAY; } wannaJump = Input::get()->checkAction(InputAction::JUMP); } void Player::move(float delta_time) { handleHorizontalMovement(delta_time); handleVerticalMovement(delta_time); updateColliderGeometry(); } void Player::handleHorizontalMovement(float delta_time) { if (state_ == State::STANDING) { // 1. Primero, determinamos cuál debe ser la velocidad (vx_) if (auto_movement_) { // La cinta transportadora tiene el control vx_ = HORIZONTAL_VELOCITY * room_->getConveyorBeltDirection(); } else { // El jugador tiene el control switch (wannaGo) { case Direction::LEFT: vx_ = -HORIZONTAL_VELOCITY; break; case Direction::RIGHT: vx_ = HORIZONTAL_VELOCITY; break; case Direction::STAY: vx_ = 0.0F; break; default: break; } } } // 2. Ahora, aplicamos el movimiento y el flip basado en la velocidad resultante if (vx_ < 0.0F) { moveHorizontal(delta_time, -1); sprite_->setFlip(SDL_FLIP_HORIZONTAL); } else if (vx_ > 0.0F) { moveHorizontal(delta_time, 1); sprite_->setFlip(SDL_FLIP_NONE); } // Si vx_ es 0.0F, no se llama a moveHorizontal, lo cual es correcto. } void Player::handleVerticalMovement(float delta_time) { if (state_ == State::STANDING) { return; } if (state_ == State::JUMPING) { applyGravity(delta_time); } // Movimiento vertical if (vy_ < 0.0F) { moveVerticalUp(delta_time); } else if (vy_ > 0.0F) { moveVerticalDown(delta_time); } } void Player::moveAndCollide(float delta_time) { } void Player::handleConveyorBelts() { if (!auto_movement_ and isOnConveyorBelt() and wannaGo == Direction::STAY) { auto_movement_ = true; } if (auto_movement_ and !isOnConveyorBelt()) { auto_movement_ = false; } } void Player::handleShouldFall() { if (!isOnFloor() and state_ == State::STANDING) { transitionToState(State::FALLING); } } void Player::transitionToState(State state) { previous_state_ = state_; state_ = state; switch (state) { case State::STANDING: vy_ = 0; break; case State::JUMPING: if (previous_state_ == State::STANDING) { vy_ = -MAX_VY; last_grounded_position_ = y_; } break; case State::FALLING: last_grounded_position_ = y_; vy_ = MAX_VY; vx_ = 0.0F; break; } } void Player::updateState() { switch (state_) { case State::STANDING: handleConveyorBelts(); handleShouldFall(); if (wannaJump) { transitionToState(State::JUMPING); } break; case State::JUMPING: auto_movement_ = false; // playJumpSound(); handleJumpEnd(); break; case State::FALLING: auto_movement_ = false; // playFallSound(); break; } } // Comprueba si está situado en alguno de los cuatro bordes de la habitación void Player::handleBorders() { 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::handleState(float delta_time) { (void)delta_time; // No usado actualmente // Reproduce sonidos según el estado if (state_ == State::FALLING) { playFallSound(); } else if (state_ == State::STANDING) { // Si no tiene suelo debajo y no está en rampa descendente -> FALLING if (!isOnFloor() && !isOnConveyorBelt() && !isOnDownSlope()) { last_grounded_position_ = static_cast(y_); // Guarda Y actual al SALIR de STANDING transitionToState(State::FALLING); // setState() establece vx_=0, vy_=MAX_VY playFallSound(); } } else if (state_ == State::JUMPING) { playJumpSound(); } } // 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; transitionToState(State::STANDING); break; case Room::Border::BOTTOM: y_ = PLAY_AREA_TOP; transitionToState(State::STANDING); break; case Room::Border::RIGHT: x_ = PLAY_AREA_LEFT; break; case Room::Border::LEFT: x_ = PLAY_AREA_RIGHT - WIDTH; break; default: break; } // CRÍTICO: Resetear last_grounded_position_ para evitar muerte falsa por diferencia de Y entre pantallas last_grounded_position_ = static_cast(y_); 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 sobre rampas // direction: -1 para izquierda, 1 para derecha void Player::handleSlopeMovement(int direction) { // No procesa rampas durante el salto (permite atravesarlas cuando salta con movimiento horizontal) // Pero SÍ procesa en STANDING y FALLING (para pegarse a las rampas) if (state_ == State::JUMPING) { return; } // Regla: Si está bajando la rampa, se pega a la slope if (isOnDownSlope()) { y_ += 1; return; } // Regla: Si está STANDING y tropieza lateralmente con una Slope, se pega a la slope // Comprueba si hay rampa en contacto lateral (solo los dos pixels inferiores) const int SIDE_X = direction < 0 ? static_cast(x_) : static_cast(x_) + WIDTH - 1; const LineVertical SIDE = { .x = SIDE_X, .y1 = static_cast(y_) + HEIGHT - 2, .y2 = static_cast(y_) + HEIGHT - 1}; // Comprueba la rampa correspondiente según la dirección const int SLOPE_Y = direction < 0 ? room_->checkLeftSlopes(&SIDE) : room_->checkRightSlopes(&SIDE); if (SLOPE_Y > -1) { // Hay rampa: sube al jugador para pegarlo a la rampa y_ = SLOPE_Y - HEIGHT; } } // Maneja el movimiento horizontal // direction: -1 para izquierda, 1 para derecha void Player::moveHorizontal(float delta_time, int direction) { const float DISPLACEMENT = vx_ * delta_time; // Crea el rectangulo de proyección en el eje X para ver si colisiona SDL_FRect proj; if (direction < 0) { // Movimiento a la izquierda proj = { .x = x_ + DISPLACEMENT, .y = y_, .w = std::ceil(std::fabs(DISPLACEMENT)), .h = HEIGHT}; } else { // Movimiento a la derecha proj = { .x = x_ + WIDTH, .y = y_, .w = std::ceil(DISPLACEMENT), .h = HEIGHT}; } // Comprueba la colisión con las superficies const int POS = direction < 0 ? room_->checkRightSurfaces(&proj) : room_->checkLeftSurfaces(&proj); // Calcula la nueva posición if (POS == -1) { // No hay colisión: mueve al jugador x_ += DISPLACEMENT; } else { // Hay colisión: reposiciona al jugador en el punto de colisión x_ = direction < 0 ? POS + 1 : POS - WIDTH; } // Maneja el movimiento sobre rampas handleSlopeMovement(direction); } // 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 // Regla: Si está JUMPING y tropieza contra el techo -> FALLING y_ = POS + 1; transitionToState(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; // VERIFICAR MUERTE ANTES de cambiar de estado (PLAYER_MECHANICS.md línea 1268-1274) const int FALL_DISTANCE = static_cast(y_) - last_grounded_position_; if (previous_state_ == State::FALLING && FALL_DISTANCE > MAX_FALLING_HEIGHT) { is_alive_ = false; // Muere si cae más de 32 píxeles } transitionToState(State::STANDING); last_grounded_position_ = static_cast(y_); // Actualizar AL ENTRAR en 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 // CORRECCIÓN: FALLING siempre se pega a rampas, JUMPING se pega solo si vx_ == 0 if (state_ == State::FALLING || (state_ == State::JUMPING && vx_ == 0.0F)) { // No está saltando O salta recto: se pega a las rampas 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; // VERIFICAR MUERTE ANTES de cambiar de estado (PLAYER_MECHANICS.md línea 1268-1274) const int FALL_DISTANCE = static_cast(y_) - last_grounded_position_; if (previous_state_ == State::FALLING && FALL_DISTANCE > MAX_FALLING_HEIGHT) { is_alive_ = false; // Muere si cae más de 32 píxeles } transitionToState(State::STANDING); last_grounded_position_ = static_cast(y_); // Actualizar AL ENTRAR en STANDING } else { // No está saltando y no hay colisón con una rampa // Calcula la nueva posición y_ += DISPLACEMENT; } } else { // Esta saltando con movimiento horizontal y no hay colisión con los muros // Calcula la nueva posición (atraviesa rampas) y_ += DISPLACEMENT; } } } // Orquesta el movimiento del jugador /* void Player::move(float delta_time) { applyGravity(delta_time); // Aplica gravedad al jugador handleState(delta_time); // Comprueba el estado del jugador // Movimiento horizontal if (vx_ < 0.0F) { moveHorizontal(delta_time, -1); // Izquierda } else if (vx_ > 0.0F) { moveHorizontal(delta_time, 1); // Derecha } // Si ha salido del suelo, el jugador cae if (state_ == State::STANDING && !isOnFloor()) { transitionToState(State::FALLING); auto_movement_ = false; } // Si ha salido de una superficie automatica, detiene el movimiento automatico if (state_ == State::STANDING && isOnFloor() && !isOnAutoSurface()) { auto_movement_ = false; } // Movimiento vertical if (vy_ < 0.0F) { moveVerticalUp(delta_time); } else if (vy_ > 0.0F) { moveVerticalDown(delta_time); } y_prev_ = y_; // Guarda Y DESPUÉS de todo el movimiento (para detectar hitos en sonidos) // Actualiza la geometría del collider y sprite updateColliderGeometry(); } */ // 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::handleJumpEnd() { // Si el jugador vuelve EXACTAMENTE a la altura inicial, debe CONTINUAR en JUMPING // Solo cuando la SUPERA (desciende más allá) cambia a FALLING if (state_ == State::JUMPING && vy_ > 0.0F && static_cast(y_) > last_grounded_position_) { transitionToState(State::FALLING); } } // Calcula y reproduce el sonido de salto basado en distancia vertical recorrida void Player::playJumpSound() { // Sistema basado en distancia vertical, no en tiempo (PLAYER_MECHANICS.md línea 107-120) const float DISTANCE_FROM_START = std::abs(y_ - static_cast(last_grounded_position_)); const int SOUND_INDEX = static_cast(DISTANCE_FROM_START / SOUND_DISTANCE_INTERVAL); // Calcular índice previo (frame anterior) const float PREV_DISTANCE = std::abs(y_prev_ - static_cast(last_grounded_position_)); const int PREVIOUS_INDEX = static_cast(PREV_DISTANCE / SOUND_DISTANCE_INTERVAL); // Solo reproduce cuando cambia de índice (nuevo hito alcanzado) if (SOUND_INDEX != PREVIOUS_INDEX && SOUND_INDEX < static_cast(jumping_sound_.size())) { Audio::get()->playSound(jumping_sound_[SOUND_INDEX], Audio::Group::GAME); } } // Calcula y reproduce el sonido de caída basado en distancia vertical recorrida void Player::playFallSound() { // Sistema basado en distancia vertical, no en tiempo (PLAYER_MECHANICS.md línea 193-206) const float DISTANCE_FALLEN = y_ - static_cast(last_grounded_position_); const int SOUND_INDEX = static_cast(DISTANCE_FALLEN / SOUND_DISTANCE_INTERVAL); // Calcular índice previo (frame anterior) const float PREV_DISTANCE = y_prev_ - static_cast(last_grounded_position_); const int PREVIOUS_INDEX = static_cast(PREV_DISTANCE / SOUND_DISTANCE_INTERVAL); // Solo reproduce cuando cambia de índice (nuevo hito alcanzado) if (SOUND_INDEX != PREVIOUS_INDEX && SOUND_INDEX < static_cast(falling_sound_.size())) { Audio::get()->playSound(falling_sound_[SOUND_INDEX], Audio::Group::GAME); } } // 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_->checkConveyorBelts(&f); } // Comprueba las rampas on_slope_l = room_->checkLeftSlopes(under_feet_.data()); 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::isOnConveyorBelt() -> bool { bool on_conveyor_belt = false; updateFeet(); // Comprueba las superficies for (auto f : under_feet_) { on_conveyor_belt |= room_->checkConveyorBelts(&f); } return on_conveyor_belt; } // 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::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; })) { 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::transitionToState(State value) { previous_state_ = state_; state_ = value; // Establecer velocidades INMEDIATAMENTE al cambiar de estado switch (state_) { case State::STANDING: vx_ = 0.0F; vy_ = 0.0F; break; case State::JUMPING: // vx_ mantiene su valor actual (heredado de STANDING) vy_ = JUMP_VELOCITY; break; case State::FALLING: vx_ = 0.0F; // CRÍTICO para pegarse a rampas vy_ = MAX_VY; auto_movement_ = false; break; default: break; } } */ // 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; 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) { auto animations = Resource::get()->getAnimations(animations_path); sprite_ = std::make_unique(animations); sprite_->setWidth(WIDTH); sprite_->setHeight(HEIGHT); sprite_->setCurrentAnimation("walk"); } // Actualiza collider_box y collision points void Player::updateColliderGeometry() { 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 } // Coloca el sprite en la posición del jugador void Player::placeSprite() { sprite_->setPos(x_, y_); }