Files
projecte_2026/source/game/editor/map_editor.cpp

2028 lines
75 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 "game/editor/editor_statusbar.hpp" // Para EditorStatusBar
#include "game/editor/room_saver.hpp" // Para RoomSaver
#include "game/entities/player.hpp" // Para Player
#include "game/game_control.hpp" // Para GameControl
#include "game/gameplay/enemy_manager.hpp" // Para EnemyManager
#include "game/gameplay/item_manager.hpp" // Para ItemManager
#include "game/gameplay/platform_manager.hpp" // Para PlatformManager
#include "game/gameplay/room.hpp" // Para Room
#include "game/options.hpp" // Para Options
#include "game/ui/console.hpp" // Para Console
#include "utils/defines.hpp" // Para Tile::SIZE, PlayArea
#include "utils/utils.hpp"
// 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 y cargar el YAML original (para edición parcial y backup)
file_path_ = Resource::List::get()->get(room_path_);
if (!file_path_.empty()) {
yaml_ = RoomSaver::loadYAML(file_path_);
yaml_backup_ = yaml_; // Copia profunda para revert
}
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 el YAML original y reescribir el fichero
yaml_ = yaml_backup_;
auto room_data_ptr = Resource::Cache::get()->getRoom(room_path_);
if (room_data_ptr) {
room_data_ = *room_data_ptr;
}
RoomSaver::saveYAML(file_path_, yaml_, room_data_);
// 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;
}
// Platforms are already synced via resetToInitialPosition during drag commit
RoomSaver::saveYAML(file_path_, yaml_, room_data_);
}
// Actualiza el editor
void MapEditor::update(float delta_time) {
// Mantener el ratón siempre visible
SDL_ShowCursor();
Mouse::last_mouse_move_time = SDL_GetTicks();
// Actualizar animaciones de enemigos e items (sin mover enemigos)
room_->updateEditorMode(delta_time);
// Actualizar posición del ratón
updateMousePosition();
// Si estamos arrastrando, actualizar la posición snapped
if (drag_.target != DragTarget::NONE) {
updateDrag();
}
// Si estamos pintando tiles, pintar en la posición actual del ratón
if (painting_ && brush_tile_ != NO_BRUSH) {
int tile_index = (mouse_tile_y_ * 32) + mouse_tile_x_;
if (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, jugador
room_->renderEnemies();
room_->renderItems();
room_->renderPlatforms();
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
for (auto type : {EntityType::ENEMY, EntityType::PLATFORM, 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;
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;
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();
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();
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};
}
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());
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";
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::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";
}
line2 = "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"};
default: return {"ITEMCOLOR1", "ITEMCOLOR2", "CONVEYOR", "TILESET", "UP", "DOWN", "LEFT", "RIGHT"};
}
}
// Modifica una propiedad del enemigo seleccionado
auto MapEditor::setEnemyProperty(const std::string& property, const std::string& value) -> std::string { // 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 == "TILESET") {
std::string tileset = val;
if (tileset.find('.') == std::string::npos) { tileset += ".gif"; }
room_data_.tile_set_file = tileset;
room_->setTileSet(tileset);
autosave();
return "tileset: " + tileset;
}
// Conexiones: UP, DOWN, LEFT, RIGHT
if (property == "UP" || property == "DOWN" || property == "LEFT" || property == "RIGHT") {
std::string connection = "0";
if (val != "0" && val != "null" && val != "none") {
try {
int num = std::stoi(val);
char buf[16];
std::snprintf(buf, sizeof(buf), "%02d.yaml", num);
connection = buf;
} catch (...) {
connection = val;
if (connection.find('.') == std::string::npos) { connection += ".yaml"; }
}
}
// Dirección opuesta para la conexión recíproca
std::string opposite;
std::string* our_field = nullptr;
if (property == "UP") {
opposite = "lower_room";
our_field = &room_data_.upper_room;
}
if (property == "DOWN") {
opposite = "upper_room";
our_field = &room_data_.lower_room;
}
if (property == "LEFT") {
opposite = "right_room";
our_field = &room_data_.left_room;
}
if (property == "RIGHT") {
opposite = "left_room";
our_field = &room_data_.right_room;
}
// Si había una conexión anterior, romper la recíproca de la otra room
if (our_field != nullptr && *our_field != "0" && !our_field->empty()) {
auto old_other = Resource::Cache::get()->getRoom(*our_field);
if (old_other) {
if (opposite == "upper_room") {
old_other->upper_room = "0";
} else if (opposite == "lower_room") {
old_other->lower_room = "0";
} else if (opposite == "left_room") {
old_other->left_room = "0";
} else if (opposite == "right_room") {
old_other->right_room = "0";
}
// Guardar la otra room
std::string other_path = Resource::List::get()->get(*our_field);
if (!other_path.empty()) {
auto other_yaml = RoomSaver::loadYAML(other_path);
RoomSaver::saveYAML(other_path, other_yaml, *old_other);
}
}
}
// Aplicar la nueva conexión
if (our_field != nullptr) { *our_field = connection; }
// 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()) {
auto other_yaml = RoomSaver::loadYAML(other_path);
RoomSaver::saveYAML(other_path, other_yaml, *other);
}
}
}
autosave();
return toLower(property) + ": " + connection;
}
return "Unknown property: " + property + " (use: itemcolor1, itemcolor2, conveyor, tileset, 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;
// Crear Room::Data por defecto con conexión recíproca
Room::Data new_room;
new_room.number = std::string(name_buf).substr(0, std::string(name_buf).find('.'));
new_room.tile_set_file = "standard.gif";
new_room.item_color1 = 11;
new_room.item_color2 = 12;
new_room.upper_room = "0";
new_room.lower_room = "0";
new_room.left_room = "0";
new_room.right_room = "0";
new_room.conveyor_belt_direction = 0;
new_room.tile_map.resize(Map::WIDTH * Map::HEIGHT, -1);
new_room.collision_tile_map.resize(Map::WIDTH * Map::HEIGHT, 0);
// Conexión recíproca: la nueva room conecta de vuelta a la actual
if (direction == "UP") {
new_room.lower_room = room_path_;
} else if (direction == "DOWN") {
new_room.upper_room = room_path_;
} else if (direction == "LEFT") {
new_room.right_room = room_path_;
} else if (direction == "RIGHT") {
new_room.left_room = room_path_;
}
// Conexiones del YAML
auto conn_str = [](const std::string& c) -> std::string { return (c == "0") ? "null" : c; };
// Crear el YAML
std::ofstream file(new_path);
if (!file.is_open()) { return "Error: cannot create " + new_path; }
file << "# NO_NAME\n";
file << "room:\n";
file << " name_en: \"NO_NAME\"\n";
file << " name_ca: \"NO_NAME\"\n";
file << " tileSetFile: standard.gif\n";
file << "\n";
file << " connections:\n";
file << " up: " << conn_str(new_room.upper_room) << "\n";
file << " down: " << conn_str(new_room.lower_room) << "\n";
file << " left: " << conn_str(new_room.left_room) << "\n";
file << " right: " << conn_str(new_room.right_room) << "\n";
file << "\n";
file << " itemColor1: bright_cyan\n";
file << " itemColor2: yellow\n";
file << "\n";
file << " conveyorBelt: none\n";
file << "\n";
file << "tilemap:\n";
for (int row = 0; row < Map::HEIGHT; ++row) {
file << " - [";
for (int col = 0; col < Map::WIDTH; ++col) {
file << "-1";
if (col < Map::WIDTH - 1) { file << ", "; }
}
file << "]\n";
}
file.close();
// 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)
if (direction == "UP") {
room_data_.upper_room = new_name;
} else if (direction == "DOWN") {
room_data_.lower_room = new_name;
} else if (direction == "LEFT") {
room_data_.left_room = new_name;
} else if (direction == "RIGHT") {
room_data_.right_room = new_name;
}
if (!direction.empty()) { autosave(); }
// Navegar a la nueva room
reenter_ = true;
if (GameControl::exit_editor) { GameControl::exit_editor(); }
if (GameControl::change_room && GameControl::change_room(new_name)) {
if (GameControl::enter_editor) { GameControl::enter_editor(); }
}
return "Created room " + new_name + (direction.empty() ? "" : " (" + toLower(direction) + ")");
}
// Elimina la habitación actual
auto MapEditor::deleteRoom() -> std::string { // 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()) {
auto other_yaml = RoomSaver::loadYAML(other_path);
RoomSaver::saveYAML(other_path, other_yaml, *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);
}
// 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