From 410b2b548def088af6aa83198953154130e74ecc Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 8 Apr 2026 16:54:26 +0200 Subject: [PATCH] refactor de l'editor --- data/room/03.yaml | 1 - source/game/editor/map_editor.cpp | 833 ++++++++++++++++++---------- source/game/editor/map_editor.hpp | 66 ++- source/game/ui/console_commands.cpp | 35 +- 4 files changed, 622 insertions(+), 313 deletions(-) diff --git a/data/room/03.yaml b/data/room/03.yaml index 169b54a..781ec1c 100644 --- a/data/room/03.yaml +++ b/data/room/03.yaml @@ -72,7 +72,6 @@ enemies: boundaries: position1: {x: 3, y: 2} position2: {x: 19, y: 2} - color: 12 # Objetos en esta habitación items: diff --git a/source/game/editor/map_editor.cpp b/source/game/editor/map_editor.cpp index c762545..1ad3a09 100644 --- a/source/game/editor/map_editor.cpp +++ b/source/game/editor/map_editor.cpp @@ -24,6 +24,7 @@ #include "game/game_control.hpp" // Para GameControl #include "game/gameplay/enemy_manager.hpp" // Para EnemyManager #include "game/gameplay/item_manager.hpp" // Para ItemManager +#include "game/gameplay/platform_manager.hpp" // Para PlatformManager #include "game/gameplay/room.hpp" // Para Room #include "game/options.hpp" // Para Options #include "game/ui/console.hpp" // Para Console @@ -217,13 +218,15 @@ void MapEditor::enter(std::shared_ptr room, std::shared_ptr player // Resetear enemigos a su posición inicial (pueden haberse movido durante el gameplay) room_->resetEnemyPositions(room_data_.enemies); + // Resetear plataformas a su posición inicial + room_->getPlatformManager()->resetPositions(room_data_.platforms); + // Crear la barra de estado statusbar_ = std::make_unique(room_->getNumber()); // Resetear estado (preservar modo de edición en re-enter) drag_ = {}; - selected_enemy_ = -1; - selected_item_ = -1; + selection_.clear(); if (!is_reenter) { brush_tile_ = NO_BRUSH; painting_ = false; @@ -256,7 +259,7 @@ void MapEditor::exit() { } // Restaurar prompt y scope de la consola - selected_enemy_ = -1; + selection_.clear(); Console::get()->setPrompt("> "); Console::get()->setScope("debug"); drag_ = {}; @@ -281,14 +284,28 @@ auto MapEditor::revert() -> std::string { } RoomSaver::saveYAML(file_path_, yaml_, room_data_); - // Resetear enemigos a posiciones originales - room_->resetEnemyPositions(room_data_.enemies); - - // Resetear items (posiciones y colores) - auto* item_mgr = room_->getItemManager(); - for (int i = 0; i < item_mgr->getCount() && i < static_cast(room_data_.items.size()); ++i) { - item_mgr->getItem(i)->setPosition(room_data_.items[i].x, room_data_.items[i].y); + // Rebuild all entities from room_data_ + auto* enemy_mgr = room_->getEnemyManager(); + enemy_mgr->clear(); + for (const auto& e : room_data_.enemies) { + enemy_mgr->addEnemy(Enemy::create(e)); } + + auto* item_mgr = room_->getItemManager(); + item_mgr->clear(); + for (const auto& i : room_data_.items) { + Item::Data item_copy = i; + item_copy.color1 = room_data_.item_color1; + item_copy.color2 = room_data_.item_color2; + item_mgr->addItem(std::make_shared(item_copy)); + } + + auto* platform_mgr = room_->getPlatformManager(); + platform_mgr->clear(); + for (const auto& p : room_data_.platforms) { + platform_mgr->addPlatform(std::make_shared(p)); + } + room_->setItemColors(room_data_.item_color1, room_data_.item_color2); // Restaurar el tilemap completo @@ -296,8 +313,7 @@ auto MapEditor::revert() -> std::string { room_->setTile(i, room_data_.tile_map[i]); } - selected_enemy_ = -1; - selected_item_ = -1; + selection_.clear(); brush_tile_ = NO_BRUSH; return "Reverted to original"; } @@ -314,6 +330,14 @@ 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; + } + RoomSaver::saveYAML(file_path_, yaml_, room_data_); } @@ -395,11 +419,12 @@ void MapEditor::render() { } // Renderizar los marcadores de boundaries y líneas de ruta (debajo de los sprites) - renderEnemyBoundaries(); + renderEntityBoundaries(); - // Renderizar entidades normales: enemigos (animados en posición inicial), items, jugador + // Renderizar entidades normales: enemigos (animados en posición inicial), items, plataformas, jugador room_->renderEnemies(); room_->renderItems(); + room_->renderPlatforms(); player_->render(); // Renderizar highlight de selección (encima de los sprites) @@ -498,8 +523,7 @@ void MapEditor::handleEvent(const SDL_Event& event) { // NOLINT(readability-fun // Click derecho: abrir TilePicker if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_RIGHT) { // Deseleccionar entidades - selected_enemy_ = -1; - selected_item_ = -1; + selection_.clear(); if (editing_collision_) { // Abrir tile picker del collision tileset @@ -536,16 +560,10 @@ void MapEditor::handleEvent(const SDL_Event& event) { // NOLINT(readability-fun SDL_FRect player_rect = player_->getRect(); if (pointInRect(mouse_game_x_, mouse_game_y_, player_rect)) { hit_entity = true; } - if (!hit_entity) { - auto* enemy_mgr = room_->getEnemyManager(); - for (int i = 0; i < enemy_mgr->getCount() && !hit_entity; ++i) { - if (pointInRect(mouse_game_x_, mouse_game_y_, enemy_mgr->getEnemy(i)->getRect())) { hit_entity = true; } - } - } - if (!hit_entity) { - auto* item_mgr = room_->getItemManager(); - for (int i = 0; i < item_mgr->getCount() && !hit_entity; ++i) { - if (pointInRect(mouse_game_x_, mouse_game_y_, item_mgr->getItem(i)->getCollider())) { hit_entity = true; } + // Check all entity types + for (auto type : {EntityType::ENEMY, EntityType::PLATFORM, EntityType::ITEM}) { + for (int i = 0; i < entityCount(type) && !hit_entity; ++i) { + if (pointInRect(mouse_game_x_, mouse_game_y_, entityRect(type, i))) { hit_entity = true; } } } @@ -580,11 +598,9 @@ void MapEditor::handleEvent(const SDL_Event& event) { // NOLINT(readability-fun // Procesa click del ratón: hit test + inicio de drag void MapEditor::handleMouseDown(float game_x, float game_y) { - // Prioridad de hit test: jugador → enemigos (initial) → enemigos (boundaries) → items - - // Helper para iniciar drag - auto start_drag = [&](DragTarget target, int index, float entity_x, float entity_y) { + auto start_drag = [&](DragTarget target, EntityType etype, int index, float entity_x, float entity_y) { drag_.target = target; + drag_.entity_type = etype; drag_.index = index; drag_.offset_x = game_x - entity_x; drag_.offset_y = game_y - entity_y; @@ -593,81 +609,66 @@ void MapEditor::handleMouseDown(float game_x, float game_y) { drag_.moved = false; }; - // 1. Hit test sobre el jugador (8x16) + // 1. Player hit test SDL_FRect player_rect = player_->getRect(); if (pointInRect(game_x, game_y, player_rect)) { - start_drag(DragTarget::PLAYER, -1, player_rect.x, player_rect.y); + start_drag(DragTarget::PLAYER, EntityType::NONE, -1, player_rect.x, player_rect.y); return; } - // 2. Hit test sobre enemigos: posición inicial (usan el rect del sprite vivo) - auto* enemy_mgr = room_->getEnemyManager(); - for (int i = 0; i < enemy_mgr->getCount(); ++i) { - SDL_FRect enemy_rect = enemy_mgr->getEnemy(i)->getRect(); - if (pointInRect(game_x, game_y, enemy_rect)) { - start_drag(DragTarget::ENEMY_INITIAL, i, enemy_rect.x, enemy_rect.y); - return; + // 2. Hit test on entity initials + for (auto type : {EntityType::ENEMY, EntityType::PLATFORM, EntityType::ITEM}) { + for (int i = 0; i < entityCount(type); ++i) { + SDL_FRect rect = entityRect(type, i); + if (pointInRect(game_x, game_y, rect)) { + start_drag(DragTarget::ENTITY_INITIAL, type, i, rect.x, rect.y); + return; + } } } - // 3. Hit test sobre boundaries de enemigos (rectángulos 8x8 en las posiciones de room_data_) - for (int i = 0; i < static_cast(room_data_.enemies.size()); ++i) { - const auto& ed = room_data_.enemies[i]; - constexpr auto SZ = static_cast(Tile::SIZE); - - SDL_FRect b1_rect = {.x = static_cast(ed.x1), .y = static_cast(ed.y1), .w = SZ, .h = SZ}; - if (pointInRect(game_x, game_y, b1_rect)) { - start_drag(DragTarget::ENEMY_BOUND1, i, b1_rect.x, b1_rect.y); - return; - } - - SDL_FRect b2_rect = {.x = static_cast(ed.x2), .y = static_cast(ed.y2), .w = SZ, .h = SZ}; - if (pointInRect(game_x, game_y, b2_rect)) { - start_drag(DragTarget::ENEMY_BOUND2, i, b2_rect.x, b2_rect.y); - return; + // 3. Hit test on boundaries (enemies and platforms) + for (auto type : {EntityType::ENEMY, EntityType::PLATFORM}) { + for (int i = 0; i < entityDataCount(type); ++i) { + auto bd = entityBoundaries(type, i); + constexpr auto SZ = static_cast(Tile::SIZE); + SDL_FRect b1_rect = {static_cast(bd.x1), static_cast(bd.y1), SZ, SZ}; + if (pointInRect(game_x, game_y, b1_rect)) { + start_drag(DragTarget::ENTITY_BOUND1, type, i, b1_rect.x, b1_rect.y); + return; + } + SDL_FRect b2_rect = {static_cast(bd.x2), static_cast(bd.y2), SZ, SZ}; + if (pointInRect(game_x, game_y, b2_rect)) { + start_drag(DragTarget::ENTITY_BOUND2, type, i, b2_rect.x, b2_rect.y); + return; + } } } - // 4. Hit test sobre items (8x8) - auto* item_mgr = room_->getItemManager(); - for (int i = 0; i < item_mgr->getCount(); ++i) { - SDL_FRect item_rect = item_mgr->getItem(i)->getCollider(); - if (pointInRect(game_x, game_y, item_rect)) { - start_drag(DragTarget::ITEM, i, item_rect.x, item_rect.y); - return; - } - } - - // Click en el fondo: deseleccionar todo - selected_enemy_ = -1; - selected_item_ = -1; + // 4. Background click: deselect + selection_.clear(); } // Procesa soltar el ratón: commit del drag -void MapEditor::handleMouseUp() { // NOLINT(readability-function-cognitive-complexity) +void MapEditor::handleMouseUp() { if (drag_.target == DragTarget::NONE) { return; } - const int IDX = drag_.index; - // Si no se movió: fue un click → seleccionar/deseleccionar if (!drag_.moved) { - if (drag_.target == DragTarget::ENEMY_INITIAL) { - selected_enemy_ = (selected_enemy_ == IDX) ? -1 : IDX; - selected_item_ = -1; - } else if (drag_.target == DragTarget::ITEM) { - selected_item_ = (selected_item_ == IDX) ? -1 : IDX; - selected_enemy_ = -1; + if (drag_.target == DragTarget::ENTITY_INITIAL) { + if (selection_.is(drag_.entity_type) && selection_.index == drag_.index) { + selection_.clear(); // deselect + } else { + selection_ = {drag_.entity_type, drag_.index}; // select + } } else { - selected_enemy_ = -1; - selected_item_ = -1; + selection_.clear(); } drag_ = {}; return; } // Hubo movimiento: commit del drag - const int SNAP_X = static_cast(drag_.snap_x); - const int SNAP_Y = static_cast(drag_.snap_y); bool changed = false; switch (drag_.target) { @@ -676,43 +677,10 @@ void MapEditor::handleMouseUp() { // NOLINT(readability-function-cognitive-comp player_->finalizeDebugTeleport(); break; - case DragTarget::ENEMY_INITIAL: - if (IDX >= 0 && IDX < static_cast(room_data_.enemies.size())) { - room_data_.enemies[IDX].x = drag_.snap_x; - room_data_.enemies[IDX].y = drag_.snap_y; - room_->getEnemyManager()->getEnemy(IDX)->resetToInitialPosition(room_data_.enemies[IDX]); - selected_enemy_ = IDX; // Seleccionar el enemigo arrastrado - changed = true; - } - break; - - case DragTarget::ENEMY_BOUND1: - if (IDX >= 0 && IDX < static_cast(room_data_.enemies.size())) { - room_data_.enemies[IDX].x1 = SNAP_X; - room_data_.enemies[IDX].y1 = SNAP_Y; - room_->getEnemyManager()->getEnemy(IDX)->resetToInitialPosition(room_data_.enemies[IDX]); - selected_enemy_ = IDX; - changed = true; - } - break; - - case DragTarget::ENEMY_BOUND2: - if (IDX >= 0 && IDX < static_cast(room_data_.enemies.size())) { - room_data_.enemies[IDX].x2 = SNAP_X; - room_data_.enemies[IDX].y2 = SNAP_Y; - room_->getEnemyManager()->getEnemy(IDX)->resetToInitialPosition(room_data_.enemies[IDX]); - selected_enemy_ = IDX; - changed = true; - } - break; - - case DragTarget::ITEM: - if (IDX >= 0 && IDX < room_->getItemManager()->getCount()) { - room_->getItemManager()->getItem(IDX)->setPosition(drag_.snap_x, drag_.snap_y); - selected_item_ = IDX; - selected_enemy_ = -1; - changed = true; - } + case DragTarget::ENTITY_INITIAL: + case DragTarget::ENTITY_BOUND1: + case DragTarget::ENTITY_BOUND2: + changed = commitEntityDrag(); break; case DragTarget::NONE: @@ -723,6 +691,142 @@ void MapEditor::handleMouseUp() { // NOLINT(readability-function-cognitive-comp drag_ = {}; } +// Commit de un drag de entidad (initial, bound1, bound2) para cualquier EntityType +auto MapEditor::commitEntityDrag() -> bool { + const int IDX = drag_.index; + const int SNAP_X = static_cast(drag_.snap_x); + const int SNAP_Y = static_cast(drag_.snap_y); + + switch (drag_.target) { + case DragTarget::ENTITY_INITIAL: + switch (drag_.entity_type) { + case EntityType::ENEMY: + if (IDX >= 0 && IDX < static_cast(room_data_.enemies.size())) { + room_data_.enemies[IDX].x = drag_.snap_x; + room_data_.enemies[IDX].y = drag_.snap_y; + room_->getEnemyManager()->getEnemy(IDX)->resetToInitialPosition(room_data_.enemies[IDX]); + selection_ = {EntityType::ENEMY, IDX}; + return true; + } + break; + case EntityType::ITEM: + if (IDX >= 0 && IDX < room_->getItemManager()->getCount()) { + room_->getItemManager()->getItem(IDX)->setPosition(drag_.snap_x, drag_.snap_y); + selection_ = {EntityType::ITEM, IDX}; + return true; + } + 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]); + selection_ = {EntityType::PLATFORM, IDX}; + return true; + } + break; + default: + break; + } + break; + + case DragTarget::ENTITY_BOUND1: + switch (drag_.entity_type) { + case EntityType::ENEMY: + if (IDX >= 0 && IDX < static_cast(room_data_.enemies.size())) { + room_data_.enemies[IDX].x1 = SNAP_X; + room_data_.enemies[IDX].y1 = SNAP_Y; + room_->getEnemyManager()->getEnemy(IDX)->resetToInitialPosition(room_data_.enemies[IDX]); + selection_ = {EntityType::ENEMY, IDX}; + 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; + } + break; + + case DragTarget::ENTITY_BOUND2: + switch (drag_.entity_type) { + case EntityType::ENEMY: + if (IDX >= 0 && IDX < static_cast(room_data_.enemies.size())) { + room_data_.enemies[IDX].x2 = SNAP_X; + room_data_.enemies[IDX].y2 = SNAP_Y; + room_->getEnemyManager()->getEnemy(IDX)->resetToInitialPosition(room_data_.enemies[IDX]); + selection_ = {EntityType::ENEMY, IDX}; + 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; + } + break; + + default: + break; + } + return false; +} + +// Mueve visualmente la entidad arrastrada a la posición snapped +void MapEditor::moveEntityVisual() { + switch (drag_.target) { + case DragTarget::ENTITY_INITIAL: + switch (drag_.entity_type) { + case EntityType::ENEMY: + if (drag_.index >= 0 && drag_.index < room_->getEnemyManager()->getCount()) { + Enemy::Data temp_data = room_data_.enemies[drag_.index]; + temp_data.x = drag_.snap_x; + temp_data.y = drag_.snap_y; + room_->getEnemyManager()->getEnemy(drag_.index)->resetToInitialPosition(temp_data); + } + break; + case EntityType::ITEM: + if (drag_.index >= 0 && drag_.index < room_->getItemManager()->getCount()) { + room_->getItemManager()->getItem(drag_.index)->setPosition(drag_.snap_x, drag_.snap_y); + } + break; + 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; + room_->getPlatformManager()->getPlatform(drag_.index)->resetToInitialPosition(temp_data); + } + break; + default: + break; + } + break; + + case DragTarget::ENTITY_BOUND1: + case DragTarget::ENTITY_BOUND2: + // Los boundaries se actualizan visualmente en renderEntityBoundaries() via drag_.snap + break; + + default: + break; + } +} + // Actualiza la posición snapped durante el drag void MapEditor::updateDrag() { float raw_x = mouse_game_x_ - drag_.offset_x; @@ -745,28 +849,10 @@ void MapEditor::updateDrag() { player_->setDebugPosition(drag_.snap_x, drag_.snap_y); break; - case DragTarget::ENEMY_INITIAL: - if (drag_.index >= 0 && drag_.index < room_->getEnemyManager()->getCount()) { - // Mover el sprite vivo del enemigo durante el arrastre - auto& enemy = room_->getEnemyManager()->getEnemy(drag_.index); - Enemy::Data temp_data = room_data_.enemies[drag_.index]; - temp_data.x = drag_.snap_x; - temp_data.y = drag_.snap_y; - enemy->resetToInitialPosition(temp_data); - } - break; - - case DragTarget::ENEMY_BOUND1: - // Los boundaries se actualizan visualmente en renderEnemyBoundaries() via drag_.snap - break; - - case DragTarget::ENEMY_BOUND2: - break; - - case DragTarget::ITEM: - if (drag_.index >= 0 && drag_.index < room_->getItemManager()->getCount()) { - room_->getItemManager()->getItem(drag_.index)->setPosition(drag_.snap_x, drag_.snap_y); - } + case DragTarget::ENTITY_INITIAL: + case DragTarget::ENTITY_BOUND1: + case DragTarget::ENTITY_BOUND2: + moveEntityVisual(); break; case DragTarget::NONE: @@ -781,25 +867,14 @@ void MapEditor::renderSelectionHighlight() { constexpr auto SZ = static_cast(Tile::SIZE); - // Highlight del enemigo seleccionado (persistente, color bright_green) - if (selected_enemy_ >= 0 && selected_enemy_ < room_->getEnemyManager()->getCount()) { - SDL_FRect enemy_rect = room_->getEnemyManager()->getEnemy(selected_enemy_)->getRect(); + // Highlight de la entidad seleccionada (persistente, color bright_green) + if (!selection_.isNone() && selection_.index < entityCount(selection_.type)) { + SDL_FRect rect = entityRect(selection_.type, selection_.index); SDL_FRect border = { - .x = enemy_rect.x - 1, - .y = enemy_rect.y - 1, - .w = enemy_rect.w + 2, - .h = enemy_rect.h + 2}; - game_surface->drawRectBorder(&border, 9); - } - - // Highlight del item seleccionado (persistente, color bright_green) - if (selected_item_ >= 0 && selected_item_ < room_->getItemManager()->getCount()) { - SDL_FRect item_rect = room_->getItemManager()->getItem(selected_item_)->getCollider(); - SDL_FRect border = { - .x = item_rect.x - 1, - .y = item_rect.y - 1, - .w = item_rect.w + 2, - .h = item_rect.h + 2}; + .x = rect.x - 1, + .y = rect.y - 1, + .w = rect.w + 2, + .h = rect.h + 2}; game_surface->drawRectBorder(&border, 9); } @@ -813,20 +888,15 @@ void MapEditor::renderSelectionHighlight() { case DragTarget::PLAYER: highlight_rect = player_->getRect(); break; - case DragTarget::ENEMY_INITIAL: - if (drag_.index >= 0 && drag_.index < room_->getEnemyManager()->getCount()) { - highlight_rect = room_->getEnemyManager()->getEnemy(drag_.index)->getRect(); + case DragTarget::ENTITY_INITIAL: + if (drag_.index >= 0 && drag_.index < entityCount(drag_.entity_type)) { + highlight_rect = entityRect(drag_.entity_type, drag_.index); } break; - case DragTarget::ENEMY_BOUND1: - case DragTarget::ENEMY_BOUND2: + case DragTarget::ENTITY_BOUND1: + case DragTarget::ENTITY_BOUND2: highlight_rect = {.x = drag_.snap_x, .y = drag_.snap_y, .w = SZ, .h = SZ}; break; - case DragTarget::ITEM: - if (drag_.index >= 0 && drag_.index < room_->getItemManager()->getCount()) { - highlight_rect = room_->getItemManager()->getItem(drag_.index)->getCollider(); - } - break; case DragTarget::NONE: return; } @@ -849,8 +919,73 @@ auto MapEditor::pointInRect(float px, float py, const SDL_FRect& rect) -> bool { return px >= rect.x && px < rect.x + rect.w && py >= rect.y && py < rect.y + rect.h; } -// Dibuja marcadores de boundaries y líneas de ruta para los enemigos -void MapEditor::renderEnemyBoundaries() { +// --- Entity helpers: acceso abstracto a datos de entidad por tipo --- + +auto MapEditor::entityCount(EntityType type) const -> int { + switch (type) { + case EntityType::ENEMY: return room_->getEnemyManager()->getCount(); + case EntityType::ITEM: return room_->getItemManager()->getCount(); + case EntityType::PLATFORM: return room_->getPlatformManager()->getCount(); + default: return 0; + } +} + +auto MapEditor::entityRect(EntityType type, int index) -> SDL_FRect { + switch (type) { + case EntityType::ENEMY: return room_->getEnemyManager()->getEnemy(index)->getRect(); + case EntityType::ITEM: return room_->getItemManager()->getItem(index)->getCollider(); + case EntityType::PLATFORM: return room_->getPlatformManager()->getPlatform(index)->getRect(); + default: return {}; + } +} + +auto MapEditor::entityHasBoundaries(EntityType type) -> bool { + return type == EntityType::ENEMY || type == EntityType::PLATFORM; +} + +auto MapEditor::entityBoundaries(EntityType type, int index) const -> BoundaryData { + switch (type) { + case EntityType::ENEMY: { + 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 {}; + } +} + +auto MapEditor::entityPosition(EntityType type, int index) const -> std::pair { + switch (type) { + case EntityType::ENEMY: return {room_data_.enemies[index].x, room_data_.enemies[index].y}; + case EntityType::ITEM: return {room_data_.items[index].x, room_data_.items[index].y}; + case EntityType::PLATFORM: return {room_data_.platforms[index].x, room_data_.platforms[index].y}; + default: return {0.0F, 0.0F}; + } +} + +auto MapEditor::entityDataCount(EntityType type) const -> int { + switch (type) { + case EntityType::ENEMY: return static_cast(room_data_.enemies.size()); + case EntityType::ITEM: return static_cast(room_data_.items.size()); + case EntityType::PLATFORM: return static_cast(room_data_.platforms.size()); + default: return 0; + } +} + +auto MapEditor::entityLabel(EntityType type) -> const char* { + switch (type) { + case EntityType::ENEMY: return "enemy"; + case EntityType::ITEM: return "item"; + case EntityType::PLATFORM: return "platform"; + default: return "none"; + } +} + +// Dibuja marcadores de boundaries y líneas de ruta para enemigos y plataformas +void MapEditor::renderEntityBoundaries() { auto game_surface = Screen::get()->getRendererSurface(); if (!game_surface) { return; } @@ -858,39 +993,42 @@ void MapEditor::renderEnemyBoundaries() { const Uint8 COLOR_BOUND2 = 13; const Uint8 COLOR_ROUTE = 15; - for (int i = 0; i < static_cast(room_data_.enemies.size()); ++i) { - const auto& enemy = room_data_.enemies[i]; - constexpr float HALF = Tile::SIZE / 2.0F; + for (auto type : {EntityType::ENEMY, EntityType::PLATFORM}) { + for (int i = 0; i < entityDataCount(type); ++i) { + auto [pos_x, pos_y] = entityPosition(type, i); + auto bd = entityBoundaries(type, i); + constexpr float HALF = Tile::SIZE / 2.0F; - // Posiciones base (pueden estar siendo arrastradas) - float init_x = enemy.x; - float init_y = enemy.y; - auto b1_x = static_cast(enemy.x1); - auto b1_y = static_cast(enemy.y1); - auto b2_x = static_cast(enemy.x2); - auto b2_y = static_cast(enemy.y2); + // 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 este enemigo, usar la posición snapped - if (drag_.index == i) { - if (drag_.target == DragTarget::ENEMY_BOUND1) { - b1_x = drag_.snap_x; - b1_y = drag_.snap_y; - } else if (drag_.target == DragTarget::ENEMY_BOUND2) { - b2_x = drag_.snap_x; - b2_y = drag_.snap_y; - } else if (drag_.target == DragTarget::ENEMY_INITIAL) { - init_x = drag_.snap_x; - init_y = drag_.snap_y; + // 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; + } } + + // 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_BOUND1); + renderBoundaryMarker(b2_x, b2_y, COLOR_BOUND2); } - - // 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_BOUND1); - renderBoundaryMarker(b2_x, b2_y, COLOR_BOUND2); } } @@ -950,17 +1088,14 @@ void MapEditor::updateStatusBarInfo() { // NOLINT(readability-function-cognitiv case DragTarget::PLAYER: line5 = "dragging: player"; break; - case DragTarget::ENEMY_INITIAL: - line5 = "dragging: enemy " + std::to_string(drag_.index); + case DragTarget::ENTITY_INITIAL: + line5 = std::string("dragging: ") + entityLabel(drag_.entity_type) + " " + std::to_string(drag_.index); break; - case DragTarget::ENEMY_BOUND1: - line5 = "dragging: e" + std::to_string(drag_.index) + " bound1"; + case DragTarget::ENTITY_BOUND1: + line5 = std::string("dragging: ") + entityLabel(drag_.entity_type)[0] + std::to_string(drag_.index) + " bound1"; break; - case DragTarget::ENEMY_BOUND2: - line5 = "dragging: e" + std::to_string(drag_.index) + " bound2"; - break; - case DragTarget::ITEM: - line5 = "dragging: item " + std::to_string(drag_.index); + case DragTarget::ENTITY_BOUND2: + line5 = std::string("dragging: ") + entityLabel(drag_.entity_type)[0] + std::to_string(drag_.index) + " bound2"; break; case DragTarget::NONE: break; @@ -968,37 +1103,59 @@ void MapEditor::updateStatusBarInfo() { // NOLINT(readability-function-cognitiv } // Líneas 2-3 según selección - if (selected_enemy_ >= 0 && selected_enemy_ < static_cast(room_data_.enemies.size())) { - // Enemigo seleccionado - const auto& e = room_data_.enemies[selected_enemy_]; - std::string anim = e.animation_path; - auto dot = anim.rfind('.'); - if (dot != std::string::npos) { anim = anim.substr(0, dot); } + switch (selection_.type) { + case EntityType::ENEMY: + if (selection_.index < static_cast(room_data_.enemies.size())) { + const auto& e = room_data_.enemies[selection_.index]; + std::string anim = e.animation_path; + auto dot = anim.rfind('.'); + if (dot != std::string::npos) { anim = anim.substr(0, dot); } - line2 = "enemy " + std::to_string(selected_enemy_) + ": " + anim; - line3 = "vx:" + std::to_string(static_cast(e.vx)) + - " vy:" + std::to_string(static_cast(e.vy)); - if (e.flip) { line3 += " flip"; } - if (e.mirror) { line3 += " mirror"; } - } else if (selected_item_ >= 0 && selected_item_ < static_cast(room_data_.items.size())) { - // Item seleccionado - const auto& it = room_data_.items[selected_item_]; - line2 = "item " + std::to_string(selected_item_) + ": tile=" + std::to_string(it.tile) + - " counter=" + std::to_string(it.counter); - line3 = "tileset: " + it.tile_set_file; - } else { - // Propiedades de la habitación - std::string conv = "none"; - if (room_data_.conveyor_belt_direction < 0) { - conv = "left"; - } else if (room_data_.conveyor_belt_direction > 0) { - conv = "right"; + line2 = "enemy " + std::to_string(selection_.index) + ": " + anim; + line3 = "vx:" + std::to_string(static_cast(e.vx)) + + " vy:" + std::to_string(static_cast(e.vy)); + if (e.flip) { line3 += " flip"; } + if (e.mirror) { line3 += " mirror"; } + } + break; + + case EntityType::ITEM: + if (selection_.index < static_cast(room_data_.items.size())) { + const auto& it = room_data_.items[selection_.index]; + line2 = "item " + std::to_string(selection_.index) + ": tile=" + std::to_string(it.tile) + + " counter=" + std::to_string(it.counter); + line3 = "tileset: " + it.tile_set_file; + } + break; + + case EntityType::PLATFORM: + 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); } + + 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)); + } + break; + + case EntityType::NONE: { + // Propiedades de la habitación + std::string conv = "none"; + if (room_data_.conveyor_belt_direction < 0) { + conv = "left"; + } else if (room_data_.conveyor_belt_direction > 0) { + conv = "right"; + } + + line2 = "conv:" + conv; + line3 = "u:" + conn(room_data_.upper_room) + " d:" + conn(room_data_.lower_room) + + " l:" + conn(room_data_.left_room) + " r:" + conn(room_data_.right_room) + + " itm:" + std::to_string(room_data_.item_color1) + "/" + std::to_string(room_data_.item_color2); + break; } - - line2 = "conv:" + conv; - line3 = "u:" + conn(room_data_.upper_room) + " d:" + conn(room_data_.lower_room) + - " l:" + conn(room_data_.left_room) + " r:" + conn(room_data_.right_room) + - " itm:" + std::to_string(room_data_.item_color1) + "/" + std::to_string(room_data_.item_color2); } // Línea 4: brush activo @@ -1015,30 +1172,21 @@ void MapEditor::updateStatusBarInfo() { // NOLINT(readability-function-cognitiv statusbar_->setLine5(line5); // Actualizar el prompt de la consola según la selección - if (selected_enemy_ >= 0) { - Console::get()->setPrompt("enemy " + std::to_string(selected_enemy_) + "> "); - } else if (selected_item_ >= 0) { - Console::get()->setPrompt("item " + std::to_string(selected_item_) + "> "); - } else { + if (selection_.isNone()) { Console::get()->setPrompt("room> "); + } else { + Console::get()->setPrompt(std::string(entityLabel(selection_.type)) + " " + std::to_string(selection_.index) + "> "); } } -// ¿Hay un enemigo seleccionado? -auto MapEditor::hasSelectedEnemy() const -> bool { - return selected_enemy_ >= 0 && selected_enemy_ < static_cast(room_data_.enemies.size()); -} - // Devuelve las propiedades válidas de SET según la selección actual auto MapEditor::getSetCompletions() const -> std::vector { - if (hasSelectedEnemy()) { - return {"ANIMATION", "COLOR", "VX", "VY", "FLIP", "MIRROR"}; + 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"}; + default: return {"ITEMCOLOR1", "ITEMCOLOR2", "CONVEYOR", "TILESET", "UP", "DOWN", "LEFT", "RIGHT"}; } - if (hasSelectedItem()) { - return {"TILE", "COUNTER"}; - } - // Room - return {"ITEMCOLOR1", "ITEMCOLOR2", "CONVEYOR", "TILESET", "UP", "DOWN", "LEFT", "RIGHT"}; } // Modifica una propiedad del enemigo seleccionado @@ -1046,7 +1194,7 @@ auto MapEditor::setEnemyProperty(const std::string& property, const std::string& if (!active_) { return "Editor not active"; } if (!hasSelectedEnemy()) { return "No enemy selected"; } - auto& enemy = room_data_.enemies[selected_enemy_]; + auto& enemy = room_data_.enemies[selection_.index]; if (property == "ANIMATION") { std::string anim = toLower(value); @@ -1057,7 +1205,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_) = Enemy::create(enemy); + enemy_mgr->getEnemy(selection_.index) = Enemy::create(enemy); } catch (const std::exception& e) { enemy.animation_path = old_anim; // Restaurar si falla return std::string("Error: ") + e.what(); @@ -1077,7 +1225,7 @@ auto MapEditor::setEnemyProperty(const std::string& property, const std::string& } catch (...) { return "Invalid value: " + value; } enemy.vy = 0.0F; // No se permiten velocidades en los dos ejes - room_->getEnemyManager()->getEnemy(selected_enemy_)->resetToInitialPosition(enemy); + room_->getEnemyManager()->getEnemy(selection_.index)->resetToInitialPosition(enemy); autosave(); return "vx: " + std::to_string(static_cast(enemy.vx)) + " vy: 0"; } @@ -1088,7 +1236,7 @@ auto MapEditor::setEnemyProperty(const std::string& property, const std::string& } catch (...) { return "Invalid value: " + value; } enemy.vx = 0.0F; // No se permiten velocidades en los dos ejes - room_->getEnemyManager()->getEnemy(selected_enemy_)->resetToInitialPosition(enemy); + room_->getEnemyManager()->getEnemy(selection_.index)->resetToInitialPosition(enemy); autosave(); return "vy: " + std::to_string(static_cast(enemy.vy)) + " vx: 0"; } @@ -1100,7 +1248,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_) = Enemy::create(enemy); + enemy_mgr->getEnemy(selection_.index) = Enemy::create(enemy); } catch (const std::exception& e) { return std::string("Error: ") + e.what(); } @@ -1116,7 +1264,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_) = Enemy::create(enemy); + enemy_mgr->getEnemy(selection_.index) = Enemy::create(enemy); } catch (const std::exception& e) { return std::string("Error: ") + e.what(); } @@ -1125,7 +1273,7 @@ auto MapEditor::setEnemyProperty(const std::string& property, const std::string& return std::string("mirror: ") + (enemy.mirror ? "on" : "off"); } - return "Unknown property: " + property + " (use: animation, color, vx, vy, flip, mirror)"; + return "Unknown property: " + property + " (use: animation, vx, vy, flip, mirror)"; } // Crea un nuevo enemigo con valores por defecto, centrado en la habitación @@ -1154,10 +1302,11 @@ auto MapEditor::addEnemy() -> std::string { room_->getEnemyManager()->addEnemy(Enemy::create(new_enemy)); // Seleccionar el nuevo enemigo - selected_enemy_ = static_cast(room_data_.enemies.size()) - 1; + int new_index = static_cast(room_data_.enemies.size()) - 1; + selection_ = {EntityType::ENEMY, new_index}; autosave(); - return "Added enemy " + std::to_string(selected_enemy_); + return "Added enemy " + std::to_string(new_index); } // Elimina el enemigo seleccionado @@ -1165,7 +1314,7 @@ auto MapEditor::deleteEnemy() -> std::string { if (!active_) { return "Editor not active"; } if (!hasSelectedEnemy()) { return "No enemy selected"; } - const int IDX = selected_enemy_; + const int IDX = selection_.index; // Eliminar de los datos room_data_.enemies.erase(room_data_.enemies.begin() + IDX); @@ -1177,7 +1326,7 @@ auto MapEditor::deleteEnemy() -> std::string { enemy_mgr->addEnemy(Enemy::create(enemy_data)); } - selected_enemy_ = -1; + selection_.clear(); autosave(); return "Deleted enemy " + std::to_string(IDX); } @@ -1188,7 +1337,7 @@ auto MapEditor::duplicateEnemy() -> std::string { if (!hasSelectedEnemy()) { return "No enemy selected"; } // Copiar datos del enemigo seleccionado - Enemy::Data copy = room_data_.enemies[selected_enemy_]; + Enemy::Data copy = room_data_.enemies[selection_.index]; // Desplazar un tile a la derecha copy.x += Tile::SIZE; @@ -1200,10 +1349,11 @@ auto MapEditor::duplicateEnemy() -> std::string { room_->getEnemyManager()->addEnemy(Enemy::create(copy)); // Seleccionar el nuevo enemigo - selected_enemy_ = static_cast(room_data_.enemies.size()) - 1; + int new_index = static_cast(room_data_.enemies.size()) - 1; + selection_ = {EntityType::ENEMY, new_index}; autosave(); - return "Duplicated as enemy " + std::to_string(selected_enemy_); + return "Duplicated as enemy " + std::to_string(new_index); } // Modifica una propiedad de la habitación @@ -1566,17 +1716,12 @@ auto MapEditor::deleteRoom() -> std::string { // NOLINT(readability-function-co return "Deleted room " + deleted_name + ", moved to " + target; } -// ¿Hay un item seleccionado? -auto MapEditor::hasSelectedItem() const -> bool { - return selected_item_ >= 0 && selected_item_ < static_cast(room_data_.items.size()); -} - // Modifica una propiedad del item seleccionado auto MapEditor::setItemProperty(const std::string& property, const std::string& value) -> std::string { if (!active_) { return "Editor not active"; } if (!hasSelectedItem()) { return "No item selected"; } - auto& item = room_data_.items[selected_item_]; + auto& item = room_data_.items[selection_.index]; if (property == "TILE") { // Abrir el tile picker visual @@ -1604,8 +1749,8 @@ void MapEditor::openTilePicker(const std::string& tileset_name, int current_tile tile_picker_.on_select = [this](int tile) { if (!hasSelectedItem()) { return; } - room_data_.items[selected_item_].tile = tile; - room_->getItemManager()->getItem(selected_item_)->setTile(tile); + room_data_.items[selection_.index].tile = tile; + room_->getItemManager()->getItem(selection_.index)->setTile(tile); autosave(); }; // Pasar color de fondo de la habitación + color de sustitución del item @@ -1628,11 +1773,11 @@ auto MapEditor::addItem() -> std::string { room_data_.items.push_back(new_item); room_->getItemManager()->addItem(std::make_shared(new_item)); - selected_item_ = static_cast(room_data_.items.size()) - 1; - selected_enemy_ = -1; + int new_index = static_cast(room_data_.items.size()) - 1; + selection_ = {EntityType::ITEM, new_index}; autosave(); - return "Added item " + std::to_string(selected_item_); + return "Added item " + std::to_string(new_index); } // Elimina el item seleccionado @@ -1640,7 +1785,7 @@ auto MapEditor::deleteItem() -> std::string { if (!active_) { return "Editor not active"; } if (!hasSelectedItem()) { return "No item selected"; } - const int IDX = selected_item_; + const int IDX = selection_.index; room_data_.items.erase(room_data_.items.begin() + IDX); // Recrear todos los items (los índices cambian al borrar) @@ -1650,7 +1795,7 @@ auto MapEditor::deleteItem() -> std::string { item_mgr->addItem(std::make_shared(item_data)); } - selected_item_ = -1; + selection_.clear(); autosave(); return "Deleted item " + std::to_string(IDX); } @@ -1660,17 +1805,137 @@ auto MapEditor::duplicateItem() -> std::string { if (!active_) { return "Editor not active"; } if (!hasSelectedItem()) { return "No item selected"; } - Item::Data copy = room_data_.items[selected_item_]; + Item::Data copy = room_data_.items[selection_.index]; copy.x += Tile::SIZE; room_data_.items.push_back(copy); room_->getItemManager()->addItem(std::make_shared(copy)); - selected_item_ = static_cast(room_data_.items.size()) - 1; - selected_enemy_ = -1; + int new_index = static_cast(room_data_.items.size()) - 1; + selection_ = {EntityType::ITEM, new_index}; autosave(); - return "Duplicated as item " + std::to_string(selected_item_); + return "Duplicated as item " + std::to_string(new_index); +} + +// Modifica una propiedad de la plataforma seleccionada +auto MapEditor::setPlatformProperty(const std::string& property, const std::string& value) -> std::string { + if (!active_) { return "Editor not active"; } + if (!hasSelectedPlatform()) { return "No platform selected"; } + + auto& platform = room_data_.platforms[selection_.index]; + + if (property == "ANIMATION") { + std::string anim = toLower(value); + if (anim.find('.') == std::string::npos) { anim += ".yaml"; } + + std::string old_anim = platform.animation_path; + platform.animation_path = anim; + try { + auto* platform_mgr = room_->getPlatformManager(); + platform_mgr->getPlatform(selection_.index) = std::make_shared(platform); + } catch (const std::exception& e) { + platform.animation_path = old_anim; + return std::string("Error: ") + e.what(); + } + + autosave(); + return "animation: " + anim; + } + + if (property == "VX") { + try { + platform.vx = 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"; + } + + if (property == "VY") { + try { + platform.vy = std::stof(value); + } catch (...) { return "Invalid value: " + value; } + platform.vx = 0.0F; + + room_->getPlatformManager()->getPlatform(selection_.index)->resetToInitialPosition(platform); + autosave(); + return "vy: " + std::to_string(static_cast(platform.vy)) + " vx: 0"; + } + + return "Unknown property: " + property + " (use: animation, vx, vy)"; +} + +// 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; + + room_data_.platforms.push_back(new_platform); + room_->getPlatformManager()->addPlatform(std::make_shared(new_platform)); + + int new_index = static_cast(room_data_.platforms.size()) - 1; + selection_ = {EntityType::PLATFORM, new_index}; + + autosave(); + return "Added platform " + std::to_string(new_index); +} + +// Elimina la plataforma seleccionada +auto MapEditor::deletePlatform() -> std::string { + if (!active_) { return "Editor not active"; } + if (!hasSelectedPlatform()) { return "No platform selected"; } + + const int IDX = selection_.index; + room_data_.platforms.erase(room_data_.platforms.begin() + IDX); + + // Recrear todas las plataformas (los índices cambian al borrar) + auto* platform_mgr = room_->getPlatformManager(); + platform_mgr->clear(); + for (const auto& platform_data : room_data_.platforms) { + platform_mgr->addPlatform(std::make_shared(platform_data)); + } + + selection_.clear(); + autosave(); + return "Deleted platform " + std::to_string(IDX); +} + +// Duplica la plataforma seleccionada (la pone un tile a la derecha) +auto MapEditor::duplicatePlatform() -> std::string { + if (!active_) { return "Editor not active"; } + 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; + + room_data_.platforms.push_back(copy); + room_->getPlatformManager()->addPlatform(std::make_shared(copy)); + + int new_index = static_cast(room_data_.platforms.size()) - 1; + selection_ = {EntityType::PLATFORM, new_index}; + + autosave(); + return "Duplicated as platform " + std::to_string(new_index); } // Elige un color de grid que contraste con el fondo diff --git a/source/game/editor/map_editor.hpp b/source/game/editor/map_editor.hpp index 75d681a..b02ea65 100644 --- a/source/game/editor/map_editor.hpp +++ b/source/game/editor/map_editor.hpp @@ -10,15 +10,28 @@ #include "external/fkyaml_node.hpp" // Para fkyaml::node #include "game/editor/mini_map.hpp" // Para MiniMap #include "game/editor/tile_picker.hpp" // Para TilePicker -#include "game/entities/enemy.hpp" // Para Enemy::Data -#include "game/entities/item.hpp" // Para Item::Data -#include "game/entities/player.hpp" // Para Player::SpawnData -#include "game/gameplay/room.hpp" // Para Room::Data -#include "game/gameplay/scoreboard.hpp" // Para Scoreboard::Data -#include "game/options.hpp" // Para Options::Cheat +#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/entities/player.hpp" // Para Player::SpawnData +#include "game/gameplay/room.hpp" // Para Room::Data +#include "game/gameplay/scoreboard.hpp" // Para Scoreboard::Data +#include "game/options.hpp" // Para Options::Cheat class EditorStatusBar; +// Tipo de entidad editable en el editor +enum class EntityType { NONE, ENEMY, ITEM, PLATFORM }; + +// Seleccion unificada: una sola entidad seleccionada a la vez +struct Selection { + EntityType type{EntityType::NONE}; + int index{-1}; + void clear() { type = EntityType::NONE; index = -1; } + [[nodiscard]] auto isNone() const -> bool { return type == EntityType::NONE; } + [[nodiscard]] auto is(EntityType t) const -> bool { return type == t && index >= 0; } +}; + class MapEditor { public: static void init(); // [SINGLETON] Crea el objeto @@ -39,7 +52,7 @@ class MapEditor { auto addEnemy() -> std::string; auto deleteEnemy() -> std::string; auto duplicateEnemy() -> std::string; - [[nodiscard]] auto hasSelectedEnemy() const -> bool; + [[nodiscard]] auto hasSelectedEnemy() const -> bool { return selection_.is(EntityType::ENEMY); } [[nodiscard]] auto getSetCompletions() const -> std::vector; // Comandos para propiedades de la habitación @@ -62,9 +75,19 @@ class MapEditor { auto addItem() -> std::string; auto deleteItem() -> std::string; auto duplicateItem() -> std::string; - [[nodiscard]] auto hasSelectedItem() const -> bool; + [[nodiscard]] auto hasSelectedItem() const -> bool { return selection_.is(EntityType::ITEM); } void openTilePicker(const std::string& tileset_name, int current_tile); + // Comandos para plataformas + auto setPlatformProperty(const std::string& property, const std::string& value) -> std::string; + auto addPlatform() -> std::string; + auto deletePlatform() -> std::string; + auto duplicatePlatform() -> std::string; + [[nodiscard]] auto hasSelectedPlatform() const -> bool { return selection_.is(EntityType::PLATFORM); } + + // Seleccion unificada + [[nodiscard]] auto getSelectionType() const -> EntityType { return selection_.type; } + private: static MapEditor* instance_; // NOLINT(readability-identifier-naming) [SINGLETON] Objeto privado @@ -82,16 +105,16 @@ class MapEditor { void loadSettings(); void saveSettings() const; - // Tipos para drag & drop y selección + // Tipos para drag & drop enum class DragTarget { NONE, PLAYER, - ENEMY_INITIAL, - ENEMY_BOUND1, - ENEMY_BOUND2, - ITEM }; + ENTITY_INITIAL, + ENTITY_BOUND1, + ENTITY_BOUND2 }; struct DragState { DragTarget target{DragTarget::NONE}; + EntityType entity_type{EntityType::NONE}; int index{-1}; float offset_x{0.0F}; float offset_y{0.0F}; @@ -102,23 +125,34 @@ class MapEditor { // Métodos internos void updateMousePosition(); - void renderEnemyBoundaries(); + void renderEntityBoundaries(); static void renderBoundaryMarker(float x, float y, Uint8 color); void renderSelectionHighlight(); void renderGrid() const; void handleMouseDown(float game_x, float game_y); void handleMouseUp(); void updateDrag(); + auto commitEntityDrag() -> bool; + void moveEntityVisual(); void autosave(); void updateStatusBarInfo(); static auto snapToGrid(float value) -> float; static auto pointInRect(float px, float py, const SDL_FRect& rect) -> bool; + // Entity helpers: acceso abstracto a datos de entidad por tipo + struct BoundaryData { int x1, y1, x2, y2; }; + auto entityCount(EntityType type) const -> int; + auto entityRect(EntityType type, int index) -> SDL_FRect; + static auto entityHasBoundaries(EntityType type) -> bool; + auto entityBoundaries(EntityType type, int index) const -> BoundaryData; + auto entityPosition(EntityType type, int index) const -> std::pair; + auto entityDataCount(EntityType type) const -> int; + static auto entityLabel(EntityType type) -> const char*; + // Estado del editor bool active_{false}; DragState drag_; - int selected_enemy_{-1}; // Índice del enemigo seleccionado (-1 = ninguno) - int selected_item_{-1}; // Índice del item seleccionado (-1 = ninguno) + Selection selection_; // Entidad seleccionada (unificada: enemy, item o platform) static constexpr int NO_BRUSH = -2; // Sin brush activo static constexpr int ERASER_BRUSH = -1; // Brush borrador (pinta tile vacío = -1) int brush_tile_{NO_BRUSH}; // Tile activo para pintar diff --git a/source/game/ui/console_commands.cpp b/source/game/ui/console_commands.cpp index f6d531f..07c34db 100644 --- a/source/game/ui/console_commands.cpp +++ b/source/game/ui/console_commands.cpp @@ -728,7 +728,7 @@ static auto cmdEdit(const std::vector& args) -> std::string { // N return "usage: edit [on|off|revert|show|hide|mapbg|mapconn] [...]"; } -// SET — modifica propiedad del enemigo seleccionado o de la habitación +// SET — modifica propiedad de la entidad seleccionada o de la habitación static auto cmdSet(const std::vector& args) -> std::string { if ((MapEditor::get() == nullptr) || !MapEditor::get()->isActive()) { return "Editor not active"; } if (args.empty()) { return "usage: set "; } @@ -740,18 +740,12 @@ static auto cmdSet(const std::vector& args) -> std::string { if (args.size() < 2) { return "usage: set "; } - // Si hay enemigo seleccionado, aplicar a enemigo - if (MapEditor::get()->hasSelectedEnemy()) { - return MapEditor::get()->setEnemyProperty(args[0], args[1]); + switch (MapEditor::get()->getSelectionType()) { + case EntityType::ENEMY: return MapEditor::get()->setEnemyProperty(args[0], args[1]); + case EntityType::ITEM: return MapEditor::get()->setItemProperty(args[0], args[1]); + case EntityType::PLATFORM: return MapEditor::get()->setPlatformProperty(args[0], args[1]); + default: return MapEditor::get()->setRoomProperty(args[0], args[1]); } - - // Si hay item seleccionado, aplicar a item - if (MapEditor::get()->hasSelectedItem()) { - return MapEditor::get()->setItemProperty(args[0], args[1]); - } - - // Si no, aplicar a la habitación - return MapEditor::get()->setRoomProperty(args[0], args[1]); } // ENEMY [ADD|DELETE|DUPLICATE] @@ -785,6 +779,22 @@ static auto cmdItem(const std::vector& args) -> std::string { } return "usage: item "; } + +// PLATFORM [ADD|DELETE|DUPLICATE] +static auto cmdPlatform(const std::vector& args) -> std::string { + if ((MapEditor::get() == nullptr) || !MapEditor::get()->isActive()) { return "Editor not active"; } + if (args.empty()) { return "usage: platform "; } + if (args[0] == "ADD") { return MapEditor::get()->addPlatform(); } + if (args[0] == "DELETE") { + if (!MapEditor::get()->hasSelectedPlatform()) { return "No platform selected"; } + return MapEditor::get()->deletePlatform(); + } + if (args[0] == "DUPLICATE") { + if (!MapEditor::get()->hasSelectedPlatform()) { return "No platform selected"; } + return MapEditor::get()->duplicatePlatform(); + } + return "usage: platform "; +} #endif // SHOW [INFO|NOTIFICATION|CHEEVO] @@ -933,6 +943,7 @@ void CommandRegistry::registerHandlers() { // NOLINT(readability-function-cogni handlers_["cmd_set"] = cmdSet; handlers_["cmd_enemy"] = cmdEnemy; handlers_["cmd_item"] = cmdItem; + handlers_["cmd_platform"] = cmdPlatform; #endif // HELP se registra en load() como lambda que captura this