#ifdef _DEBUG #include "game/editor/map_editor.hpp" #include #include // Para std::round #include // Para cout #include "core/input/mouse.hpp" // Para Mouse #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 "game/editor/editor_statusbar.hpp" // Para EditorStatusBar #include "game/ui/console.hpp" // Para Console #include "game/editor/room_saver.hpp" // Para RoomSaver #include "game/entities/player.hpp" // Para Player #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 "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() = default; // Destructor MapEditor::~MapEditor() = default; // 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 } // Guardar estado de invencibilidad y forzarla invincible_before_editor_ = Options::cheats.invincible; Options::cheats.invincible = Options::Cheat::State::ENABLED; player_->setColor(); // 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; active_ = true; std::cout << "MapEditor: ON (room " << room_path_ << ")\n"; } // Sale del modo editor void MapEditor::exit() { if (!active_) { return; } active_ = false; // Restaurar invencibilidad Options::cheats.invincible = invincible_before_editor_; player_->setColor(); // Restaurar prompt de la consola y limpiar estado selected_enemy_ = -1; Console::get()->setPrompt("> "); 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 los sprites vivos a las posiciones originales room_->resetEnemyPositions(room_data_.enemies); // Resetear items (recargar posiciones) 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); } 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(); } // 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í // 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(); // Renderizar barra de estado del editor (reemplaza al scoreboard) if (statusbar_) { statusbar_->render(); } } // Maneja eventos del editor void MapEditor::handleEvent(const SDL_Event& event) { if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_LEFT) { handleMouseDown(mouse_game_x_, mouse_game_y_); } else if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && event.button.button == SDL_BUTTON_LEFT) { 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 selected_enemy_ = -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) { // Toggle selección: si ya estaba seleccionado, deseleccionar selected_enemy_ = (selected_enemy_ == IDX) ? -1 : IDX; } else { // Click en otro tipo de entidad: deseleccionar enemigo selected_enemy_ = -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_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 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 sel_info; std::string sel_detail; // Info de drag activo (prioridad) if (drag_.target != DragTarget::NONE && drag_.moved) { switch (drag_.target) { case DragTarget::PLAYER: sel_info = "player"; break; case DragTarget::ENEMY_INITIAL: sel_info = "enemy " + std::to_string(drag_.index); break; case DragTarget::ENEMY_BOUND1: sel_info = "e" + std::to_string(drag_.index) + " bound1"; break; case DragTarget::ENEMY_BOUND2: sel_info = "e" + std::to_string(drag_.index) + " bound2"; break; case DragTarget::ITEM: sel_info = "item " + std::to_string(drag_.index); break; case DragTarget::NONE: break; } } // Info de enemigo seleccionado (persistente) else if (selected_enemy_ >= 0 && selected_enemy_ < static_cast(room_data_.enemies.size())) { const auto& e = room_data_.enemies[selected_enemy_]; // Extraer nombre corto de la animación (sin .yaml) std::string anim = e.animation_path; auto dot = anim.rfind('.'); if (dot != std::string::npos) { anim = anim.substr(0, dot); } sel_info = "enemy " + std::to_string(selected_enemy_) + ": " + anim; sel_detail = e.color + " vx:" + std::to_string(static_cast(e.vx)) + " vy:" + std::to_string(static_cast(e.vy)); if (e.flip) { sel_detail += " flip"; } } statusbar_->setSelectionInfo(sel_info); statusbar_->setSelectionDetail(sel_detail); // 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 { Console::get()->setPrompt("> "); } } // ¿Hay un enemigo seleccionado? auto MapEditor::hasSelectedEnemy() const -> bool { return selected_enemy_ >= 0 && selected_enemy_ < static_cast(room_data_.enemies.size()); } // 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"); room_->getEnemyManager()->getEnemy(selected_enemy_)->resetToInitialPosition(enemy); autosave(); return std::string("flip: ") + (enemy.flip ? "on" : "off"); } return "Unknown property: " + property + " (use: animation, color, vx, vy, flip)"; } #endif // _DEBUG