832 lines
28 KiB
C++
832 lines
28 KiB
C++
// 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 |