feat(editor): entity picker amb noms del factory i flag editable per plantilla

This commit is contained in:
JailDesigner committed 2026-05-02 11:07:02 +02:00
1 parent f591aa82fd
commit 94213f9fd6
7 files changed
+204 -5

No files matched your search

+1
View File
@@ -1,5 +1,6 @@
name: door name: door
type: interactable type: interactable
editable: true
sprite: sprite:
path: door.gif path: door.gif
size: [48, 32] size: [48, 32]
+1
View File
@@ -1,5 +1,6 @@
name: hole name: hole
type: static_hazard type: static_hazard
editable: true
sprite: sprite:
path: hole.gif path: hole.gif
size: [16, 16] size: [16, 16]
+1
View File
@@ -1,5 +1,6 @@
name: mummy name: mummy
type: dynamic_hazard type: dynamic_hazard
editable: true
sprite: sprite:
path: mummy.gif path: mummy.gif
size: [16, 32] size: [16, 32]
@@ -1,5 +1,6 @@
name: sarcophagus name: sarcophagus
type: interactable type: interactable
editable: true
sprite: sprite:
path: sarcophagus.gif path: sarcophagus.gif
size: [48, 32] size: [48, 32]
+1
View File
@@ -1,5 +1,6 @@
name: torch name: torch
type: decoration type: decoration
editable: true
sprite: sprite:
path: torch.gif path: torch.gif
size: [16, 16] size: [16, 16]
+177 -5
View File
@@ -14,7 +14,10 @@
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
#include "external/fkyaml_node.hpp"
#include "game/editor/editor_config.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/levels/level_manager.hpp"
#include "game/map/tilemap.hpp" #include "game/map/tilemap.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
@@ -106,6 +109,63 @@ namespace {
return {PX, PY}; 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<int, int> {
const int PX = lay.x + PICKER_BORDER + (vc * lay.pitch);
const int PY = lay.y + PICKER_BORDER + (vr * lay.pitch);
return {PX, PY};
}
} // namespace } // namespace
void Editor::enter() { void Editor::enter() {
@@ -119,9 +179,49 @@ void Editor::enter() {
brush_tile_ = -1; brush_tile_ = -1;
brush_entity_.clear(); brush_entity_.clear();
hand_selected_idx_ = -1; hand_selected_idx_ = -1;
ensureEditableEntities();
Notification::notify(Locale::get().text("input.editor_on")); 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<bool>()) { 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<std::string>();
const auto SIZE = sp["size"];
if (!SIZE.is_sequence() || SIZE.size() < 2) { continue; }
const int FRAME_W = SIZE[0].get_value<int>();
const int FRAME_H = SIZE[1].get_value<int>();
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 // cppcheck-suppress constParameterReference
// Tilemap& mutable per simetria amb `toggle()` i per futures // Tilemap& mutable per simetria amb `toggle()` i per futures
// invalidacions de caches d'edició, encara que `save()` sigui const. // 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) // 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 // (entity placement, hand mode). Mantenim la signatura per estabilitat
// d'API i perquè update() pugui despatxar sense branques especials. // d'API i perquè update() pugui despatxar sense branques especials.
void Editor::updateEntityBrush(int /*canvas_x*/, int /*canvas_y*/) { 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*/) { void Editor::updateHand(int /*canvas_x*/, int /*canvas_y*/) {
// Pas 6: hover, select, move, delete, marc d'entitat seleccionada. // 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*/) { void Editor::updateEntityPicker(int canvas_x, int canvas_y) {
// Pas 4: rejilla d'entitats registrades + click → brush_entity_. const auto LAY = layoutEntityPicker(static_cast<int>(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<int>(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<int>(editable_entities_.size())) {
brush_entity_ = editable_entities_[entity_picker_hover_].name;
mode_ = Mode::ENTITY;
picker_ = Picker::NONE;
}
}
}
} }
void Editor::renderEntityPicker(const Tilemap& /*tilemap*/) const { void Editor::renderEntityPicker(const Tilemap& /*tilemap*/) const {
// Pas 4: render del picker d'entitats. const auto LAY = layoutEntityPicker(static_cast<int>(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<float>(ent.frame_w),
.h = static_cast<float>(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) { void Editor::updateTilePicker(const Tilemap& tilemap, int canvas_x, int canvas_y) {
const auto LAY = layoutPicker(tilemap); const auto LAY = layoutPicker(tilemap);
+22
View File
@@ -3,8 +3,11 @@
#ifdef _DEBUG #ifdef _DEBUG
#include <cstdint> #include <cstdint>
#include <memory>
#include <string> #include <string>
#include <vector>
class Surface;
class Tilemap; class Tilemap;
// Mode editor modal del GameScene. Només disponible en Debug — en Release // 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_scroll_{0};
int entity_picker_hover_{-1}; // Índex a la llista de noms. 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<Surface> 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<EditableEntity> editable_entities_;
// Selecció en Mode::HAND (índex dins de l'EntityManager; -1 = cap). // Selecció en Mode::HAND (índex dins de l'EntityManager; -1 = cap).
int hand_selected_idx_{-1}; int hand_selected_idx_{-1};
}; };