Files
projecte_2026/source/game/editor/map_editor.cpp
Sergio Valor 8ebf7894f2 - afegides claus i portes al editor
- fix: crear una nova habitació no modificava la memoria, soles els fitxers
2026-04-10 18:34:04 +02:00

2427 lines
92 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 "external/fkyaml_node.hpp" // Para fkyaml::node (loadSettings)
#include "core/input/mouse.hpp" // Para Mouse
#include "core/rendering/render_info.hpp" // Para RenderInfo
#include "core/rendering/screen.hpp" // Para Screen
#include "core/rendering/surface.hpp" // Para Surface
#include "core/resources/resource_cache.hpp" // Para Resource::Cache
#include "core/resources/resource_list.hpp" // Para Resource::List
#include "core/resources/resource_types.hpp" // Para RoomResource
#include "game/editor/editor_statusbar.hpp" // Para EditorStatusBar
#include "game/gameplay/room_format.hpp" // Para RoomFormat
#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/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::Data item_copy = i;
item_copy.color1 = room_data_.item_color1;
item_copy.color2 = room_data_.item_color2;
item_mgr->addItem(std::make_shared<Item>(item_copy));
}
auto* platform_mgr = room_->getPlatformManager();
platform_mgr->clear();
for (const auto& p : room_data_.platforms) {
platform_mgr->addPlatform(std::make_shared<MovingPlatform>(p));
}
room_->setItemColors(room_data_.item_color1, room_data_.item_color2);
// 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);
line3 = "tileset: " + it.tile_set_file;
}
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 conv = "none";
if (room_data_.conveyor_belt_direction < 0) {
conv = "left";
} else if (room_data_.conveyor_belt_direction > 0) {
conv = "right";
}
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 + " conv:" + conv;
line3 = "u:" + conn(room_data_.upper_room) + " d:" + conn(room_data_.lower_room) +
" l:" + conn(room_data_.left_room) + " r:" + conn(room_data_.right_room) +
" itm:" + std::to_string(room_data_.item_color1) + "/" + std::to_string(room_data_.item_color2);
break;
}
}
// 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"};
case EntityType::PLATFORM:
return {"ANIMATION", "SPEED", "LOOP", "EASING"};
case EntityType::KEY:
return {"ID", "ANIMATION"};
case EntityType::DOOR:
return {"ID", "ANIMATION"};
default:
return {"ZONE", "ITEMCOLOR1", "ITEMCOLOR2", "CONVEYOR", "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 == "ITEMCOLOR1") {
auto color = static_cast<Uint8>(safeStoi(val, 0));
room_data_.item_color1 = color;
room_->setItemColors(room_data_.item_color1, room_data_.item_color2);
autosave();
return "itemcolor1: " + std::to_string(color);
}
if (property == "ITEMCOLOR2") {
auto color = static_cast<Uint8>(safeStoi(val, 0));
room_data_.item_color2 = color;
room_->setItemColors(room_data_.item_color1, room_data_.item_color2);
autosave();
return "itemcolor2: " + std::to_string(color);
}
if (property == "CONVEYOR") {
if (val == "left") {
room_data_.conveyor_belt_direction = -1;
} else if (val == "right") {
room_data_.conveyor_belt_direction = 1;
} else {
room_data_.conveyor_belt_direction = 0;
val = "none";
}
room_->setConveyorBeltDirection(room_data_.conveyor_belt_direction);
autosave();
return "conveyor: " + val;
}
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);
}
return "Unknown property: " + property + " (use: tile, counter)";
}
// Abre el tile picker para seleccionar un tile
void MapEditor::openTilePicker(const std::string& tileset_name, int current_tile) {
// Cerrar la consola si está abierta (para que el primer click vaya al picker)
if (Console::get()->isActive()) {
Console::get()->toggle();
}
tile_picker_.on_select = [this](int tile) {
if (!hasSelectedItem()) { return; }
room_data_.items[selection_.index].tile = tile;
room_->getItemManager()->getItem(selection_.index)->setTile(tile);
autosave();
};
// Pasar color de fondo de la habitación + color de sustitución del item
tile_picker_.open(tileset_name, current_tile, room_data_.bg_color, 1, room_data_.item_color1);
}
// Crea un nuevo item con valores por defecto, centrado en la habitación
auto MapEditor::addItem() -> std::string {
if (!active_) { return "Editor not active"; }
Item::Data new_item;
new_item.tile_set_file = "items.gif";
new_item.tile = 42; // Tile por defecto
new_item.x = PlayArea::CENTER_X;
new_item.y = PlayArea::CENTER_Y;
new_item.counter = 0;
new_item.color1 = room_data_.item_color1;
new_item.color2 = room_data_.item_color2;
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