segon commit

This commit is contained in:
2026-04-05 21:34:38 +02:00
parent d168ed59f9
commit 20ad7d778f
502 changed files with 178145 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
#ifdef _DEBUG
#include "game/editor/editor_statusbar.hpp"
#include <string> // Para to_string
#include <utility>
#include "core/rendering/screen.hpp" // Para Screen
#include "core/rendering/surface.hpp" // Para Surface
#include "core/rendering/text.hpp" // Para Text
#include "core/resources/resource_cache.hpp" // Para Resource::Cache
#include "game/options.hpp" // Para Options::game
#include "utils/defines.hpp" // Para Tile::SIZE
#include "utils/utils.hpp" // Para stringToColor, toLower
// Constructor
EditorStatusBar::EditorStatusBar(std::string room_number, std::string room_name)
: room_number_(std::move(room_number)),
room_name_(std::move(room_name)) {
const float SURFACE_WIDTH = Options::game.width;
constexpr float SURFACE_HEIGHT = 6.0F * Tile::SIZE; // 48 pixels, igual que el scoreboard
surface_ = std::make_shared<Surface>(SURFACE_WIDTH, SURFACE_HEIGHT);
surface_dest_ = {.x = 0, .y = Options::game.height - SURFACE_HEIGHT, .w = SURFACE_WIDTH, .h = SURFACE_HEIGHT};
}
// Pinta la barra de estado en pantalla
void EditorStatusBar::render() {
surface_->render(nullptr, &surface_dest_);
}
// Actualiza la barra de estado
void EditorStatusBar::update([[maybe_unused]] float delta_time) {
fillTexture();
}
void EditorStatusBar::setMouseTile(int tile_x, int tile_y) {
mouse_tile_x_ = tile_x;
mouse_tile_y_ = tile_y;
}
void EditorStatusBar::setLine2(const std::string& text) { line2_ = text; }
void EditorStatusBar::setLine3(const std::string& text) { line3_ = text; }
void EditorStatusBar::setLine4(const std::string& text) { line4_ = text; }
void EditorStatusBar::setLine5(const std::string& text) { line5_ = text; }
// Dibuja los elementos en la surface
void EditorStatusBar::fillTexture() {
auto previous_renderer = Screen::get()->getRendererSurface();
Screen::get()->setRendererSurface(surface_);
surface_->clear(stringToColor("black"));
auto text = Resource::Cache::get()->getText("8bithud");
const Uint8 LABEL_COLOR = stringToColor("bright_cyan");
const Uint8 VALUE_COLOR = stringToColor("white");
const Uint8 DETAIL_COLOR = stringToColor("bright_yellow");
// Línea 1: Nombre de la habitación
text->writeColored(LEFT_X, LINE1_Y, toLower(room_number_ + " " + room_name_), LABEL_COLOR);
// Línea 2: Propiedades de room o info de enemigo
if (!line2_.empty()) {
text->writeColored(LEFT_X, LINE2_Y, toLower(line2_), DETAIL_COLOR);
}
// Línea 3: Conexiones+items o propiedades del enemigo
if (!line3_.empty()) {
text->writeColored(LEFT_X, LINE3_Y, toLower(line3_), VALUE_COLOR);
}
// Línea 4: Extra
if (!line4_.empty()) {
text->writeColored(LEFT_X, LINE4_Y, toLower(line4_), DETAIL_COLOR);
}
// Línea 5: Tile coords + drag info
const std::string TILE_X_STR = (mouse_tile_x_ < 10 ? "0" : "") + std::to_string(mouse_tile_x_);
const std::string TILE_Y_STR = (mouse_tile_y_ < 10 ? "0" : "") + std::to_string(mouse_tile_y_);
std::string line5 = "tile:" + TILE_X_STR + "," + TILE_Y_STR;
if (!line5_.empty()) {
line5 += " " + line5_;
}
text->writeColored(LEFT_X, LINE5_Y, toLower(line5), stringToColor("bright_green"));
Screen::get()->setRendererSurface(previous_renderer);
}
#endif // _DEBUG

View File

@@ -0,0 +1,52 @@
#pragma once
#ifdef _DEBUG
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr
#include <string> // Para string
class Surface;
class EditorStatusBar {
public:
EditorStatusBar(std::string room_number, std::string room_name);
~EditorStatusBar() = default;
void render();
void update(float delta_time);
void setMouseTile(int tile_x, int tile_y);
void setLine2(const std::string& text);
void setLine3(const std::string& text);
void setLine4(const std::string& text);
void setLine5(const std::string& text);
private:
void fillTexture(); // Dibuja los elementos en la surface
// Constantes de posición (en pixels dentro de la surface de 256x48)
// Font 8bithud lowercase = 6px alto → 5 líneas con 8px de separación
static constexpr int LINE1_Y = 2; // Nombre de la habitación
static constexpr int LINE2_Y = 10; // Propiedades de room / enemy info
static constexpr int LINE3_Y = 18; // Conexiones+items / enemy detail
static constexpr int LINE4_Y = 26; // Extra
static constexpr int LINE5_Y = 34; // Tile coords + drag info
static constexpr int LEFT_X = 4; // Margen izquierdo
// Objetos
std::shared_ptr<Surface> surface_; // Surface donde dibujar la barra
SDL_FRect surface_dest_{}; // Rectángulo destino en pantalla
// Variables
std::string room_number_; // Número de la habitación
std::string room_name_; // Nombre de la habitación
int mouse_tile_x_{0}; // Coordenada X del ratón en tiles
int mouse_tile_y_{0}; // Coordenada Y del ratón en tiles
std::string line2_; // Contenido de la línea 2
std::string line3_; // Contenido de la línea 3
std::string line4_; // Contenido de la línea 4
std::string line5_; // Contenido de la línea 5
};
#endif // _DEBUG

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,160 @@
#pragma once
#ifdef _DEBUG
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr, unique_ptr
#include <string> // 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
#include "game/entities/player.hpp" // Para Player::SpawnData
#include "game/gameplay/room.hpp" // Para Room::Data
#include "game/gameplay/scoreboard.hpp" // Para Scoreboard::Data
#include "game/options.hpp" // Para Options::Cheat
class EditorStatusBar;
class MapEditor {
public:
static void init(); // [SINGLETON] Crea el objeto
static void destroy(); // [SINGLETON] Destruye el objeto
static auto get() -> MapEditor*; // [SINGLETON] Obtiene el objeto
void enter(std::shared_ptr<Room> room, std::shared_ptr<Player> player, const std::string& room_path, std::shared_ptr<Scoreboard::Data> scoreboard_data);
void exit();
[[nodiscard]] auto isActive() const -> bool { return active_; }
void update(float delta_time);
void render();
void handleEvent(const SDL_Event& event);
auto revert() -> std::string;
// Comandos para enemigos (llamados desde console_commands)
auto setEnemyProperty(const std::string& property, const std::string& value) -> std::string;
auto addEnemy() -> std::string;
auto deleteEnemy() -> std::string;
auto duplicateEnemy() -> std::string;
[[nodiscard]] auto hasSelectedEnemy() const -> bool;
[[nodiscard]] auto getSetCompletions() const -> std::vector<std::string>;
// Comandos para propiedades de la habitación
auto setRoomProperty(const std::string& property, const std::string& value) -> std::string;
auto createNewRoom(const std::string& direction = "") -> std::string;
auto deleteRoom() -> std::string;
// Opciones del editor (llamados desde console_commands / teclas)
auto showInfo(bool show) -> std::string;
auto showGrid(bool show) -> std::string;
[[nodiscard]] auto isGridEnabled() const -> bool { return settings_.grid; }
void toggleMiniMap();
void setReenter(bool value) { reenter_ = value; }
auto setMiniMapBg(const std::string& color) -> std::string;
auto setMiniMapConn(const std::string& color) -> std::string;
// Comandos para items
auto setItemProperty(const std::string& property, const std::string& value) -> std::string;
auto addItem() -> std::string;
auto deleteItem() -> std::string;
auto duplicateItem() -> std::string;
[[nodiscard]] auto hasSelectedItem() const -> bool;
void openTilePicker(const std::string& tileset_name, int current_tile);
private:
static MapEditor* instance_; // NOLINT(readability-identifier-naming) [SINGLETON] Objeto privado
MapEditor(); // Constructor
~MapEditor(); // Destructor
// Opciones persistentes del editor
struct Settings {
bool grid{false};
bool show_render_info{false};
std::string minimap_bg{"blue"};
std::string minimap_conn{"white"};
};
Settings settings_;
void loadSettings();
void saveSettings() const;
// Tipos para drag & drop y selección
enum class DragTarget { NONE,
PLAYER,
ENEMY_INITIAL,
ENEMY_BOUND1,
ENEMY_BOUND2,
ITEM };
struct DragState {
DragTarget target{DragTarget::NONE};
int index{-1};
float offset_x{0.0F};
float offset_y{0.0F};
float snap_x{0.0F};
float snap_y{0.0F};
bool moved{false}; // true si el ratón se movió durante el drag
};
// Métodos internos
void updateMousePosition();
void renderEnemyBoundaries();
static void renderBoundaryMarker(float x, float y, Uint8 color);
void renderSelectionHighlight();
void renderGrid() const;
void handleMouseDown(float game_x, float game_y);
void handleMouseUp();
void updateDrag();
void autosave();
void updateStatusBarInfo();
static auto snapToGrid(float value) -> float;
static auto pointInRect(float px, float py, const SDL_FRect& rect) -> bool;
// Estado del editor
bool active_{false};
DragState drag_;
int selected_enemy_{-1}; // Índice del enemigo seleccionado (-1 = ninguno)
int selected_item_{-1}; // Índice del item seleccionado (-1 = ninguno)
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
// Datos de la habitación
Room::Data room_data_;
std::string room_path_;
std::string file_path_;
// YAML: nodo original (para campos que no se editan: name_ca, etc.)
fkyaml::node yaml_;
fkyaml::node yaml_backup_;
// Referencias a objetos vivos
std::shared_ptr<Room> room_;
std::shared_ptr<Player> player_;
std::shared_ptr<Scoreboard::Data> scoreboard_data_;
// Barra de estado del editor
std::unique_ptr<EditorStatusBar> statusbar_;
// Tile picker y mini mapa
TilePicker tile_picker_;
std::unique_ptr<MiniMap> mini_map_;
bool mini_map_visible_{false};
// Estado del ratón
float mouse_game_x_{0.0F};
float mouse_game_y_{0.0F};
int mouse_tile_x_{0};
int mouse_tile_y_{0};
// Estado previo (para restaurar al salir)
Options::Cheat::State invincible_before_editor_{Options::Cheat::State::DISABLED};
bool render_info_before_editor_{false};
bool reenter_{false}; // true cuando es un re-enter tras cambio de room (no tocar render_info)
};
#endif // _DEBUG

View File

@@ -0,0 +1,391 @@
#ifdef _DEBUG
#include "game/editor/mini_map.hpp"
#include <algorithm> // Para std::max, std::min
#include <array> // Para std::array
#include <cmath> // Para std::floor
#include <iostream> // Para cout
#include <map> // Para std::map
#include <queue> // Para queue (BFS)
#include <set> // 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(Uint8 bg_color, Uint8 conn_color)
: bg_color_(bg_color),
conn_color_(conn_color) {
buildTileColorTable("standard.gif");
layoutRooms();
buildRoomSurfaces();
composeFinalSurface();
}
// Regenera la surface final con nuevo color de fondo
void MiniMap::rebuild(Uint8 bg_color, Uint8 conn_color) {
bg_color_ = bg_color;
conn_color_ = conn_color;
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<int>(tileset->getWidth()) / Tile::SIZE;
tileset_transparent_ = tileset->getTransparentColor();
int tileset_height = static_cast<int>(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 el color transparente del tileset)
Uint8 transparent = tileset->getTransparentColor();
std::array<int, 256> 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 != transparent) {
freq[pixel]++;
}
}
}
// Encontrar el color más frecuente (transparent = tile vacío, no se pinta)
Uint8 best_color = transparent;
int best_count = 0;
for (int c = 0; c < 256; ++c) {
if (c == transparent) { continue; }
if (freq[c] > best_count) {
best_count = freq[c];
best_color = static_cast<Uint8>(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<std::string, std::shared_ptr<Room::Data>> room_map;
for (const auto& r : rooms) {
room_map[r.name] = r.room;
}
// BFS para posicionar rooms
std::set<std::string> visited;
std::queue<std::pair<std::string, GridPos>> bfs;
// Empezar por la primera room
const std::string& start = rooms[0].name;
bfs.push({start, {.x = 0, .y = 0}});
visited.insert(start);
// Grid ocupado: posición → nombre de room
std::map<std::pair<int, int>, 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<Neighbor, 4> neighbors = {{
{.room = data->upper_room, .dx = 0, .dy = -1},
{.room = data->lower_room, .dx = 0, .dy = 1},
{.room = data->left_room, .dx = -1, .dy = 0},
{.room = data->right_room, .dx = 1, .dy = 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 = {.x = pos.x + dx, .y = pos.y + dy};
auto nkey = std::make_pair(neighbor_pos.x, neighbor_pos.y);
if (!grid_occupied.contains(nkey)) {
visited.insert(neighbor_name);
bfs.emplace(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<Surface> {
auto room_data = Resource::Cache::get()->getRoom(room_name);
if (!room_data) { return nullptr; }
auto surface = std::make_shared<Surface>(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<int>(tile_map.size())) { continue; }
int tile = tile_map[index];
if (tile < 0 || tile >= static_cast<int>(tile_colors_.size())) { continue; }
Uint8 color = tile_colors_[tile];
if (color != tileset_transparent_) {
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<Surface>(map_width_ + SHADOW_OFFSET, map_height_ + SHADOW_OFFSET);
auto prev = Screen::get()->getRendererSurface();
Screen::get()->setRendererSurface(map_surface_);
// 1. Fondo general
map_surface_->clear(bg_color_);
// 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<float>(px), .y = static_cast<float>(py), .w = static_cast<float>(CELL_W), .h = static_cast<float>(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<float>(px), .y = static_cast<float>(py), .w = static_cast<float>(CELL_W), .h = static_cast<float>(CELL_H)};
map_surface_->fillRect(&cell, COLOR_ROOM_BORDER);
// Miniroom dentro del borde
SDL_FRect dst = {.x = static_cast<float>(px + BORDER), .y = static_cast<float>(py + BORDER), .w = static_cast<float>(ROOM_W), .h = static_cast<float>(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
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) - 1;
SDL_FRect line = {.x = static_cast<float>(x1), .y = static_cast<float>(y_mid), .w = static_cast<float>(GAP), .h = 3.0F};
map_surface_->fillRect(&line, conn_color_);
}
// Conexión abajo
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) - 1;
int y1 = py + CELL_H;
SDL_FRect line = {.x = static_cast<float>(x_mid), .y = static_cast<float>(y1), .w = 3.0F, .h = static_cast<float>(GAP)};
map_surface_->fillRect(&line, conn_color_);
}
}
}
// Centra el viewport en una room
void MiniMap::centerOnRoom(const std::string& room_name) {
auto it = room_positions_.find(room_name);
if (it == room_positions_.end()) { return; }
const auto& pos = it->second.pos;
auto room_cx = static_cast<float>(cellPixelX(pos.x) + (CELL_W / 2));
auto room_cy = static_cast<float>(cellPixelY(pos.y) + (CELL_H / 2));
view_x_ = static_cast<float>(PlayArea::WIDTH) / 2.0F - room_cx;
view_y_ = static_cast<float>(PlayArea::HEIGHT) / 2.0F - room_cy;
}
// Devuelve el nombre de la room en una posición de pantalla, o vacío si no hay ninguna
auto MiniMap::roomAtScreen(float screen_x, float screen_y) -> std::string {
// Convertir coordenada de pantalla a coordenada dentro del minimapa
float map_x = screen_x - view_x_;
float map_y = screen_y - view_y_;
for (const auto& [name, mini] : room_positions_) {
auto rx = static_cast<float>(cellPixelX(mini.pos.x));
auto ry = static_cast<float>(cellPixelY(mini.pos.y));
if (map_x >= rx && map_x < rx + CELL_W && map_y >= ry && map_y < ry + CELL_H) {
return name;
}
}
return "";
}
// Renderiza el minimapa
void MiniMap::render(const std::string& current_room) {
if (!map_surface_) { return; }
auto game_surface = Screen::get()->getRendererSurface();
if (!game_surface) { return; }
// Renderizar la surface del minimapa con el viewport actual (alineado a pixel)
float vx = std::floor(view_x_);
float vy = std::floor(view_y_);
SDL_FRect dst = {.x = vx, .y = vy, .w = static_cast<float>(map_width_ + SHADOW_OFFSET), .h = static_cast<float>(map_height_ + SHADOW_OFFSET)};
map_surface_->render(nullptr, &dst);
// Highlight de la room actual (solo si está completamente visible en el play area)
auto it = room_positions_.find(current_room);
if (it != room_positions_.end()) {
float cur_x = vx + static_cast<float>(cellPixelX(it->second.pos.x)) - 1;
float cur_y = vy + static_cast<float>(cellPixelY(it->second.pos.y)) - 1;
auto cur_w = static_cast<float>(CELL_W + 2);
auto cur_h = static_cast<float>(CELL_H + 2);
if (cur_x >= 0 && cur_y >= 0 && cur_x + cur_w <= PlayArea::WIDTH && cur_y + cur_h <= PlayArea::HEIGHT) {
SDL_FRect highlight = {.x = cur_x, .y = cur_y, .w = cur_w, .h = cur_h};
game_surface->drawRectBorder(&highlight, stringToColor("bright_white"));
}
}
}
// Maneja eventos del minimapa (drag para explorar, click para navegar)
void MiniMap::handleEvent(const SDL_Event& event, const std::string& current_room) {
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_LEFT) {
// Guardar posición inicial para detectar si es click o drag
float mouse_x = 0.0F;
float mouse_y = 0.0F;
SDL_GetMouseState(&mouse_x, &mouse_y);
float render_x = 0.0F;
float render_y = 0.0F;
SDL_RenderCoordinatesFromWindow(Screen::get()->getRenderer(), mouse_x, mouse_y, &render_x, &render_y);
SDL_FRect dst_rect = Screen::get()->getGameSurfaceDstRect();
dragging_ = true;
drag_start_x_ = render_x - dst_rect.x;
drag_start_y_ = render_y - dst_rect.y;
view_start_x_ = view_x_;
view_start_y_ = view_y_;
}
if (event.type == SDL_EVENT_MOUSE_MOTION && dragging_) {
float mouse_x = 0.0F;
float mouse_y = 0.0F;
SDL_GetMouseState(&mouse_x, &mouse_y);
float render_x = 0.0F;
float render_y = 0.0F;
SDL_RenderCoordinatesFromWindow(Screen::get()->getRenderer(), mouse_x, mouse_y, &render_x, &render_y);
SDL_FRect dst_rect = Screen::get()->getGameSurfaceDstRect();
float game_x = render_x - dst_rect.x;
float game_y = render_y - dst_rect.y;
view_x_ = view_start_x_ + (game_x - drag_start_x_);
view_y_ = view_start_y_ + (game_y - drag_start_y_);
}
if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && event.button.button == SDL_BUTTON_LEFT) {
if (dragging_) {
// Comprobar si fue click (sin mover) o drag
float mouse_x = 0.0F;
float mouse_y = 0.0F;
SDL_GetMouseState(&mouse_x, &mouse_y);
float render_x = 0.0F;
float render_y = 0.0F;
SDL_RenderCoordinatesFromWindow(Screen::get()->getRenderer(), mouse_x, mouse_y, &render_x, &render_y);
SDL_FRect dst_rect = Screen::get()->getGameSurfaceDstRect();
float game_x = render_x - dst_rect.x;
float game_y = render_y - dst_rect.y;
float dx = game_x - drag_start_x_;
float dy = game_y - drag_start_y_;
bool was_click = (dx * dx + dy * dy) < 4.0F; // Menos de 2px de movimiento = click
if (was_click) {
// Click: navegar a la room bajo el cursor
std::string room = roomAtScreen(game_x, game_y);
if (!room.empty() && room != current_room && on_navigate) {
on_navigate(room);
}
}
dragging_ = false;
}
}
}
#endif // _DEBUG

View File

@@ -0,0 +1,103 @@
#pragma once
#ifdef _DEBUG
#include <SDL3/SDL.h>
#include <functional> // Para function
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <unordered_map> // Para unordered_map
#include <vector> // 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:
explicit MiniMap(Uint8 bg_color = 2, Uint8 conn_color = 14);
~MiniMap() = default;
void render(const std::string& current_room);
void handleEvent(const SDL_Event& event, const std::string& current_room);
void rebuild(Uint8 bg_color, Uint8 conn_color);
void centerOnRoom(const std::string& room_name);
[[nodiscard]] auto isReady() const -> bool { return !room_positions_.empty(); }
// Callback al hacer click en una minihabitación (nombre del room)
std::function<void(const std::string&)> on_navigate;
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> 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<Surface>;
void drawConnections();
auto roomAtScreen(float screen_x, float screen_y) -> std::string;
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<Uint8> tile_colors_; // tile_index → palette color index
int tileset_width_{0}; // Ancho del tileset en tiles
Uint8 tileset_transparent_{16}; // Color transparente del tileset
// Rooms renderizadas y posicionadas
std::unordered_map<std::string, RoomMini> room_positions_;
// Surface final compuesta
std::shared_ptr<Surface> 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};
// Viewport: offset de la surface del minimapa respecto al play area
float view_x_{0.0F}; // Offset X actual
float view_y_{0.0F}; // Offset Y actual
bool dragging_{false};
float drag_start_x_{0.0F}; // Posición del ratón al inicio del drag
float drag_start_y_{0.0F};
float view_start_x_{0.0F}; // Viewport al inicio del drag
float view_start_y_{0.0F};
// 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)
Uint8 bg_color_{2}; // Fondo general (configurable)
Uint8 conn_color_{14}; // Líneas de conexión (configurable)
static constexpr Uint8 COLOR_ROOM_BORDER = 0; // Borde de cada miniroom
static constexpr Uint8 COLOR_SHADOW = 1; // Sombra de cada miniroom
};
#endif // _DEBUG

View File

@@ -0,0 +1,178 @@
#ifdef _DEBUG
#include "game/editor/room_saver.hpp"
#include <cmath> // Para std::round
#include <fstream> // Para ifstream, ofstream, istreambuf_iterator
#include <iostream> // Para cout, cerr
#include <sstream> // Para ostringstream
#include "utils/defines.hpp" // Para Tile::SIZE
// Carga el YAML original directamente del filesystem (no del resource pack)
auto RoomSaver::loadYAML(const std::string& file_path) -> fkyaml::node {
std::ifstream file(file_path);
if (!file.is_open()) {
std::cerr << "RoomSaver: Cannot open " << file_path << "\n";
return {};
}
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
file.close();
return fkyaml::node::deserialize(content);
}
// Convierte una room connection al formato YAML
auto RoomSaver::roomConnectionToYAML(const std::string& connection) -> std::string {
if (connection == "0" || connection.empty()) { return "null"; }
return connection;
}
// Convierte la dirección del conveyor belt a string
auto RoomSaver::conveyorBeltToString(int direction) -> std::string {
if (direction < 0) { return "left"; }
if (direction > 0) { return "right"; }
return "none";
}
// Genera el YAML completo como texto con formato compacto
auto RoomSaver::buildYAML(const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string { // NOLINT(readability-function-cognitive-complexity)
std::ostringstream out;
// --- Cabecera: nombre como comentario ---
out << "# " << room_data.name << "\n";
// --- Sección room ---
out << "room:\n";
// Escribir todos los campos name_* del YAML original (preserva name_ca, name_en, etc.)
if (original_yaml.contains("room")) {
const auto& room_node = original_yaml["room"];
for (auto it = room_node.begin(); it != room_node.end(); ++it) {
const auto KEY = it.key().get_value<std::string>();
if (KEY.substr(0, 5) == "name_") {
out << " " << KEY << ": \"" << it.value().get_value<std::string>() << "\"\n";
}
}
}
out << " bgColor: " << room_data.bg_color << "\n";
out << " border: " << room_data.border_color << "\n";
out << " tileSetFile: " << room_data.tile_set_file << "\n";
// Conexiones
out << "\n";
out << " # Conexiones de la habitación (null = sin conexión)\n";
out << " connections:\n";
out << " up: " << roomConnectionToYAML(room_data.upper_room) << "\n";
out << " down: " << roomConnectionToYAML(room_data.lower_room) << "\n";
out << " left: " << roomConnectionToYAML(room_data.left_room) << "\n";
out << " right: " << roomConnectionToYAML(room_data.right_room) << "\n";
// Colores de items
out << "\n";
out << " # Colores de los objetos\n";
out << " itemColor1: " << (room_data.item_color1.empty() ? "yellow" : room_data.item_color1) << "\n";
out << " itemColor2: " << (room_data.item_color2.empty() ? "magenta" : room_data.item_color2) << "\n";
// Conveyor belt
out << "\n";
out << " # Dirección de la cinta transportadora: left, none, right\n";
out << " conveyorBelt: " << conveyorBeltToString(room_data.conveyor_belt_direction) << "\n";
// --- Tilemap (16 filas × 32 columnas, formato flow) ---
out << "\n";
out << "# Tilemap: 16 filas × 32 columnas (256×192 píxeles @ 8px/tile)\n";
out << "# Índices de tiles (-1 = vacío)\n";
out << "tilemap:\n";
constexpr int MAP_WIDTH = 32;
constexpr int MAP_HEIGHT = 16;
for (int row = 0; row < MAP_HEIGHT; ++row) {
out << " - [";
for (int col = 0; col < MAP_WIDTH; ++col) {
int index = (row * MAP_WIDTH) + col;
if (index < static_cast<int>(room_data.tile_map.size())) {
out << room_data.tile_map[index];
} else {
out << -1;
}
if (col < MAP_WIDTH - 1) { out << ", "; }
}
out << "]\n";
}
// --- Enemigos ---
if (!room_data.enemies.empty()) {
out << "\n";
out << "# Enemigos en esta habitación\n";
out << "enemies:\n";
for (const auto& enemy : room_data.enemies) {
out << " - animation: " << enemy.animation_path << "\n";
int pos_x = static_cast<int>(std::round(enemy.x / Tile::SIZE));
int pos_y = static_cast<int>(std::round(enemy.y / Tile::SIZE));
out << " position: {x: " << pos_x << ", y: " << pos_y << "}\n";
out << " velocity: {x: " << enemy.vx << ", y: " << enemy.vy << "}\n";
int b1_x = enemy.x1 / Tile::SIZE;
int b1_y = enemy.y1 / Tile::SIZE;
int b2_x = enemy.x2 / Tile::SIZE;
int b2_y = enemy.y2 / Tile::SIZE;
out << " boundaries:\n";
out << " position1: {x: " << b1_x << ", y: " << b1_y << "}\n";
out << " position2: {x: " << b2_x << ", y: " << b2_y << "}\n";
if (!enemy.color.empty() && enemy.color != "white") {
out << " color: " << enemy.color << "\n";
}
if (enemy.flip) { out << " flip: true\n"; }
if (enemy.mirror) { out << " mirror: true\n"; }
if (enemy.frame != -1) { out << " frame: " << enemy.frame << "\n"; }
out << "\n";
}
}
// --- Items ---
if (!room_data.items.empty()) {
out << "# Objetos en esta habitación\n";
out << "items:\n";
for (const auto& item : room_data.items) {
out << " - tileSetFile: " << item.tile_set_file << "\n";
out << " tile: " << item.tile << "\n";
int item_x = static_cast<int>(std::round(item.x / Tile::SIZE));
int item_y = static_cast<int>(std::round(item.y / Tile::SIZE));
out << " position: {x: " << item_x << ", y: " << item_y << "}\n";
if (item.counter != 0) {
out << " counter: " << item.counter << "\n";
}
out << "\n";
}
}
return out.str();
}
// Guarda el YAML a disco
auto RoomSaver::saveYAML(const std::string& file_path, const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string {
std::string content = buildYAML(original_yaml, room_data);
std::ofstream file(file_path);
if (!file.is_open()) {
std::cerr << "RoomSaver: Cannot write to " << file_path << "\n";
return "Error: Cannot write to " + file_path;
}
file << content;
file.close();
const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1);
std::cout << "RoomSaver: Saved " << FILE_NAME << "\n";
return "Saved " + FILE_NAME;
}
#endif // _DEBUG

View File

@@ -0,0 +1,35 @@
#pragma once
#ifdef _DEBUG
#include <string> // Para string
#include "external/fkyaml_node.hpp" // Para fkyaml::node
#include "game/gameplay/room.hpp" // Para Room::Data
/**
* @brief Guardado de archivos YAML de habitaciones para el editor de mapas
*
* Lee el YAML original con fkyaml (para acceder a todos los campos: name_ca, name_en, etc.)
* Genera el YAML como texto formateado compacto (idéntico al formato original de los ficheros).
* Solo se usa en builds de debug.
*/
class RoomSaver {
public:
RoomSaver() = delete;
// Carga el YAML original desde disco como nodo fkyaml (lee del filesystem, no del pack)
static auto loadYAML(const std::string& file_path) -> fkyaml::node;
// Genera y guarda el YAML completo a disco
// original_yaml: nodo fkyaml con los datos originales (para campos que no se editan: name_ca, etc.)
// room_data: datos editados (posiciones de enemigos, items, etc.)
static auto saveYAML(const std::string& file_path, const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string;
private:
static auto buildYAML(const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string;
static auto roomConnectionToYAML(const std::string& connection) -> std::string;
static auto conveyorBeltToString(int direction) -> std::string;
};
#endif // _DEBUG

View File

@@ -0,0 +1,240 @@
#ifdef _DEBUG
#include "game/editor/tile_picker.hpp"
#include <algorithm> // Para std::clamp, std::min
#include "core/rendering/screen.hpp" // Para Screen
#include "core/rendering/surface.hpp" // Para Surface
#include "core/resources/resource_cache.hpp" // Para Resource::Cache
#include "utils/defines.hpp" // Para Tile::SIZE, PlayArea
#include "utils/utils.hpp" // Para stringToColor
// Margen del borde alrededor del tileset (en pixels)
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) {
tileset_ = Resource::Cache::get()->getSurface(tileset_name);
if (!tileset_) {
open_ = false;
return;
}
spacing_in_ = tile_spacing_in;
spacing_out_ = tile_spacing_out;
// Calcular dimensiones del tileset en tiles (teniendo en cuenta spacing de entrada)
int src_cell = Tile::SIZE + spacing_in_;
tileset_width_ = static_cast<int>(tileset_->getWidth()) / src_cell;
tileset_height_ = static_cast<int>(tileset_->getHeight()) / src_cell;
// Corregir si el último tile cabe sin spacing
if (tileset_width_ == 0 && tileset_->getWidth() >= Tile::SIZE) { tileset_width_ = 1; }
if (tileset_height_ == 0 && tileset_->getHeight() >= Tile::SIZE) { tileset_height_ = 1; }
current_tile_ = current_tile;
hover_tile_ = -1;
scroll_y_ = 0;
// Dimensiones de salida (con spacing visual entre tiles)
int out_cell = Tile::SIZE + spacing_out_;
int display_w = (tileset_width_ * out_cell) - spacing_out_; // Sin trailing
int display_h = (tileset_height_ * out_cell) - spacing_out_;
// Frame: display + borde
int frame_w = display_w + (BORDER_PAD * 2);
int frame_h = display_h + (BORDER_PAD * 2);
frame_surface_ = std::make_shared<Surface>(frame_w, frame_h);
// Componer: fondo + borde + tiles uno a uno
{
auto prev = Screen::get()->getRendererSurface();
Screen::get()->setRendererSurface(frame_surface_);
Uint8 fill_color = (bg_color >= 0) ? static_cast<Uint8>(bg_color) : stringToColor("black");
frame_surface_->clear(fill_color);
// Borde doble
SDL_FRect outer = {.x = 0, .y = 0, .w = static_cast<float>(frame_w), .h = static_cast<float>(frame_h)};
frame_surface_->drawRectBorder(&outer, stringToColor("bright_white"));
SDL_FRect inner = {.x = 1, .y = 1, .w = static_cast<float>(frame_w - 2), .h = static_cast<float>(frame_h - 2)};
frame_surface_->drawRectBorder(&inner, stringToColor("white"));
// Renderizar cada tile individualmente
constexpr auto TS = static_cast<float>(Tile::SIZE);
for (int row = 0; row < tileset_height_; ++row) {
for (int col = 0; col < tileset_width_; ++col) {
// Fuente: posición en el tileset original
SDL_FRect src = {
.x = static_cast<float>(col * src_cell),
.y = static_cast<float>(row * src_cell),
.w = TS,
.h = TS};
// Destino: posición en el frame con spacing de salida
int dst_x = BORDER_PAD + (col * out_cell);
int dst_y = BORDER_PAD + (row * out_cell);
if (source_color >= 0 && target_color >= 0) {
tileset_->renderWithColorReplace(dst_x, dst_y, static_cast<Uint8>(source_color), static_cast<Uint8>(target_color), &src);
} else {
SDL_FRect dst = {.x = static_cast<float>(dst_x), .y = static_cast<float>(dst_y), .w = TS, .h = TS};
tileset_->render(&src, &dst);
}
}
}
Screen::get()->setRendererSurface(prev);
}
// Centrar en el play area
offset_x_ = (PlayArea::WIDTH - frame_w) / 2;
int offset_y = (PlayArea::HEIGHT - frame_h) / 2;
offset_y = std::max(offset_y, 0);
visible_height_ = PlayArea::HEIGHT;
frame_dst_ = {.x = static_cast<float>(offset_x_), .y = static_cast<float>(offset_y), .w = static_cast<float>(frame_w), .h = static_cast<float>(frame_h)};
// Si el frame es más alto que el play area, necesitará scroll
if (frame_h > visible_height_) {
frame_dst_.y = 0;
if (current_tile_ >= 0) {
int tile_row = current_tile_ / tileset_width_;
int tile_y_px = tile_row * out_cell;
if (tile_y_px > visible_height_ / 2) {
scroll_y_ = tile_y_px - visible_height_ / 2;
}
}
}
open_ = true;
updateMousePosition();
}
// Cierra el picker
void TilePicker::close() {
open_ = false;
tileset_.reset();
frame_surface_.reset();
}
// Renderiza el picker
void TilePicker::render() {
if (!open_ || !frame_surface_) { return; }
auto game_surface = Screen::get()->getRendererSurface();
if (!game_surface) { return; }
int frame_h = static_cast<int>(frame_dst_.h);
if (frame_h <= visible_height_) {
frame_surface_->render(nullptr, &frame_dst_);
} else {
int max_scroll = frame_h - visible_height_;
scroll_y_ = std::clamp(scroll_y_, 0, max_scroll);
SDL_FRect src = {.x = 0, .y = static_cast<float>(scroll_y_), .w = frame_dst_.w, .h = static_cast<float>(visible_height_)};
SDL_FRect dst = {.x = frame_dst_.x, .y = 0, .w = frame_dst_.w, .h = static_cast<float>(visible_height_)};
frame_surface_->render(&src, &dst);
}
// Highlights (en game_surface, encima del frame)
int out_cell = Tile::SIZE + spacing_out_;
float tileset_screen_x = frame_dst_.x + BORDER_PAD;
float tileset_screen_y = frame_dst_.y + BORDER_PAD - static_cast<float>(scroll_y_);
constexpr auto TS = static_cast<float>(Tile::SIZE);
// Highlight del tile bajo el cursor (blanco)
if (hover_tile_ >= 0) {
int col = hover_tile_ % tileset_width_;
int row = hover_tile_ / tileset_width_;
float hx = tileset_screen_x + static_cast<float>(col * out_cell);
float hy = tileset_screen_y + static_cast<float>(row * out_cell);
if (hy >= 0 && hy + TS <= visible_height_) {
SDL_FRect highlight = {.x = hx, .y = hy, .w = TS, .h = TS};
game_surface->drawRectBorder(&highlight, stringToColor("bright_white"));
}
}
// Highlight del tile actual (verde)
if (current_tile_ >= 0) {
int col = current_tile_ % tileset_width_;
int row = current_tile_ / tileset_width_;
float cx = tileset_screen_x + static_cast<float>(col * out_cell);
float cy = tileset_screen_y + static_cast<float>(row * out_cell);
if (cy >= 0 && cy + TS <= visible_height_) {
SDL_FRect cur_rect = {.x = cx, .y = cy, .w = TS, .h = TS};
game_surface->drawRectBorder(&cur_rect, stringToColor("bright_green"));
}
}
}
// Maneja eventos del picker
void TilePicker::handleEvent(const SDL_Event& event) {
if (!open_) { return; }
if (event.type == SDL_EVENT_MOUSE_MOTION) {
updateMousePosition();
}
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();
} else if (event.button.button == SDL_BUTTON_RIGHT) {
close();
}
}
if (event.type == SDL_EVENT_MOUSE_WHEEL) {
scroll_y_ -= static_cast<int>(event.wheel.y) * Tile::SIZE * 2;
int max_scroll = static_cast<int>(frame_dst_.h) - visible_height_;
max_scroll = std::max(max_scroll, 0);
scroll_y_ = std::clamp(scroll_y_, 0, max_scroll);
updateMousePosition();
}
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE) {
close();
}
}
// Calcula qué tile está bajo el cursor
void TilePicker::updateMousePosition() {
float mouse_x = 0.0F;
float mouse_y = 0.0F;
SDL_GetMouseState(&mouse_x, &mouse_y);
float render_x = 0.0F;
float render_y = 0.0F;
SDL_RenderCoordinatesFromWindow(Screen::get()->getRenderer(), mouse_x, mouse_y, &render_x, &render_y);
SDL_FRect dst_rect = Screen::get()->getGameSurfaceDstRect();
float game_x = render_x - dst_rect.x;
float game_y = render_y - dst_rect.y;
// Coordenada relativa al contenido del frame (con scroll)
float rel_x = game_x - frame_dst_.x - BORDER_PAD;
float rel_y = game_y - frame_dst_.y - BORDER_PAD + static_cast<float>(scroll_y_);
// Convertir a tile teniendo en cuenta el spacing de salida
int out_cell = Tile::SIZE + spacing_out_;
int tile_x = static_cast<int>(rel_x) / out_cell;
int tile_y = static_cast<int>(rel_y) / out_cell;
// Verificar que estamos sobre un tile y no sobre el spacing
int local_x = static_cast<int>(rel_x) % out_cell;
int local_y = static_cast<int>(rel_y) % out_cell;
bool on_tile = (local_x < Tile::SIZE && local_y < Tile::SIZE);
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;
} else {
hover_tile_ = -1;
}
}
#endif // _DEBUG

View File

@@ -0,0 +1,64 @@
#pragma once
#ifdef _DEBUG
#include <SDL3/SDL.h>
#include <functional> // Para function
#include <memory> // Para shared_ptr
#include <string> // Para string
class Surface;
/**
* @brief Selector visual de tiles de un tileset
*
* Muestra el tileset centrado en el play area.
* Hover ilumina el tile bajo el cursor.
* Click selecciona el tile y cierra el picker.
* Mouse wheel para scroll si el tileset es más alto que el play area.
* ESC o click derecho para cancelar.
*/
class TilePicker {
public:
TilePicker() = default;
~TilePicker() = default;
// Abre el picker con un tileset
// bg_color: color de fondo del panel (-1 = negro)
// 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);
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<void(int)> on_select;
private:
void updateMousePosition();
bool open_{false};
std::shared_ptr<Surface> tileset_; // Surface del tileset original
std::shared_ptr<Surface> frame_surface_; // Surface compuesta: borde + tileset
SDL_FRect frame_dst_{}; // Posición del frame en pantalla
int tileset_width_{0}; // Ancho del tileset en tiles
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
// Spacing
int spacing_in_{0}; // Spacing en el fichero fuente
int spacing_out_{1}; // Spacing visual al mostrar
// Scroll y posicionamiento
int scroll_y_{0}; // Scroll vertical en pixels
int offset_x_{0}; // Offset X para centrar en pantalla
int visible_height_{0}; // Altura visible en pixels
};
#endif // _DEBUG