diff --git a/data/entities/door/door.yaml b/data/entities/door/door.yaml index 0b041e0..b69bf66 100644 --- a/data/entities/door/door.yaml +++ b/data/entities/door/door.yaml @@ -1,5 +1,6 @@ name: door type: interactable +editable: true sprite: path: door.gif size: [48, 32] diff --git a/data/entities/hole/hole.yaml b/data/entities/hole/hole.yaml index 59c6153..900f59e 100644 --- a/data/entities/hole/hole.yaml +++ b/data/entities/hole/hole.yaml @@ -1,5 +1,6 @@ name: hole type: static_hazard +editable: true sprite: path: hole.gif size: [16, 16] diff --git a/data/entities/mummy/mummy.yaml b/data/entities/mummy/mummy.yaml index 3090dc0..ef41d5b 100644 --- a/data/entities/mummy/mummy.yaml +++ b/data/entities/mummy/mummy.yaml @@ -1,5 +1,6 @@ name: mummy type: dynamic_hazard +editable: true sprite: path: mummy.gif size: [16, 32] diff --git a/data/entities/sarcophagus/sarcophagus.yaml b/data/entities/sarcophagus/sarcophagus.yaml index ab2e2ff..701e0f3 100644 --- a/data/entities/sarcophagus/sarcophagus.yaml +++ b/data/entities/sarcophagus/sarcophagus.yaml @@ -1,5 +1,6 @@ name: sarcophagus type: interactable +editable: true sprite: path: sarcophagus.gif size: [48, 32] diff --git a/data/entities/torch/torch.yaml b/data/entities/torch/torch.yaml index b659ead..215b45f 100644 --- a/data/entities/torch/torch.yaml +++ b/data/entities/torch/torch.yaml @@ -1,5 +1,6 @@ name: torch type: decoration +editable: true sprite: path: torch.gif size: [16, 16] diff --git a/source/game/editor/editor.cpp b/source/game/editor/editor.cpp index 5f6e0ee..8f38335 100644 --- a/source/game/editor/editor.cpp +++ b/source/game/editor/editor.cpp @@ -14,7 +14,10 @@ #include "core/resources/resource_cache.hpp" #include "core/resources/resource_helper.hpp" #include "core/system/event_buffer.hpp" +#include "external/fkyaml_node.hpp" #include "game/editor/editor_config.hpp" +#include "game/entities/entity_factory.hpp" +#include "game/entities/entity_loader.hpp" #include "game/levels/level_manager.hpp" #include "game/map/tilemap.hpp" #include "utils/defaults.hpp" @@ -106,6 +109,63 @@ namespace { return {PX, PY}; } + // Mida fixa de cada cel·la del picker d'entitats. Suficient per als + // sprites més grans del catàleg actual (door 48×32, sarc 48×32) sense + // escalar — el sprite es centra dins de la cel·la. + constexpr int ENTITY_CELL = 48; + + // Geometria de l'EntityPicker. Anàloga a `PickerLayout` però amb + // cel·les fixes ENTITY_CELL × ENTITY_CELL: les entitats tenen + // dimensions diverses, així la rejilla queda visualment regular i + // el sprite es centra dins de la cel·la al render. + struct EntityPickerLayout { + int x{0}; + int y{0}; + int w{0}; + int h{0}; + int cols{0}; + int rows_visible{0}; + int spacing{0}; + int pitch{0}; // ENTITY_CELL + spacing — pas entre cel·les veïnes. + int total_items{0}; + int max_scroll{0}; + }; + + auto layoutEntityPicker(int total_items) -> EntityPickerLayout { + EntityPickerLayout lay{}; + lay.spacing = std::max(0, EditorConfig::get().pickerSpacing()); + lay.pitch = ENTITY_CELL + lay.spacing; + const int M = EditorConfig::get().pickerMargin(); + const int INNER_W = GameCanvas::WIDTH - (2 * M); + const int INNER_H = GameCanvas::HEIGHT - (2 * M); + lay.total_items = std::max(0, total_items); + if (lay.pitch <= 0 || lay.total_items == 0) { return lay; } + + const int AVAIL_W = INNER_W - (2 * PICKER_BORDER); + const int AVAIL_H = INNER_H - (2 * PICKER_BORDER); + const int MAX_COLS = std::max(1, (AVAIL_W + lay.spacing) / lay.pitch); + const int MAX_ROWS = std::max(1, (AVAIL_H + lay.spacing) / lay.pitch); + + lay.cols = std::min(MAX_COLS, lay.total_items); + const int TOTAL_ROWS = (lay.cols > 0) ? ((lay.total_items + lay.cols - 1) / lay.cols) : 0; + lay.rows_visible = std::min(MAX_ROWS, TOTAL_ROWS); + lay.max_scroll = std::max(0, TOTAL_ROWS - lay.rows_visible); + + const int CELLS_W = (lay.cols * ENTITY_CELL) + ((lay.cols - 1) * lay.spacing); + const int CELLS_H = (lay.rows_visible > 0) ? ((lay.rows_visible * ENTITY_CELL) + ((lay.rows_visible - 1) * lay.spacing)) : 0; + lay.w = CELLS_W + (2 * PICKER_BORDER); + lay.h = CELLS_H + (2 * PICKER_BORDER); + lay.x = M + ((INNER_W - lay.w) / 2); + lay.y = M + ((INNER_H - lay.h) / 2); + return lay; + } + + auto entityCellOrigin(const EntityPickerLayout& 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() { @@ -119,9 +179,49 @@ void Editor::enter() { brush_tile_ = -1; brush_entity_.clear(); hand_selected_idx_ = -1; + ensureEditableEntities(); Notification::notify(Locale::get().text("input.editor_on")); } +void Editor::ensureEditableEntities() { + if (!editable_entities_.empty()) { return; } + // Recorrem tots els noms registrats al factory i ens quedem amb els + // que la seva plantilla YAML marca com a `editable: true`. Carreguem + // el surface al cache aquí mateix perquè render/picker el tinguen + // disponible sense fer I/O al pintat. + auto& cache = Resource::Cache::get(); + auto names = EntityFactory::instance().registeredNames(); + std::ranges::sort(names); + editable_entities_.reserve(names.size()); + for (const auto& name : names) { + fkyaml::node tmpl; + try { + tmpl = EntityLoader::loadTemplate(name); + } catch (const std::exception&) { + continue; + } + if (!tmpl.contains("editable") || !tmpl["editable"].get_value()) { continue; } + if (!tmpl.contains("sprite")) { continue; } + const auto& sp = tmpl["sprite"]; + if (!sp.contains("path") || !sp.contains("size")) { continue; } + + const auto SPRITE_PATH = "data/entities/" + name + "/" + sp["path"].get_value(); + const auto SIZE = sp["size"]; + if (!SIZE.is_sequence() || SIZE.size() < 2) { continue; } + const int FRAME_W = SIZE[0].get_value(); + const int FRAME_H = SIZE[1].get_value(); + + auto surface = cache.getSurface(SPRITE_PATH); + if (surface == nullptr) { + cache.loadSurface(SPRITE_PATH, SPRITE_PATH); + surface = cache.getSurface(SPRITE_PATH); + } + if (surface == nullptr) { continue; } + + editable_entities_.push_back({.name = name, .sprite = surface, .frame_w = FRAME_W, .frame_h = FRAME_H}); + } +} + // cppcheck-suppress constParameterReference // Tilemap& mutable per simetria amb `toggle()` i per futures // invalidacions de caches d'edició, encara que `save()` sigui const. @@ -254,7 +354,7 @@ void Editor::updateTileBrush(Tilemap& tilemap, int /*canvas_x*/, int /*canvas_y* } // NOLINTBEGIN(readability-convert-member-functions-to-static) -// Stubs: l'estat i la lògica concreta es construeixen als passos següents +// Stubs: la lògica concreta es construeix 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*/) { @@ -264,15 +364,87 @@ void Editor::updateEntityBrush(int /*canvas_x*/, int /*canvas_y*/) { void Editor::updateHand(int /*canvas_x*/, int /*canvas_y*/) { // Pas 6: hover, select, move, delete, marc d'entitat seleccionada. } +// NOLINTEND(readability-convert-member-functions-to-static) -void Editor::updateEntityPicker(int /*canvas_x*/, int /*canvas_y*/) { - // Pas 4: rejilla d'entitats registrades + click → brush_entity_. +void Editor::updateEntityPicker(int canvas_x, int canvas_y) { + const auto LAY = layoutEntityPicker(static_cast(editable_entities_.size())); + entity_picker_hover_ = -1; + 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.pitch > 0 && LAY.cols > 0) { + const int VC = RX / LAY.pitch; + const int VR = RY / LAY.pitch; + const bool IN_SPACING_X = (RX % LAY.pitch) >= ENTITY_CELL; + const bool IN_SPACING_Y = (RY % LAY.pitch) >= ENTITY_CELL; + if (!IN_SPACING_X && !IN_SPACING_Y && VC < LAY.cols && VR < LAY.rows_visible) { + const int IDX = ((entity_picker_scroll_ + VR) * LAY.cols) + VC; + if (IDX < LAY.total_items) { entity_picker_hover_ = IDX; } + } + } + + for (const auto& ev : EventBuffer::events) { + if (ev.type == SDL_EVENT_MOUSE_WHEEL) { + entity_picker_scroll_ -= static_cast(ev.wheel.y); + entity_picker_scroll_ = std::clamp(entity_picker_scroll_, 0, LAY.max_scroll); + } else if (ev.type == SDL_EVENT_MOUSE_BUTTON_DOWN && ev.button.button == SDL_BUTTON_LEFT) { + if (entity_picker_hover_ >= 0 && entity_picker_hover_ < static_cast(editable_entities_.size())) { + brush_entity_ = editable_entities_[entity_picker_hover_].name; + mode_ = Mode::ENTITY; + picker_ = Picker::NONE; + } + } + } } void Editor::renderEntityPicker(const Tilemap& /*tilemap*/) const { - // Pas 4: render del picker d'entitats. + const auto LAY = layoutEntityPicker(static_cast(editable_entities_.size())); + if (LAY.cols <= 0 || LAY.rows_visible <= 0) { return; } + auto& screen = Screen::get(); + 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 = entity_picker_scroll_ + vr; + for (int vc = 0; vc < LAY.cols; ++vc) { + const int IDX = (LOGICAL_ROW * LAY.cols) + vc; + if (IDX >= LAY.total_items) { break; } + const auto& ent = editable_entities_[IDX]; + const auto [PX, PY] = entityCellOrigin(LAY, vc, vr); + // Frame 0 del strip: clip [0,0] - [frame_w, frame_h] dins de + // la cel·la. Centrem el sprite si és més petit que la cel·la. + const int OX = PX + ((ENTITY_CELL - ent.frame_w) / 2); + const int OY = PY + ((ENTITY_CELL - ent.frame_h) / 2); + const SDL_FRect SRC{ + .x = 0.0F, + .y = 0.0F, + .w = static_cast(ent.frame_w), + .h = static_cast(ent.frame_h), + }; + ent.sprite->render(OX, OY, &SRC, SDL_FLIP_NONE); + } + } + + if (entity_picker_hover_ >= 0) { + const int VR = (entity_picker_hover_ / LAY.cols) - entity_picker_scroll_; + const int VC = entity_picker_hover_ % LAY.cols; + if (VR >= 0 && VR < LAY.rows_visible) { + const auto [PX, PY] = entityCellOrigin(LAY, VC, VR); + const auto C = cfg.pickerHoverArgb(); + screen.fillRect(PX, PY, ENTITY_CELL, 1, C); + screen.fillRect(PX, PY + ENTITY_CELL - 1, ENTITY_CELL, 1, C); + screen.fillRect(PX, PY, 1, ENTITY_CELL, C); + screen.fillRect(PX + ENTITY_CELL - 1, PY, 1, ENTITY_CELL, C); + } + } + + 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); } -// NOLINTEND(readability-convert-member-functions-to-static) void Editor::updateTilePicker(const Tilemap& tilemap, int canvas_x, int canvas_y) { const auto LAY = layoutPicker(tilemap); diff --git a/source/game/editor/editor.hpp b/source/game/editor/editor.hpp index 4cce204..e99d645 100644 --- a/source/game/editor/editor.hpp +++ b/source/game/editor/editor.hpp @@ -3,8 +3,11 @@ #ifdef _DEBUG #include +#include #include +#include +class Surface; class Tilemap; // Mode editor modal del GameScene. Només disponible en Debug — en Release @@ -105,6 +108,25 @@ class Editor { int entity_picker_scroll_{0}; int entity_picker_hover_{-1}; // Índex a la llista de noms. + // Entrada editable del picker. Cada item recorda el nom (clau de + // factory + YAML), el surface del sprite (frame 0 com a icona) i la + // mida de cada frame al GIF — necessària per clipar el primer frame + // quan el GIF és un strip multi-frame. + struct EditableEntity { + std::string name; + std::shared_ptr sprite; + int frame_w{0}; + int frame_h{0}; + }; + + // Construïda lazy al primer enter() i preservada entre sessions: + // el cost és I/O + parse de N plantilles YAML, però `EntityLoader` + // ja les memoritza, així que rellegir només resoldria per sortida i + // entrada al editor — no val la pena. + void ensureEditableEntities(); + + std::vector editable_entities_; + // Selecció en Mode::HAND (índex dins de l'EntityManager; -1 = cap). int hand_selected_idx_{-1}; };