#ifdef _DEBUG #include "game/editor/map_editor.hpp" #include #include #include // Para std::round #include // Para std::remove (borrar fichero) #include // Para directory_iterator (getAnimationCompletions) #include // Para ifstream, ofstream #include // Para cout #include // Para set #include // Para std::error_code #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 "external/fkyaml_node.hpp" // Para fkyaml::node (loadSettings) #include "game/editor/editor_statusbar.hpp" // Para EditorStatusBar #include "game/entities/player.hpp" // Para Player #include "game/game_control.hpp" // Para GameControl #include "game/gameplay/door_manager.hpp" // Para DoorManager #include "game/gameplay/enemy_manager.hpp" // Para EnemyManager #include "game/gameplay/item_manager.hpp" // Para ItemManager #include "game/gameplay/key_manager.hpp" // Para KeyManager #include "game/gameplay/platform_manager.hpp" // Para PlatformManager #include "game/gameplay/room.hpp" // Para Room #include "game/gameplay/room_format.hpp" // Para RoomFormat #include "game/gameplay/zone.hpp" // Para Zone::Data #include "game/gameplay/zone_manager.hpp" // Para ZoneManager #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" // 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_integer()) { settings_.minimap_bg = static_cast(yaml["minimap_bg"].get_value()); } else if (yaml["minimap_bg"].is_string()) { settings_.minimap_bg = static_cast(safeStoi(yaml["minimap_bg"].get_value(), 2)); } } if (yaml.contains("minimap_conn")) { if (yaml["minimap_conn"].is_integer()) { settings_.minimap_conn = static_cast(yaml["minimap_conn"].get_value()); } else if (yaml["minimap_conn"].is_string()) { settings_.minimap_conn = static_cast(safeStoi(yaml["minimap_conn"].get_value(), 14)); } } } catch (...) { // Fichero corrupto o vacío, usar defaults } } // Guarda las opciones del editor a editor.yaml void MapEditor::saveSettings() const { 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: " << static_cast(settings_.minimap_bg) << "\n"; file << "minimap_conn: " << static_cast(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"; } auto MapEditor::setEditingCollision(bool collision) -> std::string { editing_collision_ = collision; brush_.clear(); // Resetear brush al cambiar de modo return editing_collision_ ? "Editing: collision" : "Editing: draw"; } void MapEditor::toggleMiniMap() { if (!mini_map_) { mini_map_ = std::make_unique(settings_.minimap_bg, 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(settings_.minimap_bg, 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 = static_cast(safeStoi(color, 2)); saveSettings(); if (mini_map_) { mini_map_->rebuild(settings_.minimap_bg, settings_.minimap_conn); } return "minimap bg: " + std::to_string(settings_.minimap_bg); } auto MapEditor::setMiniMapConn(const std::string& color) -> std::string { settings_.minimap_conn = static_cast(safeStoi(color, 14)); saveSettings(); if (mini_map_) { mini_map_->rebuild(settings_.minimap_bg, settings_.minimap_conn); } return "minimap conn: " + std::to_string(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 al fichero del editor (para autosave en disco) file_path_ = Resource::List::get()->get(room_path_); bool is_reenter = reenter_; if (!reenter_) { // Solo guardar estado previo en el primer enter (no en re-enter tras cambio de room) invincible_before_editor_ = Options::cheats.invincible; render_info_before_editor_ = RenderInfo::get()->isActive(); } reenter_ = false; // Forzar invencibilidad Options::cheats.invincible = Options::Cheat::State::ENABLED; // 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); // 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_ = {}; selection_.clear(); eyedropper_ = {}; if (!is_reenter) { brush_.clear(); painting_ = false; editing_collision_ = false; } painting_ = false; // Siempre dejar de pintar al cambiar de room // Asegurar que collision_tile_map tiene el tamaño correcto if (room_data_.collision_tile_map.size() != static_cast(Map::WIDTH * Map::HEIGHT)) { room_data_.collision_tile_map.resize(Map::WIDTH * Map::HEIGHT, 0); } 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_; if (RenderInfo::get()->isActive() != render_info_before_editor_) { RenderInfo::get()->toggle(); } } // Restaurar prompt y scope de la consola selection_.clear(); 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 room_data_ desde el cache (que mantiene la versión original) y persistir auto room_data_ptr = Resource::Cache::get()->getRoom(room_path_); if (room_data_ptr) { room_data_ = *room_data_ptr; } RoomFormat::saveYAML(file_path_, room_data_); // 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_mgr->addItem(std::make_shared(i)); } auto* platform_mgr = room_->getPlatformManager(); platform_mgr->clear(); for (const auto& p : room_data_.platforms) { platform_mgr->addPlatform(std::make_shared(p)); } // 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]); } selection_.clear(); brush_.clear(); 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; } // Sincronizar posiciones de llaves desde los sprites vivos a room_data_ // (mismo patrón que items: el sprite es la fuente de verdad durante el drag) auto* key_mgr = room_->getKeyManager(); for (int i = 0; i < key_mgr->getCount() && i < static_cast(room_data_.keys.size()); ++i) { SDL_FPoint pos = key_mgr->getKey(i)->getPos(); room_data_.keys[i].x = pos.x; room_data_.keys[i].y = pos.y; } // Platforms are already synced via resetToInitialPosition during drag commit // Doors are already synced via DoorManager::moveDoor in commitEntityDrag RoomFormat::saveYAML(file_path_, 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, estampar el patrón en la posición del ratón if (painting_ && !brush_.isEmpty()) { stampBrushAt(mouse_tile_x_, mouse_tile_y_); } // 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í // Si estamos editando colisiones, superponer el mapa de colisiones if (editing_collision_) { auto collision_surface = Resource::Cache::get()->getSurface("collision.gif"); if (collision_surface) { const int TILE_W = Tile::SIZE; for (int y = 0; y < Map::HEIGHT; ++y) { for (int x = 0; x < Map::WIDTH; ++x) { int index = (y * Map::WIDTH) + x; if (index >= static_cast(room_data_.collision_tile_map.size())) { continue; } int tile = room_data_.collision_tile_map[index]; if (tile <= 0) { continue; } // 0 = vacío, no dibujar SDL_FRect clip = { .x = static_cast(tile * TILE_W), .y = 0, .w = static_cast(TILE_W), .h = static_cast(TILE_W)}; collision_surface->render(x * TILE_W, y * TILE_W, &clip); } } } } // Grid (debajo de todo) if (settings_.grid) { renderGrid(); } // Renderizar los marcadores de boundaries y líneas de ruta (debajo de los sprites) renderEntityBoundaries(); // Renderizar entidades normales: enemigos (animados en posición inicial), items, // plataformas, llaves, puertas, jugador room_->renderEnemies(); room_->renderItems(); room_->renderPlatforms(); room_->renderKeys(); room_->renderDoors(); player_->render(); // Renderizar highlight de selección (encima de los sprites) renderSelectionHighlight(); // Preview del brush bajo el cursor o rect del eyedropper en progreso // (solo cuando no hay overlays a pantalla completa abiertos) if (!tile_picker_.isOpen() && !mini_map_visible_) { if (eyedropper_.active) { renderEyedropperRect(); } else { renderBrushPreview(); } } // 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) { // NOLINT(readability-function-cognitive-complexity) // 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: cancelar eyedropper en progreso (sin tocar el brush previo) if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE && eyedropper_.active) { eyedropper_.active = false; return; } // ESC: desactivar brush if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE && !brush_.isEmpty()) { brush_.clear(); return; } // E: toggle borrador (alterna entre brush vacío y brush 1x1 ERASE) if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_E && static_cast(event.key.repeat) == 0) { if (brush_.width == 1 && brush_.height == 1 && !brush_.tiles.empty() && brush_.tiles[0] == BrushPattern::ERASE) { brush_.clear(); } else { brush_.setSingle(BrushPattern::ERASE); } 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; } // 7: alternar entre draw y collision if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_7 && static_cast(event.key.repeat) == 0) { setEditingCollision(!editing_collision_); return; } // Cursores: navegar a habitación adyacente if (event.type == SDL_EVENT_KEY_DOWN && static_cast(event.key.repeat) == 0) { std::string direction; switch (event.key.key) { case SDLK_UP: direction = "UP"; break; case SDLK_DOWN: direction = "DOWN"; break; case SDLK_LEFT: direction = "LEFT"; break; case SDLK_RIGHT: direction = "RIGHT"; break; default: break; } if (!direction.empty() && GameControl::get_adjacent_room) { std::string adjacent = GameControl::get_adjacent_room(direction); if (!adjacent.empty() && adjacent != "0") { autosave(); reenter_ = true; if (GameControl::exit_editor) { GameControl::exit_editor(); } if (GameControl::change_room && GameControl::change_room(adjacent)) { if (GameControl::enter_editor) { GameControl::enter_editor(); } } return; } } } // T: abrir TilePicker if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_T && static_cast(event.key.repeat) == 0) { // Deseleccionar entidades selection_.clear(); const std::string tileset_name = editing_collision_ ? "collision.gif" : room_->getTileSetFile(); int tile_index = (mouse_tile_y_ * 32) + mouse_tile_x_; int current = 0; if (editing_collision_) { current = (tile_index >= 0 && tile_index < static_cast(room_data_.collision_tile_map.size())) ? room_data_.collision_tile_map[tile_index] : 0; } else { 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, tileset_name](int col, int row, int width, int height) { brush_ = buildPatternFromTileset(tileset_name, col, row, width, height); }; tile_picker_.open(tileset_name, current, 0, -1, -1, 0, 1, true); return; } // Click derecho: eyedropper (sample del nivel). Drag = rect, click = single tile. if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_RIGHT) { // Cancelar painting en curso si lo hubiese if (painting_) { painting_ = false; autosave(); } selection_.clear(); eyedropper_.active = true; eyedropper_.start_tile_x = mouse_tile_x_; eyedropper_.start_tile_y = mouse_tile_y_; return; } if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && event.button.button == SDL_BUTTON_RIGHT && eyedropper_.active) { commitEyedropper(); eyedropper_.active = false; 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_.isEmpty()) { // 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; } // 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; } } } if (!hit_entity) { // Estampar el patrón y entrar en modo painting painting_ = true; stampBrushAt(mouse_tile_x_, mouse_tile_y_); 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) { 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; drag_.snap_x = entity_x; drag_.snap_y = entity_y; drag_.moved = false; }; // 1. Player hit test SDL_FRect player_rect = player_->getRect(); if (pointInRect(game_x, game_y, player_rect)) { start_drag(DragTarget::PLAYER, EntityType::NONE, -1, player_rect.x, player_rect.y); return; } // 2. Hit test on entity initials. DOOR antes que KEY/ITEM porque ocupa más // espacio (1×4 tiles); ENEMY/PLATFORM van primero porque pueden estar // dibujados encima de items pequeños. for (auto type : {EntityType::ENEMY, EntityType::PLATFORM, EntityType::DOOR, EntityType::KEY, 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 on boundaries (enemies only) for (auto type : {EntityType::ENEMY}) { for (int i = 0; i < entityDataCount(type); ++i) { auto bd = entityBoundaries(type, i); constexpr auto SZ = static_cast(Tile::SIZE); 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. Background click: deselect selection_.clear(); } // Procesa soltar el ratón: commit del drag void MapEditor::handleMouseUp() { if (drag_.target == DragTarget::NONE) { return; } // Si no se movió: fue un click → seleccionar/deseleccionar if (!drag_.moved) { 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 { selection_.clear(); } drag_ = {}; return; } // Hubo movimiento: commit del drag bool changed = false; switch (drag_.target) { case DragTarget::PLAYER: player_->setDebugPosition(drag_.snap_x, drag_.snap_y); player_->finalizeDebugTeleport(); break; case DragTarget::ENTITY_INITIAL: case DragTarget::ENTITY_BOUND1: case DragTarget::ENTITY_BOUND2: changed = commitEntityDrag(); break; case DragTarget::NONE: break; } if (changed) { autosave(); } 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())) { auto& plat = room_data_.platforms[IDX]; if (!plat.path.empty()) { float dx = drag_.snap_x - plat.path[0].x; float dy = drag_.snap_y - plat.path[0].y; for (auto& wp : plat.path) { wp.x += dx; wp.y += dy; } } room_->getPlatformManager()->getPlatform(IDX)->resetToInitialPosition(plat); selection_ = {EntityType::PLATFORM, IDX}; return true; } break; case EntityType::KEY: if (IDX >= 0 && IDX < static_cast(room_data_.keys.size())) { // El sprite ya está en su posición visual final desde moveEntityVisual. // Solo hay que actualizar room_data_; el autosave hará el sync inverso // sprite→data igual que con items. room_data_.keys[IDX].x = drag_.snap_x; room_data_.keys[IDX].y = drag_.snap_y; selection_ = {EntityType::KEY, IDX}; return true; } break; case EntityType::DOOR: if (IDX >= 0 && IDX < static_cast(room_data_.doors.size())) { // Truco crítico: durante el drag, moveEntityVisual movió el sprite // pero los WALLs del CollisionMap siguen en la posición antigua. Antes // de llamar a moveDoor (que limpia los tiles "actuales" y escribe los // nuevos), restauramos el sprite a su posición vieja para que coincida // con los tiles. moveDoor luego hace el ciclo limpio y completo. auto* door_mgr = room_->getDoorManager(); door_mgr->getDoor(IDX)->setPosition(room_data_.doors[IDX].x, room_data_.doors[IDX].y); room_data_.doors[IDX].x = drag_.snap_x; room_data_.doors[IDX].y = drag_.snap_y; door_mgr->moveDoor(IDX, drag_.snap_x, drag_.snap_y); selection_ = {EntityType::DOOR, 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; 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; 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]; if (!temp_data.path.empty()) { float dx = drag_.snap_x - temp_data.path[0].x; float dy = drag_.snap_y - temp_data.path[0].y; for (auto& wp : temp_data.path) { wp.x += dx; wp.y += dy; } } room_->getPlatformManager()->getPlatform(drag_.index)->resetToInitialPosition(temp_data); } break; case EntityType::KEY: if (drag_.index >= 0 && drag_.index < room_->getKeyManager()->getCount()) { room_->getKeyManager()->getKey(drag_.index)->setPosition(drag_.snap_x, drag_.snap_y); } break; case EntityType::DOOR: // Solo movemos el sprite visualmente. Los WALLs del CollisionMap NO // se tocan durante el drag (la puerta arrastrada no debería bloquear // su nueva posición todavía). El bookkeeping completo se hace en // commitEntityDrag al soltar. if (drag_.index >= 0 && drag_.index < room_->getDoorManager()->getCount()) { room_->getDoorManager()->getDoor(drag_.index)->setPosition(drag_.snap_x, drag_.snap_y); } 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; 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::ENTITY_INITIAL: case DragTarget::ENTITY_BOUND1: case DragTarget::ENTITY_BOUND2: moveEntityVisual(); 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 auto SZ = static_cast(Tile::SIZE); // 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 = rect.x - 1, .y = rect.y - 1, .w = rect.w + 2, .h = rect.h + 2}; game_surface->drawRectBorder(&border, 9); } // Highlight del drag activo (temporal, color bright_white) if (drag_.target == DragTarget::NONE || !drag_.moved) { return; } const Uint8 DRAG_COLOR = 15; SDL_FRect highlight_rect{}; switch (drag_.target) { case DragTarget::PLAYER: highlight_rect = player_->getRect(); break; 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::ENTITY_BOUND1: case DragTarget::ENTITY_BOUND2: highlight_rect = {.x = drag_.snap_x, .y = drag_.snap_y, .w = SZ, .h = SZ}; 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); } // Estampa el patrón del brush en la posición indicada (anclaje top-left) void MapEditor::stampBrushAt(int tile_x, int tile_y) { if (brush_.isEmpty()) { return; } for (int dy = 0; dy < brush_.height; ++dy) { for (int dx = 0; dx < brush_.width; ++dx) { int value = brush_.at(dx, dy); if (value == BrushPattern::TRANSPARENT) { continue; } // skip — no tocar destino int tx = tile_x + dx; int ty = tile_y + dy; if (tx < 0 || tx >= Map::WIDTH || ty < 0 || ty >= Map::HEIGHT) { continue; } int idx = (ty * Map::WIDTH) + tx; // value es >= 0 (tile real) o BrushPattern::ERASE (escribe -1) if (editing_collision_) { if (idx >= 0 && idx < static_cast(room_data_.collision_tile_map.size())) { if (room_data_.collision_tile_map[idx] != value) { room_data_.collision_tile_map[idx] = value; room_->setCollisionTile(idx, value); } } } else { if (idx >= 0 && idx < static_cast(room_data_.tile_map.size())) { if (room_data_.tile_map[idx] != value) { room_data_.tile_map[idx] = value; room_->setTile(idx, value); } } } } } } // Confirma el eyedropper: muestrea el rect del nivel y lo carga en el brush void MapEditor::commitEyedropper() { int x1 = std::clamp(eyedropper_.start_tile_x, 0, Map::WIDTH - 1); int y1 = std::clamp(eyedropper_.start_tile_y, 0, Map::HEIGHT - 1); int x2 = std::clamp(mouse_tile_x_, 0, Map::WIDTH - 1); int y2 = std::clamp(mouse_tile_y_, 0, Map::HEIGHT - 1); int xmin = std::min(x1, x2); int ymin = std::min(y1, y2); int xmax = std::max(x1, x2); int ymax = std::max(y1, y2); brush_ = sampleBrush(xmin, ymin, xmax, ymax); } // Lee del tilemap activo (draw o collision) un rect y devuelve un BrushPattern. // Las celdas vacías (-1) en el tilemap origen se mapean a BrushPattern::TRANSPARENT // para que reestampar el brush no borre contenido del destino. auto MapEditor::sampleBrush(int x1, int y1, int x2, int y2) const -> BrushPattern { BrushPattern p; p.width = (x2 - x1) + 1; p.height = (y2 - y1) + 1; p.tiles.reserve(static_cast(p.width * p.height)); const auto& src = editing_collision_ ? room_data_.collision_tile_map : room_data_.tile_map; for (int y = y1; y <= y2; ++y) { for (int x = x1; x <= x2; ++x) { int idx = (y * Map::WIDTH) + x; int value = (idx >= 0 && idx < static_cast(src.size())) ? src[idx] : -1; // -1 en el tilemap = vacío → TRANSPARENT (no borra al estampar) p.tiles.push_back((value == -1) ? BrushPattern::TRANSPARENT : value); } } return p; } // Construye un BrushPattern leyendo tiles consecutivos de un tileset. // Usado por el TilePicker cuando se hace selección rectangular. auto MapEditor::buildPatternFromTileset(const std::string& tileset_name, int col, int row, int width, int height) const -> BrushPattern { BrushPattern p; auto surface = Resource::Cache::get()->getSurface(tileset_name); if (!surface || width <= 0 || height <= 0) { return p; } int cols = static_cast(surface->getWidth()) / Tile::SIZE; if (cols <= 0) { return p; } p.width = width; p.height = height; p.tiles.reserve(static_cast(width * height)); for (int dy = 0; dy < height; ++dy) { for (int dx = 0; dx < width; ++dx) { int tile = ((row + dy) * cols) + (col + dx); p.tiles.push_back(tile); } } return p; } // Renderiza la previsualización del brush bajo el cursor (snapped al grid) void MapEditor::renderBrushPreview() { if (brush_.isEmpty()) { return; } auto game_surface = Screen::get()->getRendererSurface(); if (!game_surface) { return; } const std::string tileset_name = editing_collision_ ? std::string("collision.gif") : room_->getTileSetFile(); auto tileset = Resource::Cache::get()->getSurface(tileset_name); int cols = (tileset) ? (static_cast(tileset->getWidth()) / Tile::SIZE) : 0; constexpr auto TS = static_cast(Tile::SIZE); for (int dy = 0; dy < brush_.height; ++dy) { for (int dx = 0; dx < brush_.width; ++dx) { int value = brush_.at(dx, dy); if (value == BrushPattern::TRANSPARENT) { continue; } int tx = mouse_tile_x_ + dx; int ty = mouse_tile_y_ + dy; if (tx < 0 || tx >= Map::WIDTH || ty < 0 || ty >= Map::HEIGHT) { continue; } float dst_x = static_cast(tx) * TS; float dst_y = static_cast(ty) * TS; if (value == BrushPattern::ERASE) { SDL_FRect erase_cell = {.x = dst_x, .y = dst_y, .w = TS, .h = TS}; game_surface->fillRect(&erase_cell, static_cast(room_data_.bg_color)); } else if (tileset && cols > 0) { SDL_FRect src = { .x = static_cast(value % cols) * TS, .y = static_cast(value / cols) * TS, .w = TS, .h = TS}; SDL_FRect dst = {.x = dst_x, .y = dst_y, .w = TS, .h = TS}; tileset->render(&src, &dst); } } } // Border alrededor del rectángulo completo del brush (blanco) float bx = static_cast(mouse_tile_x_) * TS; float by = static_cast(mouse_tile_y_) * TS; SDL_FRect border = { .x = bx - 1.0F, .y = by - 1.0F, .w = (static_cast(brush_.width) * TS) + 2.0F, .h = (static_cast(brush_.height) * TS) + 2.0F}; game_surface->drawRectBorder(&border, 15); } // Renderiza el rectángulo del eyedropper en progreso (cyan brillante) void MapEditor::renderEyedropperRect() { auto game_surface = Screen::get()->getRendererSurface(); if (!game_surface) { return; } int x1 = std::clamp(eyedropper_.start_tile_x, 0, Map::WIDTH - 1); int y1 = std::clamp(eyedropper_.start_tile_y, 0, Map::HEIGHT - 1); int x2 = std::clamp(mouse_tile_x_, 0, Map::WIDTH - 1); int y2 = std::clamp(mouse_tile_y_, 0, Map::HEIGHT - 1); int xmin = std::min(x1, x2); int ymin = std::min(y1, y2); int xmax = std::max(x1, x2); int ymax = std::max(y1, y2); constexpr auto TS = static_cast(Tile::SIZE); SDL_FRect rect = { .x = static_cast(xmin) * TS, .y = static_cast(ymin) * TS, .w = static_cast((xmax - xmin) + 1) * TS, .h = static_cast((ymax - ymin) + 1) * TS}; constexpr Uint8 EYEDROPPER_COLOR = 21; // BRIGHT_CYAN game_surface->drawRectBorder(&rect, EYEDROPPER_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; } // --- 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(); case EntityType::KEY: return room_->getKeyManager()->getCount(); case EntityType::DOOR: return room_->getDoorManager()->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(); case EntityType::KEY: return room_->getKeyManager()->getKey(index)->getCollider(); case EntityType::DOOR: return room_->getDoorManager()->getDoor(index)->getCollider(); default: return {}; } } auto MapEditor::entityHasBoundaries(EntityType type) -> bool { return type == EntityType::ENEMY; } 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}; } 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: { const auto& path = room_data_.platforms[index].path; return path.empty() ? std::pair{0.0F, 0.0F} : std::pair{path[0].x, path[0].y}; } case EntityType::KEY: return {room_data_.keys[index].x, room_data_.keys[index].y}; case EntityType::DOOR: return {room_data_.doors[index].x, room_data_.doors[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()); case EntityType::KEY: return static_cast(room_data_.keys.size()); case EntityType::DOOR: return static_cast(room_data_.doors.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"; case EntityType::KEY: return "key"; case EntityType::DOOR: return "door"; 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; } // Colores para la entidad seleccionada (brillantes) constexpr Uint8 SEL_BOUND1 = 21; // BRIGHT_CYAN constexpr Uint8 SEL_BOUND2 = 25; // BRIGHT_YELLOW constexpr Uint8 SEL_ROUTE = 26; // BRIGHT_WHITE // Colores para entidades no seleccionadas (apagados) constexpr Uint8 DIM_BOUND1 = 11; // CYAN constexpr Uint8 DIM_BOUND2 = 13; // YELLOW constexpr Uint8 DIM_ROUTE = 14; // WHITE (gris medio) constexpr float HALF = Tile::SIZE / 2.0F; for (int i = 0; i < entityDataCount(EntityType::ENEMY); ++i) { auto [pos_x, pos_y] = entityPosition(EntityType::ENEMY, i); auto bd = entityBoundaries(EntityType::ENEMY, i); bool is_selected = selection_.is(EntityType::ENEMY) && selection_.index == i; // Posiciones base (pueden estar siendo arrastradas) float init_x = pos_x; float init_y = pos_y; auto b1_x = static_cast(bd.x1); auto b1_y = static_cast(bd.y1); auto b2_x = static_cast(bd.x2); auto b2_y = static_cast(bd.y2); // Si estamos arrastrando una boundary de esta entidad, usar la posición snapped if (drag_.entity_type == EntityType::ENEMY && drag_.index == i) { if (drag_.target == DragTarget::ENTITY_BOUND1) { b1_x = drag_.snap_x; b1_y = drag_.snap_y; } else if (drag_.target == DragTarget::ENTITY_BOUND2) { b2_x = drag_.snap_x; b2_y = drag_.snap_y; } else if (drag_.target == DragTarget::ENTITY_INITIAL) { init_x = drag_.snap_x; init_y = drag_.snap_y; } is_selected = true; // Arrastrando = siempre iluminado } Uint8 color_b1 = is_selected ? SEL_BOUND1 : DIM_BOUND1; Uint8 color_b2 = is_selected ? SEL_BOUND2 : DIM_BOUND2; Uint8 color_route = is_selected ? SEL_ROUTE : DIM_ROUTE; // Dibujar líneas de ruta game_surface->drawLine(b1_x + HALF, b1_y + HALF, init_x + HALF, init_y + HALF, color_route); game_surface->drawLine(init_x + HALF, init_y + HALF, b2_x + HALF, b2_y + HALF, color_route); // Marcadores en las boundaries renderBoundaryMarker(b1_x, b1_y, color_b1); renderBoundaryMarker(b2_x, b2_y, color_b2); } // Render platform waypoint routes for (int i = 0; i < entityDataCount(EntityType::PLATFORM); ++i) { const auto& plat = room_data_.platforms[i]; if (plat.path.size() < 2) { continue; } bool is_selected = selection_.is(EntityType::PLATFORM) && selection_.index == i; // If dragging this platform, apply the drag offset to all points for visualization float drag_dx = 0.0F; float drag_dy = 0.0F; if (drag_.entity_type == EntityType::PLATFORM && drag_.index == i && drag_.target == DragTarget::ENTITY_INITIAL) { if (!plat.path.empty()) { drag_dx = drag_.snap_x - plat.path[0].x; drag_dy = drag_.snap_y - plat.path[0].y; } is_selected = true; } Uint8 color_wp = is_selected ? SEL_BOUND1 : DIM_BOUND1; Uint8 color_route = is_selected ? SEL_ROUTE : DIM_ROUTE; // Draw route lines between consecutive waypoints for (int j = 0; j < static_cast(plat.path.size()) - 1; ++j) { float ax = plat.path[j].x + drag_dx + HALF; float ay = plat.path[j].y + drag_dy + HALF; float bx = plat.path[j + 1].x + drag_dx + HALF; float by = plat.path[j + 1].y + drag_dy + HALF; game_surface->drawLine(ax, ay, bx, by, color_route); } // For circular mode, draw line from last to first if (plat.loop == LoopMode::CIRCULAR && plat.path.size() > 2) { int last = static_cast(plat.path.size()) - 1; float ax = plat.path[last].x + drag_dx + HALF; float ay = plat.path[last].y + drag_dy + HALF; float bx = plat.path[0].x + drag_dx + HALF; float by = plat.path[0].y + drag_dy + HALF; game_surface->drawLine(ax, ay, bx, by, color_route); } // Draw waypoint markers for (const auto& wp : plat.path) { renderBoundaryMarker(wp.x + drag_dx, wp.y + drag_dy, color_wp); } } } // 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 mouse_tile_x_ = std::max(mouse_tile_x_, 0); if (mouse_tile_x_ >= PlayArea::WIDTH / Tile::SIZE) { mouse_tile_x_ = PlayArea::WIDTH / Tile::SIZE - 1; } mouse_tile_y_ = std::max(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() { // NOLINT(readability-function-cognitive-complexity) 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::ENTITY_INITIAL: line5 = std::string("dragging: ") + entityLabel(drag_.entity_type) + " " + std::to_string(drag_.index); break; case DragTarget::ENTITY_BOUND1: line5 = std::string("dragging: ") + entityLabel(drag_.entity_type)[0] + std::to_string(drag_.index) + " bound1"; break; case DragTarget::ENTITY_BOUND2: line5 = std::string("dragging: ") + entityLabel(drag_.entity_type)[0] + std::to_string(drag_.index) + " bound2"; break; case DragTarget::NONE: break; } } // Líneas 2-3 según selección 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(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); std::string c1_marker = it.color1_overridden ? "*" : ""; std::string c2_marker = it.color2_overridden ? "*" : ""; line3 = "col:" + std::to_string(it.color1) + c1_marker + "/" + std::to_string(it.color2) + c2_marker; } 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; if (anim.size() > 5 && anim.substr(anim.size() - 5) == ".yaml") { anim = anim.substr(0, anim.size() - 5); } line2 = "platform " + std::to_string(selection_.index) + ": " + anim; line3 = "speed:" + std::to_string(static_cast(p.speed)) + " " + (p.loop == LoopMode::CIRCULAR ? "circular" : "pingpong"); if (p.easing != "linear") { line3 += " " + p.easing; } } break; case EntityType::KEY: if (selection_.index < static_cast(room_data_.keys.size())) { const auto& k = room_data_.keys[selection_.index]; std::string anim = k.animation_path; auto dot = anim.rfind('.'); if (dot != std::string::npos) { anim = anim.substr(0, dot); } line2 = "key " + std::to_string(selection_.index) + ": " + anim; line3 = "id: " + k.id; } break; case EntityType::DOOR: if (selection_.index < static_cast(room_data_.doors.size())) { const auto& d = room_data_.doors[selection_.index]; std::string anim = d.animation_path; auto dot = anim.rfind('.'); if (dot != std::string::npos) { anim = anim.substr(0, dot); } line2 = "door " + std::to_string(selection_.index) + ": " + anim; line3 = "id: " + d.id; } break; case EntityType::NONE: { // Propiedades de la habitación std::string ts_marker = room_data_.tile_set_overridden ? " (ts*)" : ""; std::string mu_marker = room_data_.music_overridden ? " (mu*)" : ""; line2 = "zone:" + room_data_.zone + ts_marker + mu_marker; 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); break; } } // Línea 4: brush activo std::string line4; if (!brush_.isEmpty()) { if (brush_.width == 1 && brush_.height == 1) { int v = brush_.tiles[0]; line4 = (v == BrushPattern::ERASE) ? "brush: eraser (e)" : ("brush: tile " + std::to_string(v)); } else { line4 = "brush: " + std::to_string(brush_.width) + "x" + std::to_string(brush_.height); } } statusbar_->setLine2(line2); statusbar_->setLine3(line3); statusbar_->setLine4(line4); statusbar_->setLine5(line5); // Actualizar el prompt de la consola según la selección if (selection_.isNone()) { Console::get()->setPrompt("room> "); } else { Console::get()->setPrompt(std::string(entityLabel(selection_.type)) + " " + std::to_string(selection_.index) + "> "); } } // Devuelve las propiedades válidas de SET según la selección actual auto MapEditor::getSetCompletions() const -> std::vector { switch (selection_.type) { case EntityType::ENEMY: return {"ANIMATION", "VX", "VY", "FLIP", "MIRROR"}; case EntityType::ITEM: return {"TILE", "COUNTER", "COLOR1", "COLOR2"}; case EntityType::PLATFORM: return {"ANIMATION", "SPEED", "LOOP", "EASING"}; case EntityType::KEY: return {"ID", "ANIMATION"}; case EntityType::DOOR: return {"ID", "ANIMATION"}; default: return {"ZONE", "TILESET", "MUSIC", "UP", "DOWN", "LEFT", "RIGHT"}; } } // Devuelve la lista de animaciones disponibles para la entidad seleccionada, // escaneando la carpeta correspondiente en data/. Cada tipo de entidad solo // puede usar animaciones de su propio directorio. Para entidades sin animación // (Item, Room/none) devuelve lista vacía. auto MapEditor::getAnimationCompletions() const -> std::vector { const char* folder = nullptr; switch (selection_.type) { case EntityType::ENEMY: folder = "data/enemies"; break; case EntityType::PLATFORM: folder = "data/platforms"; break; case EntityType::KEY: folder = "data/keys"; break; case EntityType::DOOR: folder = "data/doors"; break; default: return {}; } std::vector result; std::error_code ec; for (const auto& entry : std::filesystem::directory_iterator(folder, ec)) { if (ec) { break; } if (!entry.is_regular_file()) { continue; } const auto& path = entry.path(); if (path.extension() != ".yaml") { continue; } result.push_back(toUpper(path.stem().string())); } std::sort(result.begin(), result.end()); return result; } // Modifica una propiedad del enemigo seleccionado auto MapEditor::setEnemyProperty(const std::string& property, const std::string& value) -> std::string { // NOLINT(readability-function-cognitive-complexity) if (!active_) { return "Editor not active"; } if (!hasSelectedEnemy()) { return "No enemy selected"; } auto& enemy = room_data_.enemies[selection_.index]; 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(selection_.index) = Enemy::create(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") { return "color property removed (legacy)"; } 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(selection_.index)->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(selection_.index)->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(selection_.index) = Enemy::create(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(selection_.index) = Enemy::create(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, 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.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(Enemy::create(new_enemy)); // Seleccionar el nuevo enemigo int new_index = static_cast(room_data_.enemies.size()) - 1; selection_ = {EntityType::ENEMY, new_index}; autosave(); return "Added enemy " + std::to_string(new_index); } // Elimina el enemigo seleccionado auto MapEditor::deleteEnemy() -> std::string { if (!active_) { return "Editor not active"; } if (!hasSelectedEnemy()) { return "No enemy selected"; } const int IDX = selection_.index; // 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(Enemy::create(enemy_data)); } selection_.clear(); 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[selection_.index]; // 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(Enemy::create(copy)); // Seleccionar el nuevo enemigo int new_index = static_cast(room_data_.enemies.size()) - 1; selection_ = {EntityType::ENEMY, new_index}; autosave(); return "Duplicated as enemy " + std::to_string(new_index); } // Modifica una propiedad de la habitación auto MapEditor::setRoomProperty(const std::string& property, const std::string& value) -> std::string { // NOLINT(readability-function-cognitive-complexity) if (!active_) { return "Editor not active"; } std::string val = toLower(value); if (property == "ZONE") { const Zone::Data* zone = ZoneManager::get()->getZone(val); if (zone == nullptr) { return "Unknown zone: " + val; } room_data_.zone = val; // Si no hay overrides, propagar el tileset y la música de la nueva zona if (!room_data_.tile_set_overridden) { room_data_.tile_set_file = zone->tile_set_file; room_->setTileSet(zone->tile_set_file); } if (!room_data_.music_overridden) { room_data_.music = zone->music; } autosave(); return "zone: " + val; } if (property == "TILESET") { // "reset" / "none" limpia el override y vuelve a heredar de la zona if (val == "reset" || val == "none") { room_data_.tile_set_overridden = false; const Zone::Data* zone = ZoneManager::get()->getZone(room_data_.zone); if (zone != nullptr) { room_data_.tile_set_file = zone->tile_set_file; room_->setTileSet(zone->tile_set_file); } autosave(); return "tileset: (inherit from zone)"; } std::string tileset = val; if (tileset.find('.') == std::string::npos) { tileset += ".gif"; } room_data_.tile_set_file = tileset; room_data_.tile_set_overridden = true; room_->setTileSet(tileset); autosave(); return "tileset: " + tileset; } if (property == "MUSIC") { // "reset" / "none" limpia el override y vuelve a heredar de la zona if (val == "reset" || val == "none") { room_data_.music_overridden = false; const Zone::Data* zone = ZoneManager::get()->getZone(room_data_.zone); if (zone != nullptr) { room_data_.music = zone->music; } autosave(); return "music: (inherit from zone)"; } // Nota: el valor se guarda tal cual (case-sensitive). val ya está en lower. // Usamos el value original para respetar mayúsculas del nombre del fichero. std::string music = value; if (music.find('.') == std::string::npos) { music += ".ogg"; } room_data_.music = music; room_data_.music_overridden = true; autosave(); return "music: " + music; } // 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()) { RoomFormat::saveYAML(other_path, *old_other); } } } // Aplicar la nueva conexión if (our_field != nullptr) { *our_field = connection; } // Sincronizar la conexión al Room vivo Room::Border border = Room::Border::NONE; if (property == "UP") { border = Room::Border::TOP; } else if (property == "DOWN") { border = Room::Border::BOTTOM; } else if (property == "LEFT") { border = Room::Border::LEFT; } else if (property == "RIGHT") { border = Room::Border::RIGHT; } room_->setConnection(border, 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()) { RoomFormat::saveYAML(other_path, *other); } } } autosave(); return toLower(property) + ": " + connection; } return "Unknown property: " + property + " (use: zone, itemcolor1, itemcolor2, conveyor, tileset, music, up, down, left, right)"; } // Crea una nueva habitación auto MapEditor::createNewRoom(const std::string& direction) -> std::string { // NOLINT(readability-function-cognitive-complexity) 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; // Construir Room::Data por defecto desde la autoridad del formato Room::Data new_room = RoomFormat::createDefault(); new_room.number = std::string(name_buf).substr(0, std::string(name_buf).find('.')); // Heredar la zona de la room actual (y sus tileset/música resueltos), para // que el usuario no tenga que asignarla manualmente al expandir un nivel. // No marcamos los overrides: si la room actual tenía overrides explícitos, // la nueva se queda con los valores resueltos pero como heredados de zona. new_room.zone = room_data_.zone; const Zone::Data* zone = ZoneManager::get()->getZone(new_room.zone); if (zone != nullptr) { new_room.tile_set_file = zone->tile_set_file; new_room.music = zone->music; } // 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_; } // Persistir vía la autoridad del formato (no más std::ofstream a pelo) auto save_result = RoomFormat::saveYAML(new_path, new_room); if (save_result.find("Error") == 0) { return save_result; } // Registrar en Resource::List (mapa + assets.yaml) y cache Resource::List::get()->addAsset(new_path, Resource::List::Type::ROOM); Resource::Cache::get()->getRooms().emplace_back( RoomResource{.name = new_name, .room = std::make_shared(new_room)}); // Conectar la room actual con la nueva (recíproco: ya hecho arriba para la nueva). // Actualizamos tres sitios para que la conexión sea visible inmediatamente: // 1. room_data_ (la copia del editor) → será autosaveada al yaml // 2. la Room viva del juego (room_) → para que el navigation funcione sin reload // 3. la Room::Data cacheada en Resource::Cache → para que adjacent rooms y // futuros reloads vean la conexión nueva Room::Border border = Room::Border::NONE; if (direction == "UP") { room_data_.upper_room = new_name; border = Room::Border::TOP; } else if (direction == "DOWN") { room_data_.lower_room = new_name; border = Room::Border::BOTTOM; } else if (direction == "LEFT") { room_data_.left_room = new_name; border = Room::Border::LEFT; } else if (direction == "RIGHT") { room_data_.right_room = new_name; border = Room::Border::RIGHT; } if (border != Room::Border::NONE) { // Sincronizar con la Room viva (Game::room_cache_ apunta a este shared_ptr) room_->setConnection(border, new_name); // Sincronizar con Resource::Cache::rooms_ (datos crudos) auto cached = Resource::Cache::get()->getRoom(room_path_); if (cached) { if (direction == "UP") { cached->upper_room = new_name; } else if (direction == "DOWN") { cached->lower_room = new_name; } else if (direction == "LEFT") { cached->left_room = new_name; } else if (direction == "RIGHT") { cached->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 { // NOLINT(readability-function-cognitive-complexity) 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 unlink_reciprocal = [&](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()) { RoomFormat::saveYAML(other_path, *other); } }; unlink_reciprocal(room_data_.upper_room, "lower_room"); // Si nosotros somos su lower unlink_reciprocal(room_data_.lower_room, "upper_room"); // Si nosotros somos su upper unlink_reciprocal(room_data_.left_room, "right_room"); // Si nosotros somos su right unlink_reciprocal(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(); std::erase_if(cache_rooms, [&](const RoomResource& r) { return r.name == deleted_name; }); // Quitar de Resource::List (mapa + assets.yaml) Resource::List::get()->removeAsset(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; } // 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[selection_.index]; 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); } if (property == "COLOR1") { // "reset" / "none" limpia el override y vuelve al default const std::string LOWER = toLower(value); if (LOWER == "reset" || LOWER == "none") { item.color1 = Item::DEFAULT_COLOR1; item.color1_overridden = false; // Recrear el sprite vivo con los colores nuevos room_->getItemManager()->getItem(selection_.index) = std::make_shared(item); autosave(); return "color1: (default)"; } try { item.color1 = static_cast(std::stoi(value)); } catch (...) { return "Invalid value: " + value; } item.color1_overridden = true; room_->getItemManager()->getItem(selection_.index) = std::make_shared(item); autosave(); return "color1: " + std::to_string(item.color1); } if (property == "COLOR2") { const std::string LOWER = toLower(value); if (LOWER == "reset" || LOWER == "none") { item.color2 = Item::DEFAULT_COLOR2; item.color2_overridden = false; room_->getItemManager()->getItem(selection_.index) = std::make_shared(item); autosave(); return "color2: (default)"; } try { item.color2 = static_cast(std::stoi(value)); } catch (...) { return "Invalid value: " + value; } item.color2_overridden = true; room_->getItemManager()->getItem(selection_.index) = std::make_shared(item); autosave(); return "color2: " + std::to_string(item.color2); } return "Unknown property: " + property + " (use: tile, counter, color1, color2)"; } // 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, tileset_name](int col, int row, int /*width*/, int /*height*/) { if (!hasSelectedItem()) { return; } // Items solo usan un tile: tomamos siempre la celda superior-izquierda del rect auto surface = Resource::Cache::get()->getSurface(tileset_name); if (!surface) { return; } int cols = static_cast(surface->getWidth()) / Tile::SIZE; if (cols <= 0) { return; } int tile = (row * cols) + col; room_data_.items[selection_.index].tile = tile; room_->getItemManager()->getItem(selection_.index)->setTile(tile); autosave(); }; // Pasar color de fondo de la habitación + color del item seleccionado para previsualizarlo Uint8 preview_color = hasSelectedItem() ? room_data_.items[selection_.index].color1 : Item::DEFAULT_COLOR1; tile_picker_.open(tileset_name, current_tile, room_data_.bg_color, 1, preview_color, 0, 1, true); } // 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; // Los colores quedan en sus defaults (Item::DEFAULT_COLOR1/2) room_data_.items.push_back(new_item); room_->getItemManager()->addItem(std::make_shared(new_item)); int new_index = static_cast(room_data_.items.size()) - 1; selection_ = {EntityType::ITEM, new_index}; autosave(); return "Added item " + std::to_string(new_index); } // Elimina el item seleccionado auto MapEditor::deleteItem() -> std::string { if (!active_) { return "Editor not active"; } if (!hasSelectedItem()) { return "No item selected"; } const int IDX = selection_.index; 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)); } selection_.clear(); 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[selection_.index]; copy.x += Tile::SIZE; room_data_.items.push_back(copy); room_->getItemManager()->addItem(std::make_shared(copy)); int new_index = static_cast(room_data_.items.size()) - 1; selection_ = {EntityType::ITEM, new_index}; autosave(); 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 == "SPEED") { try { platform.speed = std::stof(value); } catch (...) { return "Invalid value: " + value; } room_->getPlatformManager()->getPlatform(selection_.index)->resetToInitialPosition(platform); autosave(); return "speed: " + std::to_string(static_cast(platform.speed)); } if (property == "LOOP") { std::string val = toLower(value); if (val == "circular") { platform.loop = LoopMode::CIRCULAR; } else { platform.loop = LoopMode::PINGPONG; val = "pingpong"; } room_->getPlatformManager()->getPlatform(selection_.index)->resetToInitialPosition(platform); autosave(); return "loop: " + val; } if (property == "EASING") { platform.easing = toLower(value); room_->getPlatformManager()->getPlatform(selection_.index)->resetToInitialPosition(platform); autosave(); return "easing: " + platform.easing; } return "Unknown property: " + property + " (use: animation, speed, loop, easing)"; } // Crea una nueva plataforma con valores por defecto, centrada en la habitación auto MapEditor::addPlatform() -> std::string { if (!active_) { return "Editor not active"; } MovingPlatform::Data new_platform; new_platform.animation_path = "bin.yaml"; new_platform.speed = 24.0F; new_platform.frame = -1; constexpr float CENTER_X = PlayArea::CENTER_X; constexpr float CENTER_Y = PlayArea::CENTER_Y; constexpr float ROUTE_HALF = 2.0F * Tile::SIZE; new_platform.path = { {CENTER_X - ROUTE_HALF, CENTER_Y, 0.0F}, {CENTER_X + ROUTE_HALF, CENTER_Y, 0.0F}}; room_data_.platforms.push_back(new_platform); room_->getPlatformManager()->addPlatform(std::make_shared(new_platform)); 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]; for (auto& wp : copy.path) { wp.x += 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); } // ============================================================================ // LLAVES // ============================================================================ // Modifica una propiedad de la llave seleccionada auto MapEditor::setKeyProperty(const std::string& property, const std::string& value) -> std::string { if (!active_) { return "Editor not active"; } if (!hasSelectedKey()) { return "No key selected"; } auto& key = room_data_.keys[selection_.index]; if (property == "ID") { key.id = value; // Recrear el Key (el id se pasa al constructor) try { room_->getKeyManager()->getKey(selection_.index) = std::make_shared(key); } catch (const std::exception& e) { return std::string("Error: ") + e.what(); } autosave(); return "id: " + value; } if (property == "ANIMATION") { std::string anim = toLower(value); if (anim.find('.') == std::string::npos) { anim += ".yaml"; } std::string old_anim = key.animation_path; key.animation_path = anim; try { room_->getKeyManager()->getKey(selection_.index) = std::make_shared(key); } catch (const std::exception& e) { key.animation_path = old_anim; return std::string("Error: ") + e.what(); } autosave(); return "animation: " + anim; } return "Unknown property: " + property + " (use: id, animation)"; } // Crea una nueva llave centrada en la habitación auto MapEditor::addKey() -> std::string { if (!active_) { return "Editor not active"; } Key::Data new_key; new_key.animation_path = "key1.yaml"; new_key.id = "1"; new_key.x = static_cast(PlayArea::CENTER_X); new_key.y = static_cast(PlayArea::CENTER_Y); room_data_.keys.push_back(new_key); try { room_->getKeyManager()->addKey(std::make_shared(new_key)); } catch (const std::exception& e) { room_data_.keys.pop_back(); return std::string("Error: ") + e.what(); } int new_index = static_cast(room_data_.keys.size()) - 1; selection_ = {EntityType::KEY, new_index}; autosave(); return "Added key " + std::to_string(new_index); } // Elimina la llave seleccionada auto MapEditor::deleteKey() -> std::string { if (!active_) { return "Editor not active"; } if (!hasSelectedKey()) { return "No key selected"; } const int IDX = selection_.index; room_data_.keys.erase(room_data_.keys.begin() + IDX); // Recrear todas las llaves (los índices cambian al borrar) auto* key_mgr = room_->getKeyManager(); key_mgr->clear(); for (const auto& key_data : room_data_.keys) { key_mgr->addKey(std::make_shared(key_data)); } selection_.clear(); autosave(); return "Deleted key " + std::to_string(IDX); } // Duplica la llave seleccionada (la pone un tile a la derecha) auto MapEditor::duplicateKey() -> std::string { if (!active_) { return "Editor not active"; } if (!hasSelectedKey()) { return "No key selected"; } Key::Data copy = room_data_.keys[selection_.index]; copy.x += Tile::SIZE; room_data_.keys.push_back(copy); try { room_->getKeyManager()->addKey(std::make_shared(copy)); } catch (const std::exception& e) { room_data_.keys.pop_back(); return std::string("Error: ") + e.what(); } int new_index = static_cast(room_data_.keys.size()) - 1; selection_ = {EntityType::KEY, new_index}; autosave(); return "Duplicated as key " + std::to_string(new_index); } // ============================================================================ // PUERTAS // ============================================================================ // Reconstruye todas las puertas vivas desde room_data_, limpiando primero los // WALLs antiguos del CollisionMap. Lo usa setDoorProperty cuando un cambio // (id, animation) requiere recrear el Door y mantener los tiles sincronizados. void MapEditor::rebuildDoors() { auto* door_mgr = room_->getDoorManager(); // Borrar una a una desde el principio: cada removeDoor limpia sus WALLs while (door_mgr->getCount() > 0) { door_mgr->removeDoor(0); } // Re-añadir desde room_data_; addDoor reescribe los WALLs si bloquean for (const auto& d : room_data_.doors) { door_mgr->addDoor(std::make_shared(d, /*start_opened=*/false)); } } // Modifica una propiedad de la puerta seleccionada auto MapEditor::setDoorProperty(const std::string& property, const std::string& value) -> std::string { if (!active_) { return "Editor not active"; } if (!hasSelectedDoor()) { return "No door selected"; } auto& door = room_data_.doors[selection_.index]; if (property == "ID") { door.id = value; // Recrear preserva el id pero pasa por el constructor → rebuild completo // para mantener los tiles del CollisionMap sincronizados. rebuildDoors(); autosave(); return "id: " + value; } if (property == "ANIMATION") { std::string anim = toLower(value); if (anim.find('.') == std::string::npos) { anim += ".yaml"; } std::string old_anim = door.animation_path; door.animation_path = anim; try { rebuildDoors(); } catch (const std::exception& e) { door.animation_path = old_anim; // Reconstruir con los datos viejos para dejar el manager coherente rebuildDoors(); return std::string("Error: ") + e.what(); } autosave(); return "animation: " + anim; } return "Unknown property: " + property + " (use: id, animation)"; } // Crea una nueva puerta centrada en la habitación, alineada al grid de 8 px auto MapEditor::addDoor() -> std::string { if (!active_) { return "Editor not active"; } Door::Data new_door; new_door.animation_path = "door1.yaml"; new_door.id = "1"; // Centrar y alinear al grid (la puerta ocupa 1×4 tiles) new_door.x = static_cast((PlayArea::CENTER_X / Tile::SIZE) * Tile::SIZE); new_door.y = static_cast((PlayArea::CENTER_Y / Tile::SIZE) * Tile::SIZE); room_data_.doors.push_back(new_door); try { // addDoor del manager ya escribe los WALLs si la puerta es bloqueante // (lo es por defecto, porque pasamos start_opened=false). room_->getDoorManager()->addDoor(std::make_shared(new_door, /*start_opened=*/false)); } catch (const std::exception& e) { room_data_.doors.pop_back(); return std::string("Error: ") + e.what(); } int new_index = static_cast(room_data_.doors.size()) - 1; selection_ = {EntityType::DOOR, new_index}; autosave(); return "Added door " + std::to_string(new_index); } // Elimina la puerta seleccionada (limpia los WALLs del CollisionMap) auto MapEditor::deleteDoor() -> std::string { if (!active_) { return "Editor not active"; } if (!hasSelectedDoor()) { return "No door selected"; } const int IDX = selection_.index; // Importante: usar removeDoor del manager (limpia los WALLs antes de borrar) room_->getDoorManager()->removeDoor(IDX); room_data_.doors.erase(room_data_.doors.begin() + IDX); selection_.clear(); autosave(); return "Deleted door " + std::to_string(IDX); } // Duplica la puerta seleccionada (la pone una altura entera abajo, 4 tiles, // para no solapar visualmente) auto MapEditor::duplicateDoor() -> std::string { if (!active_) { return "Editor not active"; } if (!hasSelectedDoor()) { return "No door selected"; } Door::Data copy = room_data_.doors[selection_.index]; copy.y += 4 * Tile::SIZE; // 4 tiles = altura completa de una puerta room_data_.doors.push_back(copy); try { room_->getDoorManager()->addDoor(std::make_shared(copy, /*start_opened=*/false)); } catch (const std::exception& e) { room_data_.doors.pop_back(); return std::string("Error: ") + e.what(); } int new_index = static_cast(room_data_.doors.size()) - 1; selection_ = {EntityType::DOOR, new_index}; autosave(); return "Duplicated as door " + std::to_string(new_index); } // 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 = 1; 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() const { auto game_surface = Screen::get()->getRendererSurface(); if (!game_surface) { return; } const Uint8 COLOR = pickGridColor(0, 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