diff --git a/CMakeLists.txt b/CMakeLists.txt index d1144b6..fffae9e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,7 +67,9 @@ set(APP_SOURCES # Game - Entities source/game/entities/enemy.cpp + source/game/entities/path_enemy.cpp source/game/entities/item.cpp + source/game/entities/moving_platform.cpp source/game/entities/player.cpp # Game - Configuration @@ -79,6 +81,7 @@ set(APP_SOURCES source/game/gameplay/tile_collider.cpp source/game/gameplay/enemy_manager.cpp source/game/gameplay/item_manager.cpp + source/game/gameplay/platform_manager.cpp source/game/gameplay/item_tracker.cpp source/game/gameplay/room_loader.cpp source/game/gameplay/room_tracker.cpp diff --git a/data/room/02.yaml b/data/room/02.yaml index d0ebaa1..e3d5685 100644 --- a/data/room/02.yaml +++ b/data/room/02.yaml @@ -83,6 +83,16 @@ enemies: color: 10 flip: true +# Plataformas móviles en esta habitación +platforms: + - animation: bin.yaml + position: {x: 5, y: 17} + velocity: {x: 25, y: 0} + boundaries: + position1: {x: 3, y: 17} + position2: {x: 20, y: 17} + frame: 0 + # Objetos en esta habitación items: - tileSetFile: items.gif diff --git a/source/game/editor/map_editor.cpp b/source/game/editor/map_editor.cpp index f6c05d7..c762545 100644 --- a/source/game/editor/map_editor.cpp +++ b/source/game/editor/map_editor.cpp @@ -195,6 +195,7 @@ void MapEditor::enter(std::shared_ptr room, std::shared_ptr player yaml_backup_ = yaml_; // Copia profunda para revert } + bool is_reenter = reenter_; if (!reenter_) { // Solo guardar estado previo en el primer enter (no en re-enter tras cambio de room) invincible_before_editor_ = Options::cheats.invincible; @@ -219,13 +220,16 @@ void MapEditor::enter(std::shared_ptr room, std::shared_ptr player // Crear la barra de estado statusbar_ = std::make_unique(room_->getNumber()); - // Resetear estado + // Resetear estado (preservar modo de edición en re-enter) drag_ = {}; selected_enemy_ = -1; selected_item_ = -1; - brush_tile_ = NO_BRUSH; - painting_ = false; - editing_collision_ = false; + if (!is_reenter) { + brush_tile_ = NO_BRUSH; + painting_ = false; + editing_collision_ = false; + } + painting_ = false; // Siempre dejar de pintar al cambiar de room // Asegurar que collision_tile_map tiene el tamaño correcto if (room_data_.collision_tile_map.size() != static_cast(Map::WIDTH * Map::HEIGHT)) { @@ -1053,7 +1057,7 @@ auto MapEditor::setEnemyProperty(const std::string& property, const std::string& enemy.animation_path = anim; try { auto* enemy_mgr = room_->getEnemyManager(); - enemy_mgr->getEnemy(selected_enemy_) = std::make_shared(enemy); + enemy_mgr->getEnemy(selected_enemy_) = Enemy::create(enemy); } catch (const std::exception& e) { enemy.animation_path = old_anim; // Restaurar si falla return std::string("Error: ") + e.what(); @@ -1096,7 +1100,7 @@ auto MapEditor::setEnemyProperty(const std::string& property, const std::string& // Recrear el enemigo (flip/mirror se aplican en el constructor) try { auto* enemy_mgr = room_->getEnemyManager(); - enemy_mgr->getEnemy(selected_enemy_) = std::make_shared(enemy); + enemy_mgr->getEnemy(selected_enemy_) = Enemy::create(enemy); } catch (const std::exception& e) { return std::string("Error: ") + e.what(); } @@ -1112,7 +1116,7 @@ auto MapEditor::setEnemyProperty(const std::string& property, const std::string& // Recrear el enemigo (flip/mirror se aplican en el constructor) try { auto* enemy_mgr = room_->getEnemyManager(); - enemy_mgr->getEnemy(selected_enemy_) = std::make_shared(enemy); + enemy_mgr->getEnemy(selected_enemy_) = Enemy::create(enemy); } catch (const std::exception& e) { return std::string("Error: ") + e.what(); } @@ -1147,7 +1151,7 @@ auto MapEditor::addEnemy() -> std::string { // Añadir a los datos y crear el sprite vivo room_data_.enemies.push_back(new_enemy); - room_->getEnemyManager()->addEnemy(std::make_shared(new_enemy)); + room_->getEnemyManager()->addEnemy(Enemy::create(new_enemy)); // Seleccionar el nuevo enemigo selected_enemy_ = static_cast(room_data_.enemies.size()) - 1; @@ -1170,7 +1174,7 @@ auto MapEditor::deleteEnemy() -> std::string { auto* enemy_mgr = room_->getEnemyManager(); enemy_mgr->clear(); for (const auto& enemy_data : room_data_.enemies) { - enemy_mgr->addEnemy(std::make_shared(enemy_data)); + enemy_mgr->addEnemy(Enemy::create(enemy_data)); } selected_enemy_ = -1; @@ -1193,7 +1197,7 @@ auto MapEditor::duplicateEnemy() -> std::string { // Añadir y crear sprite vivo room_data_.enemies.push_back(copy); - room_->getEnemyManager()->addEnemy(std::make_shared(copy)); + room_->getEnemyManager()->addEnemy(Enemy::create(copy)); // Seleccionar el nuevo enemigo selected_enemy_ = static_cast(room_data_.enemies.size()) - 1; diff --git a/source/game/editor/room_saver.cpp b/source/game/editor/room_saver.cpp index b61cbf6..49ac06e 100644 --- a/source/game/editor/room_saver.cpp +++ b/source/game/editor/room_saver.cpp @@ -112,6 +112,7 @@ auto RoomSaver::buildYAML(const fkyaml::node& original_yaml, const Room::Data& r out << "enemies:\n"; for (const auto& enemy : room_data.enemies) { out << " - animation: " << enemy.animation_path << "\n"; + if (enemy.type != "path") { out << " type: " << enemy.type << "\n"; } int pos_x = static_cast(std::round(enemy.x / Tile::SIZE)); int pos_y = static_cast(std::round(enemy.y / Tile::SIZE)); @@ -155,6 +156,33 @@ auto RoomSaver::buildYAML(const fkyaml::node& original_yaml, const Room::Data& r } } + // --- Plataformas --- + if (!room_data.platforms.empty()) { + out << "# Plataformas móviles en esta habitación\n"; + out << "platforms:\n"; + for (const auto& plat : room_data.platforms) { + out << " - animation: " << plat.animation_path << "\n"; + + int pos_x = static_cast(std::round(plat.x / Tile::SIZE)); + int pos_y = static_cast(std::round(plat.y / Tile::SIZE)); + out << " position: {x: " << pos_x << ", y: " << pos_y << "}\n"; + + out << " velocity: {x: " << plat.vx << ", y: " << plat.vy << "}\n"; + + int b1_x = plat.x1 / Tile::SIZE; + int b1_y = plat.y1 / Tile::SIZE; + int b2_x = plat.x2 / Tile::SIZE; + int b2_y = plat.y2 / Tile::SIZE; + out << " boundaries:\n"; + out << " position1: {x: " << b1_x << ", y: " << b1_y << "}\n"; + out << " position2: {x: " << b2_x << ", y: " << b2_y << "}\n"; + + if (plat.frame != -1) { out << " frame: " << plat.frame << "\n"; } + + out << "\n"; + } + } + return out.str(); } diff --git a/source/game/entities/enemy.cpp b/source/game/entities/enemy.cpp index 6fc010e..ddc6e20 100644 --- a/source/game/entities/enemy.cpp +++ b/source/game/entities/enemy.cpp @@ -6,18 +6,13 @@ #include "core/rendering/sprite/animated_sprite.hpp" // Para SAnimatedSprite #include "core/resources/resource_cache.hpp" // Para Resource -#include "utils/utils.hpp" +#include "game/entities/path_enemy.hpp" // Para PathEnemy -// Constructor +// Constructor base: configura sprite, collider, flip/mirror Enemy::Enemy(const Data& enemy) : sprite_(std::make_shared(Resource::Cache::get()->getAnimationData(enemy.animation_path))), - x1_(enemy.x1), - x2_(enemy.x2), - y1_(enemy.y1), - y2_(enemy.y2), should_flip_(enemy.flip), should_mirror_(enemy.mirror) { - // Obten el resto de valores sprite_->setPosX(enemy.x); sprite_->setPosY(enemy.y); sprite_->setVelX(enemy.vx); @@ -36,74 +31,13 @@ void Enemy::render() { sprite_->render(); } -// Actualiza las variables del objeto -void Enemy::update(float delta_time) { - sprite_->update(delta_time); - checkPath(); - collider_ = getRect(); -} - #ifdef _DEBUG // Solo actualiza la animación sin mover al enemigo void Enemy::updateAnimation(float delta_time) { sprite_->animate(delta_time); } - -// Resetea el enemigo a su posición inicial (para editor) -void Enemy::resetToInitialPosition(const Data& data) { - sprite_->setPosX(data.x); - sprite_->setPosY(data.y); - sprite_->setVelX(data.vx); - sprite_->setVelY(data.vy); - - x1_ = data.x1; - x2_ = data.x2; - y1_ = data.y1; - y2_ = data.y2; - - applyFlipMirror(data.vx); - - collider_ = getRect(); -} #endif -// Comprueba si ha llegado al limite del recorrido para darse media vuelta -void Enemy::checkPath() { // NOLINT(readability-make-member-function-const) - if (sprite_->getPosX() > x2_ || sprite_->getPosX() < x1_) { - // Recoloca - if (sprite_->getPosX() > x2_) { - sprite_->setPosX(x2_); - } else { - sprite_->setPosX(x1_); - } - - // Cambia el sentido - sprite_->setVelX(sprite_->getVelX() * (-1)); - - // Invierte el sprite - if (should_flip_) { - sprite_->flip(); - } - } - - if (sprite_->getPosY() > y2_ || sprite_->getPosY() < y1_) { - // Recoloca - if (sprite_->getPosY() > y2_) { - sprite_->setPosY(y2_); - } else { - sprite_->setPosY(y1_); - } - - // Cambia el sentido - sprite_->setVelY(sprite_->getVelY() * (-1)); - - // Invierte el sprite - if (should_flip_) { - sprite_->flip(); - } - } -} - // Aplica flip horizontal y/o mirror vertical al sprite void Enemy::applyFlipMirror(float vx) { const int FLIP = (should_flip_ && vx < 0.0F) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE; @@ -119,4 +53,10 @@ auto Enemy::getRect() -> SDL_FRect { // Obtiene el rectangulo de colision del enemigo auto Enemy::getCollider() -> SDL_FRect& { return collider_; -} \ No newline at end of file +} + +// Factory: crea el subtipo de enemigo correcto según data.type +auto Enemy::create(const Data& data) -> std::shared_ptr { + // Por ahora solo existe PathEnemy; futuros tipos se añadirán aquí + return std::make_shared(data); +} diff --git a/source/game/entities/enemy.hpp b/source/game/entities/enemy.hpp index 60a5279..69bd103 100644 --- a/source/game/entities/enemy.hpp +++ b/source/game/entities/enemy.hpp @@ -10,6 +10,7 @@ class AnimatedSprite; class Enemy { public: struct Data { + std::string type{"path"}; // Tipo de enemigo (path, etc.) std::string animation_path; // Ruta al fichero con la animación float x{0.0F}; // Posición inicial en el eje X float y{0.0F}; // Posición inicial en el eje Y @@ -25,30 +26,26 @@ class Enemy { }; explicit Enemy(const Data& enemy); // Constructor - ~Enemy() = default; // Destructor + virtual ~Enemy() = default; // Destructor virtual - void render(); // Pinta el enemigo en pantalla - void update(float delta_time); // Actualiza las variables del objeto + virtual void render(); // Pinta el enemigo en pantalla + virtual void update(float delta_time) = 0; // Actualiza las variables del objeto (pura virtual) #ifdef _DEBUG - void updateAnimation(float delta_time); // Solo actualiza la animación sin mover al enemigo - void resetToInitialPosition(const Data& data); // Resetea el enemigo a su posición inicial (para editor) + virtual void updateAnimation(float delta_time); // Solo actualiza la animación sin mover al enemigo + virtual void resetToInitialPosition(const Data& data) = 0; // Resetea a posición inicial (pura virtual) #endif auto getRect() -> SDL_FRect; // Devuelve el rectangulo que contiene al enemigo auto getCollider() -> SDL_FRect&; // Obtiene el rectangulo de colision del enemigo - private: + // Factory: crea el subtipo de enemigo correcto según data.type + static auto create(const Data& data) -> std::shared_ptr; + + protected: void applyFlipMirror(float vx); // Aplica flip horizontal y/o mirror vertical al sprite - void checkPath(); // Comprueba si ha llegado al limite del recorrido para darse media vuelta std::shared_ptr sprite_; // Sprite del enemigo - - // Variables - int x1_{0}; // Limite izquierdo de la ruta en el eje X - int x2_{0}; // Limite derecho de la ruta en el eje X - int y1_{0}; // Limite superior de la ruta en el eje Y - int y2_{0}; // Limite inferior de la ruta en el eje Y - SDL_FRect collider_{}; // Caja de colisión - bool should_flip_{false}; // Indica si el enemigo hace flip al terminar su ruta - bool should_mirror_{false}; // Indica si el enemigo se dibuja volteado verticalmente + SDL_FRect collider_{}; // Caja de colisión + bool should_flip_{false}; // Indica si el enemigo hace flip al terminar su ruta + bool should_mirror_{false}; // Indica si el enemigo se dibuja volteado verticalmente }; diff --git a/source/game/entities/moving_platform.cpp b/source/game/entities/moving_platform.cpp new file mode 100644 index 0000000..0032f5a --- /dev/null +++ b/source/game/entities/moving_platform.cpp @@ -0,0 +1,96 @@ +#include "game/entities/moving_platform.hpp" + +#include // Para rand + +#include "core/rendering/sprite/animated_sprite.hpp" // Para AnimatedSprite +#include "core/resources/resource_cache.hpp" // Para Resource + +// Constructor +MovingPlatform::MovingPlatform(const Data& data) + : sprite_(std::make_shared(Resource::Cache::get()->getAnimationData(data.animation_path))), + x1_(data.x1), + x2_(data.x2), + y1_(data.y1), + y2_(data.y2) { + sprite_->setPosX(data.x); + sprite_->setPosY(data.y); + sprite_->setVelX(data.vx); + sprite_->setVelY(data.vy); + + collider_ = getRect(); + + // Coloca un frame al azar o el designado + sprite_->setCurrentAnimationFrame((data.frame == -1) ? (rand() % sprite_->getCurrentAnimationSize()) : data.frame); +} + +// Actualiza posición, calcula desplazamiento real del frame +void MovingPlatform::update(float delta_time) { + float old_x = sprite_->getPosX(); + float old_y = sprite_->getPosY(); + + sprite_->update(delta_time); + checkPath(); + + last_dx_ = sprite_->getPosX() - old_x; + last_dy_ = sprite_->getPosY() - old_y; + + collider_ = getRect(); +} + +// Pinta la plataforma en pantalla +void MovingPlatform::render() { + sprite_->render(); +} + +#ifdef _DEBUG +// Solo actualiza la animación sin mover la plataforma +void MovingPlatform::updateAnimation(float delta_time) { + sprite_->animate(delta_time); +} + +// Resetea la plataforma a su posición inicial (para editor) +void MovingPlatform::resetToInitialPosition(const Data& data) { + sprite_->setPosX(data.x); + sprite_->setPosY(data.y); + sprite_->setVelX(data.vx); + sprite_->setVelY(data.vy); + + x1_ = data.x1; + x2_ = data.x2; + y1_ = data.y1; + y2_ = data.y2; + + collider_ = getRect(); +} +#endif + +// Devuelve el rectangulo que contiene a la plataforma +auto MovingPlatform::getRect() -> SDL_FRect { + return sprite_->getRect(); +} + +// Obtiene el rectangulo de colisión +auto MovingPlatform::getCollider() -> SDL_FRect& { + return collider_; +} + +// Comprueba los límites del recorrido para invertir dirección +void MovingPlatform::checkPath() { // NOLINT(readability-make-member-function-const) + if (sprite_->getPosX() > x2_ || sprite_->getPosX() < x1_) { + if (sprite_->getPosX() > x2_) { + sprite_->setPosX(x2_); + } else { + sprite_->setPosX(x1_); + } + sprite_->setVelX(sprite_->getVelX() * (-1)); + } + + if (sprite_->getPosY() > y2_ || sprite_->getPosY() < y1_) { + if (sprite_->getPosY() > y2_) { + sprite_->setPosY(y2_); + } else { + sprite_->setPosY(y1_); + } + sprite_->setVelY(sprite_->getVelY() * (-1)); + } +} diff --git a/source/game/entities/moving_platform.hpp b/source/game/entities/moving_platform.hpp new file mode 100644 index 0000000..4f0bb42 --- /dev/null +++ b/source/game/entities/moving_platform.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include // Para shared_ptr +#include // Para string + +class AnimatedSprite; + +class MovingPlatform { + public: + struct Data { + std::string animation_path; // Ruta al fichero con la animación + float x{0.0F}; // Posición inicial en el eje X + float y{0.0F}; // Posición inicial en el eje Y + float vx{0.0F}; // Velocidad en el eje X + float vy{0.0F}; // Velocidad en el eje Y + int x1{0}; // Límite izquierdo de la ruta en el eje X + int x2{0}; // Límite derecho de la ruta en el eje X + int y1{0}; // Límite superior de la ruta en el eje Y + int y2{0}; // Límite inferior de la ruta en el eje Y + int frame{0}; // Frame inicial para la animación + }; + + explicit MovingPlatform(const Data& data); + ~MovingPlatform() = default; + + void update(float delta_time); + void render(); +#ifdef _DEBUG + void updateAnimation(float delta_time); + void resetToInitialPosition(const Data& data); +#endif + + auto getRect() -> SDL_FRect; + auto getCollider() -> SDL_FRect&; + + // Desplazamiento real del último frame (para transportar al jugador) + [[nodiscard]] auto getLastDX() const -> float { return last_dx_; } + [[nodiscard]] auto getLastDY() const -> float { return last_dy_; } + + private: + void checkPath(); // Comprueba los límites del recorrido + + std::shared_ptr sprite_; + SDL_FRect collider_{}; + int x1_{0}; + int x2_{0}; + int y1_{0}; + int y2_{0}; + float last_dx_{0.0F}; // Desplazamiento horizontal del último frame + float last_dy_{0.0F}; // Desplazamiento vertical del último frame +}; diff --git a/source/game/entities/path_enemy.cpp b/source/game/entities/path_enemy.cpp new file mode 100644 index 0000000..c714559 --- /dev/null +++ b/source/game/entities/path_enemy.cpp @@ -0,0 +1,75 @@ +#include "game/entities/path_enemy.hpp" + +#include "core/rendering/sprite/animated_sprite.hpp" // Para AnimatedSprite + +// Constructor: delega al base y almacena boundaries +PathEnemy::PathEnemy(const Data& data) + : Enemy(data), + x1_(data.x1), + x2_(data.x2), + y1_(data.y1), + y2_(data.y2) { +} + +// Actualiza posición y animación del enemigo +void PathEnemy::update(float delta_time) { + sprite_->update(delta_time); + checkPath(); + collider_ = getRect(); +} + +#ifdef _DEBUG +// Resetea el enemigo a su posición inicial (para editor) +void PathEnemy::resetToInitialPosition(const Data& data) { + sprite_->setPosX(data.x); + sprite_->setPosY(data.y); + sprite_->setVelX(data.vx); + sprite_->setVelY(data.vy); + + x1_ = data.x1; + x2_ = data.x2; + y1_ = data.y1; + y2_ = data.y2; + + applyFlipMirror(data.vx); + + collider_ = getRect(); +} +#endif + +// Comprueba si ha llegado al limite del recorrido para darse media vuelta +void PathEnemy::checkPath() { // NOLINT(readability-make-member-function-const) + if (sprite_->getPosX() > x2_ || sprite_->getPosX() < x1_) { + // Recoloca + if (sprite_->getPosX() > x2_) { + sprite_->setPosX(x2_); + } else { + sprite_->setPosX(x1_); + } + + // Cambia el sentido + sprite_->setVelX(sprite_->getVelX() * (-1)); + + // Invierte el sprite + if (should_flip_) { + sprite_->flip(); + } + } + + if (sprite_->getPosY() > y2_ || sprite_->getPosY() < y1_) { + // Recoloca + if (sprite_->getPosY() > y2_) { + sprite_->setPosY(y2_); + } else { + sprite_->setPosY(y1_); + } + + // Cambia el sentido + sprite_->setVelY(sprite_->getVelY() * (-1)); + + // Invierte el sprite + if (should_flip_) { + sprite_->flip(); + } + } +} diff --git a/source/game/entities/path_enemy.hpp b/source/game/entities/path_enemy.hpp new file mode 100644 index 0000000..c75c8b2 --- /dev/null +++ b/source/game/entities/path_enemy.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "game/entities/enemy.hpp" + +class PathEnemy : public Enemy { + public: + explicit PathEnemy(const Data& data); + ~PathEnemy() override = default; + + void update(float delta_time) override; +#ifdef _DEBUG + void resetToInitialPosition(const Data& data) override; +#endif + + private: + void checkPath(); // Comprueba si ha llegado al limite del recorrido para darse media vuelta + + int x1_{0}; // Limite izquierdo de la ruta en el eje X + int x2_{0}; // Limite derecho de la ruta en el eje X + int y1_{0}; // Limite superior de la ruta en el eje Y + int y2_{0}; // Limite inferior de la ruta en el eje Y +}; diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index ffb3b14..55e3f27 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -398,6 +398,9 @@ 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 float foot_y = (y_ + oy) + HEIGHT; if (!tc.hasGroundBelow(x_ + ox, foot_y, WIDTH)) { @@ -537,6 +540,18 @@ 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 953cbc4..c40da55 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -72,6 +72,9 @@ class Player { void setAdjacentRoom(std::shared_ptr room, Room::Border direction); void clearAdjacentRoom(); [[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; } [[nodiscard]] auto getIgnoreInput() const -> bool { return ignore_input_; } @@ -126,6 +129,7 @@ class Player { bool is_alive_ = true; bool is_paused_ = false; bool ignore_input_ = false; + bool on_platform_ = false; bool turning_ = false; Direction facing_ = Direction::RIGHT; Room::Border border_ = Room::Border::TOP; diff --git a/source/game/gameplay/platform_manager.cpp b/source/game/gameplay/platform_manager.cpp new file mode 100644 index 0000000..68299ab --- /dev/null +++ b/source/game/gameplay/platform_manager.cpp @@ -0,0 +1,95 @@ +#include "platform_manager.hpp" + +#include "game/entities/moving_platform.hpp" // Para MovingPlatform + +// Añade una plataforma a la colección +void PlatformManager::addPlatform(std::shared_ptr platform) { + platforms_.push_back(std::move(platform)); +} + +// Elimina todas las plataformas +void PlatformManager::clear() { + platforms_.clear(); +} + +// Comprueba si no hay plataformas +auto PlatformManager::isEmpty() const -> bool { + return platforms_.empty(); +} + +// Actualiza todas las plataformas +void PlatformManager::update(float delta_time) { + for (const auto& platform : platforms_) { + platform->update(delta_time); + } +} + +// Renderiza todas las plataformas +void PlatformManager::render() { + for (const auto& platform : platforms_) { + platform->render(); + } +} + +#ifdef _DEBUG +// Solo actualiza animaciones sin mover plataformas +void PlatformManager::updateAnimations(float delta_time) { + for (const auto& platform : platforms_) { + platform->updateAnimation(delta_time); + } +} + +// Resetea todas las plataformas a su posición inicial +void PlatformManager::resetPositions(const std::vector& platform_data) { + const int COUNT = std::min(static_cast(platforms_.size()), static_cast(platform_data.size())); + for (int i = 0; i < COUNT; ++i) { + platforms_[i]->resetToInitialPosition(platform_data[i]); + } +} + +// Número de plataformas +auto PlatformManager::getCount() const -> int { + return static_cast(platforms_.size()); +} + +// Acceso a una plataforma por índice +auto PlatformManager::getPlatform(int index) -> std::shared_ptr& { + return platforms_.at(index); +} + +// Elimina la última plataforma +void PlatformManager::removeLastPlatform() { + if (!platforms_.empty()) { + 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 de 4px (medio tile) para compensar el movimiento entre frames + constexpr float TOLERANCE = 4.0F; + if (player_feet >= platform_top && 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 new file mode 100644 index 0000000..2c48368 --- /dev/null +++ b/source/game/gameplay/platform_manager.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include + +#include // Para shared_ptr +#include // Para vector + +#include "game/entities/moving_platform.hpp" // Para MovingPlatform, MovingPlatform::Data + +class PlatformManager { + public: + PlatformManager() = default; + ~PlatformManager() = default; + + // Prohibir copia y movimiento + PlatformManager(const PlatformManager&) = delete; + auto operator=(const PlatformManager&) -> PlatformManager& = delete; + PlatformManager(PlatformManager&&) = delete; + auto operator=(PlatformManager&&) -> PlatformManager& = delete; + + // Gestión de plataformas + void addPlatform(std::shared_ptr platform); + void clear(); + [[nodiscard]] auto isEmpty() const -> bool; + + // Actualización y renderizado + 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); + [[nodiscard]] auto getCount() const -> int; + auto getPlatform(int index) -> std::shared_ptr&; + void removeLastPlatform(); +#endif + + private: + std::vector> platforms_; +}; diff --git a/source/game/gameplay/room.cpp b/source/game/gameplay/room.cpp index 850429d..5c535c2 100644 --- a/source/game/gameplay/room.cpp +++ b/source/game/gameplay/room.cpp @@ -9,6 +9,7 @@ #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/platform_manager.hpp" // Para PlatformManager #include "game/gameplay/room_loader.hpp" // Para RoomLoader #include "game/gameplay/scoreboard.hpp" // Para Scoreboard::Data #include "game/gameplay/tilemap_renderer.hpp" // Para TilemapRenderer @@ -20,9 +21,10 @@ Room::Room(const std::string& room_path, std::shared_ptr data) : data_(std::move(data)) { auto room = Resource::Cache::get()->getRoom(room_path); - // Crea los managers de enemigos e items + // Crea los managers de enemigos, items y plataformas enemy_manager_ = std::make_unique(); item_manager_ = std::make_unique(room->number, data_); + platform_manager_ = std::make_unique(); initializeRoom(*room); // Crea el mapa de colisiones desde el collision_tile_map @@ -55,7 +57,12 @@ void Room::initializeRoom(const Data& room) { // Crear los enemigos usando el manager for (const auto& enemy_data : room.enemies) { - enemy_manager_->addEnemy(std::make_shared(enemy_data)); + enemy_manager_->addEnemy(Enemy::create(enemy_data)); + } + + // Crear las plataformas usando el manager + for (const auto& plat_data : room.platforms) { + platform_manager_->addPlatform(std::make_shared(plat_data)); } // Crear los items usando el manager @@ -79,6 +86,11 @@ void Room::renderMap() { tilemap_renderer_->render(); } +// Dibuja las plataformas en pantalla +void Room::renderPlatforms() { + platform_manager_->render(); +} + // Dibuja los enemigos en pantalla void Room::renderEnemies() { enemy_manager_->render(); @@ -99,6 +111,7 @@ void Room::redrawMap() { void Room::updateEditorMode(float delta_time) { tilemap_renderer_->update(delta_time); enemy_manager_->updateAnimations(delta_time); + platform_manager_->updateAnimations(delta_time); item_manager_->update(delta_time); } @@ -184,6 +197,9 @@ void Room::update(float delta_time) { // NOLINT(readability-make-member-functio // Actualiza los enemigos usando el manager enemy_manager_->update(delta_time); + // Actualiza las plataformas usando el manager + platform_manager_->update(delta_time); + // Actualiza los items usando el manager item_manager_->update(delta_time); } @@ -223,6 +239,10 @@ auto Room::itemCollision(SDL_FRect& rect) -> bool { return item_manager_->checkCollision(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 RoomLoader) auto Room::loadYAML(const std::string& file_path, bool verbose) -> Data { // NOLINT(readability-convert-member-functions-to-static) return RoomLoader::loadYAML(file_path, verbose); diff --git a/source/game/gameplay/room.hpp b/source/game/gameplay/room.hpp index 68bd509..254006b 100644 --- a/source/game/gameplay/room.hpp +++ b/source/game/gameplay/room.hpp @@ -6,14 +6,16 @@ #include // Para string #include // Para vector -#include "game/defaults.hpp" // Para Defaults::Game::Room -#include "game/entities/enemy.hpp" // Para EnemyData -#include "game/entities/item.hpp" // Para ItemData -#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/enemy.hpp" // Para EnemyData +#include "game/entities/item.hpp" // Para ItemData +#include "game/entities/moving_platform.hpp" // Para MovingPlatform::Data +#include "game/gameplay/scoreboard.hpp" // Para Scoreboard::Data +#include "utils/defines.hpp" // Para Tile::SIZE, Map::WIDTH, Map::HEIGHT class Surface; class EnemyManager; class ItemManager; +class PlatformManager; class CollisionMap; class TileCollider; class TilemapRenderer; @@ -44,6 +46,7 @@ class Room { std::vector collision_tile_map; std::vector enemies; std::vector items; + std::vector platforms; }; // Constructor y destructor @@ -55,6 +58,7 @@ class Room { [[nodiscard]] auto getBGColor() const -> Uint8 { return bg_color_; } void renderMap(); void renderEnemies(); + void renderPlatforms(); void renderItems(); #ifdef _DEBUG void redrawMap(); @@ -62,6 +66,7 @@ class Room { void resetEnemyPositions(const std::vector& enemy_data); auto getEnemyManager() -> EnemyManager* { return enemy_manager_.get(); } auto getItemManager() -> ItemManager* { return item_manager_.get(); } + auto getPlatformManager() -> PlatformManager* { return platform_manager_.get(); } void setItemColors(Uint8 color1, Uint8 color2); void setTile(int index, int tile_value); void setCollisionTile(int index, int value); @@ -75,6 +80,7 @@ class Room { auto getRoom(Border border) -> std::string; auto enemyCollision(SDL_FRect& rect) -> bool; auto itemCollision(SDL_FRect& rect) -> bool; + auto checkPlayerOnPlatform(const SDL_FRect& player_collider, float player_vy) -> MovingPlatform*; void setPaused(bool value); [[nodiscard]] auto getConveyorBeltDirection() const -> int { return conveyor_belt_direction_; } [[nodiscard]] auto getTileCollider() const -> const TileCollider&; @@ -89,6 +95,7 @@ class Room { std::unique_ptr enemy_manager_; std::unique_ptr item_manager_; + std::unique_ptr platform_manager_; std::unique_ptr collision_map_; std::unique_ptr tilemap_renderer_; std::shared_ptr surface_; diff --git a/source/game/gameplay/room_loader.cpp b/source/game/gameplay/room_loader.cpp index 9e2140e..dac2d3a 100644 --- a/source/game/gameplay/room_loader.cpp +++ b/source/game/gameplay/room_loader.cpp @@ -204,6 +204,11 @@ void RoomLoader::parseEnemyBoundaries(const fkyaml::node& bounds_node, Enemy::Da auto RoomLoader::parseEnemyData(const fkyaml::node& enemy_node) -> Enemy::Data { // NOLINT(readability-convert-member-functions-to-static) Enemy::Data enemy; + // Enemy type (default: "path") + if (enemy_node.contains("type")) { + enemy.type = enemy_node["type"].get_value(); + } + // Animation path if (enemy_node.contains("animation")) { enemy.animation_path = enemy_node["animation"].get_value(); @@ -323,6 +328,90 @@ void RoomLoader::parseItems(const fkyaml::node& yaml, Room::Data& room, bool ver } } +// Parsea los límites de movimiento de una plataforma +void RoomLoader::parsePlatformBoundaries(const fkyaml::node& bounds_node, MovingPlatform::Data& platform) { + // Formato: position1 y position2 + if (bounds_node.contains("position1")) { + const auto& pos1 = bounds_node["position1"]; + if (pos1.contains("x")) { + platform.x1 = pos1["x"].get_value() * Tile::SIZE; + } + if (pos1.contains("y")) { + platform.y1 = pos1["y"].get_value() * Tile::SIZE; + } + } + if (bounds_node.contains("position2")) { + const auto& pos2 = bounds_node["position2"]; + if (pos2.contains("x")) { + platform.x2 = pos2["x"].get_value() * Tile::SIZE; + } + if (pos2.contains("y")) { + platform.y2 = pos2["y"].get_value() * Tile::SIZE; + } + } +} + +// Parsea los datos de una plataforma individual +auto RoomLoader::parsePlatformData(const fkyaml::node& platform_node) -> MovingPlatform::Data { + MovingPlatform::Data platform; + + // Animation path + if (platform_node.contains("animation")) { + platform.animation_path = platform_node["animation"].get_value(); + } + + // Position (in tiles, convert to pixels) + if (platform_node.contains("position")) { + const auto& pos = platform_node["position"]; + if (pos.contains("x")) { + platform.x = pos["x"].get_value() * Tile::SIZE; + } + if (pos.contains("y")) { + platform.y = pos["y"].get_value() * Tile::SIZE; + } + } + + // Velocity (already in pixels/second) + if (platform_node.contains("velocity")) { + const auto& vel = platform_node["velocity"]; + if (vel.contains("x")) { + platform.vx = vel["x"].get_value(); + } + if (vel.contains("y")) { + platform.vy = vel["y"].get_value(); + } + } + + // Boundaries (in tiles, convert to pixels) + if (platform_node.contains("boundaries")) { + parsePlatformBoundaries(platform_node["boundaries"], platform); + } + + // Optional frame + platform.frame = platform_node.contains("frame") + ? platform_node["frame"].get_value_or(-1) + : -1; + + return platform; +} + +// Parsea la lista de plataformas de la habitación +void RoomLoader::parsePlatforms(const fkyaml::node& yaml, Room::Data& room, bool verbose) { + if (!yaml.contains("platforms") || yaml["platforms"].is_null()) { + return; + } + + const auto& platforms_node = yaml["platforms"]; + + for (const auto& platform_node : platforms_node) { + room.platforms.push_back(parsePlatformData(platform_node)); + } + + if (verbose) { + std::cout << "Loaded " << room.platforms.size() << " platforms\n"; + } +} + #ifdef _DEBUG // Carga una habitación desde un string YAML (para el editor de mapas, evita el resource pack) auto RoomLoader::loadFromString(const std::string& yaml_content, const std::string& file_name) -> Room::Data { @@ -333,6 +422,7 @@ auto RoomLoader::loadFromString(const std::string& yaml_content, const std::stri parseTilemap(yaml, room, file_name, false); parseEnemies(yaml, room, false); parseItems(yaml, room, false); + parsePlatforms(yaml, room, false); } catch (const fkyaml::exception& e) { std::cerr << "YAML parsing error in " << file_name << ": " << e.what() << '\n'; } catch (const std::exception& e) { @@ -367,6 +457,7 @@ auto RoomLoader::loadYAML(const std::string& file_path, bool verbose) -> Room::D parseTilemap(yaml, room, FILE_NAME, verbose); parseEnemies(yaml, room, verbose); parseItems(yaml, room, verbose); + parsePlatforms(yaml, room, verbose); if (verbose) { std::cout << "Room loaded successfully: " << FILE_NAME << '\n'; diff --git a/source/game/gameplay/room_loader.hpp b/source/game/gameplay/room_loader.hpp index d7774af..a1d53f0 100644 --- a/source/game/gameplay/room_loader.hpp +++ b/source/game/gameplay/room_loader.hpp @@ -3,10 +3,11 @@ #include // Para string #include // Para vector -#include "external/fkyaml_node.hpp" // Para fkyaml::node -#include "game/entities/enemy.hpp" // Para Enemy::Data -#include "game/entities/item.hpp" // Para Item::Data -#include "game/gameplay/room.hpp" // Para Room::Data +#include "external/fkyaml_node.hpp" // Para fkyaml::node +#include "game/entities/enemy.hpp" // Para Enemy::Data +#include "game/entities/item.hpp" // Para Item::Data +#include "game/entities/moving_platform.hpp" // Para MovingPlatform::Data +#include "game/gameplay/room.hpp" // Para Room::Data /** * @brief Cargador de archivos de habitaciones en formato YAML @@ -133,4 +134,8 @@ class RoomLoader { * @return Estructura Item::Data con los datos parseados */ static auto parseItemData(const fkyaml::node& item_node, const Room::Data& room) -> Item::Data; + + static void parsePlatforms(const fkyaml::node& yaml, Room::Data& room, bool verbose); + static auto parsePlatformData(const fkyaml::node& platform_node) -> MovingPlatform::Data; + static void parsePlatformBoundaries(const fkyaml::node& bounds_node, MovingPlatform::Data& platform); }; diff --git a/source/game/scenes/game.cpp b/source/game/scenes/game.cpp index 38c6e0e..9112fb4 100644 --- a/source/game/scenes/game.cpp +++ b/source/game/scenes/game.cpp @@ -18,6 +18,7 @@ #include "core/system/event_buffer.hpp" // Para EventBuffer #include "core/system/global_events.hpp" // Para check #include "game/defaults.hpp" // Para Defaults::Game +#include "game/entities/moving_platform.hpp" // Para MovingPlatform #include "game/game_control.hpp" // Para GameControl #include "game/gameplay/item_tracker.hpp" // Para ItemTracker #include "game/gameplay/room.hpp" // Para Room, RoomData @@ -305,6 +306,10 @@ 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) handleDebugMouseDrag(delta_time); @@ -496,12 +501,14 @@ void Game::renderPlaying() { // Renderizar habitación saliente con su offset Screen::get()->setRenderOffset(old_ox, old_oy); transition_old_room_->renderMap(); + transition_old_room_->renderPlatforms(); transition_old_room_->renderEnemies(); transition_old_room_->renderItems(); // Renderizar habitación entrante + jugador con su offset Screen::get()->setRenderOffset(new_ox, new_oy); room_->renderMap(); + room_->renderPlatforms(); room_->renderEnemies(); room_->renderItems(); if (mode_ == Mode::GAME) { @@ -514,6 +521,7 @@ void Game::renderPlaying() { } else { // --- Renderizado normal --- room_->renderMap(); + room_->renderPlatforms(); room_->renderEnemies(); room_->renderItems(); if (mode_ == Mode::GAME) { @@ -895,6 +903,14 @@ 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::checkPlayerAndItems() { room_->itemCollision(player_->getCollider()); diff --git a/source/game/scenes/game.hpp b/source/game/scenes/game.hpp index e55eca9..9fff024 100644 --- a/source/game/scenes/game.hpp +++ b/source/game/scenes/game.hpp @@ -74,6 +74,7 @@ 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 checkIfPlayerIsAlive(); // Comprueba si el jugador esta vivo void killPlayer(); // Mata al jugador