diff --git a/CMakeLists.txt b/CMakeLists.txt index 86680a8..68f68d6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -118,9 +118,10 @@ set(APP_SOURCES source/game/map/paint_layer.cpp source/game/levels/level_manager.cpp # Mòdul editor (només activa contingut en Debug via `#ifdef _DEBUG`). - # En Release el fitxer compila a un object buit i l'enllaçador no + # En Release els fitxers compilen a object buit i l'enllaçador no # arrossega cap símbol del joc final. source/game/editor/editor.cpp + source/game/editor/editor_config.cpp ${ENTITY_SOURCES} # Fitxer només del binari principal: `loadFromLevel` depèn de # Resource::Cache (→ Surface → GIF → àudio). Separat per no arrossegar diff --git a/data/editor/editor.yaml b/data/editor/editor.yaml new file mode 100644 index 0000000..26fb9b6 --- /dev/null +++ b/data/editor/editor.yaml @@ -0,0 +1,13 @@ +# Tema visual de l'editor de tilemap (només Debug). Carregat per +# EditorConfig a l'arrencada; les claus absents cauen als valors compilats +# a Defaults::Editor (veure source/utils/defaults.hpp). + +picker: + margin: 8 # px entre canvas i caixa del picker + spacing: 1 # px de separació entre tiles dins del picker + +# Colors ARGB en hex. Accepta enter (0xFF2244FF) o cadena ("#FF2244FF"). +colors: + border: "0xFF2244FF" # vora 1 px que envolta canvas i picker + picker_bg: "0xFF101824" # fons del picker + picker_hover: "0xFFFFFFFF" # vora 1 px del tile sota el cursor diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 13b84f6..dc935e7 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -29,6 +29,9 @@ #include "core/system/event_buffer.hpp" #include "core/ui/ui_config.hpp" #include "game/debug_config.hpp" +#ifdef _DEBUG +#include "game/editor/editor_config.hpp" +#endif #include "game/levels/level_manager.hpp" #include "game/options.hpp" #include "game/scenes/game_scene.hpp" @@ -119,6 +122,12 @@ Director::Director() { // data/ui.yaml cau al valor compilat a Defaults::UI. UiConfig::get().load("data/ui.yaml"); +#ifdef _DEBUG + // Tema visual de l'editor de tilemap (Debug-only). Claus absents + // cauen als valors compilats a Defaults::Editor. + EditorConfig::get().load("data/editor/editor.yaml"); +#endif + // Recursos compartits d'UI (paleta + fonts). Es carreguen abans que els // overlays perquè tots els reaprofiten sense duplicar la lògica. Els // noms de font viuen al fitxer data/ui.yaml via UiConfig. diff --git a/source/game/editor/editor.cpp b/source/game/editor/editor.cpp index 431d9f5..6c81f85 100644 --- a/source/game/editor/editor.cpp +++ b/source/game/editor/editor.cpp @@ -14,6 +14,7 @@ #include "core/resources/resource_cache.hpp" #include "core/resources/resource_helper.hpp" #include "core/system/event_buffer.hpp" +#include "game/editor/editor_config.hpp" #include "game/levels/level_manager.hpp" #include "game/map/tilemap.hpp" #include "utils/defaults.hpp" @@ -21,12 +22,20 @@ namespace { + // Gruix del border 1 px del picker: reservat dins del rectangle perquè + // no envaeixi els tiles ni se surti fora de la caixa. La cella efectiva + // de cada tile té `pitch = tile_size + spacing` i la graella s'inicia + // un píxel cap dins de cada vora. + constexpr int PICKER_BORDER = 1; + // Geometria del TilePicker en espai canvas. `cols × rows_visible` tiles - // encapsulats dins d'un rectangle amb `PICKER_MARGIN` per vora. Si el - // tileset cap en amplada (TCOLS·tile_size ≤ INNER_W), en respectem la - // graella natural — els tiles surten ordenats igual que al GIF, només - // amb scroll vertical si cal. Si no, linealitzem els índexs en una - // graella més ampla que sí cap, amb scroll vertical per veure la resta. + // encapsulats dins d'un rectangle amb `picker.margin` per vora i un + // border 1 px que el delimita. Si el tileset cap en amplada (la suma + // de tiles + spacing + border ≤ INNER_W), en respectem la graella + // natural — els tiles surten ordenats igual que al GIF; si no, + // linealitzem els índexs en una graella més ampla que sí cap. Quan el + // contingut cap sense scroll, la caixa s'arrossega a alçada mínima i + // es centra verticalment dins de l'àrea interna. struct PickerLayout { int x{0}; int y{0}; @@ -35,6 +44,8 @@ namespace { int cols{0}; int rows_visible{0}; int tile_size{0}; + int spacing{0}; + int pitch{0}; // tile_size + spacing — pas entre orígens de tiles veïns. int total_tiles{0}; int max_scroll{0}; }; @@ -42,12 +53,20 @@ namespace { auto layoutPicker(const Tilemap& tm) -> PickerLayout { PickerLayout lay{}; lay.tile_size = tm.getTileSize(); - const int M = Defaults::Editor::PICKER_MARGIN; + lay.spacing = std::max(0, EditorConfig::get().pickerSpacing()); + lay.pitch = lay.tile_size + lay.spacing; + const int M = EditorConfig::get().pickerMargin(); const int INNER_W = GameCanvas::WIDTH - (2 * M); const int INNER_H = GameCanvas::HEIGHT - (2 * M); - if (lay.tile_size <= 0) { return lay; } - const int MAX_COLS = INNER_W / lay.tile_size; - lay.rows_visible = INNER_H / lay.tile_size; + if (lay.tile_size <= 0 || lay.pitch <= 0) { return lay; } + + // Espai útil per a tiles, descomptat el border 1 px de cada vora. + // Inversió de N·ts + (N-1)·spacing ≤ AVAIL → N ≤ (AVAIL+spacing)/pitch. + const int AVAIL_W = INNER_W - (2 * PICKER_BORDER); + const int AVAIL_H = INNER_H - (2 * PICKER_BORDER); + const int MAX_COLS = std::max(0, (AVAIL_W + lay.spacing) / lay.pitch); + const int MAX_ROWS = std::max(0, (AVAIL_H + lay.spacing) / lay.pitch); + int tcols = 0; const auto TILESET = Resource::Cache::get().getSurface(tm.getTilesetPath()); if (TILESET != nullptr) { @@ -59,17 +78,34 @@ namespace { // fer servir cols = TCOLS reprodueix exactament la disposició del GIF. const bool FITS_W = (tcols > 0 && tcols <= MAX_COLS); lay.cols = FITS_W ? tcols : MAX_COLS; - lay.w = lay.cols * lay.tile_size; - lay.h = lay.rows_visible * lay.tile_size; - // Centrat horitzontalment quan la graella natural és més estreta; - // verticalment mantenim el marge superior i deixem scroll si cal. - lay.x = M + ((INNER_W - lay.w) / 2); - lay.y = M; + const int TOTAL_ROWS = (lay.cols > 0) ? ((lay.total_tiles + lay.cols - 1) / lay.cols) : 0; + // Si tot el contingut cap sense scroll, encongim la caixa a les + // files reals; així podem centrar-la verticalment. + lay.rows_visible = std::min(MAX_ROWS, TOTAL_ROWS); lay.max_scroll = std::max(0, TOTAL_ROWS - lay.rows_visible); + + // Mida real de la caixa: tiles + spacing intern + 1 px de border per cara. + const int TILES_W = (lay.cols > 0) ? ((lay.cols * lay.tile_size) + ((lay.cols - 1) * lay.spacing)) : 0; + const int TILES_H = (lay.rows_visible > 0) ? ((lay.rows_visible * lay.tile_size) + ((lay.rows_visible - 1) * lay.spacing)) : 0; + lay.w = TILES_W + (2 * PICKER_BORDER); + lay.h = TILES_H + (2 * PICKER_BORDER); + // Centrat horitzontal i vertical dins de l'àrea interna; quan el + // picker arriba a l'alçada màxima, el centrat vertical degenera a + // origen=M (no hi ha espai extra que repartir). + lay.x = M + ((INNER_W - lay.w) / 2); + lay.y = M + ((INNER_H - lay.h) / 2); return lay; } + // Origen (cantonada superior-esquerra) del tile a la cel·la (vc, vr) + // dins del picker. Centralitzat perquè render i hit-test no divergeixin. + auto pickerTileOrigin(const PickerLayout& lay, int vc, int vr) -> std::pair { + const int PX = lay.x + PICKER_BORDER + (vc * lay.pitch); + const int PY = lay.y + PICKER_BORDER + (vr * lay.pitch); + return {PX, PY}; + } + } // namespace void Editor::enter() { @@ -179,11 +215,21 @@ void Editor::updateMap(Tilemap& tilemap, int canvas_x, int canvas_y) { 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; } + // 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; + const int RX = canvas_x - INNER_X; + const int RY = canvas_y - INNER_Y; + if (RX >= 0 && RY >= 0 && LAY.tile_size > 0 && LAY.pitch > 0 && LAY.cols > 0) { + const int VC = RX / LAY.pitch; + const int VR = RY / LAY.pitch; + // Píxels de spacing entre cel·les: el clic cau a la "vora morta". + 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; } + } } for (const auto& ev : EventBuffer::events) { @@ -214,7 +260,7 @@ void Editor::render(const Tilemap& tilemap) const { tilemap.renderTilePreview(PX, PY, brush_); } - constexpr auto BORDER = Defaults::Editor::BORDER_ARGB; + const auto BORDER = EditorConfig::get().borderArgb(); screen.fillRect(0, 0, W, 1, BORDER); screen.fillRect(0, H - 1, W, 1, BORDER); screen.fillRect(0, 0, 1, H, BORDER); @@ -223,34 +269,43 @@ void Editor::render(const Tilemap& tilemap) const { void Editor::renderPicker(const Tilemap& tilemap) const { const auto LAY = layoutPicker(tilemap); + if (LAY.cols <= 0 || LAY.rows_visible <= 0) { return; } auto& screen = Screen::get(); - screen.fillRect(LAY.x, LAY.y, LAY.w, LAY.h, Defaults::Editor::PICKER_BG_ARGB); + const auto& cfg = EditorConfig::get(); + 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; 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); + const auto [PX, PY] = pickerTileOrigin(LAY, vc, vr); tilemap.renderTilePreview(PX, PY, IDX); } } - if (picker_hover_ >= 0 && LAY.cols > 0) { + if (picker_hover_ >= 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 auto [PX, PY] = pickerTileOrigin(LAY, VC, VR); const int TS = LAY.tile_size; - constexpr auto C = Defaults::Editor::PICKER_HOVER_ARGB; + const auto C = cfg.pickerHoverArgb(); 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); } } + + // Border 1 px del picker, dibuixat al damunt del fons i dels tiles + // perquè delimite clarament la caixa també quan hi ha tiles que + // arriben a la vora. + const auto BORDER = cfg.borderArgb(); + screen.fillRect(LAY.x, LAY.y, LAY.w, 1, BORDER); + screen.fillRect(LAY.x, LAY.y + LAY.h - 1, LAY.w, 1, BORDER); + screen.fillRect(LAY.x, LAY.y, 1, LAY.h, BORDER); + screen.fillRect(LAY.x + LAY.w - 1, LAY.y, 1, LAY.h, BORDER); } #endif // _DEBUG diff --git a/source/game/editor/editor_config.cpp b/source/game/editor/editor_config.cpp new file mode 100644 index 0000000..9f696d2 --- /dev/null +++ b/source/game/editor/editor_config.cpp @@ -0,0 +1,85 @@ +#include "game/editor/editor_config.hpp" + +#ifdef _DEBUG + +#include +#include +#include +#include + +#include "core/resources/resource_helper.hpp" +#include "external/fkyaml_node.hpp" +#include "utils/defaults.hpp" + +namespace { + + template + void readField(const fkyaml::node& node, const char* key, T& dst) { + if (node.contains(key)) { dst = node[key].get_value(); } + } + + // Llegeix un color ARGB. Accepta enter (0xFF2244FF al YAML) o cadena + // ("0xFF2244FF" / "#FF2244FF"). std::stoul amb base=0 detecta el prefix + // "0x" automàticament; si el valor és invàlid, deixem `dst` intacte i + // avisem perquè l'error no passi en silenci. + void readArgb(const fkyaml::node& node, const char* key, std::uint32_t& dst) { + if (!node.contains(key)) { return; } + try { + const auto& v = node[key]; + if (v.is_integer()) { + dst = static_cast(v.get_value()); + return; + } + auto s = v.get_value(); + if (!s.empty() && s.front() == '#') { s.erase(0, 1); } + dst = static_cast(std::stoul(s, nullptr, 0)); + } catch (const std::exception& e) { + std::cerr << "EditorConfig: color '" << key << "' invàlid (" << e.what() << ") — ignorat\n"; + } + } + +} // namespace + +EditorConfig::EditorConfig() + : picker_margin_(Defaults::Editor::PICKER_MARGIN), + picker_spacing_(Defaults::Editor::PICKER_SPACING), + border_argb_(Defaults::Editor::BORDER_ARGB), + picker_bg_argb_(Defaults::Editor::PICKER_BG_ARGB), + picker_hover_argb_(Defaults::Editor::PICKER_HOVER_ARGB) { +} + +auto EditorConfig::get() -> EditorConfig& { + static EditorConfig instance_; + return instance_; +} + +void EditorConfig::load(const std::string& file_path) { + auto bytes = Resource::Helper::loadFile(file_path); + if (bytes.empty()) { + std::cerr << "EditorConfig: no s'ha pogut obrir " << file_path << " — s'usaran els defaults compilats\n"; + return; + } + try { + const auto* begin = reinterpret_cast(bytes.data()); + const auto* end = begin + bytes.size(); + auto yaml = fkyaml::node::deserialize(begin, end); + + if (yaml.contains("picker")) { + const auto& p = yaml["picker"]; + readField(p, "margin", picker_margin_); + readField(p, "spacing", picker_spacing_); + } + if (yaml.contains("colors")) { + const auto& c = yaml["colors"]; + readArgb(c, "border", border_argb_); + readArgb(c, "picker_bg", picker_bg_argb_); + readArgb(c, "picker_hover", picker_hover_argb_); + } + + std::cout << "EditorConfig: carregat des de " << file_path << '\n'; + } catch (const std::exception& e) { + std::cerr << "EditorConfig: error parsejant " << file_path << ": " << e.what() << '\n'; + } +} + +#endif // _DEBUG diff --git a/source/game/editor/editor_config.hpp b/source/game/editor/editor_config.hpp new file mode 100644 index 0000000..57673b5 --- /dev/null +++ b/source/game/editor/editor_config.hpp @@ -0,0 +1,39 @@ +#pragma once + +#ifdef _DEBUG + +#include +#include + +// Configuració visual de l'editor de tilemap (Debug-only). Carregada des +// de data/editor/editor.yaml; les claus absents cauen als valors compilats +// a Defaults::Editor. Mateix patró que UiConfig (Meyers singleton + load). +class EditorConfig { + public: + static auto get() -> EditorConfig&; + + EditorConfig(const EditorConfig&) = delete; + EditorConfig(EditorConfig&&) = delete; + auto operator=(const EditorConfig&) -> EditorConfig& = delete; + auto operator=(EditorConfig&&) -> EditorConfig& = delete; + + void load(const std::string& file_path); + + [[nodiscard]] auto pickerMargin() const -> int { return picker_margin_; } + [[nodiscard]] auto pickerSpacing() const -> int { return picker_spacing_; } + [[nodiscard]] auto borderArgb() const -> std::uint32_t { return border_argb_; } + [[nodiscard]] auto pickerBgArgb() const -> std::uint32_t { return picker_bg_argb_; } + [[nodiscard]] auto pickerHoverArgb() const -> std::uint32_t { return picker_hover_argb_; } + + private: + EditorConfig(); + ~EditorConfig() = default; + + int picker_margin_; + int picker_spacing_; + std::uint32_t border_argb_; + std::uint32_t picker_bg_argb_; + std::uint32_t picker_hover_argb_; +}; + +#endif // _DEBUG diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index f182fa1..83a4e2b 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -228,6 +228,10 @@ namespace Defaults { // les vores. Prou perquè el border blau de l'editor no es // superpose amb la graella del picker. constexpr int PICKER_MARGIN = 8; + // Píxels de separació entre tiles dins de la graella del picker. + // 1 px deixa veure clarament on acaba un tile i on comença l'altre, + // útil sobretot quan dos tiles veïns tenen colors molt similars. + constexpr int PICKER_SPACING = 1; // 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;