feat(editor): entity picker amb noms del factory i flag editable per plantilla
This commit is contained in:
1 parent
f591aa82fd
commit
94213f9fd6
7 files changed
+204
-5
No files matched your search
@@ -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,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,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,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]
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user