diff --git a/data/room/02.yaml b/data/room/02.yaml index f249e04..b770774 100644 --- a/data/room/02.yaml +++ b/data/room/02.yaml @@ -91,10 +91,13 @@ items: # Plataformas móviles en esta habitación platforms: - animation: bin.yaml - position: {x: 2, y: 16} - velocity: {x: 25, y: 0} - boundaries: - position1: {x: 2, y: 16} - position2: {x: 20, y: 15} + speed: 30 + loop: circular + easing: cubicInOut frame: 0 + path: + - {x: 5, y: 18, wait: 2.0} + - {x: 20, y: 18, wait: 2.0} + - {x: 20, y: 13, wait: 2.0} + - {x: 5, y: 13, wait: 2.0} diff --git a/source/game/editor/map_editor.cpp b/source/game/editor/map_editor.cpp index e16702a..faabe4c 100644 --- a/source/game/editor/map_editor.cpp +++ b/source/game/editor/map_editor.cpp @@ -330,13 +330,7 @@ void MapEditor::autosave() { room_data_.items[i].y = pos.y; } - // Sincronizar posiciones de plataformas desde los sprites vivos a room_data_ - auto* platform_mgr = room_->getPlatformManager(); - for (int i = 0; i < platform_mgr->getCount() && i < static_cast(room_data_.platforms.size()); ++i) { - SDL_FRect rect = platform_mgr->getPlatform(i)->getRect(); - room_data_.platforms[i].x = rect.x; - room_data_.platforms[i].y = rect.y; - } + // Platforms are already synced via resetToInitialPosition during drag commit RoomSaver::saveYAML(file_path_, yaml_, room_data_); } @@ -627,8 +621,8 @@ void MapEditor::handleMouseDown(float game_x, float game_y) { } } - // 3. Hit test on boundaries (enemies and platforms) - for (auto type : {EntityType::ENEMY, EntityType::PLATFORM}) { + // 3. Hit test on boundaries (enemies only) + for (auto type : {EntityType::ENEMY}) { for (int i = 0; i < entityDataCount(type); ++i) { auto bd = entityBoundaries(type, i); constexpr auto SZ = static_cast(Tile::SIZE); @@ -718,9 +712,16 @@ auto MapEditor::commitEntityDrag() -> bool { break; case EntityType::PLATFORM: if (IDX >= 0 && IDX < static_cast(room_data_.platforms.size())) { - room_data_.platforms[IDX].x = drag_.snap_x; - room_data_.platforms[IDX].y = drag_.snap_y; - room_->getPlatformManager()->getPlatform(IDX)->resetToInitialPosition(room_data_.platforms[IDX]); + auto& plat = room_data_.platforms[IDX]; + if (!plat.path.empty()) { + float dx = drag_.snap_x - plat.path[0].x; + float dy = drag_.snap_y - plat.path[0].y; + for (auto& wp : plat.path) { + wp.x += dx; + wp.y += dy; + } + } + room_->getPlatformManager()->getPlatform(IDX)->resetToInitialPosition(plat); selection_ = {EntityType::PLATFORM, IDX}; return true; } @@ -741,15 +742,6 @@ auto MapEditor::commitEntityDrag() -> bool { return true; } break; - case EntityType::PLATFORM: - if (IDX >= 0 && IDX < static_cast(room_data_.platforms.size())) { - room_data_.platforms[IDX].x1 = SNAP_X; - room_data_.platforms[IDX].y1 = SNAP_Y; - room_->getPlatformManager()->getPlatform(IDX)->resetToInitialPosition(room_data_.platforms[IDX]); - selection_ = {EntityType::PLATFORM, IDX}; - return true; - } - break; default: break; } @@ -766,15 +758,6 @@ auto MapEditor::commitEntityDrag() -> bool { return true; } break; - case EntityType::PLATFORM: - if (IDX >= 0 && IDX < static_cast(room_data_.platforms.size())) { - room_data_.platforms[IDX].x2 = SNAP_X; - room_data_.platforms[IDX].y2 = SNAP_Y; - room_->getPlatformManager()->getPlatform(IDX)->resetToInitialPosition(room_data_.platforms[IDX]); - selection_ = {EntityType::PLATFORM, IDX}; - return true; - } - break; default: break; } @@ -807,8 +790,14 @@ void MapEditor::moveEntityVisual() { case EntityType::PLATFORM: if (drag_.index >= 0 && drag_.index < room_->getPlatformManager()->getCount()) { MovingPlatform::Data temp_data = room_data_.platforms[drag_.index]; - temp_data.x = drag_.snap_x; - temp_data.y = drag_.snap_y; + if (!temp_data.path.empty()) { + float dx = drag_.snap_x - temp_data.path[0].x; + float dy = drag_.snap_y - temp_data.path[0].y; + for (auto& wp : temp_data.path) { + wp.x += dx; + wp.y += dy; + } + } room_->getPlatformManager()->getPlatform(drag_.index)->resetToInitialPosition(temp_data); } break; @@ -940,7 +929,7 @@ auto MapEditor::entityRect(EntityType type, int index) -> SDL_FRect { } auto MapEditor::entityHasBoundaries(EntityType type) -> bool { - return type == EntityType::ENEMY || type == EntityType::PLATFORM; + return type == EntityType::ENEMY; } auto MapEditor::entityBoundaries(EntityType type, int index) const -> BoundaryData { @@ -949,10 +938,6 @@ auto MapEditor::entityBoundaries(EntityType type, int index) const -> BoundaryDa const auto& e = room_data_.enemies[index]; return {e.x1, e.y1, e.x2, e.y2}; } - case EntityType::PLATFORM: { - const auto& p = room_data_.platforms[index]; - return {p.x1, p.y1, p.x2, p.y2}; - } default: return {}; } } @@ -961,7 +946,10 @@ auto MapEditor::entityPosition(EntityType type, int index) const -> std::pair(bd.x1); - auto b1_y = static_cast(bd.y1); - auto b2_x = static_cast(bd.x2); - auto b2_y = static_cast(bd.y2); + bool is_selected = selection_.is(EntityType::ENEMY) && selection_.index == i; - // Si estamos arrastrando una boundary de esta entidad, usar la posición snapped - if (drag_.entity_type == type && drag_.index == i) { - if (drag_.target == DragTarget::ENTITY_BOUND1) { - b1_x = drag_.snap_x; - b1_y = drag_.snap_y; - } else if (drag_.target == DragTarget::ENTITY_BOUND2) { - b2_x = drag_.snap_x; - b2_y = drag_.snap_y; - } else if (drag_.target == DragTarget::ENTITY_INITIAL) { - init_x = drag_.snap_x; - init_y = drag_.snap_y; - } - is_selected = true; // Arrastrando = siempre iluminado + // Posiciones base (pueden estar siendo arrastradas) + float init_x = pos_x; + float init_y = pos_y; + auto b1_x = static_cast(bd.x1); + auto b1_y = static_cast(bd.y1); + auto b2_x = static_cast(bd.x2); + auto b2_y = static_cast(bd.y2); + + // Si estamos arrastrando una boundary de esta entidad, usar la posición snapped + if (drag_.entity_type == EntityType::ENEMY && drag_.index == i) { + if (drag_.target == DragTarget::ENTITY_BOUND1) { + b1_x = drag_.snap_x; + b1_y = drag_.snap_y; + } else if (drag_.target == DragTarget::ENTITY_BOUND2) { + b2_x = drag_.snap_x; + b2_y = drag_.snap_y; + } else if (drag_.target == DragTarget::ENTITY_INITIAL) { + init_x = drag_.snap_x; + init_y = drag_.snap_y; } + is_selected = true; // Arrastrando = siempre iluminado + } - Uint8 color_b1 = is_selected ? SEL_BOUND1 : DIM_BOUND1; - Uint8 color_b2 = is_selected ? SEL_BOUND2 : DIM_BOUND2; - Uint8 color_route = is_selected ? SEL_ROUTE : DIM_ROUTE; + Uint8 color_b1 = is_selected ? SEL_BOUND1 : DIM_BOUND1; + Uint8 color_b2 = is_selected ? SEL_BOUND2 : DIM_BOUND2; + Uint8 color_route = is_selected ? SEL_ROUTE : DIM_ROUTE; - // Dibujar líneas de ruta - game_surface->drawLine(b1_x + HALF, b1_y + HALF, init_x + HALF, init_y + HALF, color_route); - game_surface->drawLine(init_x + HALF, init_y + HALF, b2_x + HALF, b2_y + HALF, color_route); + // Dibujar líneas de ruta + game_surface->drawLine(b1_x + HALF, b1_y + HALF, init_x + HALF, init_y + HALF, color_route); + game_surface->drawLine(init_x + HALF, init_y + HALF, b2_x + HALF, b2_y + HALF, color_route); - // Marcadores en las boundaries - renderBoundaryMarker(b1_x, b1_y, color_b1); - renderBoundaryMarker(b2_x, b2_y, color_b2); + // Marcadores en las boundaries + renderBoundaryMarker(b1_x, b1_y, color_b1); + renderBoundaryMarker(b2_x, b2_y, color_b2); + } + + // Render platform waypoint routes + for (int i = 0; i < entityDataCount(EntityType::PLATFORM); ++i) { + const auto& plat = room_data_.platforms[i]; + if (plat.path.size() < 2) { continue; } + + bool is_selected = selection_.is(EntityType::PLATFORM) && selection_.index == i; + + // If dragging this platform, apply the drag offset to all points for visualization + float drag_dx = 0.0F; + float drag_dy = 0.0F; + if (drag_.entity_type == EntityType::PLATFORM && drag_.index == i && drag_.target == DragTarget::ENTITY_INITIAL) { + if (!plat.path.empty()) { + drag_dx = drag_.snap_x - plat.path[0].x; + drag_dy = drag_.snap_y - plat.path[0].y; + } + is_selected = true; + } + + Uint8 color_wp = is_selected ? SEL_BOUND1 : DIM_BOUND1; + Uint8 color_route = is_selected ? SEL_ROUTE : DIM_ROUTE; + + // Draw route lines between consecutive waypoints + for (int j = 0; j < static_cast(plat.path.size()) - 1; ++j) { + float ax = plat.path[j].x + drag_dx + HALF; + float ay = plat.path[j].y + drag_dy + HALF; + float bx = plat.path[j + 1].x + drag_dx + HALF; + float by = plat.path[j + 1].y + drag_dy + HALF; + game_surface->drawLine(ax, ay, bx, by, color_route); + } + + // For circular mode, draw line from last to first + if (plat.loop == LoopMode::CIRCULAR && plat.path.size() > 2) { + int last = static_cast(plat.path.size()) - 1; + float ax = plat.path[last].x + drag_dx + HALF; + float ay = plat.path[last].y + drag_dy + HALF; + float bx = plat.path[0].x + drag_dx + HALF; + float by = plat.path[0].y + drag_dy + HALF; + game_surface->drawLine(ax, ay, bx, by, color_route); + } + + // Draw waypoint markers + for (const auto& wp : plat.path) { + renderBoundaryMarker(wp.x + drag_dx, wp.y + drag_dy, color_wp); } } } @@ -1145,12 +1178,10 @@ void MapEditor::updateStatusBarInfo() { // NOLINT(readability-function-cognitiv if (selection_.index < static_cast(room_data_.platforms.size())) { const auto& p = room_data_.platforms[selection_.index]; std::string anim = p.animation_path; - auto dot = anim.rfind('.'); - if (dot != std::string::npos) { anim = anim.substr(0, dot); } - + if (anim.size() > 5 && anim.substr(anim.size() - 5) == ".yaml") { anim = anim.substr(0, anim.size() - 5); } line2 = "platform " + std::to_string(selection_.index) + ": " + anim; - line3 = "vx:" + std::to_string(static_cast(p.vx)) + - " vy:" + std::to_string(static_cast(p.vy)); + line3 = "speed:" + std::to_string(static_cast(p.speed)) + " " + (p.loop == LoopMode::CIRCULAR ? "circular" : "pingpong"); + if (p.easing != "linear") { line3 += " " + p.easing; } } break; @@ -1197,7 +1228,7 @@ auto MapEditor::getSetCompletions() const -> std::vector { switch (selection_.type) { case EntityType::ENEMY: return {"ANIMATION", "VX", "VY", "FLIP", "MIRROR"}; case EntityType::ITEM: return {"TILE", "COUNTER"}; - case EntityType::PLATFORM: return {"ANIMATION", "VX", "VY"}; + case EntityType::PLATFORM: return {"ANIMATION", "SPEED", "LOOP", "EASING"}; default: return {"ITEMCOLOR1", "ITEMCOLOR2", "CONVEYOR", "TILESET", "UP", "DOWN", "LEFT", "RIGHT"}; } } @@ -1856,50 +1887,56 @@ auto MapEditor::setPlatformProperty(const std::string& property, const std::stri return "animation: " + anim; } - if (property == "VX") { + if (property == "SPEED") { try { - platform.vx = std::stof(value); + platform.speed = std::stof(value); } catch (...) { return "Invalid value: " + value; } - platform.vy = 0.0F; room_->getPlatformManager()->getPlatform(selection_.index)->resetToInitialPosition(platform); autosave(); - return "vx: " + std::to_string(static_cast(platform.vx)) + " vy: 0"; + return "speed: " + std::to_string(static_cast(platform.speed)); } - if (property == "VY") { - try { - platform.vy = std::stof(value); - } catch (...) { return "Invalid value: " + value; } - platform.vx = 0.0F; + if (property == "LOOP") { + std::string val = toLower(value); + if (val == "circular") { + platform.loop = LoopMode::CIRCULAR; + } else { + platform.loop = LoopMode::PINGPONG; + val = "pingpong"; + } room_->getPlatformManager()->getPlatform(selection_.index)->resetToInitialPosition(platform); autosave(); - return "vy: " + std::to_string(static_cast(platform.vy)) + " vx: 0"; + return "loop: " + val; } - return "Unknown property: " + property + " (use: animation, vx, vy)"; + if (property == "EASING") { + platform.easing = toLower(value); + + room_->getPlatformManager()->getPlatform(selection_.index)->resetToInitialPosition(platform); + autosave(); + return "easing: " + platform.easing; + } + + return "Unknown property: " + property + " (use: animation, speed, loop, easing)"; } // Crea una nueva plataforma con valores por defecto, centrada en la habitación auto MapEditor::addPlatform() -> std::string { if (!active_) { return "Editor not active"; } - constexpr float CENTER_X = PlayArea::CENTER_X; - constexpr float CENTER_Y = PlayArea::CENTER_Y; - constexpr float ROUTE_HALF = 2.5F * Tile::SIZE; // 2.5 tiles a cada lado (5 tiles total) - MovingPlatform::Data new_platform; new_platform.animation_path = "bin.yaml"; - new_platform.x = CENTER_X; - new_platform.y = CENTER_Y; - new_platform.vx = 24.0F; - new_platform.vy = 0.0F; - new_platform.x1 = static_cast(CENTER_X - ROUTE_HALF); - new_platform.y1 = static_cast(CENTER_Y); - new_platform.x2 = static_cast(CENTER_X + ROUTE_HALF); - new_platform.y2 = static_cast(CENTER_Y); - new_platform.frame = 0; + new_platform.speed = 24.0F; + new_platform.frame = -1; + constexpr float CENTER_X = PlayArea::CENTER_X; + constexpr float CENTER_Y = PlayArea::CENTER_Y; + constexpr float ROUTE_HALF = 2.0F * Tile::SIZE; + new_platform.path = { + {CENTER_X - ROUTE_HALF, CENTER_Y, 0.0F}, + {CENTER_X + ROUTE_HALF, CENTER_Y, 0.0F} + }; room_data_.platforms.push_back(new_platform); room_->getPlatformManager()->addPlatform(std::make_shared(new_platform)); @@ -1937,9 +1974,9 @@ auto MapEditor::duplicatePlatform() -> std::string { if (!hasSelectedPlatform()) { return "No platform selected"; } MovingPlatform::Data copy = room_data_.platforms[selection_.index]; - copy.x += Tile::SIZE; - copy.x1 += Tile::SIZE; - copy.x2 += Tile::SIZE; + for (auto& wp : copy.path) { + wp.x += Tile::SIZE; + } room_data_.platforms.push_back(copy); room_->getPlatformManager()->addPlatform(std::make_shared(copy)); diff --git a/source/game/editor/room_saver.cpp b/source/game/editor/room_saver.cpp index 49ac06e..a3055d3 100644 --- a/source/game/editor/room_saver.cpp +++ b/source/game/editor/room_saver.cpp @@ -162,23 +162,18 @@ auto RoomSaver::buildYAML(const fkyaml::node& original_yaml, const Room::Data& r 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"; - + out << " speed: " << plat.speed << "\n"; + out << " loop: " << (plat.loop == LoopMode::CIRCULAR ? "circular" : "pingpong") << "\n"; + if (plat.easing != "linear") { out << " easing: " << plat.easing << "\n"; } if (plat.frame != -1) { out << " frame: " << plat.frame << "\n"; } - + out << " path:\n"; + for (const auto& wp : plat.path) { + int wx = static_cast(std::round(wp.x / Tile::SIZE)); + int wy = static_cast(std::round(wp.y / Tile::SIZE)); + out << " - {x: " << wx << ", y: " << wy; + if (wp.wait > 0.0F) { out << ", wait: " << wp.wait; } + out << "}\n"; + } out << "\n"; } } diff --git a/source/game/entities/moving_platform.cpp b/source/game/entities/moving_platform.cpp index 0032f5a..00c53dd 100644 --- a/source/game/entities/moving_platform.cpp +++ b/source/game/entities/moving_platform.cpp @@ -1,39 +1,181 @@ #include "game/entities/moving_platform.hpp" +#include // Para std::sqrt #include // Para rand #include "core/rendering/sprite/animated_sprite.hpp" // Para AnimatedSprite #include "core/resources/resource_cache.hpp" // Para Resource +#include "utils/easing_functions.hpp" // Para Easing::* + +// Resuelve el nombre de un easing a su función +auto MovingPlatform::resolveEasing(const std::string& name) -> EasingFunc { + if (name == "quadIn") { return Easing::quadIn; } + if (name == "quadOut") { return Easing::quadOut; } + if (name == "quadInOut") { return Easing::quadInOut; } + if (name == "cubicIn") { return Easing::cubicIn; } + if (name == "cubicOut") { return Easing::cubicOut; } + if (name == "cubicInOut") { return Easing::cubicInOut; } + if (name == "sineIn") { return Easing::sineIn; } + if (name == "sineOut") { return Easing::sineOut; } + if (name == "sineInOut") { return Easing::sineInOut; } + return Easing::linear; +} // 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); + path_(data.path), + speed_(data.speed), + loop_mode_(data.loop), + easing_(resolveEasing(data.easing)) { + // Colocar el sprite en el primer waypoint + if (!path_.empty()) { + sprite_->setPosX(path_[0].x); + sprite_->setPosY(path_[0].y); + } collider_ = getRect(); - // Coloca un frame al azar o el designado + // Frame inicial sprite_->setCurrentAnimationFrame((data.frame == -1) ? (rand() % sprite_->getCurrentAnimationSize()) : data.frame); + + // Calcular longitud del primer segmento + recalcSegmentLength(); } -// Actualiza posición, calcula desplazamiento real del frame +// Índice del waypoint origen del segmento actual +auto MovingPlatform::getSegmentFrom() const -> int { + if (direction_ == 1) { + return current_segment_; + } + // Pingpong retrocediendo: segmento N va de path[N+1] a path[N] + return current_segment_ + 1; +} + +// Índice del waypoint destino del segmento actual +auto MovingPlatform::getSegmentTo() const -> int { + if (direction_ == 1) { + if (loop_mode_ == LoopMode::CIRCULAR) { + return (current_segment_ + 1) % static_cast(path_.size()); + } + return current_segment_ + 1; + } + // Pingpong retrocediendo + return current_segment_; +} + +// Recalcula la longitud del segmento actual +void MovingPlatform::recalcSegmentLength() { + if (path_.size() < 2) { + segment_length_ = 0.0F; + return; + } + + int from = getSegmentFrom(); + int to = getSegmentTo(); + + float dx = path_[to].x - path_[from].x; + float dy = path_[to].y - path_[from].y; + segment_length_ = std::sqrt(dx * dx + dy * dy); +} + +// Avanza al siguiente segmento +void MovingPlatform::advanceSegment() { + int path_size = static_cast(path_.size()); + + if (loop_mode_ == LoopMode::PINGPONG) { + if (direction_ == 1) { + if (current_segment_ + 1 >= path_size - 1) { + // Llegamos al final, invertir dirección + direction_ = -1; + current_segment_ = path_size - 2; + } else { + current_segment_++; + } + } else { + if (current_segment_ <= 0) { + // Llegamos al inicio, invertir dirección + direction_ = 1; + current_segment_ = 0; + } else { + current_segment_--; + } + } + } else { + // CIRCULAR + current_segment_ = (current_segment_ + 1) % path_size; + } + + segment_progress_ = 0.0F; + recalcSegmentLength(); +} + +// Actualiza posición de la plataforma siguiendo la ruta void MovingPlatform::update(float delta_time) { + sprite_->animate(delta_time); + + if (path_.size() < 2) { + last_dx_ = 0.0F; + last_dy_ = 0.0F; + return; + } + float old_x = sprite_->getPosX(); float old_y = sprite_->getPosY(); - sprite_->update(delta_time); - checkPath(); + // Si estamos esperando en un waypoint + if (waiting_) { + wait_timer_ -= delta_time; + if (wait_timer_ <= 0.0F) { + waiting_ = false; + advanceSegment(); + } else { + last_dx_ = 0.0F; + last_dy_ = 0.0F; + return; + } + } + + // Avanzar por el segmento + if (segment_length_ > 0.0F) { + float distance = speed_ * delta_time; + float delta_progress = distance / segment_length_; + segment_progress_ += delta_progress; + } else { + // Segmento de longitud 0: saltar al siguiente + segment_progress_ = 1.0F; + } + + // Comprobar si llegamos al final del segmento + if (segment_progress_ >= 1.0F) { + segment_progress_ = 1.0F; + + // Colocar en el waypoint destino exacto + int to = getSegmentTo(); + sprite_->setPosX(path_[to].x); + sprite_->setPosY(path_[to].y); + + // Comprobar si hay espera en este waypoint + if (path_[to].wait > 0.0F) { + waiting_ = true; + wait_timer_ = path_[to].wait; + } else { + advanceSegment(); + } + } else { + // Interpolar posición con easing + float t = easing_(segment_progress_); + int from = getSegmentFrom(); + int to = getSegmentTo(); + + float new_x = path_[from].x + (path_[to].x - path_[from].x) * t; + float new_y = path_[from].y + (path_[to].y - path_[from].y) * t; + sprite_->setPosX(new_x); + sprite_->setPosY(new_y); + } last_dx_ = sprite_->getPosX() - old_x; last_dy_ = sprite_->getPosY() - old_y; - collider_ = getRect(); } @@ -50,17 +192,24 @@ void MovingPlatform::updateAnimation(float 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); + path_ = data.path; + speed_ = data.speed; + loop_mode_ = data.loop; + easing_ = resolveEasing(data.easing); - x1_ = data.x1; - x2_ = data.x2; - y1_ = data.y1; - y2_ = data.y2; + current_segment_ = 0; + direction_ = 1; + segment_progress_ = 0.0F; + waiting_ = false; + wait_timer_ = 0.0F; + + if (!path_.empty()) { + sprite_->setPosX(path_[0].x); + sprite_->setPosY(path_[0].y); + } collider_ = getRect(); + recalcSegmentLength(); } #endif @@ -73,24 +222,3 @@ auto MovingPlatform::getRect() -> SDL_FRect { 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 index 4f0bb42..6d64f93 100644 --- a/source/game/entities/moving_platform.hpp +++ b/source/game/entities/moving_platform.hpp @@ -4,22 +4,32 @@ #include // Para shared_ptr #include // Para string +#include // Para vector class AnimatedSprite; +// Punto de paso en la ruta de una plataforma +struct Waypoint { + float x{0.0F}; // Posición en pixels + float y{0.0F}; + float wait{0.0F}; // Tiempo de parada en este punto (segundos, 0 = sin parada) +}; + +// Modo de recorrido de la ruta +enum class LoopMode { PINGPONG, CIRCULAR }; + +// Tipo de función de easing +using EasingFunc = float (*)(float); + 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 + std::string animation_path; + float speed{30.0F}; // px/s a lo largo del path + LoopMode loop{LoopMode::PINGPONG}; + std::string easing{"linear"}; // Nombre del easing + int frame{0}; // Frame inicial (-1 = random) + std::vector path; // Mínimo 2 puntos }; explicit MovingPlatform(const Data& data); @@ -35,19 +45,31 @@ class MovingPlatform { 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 + void advanceSegment(); + void recalcSegmentLength(); + [[nodiscard]] auto getSegmentFrom() const -> int; + [[nodiscard]] auto getSegmentTo() const -> int; + static auto resolveEasing(const std::string& name) -> EasingFunc; 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 + float last_dx_{0.0F}; + float last_dy_{0.0F}; + + // Estado del path + std::vector path_; + float speed_{0.0F}; + LoopMode loop_mode_{LoopMode::PINGPONG}; + EasingFunc easing_{nullptr}; + + int current_segment_{0}; // Índice del segmento actual + int direction_{1}; // +1 avanzando, -1 retrocediendo (pingpong) + float segment_progress_{0.0F}; // Progreso dentro del segmento (0.0 a 1.0) + float segment_length_{0.0F}; // Longitud del segmento actual en pixels + float wait_timer_{0.0F}; // Tiempo restante de parada + bool waiting_{false}; // Está parado en un waypoint? }; diff --git a/source/game/gameplay/platform_manager.cpp b/source/game/gameplay/platform_manager.cpp index 68299ab..1bc4b15 100644 --- a/source/game/gameplay/platform_manager.cpp +++ b/source/game/gameplay/platform_manager.cpp @@ -84,9 +84,10 @@ auto PlatformManager::checkPlayerOnPlatform(const SDL_FRect& player_collider, fl 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 + // 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 && player_feet <= platform_top + TOLERANCE) { + if (player_feet >= platform_top - TOLERANCE && player_feet <= platform_top + TOLERANCE) { return platform.get(); } } diff --git a/source/game/gameplay/room_loader.cpp b/source/game/gameplay/room_loader.cpp index dac2d3a..b4e0d1e 100644 --- a/source/game/gameplay/room_loader.cpp +++ b/source/game/gameplay/room_loader.cpp @@ -328,70 +328,40 @@ 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; - } + if (platform_node.contains("speed")) { + platform.speed = platform_node["speed"].get_value(); } - - // 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(); - } + if (platform_node.contains("loop")) { + auto loop_str = platform_node["loop"].get_value(); + platform.loop = (loop_str == "circular") ? LoopMode::CIRCULAR : LoopMode::PINGPONG; } - - // Boundaries (in tiles, convert to pixels) - if (platform_node.contains("boundaries")) { - parsePlatformBoundaries(platform_node["boundaries"], platform); + if (platform_node.contains("easing")) { + platform.easing = platform_node["easing"].get_value(); } - - // Optional frame platform.frame = platform_node.contains("frame") ? platform_node["frame"].get_value_or(-1) : -1; + // Path: lista de waypoints en tiles → pixels + if (platform_node.contains("path")) { + for (const auto& wp_node : platform_node["path"]) { + Waypoint wp; + wp.x = wp_node["x"].get_value() * Tile::SIZE; + wp.y = wp_node["y"].get_value() * Tile::SIZE; + if (wp_node.contains("wait")) { + wp.wait = wp_node["wait"].get_value(); + } + platform.path.push_back(wp); + } + } + return platform; } diff --git a/source/game/gameplay/room_loader.hpp b/source/game/gameplay/room_loader.hpp index a1d53f0..5fb4837 100644 --- a/source/game/gameplay/room_loader.hpp +++ b/source/game/gameplay/room_loader.hpp @@ -137,5 +137,4 @@ class RoomLoader { 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); };