feat(editor): mode mà — selecció, mou, borra i marc d'entitats
This commit is contained in:
1 parent
9b09ccb765
commit
b4e36944d7
5 files changed
+182
-9
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
|
||||
@@ -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_);
|
||||
|
||||
@@ -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>).
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user