feat(editor): mode mà — selecció, mou, borra i marc d'entitats

This commit is contained in:
JailDesigner committed 2026-05-02 11:28:41 +02:00
1 parent 9b09ccb765
commit b4e36944d7
5 files changed
+182 -9

No files matched your search

+131 -6
View File
@@ -453,11 +453,123 @@ void Editor::updateEntityBrush(EntityManager& entities, int /*canvas_x*/, int /*
}
}
// cppcheck-suppress constParameterReference
// Manté firma simètrica amb les altres branques d'update; la lògica
// real arriba al pas 6.
void Editor::updateHand(EntityManager& /*entities*/, int /*canvas_x*/, int /*canvas_y*/) {
// Pas 6: hover, select, move, delete, marc d'entitat seleccionada.
namespace {
// Index de l'entitat sota el píxel donat. -1 si cap. Usem el rect de
// pintat (`getPixelRect`) i no el de col·lisió perquè el dissenyador
// espera "fer clic on es veu", encara que el cos físic sigui més
// estret (típic en sarc/porta amb falsa perspectiva).
auto entityAtPixel(const std::vector<Entity*>& list, int px, int py) -> int {
const auto FX = static_cast<float>(px);
const auto FY = static_cast<float>(py);
for (int i = static_cast<int>(list.size()) - 1; i >= 0; --i) {
const auto R = list[i]->getPixelRect();
if (FX >= R.x && FX < R.x + R.w && FY >= R.y && FY < R.y + R.h) { return i; }
}
return -1;
}
auto isDeleteKey(const SDL_Event& ev) -> bool {
if (ev.type != SDL_EVENT_KEY_DOWN) { return false; }
const auto SC = KeyConfig::get().scancode("editor_delete");
return SC != SDL_SCANCODE_UNKNOWN && ev.key.scancode == SC;
}
} // namespace
void Editor::updateHand(EntityManager& entities, int canvas_x, int canvas_y) {
const auto LIST = entities.getEntities();
hand_hover_idx_ = entityAtPixel(LIST, canvas_x, canvas_y);
if (hand_selected_idx_ >= static_cast<int>(LIST.size())) { hand_selected_idx_ = -1; }
for (const auto& ev : EventBuffer::events) {
const bool IS_MOUSE_DOWN = (ev.type == SDL_EVENT_MOUSE_BUTTON_DOWN);
if (IS_MOUSE_DOWN && ev.button.button == SDL_BUTTON_LEFT) {
handleHandLeftClick(entities);
} else if (IS_MOUSE_DOWN && ev.button.button == SDL_BUTTON_RIGHT) {
handleHandRightClick();
} else if (isDeleteKey(ev) && hand_selected_idx_ >= 0) {
deleteSelected(entities);
}
}
}
void Editor::handleHandLeftClick(EntityManager& entities) {
if (hand_selected_idx_ < 0) {
// Sense selecció: si pulsem sobre una entitat, seleccionem.
if (hand_hover_idx_ >= 0) { hand_selected_idx_ = hand_hover_idx_; }
return;
}
if (hand_hover_idx_ == hand_selected_idx_) {
// Mateixa entitat: deselecciona.
hand_selected_idx_ = -1;
return;
}
if (hand_hover_idx_ >= 0) {
// Una altra entitat: canvia la selecció.
hand_selected_idx_ = hand_hover_idx_;
return;
}
// Cel·la sense entitat: intenta moure-hi la seleccionada.
if (tryMoveSelected(entities)) { hand_selected_idx_ = -1; }
}
void Editor::handleHandRightClick() {
if (hand_selected_idx_ >= 0) {
hand_selected_idx_ = -1;
} else {
mode_ = Mode::TILE;
}
}
auto Editor::tryMoveSelected(EntityManager& entities) const -> bool {
if (hand_selected_idx_ < 0 || cursor_col_ < 0 || cursor_row_ < 0) { return false; }
auto list = entities.getEntities();
if (hand_selected_idx_ >= static_cast<int>(list.size())) { return false; }
auto* target = list[hand_selected_idx_];
const std::string TYPE = target->getName();
// Construïm el nou spawn excloent l'entitat que mou del check de
// solapaments: si no, sempre xocaria amb ella mateixa. Per a la
// porta, el "moviment" actualment és no-op (no portem encara la
// columna a YAML), així que el descartem amb claredat.
if (TYPE == "door") { return false; }
std::vector<Entity*> filtered;
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);
if (STR.empty()) { return false; }
if (!entities.removeEntity(target)) { return false; }
try {
auto node = fkyaml::node::deserialize(STR.begin(), STR.end());
return entities.addSpawn(node) != nullptr;
} catch (const std::exception&) {
return false;
}
}
void Editor::deleteSelected(EntityManager& entities) {
auto list = entities.getEntities();
if (hand_selected_idx_ < 0 || hand_selected_idx_ >= static_cast<int>(list.size())) { return; }
auto* target = list[hand_selected_idx_];
if (entities.removeEntity(target)) { hand_selected_idx_ = -1; }
}
void Editor::renderEntityFrame(const Entity& e, std::uint32_t argb) {
const auto R = e.getPixelRect();
auto& screen = Screen::get();
const auto X = static_cast<int>(R.x);
const auto Y = static_cast<int>(R.y);
const auto W = static_cast<int>(R.w);
const auto H = static_cast<int>(R.h);
if (W <= 0 || H <= 0) { return; }
screen.fillRect(X, Y, W, 1, argb);
screen.fillRect(X, Y + H - 1, W, 1, argb);
screen.fillRect(X, Y, 1, H, argb);
screen.fillRect(X + W - 1, Y, 1, H, argb);
}
auto Editor::tryPlaceEntity(EntityManager& entities) -> bool {
@@ -586,7 +698,7 @@ void Editor::updateTilePicker(const Tilemap& tilemap, int canvas_x, int canvas_y
}
}
void Editor::render(const Tilemap& tilemap) const {
void Editor::render(const Tilemap& tilemap, const EntityManager& entities) const {
if (!active_) { return; }
auto& screen = Screen::get();
constexpr int W = GameCanvas::WIDTH;
@@ -601,6 +713,19 @@ void Editor::render(const Tilemap& tilemap) const {
const int PX = tilemap.getOriginX() + (cursor_col_ * tilemap.getTileSize());
const int PY = tilemap.getOriginY() + (cursor_row_ * tilemap.getTileSize());
tilemap.renderTilePreview(PX, PY, brush_tile_);
} else if (mode_ == Mode::HAND && picker_ == Picker::NONE) {
// Marcs d'entitat: hover (tenue) i selecció (brillant). El
// pickerHoverArgb el reusem com a "selecció brillant"; el
// border (tipus blau) com a "hover tenue".
const auto LIST = entities.getEntities();
const auto HOVER_ARGB = EditorConfig::get().borderArgb();
const auto SELECT_ARGB = EditorConfig::get().pickerHoverArgb();
if (hand_hover_idx_ >= 0 && hand_hover_idx_ < static_cast<int>(LIST.size()) && hand_hover_idx_ != hand_selected_idx_) {
renderEntityFrame(*LIST[hand_hover_idx_], HOVER_ARGB);
}
if (hand_selected_idx_ >= 0 && hand_selected_idx_ < static_cast<int>(LIST.size())) {
renderEntityFrame(*LIST[hand_selected_idx_], SELECT_ARGB);
}
}
const auto BORDER = EditorConfig::get().borderArgb();
+28 -2
View File
@@ -7,6 +7,7 @@
#include <string>
#include <vector>
class Entity;
class EntityManager;
class Surface;
class Tilemap;
@@ -56,7 +57,9 @@ class Editor {
// Pinta el border blau i, segons mode i picker, brush preview, marc
// d'entitat seleccionada o el picker. No-op si no actiu.
void render(const Tilemap& tilemap) const;
// `entities` és necessari per al marc d'entitat hovered/seleccionada
// en Mode::HAND; en altres modes no s'usa.
void render(const Tilemap& tilemap, const EntityManager& entities) const;
private:
// Mode del cursor: determina què fa el clic esquerre sobre el mapa.
@@ -95,6 +98,24 @@ class Editor {
// o el snap no és vàlid. Retorna true si s'ha col·locat.
auto tryPlaceEntity(EntityManager& entities) -> bool;
// Mou l'entitat seleccionada a la cel·la del cursor: borra l'antiga
// i crida `addSpawn` amb la nova posició. Reusa la mateixa lògica
// de snap/solapament que la col·locació nova, excloent l'entitat
// que es mou del check de solapament.
auto tryMoveSelected(EntityManager& entities) const -> bool;
// Sub-handlers de `updateHand`. Aïllats per mantenir baixa la
// complexitat cognitiva del bucle d'esdeveniments.
void handleHandLeftClick(EntityManager& entities);
void handleHandRightClick();
// Borra l'entitat seleccionada. La validació de constraints
// (1 puerta, ≥1 sarc) es fa al pas 8.
void deleteSelected(EntityManager& entities);
// Render del marc d'una entitat: 1 px sobre el seu pixel_rect.
static void renderEntityFrame(const Entity& e, std::uint32_t argb);
void renderTilePicker(const Tilemap& tilemap) const;
void renderEntityPicker(const Tilemap& tilemap) const;
@@ -137,7 +158,12 @@ class Editor {
std::vector<EditableEntity> editable_entities_;
// Selecció en Mode::HAND (índex dins de l'EntityManager; -1 = cap).
// Selecció en Mode::HAND. Treballem amb índexs sobre el snapshot de
// `EntityManager::getEntities()` perquè és el que ens dóna l'API
// pública. Després de cada mutació (move/delete) ho recalculem; com
// que l'editor congela el joc, el snapshot només canvia per accions
// pròpies.
int hand_hover_idx_{-1};
int hand_selected_idx_{-1};
};
+15
View File
@@ -87,6 +87,21 @@ void EntityManager::setDoor(Door* door) {
}
}
auto EntityManager::removeEntity(Entity* observer) -> bool {
if (observer == nullptr) { return false; }
const bool WAS_DOOR = (door_ == observer);
const bool WAS_SARC = (observer->getName() == "sarcophagus");
auto it = std::ranges::find_if(entities_, [observer](const auto& up) { return up.get() == observer; });
if (it == entities_.end()) { return false; }
entities_.erase(it);
if (WAS_DOOR) { door_ = nullptr; }
if (WAS_SARC && total_sarcs_ > 0) { --total_sarcs_; }
// No tocar `opened_sarcs_` aquí: l'editor congela el joc i no pot
// disparar-se cap esdeveniment derivat. Quan el GameScene faça el
// reload posterior, els comptadors es re-deriven d'inici.
return true;
}
auto EntityManager::addSarcophagus(std::unique_ptr<Sarcophagus> sarc) -> Sarcophagus* {
if (sarc == nullptr) { return nullptr; }
sarc->setEventBus(&bus_);
+7
View File
@@ -44,6 +44,13 @@ class EntityManager {
// una. Si ja n'hi ha una assignada, la substitueix (útil a tests).
void setDoor(Door* door);
// Treu i destrueix l'entitat observada. Si era la porta o un sarc,
// ajusta `door_` / `total_sarcs_` en conseqüència. Pensat per al
// editor: no toca `opened_sarcs_` ni publica events — el joc està
// congelat i, en sortir del editor, GameScene torna a fer reload(),
// així que l'estat es deriva net.
auto removeEntity(Entity* observer) -> bool;
// Marca una entitat com a sarcòfag — si no es passa per `add` amb
// type=sarcophagus no es compta al total. Útil en combinació amb
// `add` quan es fa addSarcophagus(unique_ptr<Sarcophagus>).
+1 -1
View File
@@ -1328,7 +1328,7 @@ void GameScene::render() {
}
#ifdef _DEBUG
editor_.render(tilemap_);
editor_.render(tilemap_, entity_manager_);
#endif
// Overlay de pausa: text amb stroke blanc (índex 255) centrat amb X