map_editor:

- tilepicker s'obri ara amb la T
- brush de varios tiles
- eyedropper amb botó dret (de uno o varios tiles)
This commit is contained in:
2026-04-10 21:33:57 +02:00
parent 342b46ca32
commit fadc3b03c9
5 changed files with 363 additions and 88 deletions

View File

@@ -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> room, std::shared_ptr<Player> 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<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_);
}
}
}
// 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<int>(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<int>(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<int>(room_data_.collision_tile_map.size()))
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()))
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);
}
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<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_);
}
}
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<int>(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<int>(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<size_t>(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<int>(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<int>(surface->getWidth()) / Tile::SIZE;
if (cols <= 0) { return p; }
p.width = width;
p.height = height;
p.tiles.reserve(static_cast<size_t>(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<int>(tileset->getWidth()) / Tile::SIZE) : 0;
constexpr auto TS = static_cast<float>(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<float>(tx) * TS;
float dst_y = static_cast<float>(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<Uint8>(room_data_.bg_color));
} else if (tileset && cols > 0) {
SDL_FRect src = {
.x = static_cast<float>(value % cols) * TS,
.y = static_cast<float>(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<float>(mouse_tile_x_) * TS;
float by = static_cast<float>(mouse_tile_y_) * TS;
SDL_FRect border = {
.x = bx - 1.0F,
.y = by - 1.0F,
.w = (static_cast<float>(brush_.width) * TS) + 2.0F,
.h = (static_cast<float>(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<float>(Tile::SIZE);
SDL_FRect rect = {
.x = static_cast<float>(xmin) * TS,
.y = static_cast<float>(ymin) * TS,
.w = static_cast<float>((xmax - xmin) + 1) * TS,
.h = static_cast<float>((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<float>(Tile::SIZE)) * static_cast<float>(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<int>(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