feat(entities): afegeix stone i column com a muros decoratius
This commit is contained in:
1 parent
1c49248b72
commit
6749780356
18 files changed
+448
-36
No files matched your search
@@ -176,6 +176,23 @@ namespace {
|
||||
return {PX, PY};
|
||||
}
|
||||
|
||||
// Compta sub-sprites del atlas i decideix si el template demana
|
||||
// sub-picker de frames. Extret de `ensureEditableEntities` per acotar
|
||||
// la complexitat cognitiva del callsite. Per a strips d'1 frame
|
||||
// (la majoria del catàleg) retorna (1, false).
|
||||
struct AtlasVariants {
|
||||
int frame_count;
|
||||
bool has_variants;
|
||||
};
|
||||
|
||||
auto computeAtlasVariants(const Surface& surface, int frame_w, int frame_h, const fkyaml::node& tmpl) -> AtlasVariants {
|
||||
const int COLS = (frame_w > 0) ? (surface.getWidth() / frame_w) : 1;
|
||||
const int ROWS = (frame_h > 0) ? (surface.getHeight() / frame_h) : 1;
|
||||
const int FRAME_COUNT = std::max(1, COLS * ROWS);
|
||||
const bool FLAG = tmpl.contains("editable_variants") && tmpl["editable_variants"].get_value<bool>();
|
||||
return {.frame_count = FRAME_COUNT, .has_variants = FLAG && FRAME_COUNT > 1};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Editor::enter() {
|
||||
@@ -190,6 +207,10 @@ void Editor::enter() {
|
||||
picker_ = Picker::NONE;
|
||||
brush_tile_ = -1;
|
||||
brush_entity_.clear();
|
||||
brush_entity_frame_ = -1;
|
||||
entity_frame_picker_source_ = nullptr;
|
||||
entity_frame_picker_hover_ = -1;
|
||||
entity_frame_picker_scroll_ = 0;
|
||||
hand_selected_idx_ = -1;
|
||||
hand_hover_idx_ = -1;
|
||||
ensureEditableEntities();
|
||||
@@ -256,7 +277,15 @@ void Editor::ensureEditableEntities() {
|
||||
}
|
||||
if (surface == nullptr) { continue; }
|
||||
|
||||
editable_entities_.push_back({.name = name, .sprite = surface, .frame_w = FRAME_W, .frame_h = FRAME_H});
|
||||
const auto [FRAME_COUNT, HAS_VARIANTS] = computeAtlasVariants(*surface, FRAME_W, FRAME_H, tmpl);
|
||||
editable_entities_.push_back({
|
||||
.name = name,
|
||||
.sprite = surface,
|
||||
.frame_w = FRAME_W,
|
||||
.frame_h = FRAME_H,
|
||||
.frame_count = FRAME_COUNT,
|
||||
.has_variants = HAS_VARIANTS,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,6 +330,13 @@ namespace {
|
||||
s.col = (static_cast<int>(R.x) - Map::tilemap_origin_x) / Map::tile_size;
|
||||
s.row = (static_cast<int>(R.y) - Map::tilemap_origin_y) / Map::tile_size;
|
||||
}
|
||||
// Variant del atlas: si l'entitat té un AnimationComponent
|
||||
// d'un sol frame, és una entitat amb variant escollida
|
||||
// (debris, columna). Persistim l'índex perquè la pròxima
|
||||
// càrrega restauri la mateixa cara del sub-sprite.
|
||||
if (const auto& anim = e->getAnimation(); anim.has_value() && anim->frameCount() == 1) {
|
||||
s.frame = anim->currentFrame();
|
||||
}
|
||||
}
|
||||
out.push_back(std::move(s));
|
||||
}
|
||||
@@ -374,6 +410,7 @@ auto Editor::applyHotkey(int scancode) -> bool {
|
||||
// tracte per a la seva acció de confirmar-eixida.
|
||||
if (picker_ != Picker::NONE && scancode == SDL_SCANCODE_ESCAPE) {
|
||||
picker_ = Picker::NONE;
|
||||
entity_frame_picker_source_ = nullptr;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -426,6 +463,8 @@ void Editor::update(int /*delta_ms*/, Tilemap& tilemap, EntityManager& entities)
|
||||
updateTilePicker(tilemap, CX, CY);
|
||||
} else if (picker_ == Picker::ENTITY) {
|
||||
updateEntityPicker(CX, CY);
|
||||
} else if (picker_ == Picker::ENTITY_FRAME) {
|
||||
updateEntityFramePicker(CX, CY);
|
||||
} else {
|
||||
switch (mode_) {
|
||||
case Mode::TILE:
|
||||
@@ -514,7 +553,9 @@ namespace {
|
||||
|
||||
// Composa el spawn YAML per al brush actual. Retorna empty si el snap
|
||||
// no és vàlid (fora de rejilla, solapament, o duplicat de porta).
|
||||
auto buildSpawnYaml(const std::string& brush, int cursor_col, int cursor_row, const std::vector<Entity*>& existing) -> std::string {
|
||||
// `frame` (-1 = no emetre) afegeix el camp `frame: N` per a entitats
|
||||
// amb variants estàtiques (debris, columna).
|
||||
auto buildSpawnYaml(const std::string& brush, int cursor_col, int cursor_row, const std::vector<Entity*>& existing, int frame) -> std::string {
|
||||
std::ostringstream y;
|
||||
y << "type: " << brush << '\n';
|
||||
if (brush == "sarcophagus") {
|
||||
@@ -530,6 +571,7 @@ namespace {
|
||||
const auto DEST = tileCellRect(cursor_col, cursor_row, Map::tile_size);
|
||||
if (cellOccupied(existing, DEST)) { return {}; }
|
||||
y << "position: [" << cursor_col << ", " << cursor_row << "]\n";
|
||||
if (frame >= 0) { y << "frame: " << frame << '\n'; }
|
||||
return y.str();
|
||||
}
|
||||
|
||||
@@ -546,6 +588,7 @@ void Editor::updateEntityBrush(EntityManager& entities, int /*canvas_x*/, int /*
|
||||
// H (mà): manté la simetria amb el comportament del brush
|
||||
// de tiles.
|
||||
brush_entity_.clear();
|
||||
brush_entity_frame_ = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -665,7 +708,14 @@ auto Editor::tryMoveSelected(const Tilemap& tilemap, EntityManager& entities) co
|
||||
filtered.reserve(list.size() - 1);
|
||||
std::ranges::copy_if(list, std::back_inserter(filtered), [target](const Entity* e) { return e != target; });
|
||||
|
||||
const auto STR = buildSpawnYaml(TYPE, cursor_col_, cursor_row_, filtered);
|
||||
// Si l'entitat moguda té una variant fixada (AnimationComponent d'un
|
||||
// sol frame, convenció per a debris/columnes), la preservem al spawn
|
||||
// reconstruït — així moure no perd la cara escollida.
|
||||
int frame = -1;
|
||||
if (const auto& anim = target->getAnimation(); anim.has_value() && anim->frameCount() == 1) {
|
||||
frame = anim->currentFrame();
|
||||
}
|
||||
const auto STR = buildSpawnYaml(TYPE, cursor_col_, cursor_row_, filtered, frame);
|
||||
if (STR.empty()) { return false; }
|
||||
if (!entities.removeEntity(target)) { return false; }
|
||||
try {
|
||||
@@ -806,7 +856,7 @@ void Editor::renderEntityFrame(const Entity& e, std::uint32_t argb) {
|
||||
|
||||
auto Editor::tryPlaceEntity(EntityManager& entities) -> bool {
|
||||
if (brush_entity_.empty() || cursor_col_ < 0 || cursor_row_ < 0) { return false; }
|
||||
const auto STR = buildSpawnYaml(brush_entity_, cursor_col_, cursor_row_, entities.getEntities());
|
||||
const auto STR = buildSpawnYaml(brush_entity_, cursor_col_, cursor_row_, entities.getEntities(), brush_entity_frame_);
|
||||
if (STR.empty()) { return false; }
|
||||
try {
|
||||
auto node = fkyaml::node::deserialize(STR.begin(), STR.end());
|
||||
@@ -840,9 +890,21 @@ void Editor::updateEntityPicker(int canvas_x, int canvas_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;
|
||||
const auto& sel = editable_entities_[entity_picker_hover_];
|
||||
if (sel.has_variants) {
|
||||
// Entitat amb variants: encara no fixem brush. Obrim
|
||||
// el sub-picker de frames; el brush es fixarà només
|
||||
// quan l'usuari trii un frame concret allà.
|
||||
entity_frame_picker_source_ = &sel;
|
||||
entity_frame_picker_scroll_ = 0;
|
||||
entity_frame_picker_hover_ = -1;
|
||||
picker_ = Picker::ENTITY_FRAME;
|
||||
} else {
|
||||
brush_entity_ = sel.name;
|
||||
brush_entity_frame_ = -1;
|
||||
mode_ = Mode::ENTITY;
|
||||
picker_ = Picker::NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -896,6 +958,106 @@ void Editor::renderEntityPicker(const Tilemap& /*tilemap*/) const {
|
||||
screen.fillRect(LAY.x + LAY.w - 1, LAY.y, 1, LAY.h, BORDER);
|
||||
}
|
||||
|
||||
void Editor::updateEntityFramePicker(int canvas_x, int canvas_y) {
|
||||
// Si la font ha desaparegut (no hauria de passar mentre el picker
|
||||
// estigui obert, però defensem el cas), tanquem el sub-picker en lloc
|
||||
// de fer crash.
|
||||
if (entity_frame_picker_source_ == nullptr) {
|
||||
picker_ = Picker::ENTITY;
|
||||
return;
|
||||
}
|
||||
const auto LAY = layoutEntityPicker(entity_frame_picker_source_->frame_count);
|
||||
entity_frame_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_frame_picker_scroll_ + VR) * LAY.cols) + VC;
|
||||
if (IDX < LAY.total_items) { entity_frame_picker_hover_ = IDX; }
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& ev : EventBuffer::events) {
|
||||
if (ev.type == SDL_EVENT_MOUSE_WHEEL) {
|
||||
entity_frame_picker_scroll_ -= static_cast<int>(ev.wheel.y);
|
||||
entity_frame_picker_scroll_ = std::clamp(entity_frame_picker_scroll_, 0, LAY.max_scroll);
|
||||
} else if (ev.type == SDL_EVENT_MOUSE_BUTTON_DOWN && ev.button.button == SDL_BUTTON_LEFT) {
|
||||
if (entity_frame_picker_hover_ >= 0) {
|
||||
brush_entity_ = entity_frame_picker_source_->name;
|
||||
brush_entity_frame_ = entity_frame_picker_hover_;
|
||||
mode_ = Mode::ENTITY;
|
||||
picker_ = Picker::NONE;
|
||||
}
|
||||
} else if (ev.type == SDL_EVENT_MOUSE_BUTTON_DOWN && ev.button.button == SDL_BUTTON_RIGHT) {
|
||||
// Botó dret: tanca el sub-picker i torna al picker d'entitats
|
||||
// sense fixar brush. Coherent amb la convenció de "right =
|
||||
// cancel·la l'acció actual" dels altres modes.
|
||||
picker_ = Picker::ENTITY;
|
||||
entity_frame_picker_source_ = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Editor::renderEntityFramePicker() const {
|
||||
if (entity_frame_picker_source_ == nullptr) { return; }
|
||||
const auto& src = *entity_frame_picker_source_;
|
||||
const auto LAY = layoutEntityPicker(src.frame_count);
|
||||
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());
|
||||
|
||||
// Cols del atlas (sub-sprites per fila): es dedueix de la mida del
|
||||
// surface i del frame, igual que fa `EntityManager::renderEntity`.
|
||||
const int ATLAS_COLS = (src.frame_w > 0) ? (src.sprite->getWidth() / src.frame_w) : 1;
|
||||
const int ATLAS_COLS_SAFE = (ATLAS_COLS > 0) ? ATLAS_COLS : 1;
|
||||
|
||||
for (int vr = 0; vr < LAY.rows_visible; ++vr) {
|
||||
const int LOGICAL_ROW = entity_frame_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 [PX, PY] = entityCellOrigin(LAY, vc, vr);
|
||||
const int OX = PX + ((ENTITY_CELL - src.frame_w) / 2);
|
||||
const int OY = PY + ((ENTITY_CELL - src.frame_h) / 2);
|
||||
const int A_COL = IDX % ATLAS_COLS_SAFE;
|
||||
const int A_ROW = IDX / ATLAS_COLS_SAFE;
|
||||
const SDL_FRect SRC{
|
||||
.x = static_cast<float>(A_COL * src.frame_w),
|
||||
.y = static_cast<float>(A_ROW * src.frame_h),
|
||||
.w = static_cast<float>(src.frame_w),
|
||||
.h = static_cast<float>(src.frame_h),
|
||||
};
|
||||
src.sprite->render(OX, OY, &SRC, SDL_FLIP_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
if (entity_frame_picker_hover_ >= 0) {
|
||||
const int VR = (entity_frame_picker_hover_ / LAY.cols) - entity_frame_picker_scroll_;
|
||||
const int VC = entity_frame_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);
|
||||
}
|
||||
|
||||
void Editor::updateTilePicker(const Tilemap& tilemap, int canvas_x, int canvas_y) {
|
||||
const auto LAY = layoutPicker(tilemap);
|
||||
tile_picker_hover_ = -1;
|
||||
@@ -940,6 +1102,8 @@ void Editor::render(const Tilemap& tilemap, const EntityManager& entities) const
|
||||
renderTilePicker(tilemap);
|
||||
} else if (picker_ == Picker::ENTITY) {
|
||||
renderEntityPicker(tilemap);
|
||||
} else if (picker_ == Picker::ENTITY_FRAME) {
|
||||
renderEntityFramePicker();
|
||||
} 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());
|
||||
|
||||
@@ -78,9 +78,13 @@ class Editor {
|
||||
HAND };
|
||||
|
||||
// Quin picker està obert (si n'hi ha). Mútuament excloents.
|
||||
// - ENTITY_FRAME: sub-picker que apareix després de clicar una entitat
|
||||
// amb variants al picker principal. Mostra tots els sub-sprites del
|
||||
// atlas (índex frame) per a triar quina cara representa la instància.
|
||||
enum class Picker : std::uint8_t { NONE,
|
||||
TILE,
|
||||
ENTITY };
|
||||
ENTITY,
|
||||
ENTITY_FRAME };
|
||||
|
||||
// Consum de hotkeys pròpies (T, E, H, ESC). Modifica `mode_` / `picker_`.
|
||||
// Es crida al principi d'`update` perquè altres handlers no vegin
|
||||
@@ -100,6 +104,7 @@ class Editor {
|
||||
void updateHand(const Tilemap& tilemap, EntityManager& entities, 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 updateEntityFramePicker(int canvas_x, int canvas_y);
|
||||
|
||||
// Intent de col·locació segons brush_entity_ a la cel·la del cursor.
|
||||
// Construeix un node spawn sintètic, fa el snap concret per tipus
|
||||
@@ -138,6 +143,7 @@ class Editor {
|
||||
|
||||
void renderTilePicker(const Tilemap& tilemap) const;
|
||||
void renderEntityPicker(const Tilemap& tilemap) const;
|
||||
void renderEntityFramePicker() const;
|
||||
|
||||
bool active_{false};
|
||||
Mode mode_{Mode::TILE};
|
||||
@@ -153,21 +159,22 @@ class Editor {
|
||||
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.
|
||||
|
||||
// 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.
|
||||
// - `frame_count` és el nombre total de sub-sprites del atlas
|
||||
// (cols × rows segons les dimensions del surface i del frame).
|
||||
// - `has_variants` és `true` quan el template declara `editable_variants:
|
||||
// true` i hi ha més d'un frame: aleshores clicar al picker obre el
|
||||
// sub-picker de frames en lloc de fixar el brush directament.
|
||||
struct EditableEntity {
|
||||
std::string name;
|
||||
std::shared_ptr<Surface> sprite;
|
||||
int frame_w{0};
|
||||
int frame_h{0};
|
||||
int frame_count{1};
|
||||
bool has_variants{false};
|
||||
};
|
||||
|
||||
// Construïda lazy al primer enter() i preservada entre sessions:
|
||||
@@ -178,6 +185,23 @@ class Editor {
|
||||
|
||||
std::vector<EditableEntity> editable_entities_;
|
||||
|
||||
// Brush actiu per Mode::ENTITY (nom registrat al factory; "" = cap).
|
||||
std::string brush_entity_;
|
||||
// Frame escollit per al brush actiu (-1 = no aplicable: entitat sense
|
||||
// variants o brush encara per fixar). El emet el sub-picker en clicar
|
||||
// un frame; `tryPlaceEntity` el passa al spawn YAML com a `frame: N`.
|
||||
int brush_entity_frame_{-1};
|
||||
// Estat de l'EntityPicker.
|
||||
int entity_picker_scroll_{0};
|
||||
int entity_picker_hover_{-1}; // Índex a la llista de noms.
|
||||
// Estat del sub-picker de frames. `entity_frame_picker_source_` apunta
|
||||
// a la entitat activa (vista al EntityPicker) per a la qual el
|
||||
// sub-picker mostra les variants. Quan el sub-picker es tanca sense
|
||||
// selecció, ho deixem a nullptr i `brush_entity_frame_ = -1`.
|
||||
int entity_frame_picker_scroll_{0};
|
||||
int entity_frame_picker_hover_{-1};
|
||||
const EditableEntity* entity_frame_picker_source_{nullptr};
|
||||
|
||||
// Font compartida amb el Notifier/HUD: el surface el carrega Director
|
||||
// a l'arrencada, ací només prenem la referència del cache. shared_ptr
|
||||
// perquè `Text` accepta el surface per shared_ptr i així no copiem.
|
||||
|
||||
Reference in new issue
Block a user