diff --git a/source/game/editor/editor.cpp b/source/game/editor/editor.cpp index 9a78b47..416674a 100644 --- a/source/game/editor/editor.cpp +++ b/source/game/editor/editor.cpp @@ -4,14 +4,64 @@ #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/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; @@ -24,6 +74,7 @@ void Editor::enter() { void Editor::exit(Tilemap& tilemap) { if (!active_) { return; } active_ = false; + picker_open_ = false; const bool SAVED = tilemap.save(Defaults::Editor::SAVE_PATH); Notification::notify(Locale::get().text(SAVED ? "input.editor_off" : "input.editor_save_error")); } @@ -39,33 +90,63 @@ void Editor::toggle(Tilemap& tilemap) { void Editor::update(int /*delta_ms*/, Tilemap& tilemap) { if (!active_) { return; } - // 1. Cursor → cel·la. Convertim la posició del ratolí (espai finestra) - // a coordenades del canvas amb Screen::windowToCanvas, i d'allí al - // sistema de cel·les del tilemap restant l'origin i dividint pel - // tile_size. Fora del mapa → cursor_col/row = -1. + // 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 CANVAS_PT = Screen::get().windowToCanvas(mouse_x, mouse_y); - const int CX = static_cast(CANVAS_PT.x) - tilemap.getOriginX(); - const int CY = static_cast(CANVAS_PT.y) - tilemap.getOriginY(); + 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(); - int col = -1; - int row = -1; - if (TS > 0 && CX >= 0 && CY >= 0) { - const int C = CX / TS; - const int R = CY / TS; + 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()) { - col = C; - row = R; + cursor_col_ = C; + cursor_row_ = R; } } - cursor_col_ = col; - cursor_row_ = row; - // 2. Clicks: només per-esdeveniment (no per estat). Un clic = un - // tile col·locat o brush netejat. El picker encara no existeix, així - // que el right-click no obre res: només neteja el brush. for (const auto& ev : EventBuffer::events) { if (ev.type != SDL_EVENT_MOUSE_BUTTON_DOWN) { continue; } if (ev.button.button == SDL_BUTTON_LEFT) { @@ -78,26 +159,81 @@ void Editor::update(int /*delta_ms*/, Tilemap& tilemap) { } } +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; - constexpr auto BORDER = Defaults::Editor::BORDER_ARGB; - // Preview del brush: blitejem el tile seleccionat a la cel·la sota - // el cursor, abans del border, així el border no el talla si caiguera - // al perímetre. Cap efecte si no hi ha brush o el cursor està fora. - if (brush_ >= 0 && cursor_col_ >= 0 && cursor_row_ >= 0) { + 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_); } - screen.fillRect(0, 0, W, 1, BORDER); // top - screen.fillRect(0, H - 1, W, 1, BORDER); // bottom - screen.fillRect(0, 0, 1, H, BORDER); // left - screen.fillRect(W - 1, 0, 1, H, BORDER); // right + 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 diff --git a/source/game/editor/editor.hpp b/source/game/editor/editor.hpp index 3e442f0..9b033c1 100644 --- a/source/game/editor/editor.hpp +++ b/source/game/editor/editor.hpp @@ -42,15 +42,24 @@ class Editor { void render(const Tilemap& tilemap) const; private: + // Les branques d'update / render segons el mode (picker obert o + // pintura al mapa) queden separades per claredat; no formen part + // de l'API pública perquè només té sentit cridar-les des d'`update`. + void updateMap(Tilemap& tilemap, int canvas_x, int canvas_y); + void updatePicker(const Tilemap& tilemap, int canvas_x, int canvas_y); + void renderPicker(const Tilemap& tilemap) const; + bool active_{false}; // Estat de l'edició (brush + cursor + picker). Es poblen al pas // següent; els declarem ara perquè `update()` pugui mutar-los i les // signatures queden estables entre hitos. int brush_{-1}; // Índex de tile al tileset; -1 = sense brush. - int cursor_col_{-1}; // Cel·la sota el cursor (-1 = fora). + int cursor_col_{-1}; // Cel·la del mapa sota el cursor (-1 = fora). int cursor_row_{-1}; bool picker_open_{false}; + int picker_scroll_{0}; // Fila superior visible del picker (linealitzat). + int picker_hover_{-1}; // Índex del tile sota el cursor dins del picker. }; #endif // _DEBUG diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index 0ea9db0..f906fca 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -164,6 +164,15 @@ namespace Defaults { constexpr std::uint32_t BORDER_ARGB = 0xFF2244FFU; // Ruta de persistència: writer surgical sobre la font del repo. constexpr const char* SAVE_PATH = "data/maps/pyramid.yaml"; + // Píxels entre el canvas i el rectangle del TilePicker per totes + // les vores. Prou perquè el border blau de l'editor no es + // superpose amb la graella del picker. + constexpr int PICKER_MARGIN = 8; + // Fons del TilePicker (blau fosc, prou contrastat amb els tiles + // de color clar i clarament diferent del canvas). + constexpr std::uint32_t PICKER_BG_ARGB = 0xFF101824U; + // Vora 1 px del tile sota el cursor al picker. + constexpr std::uint32_t PICKER_HOVER_ARGB = 0xFFFFFFFFU; } // namespace Editor #endif