2431 lines
92 KiB
C++
2431 lines
92 KiB
C++
#ifdef _DEBUG
|
||
|
||
#include "game/editor/map_editor.hpp"
|
||
|
||
#include <SDL3/SDL.h>
|
||
|
||
#include <algorithm>
|
||
#include <cmath> // Para std::round
|
||
#include <cstdio> // Para std::remove (borrar fichero)
|
||
#include <fstream> // Para ifstream, ofstream
|
||
#include <iostream> // Para cout
|
||
#include <set> // Para set
|
||
|
||
#include "core/input/mouse.hpp" // Para Mouse
|
||
#include "core/rendering/render_info.hpp" // Para RenderInfo
|
||
#include "core/rendering/screen.hpp" // Para Screen
|
||
#include "core/rendering/surface.hpp" // Para Surface
|
||
#include "core/resources/resource_cache.hpp" // Para Resource::Cache
|
||
#include "core/resources/resource_list.hpp" // Para Resource::List
|
||
#include "core/resources/resource_types.hpp" // Para RoomResource
|
||
#include "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<char>(file)), std::istreambuf_iterator<char>());
|
||
file.close();
|
||
|
||
try {
|
||
auto yaml = fkyaml::node::deserialize(content);
|
||
if (yaml.contains("grid")) { settings_.grid = yaml["grid"].get_value<bool>(); }
|
||
if (yaml.contains("show_render_info")) { settings_.show_render_info = yaml["show_render_info"].get_value<bool>(); }
|
||
if (yaml.contains("minimap_bg")) {
|
||
if (yaml["minimap_bg"].is_integer()) {
|
||
settings_.minimap_bg = static_cast<Uint8>(yaml["minimap_bg"].get_value<int>());
|
||
} else if (yaml["minimap_bg"].is_string()) {
|
||
settings_.minimap_bg = static_cast<Uint8>(safeStoi(yaml["minimap_bg"].get_value<std::string>(), 2));
|
||
}
|
||
}
|
||
if (yaml.contains("minimap_conn")) {
|
||
if (yaml["minimap_conn"].is_integer()) {
|
||
settings_.minimap_conn = static_cast<Uint8>(yaml["minimap_conn"].get_value<int>());
|
||
} else if (yaml["minimap_conn"].is_string()) {
|
||
settings_.minimap_conn = static_cast<Uint8>(safeStoi(yaml["minimap_conn"].get_value<std::string>(), 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<int>(settings_.minimap_bg) << "\n";
|
||
file << "minimap_conn: " << static_cast<int>(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_tile_ = NO_BRUSH; // Resetear brush al cambiar de modo
|
||
return editing_collision_ ? "Editing: collision" : "Editing: draw";
|
||
}
|
||
|
||
void MapEditor::toggleMiniMap() {
|
||
if (!mini_map_) {
|
||
mini_map_ = std::make_unique<MiniMap>(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<MiniMap>(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<Uint8>(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<Uint8>(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> room, std::shared_ptr<Player> player, const std::string& room_path, std::shared_ptr<Scoreboard::Data> 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<EditorStatusBar>(room_->getNumber());
|
||
|
||
// Resetear estado (preservar modo de edición en re-enter)
|
||
drag_ = {};
|
||
selection_.clear();
|
||
if (!is_reenter) {
|
||
brush_tile_ = NO_BRUSH;
|
||
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<size_t>(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<Item>(i));
|
||
}
|
||
|
||
auto* platform_mgr = room_->getPlatformManager();
|
||
platform_mgr->clear();
|
||
for (const auto& p : room_data_.platforms) {
|
||
platform_mgr->addPlatform(std::make_shared<MovingPlatform>(p));
|
||
}
|
||
|
||
// Restaurar el tilemap completo
|
||
for (int i = 0; i < static_cast<int>(room_data_.tile_map.size()); ++i) {
|
||
room_->setTile(i, room_data_.tile_map[i]);
|
||
}
|
||
|
||
selection_.clear();
|
||
brush_tile_ = NO_BRUSH;
|
||
return "Reverted to original";
|
||
}
|
||
|
||
// Auto-guarda al YAML tras soltar una entidad
|
||
void MapEditor::autosave() {
|
||
if (file_path_.empty()) { return; }
|
||
|
||
// Sincronizar posiciones de items desde los sprites vivos a room_data_
|
||
auto* item_mgr = room_->getItemManager();
|
||
for (int i = 0; i < item_mgr->getCount() && i < static_cast<int>(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<int>(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, pintar en la posición actual del ratón
|
||
if (painting_ && brush_tile_ != NO_BRUSH) {
|
||
int tile_index = (mouse_tile_y_ * 32) + mouse_tile_x_;
|
||
if (editing_collision_) {
|
||
// Pintar en el mapa de colisiones
|
||
if (tile_index >= 0 && tile_index < static_cast<int>(room_data_.collision_tile_map.size())) {
|
||
if (room_data_.collision_tile_map[tile_index] != brush_tile_) {
|
||
room_data_.collision_tile_map[tile_index] = brush_tile_;
|
||
room_->setCollisionTile(tile_index, brush_tile_);
|
||
}
|
||
}
|
||
} else {
|
||
// Pintar en el mapa de dibujo
|
||
if (tile_index >= 0 && tile_index < static_cast<int>(room_data_.tile_map.size())) {
|
||
if (room_data_.tile_map[tile_index] != brush_tile_) {
|
||
room_data_.tile_map[tile_index] = brush_tile_;
|
||
room_->setTile(tile_index, brush_tile_);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Actualizar la barra de estado
|
||
updateStatusBarInfo();
|
||
if (statusbar_) {
|
||
statusbar_->update(delta_time);
|
||
}
|
||
}
|
||
|
||
// Renderiza el editor
|
||
void MapEditor::render() {
|
||
// El tilemap ya ha sido renderizado por Game::renderPlaying() antes de llamar aquí
|
||
|
||
// 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<int>(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<float>(tile * TILE_W),
|
||
.y = 0,
|
||
.w = static_cast<float>(TILE_W),
|
||
.h = static_cast<float>(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();
|
||
|
||
// 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<int>(event.key.repeat) == 0) {
|
||
mini_map_visible_ = false;
|
||
return;
|
||
}
|
||
mini_map_->handleEvent(event, room_path_);
|
||
return;
|
||
}
|
||
|
||
// ESC: desactivar brush
|
||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE && brush_tile_ != NO_BRUSH) {
|
||
brush_tile_ = NO_BRUSH;
|
||
return;
|
||
}
|
||
|
||
// E: toggle borrador
|
||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_E && static_cast<int>(event.key.repeat) == 0) {
|
||
brush_tile_ = (brush_tile_ == ERASER_BRUSH) ? NO_BRUSH : ERASER_BRUSH;
|
||
return;
|
||
}
|
||
|
||
// M: toggle mini mapa
|
||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_M && static_cast<int>(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<int>(event.key.repeat) == 0) {
|
||
setEditingCollision(!editing_collision_);
|
||
return;
|
||
}
|
||
|
||
// Cursores: navegar a habitación adyacente
|
||
if (event.type == SDL_EVENT_KEY_DOWN && static_cast<int>(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;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Click derecho: abrir TilePicker
|
||
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_RIGHT) {
|
||
// Deseleccionar entidades
|
||
selection_.clear();
|
||
|
||
if (editing_collision_) {
|
||
// Abrir tile picker del collision tileset
|
||
int tile_index = (mouse_tile_y_ * 32) + mouse_tile_x_;
|
||
int current = (tile_index >= 0 && tile_index < static_cast<int>(room_data_.collision_tile_map.size()))
|
||
? room_data_.collision_tile_map[tile_index]
|
||
: 0;
|
||
|
||
tile_picker_.on_select = [this](int tile) {
|
||
brush_tile_ = tile;
|
||
};
|
||
tile_picker_.open("collision.gif", current, 0);
|
||
} else {
|
||
// Abrir tile picker del mapa de dibujo
|
||
int tile_index = (mouse_tile_y_ * 32) + mouse_tile_x_;
|
||
int current = (tile_index >= 0 && tile_index < static_cast<int>(room_data_.tile_map.size()))
|
||
? room_data_.tile_map[tile_index]
|
||
: -1;
|
||
|
||
tile_picker_.on_select = [this](int tile) {
|
||
brush_tile_ = tile;
|
||
};
|
||
tile_picker_.open(room_->getTileSetFile(), current, 0);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Click izquierdo
|
||
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_LEFT) {
|
||
// Si hay brush activo y no hacemos hit en ninguna entidad → pintar
|
||
if (brush_tile_ != NO_BRUSH) {
|
||
// Comprobar si hay hit en alguna entidad primero
|
||
bool hit_entity = false;
|
||
SDL_FRect player_rect = player_->getRect();
|
||
if (pointInRect(mouse_game_x_, mouse_game_y_, player_rect)) { hit_entity = true; }
|
||
|
||
// 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) {
|
||
// Pintar tile y entrar en modo painting
|
||
painting_ = true;
|
||
int tile_index = (mouse_tile_y_ * 32) + mouse_tile_x_;
|
||
if (editing_collision_) {
|
||
if (tile_index >= 0 && tile_index < static_cast<int>(room_data_.collision_tile_map.size())) {
|
||
room_data_.collision_tile_map[tile_index] = brush_tile_;
|
||
room_->setCollisionTile(tile_index, brush_tile_);
|
||
}
|
||
} else {
|
||
if (tile_index >= 0 && tile_index < static_cast<int>(room_data_.tile_map.size())) {
|
||
room_data_.tile_map[tile_index] = brush_tile_;
|
||
room_->setTile(tile_index, brush_tile_);
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
handleMouseDown(mouse_game_x_, mouse_game_y_);
|
||
} else if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && event.button.button == SDL_BUTTON_LEFT) {
|
||
if (painting_) {
|
||
painting_ = false;
|
||
autosave();
|
||
} else {
|
||
handleMouseUp();
|
||
}
|
||
}
|
||
}
|
||
|
||
// Procesa click del ratón: hit test + inicio de drag
|
||
void MapEditor::handleMouseDown(float game_x, float game_y) {
|
||
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<float>(Tile::SIZE);
|
||
SDL_FRect b1_rect = {static_cast<float>(bd.x1), static_cast<float>(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<float>(bd.x2), static_cast<float>(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<int>(drag_.snap_x);
|
||
const int SNAP_Y = static_cast<int>(drag_.snap_y);
|
||
|
||
switch (drag_.target) {
|
||
case DragTarget::ENTITY_INITIAL:
|
||
switch (drag_.entity_type) {
|
||
case EntityType::ENEMY:
|
||
if (IDX >= 0 && IDX < static_cast<int>(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<int>(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<int>(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<int>(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<int>(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<int>(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<float>(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);
|
||
}
|
||
|
||
// Alinea un valor a la cuadrícula de 8x8
|
||
auto MapEditor::snapToGrid(float value) -> float {
|
||
return std::round(value / static_cast<float>(Tile::SIZE)) * static_cast<float>(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<float, float> {
|
||
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<int>(room_data_.enemies.size());
|
||
case EntityType::ITEM:
|
||
return static_cast<int>(room_data_.items.size());
|
||
case EntityType::PLATFORM:
|
||
return static_cast<int>(room_data_.platforms.size());
|
||
case EntityType::KEY:
|
||
return static_cast<int>(room_data_.keys.size());
|
||
case EntityType::DOOR:
|
||
return static_cast<int>(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<float>(bd.x1);
|
||
auto b1_y = static_cast<float>(bd.y1);
|
||
auto b2_x = static_cast<float>(bd.x2);
|
||
auto b2_y = static_cast<float>(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<int>(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<int>(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<float>(Tile::SIZE), .h = static_cast<float>(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<int>(mouse_game_x_) / Tile::SIZE;
|
||
mouse_tile_y_ = static_cast<int>(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<int>(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<int>(e.vx)) +
|
||
" vy:" + std::to_string(static_cast<int>(e.vy));
|
||
if (e.flip) { line3 += " flip"; }
|
||
if (e.mirror) { line3 += " mirror"; }
|
||
}
|
||
break;
|
||
|
||
case EntityType::ITEM:
|
||
if (selection_.index < static_cast<int>(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<int>(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<int>(p.speed)) + " " + (p.loop == LoopMode::CIRCULAR ? "circular" : "pingpong");
|
||
if (p.easing != "linear") { line3 += " " + p.easing; }
|
||
}
|
||
break;
|
||
|
||
case EntityType::KEY:
|
||
if (selection_.index < static_cast<int>(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<int>(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_tile_ == ERASER_BRUSH) {
|
||
line4 = "brush: eraser (e)";
|
||
} else if (brush_tile_ != NO_BRUSH) {
|
||
line4 = "brush: tile " + std::to_string(brush_tile_);
|
||
}
|
||
|
||
statusbar_->setLine2(line2);
|
||
statusbar_->setLine3(line3);
|
||
statusbar_->setLine4(line4);
|
||
statusbar_->setLine5(line5);
|
||
|
||
// Actualizar el prompt de la consola según la selección
|
||
if (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<std::string> {
|
||
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"};
|
||
}
|
||
}
|
||
|
||
// 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<int>(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<int>(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<int>(CENTER_X - ROUTE_HALF);
|
||
new_enemy.y1 = static_cast<int>(CENTER_Y);
|
||
new_enemy.x2 = static_cast<int>(CENTER_X + ROUTE_HALF);
|
||
new_enemy.y2 = static_cast<int>(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<int>(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<int>(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<int> 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<Room::Data>(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>(item);
|
||
autosave();
|
||
return "color1: (default)";
|
||
}
|
||
try {
|
||
item.color1 = static_cast<Uint8>(std::stoi(value));
|
||
} catch (...) { return "Invalid value: " + value; }
|
||
item.color1_overridden = true;
|
||
room_->getItemManager()->getItem(selection_.index) = std::make_shared<Item>(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>(item);
|
||
autosave();
|
||
return "color2: (default)";
|
||
}
|
||
try {
|
||
item.color2 = static_cast<Uint8>(std::stoi(value));
|
||
} catch (...) { return "Invalid value: " + value; }
|
||
item.color2_overridden = true;
|
||
room_->getItemManager()->getItem(selection_.index) = std::make_shared<Item>(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](int tile) {
|
||
if (!hasSelectedItem()) { return; }
|
||
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);
|
||
}
|
||
|
||
// 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<Item>(new_item));
|
||
|
||
int new_index = static_cast<int>(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>(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<Item>(copy));
|
||
|
||
int new_index = static_cast<int>(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<MovingPlatform>(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<int>(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<MovingPlatform>(new_platform));
|
||
|
||
int new_index = static_cast<int>(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<MovingPlatform>(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<MovingPlatform>(copy));
|
||
|
||
int new_index = static_cast<int>(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>(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>(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<float>(PlayArea::CENTER_X);
|
||
new_key.y = static_cast<float>(PlayArea::CENTER_Y);
|
||
|
||
room_data_.keys.push_back(new_key);
|
||
try {
|
||
room_->getKeyManager()->addKey(std::make_shared<Key>(new_key));
|
||
} catch (const std::exception& e) {
|
||
room_data_.keys.pop_back();
|
||
return std::string("Error: ") + e.what();
|
||
}
|
||
|
||
int new_index = static_cast<int>(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>(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<Key>(copy));
|
||
} catch (const std::exception& e) {
|
||
room_data_.keys.pop_back();
|
||
return std::string("Error: ") + e.what();
|
||
}
|
||
|
||
int new_index = static_cast<int>(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<Door>(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<float>((PlayArea::CENTER_X / Tile::SIZE) * Tile::SIZE);
|
||
new_door.y = static_cast<float>((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<Door>(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<int>(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<Door>(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<int>(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>& 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
|