migrades portes i plataformes a solidActor

This commit is contained in:
2026-04-11 12:54:54 +02:00
parent 49f6ed41e6
commit 5b2f986d32
22 changed files with 686 additions and 286 deletions

View File

@@ -5,7 +5,9 @@
// Constructor: carga la animación y posiciona la puerta. Si start_opened es
// true, la puerta se crea ya abierta (estado OPENED, animación "opened"); en
// caso contrario, se crea cerrada (estado CLOSED, animación "closed").
// caso contrario, se crea cerrada (estado CLOSED, animación "closed") y
// activa el flag BLOCKS_PLAYER del SolidActor para que los sweeps del
// SolidActorManager la vean como muro.
Door::Door(const Data& data, bool start_opened)
: sprite_(std::make_shared<AnimatedSprite>(Resource::Cache::get()->getAnimationData(data.animation_path))),
id_(data.id),
@@ -13,7 +15,10 @@ Door::Door(const Data& data, bool start_opened)
sprite_->setPosX(data.x);
sprite_->setPosY(data.y);
sprite_->setCurrentAnimation(start_opened ? "opened" : "closed");
collider_ = sprite_->getRect();
aabb_ = sprite_->getRect();
if (!start_opened) {
flags_ = BLOCKS_PLAYER;
}
}
// Pinta la puerta en pantalla
@@ -23,8 +28,9 @@ void Door::render() {
// Avanza la animación. Solo OPENING anima de verdad; CLOSED y OPENED son
// frames estáticos. Cuando la animación de OPENING termina, transiciona a
// OPENED, fija el frame final y marca just_opened_ para que el DoorManager
// libere los tiles de colisión.
// OPENED, fija el frame final, limpia el flag BLOCKS_PLAYER (los sweeps
// del SolidActorManager dejan de verla como muro) y marca just_opened_
// para que el DoorManager persista el estado abierto en DoorTracker.
void Door::update(float delta_time) {
if (is_paused_) {
return;
@@ -35,6 +41,7 @@ void Door::update(float delta_time) {
if (sprite_->animationIsCompleted()) {
state_ = State::OPENED;
sprite_->setCurrentAnimation("opened");
flags_ = 0; // Deja de bloquear al Player
just_opened_ = true;
}
}
@@ -65,11 +72,12 @@ auto Door::justOpened() -> bool {
}
#ifdef _DEBUG
// Mueve la puerta a la posición indicada (sprite + collider). NO toca el
// CollisionMap — eso es responsabilidad del DoorManager (moveDoor/removeDoor).
// Mueve la puerta a la posición indicada (sprite + AABB del SolidActor).
// Usado por el editor; el DoorManager::moveDoor se encarga del bookkeeping
// de registro en el SolidActorManager.
void Door::setPosition(float x, float y) {
sprite_->setPosX(x);
sprite_->setPosY(y);
collider_ = sprite_->getRect();
aabb_ = sprite_->getRect();
}
#endif

View File

@@ -5,6 +5,8 @@
#include <memory> // Para shared_ptr
#include <string> // Para string
#include "game/entities/solid_actor.hpp" // Para SolidActor
class AnimatedSprite;
/**
@@ -15,12 +17,12 @@ class AnimatedSprite;
* - "opening": animación de transición que se reproduce una sola vez
* - "opened": estado final no bloqueante (frame estático)
*
* El comportamiento de "muro" se implementa marcando los 4 tiles que ocupa
* la puerta como WALL en el CollisionMap (lo gestiona el DoorManager). Cuando
* la puerta termina de abrirse, los tiles vuelven a EMPTY y el jugador puede
* pasar.
* El comportamiento de "muro" se implementa como SolidActor con flag
* BLOCKS_PLAYER. El SolidActorManager de la Room lo consulta en los sweeps
* del Player. Cuando la puerta termina de abrirse, se limpia el flag
* BLOCKS_PLAYER y el sweep deja de verla como pared.
*/
class Door {
class Door : public SolidActor {
public:
enum class State : int {
CLOSED = 0,
@@ -41,11 +43,10 @@ class Door {
void render(); // Pinta la puerta en pantalla
void update(float delta_time); // Avanza la animación; si OPENING termina → OPENED
auto getCollider() -> SDL_FRect& { return collider_; } // Rectángulo de colisión (8x32)
[[nodiscard]] auto getPos() const -> SDL_FPoint; // Posición en píxeles
[[nodiscard]] auto getId() const -> const std::string& { return id_; } // Identificador
[[nodiscard]] auto getState() const -> State { return state_; } // Estado actual
[[nodiscard]] auto isBlocking() const -> bool { return state_ != State::OPENED; } // True si bloquea al jugador
auto getCollider() -> SDL_FRect& { return aabb_; } // Rectángulo de colisión (8x32)
[[nodiscard]] auto getPos() const -> SDL_FPoint; // Posición en píxeles
[[nodiscard]] auto getId() const -> const std::string& { return id_; } // Identificador
[[nodiscard]] auto getState() const -> State { return state_; } // Estado actual
void startOpening(); // Transición CLOSED → OPENING
auto justOpened() -> bool; // Flag one-shot consumido por el manager
@@ -53,12 +54,11 @@ class Door {
void setPaused(bool paused) { is_paused_ = paused; } // Pausa/despausa la animación
#ifdef _DEBUG
void setPosition(float x, float y); // Mueve sprite y collider en vivo (editor; NO toca CollisionMap)
void setPosition(float x, float y); // Mueve sprite y AABB en vivo (editor)
#endif
private:
std::shared_ptr<AnimatedSprite> sprite_; // Sprite animado de la puerta
SDL_FRect collider_{}; // Rectángulo de colisión
std::string id_; // Identificador
State state_{State::CLOSED}; // Estado actual
bool just_opened_{false}; // Flag one-shot: la puerta acaba de transicionar a OPENED

View File

@@ -28,13 +28,19 @@ MovingPlatform::MovingPlatform(const Data& data)
speed_(data.speed),
loop_mode_(data.loop),
easing_(resolveEasing(data.easing)) {
// Flags del SolidActor: jump-through desde abajo, carry al Player encima.
// NOTA: sin BLOCKS_PLAYER, las plataformas móviles no bloquean lateralmente
// ni por arriba. ONEWAY_TOP hace que los sweeps verticales solo las vean
// al caer desde arriba.
flags_ = CARRY_ON_TOP | ONEWAY_TOP;
// Colocar el sprite en el primer waypoint
if (!path_.empty()) {
sprite_->setPosX(path_[0].x);
sprite_->setPosY(path_[0].y);
}
collider_ = getRect();
aabb_ = getRect();
// Frame inicial
sprite_->setCurrentAnimationFrame((data.frame == -1) ? (rand() % sprite_->getCurrentAnimationSize()) : data.frame);
@@ -115,8 +121,8 @@ void MovingPlatform::update(float delta_time) {
sprite_->animate(delta_time);
if (path_.size() < 2) {
last_dx_ = 0.0F;
last_dy_ = 0.0F;
last_delta_.x = 0.0F;
last_delta_.y = 0.0F;
return;
}
@@ -130,8 +136,8 @@ void MovingPlatform::update(float delta_time) {
waiting_ = false;
advanceSegment();
} else {
last_dx_ = 0.0F;
last_dy_ = 0.0F;
last_delta_.x = 0.0F;
last_delta_.y = 0.0F;
return;
}
}
@@ -174,9 +180,9 @@ void MovingPlatform::update(float delta_time) {
sprite_->setPosY(new_y);
}
last_dx_ = sprite_->getPosX() - old_x;
last_dy_ = sprite_->getPosY() - old_y;
collider_ = getRect();
last_delta_.x = sprite_->getPosX() - old_x;
last_delta_.y = sprite_->getPosY() - old_y;
aabb_ = getRect();
}
// Pinta la plataforma en pantalla
@@ -208,7 +214,7 @@ void MovingPlatform::resetToInitialPosition(const Data& data) {
sprite_->setPosY(path_[0].y);
}
collider_ = getRect();
aabb_ = getRect();
recalcSegmentLength();
}
#endif
@@ -217,8 +223,3 @@ void MovingPlatform::resetToInitialPosition(const Data& data) {
auto MovingPlatform::getRect() -> SDL_FRect {
return sprite_->getRect();
}
// Obtiene el rectangulo de colisión
auto MovingPlatform::getCollider() -> SDL_FRect& {
return collider_;
}

View File

@@ -6,6 +6,8 @@
#include <string> // Para string
#include <vector> // Para vector
#include "game/entities/solid_actor.hpp" // Para SolidActor
class AnimatedSprite;
// Punto de paso en la ruta de una plataforma
@@ -22,7 +24,7 @@ enum class LoopMode { PINGPONG,
// Tipo de función de easing
using EasingFunc = float (*)(float);
class MovingPlatform {
class MovingPlatform : public SolidActor {
public:
struct Data {
std::string animation_path;
@@ -44,10 +46,7 @@ class MovingPlatform {
#endif
auto getRect() -> SDL_FRect;
auto getCollider() -> SDL_FRect&;
[[nodiscard]] auto getLastDX() const -> float { return last_dx_; }
[[nodiscard]] auto getLastDY() const -> float { return last_dy_; }
auto getCollider() -> SDL_FRect& { return aabb_; }
private:
void advanceSegment();
@@ -57,9 +56,6 @@ class MovingPlatform {
static auto resolveEasing(const std::string& name) -> EasingFunc;
std::shared_ptr<AnimatedSprite> sprite_;
SDL_FRect collider_{};
float last_dx_{0.0F};
float last_dy_{0.0F};
// Estado del path
std::vector<Waypoint> path_;

View File

@@ -9,7 +9,9 @@
#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
@@ -18,6 +20,29 @@
#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
// ============================================================================
@@ -46,6 +71,16 @@ void Player::render() {
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();
@@ -236,34 +271,48 @@ void Player::startJump() {
// Fase 4a: Movimiento horizontal
// ============================================================================
void Player::moveHorizontal(float delta_time) {
// 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();
// Early exit: si hay pared inmediata en la dirección de movimiento, parar
// y poner vx_=0. Sin esto, el player choca, queda re-posicionado en el
// mismo sitio pero conserva vx_ != 0, así que animate() reproduce walk
// anim continuamente mientras empuja contra la pared.
if (vx_ > 0.0F && tc.checkWallRight(x_, y_, WIDTH, HEIGHT) != Collision::NONE) {
vx_ = 0.0F;
return;
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 && tc.checkWallLeft(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)
float wall = tc.checkWallLeft(new_x, y_, WIDTH, HEIGHT);
if (wall != Collision::NONE && wall > new_x) {
new_x = wall;
// 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;
}
wall = tc.checkWallRight(new_x, y_, WIDTH, HEIGHT);
if (wall != Collision::NONE) {
float corrected = wall - WIDTH;
if (corrected < new_x) { new_x = corrected; }
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;
@@ -355,44 +404,74 @@ void Player::detectSlopeEntry() {
// 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 auto& tc = room_->getTileCollider();
float displacement = vy_ * delta_time;
const float DISPLACEMENT = vy_ * delta_time;
if (vy_ < 0.0F) {
// Subiendo: comprobar techo
float new_y = y_ + displacement;
float ceiling = tc.checkCeiling(x_, new_y, WIDTH);
if (ceiling != Collision::NONE) {
y_ = ceiling;
vy_ = 0.0F;
} else {
y_ = new_y;
}
moveVerticalUp(DISPLACEMENT);
} else if (vy_ > 0.0F) {
// Bajando: comprobar suelo
float foot_y = y_ + HEIGHT;
float new_foot_y = foot_y + displacement;
auto hit = tc.checkFloor(x_, foot_y, WIDTH, new_foot_y);
if (hit.y != Collision::NONE) {
y_ = hit.y - HEIGHT;
if (hit.type == TileCollider::Tile::SLOPE_L || hit.type == TileCollider::Tile::SLOPE_R) {
slope_tile_x_ = hit.tile_x;
slope_tile_y_ = hit.tile_y;
slope_type_ = hit.type;
transitionToState(State::ON_SLOPE);
} else {
transitionToState(State::ON_GROUND);
}
} else {
y_ += displacement;
#ifdef _DEBUG
if (y_ > PlayArea::BOTTOM + 100) { y_ = PlayArea::TOP + 2; }
#endif
}
moveVerticalDown(DISPLACEMENT);
}
}
@@ -404,6 +483,7 @@ 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
@@ -415,13 +495,14 @@ void Player::checkFalling() {
return;
}
// ON_GROUND: si está sobre una plataforma móvil, no comprobar tiles
if (on_platform_) { return; }
// ON_GROUND: comprobar si sigue habiendo suelo (el tilemap extendido
// incluye tiles de las rooms adyacentes, así que no hace falta cross-room)
// 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;
if (!tc.hasGroundBelow(x_, foot_y, WIDTH)) {
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);
@@ -436,6 +517,16 @@ void Player::checkFalling() {
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;
}
}
@@ -467,6 +558,7 @@ void Player::transitionToState(State state) {
break;
case State::ON_AIR:
last_grounded_position_ = static_cast<int>(y_);
current_carrier_ = nullptr; // Perder carry al despegar
break;
}
}
@@ -518,18 +610,6 @@ void Player::syncSpriteAndCollider() {
collider_box_ = getRect();
}
// Aplica el desplazamiento de una plataforma móvil al jugador
void Player::applyPlatformDisplacement(float dx, float surface_y) {
y_ = surface_y - HEIGHT; // Snap vertical al top de la plataforma
x_ += dx; // Desplazamiento horizontal
vy_ = 0.0F;
on_platform_ = true;
if (state_ != State::ON_GROUND) {
transitionToState(State::ON_GROUND);
}
syncSpriteAndCollider();
}
void Player::placeSprite() {
sprite_->setPos(x_, y_);
}

View File

@@ -12,6 +12,7 @@
#include "game/options.hpp" // Para Cheat, Options
#include "utils/defines.hpp" // Para PlayArea, Tile, Flip
struct JA_Sound_t;
class SolidActor;
class Player {
public:
@@ -72,8 +73,6 @@ class Player {
void setRoom(std::shared_ptr<Room> room) { room_ = std::move(room); }
[[nodiscard]] auto isAlive() const -> bool { return is_alive_; }
[[nodiscard]] auto getVY() const -> float { return vy_; }
void applyPlatformDisplacement(float dx, float surface_y);
void clearPlatformFlag() { on_platform_ = false; }
void setPaused(bool value) { is_paused_ = value; }
void setIgnoreInput(bool value) { ignore_input_ = value; }
@@ -119,7 +118,7 @@ class Player {
bool is_alive_ = true;
bool is_paused_ = false;
bool ignore_input_ = false;
bool on_platform_ = false;
SolidActor* current_carrier_ = nullptr; // Actor con CARRY_ON_TOP sobre el que estamos de pie
bool turning_ = false;
Direction facing_ = Direction::RIGHT;
Room::Border border_ = Room::Border::TOP;
@@ -134,8 +133,11 @@ class Player {
void updateVelocity(float delta_time);
void applyGravity(float delta_time);
void handleJumpAndDrop();
[[nodiscard]] auto stuckAgainstWall() const -> bool;
void moveHorizontal(float delta_time);
void moveVertical(float delta_time);
void moveVerticalUp(float displacement);
void moveVerticalDown(float displacement);
void followSlope();
void exitSlope();
void detectSlopeEntry();

View File

@@ -0,0 +1,53 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
/**
* @brief Clase base ligera para entidades con AABB dinámico de colisión.
*
* Un SolidActor expone un rectángulo (aabb_) al SolidActorManager, que lo
* usa para los sweeps del Player y otras queries. No es polimórfica: no
* tiene métodos virtuales (las entidades concretas viven en sus propios
* managers, que las actualizan y las renderizan). La base solo unifica
* el "bit de colisión" — AABB, flags y delta del último frame.
*
* Hereda de esta clase cualquier entidad que necesite participar en la
* resolución de colisión del Player: puertas, plataformas móviles,
* bloques empujables, ascensores, compuertas, etc.
*
* Las flags determinan el comportamiento del actor ante el sweep:
* - BLOCKS_PLAYER: el AABB bloquea al Player como un muro en las 4 dirs.
* - CARRY_ON_TOP: si el Player está de pie encima, aplica last_delta_.x.
* - ONEWAY_TOP: solo bloquea desde arriba (jump-through desde abajo).
* - KILLS_ON_CRUSH: (futuro) si aplasta al Player contra tile sólido, mata.
*/
class SolidActor {
public:
enum Flags : uint32_t {
BLOCKS_PLAYER = 1U << 0U,
CARRY_ON_TOP = 1U << 1U,
ONEWAY_TOP = 1U << 2U,
KILLS_ON_CRUSH = 1U << 3U,
};
SolidActor() = default;
SolidActor(const SolidActor&) = delete;
auto operator=(const SolidActor&) -> SolidActor& = delete;
SolidActor(SolidActor&&) = delete;
auto operator=(SolidActor&&) -> SolidActor& = delete;
~SolidActor() = default;
[[nodiscard]] auto getAABB() const -> const SDL_FRect& { return aabb_; }
[[nodiscard]] auto getFlags() const -> uint32_t { return flags_; }
[[nodiscard]] auto getLastDelta() const -> SDL_FPoint { return last_delta_; }
[[nodiscard]] auto isBlocking() const -> bool { return (flags_ & BLOCKS_PLAYER) != 0U; }
[[nodiscard]] auto carriesOnTop() const -> bool { return (flags_ & CARRY_ON_TOP) != 0U; }
[[nodiscard]] auto isOneWayTop() const -> bool { return (flags_ & ONEWAY_TOP) != 0U; }
protected:
SDL_FRect aabb_{}; // Rectángulo de colisión (room-local pixel space)
SDL_FPoint last_delta_{}; // (dx, dy) del último frame — para carry horizontal
uint32_t flags_{0};
};