Files
projecte_2026/source/game/entities/player.cpp

832 lines
28 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// IWYU pragma: no_include <bits/std_abs.h>
#include "game/entities/player.hpp"
#include <algorithm> // Para max, min
#include <cmath> // Para ceil, abs
#include <iostream>
#include <ranges> // Para std::ranges::any_of
#include "core/audio/audio.hpp" // Para Audio
#include "core/input/input.hpp" // Para Input, InputAction
#include "core/rendering/sprite/animated_sprite.hpp" // Para SAnimatedSprite
#include "core/resources/resource_cache.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
#ifdef _DEBUG
#include "core/system/debug.hpp" // Para Debug
#endif
// Constructor
Player::Player(const Data& player)
: room_(player.room) {
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_);
#ifdef _DEBUG
if (Debug::get()->isEnabled()) {
Screen::get()->getRendererSurface()->putPixel(under_right_foot_.x, under_right_foot_.y, 8);
Screen::get()->getRendererSurface()->putPixel(under_left_foot_.x, under_left_foot_.y, 8);
}
#endif
}
// Actualiza las variables del objeto
void Player::update(float delta_time) {
if (!is_paused_) {
handleInput();
updateState(delta_time);
move(delta_time);
handleKillingTiles(); // Los collider_points_ están actualizados por syncSpriteAndCollider() dentro de move()
animate(delta_time);
border_ = handleBorders();
}
}
// Comprueba las entradas y modifica variables
void Player::handleInput() {
if (ignore_input_) { return; }
if (Input::get()->checkAction(InputAction::LEFT)) {
wanna_go_ = Direction::LEFT;
} else if (Input::get()->checkAction(InputAction::RIGHT)) {
wanna_go_ = Direction::RIGHT;
} else {
wanna_go_ = Direction::NONE;
}
const bool JUMP_PRESSED = Input::get()->checkAction(InputAction::JUMP);
wanna_jump_ = JUMP_PRESSED && !jump_held_; // Solo en el flanco de pulsación
jump_held_ = JUMP_PRESSED;
wanna_down_ = Input::get()->checkAction(InputAction::DOWN);
}
// La lógica de movimiento está distribuida en move
void Player::move(float delta_time) {
switch (state_) {
case State::ON_GROUND:
moveOnGround(delta_time);
break;
case State::ON_SLOPE:
moveOnSlope(delta_time);
break;
case State::ON_AIR:
moveOnAir(delta_time);
break;
}
syncSpriteAndCollider(); // Actualiza la posición del sprite y las colisiones
#ifdef _DEBUG
Debug::get()->set("P.X", std::to_string(static_cast<int>(x_)));
Debug::get()->set("P.Y", std::to_string(static_cast<int>(y_)));
Debug::get()->set("P.LGP", std::to_string(last_grounded_position_));
switch (state_) {
case State::ON_GROUND:
Debug::get()->set("P.STATE", "ON_GROUND");
break;
case State::ON_SLOPE:
Debug::get()->set("P.STATE", "ON_SLOPE");
break;
case State::ON_AIR:
Debug::get()->set("P.STATE", "ON_AIR");
break;
}
#endif
}
void Player::handleConveyorBelts() {
if (!auto_movement_ and isOnConveyorBelt() and wanna_go_ == Direction::NONE) {
auto_movement_ = true;
}
if (auto_movement_ and !isOnConveyorBelt()) {
auto_movement_ = false;
}
}
void Player::handleShouldFall() {
if (!isOnFloor() and (state_ == State::ON_GROUND || state_ == State::ON_SLOPE)) {
vy_ = 0.0F;
transitionToState(State::ON_AIR);
}
}
void Player::transitionToState(State state) {
previous_state_ = state_;
state_ = state;
switch (state) {
case State::ON_GROUND:
vy_ = 0;
if (previous_state_ == State::ON_AIR) {
Audio::get()->playSound(land_sound_, Audio::Group::GAME);
}
current_slope_ = nullptr;
break;
case State::ON_SLOPE:
vy_ = 0;
if (previous_state_ == State::ON_AIR) {
Audio::get()->playSound(land_sound_, Audio::Group::GAME);
}
updateCurrentSlope();
if (current_slope_ == nullptr) {
// Los pies no coinciden con ninguna rampa: tratar como suelo plano
state_ = State::ON_GROUND;
}
break;
case State::ON_AIR:
last_grounded_position_ = static_cast<int>(y_);
current_slope_ = nullptr;
break;
}
}
void Player::updateState(float delta_time) {
switch (state_) {
case State::ON_GROUND:
updateOnGround(delta_time);
break;
case State::ON_SLOPE:
updateOnSlope(delta_time);
break;
case State::ON_AIR:
updateOnAir(delta_time);
break;
}
}
// Inicia un salto desde ON_GROUND/ON_SLOPE: velocidad inicial hacia arriba + sonido + transición
void Player::startJump() {
vy_ = JUMP_VELOCITY;
last_grounded_position_ = y_;
Audio::get()->playSound(jump_sound_, Audio::Group::GAME);
transitionToState(State::ON_AIR);
}
// Actualización lógica del estado ON_GROUND
void Player::updateOnGround(float delta_time) {
(void)delta_time; // No usado en este método, pero se mantiene por consistencia
handleConveyorBelts(); // Gestiona las cintas transportadoras
// El salto tiene prioridad sobre la caída por falta de suelo
if (wanna_jump_) {
startJump();
return;
}
handleShouldFall(); // Verifica si debe caer (no tiene suelo)
}
// Actualización lógica del estado ON_SLOPE
void Player::updateOnSlope(float delta_time) {
(void)delta_time; // No usado en este método, pero se mantiene por consistencia
if (wanna_jump_) {
startJump();
return;
}
// DOWN: dejarse caer atravesando la rampa
if (wanna_down_) {
y_ += 1.0F;
vy_ = 0.0F;
transitionToState(State::ON_AIR);
return;
}
handleShouldFall();
}
// Actualización lógica del estado ON_AIR
void Player::updateOnAir(float delta_time) {
(void)delta_time;
auto_movement_ = false; // Desactiva el movimiento automático en el aire
}
// Movimiento físico del estado ON_GROUND
void Player::moveOnGround(float delta_time) {
// Determinama cuál debe ser la velocidad a partir de automovement o de wanna_go_
updateVelocity(delta_time);
if (vx_ == 0.0F) { return; }
// Movimiento horizontal y colision con muros
applyHorizontalMovement(delta_time);
// Comprueba colision con rampas, corrige y cambia estado
const int SIDE_X = vx_ < 0.0F ? static_cast<int>(x_) : static_cast<int>(x_) + WIDTH - 1;
const LineVertical SIDE = {
.x = SIDE_X,
.y1 = static_cast<int>(y_) + HEIGHT - 2,
.y2 = static_cast<int>(y_) + HEIGHT - 1};
// Comprueba la rampa correspondiente según la dirección
const int SLOPE_Y = vx_ < 0.0F ? room_->checkLeftSlopes(SIDE) : room_->checkRightSlopes(SIDE);
if (SLOPE_Y != Collision::NONE) {
// Hay rampa: sube al jugador para pegarlo a la rampa
y_ = SLOPE_Y - HEIGHT;
transitionToState(State::ON_SLOPE);
}
#ifdef _DEBUG
Debug::get()->set("sl.detect_y", SLOPE_Y != Collision::NONE ? std::to_string(SLOPE_Y) : "-");
#endif
// Comprueba si está sobre una rampa
if (isOnSlope()) { transitionToState(State::ON_SLOPE); }
}
// Movimiento físico del estado ON_SLOPE
void Player::moveOnSlope(float delta_time) {
// Determinama cuál debe ser la velocidad a partir de automovement o de wanna_go_
updateVelocity(delta_time);
// Verificar rampa válida antes de comprobar velocidad: si no hay rampa siempre caer,
// independientemente de si hay o no input (evita bloqueo con vx_=0 y slope null)
if (current_slope_ == nullptr) {
vy_ = 0.0F;
transitionToState(State::ON_AIR);
return;
}
if (vx_ == 0.0F) { return; }
// Determinar el tipo de rampa
const bool IS_LEFT_SLOPE = isLeftSlope();
// Movimiento horizontal con colisión lateral
applyHorizontalMovement(delta_time);
// Seleccionar el pie apropiado según el tipo de rampa
// Left slopes (forma \) colisionan con el pie izquierdo
// Right slopes (forma /) colisionan con el pie derecho
const int X = IS_LEFT_SLOPE ? x_ : x_ + WIDTH - 1;
// Calcular la Y basada en la ecuación de la rampa (45 grados)
// Left slope (\): y aumenta con x -> y = y1 + (x - x1)
// Right slope (/): y disminuye con x -> y = y1 - (x - x1)
if (IS_LEFT_SLOPE) {
y_ = current_slope_->y1 + (X - current_slope_->x1) - HEIGHT;
} else {
y_ = current_slope_->y1 - (X - current_slope_->x1) - HEIGHT;
}
// Verificar si el pie ha salido de los límites horizontales de la rampa
// Usar min/max porque LEFT slopes tienen x1<x2 pero RIGHT slopes tienen x1>x2
const int MIN_X = std::min(current_slope_->x1, current_slope_->x2);
const int MAX_X = std::max(current_slope_->x1, current_slope_->x2);
const bool OUT_OF_BOUNDS = (X < MIN_X) || (X > MAX_X);
#ifdef _DEBUG
Debug::get()->set("sl.foot", std::to_string(X));
Debug::get()->set("sl.y_c", std::to_string(static_cast<int>(y_)));
Debug::get()->set("sl.oob", OUT_OF_BOUNDS ? "YES" : "ok");
#endif
if (OUT_OF_BOUNDS) {
// Determinar si estamos saliendo por arriba o por abajo de la rampa
const bool EXITING_DOWNWARD = (X > current_slope_->x2 && IS_LEFT_SLOPE) ||
(X < current_slope_->x1 && !IS_LEFT_SLOPE);
const bool EXITING_UPWARD = (X < current_slope_->x1 && IS_LEFT_SLOPE) ||
(X > current_slope_->x2 && !IS_LEFT_SLOPE);
#ifdef _DEBUG
Debug::get()->set("sl.oob", EXITING_DOWNWARD ? "DOWN" : "UP");
#endif
if (EXITING_DOWNWARD) {
// Salida por abajo: no hacer nada
// y_ += 1.0F;
}
if (EXITING_UPWARD) {
// Salida por arriba: bajar un pixel ya que ha subido 1 de mas al salirse de la recta
y_ += 1.0F;
}
// Verificar si hay soporte debajo (suelo plano o conveyor belt)
if (isOnTopSurface() || isOnConveyorBelt()) {
// Hay soporte: transición a ON_GROUND (podría ser superficie o conveyor belt)
transitionToState(State::ON_GROUND);
} else {
// Sin soporte: empezar a caer
vy_ = 0.0F;
transitionToState(State::ON_AIR);
}
return;
}
// Verificar transición a superficie plana
/*if (isOnTopSurface()) {
transitionToState(State::ON_GROUND);
return;
}*/
}
// Movimiento físico del estado ON_AIR
// El jugador puede moverse horizontalmente en el aire y la gravedad siempre actúa.
void Player::moveOnAir(float delta_time) {
// Movimiento horizontal libre según wanna_go_ (permite girar en el aire)
updateVelocity(delta_time);
applyHorizontalMovement(delta_time);
// Gravedad
applyGravity(delta_time);
const float DISPLACEMENT_Y = vy_ * delta_time;
// Subiendo: comprobar techo
if (vy_ < 0.0F) {
const SDL_FRect PROJECTION = getProjection(Direction::UP, DISPLACEMENT_Y);
const int POS = room_->checkBottomSurfaces(PROJECTION);
if (POS == Collision::NONE) {
y_ += DISPLACEMENT_Y;
} else {
// Choque con techo: se pega por debajo y empieza a caer
y_ = POS + 1;
vy_ = 0.0F;
}
return;
}
// Bajando: comprobar aterrizaje en superficies y rampas
if (vy_ > 0.0F) {
const SDL_FRect PROJECTION = getProjection(Direction::DOWN, DISPLACEMENT_Y);
handleLandingFromAir(DISPLACEMENT_Y, PROJECTION);
}
}
// Comprueba si el punto central del jugador ha sobrepasado alguno de los bordes
auto Player::handleBorders() -> Room::Border {
const float CENTER_X = x_ + WIDTH / 2.0F;
const float CENTER_Y = y_ + HEIGHT / 2.0F;
if (CENTER_X < PlayArea::LEFT) {
return Room::Border::LEFT;
}
if (CENTER_X > PlayArea::RIGHT) {
return Room::Border::RIGHT;
}
if (CENTER_Y < PlayArea::TOP) {
return Room::Border::TOP;
}
if (CENTER_Y > PlayArea::BOTTOM) {
return Room::Border::BOTTOM;
}
return Room::Border::NONE;
}
// Cambia al jugador de un borde al opuesto conservando velocidad y estado.
// Usa el punto central para calcular la posición en la pantalla destino.
void Player::switchBorders() {
switch (border_) {
case Room::Border::TOP:
y_ += PlayArea::HEIGHT;
last_grounded_position_ = static_cast<int>(y_);
break;
case Room::Border::BOTTOM:
y_ -= PlayArea::HEIGHT;
last_grounded_position_ = static_cast<int>(y_);
break;
case Room::Border::RIGHT:
x_ -= PlayArea::WIDTH;
break;
case Room::Border::LEFT:
x_ += PlayArea::WIDTH;
break;
default:
break;
}
border_ = Room::Border::NONE;
syncSpriteAndCollider();
}
// Aplica gravedad al jugador
void Player::applyGravity(float delta_time) {
if (state_ == State::ON_AIR) {
// Si está subiendo y ha soltado el botón de salto, gravedad aumentada para cortar el salto
const float GRAVITY = (vy_ < 0.0F && !jump_held_)
? GRAVITY_FORCE * LOW_JUMP_GRAVITY_MULT
: GRAVITY_FORCE;
vy_ += GRAVITY * delta_time;
vy_ = std::min(vy_, MAX_VY);
}
}
// Establece la animación del jugador
void Player::animate(float delta_time) { // NOLINT(readability-make-member-function-const)
if (state_ == State::ON_AIR) {
sprite_->setCurrentAnimation("jump");
} else if (vx_ != 0) {
sprite_->setCurrentAnimation("default");
sprite_->update(delta_time);
} else {
sprite_->setCurrentAnimation("stand");
}
}
// Comprueba si el jugador tiene suelo debajo de los pies
auto Player::isOnFloor() -> bool {
bool on_top_surface = false;
bool on_conveyor_belt = false;
updateFeet();
// Comprueba las superficies
on_top_surface |= room_->checkTopSurfaces(under_left_foot_);
on_top_surface |= room_->checkTopSurfaces(under_right_foot_);
// Comprueba las cintas transportadoras
on_conveyor_belt |= room_->checkConveyorBelts(under_left_foot_);
on_conveyor_belt |= room_->checkConveyorBelts(under_right_foot_);
// Comprueba las rampas
auto on_slope_l = room_->checkLeftSlopes(under_left_foot_);
auto on_slope_r = room_->checkRightSlopes(under_right_foot_);
return on_top_surface || on_conveyor_belt || on_slope_l || on_slope_r;
}
// Comprueba si el jugador está sobre una superficie
auto Player::isOnTopSurface() -> bool {
bool on_top_surface = false;
updateFeet();
// Comprueba las superficies
on_top_surface |= room_->checkTopSurfaces(under_left_foot_);
on_top_surface |= room_->checkTopSurfaces(under_right_foot_);
return on_top_surface;
}
// Comprueba si el jugador esta sobre una cinta transportadora
auto Player::isOnConveyorBelt() -> bool {
bool on_conveyor_belt = false;
updateFeet();
// Comprueba las superficies
on_conveyor_belt |= room_->checkConveyorBelts(under_left_foot_);
on_conveyor_belt |= room_->checkConveyorBelts(under_right_foot_);
return on_conveyor_belt;
}
// Comprueba si el jugador está sobre una rampa
// Retorna true SOLO si un pie está en rampa Y el otro pie está volando (sin soporte)
auto Player::isOnSlope() -> bool {
updateFeet();
// Verificar qué pie está en qué tipo de rampa
const bool LEFT_FOOT_ON_LEFT_SLOPE = room_->checkLeftSlopes(under_left_foot_);
const bool RIGHT_FOOT_ON_RIGHT_SLOPE = room_->checkRightSlopes(under_right_foot_);
// Verificar si cada pie está "volando" (sin soporte: ni top surface ni conveyor belt)
const bool LEFT_FOOT_FLYING = !(room_->checkTopSurfaces(under_left_foot_) ||
room_->checkConveyorBelts(under_left_foot_));
const bool RIGHT_FOOT_FLYING = !(room_->checkTopSurfaces(under_right_foot_) ||
room_->checkConveyorBelts(under_right_foot_));
// Retornar true si UN pie en rampa Y el OTRO volando
return (LEFT_FOOT_ON_LEFT_SLOPE && RIGHT_FOOT_FLYING) ||
(RIGHT_FOOT_ON_RIGHT_SLOPE && LEFT_FOOT_FLYING);
}
// Comprueba si current_slope_ es una rampa izquierda (ascendente a la izquierda)
// Las rampas izquierdas tienen forma \ con x1 < x2 (x aumenta de izq a der)
auto Player::isLeftSlope() -> bool {
if (current_slope_ == nullptr) {
return false;
}
// Left slopes (\): x1 < x2 (x aumenta de izquierda a derecha)
// Right slopes (/): x1 > x2 (x decrece de izquierda a derecha)
return current_slope_->x1 < current_slope_->x2;
}
// Actualiza current_slope_ con la rampa correcta según el pie que toca
void Player::updateCurrentSlope() {
updateFeet();
// Left slopes (\) ascendentes a izquierda tocan el pie izquierdo
if (room_->checkLeftSlopes(under_left_foot_)) {
current_slope_ = room_->getSlopeAtPoint(under_left_foot_);
}
// Right slopes (/) ascendentes a derecha tocan el pie derecho
else if (room_->checkRightSlopes(under_right_foot_)) {
current_slope_ = room_->getSlopeAtPoint(under_right_foot_);
}
// Fallback para casos edge
else {
current_slope_ = room_->getSlopeAtPoint(under_left_foot_);
if (current_slope_ == nullptr) {
current_slope_ = room_->getSlopeAtPoint(under_right_foot_);
}
}
#ifdef _DEBUG
if (current_slope_ != nullptr) {
Debug::get()->set("sl.type", isLeftSlope() ? "L\\" : "R/");
Debug::get()->set("sl.p1", std::to_string(current_slope_->x1) + "," + std::to_string(current_slope_->y1));
Debug::get()->set("sl.p2", std::to_string(current_slope_->x2) + "," + std::to_string(current_slope_->y2));
} else {
Debug::get()->set("sl.type", "null");
Debug::get()->unset("sl.p1");
Debug::get()->unset("sl.p2");
}
#endif
}
// 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) -> bool {
return room_->getTile(c) == Room::Tile::KILL;
})) {
markAsDead(); // 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 (0 = automático según options)
void Player::setColor(Uint8 color) {
if (color != 0) {
color_ = color;
return;
}
// Color personalizado desde opciones
if (Options::game.player_color >= 0) {
color_ = static_cast<Uint8>(Options::game.player_color);
} else {
color_ = 14;
}
// Si el color coincide con el fondo de la habitación, usar fallback
if (room_ != nullptr && color_ == room_->getBGColor()) {
color_ = (room_->getBGColor() != 14)
? 14
: 1;
}
}
// Actualiza los puntos de colisión
void Player::updateColliderPoints() {
// 3 columnas × 4 filas: garantiza que cada tile de 8×8 que solape el jugador tenga al menos un punto
const float L = x_;
const float M = x_ + (WIDTH / 2);
const float R = x_ + WIDTH - 1;
const float Y0 = y_;
const float Y1 = y_ + 8;
const float Y2 = y_ + 16;
const float Y3 = y_ + HEIGHT - 1;
collider_points_[0] = {.x = L, .y = Y0};
collider_points_[1] = {.x = M, .y = Y0};
collider_points_[2] = {.x = R, .y = Y0};
collider_points_[3] = {.x = L, .y = Y1};
collider_points_[4] = {.x = M, .y = Y1};
collider_points_[5] = {.x = R, .y = Y1};
collider_points_[6] = {.x = L, .y = Y2};
collider_points_[7] = {.x = M, .y = Y2};
collider_points_[8] = {.x = R, .y = Y2};
collider_points_[9] = {.x = L, .y = Y3};
collider_points_[10] = {.x = M, .y = Y3};
collider_points_[11] = {.x = R, .y = Y3};
}
// Actualiza los puntos de los pies
void Player::updateFeet() {
under_left_foot_ = {
.x = x_,
.y = y_ + HEIGHT};
under_right_foot_ = {
.x = x_ + WIDTH - 1,
.y = y_ + HEIGHT};
}
// Inicializa los sonidos de salto y aterrizaje
void Player::initSounds() { // NOLINT(readability-convert-member-functions-to-static)
jump_sound_ = Resource::Cache::get()->getSound("jump.wav");
land_sound_ = Resource::Cache::get()->getSound("land.wav");
}
// 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);
}
// Resuelve nombre de skin a fichero de animación
auto Player::skinToAnimationPath(const std::string& skin_name) -> std::string {
if (skin_name == "default") {
return "player.yaml";
}
return skin_name + ".yaml";
}
// Cambia la skin del jugador en caliente preservando la orientación actual
void Player::setSkin(const std::string& skin_name) {
const auto FLIP = sprite_->getFlip();
initSprite(skinToAnimationPath(skin_name));
sprite_->setFlip(FLIP);
}
// Inicializa el sprite del jugador
void Player::initSprite(const std::string& animations_path) { // NOLINT(readability-convert-member-functions-to-static)
const auto& animation_data = Resource::Cache::get()->getAnimationData(animations_path);
sprite_ = std::make_unique<AnimatedSprite>(animation_data);
sprite_->setWidth(WIDTH);
sprite_->setHeight(HEIGHT);
sprite_->setCurrentAnimation("default");
}
// Actualiza la posición del sprite y las colisiones
void Player::syncSpriteAndCollider() {
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
#ifdef _DEBUG
updateFeet();
#endif
}
// Coloca el sprite en la posición del jugador
void Player::placeSprite() {
sprite_->setPos(x_, y_);
}
// Calcula la velocidad en x con inercia ligera (interpolación hacia vel. objetivo)
void Player::updateVelocity(float delta_time) {
float target = 0.0F;
if (auto_movement_) {
// La cinta transportadora tiene el control
target = HORIZONTAL_VELOCITY * room_->getConveyorBeltDirection();
} else {
switch (wanna_go_) {
case Direction::LEFT: target = -HORIZONTAL_VELOCITY; break;
case Direction::RIGHT: target = HORIZONTAL_VELOCITY; break;
default: target = 0.0F; break;
}
}
// Orientación del sprite según la dirección deseada (sin cambiar cuando target=0)
if (target > 0.0F) { sprite_->setFlip(Flip::RIGHT); }
else if (target < 0.0F) { sprite_->setFlip(Flip::LEFT); }
// Inercia:
// - En el aire: inercia completa (arranque y frenada graduales)
// - En suelo/rampa: arranque instantáneo, frenada gradual
const float STEP = HORIZONTAL_ACCEL * delta_time;
if (state_ == State::ON_AIR) {
if (vx_ < target) {
vx_ = std::min(vx_ + STEP, target);
} else if (vx_ > target) {
vx_ = std::max(vx_ - STEP, target);
}
} else {
if (target != 0.0F) {
vx_ = target; // Arranque instantáneo
} else if (vx_ > 0.0F) {
vx_ = std::max(vx_ - STEP, 0.0F); // Frenada gradual
} else if (vx_ < 0.0F) {
vx_ = std::min(vx_ + STEP, 0.0F); // Frenada gradual
}
}
}
// Aplica movimiento horizontal con colisión de muros
void Player::applyHorizontalMovement(float delta_time) {
if (vx_ == 0.0F) { return; }
const float DISPLACEMENT = vx_ * delta_time;
if (vx_ < 0.0F) {
const SDL_FRect PROJECTION = getProjection(Direction::LEFT, DISPLACEMENT);
const int POS = room_->checkRightSurfaces(PROJECTION);
if (POS == Collision::NONE) {
x_ += DISPLACEMENT;
} else {
x_ = POS + 1;
}
} else {
const SDL_FRect PROJECTION = getProjection(Direction::RIGHT, DISPLACEMENT);
const int POS = room_->checkLeftSurfaces(PROJECTION);
if (POS == Collision::NONE) {
x_ += DISPLACEMENT;
} else {
x_ = POS - WIDTH;
}
}
}
// Detecta aterrizaje en superficies y rampas
auto Player::handleLandingFromAir(float displacement, const SDL_FRect& projection) -> bool {
// Comprueba la colisión con las superficies y las cintas transportadoras
const float POS = std::max(room_->checkTopSurfaces(projection), room_->checkAutoSurfaces(projection));
if (POS != Collision::NONE) {
// Si hay colisión lo mueve hasta donde no colisiona y pasa a estar sobre la superficie
y_ = POS - HEIGHT;
transitionToState(State::ON_GROUND);
return true;
}
// Comprueba la colisión con las rampas
auto rect = toSDLRect(projection);
const LineVertical LEFT_SIDE = {.x = rect.x, .y1 = rect.y, .y2 = rect.y + rect.h};
const LineVertical RIGHT_SIDE = {.x = rect.x + rect.w - 1, .y1 = rect.y, .y2 = rect.y + rect.h};
const float POINT = std::max(room_->checkRightSlopes(RIGHT_SIDE), room_->checkLeftSlopes(LEFT_SIDE));
if (POINT != Collision::NONE) {
y_ = POINT - HEIGHT;
transitionToState(State::ON_SLOPE);
return true;
}
// No hay colisión
y_ += displacement;
#ifdef _DEBUG
// Guarda por si en debug el jugador se sale de la pantalla, para que no esté cayendo infinitamente
if (y_ > PlayArea::BOTTOM + HEIGHT) { y_ = PlayArea::TOP + 2; }
#endif
return false;
}
// Devuelve el rectangulo de proyeccion
auto Player::getProjection(Direction direction, float displacement) -> SDL_FRect { // NOLINT(readability-convert-member-functions-to-static)
switch (direction) {
case Direction::LEFT:
return {
.x = x_ + displacement,
.y = y_,
.w = std::ceil(std::fabs(displacement)), // Para evitar que tenga una anchura de 0 pixels
.h = HEIGHT - 1}; // -1 para dar ventana de 2px en aperturas de altura exacta
case Direction::RIGHT:
return {
.x = x_ + WIDTH,
.y = y_,
.w = std::ceil(displacement), // Para evitar que tenga una anchura de 0 pixels
.h = HEIGHT - 1}; // -1 para dar ventana de 2px en aperturas de altura exacta
case Direction::UP:
return {
.x = x_,
.y = y_ + displacement,
.w = WIDTH,
.h = std::ceil(std::fabs(displacement)) // Para evitar que tenga una altura de 0 pixels
};
case Direction::DOWN:
return {
.x = x_,
.y = y_ + HEIGHT,
.w = WIDTH,
.h = std::ceil(displacement) // Para evitar que tenga una altura de 0 pixels
};
default:
return {
.x = 0.0F,
.y = 0.0F,
.w = 0.0F,
.h = 0.0F};
}
}
// Marca al jugador como muerto
void Player::markAsDead() {
is_alive_ = (Options::cheats.invincible == Options::Cheat::State::ENABLED);
}
#ifdef _DEBUG
// Establece la posición del jugador directamente (debug)
void Player::setDebugPosition(float x, float y) {
x_ = x;
y_ = y;
syncSpriteAndCollider();
}
// Fija estado ON_GROUND, velocidades a 0, actualiza last_grounded_position_ (debug)
void Player::finalizeDebugTeleport() {
vx_ = 0.0F;
vy_ = 0.0F;
last_grounded_position_ = static_cast<int>(y_);
transitionToState(State::ON_GROUND);
syncSpriteAndCollider();
}
#endif