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

@@ -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] - [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, 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, 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, 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, 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, -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] - [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) # Mapa de colisiones (0 = vacio, 1 = solido)
collision: 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] - [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, 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, 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, 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, 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] - [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 # Enemigos en esta habitación

View File

@@ -131,7 +131,7 @@ auto MapEditor::showGrid(bool show) -> std::string {
auto MapEditor::setEditingCollision(bool collision) -> std::string { auto MapEditor::setEditingCollision(bool collision) -> std::string {
editing_collision_ = collision; 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"; 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) // Resetear estado (preservar modo de edición en re-enter)
drag_ = {}; drag_ = {};
selection_.clear(); selection_.clear();
eyedropper_ = {};
if (!is_reenter) { if (!is_reenter) {
brush_tile_ = NO_BRUSH; brush_.clear();
painting_ = false; painting_ = false;
editing_collision_ = false; editing_collision_ = false;
} }
@@ -311,7 +312,7 @@ auto MapEditor::revert() -> std::string {
} }
selection_.clear(); selection_.clear();
brush_tile_ = NO_BRUSH; brush_.clear();
return "Reverted to original"; return "Reverted to original";
} }
@@ -359,26 +360,9 @@ void MapEditor::update(float delta_time) {
updateDrag(); updateDrag();
} }
// Si estamos pintando tiles, pintar en la posición actual del ratón // Si estamos pintando tiles, estampar el patrón en la posición del ratón
if (painting_ && brush_tile_ != NO_BRUSH) { if (painting_ && !brush_.isEmpty()) {
int tile_index = (mouse_tile_y_ * 32) + mouse_tile_x_; stampBrushAt(mouse_tile_x_, mouse_tile_y_);
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 // Actualizar la barra de estado
@@ -434,6 +418,16 @@ void MapEditor::render() {
// Renderizar highlight de selección (encima de los sprites) // Renderizar highlight de selección (encima de los sprites)
renderSelectionHighlight(); 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) // Tile picker o mini mapa (encima de todo en el play area)
if (tile_picker_.isOpen()) { if (tile_picker_.isOpen()) {
tile_picker_.render(); tile_picker_.render();
@@ -467,15 +461,25 @@ void MapEditor::handleEvent(const SDL_Event& event) { // NOLINT(readability-fun
return; return;
} }
// ESC: desactivar brush // ESC: cancelar eyedropper en progreso (sin tocar el brush previo)
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE && brush_tile_ != NO_BRUSH) { if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE && eyedropper_.active) {
brush_tile_ = NO_BRUSH; eyedropper_.active = false;
return; 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) { 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; return;
} }
@@ -524,41 +528,54 @@ void MapEditor::handleEvent(const SDL_Event& event) { // NOLINT(readability-fun
} }
} }
// Click derecho: abrir TilePicker // T: abrir TilePicker
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_RIGHT) { if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_T && static_cast<int>(event.key.repeat) == 0) {
// Deseleccionar entidades // Deseleccionar entidades
selection_.clear(); 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_) { if (editing_collision_) {
// Abrir tile picker del collision tileset current = (tile_index >= 0 && tile_index < static_cast<int>(room_data_.collision_tile_map.size()))
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] ? room_data_.collision_tile_map[tile_index]
: 0; : 0;
tile_picker_.on_select = [this](int tile) {
brush_tile_ = tile;
};
tile_picker_.open("collision.gif", current, 0);
} else { } else {
// Abrir tile picker del mapa de dibujo current = (tile_index >= 0 && tile_index < static_cast<int>(room_data_.tile_map.size()))
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] ? room_data_.tile_map[tile_index]
: -1; : -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; return;
} }
// Click izquierdo // Click izquierdo
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_LEFT) { 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 // 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 // Comprobar si hay hit en alguna entidad primero
bool hit_entity = false; bool hit_entity = false;
SDL_FRect player_rect = player_->getRect(); SDL_FRect player_rect = player_->getRect();
@@ -572,20 +589,9 @@ void MapEditor::handleEvent(const SDL_Event& event) { // NOLINT(readability-fun
} }
if (!hit_entity) { if (!hit_entity) {
// Pintar tile y entrar en modo painting // Estampar el patrón y entrar en modo painting
painting_ = true; painting_ = true;
int tile_index = (mouse_tile_y_ * 32) + mouse_tile_x_; stampBrushAt(mouse_tile_x_, mouse_tile_y_);
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; return;
} }
} }
@@ -951,6 +957,158 @@ void MapEditor::renderSelectionHighlight() {
game_surface->drawRectBorder(&border, DRAG_COLOR); 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 // Alinea un valor a la cuadrícula de 8x8
auto MapEditor::snapToGrid(float value) -> float { auto MapEditor::snapToGrid(float value) -> float {
return std::round(value / static_cast<float>(Tile::SIZE)) * static_cast<float>(Tile::SIZE); 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 // Línea 4: brush activo
std::string line4; std::string line4;
if (brush_tile_ == ERASER_BRUSH) { if (!brush_.isEmpty()) {
line4 = "brush: eraser (e)"; if (brush_.width == 1 && brush_.height == 1) {
} else if (brush_tile_ != NO_BRUSH) { int v = brush_.tiles[0];
line4 = "brush: tile " + std::to_string(brush_tile_); 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); statusbar_->setLine2(line2);
@@ -1989,8 +2150,14 @@ void MapEditor::openTilePicker(const std::string& tileset_name, int current_tile
Console::get()->toggle(); 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; } 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_data_.items[selection_.index].tile = tile;
room_->getItemManager()->getItem(selection_.index)->setTile(tile); room_->getItemManager()->getItem(selection_.index)->setTile(tile);
autosave(); autosave();
@@ -1999,7 +2166,7 @@ void MapEditor::openTilePicker(const std::string& tileset_name, int current_tile
Uint8 preview_color = hasSelectedItem() Uint8 preview_color = hasSelectedItem()
? room_data_.items[selection_.index].color1 ? room_data_.items[selection_.index].color1
: Item::DEFAULT_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 // Crea un nuevo item con valores por defecto, centrado en la habitación

View File

@@ -6,6 +6,7 @@
#include <memory> // Para shared_ptr, unique_ptr #include <memory> // Para shared_ptr, unique_ptr
#include <string> // Para string #include <string> // Para string
#include <vector> // Para vector
#include "game/editor/mini_map.hpp" // Para MiniMap #include "game/editor/mini_map.hpp" // Para MiniMap
#include "game/editor/tile_picker.hpp" // Para TilePicker #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; } [[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<int> 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 { class MapEditor {
public: public:
static void init(); // [SINGLETON] Crea el objeto static void init(); // [SINGLETON] Crea el objeto
@@ -152,9 +187,15 @@ class MapEditor {
void renderEntityBoundaries(); void renderEntityBoundaries();
static void renderBoundaryMarker(float x, float y, Uint8 color); static void renderBoundaryMarker(float x, float y, Uint8 color);
void renderSelectionHighlight(); void renderSelectionHighlight();
void renderBrushPreview();
void renderEyedropperRect();
void renderGrid() const; void renderGrid() const;
void handleMouseDown(float game_x, float game_y); void handleMouseDown(float game_x, float game_y);
void handleMouseUp(); 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 // Reconstruye todas las puertas vivas desde room_data_, limpiando primero
// los WALLs antiguos del CollisionMap. Lo usa setDoorProperty cuando un // los WALLs antiguos del CollisionMap. Lo usa setDoorProperty cuando un
@@ -184,12 +225,11 @@ class MapEditor {
// Estado del editor // Estado del editor
bool active_{false}; bool active_{false};
DragState drag_; DragState drag_;
Selection selection_; // Entidad seleccionada (unificada: enemy, item o platform) Selection selection_; // Entidad seleccionada (unificada: enemy, item o platform)
static constexpr int NO_BRUSH = -2; // Sin brush activo BrushPattern brush_; // Brush activo (vacío = sin brush)
static constexpr int ERASER_BRUSH = -1; // Brush borrador (pinta tile vacío = -1) EyedropperState eyedropper_; // Estado del eyedropper (clic derecho)
int brush_tile_{NO_BRUSH}; // Tile activo para pintar bool painting_{false}; // true mientras se está pintando con click izquierdo mantenido
bool painting_{false}; // true mientras se está pintando con click izquierdo mantenido bool editing_collision_{false}; // true = editando collision tilemap, false = editando draw tilemap
bool editing_collision_{false}; // true = editando collision tilemap, false = editando draw tilemap
// Datos de la habitación // Datos de la habitación
Room::Data room_data_; Room::Data room_data_;

View File

@@ -14,7 +14,7 @@
static constexpr int BORDER_PAD = 3; static constexpr int BORDER_PAD = 3;
// Abre el picker con un tileset // 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); tileset_ = Resource::Cache::get()->getSurface(tileset_name);
if (!tileset_) { if (!tileset_) {
open_ = false; 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_in_ = tile_spacing_in;
spacing_out_ = tile_spacing_out; 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) // Calcular dimensiones del tileset en tiles (teniendo en cuenta spacing de entrada)
int src_cell = Tile::SIZE + spacing_in_; 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<float>(scroll_y_); float tileset_screen_y = frame_dst_.y + BORDER_PAD - static_cast<float>(scroll_y_);
constexpr auto TS = static_cast<float>(Tile::SIZE); constexpr auto TS = static_cast<float>(Tile::SIZE);
// Highlight del tile bajo el cursor (blanco) // Si hay un drag rect activo, dibujar el rectángulo en progreso (blanco)
if (hover_tile_ >= 0) { // 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<float>(col_min * out_cell);
float ry = tileset_screen_y + static_cast<float>(row_min * out_cell);
float rw = static_cast<float>((cells_w * out_cell) - spacing_out_);
float rh = static_cast<float>((cells_h * out_cell) - spacing_out_);
if (ry + rh > 0 && ry < static_cast<float>(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 col = hover_tile_ % tileset_width_;
int row = hover_tile_ / tileset_width_; int row = hover_tile_ / tileset_width_;
float hx = tileset_screen_x + static_cast<float>(col * out_cell); float hx = tileset_screen_x + static_cast<float>(col * out_cell);
float hy = tileset_screen_y + static_cast<float>(row * out_cell); float hy = tileset_screen_y + static_cast<float>(row * out_cell);
if (hy >= 0 && hy + TS <= visible_height_) { if (hy >= 0 && hy + TS <= static_cast<float>(visible_height_)) {
SDL_FRect highlight = {.x = hx, .y = hy, .w = TS, .h = TS}; SDL_FRect highlight = {.x = hx, .y = hy, .w = TS, .h = TS};
game_surface->drawRectBorder(&highlight, 15); game_surface->drawRectBorder(&highlight, 15);
} }
@@ -163,7 +191,7 @@ void TilePicker::render() {
int row = current_tile_ / tileset_width_; int row = current_tile_ / tileset_width_;
float cx = tileset_screen_x + static_cast<float>(col * out_cell); float cx = tileset_screen_x + static_cast<float>(col * out_cell);
float cy = tileset_screen_y + static_cast<float>(row * out_cell); float cy = tileset_screen_y + static_cast<float>(row * out_cell);
if (cy >= 0 && cy + TS <= visible_height_) { if (cy >= 0 && cy + TS <= static_cast<float>(visible_height_)) {
SDL_FRect cur_rect = {.x = cx, .y = cy, .w = TS, .h = TS}; SDL_FRect cur_rect = {.x = cx, .y = cy, .w = TS, .h = TS};
game_surface->drawRectBorder(&cur_rect, 9); 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.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
if (event.button.button == SDL_BUTTON_LEFT && hover_tile_ >= 0) { if (event.button.button == SDL_BUTTON_LEFT) {
if (on_select) { on_select(hover_tile_); } if (allow_rect_ && hover_tile_ >= 0) {
close(); // 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) { } else if (event.button.button == SDL_BUTTON_RIGHT) {
close(); 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) { if (event.type == SDL_EVENT_MOUSE_WHEEL) {
scroll_y_ -= static_cast<int>(event.wheel.y) * Tile::SIZE * 2; scroll_y_ -= static_cast<int>(event.wheel.y) * Tile::SIZE * 2;
int max_scroll = static_cast<int>(frame_dst_.h) - visible_height_; int max_scroll = static_cast<int>(frame_dst_.h) - visible_height_;
@@ -231,7 +289,8 @@ void TilePicker::updateMousePosition() {
if (on_tile && rel_x >= 0 && rel_y >= 0 && if (on_tile && rel_x >= 0 && rel_y >= 0 &&
tile_x >= 0 && tile_x < tileset_width_ && tile_x >= 0 && tile_x < tileset_width_ &&
tile_y >= 0 && tile_y < tileset_height_) { 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 { } else {
hover_tile_ = -1; hover_tile_ = -1;
} }

View File

@@ -16,6 +16,7 @@ class Surface;
* Muestra el tileset centrado en el play area. * Muestra el tileset centrado en el play area.
* Hover ilumina el tile bajo el cursor. * Hover ilumina el tile bajo el cursor.
* Click selecciona el tile y cierra el picker. * 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. * Mouse wheel para scroll si el tileset es más alto que el play area.
* ESC o click derecho para cancelar. * ESC o click derecho para cancelar.
*/ */
@@ -29,15 +30,17 @@ class TilePicker {
// source_color/target_color: sustitución de color (-1 = sin sustitución) // 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_in: pixels de separación entre tiles en el fichero fuente
// tile_spacing_out: pixels de separación visual entre tiles al mostrar // 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(); void close();
[[nodiscard]] auto isOpen() const -> bool { return open_; } [[nodiscard]] auto isOpen() const -> bool { return open_; }
void render(); void render();
void handleEvent(const SDL_Event& event); void handleEvent(const SDL_Event& event);
// Callback al seleccionar un tile (índice del tile) // Callback al seleccionar (col, row, w, h dentro del tileset).
std::function<void(int)> on_select; // Para selección de un solo tile: w=h=1.
std::function<void(int, int, int, int)> on_select;
private: private:
void updateMousePosition(); void updateMousePosition();
@@ -50,6 +53,12 @@ class TilePicker {
int tileset_height_{0}; // Alto del tileset en tiles int tileset_height_{0}; // Alto del tileset en tiles
int current_tile_{-1}; // Tile actualmente seleccionado (highlight) int current_tile_{-1}; // Tile actualmente seleccionado (highlight)
int hover_tile_{-1}; // Tile bajo el cursor 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 // Spacing
int spacing_in_{0}; // Spacing en el fichero fuente int spacing_in_{0}; // Spacing en el fichero fuente