diff --git a/.gitignore b/.gitignore index 770bdc8..8972202 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ tools/pack_resources/pack_tool.exe *.res dist/ .claude/settings.local.json +_projecte_2026 diff --git a/CMakeLists.txt b/CMakeLists.txt index c4b4207..f649371 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -86,6 +86,7 @@ set(APP_SOURCES source/game/gameplay/key_manager.cpp source/game/gameplay/door_manager.cpp source/game/gameplay/platform_manager.cpp + source/game/gameplay/solid_actor_manager.cpp source/game/gameplay/item_tracker.cpp source/game/gameplay/key_tracker.cpp source/game/gameplay/door_tracker.cpp diff --git a/source/core/input/global_inputs.cpp b/source/core/input/global_inputs.cpp index 83fd1d7..e8537a1 100644 --- a/source/core/input/global_inputs.cpp +++ b/source/core/input/global_inputs.cpp @@ -12,11 +12,11 @@ #ifdef _DEBUG #include "core/system/debug.hpp" // Para Debug (persistencia de render_info en debug.yaml) #endif -#include "game/options.hpp" // Para Options, options, OptionsVideo, Section -#include "game/scene_manager.hpp" // Para SceneManager -#include "game/ui/console.hpp" // Para Console -#include "game/ui/notifier.hpp" // Para Notifier, NotificationText -#include "utils/utils.hpp" // Para stringInVector +#include "game/options.hpp" // Para Options, options, OptionsVideo, Section +#include "game/scene_manager.hpp" // Para SceneManager +#include "game/ui/console.hpp" // Para Console +#include "game/ui/notifier.hpp" // Para Notifier, NotificationText +#include "utils/utils.hpp" // Para stringInVector namespace GlobalInputs { diff --git a/source/core/system/debug.hpp b/source/core/system/debug.hpp index e97e9b9..fce0263 100644 --- a/source/core/system/debug.hpp +++ b/source/core/system/debug.hpp @@ -49,7 +49,7 @@ class Debug { void setInitialScene(SceneManager::Scene s) { initial_scene_ = s; } // Establece la escena inicial de debug [[nodiscard]] auto getLazyLoading() const -> bool { return lazy_loading_; } // Indica si el modo lazy de recursos está activo [[nodiscard]] auto getRenderInfoEnabled() const -> bool { return render_info_enabled_; } // Indica si el overlay RenderInfo arranca activo - void setRenderInfoEnabled(bool value); // Persiste el estado del overlay RenderInfo en debug.yaml + void setRenderInfoEnabled(bool value); // Persiste el estado del overlay RenderInfo en debug.yaml private: static Debug* debug; // [SINGLETON] Objeto privado diff --git a/source/game/editor/map_editor.cpp b/source/game/editor/map_editor.cpp index 6faee10..e9b6aab 100644 --- a/source/game/editor/map_editor.cpp +++ b/source/game/editor/map_editor.cpp @@ -750,16 +750,12 @@ auto MapEditor::commitEntityDrag() -> bool { break; case EntityType::DOOR: if (IDX >= 0 && IDX < static_cast(room_data_.doors.size())) { - // Truco crítico: durante el drag, moveEntityVisual movió el sprite - // pero los WALLs del CollisionMap siguen en la posición antigua. Antes - // de llamar a moveDoor (que limpia los tiles "actuales" y escribe los - // nuevos), restauramos el sprite a su posición vieja para que coincida - // con los tiles. moveDoor luego hace el ciclo limpio y completo. - auto* door_mgr = room_->getDoorManager(); - door_mgr->getDoor(IDX)->setPosition(room_data_.doors[IDX].x, room_data_.doors[IDX].y); + // Con el SolidActorManager, moveDoor solo reposiciona el AABB + // del Door (no hay tiles que sincronizar). moveEntityVisual ya + // movió el sprite durante el drag; moveDoor hace el ajuste final. room_data_.doors[IDX].x = drag_.snap_x; room_data_.doors[IDX].y = drag_.snap_y; - door_mgr->moveDoor(IDX, drag_.snap_x, drag_.snap_y); + room_->getDoorManager()->moveDoor(IDX, drag_.snap_x, drag_.snap_y); selection_ = {EntityType::DOOR, IDX}; return true; } @@ -2503,11 +2499,12 @@ auto MapEditor::duplicateKey() -> std::string { // (id, animation) requiere recrear el Door y mantener los tiles sincronizados. void MapEditor::rebuildDoors() { auto* door_mgr = room_->getDoorManager(); - // Borrar una a una desde el principio: cada removeDoor limpia sus WALLs + // Borrar una a una desde el principio: cada removeDoor desregistra el + // Door del SolidActorManager. while (door_mgr->getCount() > 0) { door_mgr->removeDoor(0); } - // Re-añadir desde room_data_; addDoor reescribe los WALLs si bloquean + // Re-añadir desde room_data_; addDoor las registra como SolidActor. for (const auto& d : room_data_.doors) { door_mgr->addDoor(std::make_shared(d, /*start_opened=*/false)); } @@ -2584,7 +2581,7 @@ auto MapEditor::deleteDoor() -> std::string { const int IDX = selection_.index; - // Importante: usar removeDoor del manager (limpia los WALLs antes de borrar) + // Importante: usar removeDoor del manager (desregistra del SolidActorManager) room_->getDoorManager()->removeDoor(IDX); room_data_.doors.erase(room_data_.doors.begin() + IDX); diff --git a/source/game/entities/door.cpp b/source/game/entities/door.cpp index 22c66e5..4d69eff 100644 --- a/source/game/entities/door.cpp +++ b/source/game/entities/door.cpp @@ -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(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 diff --git a/source/game/entities/door.hpp b/source/game/entities/door.hpp index e9942cb..92ce877 100644 --- a/source/game/entities/door.hpp +++ b/source/game/entities/door.hpp @@ -5,6 +5,8 @@ #include // Para shared_ptr #include // 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 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 diff --git a/source/game/entities/moving_platform.cpp b/source/game/entities/moving_platform.cpp index 00c53dd..9cbeb04 100644 --- a/source/game/entities/moving_platform.cpp +++ b/source/game/entities/moving_platform.cpp @@ -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_; -} diff --git a/source/game/entities/moving_platform.hpp b/source/game/entities/moving_platform.hpp index 6dee827..b082c42 100644 --- a/source/game/entities/moving_platform.hpp +++ b/source/game/entities/moving_platform.hpp @@ -6,6 +6,8 @@ #include // Para string #include // 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 sprite_; - SDL_FRect collider_{}; - float last_dx_{0.0F}; - float last_dy_{0.0F}; // Estado del path std::vector path_; diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 7d4a543..f9637ff 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -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(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_); } diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index 3b4c888..1f396d4 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -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_ = 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(); diff --git a/source/game/entities/solid_actor.hpp b/source/game/entities/solid_actor.hpp new file mode 100644 index 0000000..cdc212f --- /dev/null +++ b/source/game/entities/solid_actor.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include + +/** + * @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}; +}; diff --git a/source/game/gameplay/door_manager.cpp b/source/game/gameplay/door_manager.cpp index 48dd3af..1482e0f 100644 --- a/source/game/gameplay/door_manager.cpp +++ b/source/game/gameplay/door_manager.cpp @@ -2,32 +2,31 @@ #include // Para std::move -#include "game/entities/door.hpp" // Para Door -#include "game/gameplay/collision_map.hpp" // Para CollisionMap -#include "game/gameplay/door_tracker.hpp" // Para DoorTracker -#include "game/gameplay/inventory.hpp" // Para Inventory -#include "game/gameplay/tile_collider.hpp" // Para TileCollider::Tile -#include "utils/defines.hpp" // Para Map::WIDTH, Tile::SIZE -#include "utils/utils.hpp" // Para checkCollision +#include "game/entities/door.hpp" // Para Door +#include "game/gameplay/door_tracker.hpp" // Para DoorTracker +#include "game/gameplay/inventory.hpp" // Para Inventory +#include "game/gameplay/solid_actor_manager.hpp" // Para SolidActorManager +#include "utils/utils.hpp" // Para checkCollision // Constructor -DoorManager::DoorManager(std::string room_id, CollisionMap* collision_map) +DoorManager::DoorManager(std::string room_id, SolidActorManager* solid_actors) : room_id_(std::move(room_id)), - collision_map_(collision_map) { + solid_actors_(solid_actors) { } -// Añade una puerta. Si está bloqueante (CLOSED), pinta sus 4 tiles como WALL -// en el CollisionMap. Si ya está OPENED (porque venía persistida del -// DoorTracker), no se tocan los tiles. +// Añade una puerta y la registra en el SolidActorManager. El bit +// BLOCKS_PLAYER del propio Door determina si bloquea al Player (se setea en +// el constructor si la puerta no arranca ya abierta desde el DoorTracker). void DoorManager::addDoor(std::shared_ptr door) { // NOLINT(readability-identifier-naming) - if (door->isBlocking()) { - writeDoorTiles(*door, static_cast(TileCollider::Tile::WALL)); - } + solid_actors_->registerActor(door.get()); doors_.push_back(std::move(door)); } -// Elimina todas las puertas +// Elimina todas las puertas y las desregistra del SolidActorManager void DoorManager::clear() { + for (const auto& door : doors_) { + solid_actors_->unregisterActor(door.get()); + } doors_.clear(); } @@ -36,9 +35,10 @@ void DoorManager::update(float delta_time) { for (const auto& door : doors_) { door->update(delta_time); - // Si la puerta acaba de transicionar a OPENED, liberar los tiles y persistir + // Si la puerta acaba de transicionar a OPENED, persistir en DoorTracker. + // El flag BLOCKS_PLAYER ya se limpió dentro de Door::update(), así que + // el sweep del SolidActorManager deja de verla como muro automáticamente. if (door->justOpened()) { - writeDoorTiles(*door, static_cast(TileCollider::Tile::EMPTY)); DoorTracker::get()->addDoor(room_id_, door->getPos()); } } @@ -79,48 +79,17 @@ void DoorManager::tryUnlock(const SDL_FRect& player_rect) { } #ifdef _DEBUG -// Mueve una puerta del editor: limpia los WALLs viejos, reposiciona el sprite, -// y reescribe los WALLs nuevos si la puerta sigue siendo bloqueante. +// Mueve una puerta del editor: reposiciona su sprite y AABB. El registro +// en el SolidActorManager se mantiene (el manager lee el AABB actual). void DoorManager::moveDoor(int index, float x, float y) { if (index < 0 || index >= static_cast(doors_.size())) { return; } - auto& door = doors_[index]; - - // Limpiar los WALLs viejos antes de mover - if (door->isBlocking()) { - writeDoorTiles(*door, static_cast(TileCollider::Tile::EMPTY)); - } - - // Reposicionar el sprite y el collider del Door - door->setPosition(x, y); - - // Re-escribir los WALLs nuevos en la nueva posición si sigue siendo bloqueante - if (door->isBlocking()) { - writeDoorTiles(*door, static_cast(TileCollider::Tile::WALL)); - } + doors_[index]->setPosition(x, y); } -// Elimina una puerta del editor, limpiando los WALLs antes de borrarla del vector +// Elimina una puerta del editor, desregistrándola del SolidActorManager void DoorManager::removeDoor(int index) { if (index < 0 || index >= static_cast(doors_.size())) { return; } - auto& door = doors_[index]; - - if (door->isBlocking()) { - writeDoorTiles(*door, static_cast(TileCollider::Tile::EMPTY)); - } - + solid_actors_->unregisterActor(doors_[index].get()); doors_.erase(doors_.begin() + index); } #endif - -// Setea las 4 celdas que ocupa la puerta (1 col × 4 filas) al valor indicado -void DoorManager::writeDoorTiles(const Door& door, int tile_value) { - // Convertir posición en píxeles a coordenadas de tile - const SDL_FPoint POS = door.getPos(); - const int COL = static_cast(POS.x) / Tile::SIZE; - const int ROW = static_cast(POS.y) / Tile::SIZE; - - for (int i = 0; i < DOOR_TILES_HEIGHT; ++i) { - const int INDEX = ((ROW + i) * Map::WIDTH) + COL; - collision_map_->setCollisionTile(INDEX, tile_value); - } -} diff --git a/source/game/gameplay/door_manager.hpp b/source/game/gameplay/door_manager.hpp index ff5e604..0894f42 100644 --- a/source/game/gameplay/door_manager.hpp +++ b/source/game/gameplay/door_manager.hpp @@ -8,7 +8,7 @@ #include "game/entities/door.hpp" // Para Door, Door::Data -class CollisionMap; +class SolidActorManager; /** * @brief Gestor de puertas de una habitación @@ -18,14 +18,14 @@ class CollisionMap; * - Actualizar y renderizar todas las puertas * - Detectar contacto del jugador con puertas cerradas y disparar la apertura * si tiene la llave correspondiente (consultando el Inventory global) - * - Sincronizar el estado bloqueante con el CollisionMap: cuando una puerta - * está CLOSED u OPENING, sus 4 tiles son WALL; cuando pasa a OPENED, se - * ponen a EMPTY + * - Registrar cada puerta en el SolidActorManager para que sus AABBs + * participen en los sweeps de colisión del Player. El bit BLOCKS_PLAYER + * del propio Door se encarga de activar/desactivar el bloqueo al abrir. * - Persistir el estado abierto en DoorTracker */ class DoorManager { public: - DoorManager(std::string room_id, CollisionMap* collision_map); + DoorManager(std::string room_id, SolidActorManager* solid_actors); ~DoorManager() = default; // Prohibir copia y movimiento para evitar duplicación accidental @@ -35,7 +35,7 @@ class DoorManager { auto operator=(DoorManager&&) -> DoorManager& = delete; // Gestión de puertas - void addDoor(std::shared_ptr door); // Añade una puerta y aplica WALLs si está cerrada + void addDoor(std::shared_ptr door); // Añade una puerta y la registra en el SolidActorManager void clear(); // Elimina todas las puertas // Actualización y renderizado @@ -66,24 +66,20 @@ class DoorManager { /** * @brief Mueve la puerta indicada a (x, y) en píxeles * - * Limpia los WALLs viejos del CollisionMap y, si la puerta sigue siendo - * bloqueante, escribe los nuevos. Encapsula el bookkeeping de tiles para - * que el editor nunca toque el CollisionMap directamente. + * Reposiciona el sprite y el AABB de la puerta. El registro en el + * SolidActorManager no cambia: el manager siempre lee el AABB actual + * del SolidActor durante los sweeps. */ void moveDoor(int index, float x, float y); /** - * @brief Elimina la puerta indicada del vector y limpia sus WALLs del CollisionMap + * @brief Elimina la puerta indicada del vector y la desregistra del SolidActorManager */ void removeDoor(int index); #endif private: - static constexpr int DOOR_TILES_HEIGHT = 4; // Una puerta ocupa 4 tiles verticalmente - - void writeDoorTiles(const Door& door, int tile_value); // Setea las 4 celdas en el CollisionMap - std::vector> doors_; // Colección de puertas std::string room_id_; // Identificador de la habitación - CollisionMap* collision_map_; // Referencia no propietaria al CollisionMap de la Room + SolidActorManager* solid_actors_; // Referencia no propietaria al SolidActorManager de la Room }; diff --git a/source/game/gameplay/platform_manager.cpp b/source/game/gameplay/platform_manager.cpp index 1bc4b15..d867244 100644 --- a/source/game/gameplay/platform_manager.cpp +++ b/source/game/gameplay/platform_manager.cpp @@ -1,14 +1,26 @@ #include "platform_manager.hpp" -#include "game/entities/moving_platform.hpp" // Para MovingPlatform +#include "game/entities/moving_platform.hpp" // Para MovingPlatform +#include "game/gameplay/solid_actor_manager.hpp" // Para SolidActorManager -// Añade una plataforma a la colección +// Constructor: recibe el SolidActorManager para registrar las plataformas +// como SolidActors (flags CARRY_ON_TOP | ONEWAY_TOP) y que los sweeps del +// Player las detecten en moveVertical/checkFalling. +PlatformManager::PlatformManager(SolidActorManager* solid_actors) + : solid_actors_(solid_actors) { +} + +// Añade una plataforma a la colección y la registra en el SolidActorManager void PlatformManager::addPlatform(std::shared_ptr platform) { + solid_actors_->registerActor(platform.get()); platforms_.push_back(std::move(platform)); } -// Elimina todas las plataformas +// Elimina todas las plataformas y las desregistra del SolidActorManager void PlatformManager::clear() { + for (const auto& platform : platforms_) { + solid_actors_->unregisterActor(platform.get()); + } platforms_.clear(); } @@ -57,40 +69,11 @@ auto PlatformManager::getPlatform(int index) -> std::shared_ptr& return platforms_.at(index); } -// Elimina la última plataforma +// Elimina la última plataforma (y la desregistra del SolidActorManager) void PlatformManager::removeLastPlatform() { if (!platforms_.empty()) { + solid_actors_->unregisterActor(platforms_.back().get()); platforms_.pop_back(); } } #endif - -// Comprueba si el jugador está sobre alguna plataforma -// Devuelve puntero a la plataforma o nullptr si no está sobre ninguna -auto PlatformManager::checkPlayerOnPlatform(const SDL_FRect& player_collider, float player_vy) -> MovingPlatform* { - // Solo detectamos si el jugador está cayendo o quieto (no saltando hacia arriba) - if (player_vy < 0.0F) { - return nullptr; - } - - for (const auto& platform : platforms_) { - SDL_FRect plat_rect = platform->getCollider(); - - // Comprobar overlap horizontal - if (player_collider.x + player_collider.w <= plat_rect.x) { continue; } - if (player_collider.x >= plat_rect.x + plat_rect.w) { continue; } - - // Comprobar que los pies del jugador están cerca del top de la plataforma - float player_feet = player_collider.y + player_collider.h; - float platform_top = plat_rect.y; - - // Tolerancia bidireccional de 4px para compensar el movimiento entre frames - // (cuando la plataforma baja, los pies quedan por encima del top momentáneamente) - constexpr float TOLERANCE = 4.0F; - if (player_feet >= platform_top - TOLERANCE && player_feet <= platform_top + TOLERANCE) { - return platform.get(); - } - } - - return nullptr; -} diff --git a/source/game/gameplay/platform_manager.hpp b/source/game/gameplay/platform_manager.hpp index 2c48368..a8184de 100644 --- a/source/game/gameplay/platform_manager.hpp +++ b/source/game/gameplay/platform_manager.hpp @@ -7,9 +7,11 @@ #include "game/entities/moving_platform.hpp" // Para MovingPlatform, MovingPlatform::Data +class SolidActorManager; + class PlatformManager { public: - PlatformManager() = default; + explicit PlatformManager(SolidActorManager* solid_actors); ~PlatformManager() = default; // Prohibir copia y movimiento @@ -27,10 +29,6 @@ class PlatformManager { void update(float delta_time); void render(); - // Detección de plataforma bajo el jugador - // Devuelve puntero a la plataforma sobre la que está el jugador, o nullptr - auto checkPlayerOnPlatform(const SDL_FRect& player_collider, float player_vy) -> MovingPlatform*; - #ifdef _DEBUG void updateAnimations(float delta_time); void resetPositions(const std::vector& platform_data); @@ -41,4 +39,5 @@ class PlatformManager { private: std::vector> platforms_; + SolidActorManager* solid_actors_{nullptr}; // Referencia no propietaria al SolidActorManager de la Room }; diff --git a/source/game/gameplay/room.cpp b/source/game/gameplay/room.cpp index 13bf605..4e6ac71 100644 --- a/source/game/gameplay/room.cpp +++ b/source/game/gameplay/room.cpp @@ -2,22 +2,23 @@ #include // Para std::move -#include "core/rendering/surface.hpp" // Para Surface -#include "core/resources/resource_cache.hpp" // Para Resource -#include "game/defaults.hpp" // Para Defaults::Game -#include "game/gameplay/collision_map.hpp" // Para CollisionMap -#include "game/gameplay/door_manager.hpp" // Para DoorManager -#include "game/gameplay/door_tracker.hpp" // Para DoorTracker -#include "game/gameplay/enemy_manager.hpp" // Para EnemyManager -#include "game/gameplay/item_manager.hpp" // Para ItemManager -#include "game/gameplay/item_tracker.hpp" // Para ItemTracker -#include "game/gameplay/key_manager.hpp" // Para KeyManager -#include "game/gameplay/key_tracker.hpp" // Para KeyTracker -#include "game/gameplay/platform_manager.hpp" // Para PlatformManager -#include "game/gameplay/room_format.hpp" // Para RoomFormat -#include "game/gameplay/scoreboard.hpp" // Para Scoreboard::Data -#include "game/gameplay/tilemap_renderer.hpp" // Para TilemapRenderer -#include "utils/defines.hpp" // Para TILE_SIZE +#include "core/rendering/surface.hpp" // Para Surface +#include "core/resources/resource_cache.hpp" // Para Resource +#include "game/defaults.hpp" // Para Defaults::Game +#include "game/gameplay/collision_map.hpp" // Para CollisionMap +#include "game/gameplay/door_manager.hpp" // Para DoorManager +#include "game/gameplay/door_tracker.hpp" // Para DoorTracker +#include "game/gameplay/enemy_manager.hpp" // Para EnemyManager +#include "game/gameplay/item_manager.hpp" // Para ItemManager +#include "game/gameplay/item_tracker.hpp" // Para ItemTracker +#include "game/gameplay/key_manager.hpp" // Para KeyManager +#include "game/gameplay/key_tracker.hpp" // Para KeyTracker +#include "game/gameplay/platform_manager.hpp" // Para PlatformManager +#include "game/gameplay/room_format.hpp" // Para RoomFormat +#include "game/gameplay/scoreboard.hpp" // Para Scoreboard::Data +#include "game/gameplay/solid_actor_manager.hpp" // Para SolidActorManager +#include "game/gameplay/tilemap_renderer.hpp" // Para TilemapRenderer +#include "utils/defines.hpp" // Para TILE_SIZE #include "utils/utils.hpp" // Constructor @@ -25,18 +26,24 @@ Room::Room(const std::string& room_path, std::shared_ptr data) : data_(std::move(data)) { auto room = Resource::Cache::get()->getRoom(room_path); + // Gestor de actores sólidos dinámicos (AABBs): puertas, plataformas móviles, + // y otros actores que participan en la resolución de colisión del Player + // sin escribir en el tilemap. Debe ir antes que DoorManager/PlatformManager + // para poder pasarles su puntero. + solid_actor_manager_ = std::make_unique(); + // Crea los managers de enemigos, items, plataformas y llaves enemy_manager_ = std::make_unique(); item_manager_ = std::make_unique(room->number, data_); - platform_manager_ = std::make_unique(); + platform_manager_ = std::make_unique(solid_actor_manager_.get()); key_manager_ = std::make_unique(room->number); - // Crea el mapa de colisiones desde el collision_tile_map (debe ir antes - // del DoorManager porque éste lo necesita para mutar tiles dinámicamente) + // Crea el mapa de colisiones desde el collision_tile_map collision_map_ = std::make_unique(room->collision_tile_map); - // Crea el manager de puertas (necesita el CollisionMap para sincronizar muros) - door_manager_ = std::make_unique(room->number, collision_map_.get()); + // Crea el manager de puertas (registra los Door como SolidActor en el + // SolidActorManager; ya no escribe tiles en el CollisionMap). + door_manager_ = std::make_unique(room->number, solid_actor_manager_.get()); initializeRoom(*room); @@ -250,6 +257,14 @@ auto Room::getTileCollider() const -> const TileCollider& { return collision_map_->getTileCollider(); } +auto Room::getSolidActors() const -> const SolidActorManager& { + return *solid_actor_manager_; +} + +auto Room::getSolidActors() -> SolidActorManager& { + return *solid_actor_manager_; +} + auto Room::getCollisionTileMap() const -> const std::vector& { return collision_map_->getCollisionTileMap(); } @@ -258,6 +273,10 @@ void Room::updateCollisionBorders(const CollisionMap::AdjacentData& adjacent) { collision_map_->updateBorders(adjacent); } +void Room::updateSolidActorBorders(const SolidActorManager::AdjacentActors& adjacent) { + solid_actor_manager_->setAdjacent(adjacent); +} + // Devuelve la cadena del fichero de la habitación contigua segun el borde auto Room::getRoom(Border border) -> std::string { // NOLINT(readability-convert-member-functions-to-static) switch (border) { @@ -290,10 +309,6 @@ void Room::tryUnlockDoors(const SDL_FRect& player_rect) { door_manager_->tryUnlock(player_rect); } -auto Room::checkPlayerOnPlatform(const SDL_FRect& player_collider, float player_vy) -> MovingPlatform* { - return platform_manager_->checkPlayerOnPlatform(player_collider, player_vy); -} - // Carga una habitación desde un archivo YAML (delegado a RoomFormat) auto Room::loadYAML(const std::string& file_path, bool verbose) -> Data { // NOLINT(readability-convert-member-functions-to-static) return RoomFormat::loadYAML(file_path, verbose); diff --git a/source/game/gameplay/room.hpp b/source/game/gameplay/room.hpp index 0a2c4d5..f9e431b 100644 --- a/source/game/gameplay/room.hpp +++ b/source/game/gameplay/room.hpp @@ -6,15 +6,16 @@ #include // Para string #include // Para vector -#include "game/defaults.hpp" // Para Defaults::Game::Room -#include "game/entities/door.hpp" // Para Door::Data -#include "game/entities/enemy.hpp" // Para EnemyData -#include "game/entities/item.hpp" // Para ItemData -#include "game/entities/key.hpp" // Para Key::Data -#include "game/entities/moving_platform.hpp" // Para MovingPlatform::Data -#include "game/gameplay/collision_map.hpp" // Para CollisionMap::AdjacentData -#include "game/gameplay/scoreboard.hpp" // Para Scoreboard::Data -#include "utils/defines.hpp" // Para Tile::SIZE, Map::WIDTH, Map::HEIGHT +#include "game/defaults.hpp" // Para Defaults::Game::Room +#include "game/entities/door.hpp" // Para Door::Data +#include "game/entities/enemy.hpp" // Para EnemyData +#include "game/entities/item.hpp" // Para ItemData +#include "game/entities/key.hpp" // Para Key::Data +#include "game/entities/moving_platform.hpp" // Para MovingPlatform::Data +#include "game/gameplay/collision_map.hpp" // Para CollisionMap::AdjacentData +#include "game/gameplay/scoreboard.hpp" // Para Scoreboard::Data +#include "game/gameplay/solid_actor_manager.hpp" // Para SolidActorManager::AdjacentActors +#include "utils/defines.hpp" // Para Tile::SIZE, Map::WIDTH, Map::HEIGHT class Surface; class EnemyManager; class ItemManager; @@ -82,6 +83,7 @@ class Room { auto getPlatformManager() -> PlatformManager* { return platform_manager_.get(); } auto getKeyManager() -> KeyManager* { return key_manager_.get(); } auto getDoorManager() -> DoorManager* { return door_manager_.get(); } + auto getSolidActorManager() -> SolidActorManager* { return solid_actor_manager_.get(); } void setTile(int index, int tile_value); void setCollisionTile(int index, int value); void setConnection(Border border, const std::string& room_name); @@ -95,11 +97,13 @@ class Room { auto itemCollision(SDL_FRect& rect) -> bool; auto keyCollision(SDL_FRect& rect) -> bool; void tryUnlockDoors(const SDL_FRect& player_rect); - auto checkPlayerOnPlatform(const SDL_FRect& player_collider, float player_vy) -> MovingPlatform*; void setPaused(bool value); [[nodiscard]] auto getTileCollider() const -> const TileCollider&; + [[nodiscard]] auto getSolidActors() const -> const SolidActorManager&; + [[nodiscard]] auto getSolidActors() -> SolidActorManager&; [[nodiscard]] auto getCollisionTileMap() const -> const std::vector&; void updateCollisionBorders(const CollisionMap::AdjacentData& adjacent); + void updateSolidActorBorders(const SolidActorManager::AdjacentActors& adjacent); // Método de carga de archivos YAML (delegado a RoomFormat) static auto loadYAML(const std::string& file_path, bool verbose = false) -> Data; @@ -114,6 +118,7 @@ class Room { std::unique_ptr platform_manager_; std::unique_ptr key_manager_; std::unique_ptr door_manager_; + std::unique_ptr solid_actor_manager_; std::unique_ptr collision_map_; std::unique_ptr tilemap_renderer_; std::shared_ptr surface_; diff --git a/source/game/gameplay/solid_actor_manager.cpp b/source/game/gameplay/solid_actor_manager.cpp new file mode 100644 index 0000000..f6a8e20 --- /dev/null +++ b/source/game/gameplay/solid_actor_manager.cpp @@ -0,0 +1,197 @@ +#include "solid_actor_manager.hpp" + +#include // Para std::ranges::find, std::max, std::min + +#include "game/entities/solid_actor.hpp" // Para SolidActor +#include "utils/defines.hpp" // Para Collision::NONE, PlayArea::WIDTH/HEIGHT + +// Registro de actores +void SolidActorManager::registerActor(SolidActor* actor) { + if (actor == nullptr) { return; } + actors_.push_back(actor); +} + +// Desregistro (no borra el objeto — la entidad concreta vive en su propio manager) +void SolidActorManager::unregisterActor(SolidActor* actor) { + auto it = std::ranges::find(actors_, actor); + if (it != actors_.end()) { + actors_.erase(it); + } +} + +void SolidActorManager::clear() { + actors_.clear(); +} + +void SolidActorManager::setAdjacent(const AdjacentActors& adjacent) { + adjacent_ = adjacent; +} + +void SolidActorManager::clearAdjacent() { + adjacent_ = {}; +} + +// Itera sobre todos los actores (local + 4 vecinos cardinales), aplicando +// el offset pertinente al AABB vecino para traerlo al sistema de coordenadas +// de la room actual. Las rooms vecinas tienen sus actores en coordenadas +// locales propias; para verlos desde aquí, se traslada: +// - left: +WIDTH en X (porque en la room actual están a la izquierda del +// borde 0 de coordenadas; trasladándolos por -WIDTH los colocamos "a la +// izquierda" del Player, con x negativas). +// - right: -WIDTH en X (análogo). +// - upper: +HEIGHT en Y. +// - lower: -HEIGHT en Y. +// +// NOTA: el Player siempre está en [0, WIDTH) × [0, HEIGHT) de la room actual. +// Los actores vecinos se muestran con coordenadas fuera de ese rango, y los +// sweeps solo los ven cuando el Player está lo bastante cerca del borde como +// para solapar con ellos. +template +void SolidActorManager::forEachActor(Fn&& fn) const { + // Locales + for (auto* a : actors_) { + fn(a, a->getAABB()); + } + + // Adyacente izquierda: sus actores están en [0, WIDTH) de SU room; + // desde la room actual, esos actores están en [-WIDTH, 0). Offset: -WIDTH en X. + if (adjacent_.left != nullptr) { + for (auto* a : adjacent_.left->actors_) { + SDL_FRect r = a->getAABB(); + r.x -= PlayArea::WIDTH; + fn(a, r); + } + } + // Adyacente derecha: offset +WIDTH en X. + if (adjacent_.right != nullptr) { + for (auto* a : adjacent_.right->actors_) { + SDL_FRect r = a->getAABB(); + r.x += PlayArea::WIDTH; + fn(a, r); + } + } + // Adyacente superior: offset -HEIGHT en Y (arriba tiene y negativa). + if (adjacent_.upper != nullptr) { + for (auto* a : adjacent_.upper->actors_) { + SDL_FRect r = a->getAABB(); + r.y -= PlayArea::HEIGHT; + fn(a, r); + } + } + // Adyacente inferior: offset +HEIGHT en Y. + if (adjacent_.lower != nullptr) { + for (auto* a : adjacent_.lower->actors_) { + SDL_FRect r = a->getAABB(); + r.y += PlayArea::HEIGHT; + fn(a, r); + } + } +} + +// Devuelve el borde derecho del actor bloqueante "a la izquierda" del Player. +// Criterio: el AABB del actor solapa el rect del Player y el borde izquierdo +// del actor está a la izquierda del borde izquierdo del Player (el Player se +// mete en él por la izquierda). +auto SolidActorManager::checkWallLeft(float px, float py, float pw, float ph) const -> float { + float result = Collision::NONE; + forEachActor([&](const SolidActor* a, const SDL_FRect& r) { + if (!a->isBlocking()) { return; } + if (a->isOneWayTop()) { return; } // Los jump-through no son pared lateral + // Y-overlap + if (py + ph <= r.y) { return; } + if (py >= r.y + r.h) { return; } + // X-overlap y "a la izquierda": el Player (px) está dentro del actor. + const float RIGHT_EDGE = r.x + r.w; + if (r.x < px && RIGHT_EDGE > px) { + if (result == Collision::NONE || RIGHT_EDGE > result) { + result = RIGHT_EDGE; + } + } + }); + return result; +} + +// Devuelve el borde izquierdo del actor bloqueante "a la derecha" del Player. +auto SolidActorManager::checkWallRight(float px, float py, float pw, float ph) const -> float { + float result = Collision::NONE; + forEachActor([&](const SolidActor* a, const SDL_FRect& r) { + if (!a->isBlocking()) { return; } + if (a->isOneWayTop()) { return; } + // Y-overlap + if (py + ph <= r.y) { return; } + if (py >= r.y + r.h) { return; } + // X-overlap y "a la derecha": el borde derecho del Player entra en el actor. + const float PLAYER_RIGHT = px + pw; + if (r.x < PLAYER_RIGHT && r.x + r.w > PLAYER_RIGHT) { + if (result == Collision::NONE || r.x < result) { + result = r.x; + } + } + }); + return result; +} + +// Techo: devuelve el borde inferior del actor bloqueante que está por encima +// del Player y solapa su parte superior. +auto SolidActorManager::checkCeiling(float px, float py, float pw) const -> float { + float result = Collision::NONE; + forEachActor([&](const SolidActor* a, const SDL_FRect& r) { + if (!a->isBlocking()) { return; } + if (a->isOneWayTop()) { return; } // Los jump-through no tienen techo + // X-overlap + if (px + pw <= r.x) { return; } + if (px >= r.x + r.w) { return; } + // Y: el actor debe estar por encima del Player (su bottom edge entre py y py+ph) + const float BOTTOM_EDGE = r.y + r.h; + if (r.y < py && BOTTOM_EDGE > py) { + if (result == Collision::NONE || BOTTOM_EDGE > result) { + result = BOTTOM_EDGE; + } + } + }); + return result; +} + +// Suelo: el actor más cercano (por arriba) cuyo top edge esté entre +// foot_y_cur y foot_y_new. Para ONEWAY_TOP, solo cuentan si foot_y_cur <= r.y. +auto SolidActorManager::checkFloor(float px, float foot_y_cur, float pw, float foot_y_new) const -> FloorHit { + FloorHit hit; + forEachActor([&](SolidActor* a, const SDL_FRect& r) { + // Un actor participa en el suelo si es bloqueante O si tiene CARRY_ON_TOP + // (las plataformas jump-through tienen CARRY_ON_TOP sin BLOCKS_PLAYER). + const bool PARTICIPATES = a->isBlocking() || a->carriesOnTop(); + if (!PARTICIPATES) { return; } + // X-overlap + if (px + pw <= r.x) { return; } + if (px >= r.x + r.w) { return; } + // Y: el top del actor debe estar en [foot_y_cur, foot_y_new] + if (r.y < foot_y_cur) { return; } // actor por encima: no es suelo, es techo + if (r.y > foot_y_new) { return; } // actor fuera del rango de caída + // ONEWAY_TOP: ya garantizado por foot_y_cur <= r.y (chequeo implícito arriba) + // Actualizar hit si es más alto (menor y) que el actual + if (hit.y == Collision::NONE || r.y < hit.y) { + hit.y = r.y; + hit.carrier = a->carriesOnTop() ? a : nullptr; + } + }); + return hit; +} + +// Hay suelo "inmediato" debajo (tolerancia 1 px). Usado por checkFalling para +// no desengancharse de plataformas móviles que bajan/suben pixel a pixel. +auto SolidActorManager::hasGroundBelow(float px, float foot_y, float pw) const -> bool { + bool found = false; + forEachActor([&](const SolidActor* a, const SDL_FRect& r) { + if (found) { return; } + const bool PARTICIPATES = a->isBlocking() || a->carriesOnTop(); + if (!PARTICIPATES) { return; } + // X-overlap + if (px + pw <= r.x) { return; } + if (px >= r.x + r.w) { return; } + // Top del actor dentro de [foot_y, foot_y + 1] + if (r.y >= foot_y && r.y <= foot_y + 1.0F) { + found = true; + } + }); + return found; +} diff --git a/source/game/gameplay/solid_actor_manager.hpp b/source/game/gameplay/solid_actor_manager.hpp new file mode 100644 index 0000000..06aff1a --- /dev/null +++ b/source/game/gameplay/solid_actor_manager.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include + +#include + +class SolidActor; + +/** + * @brief Gestor de actores sólidos dinámicos (AABB) de una habitación. + * + * Paralelo al TileCollider: proporciona sweeps axis-aligned del Player + * contra una lista de AABBs de entidades (puertas, plataformas móviles, + * bloques empujables, ascensores, etc). El manager NO es propietario de + * los actores — sus entidades concretas viven en sus respectivos managers + * (DoorManager, PlatformManager, ...). Este manager solo mantiene una + * lista de raw pointers para consultas de colisión. + * + * Cross-room: igual que CollisionMap::updateBorders, este manager acepta + * punteros a los managers de las rooms adyacentes (setAdjacent). Cuando + * el Player está cerca del borde, los sweeps iteran también sobre los + * actores vecinos, trasladando sus AABBs por ±PlayArea::WIDTH/HEIGHT + * para ponerlos en el sistema de coordenadas de la room actual. + */ +class SolidActorManager { + public: + struct FloorHit { + float y{-1}; + SolidActor* carrier{nullptr}; + }; + + struct AdjacentActors { + SolidActorManager* upper{nullptr}; + SolidActorManager* lower{nullptr}; + SolidActorManager* left{nullptr}; + SolidActorManager* right{nullptr}; + }; + + SolidActorManager() = default; + ~SolidActorManager() = default; + + SolidActorManager(const SolidActorManager&) = delete; + auto operator=(const SolidActorManager&) -> SolidActorManager& = delete; + SolidActorManager(SolidActorManager&&) = delete; + auto operator=(SolidActorManager&&) -> SolidActorManager& = delete; + + // Registro de actores (el manager no es propietario) + void registerActor(SolidActor* actor); + void unregisterActor(SolidActor* actor); + void clear(); + + // Cross-room: punteros a los managers de las rooms adyacentes + void setAdjacent(const AdjacentActors& adjacent); + void clearAdjacent(); + + // Sweeps del Player (paralelos a TileCollider). Todas las coordenadas + // están en el sistema de la room actual (room-local pixel space). + + /// Devuelve el x del borde derecho del primer actor bloqueante cuyo AABB + /// esté "a la izquierda" del Player y solape su rect (px, py, pw, ph). + /// Collision::NONE si no hay colisión. + [[nodiscard]] auto checkWallLeft(float px, float py, float pw, float ph) const -> float; + + /// Devuelve el x del borde izquierdo del primer actor bloqueante cuyo AABB + /// esté "a la derecha" del Player y solape su rect. Collision::NONE si no. + [[nodiscard]] auto checkWallRight(float px, float py, float pw, float ph) const -> float; + + /// Devuelve el y del borde inferior del primer techo bloqueante que solape + /// la parte superior del Player a (px, py, pw). Collision::NONE si no. + [[nodiscard]] auto checkCeiling(float px, float py, float pw) const -> float; + + /// Devuelve el suelo más cercano debajo de los pies del Player dentro del + /// rango vertical [foot_y_cur, foot_y_new]. Los actores ONEWAY_TOP solo + /// colisionan si foot_y_cur <= r.y (el Player venía desde arriba). + /// Si el actor tiene CARRY_ON_TOP, FloorHit.carrier apunta a él. + [[nodiscard]] auto checkFloor(float px, float foot_y_cur, float pw, float foot_y_new) const -> FloorHit; + + /// Devuelve true si hay algún actor con top justo debajo de los pies + /// (dentro de 1 px). Usado por checkFalling para no desengancharse de + /// plataformas móviles. + [[nodiscard]] auto hasGroundBelow(float px, float foot_y, float pw) const -> bool; + + private: + // Itera sobre todos los actores relevantes (locales + vecinos) aplicando + // el offset correcto a los AABBs vecinos. La lambda recibe (actor, aabb_world). + template + void forEachActor(Fn&& fn) const; + + std::vector actors_; + AdjacentActors adjacent_{}; +}; diff --git a/source/game/scenes/game.cpp b/source/game/scenes/game.cpp index f7691fe..f19bbf7 100644 --- a/source/game/scenes/game.cpp +++ b/source/game/scenes/game.cpp @@ -319,9 +319,6 @@ void Game::updatePlaying(float delta_time) { updateAdjacentRooms(delta_time); switch (mode_) { case Mode::GAME: - // Plataformas: resetear flag y detectar antes de la física del player - player_->clearPlatformFlag(); - checkPlayerAndPlatforms(); #ifdef _DEBUG // Maneja el arrastre del jugador con el ratón (debug) @@ -858,6 +855,24 @@ void Game::buildCollisionBorders() { adj.bottom_right = getDiagCollision(Room::Border::BOTTOM, Room::Border::RIGHT); room_->updateCollisionBorders(adj); + + // Además del tilemap extendido, propagar también los punteros a los + // SolidActorManager de las rooms cardinales adyacentes. Esto permite + // que los sweeps del Player vean AABBs dinámicos (puertas, plataformas) + // de la room vecina cuando está cerca del borde, sin tener que esperar + // a una transición completa de room. + auto getAdjacentSolidActors = [&](Room::Border b) -> SolidActorManager* { + auto name = room_->getRoom(b); + if (name == "0") { return nullptr; } + return &getOrCreateRoom(name)->getSolidActors(); + }; + + SolidActorManager::AdjacentActors sadj; + sadj.upper = getAdjacentSolidActors(Room::Border::TOP); + sadj.lower = getAdjacentSolidActors(Room::Border::BOTTOM); + sadj.left = getAdjacentSolidActors(Room::Border::LEFT); + sadj.right = getAdjacentSolidActors(Room::Border::RIGHT); + room_->updateSolidActorBorders(sadj); } // Actualiza los enemigos de las habitaciones adyacentes a la actual @@ -950,14 +965,6 @@ auto Game::checkPlayerAndEnemies() -> bool { return DEATH; } -// Comprueba si el jugador está sobre una plataforma móvil y lo transporta -void Game::checkPlayerAndPlatforms() { - auto* platform = room_->checkPlayerOnPlatform(player_->getCollider(), player_->getVY()); - if (platform != nullptr) { - player_->applyPlatformDisplacement(platform->getLastDX(), platform->getCollider().y); - } -} - // Comprueba las colisiones del jugador con los objetos void Game::checkPlayerAndKeys() { room_->keyCollision(player_->getCollider()); diff --git a/source/game/scenes/game.hpp b/source/game/scenes/game.hpp index 5ffab51..55041fa 100644 --- a/source/game/scenes/game.hpp +++ b/source/game/scenes/game.hpp @@ -75,7 +75,6 @@ class Game { void handleInput(); // Comprueba el teclado void checkPlayerIsOnBorder(); // Comprueba si el jugador esta en el borde de la pantalla y actua auto checkPlayerAndEnemies() -> bool; // Comprueba las colisiones del jugador con los enemigos - void checkPlayerAndPlatforms(); // Comprueba si el jugador está sobre una plataforma móvil void checkPlayerAndItems(); // Comprueba las colisiones del jugador con los objetos void checkPlayerAndKeys(); // Comprueba las colisiones del jugador con las llaves void checkIfPlayerIsAlive(); // Comprueba si el jugador esta vivo