#include "game/editor/editor.hpp" #ifdef _DEBUG #include #include #include "core/input/key_config.hpp" #include "core/locale/locale.hpp" #include "core/rendering/notification_presets.hpp" #include "core/rendering/screen.hpp" #include "core/rendering/surface.hpp" #include "core/resources/resource_cache.hpp" #include "core/resources/resource_helper.hpp" #include "core/system/event_buffer.hpp" #include "game/map/tilemap.hpp" #include "utils/defaults.hpp" #include "utils/defines.hpp" namespace { // Geometria del TilePicker en espai canvas. `cols × rows_visible` tiles // encapsulats dins d'un rectangle amb `PICKER_MARGIN` per vora. Com que // el tileset real (p.ex. 32×16 tiles de 16 px = 512×256) no cabria a // 1× dins del canvas 336×240, linealitzem els índexs i els repartim // en una graella que SÍ cap, amb scroll vertical per veure la resta. struct PickerLayout { int x{0}; int y{0}; int w{0}; int h{0}; int cols{0}; int rows_visible{0}; int tile_size{0}; int total_tiles{0}; int max_scroll{0}; }; auto layoutPicker(const Tilemap& tm) -> PickerLayout { PickerLayout lay{}; lay.tile_size = tm.getTileSize(); const int M = Defaults::Editor::PICKER_MARGIN; const int INNER_W = GameCanvas::WIDTH - (2 * M); const int INNER_H = GameCanvas::HEIGHT - (2 * M); if (lay.tile_size <= 0) { return lay; } lay.cols = INNER_W / lay.tile_size; lay.rows_visible = INNER_H / lay.tile_size; lay.x = M; lay.y = M; lay.w = lay.cols * lay.tile_size; lay.h = lay.rows_visible * lay.tile_size; const auto TILESET = Resource::Cache::get().getSurface(tm.getTilesetPath()); if (TILESET != nullptr) { const int TCOLS = TILESET->getWidth() / lay.tile_size; const int TROWS = TILESET->getHeight() / lay.tile_size; lay.total_tiles = TCOLS * TROWS; } const int TOTAL_ROWS = (lay.cols > 0) ? ((lay.total_tiles + lay.cols - 1) / lay.cols) : 0; lay.max_scroll = std::max(0, TOTAL_ROWS - lay.rows_visible); return lay; } } // namespace void Editor::enter() { if (active_) { return; } active_ = true; Notification::notify(Locale::get().text("input.editor_on")); } // cppcheck-suppress constParameterReference // Tilemap& mutable per simetria amb `toggle()` i per futures // invalidacions de caches d'edició, encara que `save()` sigui const. void Editor::exit(Tilemap& tilemap) { if (!active_) { return; } active_ = false; picker_open_ = false; // Resolem la ruta via Resource::Helper perquè en Debug el root de // dades és AEE_SOURCE_DIR (ruta absoluta al repo), no el CWD. Sense // això, el writer obriria `data/maps/pyramid.yaml` relatiu al CWD // i fallaria sempre excepte si el binari es llança des del repo root. const auto ABS_PATH = Resource::Helper::resolve(Defaults::Editor::SAVE_PATH); const bool SAVED = tilemap.save(ABS_PATH); Notification::notify(Locale::get().text(SAVED ? "input.editor_off" : "input.editor_save_error")); } void Editor::toggle(Tilemap& tilemap) { if (active_) { exit(tilemap); } else { enter(); } } void Editor::update(int /*delta_ms*/, Tilemap& tilemap) { if (!active_) { return; } // Posició del ratolí en coordenades del canvas (independent del // zoom de finestra i del mode d'escalat). float mouse_x = 0.0F; float mouse_y = 0.0F; SDL_GetMouseState(&mouse_x, &mouse_y); const auto CP = Screen::get().windowToCanvas(mouse_x, mouse_y); const int CX = static_cast(CP.x); const int CY = static_cast(CP.y); // Consumim tecles pròpies del picker abans que cap altre handler // (GlobalInputs) les vegi. ESC només es consumeix si el picker està // obert — fora d'ell segueix arribant a GlobalInputs per la seva // acció de confirmar-eixida. const SDL_Scancode PICKER_SC = KeyConfig::get().scancode("editor_picker"); for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) { bool consumed = false; if (it->type == SDL_EVENT_KEY_DOWN) { if (PICKER_SC != SDL_SCANCODE_UNKNOWN && it->key.scancode == PICKER_SC) { picker_open_ = !picker_open_; if (picker_open_) { picker_scroll_ = 0; } consumed = true; } else if (picker_open_ && it->key.scancode == SDL_SCANCODE_ESCAPE) { picker_open_ = false; consumed = true; } } if (consumed) { it = EventBuffer::events.erase(it); } else { ++it; } } if (picker_open_) { updatePicker(tilemap, CX, CY); } else { updateMap(tilemap, CX, CY); } } void Editor::updateMap(Tilemap& tilemap, int canvas_x, int canvas_y) { // Cursor → cel·la del tilemap. Fora del mapa deixem -1/-1 perquè // `render()` sàpiga no dibuixar preview i `setTileAt` ignori el clic. const int TS = tilemap.getTileSize(); const int MX = canvas_x - tilemap.getOriginX(); const int MY = canvas_y - tilemap.getOriginY(); cursor_col_ = -1; cursor_row_ = -1; if (TS > 0 && MX >= 0 && MY >= 0) { const int C = MX / TS; const int R = MY / TS; if (C < tilemap.getCols() && R < tilemap.getRows()) { cursor_col_ = C; cursor_row_ = R; } } for (const auto& ev : EventBuffer::events) { if (ev.type != SDL_EVENT_MOUSE_BUTTON_DOWN) { continue; } if (ev.button.button == SDL_BUTTON_LEFT) { if (brush_ >= 0 && cursor_col_ >= 0 && cursor_row_ >= 0) { tilemap.setTileAt(cursor_col_, cursor_row_, brush_); } } else if (ev.button.button == SDL_BUTTON_RIGHT) { brush_ = -1; } } } void Editor::updatePicker(const Tilemap& tilemap, int canvas_x, int canvas_y) { const auto LAY = layoutPicker(tilemap); picker_hover_ = -1; if (canvas_x >= LAY.x && canvas_x < LAY.x + LAY.w && canvas_y >= LAY.y && canvas_y < LAY.y + LAY.h && LAY.tile_size > 0) { const int VC = (canvas_x - LAY.x) / LAY.tile_size; const int VR = (canvas_y - LAY.y) / LAY.tile_size; const int IDX = ((picker_scroll_ + VR) * LAY.cols) + VC; if (IDX < LAY.total_tiles) { picker_hover_ = IDX; } } for (const auto& ev : EventBuffer::events) { if (ev.type == SDL_EVENT_MOUSE_WHEEL) { picker_scroll_ -= static_cast(ev.wheel.y); picker_scroll_ = std::clamp(picker_scroll_, 0, LAY.max_scroll); } else if (ev.type == SDL_EVENT_MOUSE_BUTTON_DOWN && ev.button.button == SDL_BUTTON_LEFT) { if (picker_hover_ >= 0) { brush_ = picker_hover_; picker_open_ = false; } } } } void Editor::render(const Tilemap& tilemap) const { if (!active_) { return; } auto& screen = Screen::get(); constexpr int W = GameCanvas::WIDTH; constexpr int H = GameCanvas::HEIGHT; if (picker_open_) { renderPicker(tilemap); } else if (brush_ >= 0 && cursor_col_ >= 0 && cursor_row_ >= 0) { // Preview del brush sota el cursor (només quan el picker és tancat). const int PX = tilemap.getOriginX() + (cursor_col_ * tilemap.getTileSize()); const int PY = tilemap.getOriginY() + (cursor_row_ * tilemap.getTileSize()); tilemap.renderTilePreview(PX, PY, brush_); } constexpr auto BORDER = Defaults::Editor::BORDER_ARGB; screen.fillRect(0, 0, W, 1, BORDER); screen.fillRect(0, H - 1, W, 1, BORDER); screen.fillRect(0, 0, 1, H, BORDER); screen.fillRect(W - 1, 0, 1, H, BORDER); } void Editor::renderPicker(const Tilemap& tilemap) const { const auto LAY = layoutPicker(tilemap); auto& screen = Screen::get(); screen.fillRect(LAY.x, LAY.y, LAY.w, LAY.h, Defaults::Editor::PICKER_BG_ARGB); for (int vr = 0; vr < LAY.rows_visible; ++vr) { const int LOGICAL_ROW = picker_scroll_ + vr; for (int vc = 0; vc < LAY.cols; ++vc) { const int IDX = (LOGICAL_ROW * LAY.cols) + vc; if (IDX >= LAY.total_tiles) { break; } const int PX = LAY.x + (vc * LAY.tile_size); const int PY = LAY.y + (vr * LAY.tile_size); tilemap.renderTilePreview(PX, PY, IDX); } } if (picker_hover_ >= 0 && LAY.cols > 0) { const int VR = (picker_hover_ / LAY.cols) - picker_scroll_; const int VC = picker_hover_ % LAY.cols; if (VR >= 0 && VR < LAY.rows_visible) { const int PX = LAY.x + (VC * LAY.tile_size); const int PY = LAY.y + (VR * LAY.tile_size); const int TS = LAY.tile_size; constexpr auto C = Defaults::Editor::PICKER_HOVER_ARGB; screen.fillRect(PX, PY, TS, 1, C); screen.fillRect(PX, PY + TS - 1, TS, 1, C); screen.fillRect(PX, PY, 1, TS, C); screen.fillRect(PX + TS - 1, PY, 1, TS, C); } } } #endif // _DEBUG