#ifdef _DEBUG #include "game/editor/map_editor.hpp" #include #include // Para std::round #include // Para std::remove (borrar fichero) #include // Para ifstream, ofstream #include // Para cout #include // Para set #include "core/input/mouse.hpp" // Para Mouse #include "core/rendering/render_info.hpp" // Para RenderInfo #include "core/rendering/screen.hpp" // Para Screen #include "core/rendering/surface.hpp" // Para Surface #include "core/resources/resource_cache.hpp" // Para Resource::Cache #include "core/resources/resource_list.hpp" // Para Resource::List #include "core/resources/resource_types.hpp" // Para RoomResource #include "game/editor/editor_statusbar.hpp" // Para EditorStatusBar #include "game/editor/room_saver.hpp" // Para RoomSaver #include "game/entities/player.hpp" // Para Player #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/room.hpp" // Para Room #include "game/options.hpp" // Para Options #include "game/ui/console.hpp" // Para Console #include "utils/defines.hpp" // Para Tile::SIZE, PlayArea #include "utils/utils.hpp" // Para stringToColor // Singleton MapEditor* MapEditor::instance_ = nullptr; void MapEditor::init() { instance_ = new MapEditor(); } void MapEditor::destroy() { delete instance_; instance_ = nullptr; } auto MapEditor::get() -> MapEditor* { return instance_; } // Constructor MapEditor::MapEditor() { loadSettings(); } // Destructor MapEditor::~MapEditor() = default; // Carga las opciones del editor desde editor.yaml void MapEditor::loadSettings() { std::string path = Resource::List::get()->get("editor.yaml"); if (path.empty()) { return; } std::ifstream file(path); if (!file.is_open()) { return; } std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); file.close(); try { auto yaml = fkyaml::node::deserialize(content); if (yaml.contains("grid")) { settings_.grid = yaml["grid"].get_value(); } if (yaml.contains("show_render_info")) { settings_.show_render_info = yaml["show_render_info"].get_value(); } if (yaml.contains("minimap_bg")) { if (yaml["minimap_bg"].is_string()) { settings_.minimap_bg = yaml["minimap_bg"].get_value(); } else if (yaml["minimap_bg"].is_integer()) { settings_.minimap_bg = std::to_string(yaml["minimap_bg"].get_value()); } } if (yaml.contains("minimap_conn")) { if (yaml["minimap_conn"].is_string()) { settings_.minimap_conn = yaml["minimap_conn"].get_value(); } else if (yaml["minimap_conn"].is_integer()) { settings_.minimap_conn = std::to_string(yaml["minimap_conn"].get_value()); } } } catch (...) { // Fichero corrupto o vacío, usar defaults } } // Guarda las opciones del editor a editor.yaml void MapEditor::saveSettings() { std::string path = Resource::List::get()->get("editor.yaml"); if (path.empty()) { return; } std::ofstream file(path); if (!file.is_open()) { return; } file << "# Map Editor Settings\n"; file << "grid: " << (settings_.grid ? "true" : "false") << "\n"; file << "show_render_info: " << (settings_.show_render_info ? "true" : "false") << "\n"; file << "minimap_bg: " << settings_.minimap_bg << "\n"; file << "minimap_conn: " << settings_.minimap_conn << "\n"; file.close(); } // Muestra/oculta render info (persistente) auto MapEditor::showInfo(bool show) -> std::string { settings_.show_render_info = show; if (RenderInfo::get()->isActive() != show) { RenderInfo::get()->toggle(); } saveSettings(); return show ? "Info ON" : "Info OFF"; } // Muestra/oculta grid (persistente) auto MapEditor::showGrid(bool show) -> std::string { settings_.grid = show; saveSettings(); return show ? "Grid ON" : "Grid OFF"; } // Parsea un color por nombre o índice numérico static auto parseColor(const std::string& value) -> Uint8 { try { return static_cast(std::stoi(value)); } catch (...) { return stringToColor(value); } } void MapEditor::toggleMiniMap() { if (!mini_map_) { mini_map_ = std::make_unique(parseColor(settings_.minimap_bg), parseColor(settings_.minimap_conn)); mini_map_->on_navigate = [this](const std::string& room_name) { mini_map_visible_ = false; reenter_ = true; if (GameControl::exit_editor) { GameControl::exit_editor(); } if (GameControl::change_room && GameControl::change_room(room_name)) { if (GameControl::enter_editor) { GameControl::enter_editor(); } } }; } mini_map_visible_ = !mini_map_visible_; if (mini_map_visible_) { // Reconstruir el minimapa (pueden haber cambiado rooms, conexiones, tiles) mini_map_ = std::make_unique(parseColor(settings_.minimap_bg), parseColor(settings_.minimap_conn)); mini_map_->on_navigate = [this](const std::string& room_name) { mini_map_visible_ = false; reenter_ = true; if (GameControl::exit_editor) { GameControl::exit_editor(); } if (GameControl::change_room && GameControl::change_room(room_name)) { if (GameControl::enter_editor) { GameControl::enter_editor(); } } }; mini_map_->centerOnRoom(room_path_); } } auto MapEditor::setMiniMapBg(const std::string& color) -> std::string { settings_.minimap_bg = toLower(color); saveSettings(); if (mini_map_) { mini_map_->rebuild(parseColor(settings_.minimap_bg), parseColor(settings_.minimap_conn)); } return "minimap bg: " + settings_.minimap_bg; } auto MapEditor::setMiniMapConn(const std::string& color) -> std::string { settings_.minimap_conn = toLower(color); saveSettings(); if (mini_map_) { mini_map_->rebuild(parseColor(settings_.minimap_bg), parseColor(settings_.minimap_conn)); } return "minimap conn: " + settings_.minimap_conn; } // Entra en modo editor void MapEditor::enter(std::shared_ptr room, std::shared_ptr player, const std::string& room_path, std::shared_ptr scoreboard_data) { if (active_) { return; } room_ = std::move(room); player_ = std::move(player); room_path_ = room_path; scoreboard_data_ = std::move(scoreboard_data); // Cargar una copia de los datos de la habitación (para boundaries y edición) auto room_data_ptr = Resource::Cache::get()->getRoom(room_path); if (room_data_ptr) { room_data_ = *room_data_ptr; } // Obtener la ruta completa y cargar el YAML original (para edición parcial y backup) file_path_ = Resource::List::get()->get(room_path_); if (!file_path_.empty()) { yaml_ = RoomSaver::loadYAML(file_path_); yaml_backup_ = yaml_; // Copia profunda para revert } if (!reenter_) { // Solo guardar estado previo en el primer enter (no en re-enter tras cambio de room) invincible_before_editor_ = Options::cheats.invincible; render_info_before_editor_ = RenderInfo::get()->isActive(); } reenter_ = false; // Forzar invencibilidad Options::cheats.invincible = Options::Cheat::State::ENABLED; player_->setColor(); // Aplicar el setting de render_info del editor if (settings_.show_render_info != RenderInfo::get()->isActive()) { RenderInfo::get()->toggle(); } // Activar scope de la consola para el editor Console::get()->setScope("editor"); // Resetear enemigos a su posición inicial (pueden haberse movido durante el gameplay) room_->resetEnemyPositions(room_data_.enemies); // Crear la barra de estado statusbar_ = std::make_unique(room_->getNumber(), room_->getName()); // Resetear estado drag_ = {}; selected_enemy_ = -1; selected_item_ = -1; brush_tile_ = NO_BRUSH; painting_ = false; active_ = true; std::cout << "MapEditor: ON (room " << room_path_ << ")\n"; } // Sale del modo editor void MapEditor::exit() { if (!active_) { return; } active_ = false; if (!reenter_) { // Solo restaurar en el exit final (no en cambio de room) Options::cheats.invincible = invincible_before_editor_; player_->setColor(); if (RenderInfo::get()->isActive() != render_info_before_editor_) { RenderInfo::get()->toggle(); } } // Restaurar prompt y scope de la consola selected_enemy_ = -1; Console::get()->setPrompt("> "); Console::get()->setScope("debug"); drag_ = {}; statusbar_.reset(); room_.reset(); player_.reset(); scoreboard_data_.reset(); std::cout << "MapEditor: OFF\n"; } // Revierte todos los cambios al estado original auto MapEditor::revert() -> std::string { if (!active_) { return "Editor not active"; } if (file_path_.empty()) { return "Error: No file path"; } // Restaurar el YAML original y reescribir el fichero yaml_ = yaml_backup_; auto room_data_ptr = Resource::Cache::get()->getRoom(room_path_); if (room_data_ptr) { room_data_ = *room_data_ptr; } 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); } room_->setItemColors(room_data_.item_color1, room_data_.item_color2); // Refrescar visuales de la habitación room_->setBgColor(room_data_.bg_color); Screen::get()->setBorderColor(stringToColor(room_data_.border_color)); // Restaurar el tilemap completo for (int i = 0; i < static_cast(room_data_.tile_map.size()); ++i) { room_->setTile(i, room_data_.tile_map[i]); } selected_enemy_ = -1; selected_item_ = -1; brush_tile_ = NO_BRUSH; return "Reverted to original"; } // Auto-guarda al YAML tras soltar una entidad void MapEditor::autosave() { if (file_path_.empty()) { return; } // Sincronizar posiciones de items desde los sprites vivos a room_data_ auto* item_mgr = room_->getItemManager(); for (int i = 0; i < item_mgr->getCount() && i < static_cast(room_data_.items.size()); ++i) { SDL_FPoint pos = item_mgr->getItem(i)->getPos(); room_data_.items[i].x = pos.x; room_data_.items[i].y = pos.y; } RoomSaver::saveYAML(file_path_, yaml_, room_data_); } // Actualiza el editor void MapEditor::update(float delta_time) { // Mantener el ratón siempre visible SDL_ShowCursor(); Mouse::last_mouse_move_time = SDL_GetTicks(); // Actualizar animaciones de enemigos e items (sin mover enemigos) room_->updateEditorMode(delta_time); // Actualizar posición del ratón updateMousePosition(); // Si estamos arrastrando, actualizar la posición snapped if (drag_.target != DragTarget::NONE) { updateDrag(); } // Si estamos pintando tiles, pintar en la posición actual del ratón if (painting_ && brush_tile_ != NO_BRUSH) { int tile_index = mouse_tile_y_ * 32 + mouse_tile_x_; if (tile_index >= 0 && tile_index < static_cast(room_data_.tile_map.size())) { if (room_data_.tile_map[tile_index] != brush_tile_) { room_data_.tile_map[tile_index] = brush_tile_; room_->setTile(tile_index, brush_tile_); } } } // Actualizar la barra de estado updateStatusBarInfo(); if (statusbar_) { statusbar_->update(delta_time); } } // Renderiza el editor void MapEditor::render() { // El tilemap ya ha sido renderizado por Game::renderPlaying() antes de llamar aquí // Grid (debajo de todo) if (settings_.grid) { renderGrid(); } // Renderizar los marcadores de boundaries y líneas de ruta (debajo de los sprites) renderEnemyBoundaries(); // Renderizar entidades normales: enemigos (animados en posición inicial), items, jugador room_->renderEnemies(); room_->renderItems(); player_->render(); // Renderizar highlight de selección (encima de los sprites) renderSelectionHighlight(); // Tile picker o mini mapa (encima de todo en el play area) if (tile_picker_.isOpen()) { tile_picker_.render(); } else if (mini_map_visible_ && mini_map_) { mini_map_->render(room_path_); } // Renderizar barra de estado del editor (reemplaza al scoreboard) if (statusbar_) { statusbar_->render(); } } // Maneja eventos del editor void MapEditor::handleEvent(const SDL_Event& event) { // Si el tile picker está abierto, los eventos van a él if (tile_picker_.isOpen()) { tile_picker_.handleEvent(event); return; } // Si el mini mapa está visible, delegar eventos (ESC o M para cerrar) if (mini_map_visible_ && mini_map_) { if (event.type == SDL_EVENT_KEY_DOWN && (event.key.key == SDLK_ESCAPE || event.key.key == SDLK_M) && static_cast(event.key.repeat) == 0) { mini_map_visible_ = false; return; } mini_map_->handleEvent(event, room_path_); return; } // ESC: desactivar brush if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE && brush_tile_ != NO_BRUSH) { brush_tile_ = NO_BRUSH; return; } // E: toggle borrador if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_E && static_cast(event.key.repeat) == 0) { brush_tile_ = (brush_tile_ == ERASER_BRUSH) ? NO_BRUSH : ERASER_BRUSH; return; } // M: toggle mini mapa if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_M && static_cast(event.key.repeat) == 0) { toggleMiniMap(); return; } // Click derecho: abrir TilePicker del mapa if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_RIGHT) { // Deseleccionar entidades selected_enemy_ = -1; selected_item_ = -1; // Tile bajo el cursor como tile actual del picker int tile_index = mouse_tile_y_ * 32 + mouse_tile_x_; int current = (tile_index >= 0 && tile_index < static_cast(room_data_.tile_map.size())) ? room_data_.tile_map[tile_index] : -1; tile_picker_.on_select = [this](int tile) { brush_tile_ = tile; }; tile_picker_.open(room_->getTileSetFile(), current, stringToColor(room_data_.bg_color)); return; } // Click izquierdo if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_LEFT) { // Si hay brush activo y no hacemos hit en ninguna entidad → pintar if (brush_tile_ != NO_BRUSH) { // Comprobar si hay hit en alguna entidad primero bool hit_entity = false; 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; } } } if (!hit_entity) { // Pintar tile y entrar en modo painting painting_ = true; int tile_index = mouse_tile_y_ * 32 + mouse_tile_x_; if (tile_index >= 0 && tile_index < static_cast(room_data_.tile_map.size())) { room_data_.tile_map[tile_index] = brush_tile_; room_->setTile(tile_index, brush_tile_); } return; } } handleMouseDown(mouse_game_x_, mouse_game_y_); } else if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && event.button.button == SDL_BUTTON_LEFT) { if (painting_) { painting_ = false; autosave(); } else { handleMouseUp(); } } } // 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 startDrag = [&](DragTarget target, int index, float entity_x, float entity_y) { drag_.target = target; drag_.index = index; drag_.offset_x = game_x - entity_x; drag_.offset_y = game_y - entity_y; drag_.snap_x = entity_x; drag_.snap_y = entity_y; drag_.moved = false; }; // 1. Hit test sobre el jugador (8x16) SDL_FRect player_rect = player_->getRect(); if (pointInRect(game_x, game_y, player_rect)) { startDrag(DragTarget::PLAYER, -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)) { startDrag(DragTarget::ENEMY_INITIAL, i, enemy_rect.x, enemy_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 float 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)) { startDrag(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)) { startDrag(DragTarget::ENEMY_BOUND2, 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)) { startDrag(DragTarget::ITEM, i, item_rect.x, item_rect.y); return; } } // Click en el fondo: deseleccionar todo selected_enemy_ = -1; selected_item_ = -1; } // Procesa soltar el ratón: commit del drag 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; } else { selected_enemy_ = -1; selected_item_ = -1; } 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) { case DragTarget::PLAYER: player_->setDebugPosition(drag_.snap_x, drag_.snap_y); 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; 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; 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; } break; case DragTarget::NONE: break; } if (changed) { autosave(); } drag_ = {}; } // Actualiza la posición snapped durante el drag void MapEditor::updateDrag() { float raw_x = mouse_game_x_ - drag_.offset_x; float raw_y = mouse_game_y_ - drag_.offset_y; float new_snap_x = snapToGrid(raw_x); float new_snap_y = snapToGrid(raw_y); // Detectar si hubo movimiento real (el snap cambió respecto al inicio) if (new_snap_x != drag_.snap_x || new_snap_y != drag_.snap_y) { drag_.moved = true; } drag_.snap_x = new_snap_x; drag_.snap_y = new_snap_y; // Mientras arrastramos, mover la entidad visualmente a la posición snapped switch (drag_.target) { case DragTarget::PLAYER: 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); } break; case DragTarget::NONE: break; } } // Dibuja highlight del elemento seleccionado/arrastrado void MapEditor::renderSelectionHighlight() { auto game_surface = Screen::get()->getRendererSurface(); if (!game_surface) { return; } constexpr float 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(); 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, stringToColor("bright_green")); } // 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}; game_surface->drawRectBorder(&border, stringToColor("bright_green")); } // Highlight del drag activo (temporal, color bright_white) if (drag_.target == DragTarget::NONE || !drag_.moved) { return; } const Uint8 DRAG_COLOR = stringToColor("bright_white"); SDL_FRect highlight_rect{}; switch (drag_.target) { 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(); } break; case DragTarget::ENEMY_BOUND1: case DragTarget::ENEMY_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; } SDL_FRect border = { .x = highlight_rect.x - 1, .y = highlight_rect.y - 1, .w = highlight_rect.w + 2, .h = highlight_rect.h + 2}; game_surface->drawRectBorder(&border, DRAG_COLOR); } // Alinea un valor a la cuadrícula de 8x8 auto MapEditor::snapToGrid(float value) -> float { return std::round(value / static_cast(Tile::SIZE)) * static_cast(Tile::SIZE); } // Hit test: punto dentro de rectángulo 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() { auto game_surface = Screen::get()->getRendererSurface(); if (!game_surface) { return; } const Uint8 COLOR_BOUND1 = stringToColor("bright_cyan"); const Uint8 COLOR_BOUND2 = stringToColor("bright_yellow"); const Uint8 COLOR_ROUTE = stringToColor("bright_white"); 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; // Posiciones base (pueden estar siendo arrastradas) float init_x = enemy.x; float init_y = enemy.y; float b1_x = static_cast(enemy.x1); float b1_y = static_cast(enemy.y1); float b2_x = static_cast(enemy.x2); float b2_y = static_cast(enemy.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; } } // 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); } } // Dibuja un marcador de boundary (rectángulo pequeño) en una posición void MapEditor::renderBoundaryMarker(float x, float y, Uint8 color) { auto game_surface = Screen::get()->getRendererSurface(); if (!game_surface) { return; } SDL_FRect marker = {.x = x, .y = y, .w = static_cast(Tile::SIZE), .h = static_cast(Tile::SIZE)}; game_surface->drawRectBorder(&marker, color); } // Convierte coordenadas de ventana a coordenadas de juego y tile void MapEditor::updateMousePosition() { float mouse_x = 0.0F; float mouse_y = 0.0F; SDL_GetMouseState(&mouse_x, &mouse_y); float render_x = 0.0F; float render_y = 0.0F; SDL_RenderCoordinatesFromWindow(Screen::get()->getRenderer(), mouse_x, mouse_y, &render_x, &render_y); SDL_FRect dst_rect = Screen::get()->getGameSurfaceDstRect(); mouse_game_x_ = render_x - dst_rect.x; mouse_game_y_ = render_y - dst_rect.y; // Convertir a coordenadas de tile (clampeadas al área de juego) mouse_tile_x_ = static_cast(mouse_game_x_) / Tile::SIZE; mouse_tile_y_ = static_cast(mouse_game_y_) / Tile::SIZE; // Clampear a los límites del mapa if (mouse_tile_x_ < 0) { mouse_tile_x_ = 0; } if (mouse_tile_x_ >= PlayArea::WIDTH / Tile::SIZE) { mouse_tile_x_ = PlayArea::WIDTH / Tile::SIZE - 1; } if (mouse_tile_y_ < 0) { mouse_tile_y_ = 0; } if (mouse_tile_y_ >= PlayArea::HEIGHT / Tile::SIZE) { mouse_tile_y_ = PlayArea::HEIGHT / Tile::SIZE - 1; } } // Actualiza la información de la barra de estado void MapEditor::updateStatusBarInfo() { if (!statusbar_) { return; } statusbar_->setMouseTile(mouse_tile_x_, mouse_tile_y_); std::string line2; std::string line3; std::string line5; // Helper para conexiones compactas auto conn = [](const std::string& r) -> std::string { if (r == "0" || r.empty()) { return "-"; } return r.substr(0, r.find('.')); }; // Info de drag activo (línea 5, junto a tile coords) if (drag_.target != DragTarget::NONE && drag_.moved) { switch (drag_.target) { case DragTarget::PLAYER: line5 = "dragging: player"; break; case DragTarget::ENEMY_INITIAL: line5 = "dragging: enemy " + std::to_string(drag_.index); break; case DragTarget::ENEMY_BOUND1: line5 = "dragging: e" + 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); break; case DragTarget::NONE: break; } } // 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); } line2 = "enemy " + std::to_string(selected_enemy_) + ": " + anim + " " + e.color; 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 = "bg:" + room_data_.bg_color + " brd:" + room_data_.border_color + " 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:" + room_data_.item_color1 + "/" + room_data_.item_color2; } // Línea 4: brush activo std::string line4; if (brush_tile_ == ERASER_BRUSH) { line4 = "brush: eraser (e)"; } else if (brush_tile_ != NO_BRUSH) { line4 = "brush: tile " + std::to_string(brush_tile_); } statusbar_->setLine2(line2); statusbar_->setLine3(line3); statusbar_->setLine4(line4); 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 { Console::get()->setPrompt("room> "); } } // ¿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"}; } if (hasSelectedItem()) { return {"TILE", "COUNTER"}; } // Room return {"BGCOLOR", "BORDER", "ITEMCOLOR1", "ITEMCOLOR2", "CONVEYOR", "TILESET", "UP", "DOWN", "LEFT", "RIGHT"}; } // Modifica una propiedad del enemigo seleccionado auto MapEditor::setEnemyProperty(const std::string& property, const std::string& value) -> std::string { if (!active_) { return "Editor not active"; } if (!hasSelectedEnemy()) { return "No enemy selected"; } auto& enemy = room_data_.enemies[selected_enemy_]; if (property == "ANIMATION") { std::string anim = toLower(value); if (anim.find('.') == std::string::npos) { anim += ".yaml"; } // Intentar recrear el enemigo con la nueva animación std::string old_anim = enemy.animation_path; enemy.animation_path = anim; try { auto* enemy_mgr = room_->getEnemyManager(); enemy_mgr->getEnemy(selected_enemy_) = std::make_shared(enemy); } catch (const std::exception& e) { enemy.animation_path = old_anim; // Restaurar si falla return std::string("Error: ") + e.what(); } autosave(); return "animation: " + anim; } if (property == "COLOR") { std::string color = toLower(value); // Intentar recrear el enemigo con el nuevo color std::string old_color = enemy.color; enemy.color = color; try { auto* enemy_mgr = room_->getEnemyManager(); enemy_mgr->getEnemy(selected_enemy_) = std::make_shared(enemy); } catch (const std::exception& e) { enemy.color = old_color; return std::string("Error: ") + e.what(); } autosave(); return "color: " + color; } if (property == "VX") { try { enemy.vx = std::stof(value); } catch (...) { return "Invalid value: " + value; } enemy.vy = 0.0F; // No se permiten velocidades en los dos ejes room_->getEnemyManager()->getEnemy(selected_enemy_)->resetToInitialPosition(enemy); autosave(); return "vx: " + std::to_string(static_cast(enemy.vx)) + " vy: 0"; } if (property == "VY") { try { enemy.vy = std::stof(value); } catch (...) { return "Invalid value: " + value; } enemy.vx = 0.0F; // No se permiten velocidades en los dos ejes room_->getEnemyManager()->getEnemy(selected_enemy_)->resetToInitialPosition(enemy); autosave(); return "vy: " + std::to_string(static_cast(enemy.vy)) + " vx: 0"; } if (property == "FLIP") { enemy.flip = (value == "ON" || value == "TRUE" || value == "1"); if (enemy.flip) { enemy.mirror = false; } // Mutuamente excluyentes // 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); } catch (const std::exception& e) { return std::string("Error: ") + e.what(); } autosave(); return std::string("flip: ") + (enemy.flip ? "on" : "off"); } if (property == "MIRROR") { enemy.mirror = (value == "ON" || value == "TRUE" || value == "1"); if (enemy.mirror) { enemy.flip = false; } // Mutuamente excluyentes // 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); } catch (const std::exception& e) { return std::string("Error: ") + e.what(); } autosave(); return std::string("mirror: ") + (enemy.mirror ? "on" : "off"); } return "Unknown property: " + property + " (use: animation, color, vx, vy, flip, mirror)"; } // Crea un nuevo enemigo con valores por defecto, centrado en la habitación auto MapEditor::addEnemy() -> 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.0F * Tile::SIZE; // 2 tiles a cada lado (5 tiles total con el centro) Enemy::Data new_enemy; new_enemy.animation_path = "spider.yaml"; new_enemy.x = CENTER_X; new_enemy.y = CENTER_Y; new_enemy.vx = 24.0F; new_enemy.vy = 0.0F; new_enemy.x1 = static_cast(CENTER_X - ROUTE_HALF); new_enemy.y1 = static_cast(CENTER_Y); new_enemy.x2 = static_cast(CENTER_X + ROUTE_HALF); new_enemy.y2 = static_cast(CENTER_Y); new_enemy.color = "white"; new_enemy.flip = true; new_enemy.frame = -1; // 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)); // Seleccionar el nuevo enemigo selected_enemy_ = static_cast(room_data_.enemies.size()) - 1; autosave(); return "Added enemy " + std::to_string(selected_enemy_); } // Elimina el enemigo seleccionado auto MapEditor::deleteEnemy() -> std::string { if (!active_) { return "Editor not active"; } if (!hasSelectedEnemy()) { return "No enemy selected"; } const int IDX = selected_enemy_; // Eliminar de los datos room_data_.enemies.erase(room_data_.enemies.begin() + IDX); // Recrear todos los enemigos vivos (los índices cambian al borrar) auto* enemy_mgr = room_->getEnemyManager(); enemy_mgr->clear(); for (const auto& enemy_data : room_data_.enemies) { enemy_mgr->addEnemy(std::make_shared(enemy_data)); } selected_enemy_ = -1; autosave(); return "Deleted enemy " + std::to_string(IDX); } // Duplica el enemigo seleccionado (lo pone un tile a la derecha) auto MapEditor::duplicateEnemy() -> std::string { if (!active_) { return "Editor not active"; } if (!hasSelectedEnemy()) { return "No enemy selected"; } // Copiar datos del enemigo seleccionado Enemy::Data copy = room_data_.enemies[selected_enemy_]; // Desplazar un tile a la derecha copy.x += Tile::SIZE; copy.x1 += Tile::SIZE; copy.x2 += Tile::SIZE; // Añadir y crear sprite vivo room_data_.enemies.push_back(copy); room_->getEnemyManager()->addEnemy(std::make_shared(copy)); // Seleccionar el nuevo enemigo selected_enemy_ = static_cast(room_data_.enemies.size()) - 1; autosave(); return "Duplicated as enemy " + std::to_string(selected_enemy_); } // Modifica una propiedad de la habitación auto MapEditor::setRoomProperty(const std::string& property, const std::string& value) -> std::string { if (!active_) { return "Editor not active"; } std::string val = toLower(value); if (property == "BGCOLOR") { room_data_.bg_color = val; room_->setBgColor(val); autosave(); return "bgcolor: " + val; } if (property == "BORDER") { room_data_.border_color = val; Screen::get()->setBorderColor(stringToColor(val)); autosave(); return "border: " + val; } if (property == "ITEMCOLOR1") { room_data_.item_color1 = val; room_->setItemColors(room_data_.item_color1, room_data_.item_color2); autosave(); return "itemcolor1: " + val; } if (property == "ITEMCOLOR2") { room_data_.item_color2 = val; room_->setItemColors(room_data_.item_color1, room_data_.item_color2); autosave(); return "itemcolor2: " + val; } if (property == "CONVEYOR") { if (val == "left") { room_data_.conveyor_belt_direction = -1; } else if (val == "right") { room_data_.conveyor_belt_direction = 1; } else { room_data_.conveyor_belt_direction = 0; val = "none"; } autosave(); return "conveyor: " + val; } if (property == "TILESET") { std::string tileset = val; if (tileset.find('.') == std::string::npos) { tileset += ".gif"; } room_data_.tile_set_file = tileset; autosave(); return "tileset: " + tileset; } // Conexiones: UP, DOWN, LEFT, RIGHT if (property == "UP" || property == "DOWN" || property == "LEFT" || property == "RIGHT") { std::string connection = "0"; if (val != "0" && val != "null" && val != "none") { try { int num = std::stoi(val); char buf[16]; std::snprintf(buf, sizeof(buf), "%02d.yaml", num); connection = buf; } catch (...) { connection = val; if (connection.find('.') == std::string::npos) { connection += ".yaml"; } } } // Dirección opuesta para la conexión recíproca std::string opposite; std::string* our_field = nullptr; if (property == "UP") { opposite = "lower_room"; our_field = &room_data_.upper_room; } if (property == "DOWN") { opposite = "upper_room"; our_field = &room_data_.lower_room; } if (property == "LEFT") { opposite = "right_room"; our_field = &room_data_.left_room; } if (property == "RIGHT") { opposite = "left_room"; our_field = &room_data_.right_room; } // Si había una conexión anterior, romper la recíproca de la otra room if (our_field != nullptr && *our_field != "0" && !our_field->empty()) { auto old_other = Resource::Cache::get()->getRoom(*our_field); if (old_other) { if (opposite == "upper_room") { old_other->upper_room = "0"; } else if (opposite == "lower_room") { old_other->lower_room = "0"; } else if (opposite == "left_room") { old_other->left_room = "0"; } else if (opposite == "right_room") { old_other->right_room = "0"; } // Guardar la otra room std::string other_path = Resource::List::get()->get(*our_field); if (!other_path.empty()) { auto other_yaml = RoomSaver::loadYAML(other_path); RoomSaver::saveYAML(other_path, other_yaml, *old_other); } } } // Aplicar la nueva conexión if (our_field != nullptr) { *our_field = connection; } // Crear la conexión recíproca en la otra room if (connection != "0") { auto other = Resource::Cache::get()->getRoom(connection); if (other) { if (opposite == "upper_room") { other->upper_room = room_path_; } else if (opposite == "lower_room") { other->lower_room = room_path_; } else if (opposite == "left_room") { other->left_room = room_path_; } else if (opposite == "right_room") { other->right_room = room_path_; } std::string other_path = Resource::List::get()->get(connection); if (!other_path.empty()) { auto other_yaml = RoomSaver::loadYAML(other_path); RoomSaver::saveYAML(other_path, other_yaml, *other); } } } autosave(); return toLower(property) + ": " + connection; } return "Unknown property: " + property + " (use: bgcolor, border, itemcolor1, itemcolor2, conveyor, tileset, up, down, left, right)"; } // Obtiene la ruta de assets.yaml derivada de la ruta de una room auto MapEditor::getAssetsYamlPath() -> std::string { std::string ref_path = Resource::List::get()->get(room_path_); if (ref_path.empty()) { return ""; } // ref_path es algo como .../data/room/03.yaml → queremos .../config/assets.yaml auto pos = ref_path.find("/data/room/"); if (pos == std::string::npos) { return ""; } return ref_path.substr(0, pos) + "/config/assets.yaml"; } // Añade una room a assets.yaml (bajo la sección rooms:) void MapEditor::addRoomToAssetsYaml(const std::string& room_name) { std::string assets_path = getAssetsYamlPath(); if (assets_path.empty()) { return; } // Leer el fichero std::ifstream in(assets_path); if (!in.is_open()) { return; } std::string content((std::istreambuf_iterator(in)), std::istreambuf_iterator()); in.close(); // Buscar la última entrada de room y añadir después std::string entry = " - type: ROOM\n path: ${PREFIX}/data/room/" + room_name + "\n"; auto last_room = content.rfind("path: ${PREFIX}/data/room/"); if (last_room != std::string::npos) { auto end_of_line = content.find('\n', last_room); if (end_of_line != std::string::npos) { content.insert(end_of_line + 1, entry); } } std::ofstream out(assets_path); if (out.is_open()) { out << content; out.close(); } } // Quita una room de assets.yaml void MapEditor::removeRoomFromAssetsYaml(const std::string& room_name) { std::string assets_path = getAssetsYamlPath(); if (assets_path.empty()) { return; } std::ifstream in(assets_path); if (!in.is_open()) { return; } std::string content((std::istreambuf_iterator(in)), std::istreambuf_iterator()); in.close(); // Buscar "path: ${PREFIX}/data/room/XX.yaml" y eliminar esa entry (2 líneas: type + path) std::string search = "path: ${PREFIX}/data/room/" + room_name; auto pos = content.find(search); if (pos != std::string::npos) { // Retroceder hasta " - type: ROOM\n" auto line_start = content.rfind(" - type: ROOM", pos); // Avanzar hasta el fin de la línea del path auto line_end = content.find('\n', pos); if (line_start != std::string::npos && line_end != std::string::npos) { content.erase(line_start, line_end - line_start + 1); } } std::ofstream out(assets_path); if (out.is_open()) { out << content; out.close(); } } // Crea una nueva habitación auto MapEditor::createNewRoom(const std::string& direction) -> std::string { if (!active_) { return "Editor not active"; } // Validar dirección si se proporcionó if (!direction.empty() && direction != "LEFT" && direction != "RIGHT" && direction != "UP" && direction != "DOWN") { return "usage: room new [left|right|up|down]"; } // Comprobar que no hay ya una room en esa dirección if (!direction.empty()) { std::string* existing = nullptr; if (direction == "UP") { existing = &room_data_.upper_room; } else if (direction == "DOWN") { existing = &room_data_.lower_room; } else if (direction == "LEFT") { existing = &room_data_.left_room; } else if (direction == "RIGHT") { existing = &room_data_.right_room; } if (existing != nullptr && *existing != "0" && !existing->empty()) { return "Already connected " + toLower(direction) + ": " + *existing; } } // Encontrar el primer número libre (reutiliza huecos) auto& rooms = Resource::Cache::get()->getRooms(); std::set used; for (const auto& r : rooms) { try { used.insert(std::stoi(r.name.substr(0, r.name.find('.')))); } catch (...) {} } int new_num = 1; while (used.contains(new_num)) { ++new_num; } char name_buf[16]; std::snprintf(name_buf, sizeof(name_buf), "%02d.yaml", new_num); std::string new_name = name_buf; // Derivar la ruta de la carpeta de rooms std::string ref_path = Resource::List::get()->get(room_path_); if (ref_path.empty()) { return "Error: cannot resolve room path"; } std::string room_dir = ref_path.substr(0, ref_path.find_last_of("\\/") + 1); std::string new_path = room_dir + new_name; // Crear Room::Data por defecto con conexión recíproca Room::Data new_room; new_room.number = std::string(name_buf).substr(0, std::string(name_buf).find('.')); new_room.name = "NO_NAME"; new_room.bg_color = "black"; new_room.border_color = "magenta"; new_room.tile_set_file = "standard.gif"; new_room.item_color1 = "bright_cyan"; new_room.item_color2 = "yellow"; new_room.upper_room = "0"; new_room.lower_room = "0"; new_room.left_room = "0"; new_room.right_room = "0"; new_room.conveyor_belt_direction = 0; new_room.tile_map.resize(32 * 16, -1); // Conexión recíproca: la nueva room conecta de vuelta a la actual if (direction == "UP") { new_room.lower_room = room_path_; } else if (direction == "DOWN") { new_room.upper_room = room_path_; } else if (direction == "LEFT") { new_room.right_room = room_path_; } else if (direction == "RIGHT") { new_room.left_room = room_path_; } // Conexiones del YAML auto connStr = [](const std::string& c) -> std::string { return (c == "0") ? "null" : c; }; // Crear el YAML std::ofstream file(new_path); if (!file.is_open()) { return "Error: cannot create " + new_path; } file << "# NO_NAME\n"; file << "room:\n"; file << " name_en: \"NO_NAME\"\n"; file << " name_ca: \"NO_NAME\"\n"; file << " bgColor: black\n"; file << " border: magenta\n"; file << " tileSetFile: standard.gif\n"; file << "\n"; file << " connections:\n"; file << " up: " << connStr(new_room.upper_room) << "\n"; file << " down: " << connStr(new_room.lower_room) << "\n"; file << " left: " << connStr(new_room.left_room) << "\n"; file << " right: " << connStr(new_room.right_room) << "\n"; file << "\n"; file << " itemColor1: bright_cyan\n"; file << " itemColor2: yellow\n"; file << "\n"; file << " conveyorBelt: none\n"; file << "\n"; file << "tilemap:\n"; for (int row = 0; row < 16; ++row) { file << " - ["; for (int col = 0; col < 32; ++col) { file << "-1"; if (col < 31) { file << ", "; } } file << "]\n"; } file.close(); // Registrar en Resource::List, cache y assets.yaml Resource::List::get()->add(new_path, Resource::List::Type::ROOM, true, true); Resource::Cache::get()->getRooms().emplace_back( RoomResource{.name = new_name, .room = std::make_shared(new_room)}); addRoomToAssetsYaml(new_name); // Conectar la room actual con la nueva (recíproco: ya hecho arriba para la nueva) if (direction == "UP") { room_data_.upper_room = new_name; } else if (direction == "DOWN") { room_data_.lower_room = new_name; } else if (direction == "LEFT") { room_data_.left_room = new_name; } else if (direction == "RIGHT") { room_data_.right_room = new_name; } if (!direction.empty()) { autosave(); } // Navegar a la nueva room reenter_ = true; if (GameControl::exit_editor) { GameControl::exit_editor(); } if (GameControl::change_room && GameControl::change_room(new_name)) { if (GameControl::enter_editor) { GameControl::enter_editor(); } } return "Created room " + new_name + (direction.empty() ? "" : " (" + toLower(direction) + ")"); } // Elimina la habitación actual auto MapEditor::deleteRoom() -> std::string { if (!active_) { return "Editor not active"; } std::string deleted_name = room_path_; // Buscar una room vecina a la que navegar después de borrar std::string target = "0"; if (room_data_.upper_room != "0" && !room_data_.upper_room.empty()) { target = room_data_.upper_room; } else if (room_data_.lower_room != "0" && !room_data_.lower_room.empty()) { target = room_data_.lower_room; } else if (room_data_.left_room != "0" && !room_data_.left_room.empty()) { target = room_data_.left_room; } else if (room_data_.right_room != "0" && !room_data_.right_room.empty()) { target = room_data_.right_room; } if (target == "0") { // Buscar la primera room que no sea esta for (const auto& r : Resource::Cache::get()->getRooms()) { if (r.name != deleted_name) { target = r.name; break; } } } if (target == "0") { return "Cannot delete: no other room to navigate to"; } // Desenlazar todas las conexiones recíprocas auto unlinkReciprocal = [&](const std::string& neighbor, const std::string& field_name) { if (neighbor == "0" || neighbor.empty()) { return; } auto other = Resource::Cache::get()->getRoom(neighbor); if (!other) { return; } if (field_name == "upper_room") { other->upper_room = "0"; } else if (field_name == "lower_room") { other->lower_room = "0"; } else if (field_name == "left_room") { other->left_room = "0"; } else if (field_name == "right_room") { other->right_room = "0"; } // Guardar la otra room std::string other_path = Resource::List::get()->get(neighbor); if (!other_path.empty()) { auto other_yaml = RoomSaver::loadYAML(other_path); RoomSaver::saveYAML(other_path, other_yaml, *other); } }; unlinkReciprocal(room_data_.upper_room, "lower_room"); // Si nosotros somos su lower unlinkReciprocal(room_data_.lower_room, "upper_room"); // Si nosotros somos su upper unlinkReciprocal(room_data_.left_room, "right_room"); // Si nosotros somos su right unlinkReciprocal(room_data_.right_room, "left_room"); // Si nosotros somos su left // Navegar a la room destino antes de borrar reenter_ = true; if (GameControl::exit_editor) { GameControl::exit_editor(); } if (GameControl::change_room) { GameControl::change_room(target); } // Borrar el YAML del disco std::string yaml_path = Resource::List::get()->get(deleted_name); if (!yaml_path.empty()) { std::remove(yaml_path.c_str()); } // Quitar del cache auto& cache_rooms = Resource::Cache::get()->getRooms(); cache_rooms.erase( std::remove_if(cache_rooms.begin(), cache_rooms.end(), [&](const RoomResource& r) { return r.name == deleted_name; }), cache_rooms.end()); // Quitar de assets.yaml removeRoomFromAssetsYaml(deleted_name); // Re-entrar al editor en la room destino if (GameControl::enter_editor) { GameControl::enter_editor(); } 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_]; if (property == "TILE") { // Abrir el tile picker visual openTilePicker(item.tile_set_file, item.tile); return "Select tile..."; } if (property == "COUNTER") { try { item.counter = std::stoi(value); } catch (...) { return "Invalid value: " + value; } autosave(); return "counter: " + std::to_string(item.counter); } return "Unknown property: " + property + " (use: tile, counter)"; } // Abre el tile picker para seleccionar un tile void MapEditor::openTilePicker(const std::string& tileset_name, int current_tile) { // Cerrar la consola si está abierta (para que el primer click vaya al picker) if (Console::get()->isActive()) { Console::get()->toggle(); } tile_picker_.on_select = [this](int tile) { if (!hasSelectedItem()) { return; } room_data_.items[selected_item_].tile = tile; room_->getItemManager()->getItem(selected_item_)->setTile(tile); autosave(); }; // Pasar color de fondo de la habitación + color de sustitución del item int bg = stringToColor(room_data_.bg_color); int item_color = stringToColor(room_data_.item_color1); tile_picker_.open(tileset_name, current_tile, bg, 1, item_color); } // Crea un nuevo item con valores por defecto, centrado en la habitación auto MapEditor::addItem() -> std::string { if (!active_) { return "Editor not active"; } Item::Data new_item; new_item.tile_set_file = "items.gif"; new_item.tile = 42; // Tile por defecto new_item.x = PlayArea::CENTER_X; new_item.y = PlayArea::CENTER_Y; new_item.counter = 0; new_item.color1 = stringToColor(room_data_.item_color1); new_item.color2 = stringToColor(room_data_.item_color2); 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; autosave(); return "Added item " + std::to_string(selected_item_); } // Elimina el item seleccionado auto MapEditor::deleteItem() -> std::string { if (!active_) { return "Editor not active"; } if (!hasSelectedItem()) { return "No item selected"; } const int IDX = selected_item_; room_data_.items.erase(room_data_.items.begin() + IDX); // Recrear todos los items (los índices cambian al borrar) auto* item_mgr = room_->getItemManager(); item_mgr->clear(); for (const auto& item_data : room_data_.items) { item_mgr->addItem(std::make_shared(item_data)); } selected_item_ = -1; autosave(); return "Deleted item " + std::to_string(IDX); } // Duplica el item seleccionado (lo pone un tile a la derecha) 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_]; 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; autosave(); return "Duplicated as item " + std::to_string(selected_item_); } // Elige un color de grid que contraste con el fondo // Empieza con bright_black (1), si coincide con el bg en la paleta activa, sube índices static auto pickGridColor(Uint8 bg, const std::shared_ptr& surface) -> Uint8 { Uint8 grid = static_cast(PaletteColor::BRIGHT_BLACK); Uint32 bg_argb = surface->getPaletteColor(bg); // Si bright_black es igual al bg, buscar el siguiente color distinto while (grid < 15 && surface->getPaletteColor(grid) == bg_argb) { grid += 2; // Saltar de 2 en 2 para mantenerse en tonos discretos } return grid; } // Dibuja la cuadrícula de tiles (líneas de puntos, 1 pixel sí / 1 no) void MapEditor::renderGrid() { auto game_surface = Screen::get()->getRendererSurface(); if (!game_surface) { return; } const Uint8 COLOR = pickGridColor(stringToColor(room_data_.bg_color), game_surface); // Líneas verticales (cada 8 pixels) for (int x = Tile::SIZE; x < PlayArea::WIDTH; x += Tile::SIZE) { for (int y = 0; y < PlayArea::HEIGHT; y += 2) { game_surface->putPixel(x, y, COLOR); } } // Líneas horizontales (cada 8 pixels) for (int y = Tile::SIZE; y < PlayArea::HEIGHT; y += Tile::SIZE) { for (int x = 0; x < PlayArea::WIDTH; x += 2) { game_surface->putPixel(x, y, COLOR); } } } #endif // _DEBUG