From fadc3b03c9f3963d8bf822c8f753555e64117d8a Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 10 Apr 2026 21:33:57 +0200 Subject: [PATCH] =?UTF-8?q?map=5Feditor:=20-=20tilepicker=20s'obri=20ara?= =?UTF-8?q?=20amb=20la=20T=20-=20brush=20de=20varios=20tiles=20-=20eyedrop?= =?UTF-8?q?per=20amb=20bot=C3=B3=20dret=20(de=20uno=20o=20varios=20tiles)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/room/03.yaml | 8 +- source/game/editor/map_editor.cpp | 299 ++++++++++++++++++++++------- source/game/editor/map_editor.hpp | 52 ++++- source/game/editor/tile_picker.cpp | 77 +++++++- source/game/editor/tile_picker.hpp | 15 +- 5 files changed, 363 insertions(+), 88 deletions(-) diff --git a/data/room/03.yaml b/data/room/03.yaml index b5f6438..185aee7 100644 --- a/data/room/03.yaml +++ b/data/room/03.yaml @@ -30,8 +30,8 @@ tilemap: - [49, 50, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 380, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169] - [-1, -1, -1, -1, -1, -1, -1, 169, 169, 169, 169, 169, 169, 169, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169] - [-1, -1, -1, -1, -1, -1, -1, -1, 168, 168, 168, 168, 168, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169] - - [-1, -1, -1, -1, -1, -1, -1, -1, 168, 134, 168, 134, 168, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169, 169, 169, -1, -1, -1, -1, -1, 169, 169] - - [-1, -1, -1, -1, -1, -1, -1, -1, 168, 168, 168, 168, 168, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169, 169, 169, -1, -1, -1, -1, -1, 169, 169] + - [-1, -1, -1, -1, -1, -1, -1, -1, 168, 134, 168, 134, 168, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169] + - [-1, -1, -1, -1, -1, -1, -1, -1, 168, 168, 168, 168, 168, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169] - [169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169] # Mapa de colisiones (0 = vacio, 1 = solido) collision: @@ -53,8 +53,8 @@ tilemap: - [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] - [0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] - - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1] - - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] # Enemigos en esta habitación diff --git a/source/game/editor/map_editor.cpp b/source/game/editor/map_editor.cpp index 4d14250..c5a8c19 100644 --- a/source/game/editor/map_editor.cpp +++ b/source/game/editor/map_editor.cpp @@ -131,7 +131,7 @@ auto MapEditor::showGrid(bool show) -> std::string { auto MapEditor::setEditingCollision(bool collision) -> std::string { editing_collision_ = collision; - brush_tile_ = NO_BRUSH; // Resetear brush al cambiar de modo + brush_.clear(); // Resetear brush al cambiar de modo return editing_collision_ ? "Editing: collision" : "Editing: draw"; } @@ -230,8 +230,9 @@ void MapEditor::enter(std::shared_ptr room, std::shared_ptr player // Resetear estado (preservar modo de edición en re-enter) drag_ = {}; selection_.clear(); + eyedropper_ = {}; if (!is_reenter) { - brush_tile_ = NO_BRUSH; + brush_.clear(); painting_ = false; editing_collision_ = false; } @@ -311,7 +312,7 @@ auto MapEditor::revert() -> std::string { } selection_.clear(); - brush_tile_ = NO_BRUSH; + brush_.clear(); return "Reverted to original"; } @@ -359,26 +360,9 @@ void MapEditor::update(float delta_time) { 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(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(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_); - } - } - } + // Si estamos pintando tiles, estampar el patrón en la posición del ratón + if (painting_ && !brush_.isEmpty()) { + stampBrushAt(mouse_tile_x_, mouse_tile_y_); } // Actualizar la barra de estado @@ -434,6 +418,16 @@ void MapEditor::render() { // Renderizar highlight de selección (encima de los sprites) renderSelectionHighlight(); + // Preview del brush bajo el cursor o rect del eyedropper en progreso + // (solo cuando no hay overlays a pantalla completa abiertos) + if (!tile_picker_.isOpen() && !mini_map_visible_) { + if (eyedropper_.active) { + renderEyedropperRect(); + } else { + renderBrushPreview(); + } + } + // Tile picker o mini mapa (encima de todo en el play area) if (tile_picker_.isOpen()) { tile_picker_.render(); @@ -467,15 +461,25 @@ void MapEditor::handleEvent(const SDL_Event& event) { // NOLINT(readability-fun return; } - // ESC: desactivar brush - if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE && brush_tile_ != NO_BRUSH) { - brush_tile_ = NO_BRUSH; + // ESC: cancelar eyedropper en progreso (sin tocar el brush previo) + if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE && eyedropper_.active) { + eyedropper_.active = false; return; } - // E: toggle borrador + // ESC: desactivar brush + if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE && !brush_.isEmpty()) { + brush_.clear(); + return; + } + + // E: toggle borrador (alterna entre brush vacío y brush 1x1 ERASE) if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_E && static_cast(event.key.repeat) == 0) { - brush_tile_ = (brush_tile_ == ERASER_BRUSH) ? NO_BRUSH : ERASER_BRUSH; + if (brush_.width == 1 && brush_.height == 1 && !brush_.tiles.empty() && brush_.tiles[0] == BrushPattern::ERASE) { + brush_.clear(); + } else { + brush_.setSingle(BrushPattern::ERASE); + } return; } @@ -524,41 +528,54 @@ void MapEditor::handleEvent(const SDL_Event& event) { // NOLINT(readability-fun } } - // Click derecho: abrir TilePicker - if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_RIGHT) { + // T: abrir TilePicker + if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_T && static_cast(event.key.repeat) == 0) { // Deseleccionar entidades selection_.clear(); + const std::string tileset_name = editing_collision_ ? "collision.gif" : room_->getTileSetFile(); + int tile_index = (mouse_tile_y_ * 32) + mouse_tile_x_; + int current = 0; if (editing_collision_) { - // 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(room_data_.collision_tile_map.size())) + current = (tile_index >= 0 && tile_index < static_cast(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(room_data_.tile_map.size())) + current = (tile_index >= 0 && tile_index < static_cast(room_data_.tile_map.size())) ? room_data_.tile_map[tile_index] : -1; - - tile_picker_.on_select = [this](int tile) { - brush_tile_ = tile; - }; - tile_picker_.open(room_->getTileSetFile(), current, 0); } + + tile_picker_.on_select = [this, tileset_name](int col, int row, int width, int height) { + brush_ = buildPatternFromTileset(tileset_name, col, row, width, height); + }; + tile_picker_.open(tileset_name, current, 0, -1, -1, 0, 1, true); + return; + } + + // Click derecho: eyedropper (sample del nivel). Drag = rect, click = single tile. + if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_RIGHT) { + // Cancelar painting en curso si lo hubiese + if (painting_) { + painting_ = false; + autosave(); + } + selection_.clear(); + eyedropper_.active = true; + eyedropper_.start_tile_x = mouse_tile_x_; + eyedropper_.start_tile_y = mouse_tile_y_; + return; + } + if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && event.button.button == SDL_BUTTON_RIGHT && eyedropper_.active) { + commitEyedropper(); + eyedropper_.active = false; return; } // Click izquierdo if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_LEFT) { // Si hay brush activo y no hacemos hit en ninguna entidad → pintar - if (brush_tile_ != NO_BRUSH) { + if (!brush_.isEmpty()) { // Comprobar si hay hit en alguna entidad primero bool hit_entity = false; SDL_FRect player_rect = player_->getRect(); @@ -572,20 +589,9 @@ void MapEditor::handleEvent(const SDL_Event& event) { // NOLINT(readability-fun } if (!hit_entity) { - // Pintar tile y entrar en modo painting + // Estampar el patrón 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(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(room_data_.tile_map.size())) { - room_data_.tile_map[tile_index] = brush_tile_; - room_->setTile(tile_index, brush_tile_); - } - } + stampBrushAt(mouse_tile_x_, mouse_tile_y_); return; } } @@ -951,6 +957,158 @@ void MapEditor::renderSelectionHighlight() { game_surface->drawRectBorder(&border, DRAG_COLOR); } +// Estampa el patrón del brush en la posición indicada (anclaje top-left) +void MapEditor::stampBrushAt(int tile_x, int tile_y) { + if (brush_.isEmpty()) { return; } + for (int dy = 0; dy < brush_.height; ++dy) { + for (int dx = 0; dx < brush_.width; ++dx) { + int value = brush_.at(dx, dy); + if (value == BrushPattern::TRANSPARENT) { continue; } // skip — no tocar destino + int tx = tile_x + dx; + int ty = tile_y + dy; + if (tx < 0 || tx >= Map::WIDTH || ty < 0 || ty >= Map::HEIGHT) { continue; } + int idx = (ty * Map::WIDTH) + tx; + // value es >= 0 (tile real) o BrushPattern::ERASE (escribe -1) + if (editing_collision_) { + if (idx >= 0 && idx < static_cast(room_data_.collision_tile_map.size())) { + if (room_data_.collision_tile_map[idx] != value) { + room_data_.collision_tile_map[idx] = value; + room_->setCollisionTile(idx, value); + } + } + } else { + if (idx >= 0 && idx < static_cast(room_data_.tile_map.size())) { + if (room_data_.tile_map[idx] != value) { + room_data_.tile_map[idx] = value; + room_->setTile(idx, value); + } + } + } + } + } +} + +// Confirma el eyedropper: muestrea el rect del nivel y lo carga en el brush +void MapEditor::commitEyedropper() { + int x1 = std::clamp(eyedropper_.start_tile_x, 0, Map::WIDTH - 1); + int y1 = std::clamp(eyedropper_.start_tile_y, 0, Map::HEIGHT - 1); + int x2 = std::clamp(mouse_tile_x_, 0, Map::WIDTH - 1); + int y2 = std::clamp(mouse_tile_y_, 0, Map::HEIGHT - 1); + int xmin = std::min(x1, x2); + int ymin = std::min(y1, y2); + int xmax = std::max(x1, x2); + int ymax = std::max(y1, y2); + brush_ = sampleBrush(xmin, ymin, xmax, ymax); +} + +// Lee del tilemap activo (draw o collision) un rect y devuelve un BrushPattern. +// Las celdas vacías (-1) en el tilemap origen se mapean a BrushPattern::TRANSPARENT +// para que reestampar el brush no borre contenido del destino. +auto MapEditor::sampleBrush(int x1, int y1, int x2, int y2) const -> BrushPattern { + BrushPattern p; + p.width = (x2 - x1) + 1; + p.height = (y2 - y1) + 1; + p.tiles.reserve(static_cast(p.width * p.height)); + const auto& src = editing_collision_ ? room_data_.collision_tile_map : room_data_.tile_map; + for (int y = y1; y <= y2; ++y) { + for (int x = x1; x <= x2; ++x) { + int idx = (y * Map::WIDTH) + x; + int value = (idx >= 0 && idx < static_cast(src.size())) ? src[idx] : -1; + // -1 en el tilemap = vacío → TRANSPARENT (no borra al estampar) + p.tiles.push_back((value == -1) ? BrushPattern::TRANSPARENT : value); + } + } + return p; +} + +// Construye un BrushPattern leyendo tiles consecutivos de un tileset. +// Usado por el TilePicker cuando se hace selección rectangular. +auto MapEditor::buildPatternFromTileset(const std::string& tileset_name, int col, int row, int width, int height) const -> BrushPattern { + BrushPattern p; + auto surface = Resource::Cache::get()->getSurface(tileset_name); + if (!surface || width <= 0 || height <= 0) { return p; } + int cols = static_cast(surface->getWidth()) / Tile::SIZE; + if (cols <= 0) { return p; } + p.width = width; + p.height = height; + p.tiles.reserve(static_cast(width * height)); + for (int dy = 0; dy < height; ++dy) { + for (int dx = 0; dx < width; ++dx) { + int tile = ((row + dy) * cols) + (col + dx); + p.tiles.push_back(tile); + } + } + return p; +} + +// Renderiza la previsualización del brush bajo el cursor (snapped al grid) +void MapEditor::renderBrushPreview() { + if (brush_.isEmpty()) { return; } + auto game_surface = Screen::get()->getRendererSurface(); + if (!game_surface) { return; } + + const std::string tileset_name = editing_collision_ ? std::string("collision.gif") : room_->getTileSetFile(); + auto tileset = Resource::Cache::get()->getSurface(tileset_name); + int cols = (tileset) ? (static_cast(tileset->getWidth()) / Tile::SIZE) : 0; + + constexpr auto TS = static_cast(Tile::SIZE); + for (int dy = 0; dy < brush_.height; ++dy) { + for (int dx = 0; dx < brush_.width; ++dx) { + int value = brush_.at(dx, dy); + if (value == BrushPattern::TRANSPARENT) { continue; } + int tx = mouse_tile_x_ + dx; + int ty = mouse_tile_y_ + dy; + if (tx < 0 || tx >= Map::WIDTH || ty < 0 || ty >= Map::HEIGHT) { continue; } + float dst_x = static_cast(tx) * TS; + float dst_y = static_cast(ty) * TS; + if (value == BrushPattern::ERASE) { + SDL_FRect erase_cell = {.x = dst_x, .y = dst_y, .w = TS, .h = TS}; + game_surface->fillRect(&erase_cell, static_cast(room_data_.bg_color)); + } else if (tileset && cols > 0) { + SDL_FRect src = { + .x = static_cast(value % cols) * TS, + .y = static_cast(value / cols) * TS, + .w = TS, + .h = TS}; + SDL_FRect dst = {.x = dst_x, .y = dst_y, .w = TS, .h = TS}; + tileset->render(&src, &dst); + } + } + } + + // Border alrededor del rectángulo completo del brush (blanco) + float bx = static_cast(mouse_tile_x_) * TS; + float by = static_cast(mouse_tile_y_) * TS; + SDL_FRect border = { + .x = bx - 1.0F, + .y = by - 1.0F, + .w = (static_cast(brush_.width) * TS) + 2.0F, + .h = (static_cast(brush_.height) * TS) + 2.0F}; + game_surface->drawRectBorder(&border, 15); +} + +// Renderiza el rectángulo del eyedropper en progreso (cyan brillante) +void MapEditor::renderEyedropperRect() { + auto game_surface = Screen::get()->getRendererSurface(); + if (!game_surface) { return; } + int x1 = std::clamp(eyedropper_.start_tile_x, 0, Map::WIDTH - 1); + int y1 = std::clamp(eyedropper_.start_tile_y, 0, Map::HEIGHT - 1); + int x2 = std::clamp(mouse_tile_x_, 0, Map::WIDTH - 1); + int y2 = std::clamp(mouse_tile_y_, 0, Map::HEIGHT - 1); + int xmin = std::min(x1, x2); + int ymin = std::min(y1, y2); + int xmax = std::max(x1, x2); + int ymax = std::max(y1, y2); + constexpr auto TS = static_cast(Tile::SIZE); + SDL_FRect rect = { + .x = static_cast(xmin) * TS, + .y = static_cast(ymin) * TS, + .w = static_cast((xmax - xmin) + 1) * TS, + .h = static_cast((ymax - ymin) + 1) * TS}; + constexpr Uint8 EYEDROPPER_COLOR = 21; // BRIGHT_CYAN + game_surface->drawRectBorder(&rect, EYEDROPPER_COLOR); +} + // Alinea un valor a la cuadrícula de 8x8 auto MapEditor::snapToGrid(float value) -> float { return std::round(value / static_cast(Tile::SIZE)) * static_cast(Tile::SIZE); @@ -1315,10 +1473,13 @@ void MapEditor::updateStatusBarInfo() { // NOLINT(readability-function-cognitiv // 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_); + if (!brush_.isEmpty()) { + if (brush_.width == 1 && brush_.height == 1) { + int v = brush_.tiles[0]; + line4 = (v == BrushPattern::ERASE) ? "brush: eraser (e)" : ("brush: tile " + std::to_string(v)); + } else { + line4 = "brush: " + std::to_string(brush_.width) + "x" + std::to_string(brush_.height); + } } statusbar_->setLine2(line2); @@ -1989,8 +2150,14 @@ void MapEditor::openTilePicker(const std::string& tileset_name, int current_tile Console::get()->toggle(); } - tile_picker_.on_select = [this](int tile) { + tile_picker_.on_select = [this, tileset_name](int col, int row, int /*width*/, int /*height*/) { if (!hasSelectedItem()) { return; } + // Items solo usan un tile: tomamos siempre la celda superior-izquierda del rect + auto surface = Resource::Cache::get()->getSurface(tileset_name); + if (!surface) { return; } + int cols = static_cast(surface->getWidth()) / Tile::SIZE; + if (cols <= 0) { return; } + int tile = (row * cols) + col; room_data_.items[selection_.index].tile = tile; room_->getItemManager()->getItem(selection_.index)->setTile(tile); autosave(); @@ -1999,7 +2166,7 @@ void MapEditor::openTilePicker(const std::string& tileset_name, int current_tile Uint8 preview_color = hasSelectedItem() ? room_data_.items[selection_.index].color1 : Item::DEFAULT_COLOR1; - tile_picker_.open(tileset_name, current_tile, room_data_.bg_color, 1, preview_color); + tile_picker_.open(tileset_name, current_tile, room_data_.bg_color, 1, preview_color, 0, 1, true); } // Crea un nuevo item con valores por defecto, centrado en la habitación diff --git a/source/game/editor/map_editor.hpp b/source/game/editor/map_editor.hpp index 8cdaef7..525d05b 100644 --- a/source/game/editor/map_editor.hpp +++ b/source/game/editor/map_editor.hpp @@ -6,6 +6,7 @@ #include // Para shared_ptr, unique_ptr #include // Para string +#include // Para vector #include "game/editor/mini_map.hpp" // Para MiniMap #include "game/editor/tile_picker.hpp" // Para TilePicker @@ -41,6 +42,40 @@ struct Selection { [[nodiscard]] auto is(EntityType t) const -> bool { return type == t && index >= 0; } }; +// Brush rectangular para pintar tiles. Una sola estructura cubre: +// - brush vacío (width=0, height=0) +// - single tile (width=1, height=1, tiles={N}) +// - borrador explícito (width=1, height=1, tiles={ERASE}) +// - patrón rectangular sampleado del nivel o del tileset +struct BrushPattern { + static constexpr int TRANSPARENT = -2; // celda no se escribe al estampar + static constexpr int ERASE = -1; // celda escribe -1 (vacía el destino) + + int width{0}; + int height{0}; + std::vector tiles; // size = width*height, row-major + + [[nodiscard]] auto isEmpty() const -> bool { return width <= 0 || height <= 0; } + [[nodiscard]] auto at(int dx, int dy) const -> int { return tiles[(dy * width) + dx]; } + void clear() { + width = 0; + height = 0; + tiles.clear(); + } + void setSingle(int tile) { + width = 1; + height = 1; + tiles = {tile}; + } +}; + +// Estado del eyedropper (clic derecho con drag para samplear el nivel) +struct EyedropperState { + bool active{false}; + int start_tile_x{0}; + int start_tile_y{0}; +}; + class MapEditor { public: static void init(); // [SINGLETON] Crea el objeto @@ -152,9 +187,15 @@ class MapEditor { void renderEntityBoundaries(); static void renderBoundaryMarker(float x, float y, Uint8 color); void renderSelectionHighlight(); + void renderBrushPreview(); + void renderEyedropperRect(); void renderGrid() const; void handleMouseDown(float game_x, float game_y); void handleMouseUp(); + void stampBrushAt(int tile_x, int tile_y); + void commitEyedropper(); + [[nodiscard]] auto sampleBrush(int x1, int y1, int x2, int y2) const -> BrushPattern; + [[nodiscard]] auto buildPatternFromTileset(const std::string& tileset_name, int col, int row, int width, int height) const -> BrushPattern; // Reconstruye todas las puertas vivas desde room_data_, limpiando primero // los WALLs antiguos del CollisionMap. Lo usa setDoorProperty cuando un @@ -184,12 +225,11 @@ class MapEditor { // Estado del editor bool active_{false}; DragState drag_; - Selection selection_; // Entidad seleccionada (unificada: enemy, item o platform) - static constexpr int NO_BRUSH = -2; // Sin brush activo - static constexpr int ERASER_BRUSH = -1; // Brush borrador (pinta tile vacío = -1) - int brush_tile_{NO_BRUSH}; // Tile activo para pintar - bool painting_{false}; // true mientras se está pintando con click izquierdo mantenido - bool editing_collision_{false}; // true = editando collision tilemap, false = editando draw tilemap + Selection selection_; // Entidad seleccionada (unificada: enemy, item o platform) + BrushPattern brush_; // Brush activo (vacío = sin brush) + EyedropperState eyedropper_; // Estado del eyedropper (clic derecho) + bool painting_{false}; // true mientras se está pintando con click izquierdo mantenido + bool editing_collision_{false}; // true = editando collision tilemap, false = editando draw tilemap // Datos de la habitación Room::Data room_data_; diff --git a/source/game/editor/tile_picker.cpp b/source/game/editor/tile_picker.cpp index 9ff740c..fe7707d 100644 --- a/source/game/editor/tile_picker.cpp +++ b/source/game/editor/tile_picker.cpp @@ -14,7 +14,7 @@ static constexpr int BORDER_PAD = 3; // Abre el picker con un tileset -void TilePicker::open(const std::string& tileset_name, int current_tile, int bg_color, int source_color, int target_color, int tile_spacing_in, int tile_spacing_out) { +void TilePicker::open(const std::string& tileset_name, int current_tile, int bg_color, int source_color, int target_color, int tile_spacing_in, int tile_spacing_out, bool allow_rect_selection) { tileset_ = Resource::Cache::get()->getSurface(tileset_name); if (!tileset_) { open_ = false; @@ -23,6 +23,10 @@ void TilePicker::open(const std::string& tileset_name, int current_tile, int bg_ spacing_in_ = tile_spacing_in; spacing_out_ = tile_spacing_out; + allow_rect_ = allow_rect_selection; + selecting_rect_ = false; + rect_start_tile_ = -1; + last_valid_hover_ = -1; // Calcular dimensiones del tileset en tiles (teniendo en cuenta spacing de entrada) int src_cell = Tile::SIZE + spacing_in_; @@ -145,13 +149,37 @@ void TilePicker::render() { float tileset_screen_y = frame_dst_.y + BORDER_PAD - static_cast(scroll_y_); constexpr auto TS = static_cast(Tile::SIZE); - // Highlight del tile bajo el cursor (blanco) - if (hover_tile_ >= 0) { + // Si hay un drag rect activo, dibujar el rectángulo en progreso (blanco) + // y omitir el highlight de hover individual. + if (selecting_rect_ && rect_start_tile_ >= 0) { + int end_tile = (hover_tile_ >= 0) ? hover_tile_ : last_valid_hover_; + if (end_tile >= 0) { + int c1 = rect_start_tile_ % tileset_width_; + int r1 = rect_start_tile_ / tileset_width_; + int c2 = end_tile % tileset_width_; + int r2 = end_tile / tileset_width_; + int col_min = std::min(c1, c2); + int col_max = std::max(c1, c2); + int row_min = std::min(r1, r2); + int row_max = std::max(r1, r2); + int cells_w = col_max - col_min + 1; + int cells_h = row_max - row_min + 1; + float rx = tileset_screen_x + static_cast(col_min * out_cell); + float ry = tileset_screen_y + static_cast(row_min * out_cell); + float rw = static_cast((cells_w * out_cell) - spacing_out_); + float rh = static_cast((cells_h * out_cell) - spacing_out_); + if (ry + rh > 0 && ry < static_cast(visible_height_)) { + SDL_FRect rect_box = {.x = rx, .y = ry, .w = rw, .h = rh}; + game_surface->drawRectBorder(&rect_box, 15); + } + } + } else if (hover_tile_ >= 0) { + // Highlight del tile bajo el cursor (blanco) int col = hover_tile_ % tileset_width_; int row = hover_tile_ / tileset_width_; float hx = tileset_screen_x + static_cast(col * out_cell); float hy = tileset_screen_y + static_cast(row * out_cell); - if (hy >= 0 && hy + TS <= visible_height_) { + if (hy >= 0 && hy + TS <= static_cast(visible_height_)) { SDL_FRect highlight = {.x = hx, .y = hy, .w = TS, .h = TS}; game_surface->drawRectBorder(&highlight, 15); } @@ -163,7 +191,7 @@ void TilePicker::render() { int row = current_tile_ / tileset_width_; float cx = tileset_screen_x + static_cast(col * out_cell); float cy = tileset_screen_y + static_cast(row * out_cell); - if (cy >= 0 && cy + TS <= visible_height_) { + if (cy >= 0 && cy + TS <= static_cast(visible_height_)) { SDL_FRect cur_rect = {.x = cx, .y = cy, .w = TS, .h = TS}; game_surface->drawRectBorder(&cur_rect, 9); } @@ -179,14 +207,44 @@ void TilePicker::handleEvent(const SDL_Event& event) { } if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { - if (event.button.button == SDL_BUTTON_LEFT && hover_tile_ >= 0) { - if (on_select) { on_select(hover_tile_); } - close(); + if (event.button.button == SDL_BUTTON_LEFT) { + if (allow_rect_ && hover_tile_ >= 0) { + // Iniciar selección rectangular: el commit se hace al soltar + selecting_rect_ = true; + rect_start_tile_ = hover_tile_; + last_valid_hover_ = hover_tile_; + } else if (!allow_rect_ && hover_tile_ >= 0) { + // Modo single-tile: confirmar inmediatamente + int col = hover_tile_ % tileset_width_; + int row = hover_tile_ / tileset_width_; + if (on_select) { on_select(col, row, 1, 1); } + close(); + } } else if (event.button.button == SDL_BUTTON_RIGHT) { close(); } } + if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && event.button.button == SDL_BUTTON_LEFT && selecting_rect_) { + int end_tile = (hover_tile_ >= 0) ? hover_tile_ : last_valid_hover_; + if (end_tile >= 0 && rect_start_tile_ >= 0) { + int c1 = rect_start_tile_ % tileset_width_; + int r1 = rect_start_tile_ / tileset_width_; + int c2 = end_tile % tileset_width_; + int r2 = end_tile / tileset_width_; + int col_min = std::min(c1, c2); + int col_max = std::max(c1, c2); + int row_min = std::min(r1, r2); + int row_max = std::max(r1, r2); + int width = col_max - col_min + 1; + int height = row_max - row_min + 1; + if (on_select) { on_select(col_min, row_min, width, height); } + } + selecting_rect_ = false; + rect_start_tile_ = -1; + close(); + } + if (event.type == SDL_EVENT_MOUSE_WHEEL) { scroll_y_ -= static_cast(event.wheel.y) * Tile::SIZE * 2; int max_scroll = static_cast(frame_dst_.h) - visible_height_; @@ -231,7 +289,8 @@ void TilePicker::updateMousePosition() { if (on_tile && rel_x >= 0 && rel_y >= 0 && tile_x >= 0 && tile_x < tileset_width_ && tile_y >= 0 && tile_y < tileset_height_) { - hover_tile_ = tile_y * tileset_width_ + tile_x; + hover_tile_ = (tile_y * tileset_width_) + tile_x; + last_valid_hover_ = hover_tile_; } else { hover_tile_ = -1; } diff --git a/source/game/editor/tile_picker.hpp b/source/game/editor/tile_picker.hpp index bb9b886..8914207 100644 --- a/source/game/editor/tile_picker.hpp +++ b/source/game/editor/tile_picker.hpp @@ -16,6 +16,7 @@ class Surface; * Muestra el tileset centrado en el play area. * Hover ilumina el tile bajo el cursor. * Click selecciona el tile y cierra el picker. + * Si allow_rect_selection es true, click + drag selecciona un rect de tiles. * Mouse wheel para scroll si el tileset es más alto que el play area. * ESC o click derecho para cancelar. */ @@ -29,15 +30,17 @@ class TilePicker { // source_color/target_color: sustitución de color (-1 = sin sustitución) // tile_spacing_in: pixels de separación entre tiles en el fichero fuente // tile_spacing_out: pixels de separación visual entre tiles al mostrar - void open(const std::string& tileset_name, int current_tile = -1, int bg_color = -1, int source_color = -1, int target_color = -1, int tile_spacing_in = 0, int tile_spacing_out = 1); + // allow_rect_selection: si true, permite arrastrar para seleccionar varios tiles + void open(const std::string& tileset_name, int current_tile = -1, int bg_color = -1, int source_color = -1, int target_color = -1, int tile_spacing_in = 0, int tile_spacing_out = 1, bool allow_rect_selection = false); void close(); [[nodiscard]] auto isOpen() const -> bool { return open_; } void render(); void handleEvent(const SDL_Event& event); - // Callback al seleccionar un tile (índice del tile) - std::function on_select; + // Callback al seleccionar (col, row, w, h dentro del tileset). + // Para selección de un solo tile: w=h=1. + std::function on_select; private: void updateMousePosition(); @@ -50,6 +53,12 @@ class TilePicker { int tileset_height_{0}; // Alto del tileset en tiles int current_tile_{-1}; // Tile actualmente seleccionado (highlight) int hover_tile_{-1}; // Tile bajo el cursor + int last_valid_hover_{-1}; // Último hover_tile_ válido (para soltar sobre spacing) + + // Selección rectangular + bool allow_rect_{false}; // ¿Se permite drag para seleccionar rect? + bool selecting_rect_{false}; // ¿Hay un drag activo? + int rect_start_tile_{-1}; // Tile donde empezó el drag // Spacing int spacing_in_{0}; // Spacing en el fichero fuente