From 2ef5968e0d5c07a0dc1a151510a4bee09b9aebb5 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sat, 2 May 2026 10:58:24 +0200 Subject: [PATCH] =?UTF-8?q?feat(editor):=20m=C3=A0quina=20d'estats=20Mode?= =?UTF-8?q?=20{TILE,ENTITY,HAND}=20+=20Picker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/game/editor/editor.cpp | 191 +++++++++++++++++++++++----------- source/game/editor/editor.hpp | 89 ++++++++++++---- 2 files changed, 196 insertions(+), 84 deletions(-) diff --git a/source/game/editor/editor.cpp b/source/game/editor/editor.cpp index 06bb425..5f6e0ee 100644 --- a/source/game/editor/editor.cpp +++ b/source/game/editor/editor.cpp @@ -111,6 +111,14 @@ namespace { void Editor::enter() { if (active_) { return; } active_ = true; + // Estat de novella: tornem a Mode::TILE sense brush ni picker. Els + // brushs (tile/entity) i la selecció de la mà no es preserven entre + // sessions del editor: cada entrada parteix net. + mode_ = Mode::TILE; + picker_ = Picker::NONE; + brush_tile_ = -1; + brush_entity_.clear(); + hand_selected_idx_ = -1; Notification::notify(Locale::get().text("input.editor_on")); } @@ -120,7 +128,7 @@ void Editor::enter() { void Editor::exit(Tilemap& tilemap) { if (!active_) { return; } active_ = false; - picker_open_ = false; + picker_ = Picker::NONE; // El writer salva sempre sobre la sala actualment carregada (segons // el LevelManager): així el "9" de l'editor desa el que tens davant, // no una constant fixa. Resolem la ruta via Resource::Helper perquè @@ -140,9 +148,54 @@ void Editor::toggle(Tilemap& tilemap) { } } +auto Editor::applyHotkey(int scancode) -> bool { + const SDL_Scancode TILE_PICKER_SC = KeyConfig::get().scancode("editor_tile_picker"); + const SDL_Scancode ENTITY_PICKER_SC = KeyConfig::get().scancode("editor_entity_picker"); + const SDL_Scancode HAND_SC = KeyConfig::get().scancode("editor_hand"); + + if (TILE_PICKER_SC != SDL_SCANCODE_UNKNOWN && scancode == TILE_PICKER_SC) { + picker_ = (picker_ == Picker::TILE) ? Picker::NONE : Picker::TILE; + if (picker_ == Picker::TILE) { tile_picker_scroll_ = 0; } + return true; + } + if (ENTITY_PICKER_SC != SDL_SCANCODE_UNKNOWN && scancode == ENTITY_PICKER_SC) { + picker_ = (picker_ == Picker::ENTITY) ? Picker::NONE : Picker::ENTITY; + if (picker_ == Picker::ENTITY) { entity_picker_scroll_ = 0; } + return true; + } + if (HAND_SC != SDL_SCANCODE_UNKNOWN && scancode == HAND_SC) { + mode_ = Mode::HAND; + picker_ = Picker::NONE; + hand_selected_idx_ = -1; + return true; + } + // ESC només tanca el picker; fora d'ell, deixem que GlobalInputs el + // tracte per a la seva acció de confirmar-eixida. + if (picker_ != Picker::NONE && scancode == SDL_SCANCODE_ESCAPE) { + picker_ = Picker::NONE; + return true; + } + return false; +} + +void Editor::handleHotkeys() { + // Consumim les pròpies abans que cap altre handler (GlobalInputs) les + // veja. La transició concreta de cada tecla viu a `applyHotkey`. + for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) { + const bool IS_KEY = (it->type == SDL_EVENT_KEY_DOWN); + if (IS_KEY && applyHotkey(it->key.scancode)) { + it = EventBuffer::events.erase(it); + } else { + ++it; + } + } +} + void Editor::update(int /*delta_ms*/, Tilemap& tilemap) { if (!active_) { return; } + handleHotkeys(); + // Posició del ratolí en coordenades del canvas (independent del // zoom de finestra i del mode d'escalat). float mouse_x = 0.0F; @@ -152,43 +205,10 @@ void Editor::update(int /*delta_ms*/, Tilemap& tilemap) { 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_tile_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. + // Cursor → cel·la del tilemap. Comú a tots els modes amb mapa visible. const int TS = tilemap.getTileSize(); - const int MX = canvas_x - tilemap.getOriginX(); - const int MY = canvas_y - tilemap.getOriginY(); + const int MX = CX - tilemap.getOriginX(); + const int MY = CY - tilemap.getOriginY(); cursor_col_ = -1; cursor_row_ = -1; if (TS > 0 && MX >= 0 && MY >= 0) { @@ -200,21 +220,63 @@ void Editor::updateMap(Tilemap& tilemap, int canvas_x, int canvas_y) { } } - 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; + // Dispatch: el picker, si està obert, té prioritat sobre el mode. + if (picker_ == Picker::TILE) { + updateTilePicker(tilemap, CX, CY); + } else if (picker_ == Picker::ENTITY) { + updateEntityPicker(CX, CY); + } else { + switch (mode_) { + case Mode::TILE: + updateTileBrush(tilemap, CX, CY); + break; + case Mode::ENTITY: + updateEntityBrush(CX, CY); + break; + case Mode::HAND: + updateHand(CX, CY); + break; } } } -void Editor::updatePicker(const Tilemap& tilemap, int canvas_x, int canvas_y) { +void Editor::updateTileBrush(Tilemap& tilemap, int /*canvas_x*/, int /*canvas_y*/) { + for (const auto& ev : EventBuffer::events) { + if (ev.type != SDL_EVENT_MOUSE_BUTTON_DOWN) { continue; } + if (ev.button.button == SDL_BUTTON_LEFT) { + if (brush_tile_ >= 0 && cursor_col_ >= 0 && cursor_row_ >= 0) { + tilemap.setTileAt(cursor_col_, cursor_row_, brush_tile_); + } + } else if (ev.button.button == SDL_BUTTON_RIGHT) { + brush_tile_ = -1; + } + } +} + +// NOLINTBEGIN(readability-convert-member-functions-to-static) +// Stubs: l'estat i la lògica concreta es construeixen als passos següents +// (entity placement, hand mode). Mantenim la signatura per estabilitat +// d'API i perquè update() pugui despatxar sense branques especials. +void Editor::updateEntityBrush(int /*canvas_x*/, int /*canvas_y*/) { + // Pas 5: col·locació amb snap per tipus + bloqueig de solapaments. +} + +void Editor::updateHand(int /*canvas_x*/, int /*canvas_y*/) { + // Pas 6: hover, select, move, delete, marc d'entitat seleccionada. +} + +void Editor::updateEntityPicker(int /*canvas_x*/, int /*canvas_y*/) { + // Pas 4: rejilla d'entitats registrades + click → brush_entity_. +} + +void Editor::renderEntityPicker(const Tilemap& /*tilemap*/) const { + // Pas 4: render del picker d'entitats. +} +// NOLINTEND(readability-convert-member-functions-to-static) + +void Editor::updateTilePicker(const Tilemap& tilemap, int canvas_x, int canvas_y) { const auto LAY = layoutPicker(tilemap); - picker_hover_ = -1; + tile_picker_hover_ = -1; // Espai útil per als tiles dins de la caixa (descomptat el border 1 px). const int INNER_X = LAY.x + PICKER_BORDER; const int INNER_Y = LAY.y + PICKER_BORDER; @@ -227,19 +289,20 @@ void Editor::updatePicker(const Tilemap& tilemap, int canvas_x, int canvas_y) { const bool IN_SPACING_X = (RX % LAY.pitch) >= LAY.tile_size; const bool IN_SPACING_Y = (RY % LAY.pitch) >= LAY.tile_size; if (!IN_SPACING_X && !IN_SPACING_Y && VC < LAY.cols && VR < LAY.rows_visible) { - const int IDX = ((picker_scroll_ + VR) * LAY.cols) + VC; - if (IDX < LAY.total_tiles) { picker_hover_ = IDX; } + const int IDX = ((tile_picker_scroll_ + VR) * LAY.cols) + VC; + if (IDX < LAY.total_tiles) { tile_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); + tile_picker_scroll_ -= static_cast(ev.wheel.y); + tile_picker_scroll_ = std::clamp(tile_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; + if (tile_picker_hover_ >= 0) { + brush_tile_ = tile_picker_hover_; + mode_ = Mode::TILE; + picker_ = Picker::NONE; } } } @@ -251,13 +314,15 @@ void Editor::render(const Tilemap& tilemap) const { 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) { + if (picker_ == Picker::TILE) { + renderTilePicker(tilemap); + } else if (picker_ == Picker::ENTITY) { + renderEntityPicker(tilemap); + } else if (mode_ == Mode::TILE && brush_tile_ >= 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_); + tilemap.renderTilePreview(PX, PY, brush_tile_); } const auto BORDER = EditorConfig::get().borderArgb(); @@ -267,7 +332,7 @@ void Editor::render(const Tilemap& tilemap) const { screen.fillRect(W - 1, 0, 1, H, BORDER); } -void Editor::renderPicker(const Tilemap& tilemap) const { +void Editor::renderTilePicker(const Tilemap& tilemap) const { const auto LAY = layoutPicker(tilemap); if (LAY.cols <= 0 || LAY.rows_visible <= 0) { return; } auto& screen = Screen::get(); @@ -275,7 +340,7 @@ void Editor::renderPicker(const Tilemap& tilemap) const { screen.fillRect(LAY.x, LAY.y, LAY.w, LAY.h, cfg.pickerBgArgb()); for (int vr = 0; vr < LAY.rows_visible; ++vr) { - const int LOGICAL_ROW = picker_scroll_ + vr; + const int LOGICAL_ROW = tile_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; } @@ -284,9 +349,9 @@ void Editor::renderPicker(const Tilemap& tilemap) const { } } - if (picker_hover_ >= 0) { - const int VR = (picker_hover_ / LAY.cols) - picker_scroll_; - const int VC = picker_hover_ % LAY.cols; + if (tile_picker_hover_ >= 0) { + const int VR = (tile_picker_hover_ / LAY.cols) - tile_picker_scroll_; + const int VC = tile_picker_hover_ % LAY.cols; if (VR >= 0 && VR < LAY.rows_visible) { const auto [PX, PY] = pickerTileOrigin(LAY, VC, VR); const int TS = LAY.tile_size; diff --git a/source/game/editor/editor.hpp b/source/game/editor/editor.hpp index 9b033c1..4cce204 100644 --- a/source/game/editor/editor.hpp +++ b/source/game/editor/editor.hpp @@ -2,6 +2,9 @@ #ifdef _DEBUG +#include +#include + class Tilemap; // Mode editor modal del GameScene. Només disponible en Debug — en Release @@ -11,13 +14,21 @@ class Tilemap; // L'editor NO gestiona la tecla de toggle pròpia (`editor_toggle`): això // ho fa GameScene per poder decidir la coexistència amb la pausa F11 // (mútuament excloents). L'editor s'encarrega de: -// - Estat propi (actiu/no actiu, brush, picker obert). -// - Input del ratolí i tecles internes (`T`, `ESC`). +// - Estat propi (mode actiu, brush per mode, picker obert). +// - Input del ratolí i tecles internes (`T`, `E`, `H`, `Supr`, `ESC`). // - Render del border blau + brush preview + picker. -// - Persistència del tilemap al YAML del repo en sortir. +// - Persistència del tilemap i de les entitats al YAML del repo en sortir. // -// GameScene, quan `active()` és true, salta el tick de jugadors/entitats -// i no els pinta; el tilemap sí es pinta perquè és el que s'està editant. +// Modes del cursor (`Mode`): +// - TILE : el clic esquerre pinta el `brush_tile_` al tilemap. +// - ENTITY : el clic esquerre col·loca una instància de `brush_entity_`. +// - HAND : selecciona/mou/borra entitats existents. +// +// Pickers (`Picker`): +// - TILE_PICKER (`T`) : selecciona índex de tile. +// - ENTITY_PICKER (`E`) : selecciona tipus d'entitat (nom registrat al +// factory). +// Mútuament excloents: obrir un tanca l'altre. class Editor { public: // Entra al mode editor: notifica i marca `active=true`. La congelació @@ -37,29 +48,65 @@ class Editor { // Processa input propi (brush, picker). No-op si no actiu. void update(int delta_ms, Tilemap& tilemap); - // Pinta el border blau i, més endavant, brush preview + picker. - // No-op si no actiu. + // Pinta el border blau i, segons mode i picker, brush preview, marc + // d'entitat seleccionada o el picker. No-op si no actiu. 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; + // Mode del cursor: determina què fa el clic esquerre sobre el mapa. + enum class Mode : std::uint8_t { TILE, + ENTITY, + HAND }; + + // Quin picker està obert (si n'hi ha). Mútuament excloents. + enum class Picker : std::uint8_t { NONE, + TILE, + ENTITY }; + + // Consum de hotkeys pròpies (T, E, H, ESC). Modifica `mode_` / `picker_`. + // Es crida al principi d'`update` perquè altres handlers no vegin + // tecles ja interpretades pel propi editor. + void handleHotkeys(); + + // Aplica una tecla i retorna true si l'editor l'ha consumit. Aïllat + // per mantenir baixa la complexitat cognitiva de `handleHotkeys` — + // l'iteració del buffer queda neta i la lògica de transició viu en + // un sol lloc. + auto applyHotkey(int scancode) -> bool; + + // Branques d'update segons (mode, picker). Només té sentit cridar-les + // des d'`update`. + void updateTileBrush(Tilemap& tilemap, int canvas_x, int canvas_y); + void updateEntityBrush(int canvas_x, int canvas_y); + void updateHand(int canvas_x, int canvas_y); + void updateTilePicker(const Tilemap& tilemap, int canvas_x, int canvas_y); + void updateEntityPicker(int canvas_x, int canvas_y); + + void renderTilePicker(const Tilemap& tilemap) const; + void renderEntityPicker(const Tilemap& tilemap) const; bool active_{false}; + Mode mode_{Mode::TILE}; + Picker picker_{Picker::NONE}; - // 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 del mapa sota el cursor (-1 = fora). + // Cursor en cel·les del tilemap. -1/-1 = fora del mapa. + int cursor_col_{-1}; 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. + + // Brush actiu per Mode::TILE (índex al tileset; -1 = sense brush). + int brush_tile_{-1}; + // Estat del TilePicker. + int tile_picker_scroll_{0}; + int tile_picker_hover_{-1}; + + // Brush actiu per Mode::ENTITY (nom registrat al factory; "" = cap). + std::string brush_entity_; + // Estat de l'EntityPicker. + int entity_picker_scroll_{0}; + int entity_picker_hover_{-1}; // Índex a la llista de noms. + + // Selecció en Mode::HAND (índex dins de l'EntityManager; -1 = cap). + int hand_selected_idx_{-1}; }; #endif // _DEBUG