diff --git a/CMakeLists.txt b/CMakeLists.txt index 39f87ff..a5c59c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,6 +101,7 @@ set(APP_SOURCES source/game/editor/editor_statusbar.cpp source/game/editor/room_saver.cpp source/game/editor/tile_picker.cpp + source/game/editor/mini_map.cpp # Game - UI source/game/ui/console.cpp diff --git a/source/game/editor/map_editor.cpp b/source/game/editor/map_editor.cpp index b7c3786..bcfa4d5 100644 --- a/source/game/editor/map_editor.cpp +++ b/source/game/editor/map_editor.cpp @@ -100,6 +100,13 @@ auto MapEditor::showGrid(bool show) -> std::string { return show ? "Grid ON" : "Grid OFF"; } +void MapEditor::toggleMiniMap() { + if (!mini_map_) { + mini_map_ = std::make_unique(); + } + mini_map_visible_ = !mini_map_visible_; +} + // Entra en modo editor void MapEditor::enter(std::shared_ptr room, std::shared_ptr player, const std::string& room_path, std::shared_ptr scoreboard_data) { if (active_) { return; } @@ -285,9 +292,11 @@ void MapEditor::render() { // Renderizar highlight de selección (encima de los sprites) renderSelectionHighlight(); - // Tile picker (encima de todo en el play area) + // 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) @@ -304,6 +313,20 @@ void MapEditor::handleEvent(const SDL_Event& event) { return; } + // Si el mini mapa está visible, cerrarlo con click o ESC + if (mini_map_visible_) { + if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN || + (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE)) { + mini_map_visible_ = false; + return; + } + if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_M) { + mini_map_visible_ = false; + return; + } + return; // Bloquear otros eventos mientras el minimapa está visible + } + // ESC: desactivar brush if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE && brush_tile_ != NO_BRUSH) { brush_tile_ = NO_BRUSH; @@ -316,6 +339,12 @@ void MapEditor::handleEvent(const SDL_Event& event) { return; } + // M: toggle mini mapa + if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_M && static_cast(event.key.repeat) == 0) { + toggleMiniMap(); + return; + } + // Click derecho: abrir TilePicker del mapa if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_RIGHT) { // Deseleccionar entidades diff --git a/source/game/editor/map_editor.hpp b/source/game/editor/map_editor.hpp index f156f3d..e45dd40 100644 --- a/source/game/editor/map_editor.hpp +++ b/source/game/editor/map_editor.hpp @@ -8,6 +8,7 @@ #include // Para string #include "external/fkyaml_node.hpp" // Para fkyaml::node +#include "game/editor/mini_map.hpp" // Para MiniMap #include "game/editor/tile_picker.hpp" // Para TilePicker #include "game/entities/enemy.hpp" // Para Enemy::Data #include "game/entities/item.hpp" // Para Item::Data @@ -47,6 +48,7 @@ class MapEditor { auto showInfo(bool show) -> std::string; auto showGrid(bool show) -> std::string; [[nodiscard]] auto isGridEnabled() const -> bool { return settings_.grid; } + void toggleMiniMap(); // Comandos para items auto setItemProperty(const std::string& property, const std::string& value) -> std::string; @@ -130,8 +132,10 @@ class MapEditor { // Barra de estado del editor std::unique_ptr statusbar_; - // Tile picker (para seleccionar tiles de un tileset) + // Tile picker y mini mapa TilePicker tile_picker_; + std::unique_ptr mini_map_; + bool mini_map_visible_{false}; // Estado del ratón float mouse_game_x_{0.0F}; diff --git a/source/game/editor/mini_map.cpp b/source/game/editor/mini_map.cpp new file mode 100644 index 0000000..eae4406 --- /dev/null +++ b/source/game/editor/mini_map.cpp @@ -0,0 +1,298 @@ +#ifdef _DEBUG + +#include "game/editor/mini_map.hpp" + +#include // Para std::max, std::min +#include // Para std::array +#include // Para cout +#include // Para std::map +#include // Para queue (BFS) +#include // Para set + +#include "core/rendering/screen.hpp" // Para Screen +#include "core/rendering/surface.hpp" // Para Surface +#include "core/resources/resource_cache.hpp" // Para Resource::Cache +#include "game/gameplay/room.hpp" // Para Room::Data +#include "utils/defines.hpp" // Para Tile::SIZE, PlayArea +#include "utils/utils.hpp" // Para stringToColor + +// Constructor: construye todo el minimapa +MiniMap::MiniMap() { + buildTileColorTable("standard.gif"); + layoutRooms(); + buildRoomSurfaces(); + composeFinalSurface(); +} + +// Analiza el tileset y crea tabla: tile_index → color predominante +void MiniMap::buildTileColorTable(const std::string& tileset_name) { + auto tileset = Resource::Cache::get()->getSurface(tileset_name); + if (!tileset) { return; } + + tileset_width_ = static_cast(tileset->getWidth()) / Tile::SIZE; + int tileset_height = static_cast(tileset->getHeight()) / Tile::SIZE; + int total_tiles = tileset_width_ * tileset_height; + + tile_colors_.resize(total_tiles, 0); + + for (int tile = 0; tile < total_tiles; ++tile) { + int tile_x = (tile % tileset_width_) * Tile::SIZE; + int tile_y = (tile / tileset_width_) * Tile::SIZE; + + // Contar frecuencia de cada color en el tile (ignorar transparente=255 y negro=0) + std::array freq{}; + for (int y = 0; y < Tile::SIZE; ++y) { + for (int x = 0; x < Tile::SIZE; ++x) { + Uint8 pixel = tileset->getPixel(tile_x + x, tile_y + y); + if (pixel != 0 && pixel != 255) { + freq[pixel]++; + } + } + } + + // Encontrar el color más frecuente + Uint8 best_color = 0; + int best_count = 0; + for (int c = 1; c < 256; ++c) { + if (freq[c] > best_count) { + best_count = freq[c]; + best_color = static_cast(c); + } + } + + tile_colors_[tile] = best_color; + } +} + +// Posiciona las rooms en un grid usando BFS desde las conexiones +void MiniMap::layoutRooms() { + auto& rooms = Resource::Cache::get()->getRooms(); + if (rooms.empty()) { return; } + + // Mapa de nombre → Room::Data + std::unordered_map> room_map; + for (const auto& r : rooms) { + room_map[r.name] = r.room; + } + + // BFS para posicionar rooms + std::set visited; + std::queue> bfs; + + // Empezar por la primera room + const std::string& start = rooms[0].name; + bfs.push({start, {0, 0}}); + visited.insert(start); + + // Grid ocupado: posición → nombre de room + std::map, std::string> grid_occupied; + + while (!bfs.empty()) { + auto [name, pos] = bfs.front(); + bfs.pop(); + + auto key = std::make_pair(pos.x, pos.y); + if (grid_occupied.contains(key)) { continue; } + + grid_occupied[key] = name; + room_positions_[name] = RoomMini{.surface = nullptr, .pos = pos}; + + auto it = room_map.find(name); + if (it == room_map.end()) { continue; } + const auto& data = it->second; + + // Vecinos: up, down, left, right + struct Neighbor { + std::string room; + int dx, dy; + }; + std::array neighbors = {{ + {data->upper_room, 0, -1}, + {data->lower_room, 0, 1}, + {data->left_room, -1, 0}, + {data->right_room, 1, 0}, + }}; + + for (const auto& [neighbor_name, dx, dy] : neighbors) { + if (neighbor_name == "0" || neighbor_name.empty()) { continue; } + if (visited.contains(neighbor_name)) { continue; } + + GridPos neighbor_pos = {pos.x + dx, pos.y + dy}; + auto nkey = std::make_pair(neighbor_pos.x, neighbor_pos.y); + if (!grid_occupied.contains(nkey)) { + visited.insert(neighbor_name); + bfs.push({neighbor_name, neighbor_pos}); + } + } + } + + // Calcular bounds del grid + min_grid_x_ = 0; + min_grid_y_ = 0; + int max_grid_x = 0; + int max_grid_y = 0; + for (const auto& [name, mini] : room_positions_) { + min_grid_x_ = std::min(min_grid_x_, mini.pos.x); + min_grid_y_ = std::min(min_grid_y_, mini.pos.y); + max_grid_x = std::max(max_grid_x, mini.pos.x); + max_grid_y = std::max(max_grid_y, mini.pos.y); + } + + int cols = max_grid_x - min_grid_x_ + 1; + int rows = max_grid_y - min_grid_y_ + 1; + map_width_ = cols * (CELL_W + GAP) - GAP + PADDING * 2; + map_height_ = rows * (CELL_H + GAP) - GAP + PADDING * 2; + + std::cout << "MiniMap: " << room_positions_.size() << " rooms, grid " << cols << "x" << rows + << " → " << map_width_ << "x" << map_height_ << " px\n"; +} + +// Genera una mini-surface de 32x16 por room +void MiniMap::buildRoomSurfaces() { + for (auto& [name, mini] : room_positions_) { + mini.surface = getRoomMiniSurface(name); + } +} + +// Genera la mini-surface de una room: 1 pixel por tile, color predominante +auto MiniMap::getRoomMiniSurface(const std::string& room_name) -> std::shared_ptr { + auto room_data = Resource::Cache::get()->getRoom(room_name); + if (!room_data) { return nullptr; } + + auto surface = std::make_shared(ROOM_W, ROOM_H); + + auto prev = Screen::get()->getRendererSurface(); + Screen::get()->setRendererSurface(surface); + surface->clear(stringToColor(room_data->bg_color)); + + const auto& tile_map = room_data->tile_map; + for (int y = 0; y < ROOM_H; ++y) { + for (int x = 0; x < ROOM_W; ++x) { + int index = y * ROOM_W + x; + if (index >= static_cast(tile_map.size())) { continue; } + + int tile = tile_map[index]; + if (tile < 0 || tile >= static_cast(tile_colors_.size())) { continue; } + + Uint8 color = tile_colors_[tile]; + if (color != 0) { + surface->putPixel(x, y, color); + } + } + } + + Screen::get()->setRendererSurface(prev); + return surface; +} + +// Compone la surface final con todas las rooms posicionadas +void MiniMap::composeFinalSurface() { + if (map_width_ <= 0 || map_height_ <= 0) { return; } + + // Surface un poco más grande para la sombra del borde inferior/derecho + map_surface_ = std::make_shared(map_width_ + SHADOW_OFFSET, map_height_ + SHADOW_OFFSET); + + auto prev = Screen::get()->getRendererSurface(); + Screen::get()->setRendererSurface(map_surface_); + + // 1. Fondo general + map_surface_->clear(COLOR_BACKGROUND); + + // 2. Líneas de conexión entre rooms (debajo de todo) + drawConnections(); + + // 3. Sombras de las rooms (desplazadas 1px abajo-derecha) + for (const auto& [name, mini] : room_positions_) { + if (!mini.surface) { continue; } + int px = cellPixelX(mini.pos.x) + SHADOW_OFFSET; + int py = cellPixelY(mini.pos.y) + SHADOW_OFFSET; + SDL_FRect shadow = {.x = static_cast(px), .y = static_cast(py), + .w = static_cast(CELL_W), .h = static_cast(CELL_H)}; + map_surface_->fillRect(&shadow, COLOR_SHADOW); + } + + // 4. Borde negro de cada room + contenido de la room + for (const auto& [name, mini] : room_positions_) { + if (!mini.surface) { continue; } + int px = cellPixelX(mini.pos.x); + int py = cellPixelY(mini.pos.y); + + // Borde negro (la celda entera) + SDL_FRect cell = {.x = static_cast(px), .y = static_cast(py), + .w = static_cast(CELL_W), .h = static_cast(CELL_H)}; + map_surface_->fillRect(&cell, COLOR_ROOM_BORDER); + + // Miniroom dentro del borde + SDL_FRect dst = {.x = static_cast(px + BORDER), .y = static_cast(py + BORDER), + .w = static_cast(ROOM_W), .h = static_cast(ROOM_H)}; + mini.surface->render(nullptr, &dst); + } + + Screen::get()->setRendererSurface(prev); +} + +// Dibuja las líneas de conexión entre rooms vecinas +void MiniMap::drawConnections() { + for (const auto& [name, mini] : room_positions_) { + auto room_data = Resource::Cache::get()->getRoom(name); + if (!room_data) { continue; } + + int px = cellPixelX(mini.pos.x); + int py = cellPixelY(mini.pos.y); + + // Conexión derecha: línea horizontal desde el borde derecho de esta room hasta el borde izquierdo de la vecina + if (room_data->right_room != "0" && !room_data->right_room.empty() && room_positions_.contains(room_data->right_room)) { + int x1 = px + CELL_W; + int y_mid = py + CELL_H / 2; + SDL_FRect line = {.x = static_cast(x1), .y = static_cast(y_mid), + .w = static_cast(GAP), .h = 1.0F}; + map_surface_->fillRect(&line, COLOR_CONNECTION); + } + + // Conexión abajo: línea vertical desde el borde inferior de esta room hasta el borde superior de la vecina + if (room_data->lower_room != "0" && !room_data->lower_room.empty() && room_positions_.contains(room_data->lower_room)) { + int x_mid = px + CELL_W / 2; + int y1 = py + CELL_H; + SDL_FRect line = {.x = static_cast(x_mid), .y = static_cast(y1), + .w = 1.0F, .h = static_cast(GAP)}; + map_surface_->fillRect(&line, COLOR_CONNECTION); + } + } +} + +// Renderiza el minimapa centrado en la room actual +void MiniMap::render(const std::string& current_room) { + if (!map_surface_) { return; } + + auto game_surface = Screen::get()->getRendererSurface(); + if (!game_surface) { return; } + + // Fondo negro en el play area + SDL_FRect bg = {.x = 0, .y = 0, .w = static_cast(PlayArea::WIDTH), .h = static_cast(PlayArea::HEIGHT)}; + game_surface->fillRect(&bg, 0); + + // Centrar el minimapa en el play area, centrado en la room actual + auto it = room_positions_.find(current_room); + if (it == room_positions_.end()) { return; } + const auto& current_pos = it->second.pos; + + int room_cx = cellPixelX(current_pos.x) + CELL_W / 2; + int room_cy = cellPixelY(current_pos.y) + CELL_H / 2; + + int offset_x = PlayArea::WIDTH / 2 - room_cx; + int offset_y = PlayArea::HEIGHT / 2 - room_cy; + + // Renderizar la surface del minimapa con offset + SDL_FRect dst = {.x = static_cast(offset_x), .y = static_cast(offset_y), + .w = static_cast(map_width_ + SHADOW_OFFSET), .h = static_cast(map_height_ + SHADOW_OFFSET)}; + map_surface_->render(nullptr, &dst); + + // Highlight de la room actual (borde bright_white alrededor de la celda) + float cur_x = static_cast(offset_x + cellPixelX(current_pos.x)); + float cur_y = static_cast(offset_y + cellPixelY(current_pos.y)); + SDL_FRect highlight = {.x = cur_x - 1, .y = cur_y - 1, + .w = static_cast(CELL_W + 2), .h = static_cast(CELL_H + 2)}; + game_surface->drawRectBorder(&highlight, stringToColor("bright_white")); +} + +#endif // _DEBUG diff --git a/source/game/editor/mini_map.hpp b/source/game/editor/mini_map.hpp new file mode 100644 index 0000000..4079412 --- /dev/null +++ b/source/game/editor/mini_map.hpp @@ -0,0 +1,85 @@ +#pragma once + +#ifdef _DEBUG + +#include + +#include // Para shared_ptr +#include // Para string +#include // Para unordered_map +#include // Para vector + +class Surface; + +/** + * @brief Minimapa global del juego para el editor + * + * Genera una vista en miniatura de todas las habitaciones del juego, + * posicionadas según sus conexiones. + * Cada tile del mapa se representa como 1 pixel del color predominante de ese tile. + * Resultado: cada room = 32x16 pixels. + */ +class MiniMap { + public: + MiniMap(); + ~MiniMap() = default; + + void render(const std::string& current_room); // Dibuja el minimapa centrado en la room actual + [[nodiscard]] auto isReady() const -> bool { return !room_positions_.empty(); } + + private: + // Posición de una room en el grid del minimapa + struct GridPos { + int x{0}; + int y{0}; + }; + + // Una room renderizada + struct RoomMini { + std::shared_ptr surface; // 32x16 pixels + GridPos pos; // Posición en el grid + }; + + void buildTileColorTable(const std::string& tileset_name); + void buildRoomSurfaces(); + void layoutRooms(); + void composeFinalSurface(); + auto getRoomMiniSurface(const std::string& room_name) -> std::shared_ptr; + void drawConnections(); + auto cellPixelX(int grid_x) const -> int { return PADDING + (grid_x - min_grid_x_) * (CELL_W + GAP); } + auto cellPixelY(int grid_y) const -> int { return PADDING + (grid_y - min_grid_y_) * (CELL_H + GAP); } + + // Tabla de color predominante por tile index + std::vector tile_colors_; // tile_index → palette color index + int tileset_width_{0}; // Ancho del tileset en tiles + + // Rooms renderizadas y posicionadas + std::unordered_map room_positions_; + + // Surface final compuesta + std::shared_ptr map_surface_; + int map_width_{0}; // Ancho en pixels + int map_height_{0}; // Alto en pixels + + // Offset para normalizar coordenadas + int min_grid_x_{0}; + int min_grid_y_{0}; + + // Constantes + static constexpr int ROOM_W = 32; // Ancho de una room en pixels del minimapa + static constexpr int ROOM_H = 16; // Alto de una room en pixels del minimapa + static constexpr int BORDER = 1; // Borde alrededor de cada room + static constexpr int CELL_W = ROOM_W + BORDER * 2; // Room + borde + static constexpr int CELL_H = ROOM_H + BORDER * 2; + static constexpr int GAP = 4; // Separación entre celdas + static constexpr int SHADOW_OFFSET = 1; // Desplazamiento de la sombra + static constexpr int PADDING = 4; // Padding alrededor del minimapa + + // Colores del minimapa (índices de paleta) + static constexpr Uint8 COLOR_BACKGROUND = 2; // Fondo general (blue, si border es 0 usamos 2) + static constexpr Uint8 COLOR_ROOM_BORDER = 0; // Borde de cada miniroom + static constexpr Uint8 COLOR_SHADOW = 1; // Sombra de cada miniroom + static constexpr Uint8 COLOR_CONNECTION = 14; // Líneas de conexión entre rooms +}; + +#endif // _DEBUG