diff --git a/source/game/editor/editor.cpp b/source/game/editor/editor.cpp index ba68cb6..4a2625f 100644 --- a/source/game/editor/editor.cpp +++ b/source/game/editor/editor.cpp @@ -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& list, int px, int py) -> int { + const auto FX = static_cast(px); + const auto FY = static_cast(py); + for (int i = static_cast(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(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(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 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(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(R.x); + const auto Y = static_cast(R.y); + const auto W = static_cast(R.w); + const auto H = static_cast(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(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(LIST.size())) { + renderEntityFrame(*LIST[hand_selected_idx_], SELECT_ARGB); + } } const auto BORDER = EditorConfig::get().borderArgb(); diff --git a/source/game/editor/editor.hpp b/source/game/editor/editor.hpp index 6a48baf..39927d4 100644 --- a/source/game/editor/editor.hpp +++ b/source/game/editor/editor.hpp @@ -7,6 +7,7 @@ #include #include +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 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}; }; diff --git a/source/game/entities/entity_manager.cpp b/source/game/entities/entity_manager.cpp index 0a679c6..45b67bb 100644 --- a/source/game/entities/entity_manager.cpp +++ b/source/game/entities/entity_manager.cpp @@ -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 sarc) -> Sarcophagus* { if (sarc == nullptr) { return nullptr; } sarc->setEventBus(&bus_); diff --git a/source/game/entities/entity_manager.hpp b/source/game/entities/entity_manager.hpp index 363ed1b..77b3c25 100644 --- a/source/game/entities/entity_manager.hpp +++ b/source/game/entities/entity_manager.hpp @@ -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). diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index f5489da..ca0fd7a 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -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