// 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_) { // 1. Procesamiento de entrada: captura las intenciones del jugador checkInput(); // 2. Física: aplica gravedad y actualiza velocidades applyGravity(delta_time); updateVelocity(); // 3. Movimiento: ejecuta movimiento, actualiza estado durante movimiento, resuelve colisiones move(delta_time); // 4. Finalización: animación, comprobaciones y efectos animate(delta_time); checkBorders(); checkJumpEnd(); checkKillingTiles(); setColor(); } } // Comprueba las entradas y establece las banderas de intención void Player::checkInput() { // Resetea las banderas de intención want_to_jump_ = false; want_to_move_left_ = false; want_to_move_right_ = false; // Captura las intenciones de movimiento if (Input::get()->checkInput(InputAction::LEFT)) { want_to_move_left_ = true; } else if (Input::get()->checkInput(InputAction::RIGHT)) { want_to_move_right_ = true; } // Captura la intención de salto if (Input::get()->checkInput(InputAction::JUMP)) { want_to_jump_ = true; } } // 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; } } // Actualiza el estado del jugador basado en física e intenciones void Player::updateState(float delta_time) { // Guarda el estado anterior para detectar transiciones previous_state_ = state_; // Máquina de estados: determina transiciones basándose en PLAYER_RULES.md switch (state_) { case State::STANDING: { // Comprueba muerte por caída desde altura if (previous_state_ == State::FALLING) { const int FALLING_DISTANCE = static_cast(y_) - last_grounded_position_; if (FALLING_DISTANCE > MAX_FALLING_HEIGHT) { is_alive_ = false; } } // Actualiza la posición de tierra last_grounded_position_ = static_cast(y_); jumping_time_ = 0.0F; // Regla: Si no tiene suelo debajo y no está JUMPING -> FALLING if (shouldFall()) { setState(State::FALLING); return; } // Regla: Puede saltar si está sobre suelo o superficie automática if (want_to_jump_ && canJump()) { setState(State::JUMPING); return; } // Nota: auto_movement_ se gestiona en updateVelocity() basado en el input break; } case State::JUMPING: { // Actualiza el tiempo de salto jumping_time_ += delta_time; playJumpSound(); // Regla: Si durante el salto Y > jump_init_pos -> FALLING if (static_cast(y_) >= jump_init_pos_ && vy_ > 0.0F) { setState(State::FALLING); return; } break; } case State::FALLING: { // Reproduce sonido de caída playFallSound(); break; } default: break; } } // Comprueba si el jugador puede saltar auto Player::canJump() -> bool { // Solo puede saltar si está STANDING y sobre suelo o superficie automática return state_ == State::STANDING && (isOnFloor() || isOnAutoSurface()); } // Comprueba si el jugador debe caer auto Player::shouldFall() -> bool { // Cae si no tiene suelo, no está en superficie automática, y no está en rampa descendente return !isOnFloor() && !isOnAutoSurface() && !isOnDownSlope(); } // Actualiza velocidad basada en estado e intenciones void Player::updateVelocity() { switch (state_) { case State::STANDING: { // Regla: Si está STANDING -> vy_ = 0 vy_ = 0.0F; if (!auto_movement_) { // Movimiento normal: el jugador controla la dirección if (want_to_move_left_) { vx_ = -HORIZONTAL_VELOCITY; sprite_->setFlip(SDL_FLIP_HORIZONTAL); } else if (want_to_move_right_) { vx_ = HORIZONTAL_VELOCITY; sprite_->setFlip(SDL_FLIP_NONE); } else { // No se pulsa ninguna dirección vx_ = 0.0F; // Regla conveyor belt: cuando el jugador deja de pulsar, se acopla al movimiento if (isOnAutoSurface()) { auto_movement_ = true; } } } else { // Movimiento automático: conveyor belt controla la dirección // Regla conveyor belt: el jugador no puede cambiar de dirección vx_ = HORIZONTAL_VELOCITY * room_->getAutoSurfaceDirection(); sprite_->setFlip(vx_ > 0.0F ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL); } break; } case State::JUMPING: { // Durante el salto, mantiene la velocidad horizontal // La velocidad vertical la controla applyGravity() break; } case State::FALLING: { // Regla: Si está FALLING -> vx_ = 0 (no puede cambiar dirección en el aire) vx_ = 0.0F; // La velocidad vertical es MAX_VY (ya configurada por setState) 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 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; 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; setState(State::STANDING); auto_movement_ = false; // Desactiva conveyor belt al aterrizar } else { // Si no hay colisión con los muros, comprueba la colisión con las rampas // Regla: La unica forma de atravesar una Slope es en estado JUMPING y con vx_ != 0 if (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) { // Hay colisión con una rampa: se pega a ella y_ = POINT - HEIGHT; setState(State::STANDING); } else { // No hay colisión con rampa: continúa cayendo y_ += DISPLACEMENT; } } else { // Está saltando con movimiento horizontal: atraviesa las rampas y_ += DISPLACEMENT; } } } // Orquesta el movimiento del jugador void Player::move(float delta_time) { // Movimiento horizontal if (vx_ < 0.0F) { moveHorizontal(delta_time, -1); // Izquierda } else if (vx_ > 0.0F) { moveHorizontal(delta_time, 1); // Derecha } // Actualización de estado DURANTE el movimiento (después de horizontal, antes de vertical) // Esto asegura que el estado se actualice con la posición correcta updateState(delta_time); // Movimiento vertical if (vy_ < 0.0F) { moveVerticalUp(delta_time); } else if (vy_ > 0.0F) { moveVerticalDown(delta_time); } // 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::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) { // Solo actualiza el estado y configura las variables iniciales // NO llama a updateState() para evitar recursión circular previous_state_ = state_; state_ = value; switch (state_) { case State::STANDING: // Se establecerá vy_ = 0 en updateVelocity() break; case State::JUMPING: // Configura el salto vy_ = JUMP_VELOCITY; jump_init_pos_ = y_; jumping_time_ = 0.0F; break; case State::FALLING: // Configura la caída vy_ = MAX_VY; // vx_ = 0 se establecerá en updateVelocity() if (previous_state_ == State::STANDING) { last_grounded_position_ = static_cast(y_); } auto_movement_ = false; jumping_time_ = 0.0F; 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; 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"); } // 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_); }