Files
projecte_2026/source/game/entities/player.cpp
2026-04-11 18:40:03 +02:00

690 lines
24 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 "core/audio/audio.hpp" // Para Audio
#include "core/input/input.hpp" // Para Input, InputAction
#include "core/rendering/sprite/animated_sprite.hpp" // Para AnimatedSprite
#include "core/resources/resource_cache.hpp" // Para Resource
#include "game/defaults.hpp" // Para Defaults::Game::Player, Defaults::Sound::Files
#include "game/entities/solid_actor.hpp" // Para SolidActor
#include "game/gameplay/room.hpp" // Para Room
#include "game/gameplay/solid_actor_manager.hpp" // Para SolidActorManager
#include "game/gameplay/tile_collider.hpp" // Para TileCollider
#include "game/options.hpp" // Para Cheat, Options
#include "utils/defines.hpp" // Para PlayArea, Collision
#ifdef _DEBUG
#include "core/system/debug.hpp" // Para Debug
#endif
// Helpers NONE-aware para combinar un hit del TileCollider con un hit del
// SolidActorManager. Devuelven el valor más "clampante" (más a la derecha
// para left-wall, más a la izquierda para right-wall, más abajo para ceiling).
namespace {
auto combineLeftWall(float tile_hit, float actor_hit) -> float {
if (tile_hit == Collision::NONE) { return actor_hit; }
if (actor_hit == Collision::NONE) { return tile_hit; }
return std::max(tile_hit, actor_hit);
}
auto combineRightWall(float tile_hit, float actor_hit) -> float {
if (tile_hit == Collision::NONE) { return actor_hit; }
if (actor_hit == Collision::NONE) { return tile_hit; }
return std::min(tile_hit, actor_hit);
}
auto combineCeiling(float tile_hit, float actor_hit) -> float {
if (tile_hit == Collision::NONE) { return actor_hit; }
if (actor_hit == Collision::NONE) { return tile_hit; }
return std::max(tile_hit, actor_hit);
}
} // namespace
// ============================================================================
// Constructor
// ============================================================================
Player::Player(const Data& player)
: room_(player.room) {
initSprite(player.animations_path);
applySpawnValues(player.spawn_data);
placeSprite();
initSounds();
previous_state_ = state_;
}
// ============================================================================
// Render
// ============================================================================
void Player::render() {
sprite_->render();
}
// ============================================================================
// Update — pipeline principal (6 fases)
// ============================================================================
void Player::update(float delta_time) {
if (is_paused_) { return; }
// 0. Carry de plataforma móvil (SolidActor con CARRY_ON_TOP).
// Snap absoluto del eje Y al top del AABB y desplazamiento horizontal
// por el delta del último frame del actor. Esto mantiene al Player
// pegado a la plataforma cuando sube/baja y lo arrastra lateralmente.
if (current_carrier_ != nullptr) {
const auto& aabb = current_carrier_->getAABB();
x_ += current_carrier_->getLastDelta().x;
y_ = aabb.y - HEIGHT;
}
// 1. Leer input
handleInput();
// 2. Calcular velocidades
updateVelocity(delta_time);
if (state_ == State::ON_AIR) {
applyGravity(delta_time);
}
// 3. Saltar o caer a través de plataformas/slopes
handleJumpAndDrop();
// 4. Aplicar movimiento con colisión
moveHorizontal(delta_time);
moveVertical(delta_time);
// 5. Detectar caída
checkFalling();
// 6. Kill tiles
const auto& ktc = room_->getTileCollider();
if (ktc.touchesKillTile(x_, y_, WIDTH, HEIGHT)) {
markAsDead();
}
// 7. Finalizar
syncSpriteAndCollider();
animate(delta_time);
border_ = handleBorders();
#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
}
// ============================================================================
// Fase 1: Input
// ============================================================================
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_;
jump_held_ = JUMP_PRESSED;
const bool DOWN_PRESSED = Input::get()->checkAction(InputAction::DOWN);
wanna_down_ = DOWN_PRESSED && !down_held_;
down_held_ = DOWN_PRESSED;
}
// ============================================================================
// Fase 2: Velocidades
// ============================================================================
void Player::updateVelocity(float delta_time) {
float target = 0.0F;
switch (wanna_go_) {
case Direction::LEFT:
target = -HORIZONTAL_VELOCITY;
break;
case Direction::RIGHT:
target = HORIZONTAL_VELOCITY;
break;
default:
target = 0.0F;
break;
}
// Detectar cambio de dirección (solo en suelo — en el aire no se gira)
if (state_ != State::ON_AIR && target != 0.0F) {
Direction new_facing = (target > 0.0F) ? Direction::RIGHT : Direction::LEFT;
if (new_facing != facing_) {
facing_ = new_facing;
turning_ = true;
sprite_->resetAnimation();
}
}
// Orientación del sprite (solo en suelo, en el aire se mantiene la dirección del salto)
if (state_ != State::ON_AIR) {
if (target > 0.0F) {
sprite_->setFlip(Flip::RIGHT);
} else if (target < 0.0F) {
sprite_->setFlip(Flip::LEFT);
}
}
// Inercia: aire = gradual ambas direcciones, suelo = instantáneo arranque, gradual frenada
const float STEP = HORIZONTAL_ACCEL * delta_time;
if (state_ == State::ON_AIR) {
// En el aire, permitir mantener velocidad boosteada si vamos en la misma dirección
float air_target = target;
if ((target > 0.0F && vx_ > target) || (target < 0.0F && vx_ < target)) {
air_target = vx_; // No frenar el boost
}
if (vx_ < air_target) {
vx_ = std::min(vx_ + STEP, air_target);
} else if (vx_ > air_target) {
vx_ = std::max(vx_ - STEP, air_target);
}
} else {
if (target != 0.0F) {
vx_ = target;
} else if (vx_ > 0.0F) {
vx_ = std::max(vx_ - STEP, 0.0F);
} else if (vx_ < 0.0F) {
vx_ = std::min(vx_ + STEP, 0.0F);
}
}
}
void Player::applyGravity(float delta_time) {
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);
}
// ============================================================================
// Fase 3: Saltar y drop-through
// ============================================================================
void Player::handleJumpAndDrop() {
if (state_ == State::ON_AIR) { return; }
if (wanna_jump_) {
// No saltar si hay techo justo encima (pasillo de altura justa).
// Sin esta comprobación se reproducían sonido de salto + landing y un frame
// de salto antes de colisionar inmediatamente con el techo.
const auto& tc = room_->getTileCollider();
if (tc.checkCeiling(x_, y_ - 1, WIDTH) != Collision::NONE) {
return;
}
startJump();
return;
}
// Drop-through: plataforma passable
if (wanna_down_ && state_ == State::ON_GROUND) {
const auto& tc = room_->getTileCollider();
float foot_y = y_ + HEIGHT;
int foot_row = tc.toTile(static_cast<int>(foot_y));
int left_col = tc.toTile(static_cast<int>(x_));
int right_col = tc.toTile(static_cast<int>(x_ + WIDTH - 1));
for (int col = left_col; col <= right_col; ++col) {
if (tc.getTileAt(col, foot_row) == TileCollider::Tile::PASSABLE) {
y_ += 1.0F;
vy_ = 0.0F;
transitionToState(State::ON_AIR);
return;
}
}
}
}
void Player::startJump() {
vy_ = JUMP_VELOCITY;
vx_ *= JUMP_SPEED_BOOST;
last_grounded_position_ = y_;
Audio::get()->playSound(jump_sound_, Audio::Group::GAME);
transitionToState(State::ON_AIR);
}
// ============================================================================
// Fase 4a: Movimiento horizontal
// ============================================================================
// Early exit del movimiento horizontal si el player ya está pegado a una
// pared en su dirección de movimiento. Sin esto, el player choca pero
// conserva vx_ != 0 y animate() reproduce "walk" continuamente.
auto Player::stuckAgainstWall() const -> bool {
const auto& tc = room_->getTileCollider();
const auto& sm = room_->getSolidActors();
if (vx_ > 0.0F) {
return tc.checkWallRight(x_, y_, WIDTH, HEIGHT) != Collision::NONE ||
sm.checkWallRight(x_, y_, WIDTH, HEIGHT) != Collision::NONE;
}
if (vx_ < 0.0F) {
return tc.checkWallLeft(x_, y_, WIDTH, HEIGHT) != Collision::NONE ||
sm.checkWallLeft(x_, y_, WIDTH, HEIGHT) != Collision::NONE;
}
return false;
}
void Player::moveHorizontal(float delta_time) {
if (stuckAgainstWall()) {
vx_ = 0.0F;
return;
}
const auto& tc = room_->getTileCollider();
const auto& sm = room_->getSolidActors();
float new_x = x_ + (vx_ * delta_time);
// Comprobar ambos muros siempre (el tilemap extendido incluye paredes de rooms
// adyacentes; comprobar ambos lados evita solapamiento en zona de borde).
// Se combinan tiles + solid actors en cada lado tomando el muro más "clampante".
const float LEFT_WALL = combineLeftWall(
tc.checkWallLeft(new_x, y_, WIDTH, HEIGHT),
sm.checkWallLeft(new_x, y_, WIDTH, HEIGHT));
if (LEFT_WALL != Collision::NONE && LEFT_WALL > new_x) {
new_x = LEFT_WALL;
}
const float RIGHT_WALL = combineRightWall(
tc.checkWallRight(new_x, y_, WIDTH, HEIGHT),
sm.checkWallRight(new_x, y_, WIDTH, HEIGHT));
if (RIGHT_WALL != Collision::NONE) {
const float CORRECTED = RIGHT_WALL - WIDTH;
new_x = std::min(new_x, CORRECTED);
}
x_ = new_x;
// Slope following y detección solo cuando hay movimiento horizontal
if (vx_ == 0.0F) { return; }
if (state_ == State::ON_SLOPE) { followSlope(); }
if (state_ == State::ON_GROUND) { detectSlopeEntry(); }
}
// Ajusta Y del jugador para seguir la superficie de la slope mientras camina.
// Si el pie sale del tile actual, busca el siguiente tile de slope en la fila
// actual y la inferior (las slopes en escalera bajan una fila por tile).
// Si no encuentra slope, llama a exitSlope().
void Player::followSlope() {
const auto& tc = room_->getTileCollider();
// SLOPE_L (\): pie izquierdo. SLOPE_R (/): pie derecho.
float foot_x = (slope_type_ == TileCollider::Tile::SLOPE_L) ? x_ : x_ + WIDTH - 1;
// Calcular Y en la slope actual
float surface_y = tc.getSlopeY(slope_tile_x_, slope_tile_y_, foot_x);
y_ = surface_y - HEIGHT;
// Comprobar si hemos salido del tile actual (coordenadas del mapa extendido)
int foot_tile_x = tc.toTile(static_cast<int>(foot_x));
int foot_tile_y = tc.toTile(static_cast<int>(y_ + HEIGHT));
if (foot_tile_x != slope_tile_x_ || foot_tile_y != slope_tile_y_) {
// Buscar slope en el tile calculado y en el de abajo (la escalera de slopes
// siempre tiene el siguiente tile una fila arriba o abajo)
for (int row = foot_tile_y; row <= foot_tile_y + 1; ++row) {
auto new_tile = tc.getTileAt(foot_tile_x, row);
if (new_tile == TileCollider::Tile::SLOPE_L || new_tile == TileCollider::Tile::SLOPE_R) {
slope_tile_x_ = foot_tile_x;
slope_tile_y_ = row;
slope_type_ = new_tile;
surface_y = tc.getSlopeY(slope_tile_x_, slope_tile_y_, foot_x);
y_ = surface_y - HEIGHT;
return;
}
}
exitSlope();
}
}
// El jugador ha salido del tile de slope sin encontrar otra slope adyacente.
// Comprueba si hay suelo debajo (foot_y y foot_y+1 para cubrir el boundary exacto
// entre filas cuando se sale por el extremo inferior de la slope).
// Si hay suelo, snapea al borde del tile. Si no, empieza a caer.
void Player::exitSlope() {
const auto& tc = room_->getTileCollider();
float foot_y = y_ + HEIGHT;
// Comprobar suelo en la fila actual y la siguiente (al salir por abajo de una slope,
// los pies pueden estar en el último pixel de la fila, justo antes del suelo)
for (int check = 0; check <= 1; ++check) {
float check_y = foot_y + check;
if (tc.hasGroundBelow(x_, check_y, WIDTH)) {
int row = tc.toTile(static_cast<int>(check_y));
y_ = tc.toPixel(row) - HEIGHT;
transitionToState(State::ON_GROUND);
return;
}
}
vy_ = 0.0F;
transitionToState(State::ON_AIR);
}
// Detecta si el jugador ha pisado una slope caminando desde suelo plano.
// Las slopes en escalera están una fila arriba del suelo, así que checkSlopeBelow
// también mira la fila superior.
void Player::detectSlopeEntry() {
const auto& tc = room_->getTileCollider();
float foot_y = y_ + HEIGHT;
auto slope = tc.checkSlopeBelow(x_, foot_y, WIDTH);
if (slope.on_slope) {
y_ = slope.surface_y - HEIGHT;
slope_tile_x_ = slope.tile_x;
slope_tile_y_ = slope.tile_y;
slope_type_ = slope.type;
transitionToState(State::ON_SLOPE);
}
}
// ============================================================================
// Fase 4b: Movimiento vertical
// ============================================================================
// Subiendo: comprobar techo (tiles + solid actors). Si hay colisión,
// snap y parar vy.
void Player::moveVerticalUp(float displacement) {
const auto& tc = room_->getTileCollider();
const auto& sm = room_->getSolidActors();
const float NEW_Y = y_ + displacement;
const float CEILING = combineCeiling(
tc.checkCeiling(x_, NEW_Y, WIDTH),
sm.checkCeiling(x_, NEW_Y, WIDTH));
if (CEILING != Collision::NONE) {
y_ = CEILING;
vy_ = 0.0F;
} else {
y_ = NEW_Y;
}
}
// Bajando: comprobar suelo en tiles y en solid actors; el que esté antes
// (menor y) gana. Si es un SolidActor con CARRY_ON_TOP, guarda el carrier.
void Player::moveVerticalDown(float displacement) {
const auto& tc = room_->getTileCollider();
const auto& sm = room_->getSolidActors();
const float FOOT_Y = y_ + HEIGHT;
const float NEW_FOOT_Y = FOOT_Y + displacement;
const auto TILE_HIT = tc.checkFloor(x_, FOOT_Y, WIDTH, NEW_FOOT_Y);
const auto ACTOR_HIT = sm.checkFloor(x_, FOOT_Y, WIDTH, NEW_FOOT_Y);
// El tile tiene prioridad si está igual o más arriba que el actor.
const bool TILE_WINS = (TILE_HIT.y != Collision::NONE) &&
(ACTOR_HIT.y == Collision::NONE || TILE_HIT.y <= ACTOR_HIT.y);
const bool ACTOR_WINS = !TILE_WINS && (ACTOR_HIT.y != Collision::NONE);
if (TILE_WINS) {
y_ = TILE_HIT.y - HEIGHT;
const bool IS_SLOPE = TILE_HIT.type == TileCollider::Tile::SLOPE_L ||
TILE_HIT.type == TileCollider::Tile::SLOPE_R;
if (IS_SLOPE) {
slope_tile_x_ = TILE_HIT.tile_x;
slope_tile_y_ = TILE_HIT.tile_y;
slope_type_ = TILE_HIT.type;
transitionToState(State::ON_SLOPE);
} else {
transitionToState(State::ON_GROUND);
}
current_carrier_ = nullptr;
return;
}
if (ACTOR_WINS) {
y_ = ACTOR_HIT.y - HEIGHT;
transitionToState(State::ON_GROUND);
current_carrier_ = ACTOR_HIT.carrier;
return;
}
// Cae libremente
y_ += displacement;
#ifdef _DEBUG
if (y_ > PlayArea::BOTTOM + 100) { y_ = PlayArea::TOP + 2; }
#endif
}
void Player::moveVertical(float delta_time) {
if (state_ != State::ON_AIR) { return; }
const float DISPLACEMENT = vy_ * delta_time;
if (vy_ < 0.0F) {
moveVerticalUp(DISPLACEMENT);
} else if (vy_ > 0.0F) {
moveVerticalDown(DISPLACEMENT);
}
}
// ============================================================================
// Fase 5: Detección de caída
// ============================================================================
void Player::checkFalling() {
if (state_ == State::ON_AIR) { return; }
const auto& tc = room_->getTileCollider();
const auto& sm = room_->getSolidActors();
if (state_ == State::ON_SLOPE) {
// Verificar que el tile de slope sigue existiendo
auto tile = tc.getTileAt(slope_tile_x_, slope_tile_y_);
if (tile != TileCollider::Tile::SLOPE_L && tile != TileCollider::Tile::SLOPE_R) {
vy_ = 0.0F;
transitionToState(State::ON_AIR);
}
return;
}
// ON_GROUND: comprobar si sigue habiendo suelo (tile o solid actor).
// El tilemap extendido incluye tiles de las rooms adyacentes, así que
// no hace falta cross-room para tiles.
float foot_y = y_ + HEIGHT;
const bool TILE_GROUND = tc.hasGroundBelow(x_, foot_y, WIDTH);
const bool ACTOR_GROUND = sm.hasGroundBelow(x_, foot_y, WIDTH);
if (!TILE_GROUND && !ACTOR_GROUND) {
// Sticking: si no hay suelo pero hay slope debajo, snapear a ella
// para transición suave suelo→slope (bajada de rampas sin caer)
auto slope = tc.checkSlopeBelow(x_, foot_y, WIDTH);
if (slope.on_slope) {
y_ = slope.surface_y - HEIGHT;
slope_tile_x_ = slope.tile_x;
slope_tile_y_ = slope.tile_y;
slope_type_ = slope.type;
transitionToState(State::ON_SLOPE);
return;
}
vy_ = 0.0F;
transitionToState(State::ON_AIR);
return;
}
// Refrescar current_carrier_ para la próxima iteración: si hay actor
// debajo, comprobar si lleva CARRY_ON_TOP y guardarlo; si no, limpiarlo.
if (ACTOR_GROUND) {
auto hit = sm.checkFloor(x_, foot_y, WIDTH, foot_y + 1.0F);
current_carrier_ = hit.carrier;
} else {
current_carrier_ = nullptr;
}
}
// ============================================================================
// Gestión de estado
// ============================================================================
void Player::transitionToState(State state) {
previous_state_ = state_;
state_ = state;
switch (state) {
case State::ON_GROUND:
case State::ON_SLOPE:
vy_ = 0;
// Clamp vx al aterrizar (el salto puede dar un boost extra)
vx_ = std::clamp(vx_, -HORIZONTAL_VELOCITY, HORIZONTAL_VELOCITY);
if (previous_state_ == State::ON_AIR) {
Audio::get()->playSound(land_sound_, Audio::Group::GAME);
}
break;
case State::ON_AIR:
last_grounded_position_ = static_cast<int>(y_);
current_carrier_ = nullptr; // Perder carry al despegar
break;
}
}
// ============================================================================
// Bordes de pantalla
// ============================================================================
auto Player::handleBorders() const -> 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;
}
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();
}
// ============================================================================
// Geometría y renderizado
// ============================================================================
void Player::syncSpriteAndCollider() {
placeSprite();
collider_box_ = getRect();
}
void Player::placeSprite() {
sprite_->setPos(x_, y_);
}
void Player::animate(float delta_time) { // NOLINT(readability-make-member-function-const)
if (state_ == State::ON_AIR) {
turning_ = false;
const bool NEAR_PEAK = vy_ > JUMP_VELOCITY * 0.5F && vy_ < -JUMP_VELOCITY * 0.5F;
sprite_->setCurrentAnimation(NEAR_PEAK ? "jump_peak" : "jump");
} else if (vx_ != 0) {
if (turning_) {
sprite_->setCurrentAnimation("turn_walk");
} else {
sprite_->setCurrentAnimation("walk");
}
sprite_->update(delta_time);
} else {
turning_ = false;
sprite_->setCurrentAnimation("stand");
}
}
// ============================================================================
// Skin
// ============================================================================
auto Player::skinToAnimationPath(const std::string& skin_name) -> std::string {
if (skin_name == Defaults::Game::Player::SKIN) { return Defaults::Game::Player::DEFAULT_ANIMATION; }
return skin_name + ".yaml";
}
// ============================================================================
// Inicialización
// ============================================================================
void Player::initSprite(const std::string& animations_path) {
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("walk");
}
void Player::initSounds() {
jump_sound_ = Resource::Cache::get()->getSound(Defaults::Sound::Files::JUMP);
land_sound_ = Resource::Cache::get()->getSound(Defaults::Sound::Files::LAND);
}
void Player::applySpawnValues(const SpawnData& spawn) {
x_ = spawn.x;
y_ = spawn.y;
vx_ = spawn.vx;
vy_ = spawn.vy;
last_grounded_position_ = spawn.last_grounded_position;
state_ = spawn.state;
sprite_->setFlip(spawn.flip);
facing_ = (spawn.flip == Flip::LEFT) ? Direction::LEFT : Direction::RIGHT;
}
// ============================================================================
// Utilidades
// ============================================================================
void Player::markAsDead() {
is_alive_ = (Options::cheats.invincible == Options::Cheat::State::ENABLED);
}
#ifdef _DEBUG
void Player::setDebugPosition(float x, float y) {
x_ = x;
y_ = y;
syncSpriteAndCollider();
}
void Player::finalizeDebugTeleport() {
vx_ = 0.0F;
vy_ = 0.0F;
last_grounded_position_ = static_cast<int>(y_);
slope_tile_x_ = 0;
slope_tile_y_ = 0;
slope_type_ = TileCollider::Tile::EMPTY;
transitionToState(State::ON_GROUND);
syncSpriteAndCollider();
}
#endif