From 9b09ccb76582febecdb77ac4a81bacedca5fdad4 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sat, 2 May 2026 11:21:05 +0200 Subject: [PATCH] =?UTF-8?q?feat(editor):=20col=C2=B7locaci=C3=B3=20d'entit?= =?UTF-8?q?ats=20amb=20snap=20per=20tipus=20i=20bloqueig=20de=20solapament?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/game/editor/editor.cpp | 128 ++++++++++++++++-- source/game/editor/editor.hpp | 16 ++- source/game/entities/entity_manager.hpp | 10 ++ .../entities/entity_manager_loadlevel.cpp | 47 +++---- source/game/scenes/game_scene.cpp | 2 +- 5 files changed, 165 insertions(+), 38 deletions(-) diff --git a/source/game/editor/editor.cpp b/source/game/editor/editor.cpp index 8f38335..ba68cb6 100644 --- a/source/game/editor/editor.cpp +++ b/source/game/editor/editor.cpp @@ -5,6 +5,7 @@ #include #include +#include #include "core/input/key_config.hpp" #include "core/locale/locale.hpp" @@ -16,9 +17,12 @@ #include "core/system/event_buffer.hpp" #include "external/fkyaml_node.hpp" #include "game/editor/editor_config.hpp" +#include "game/entities/entity.hpp" #include "game/entities/entity_factory.hpp" #include "game/entities/entity_loader.hpp" +#include "game/entities/entity_manager.hpp" #include "game/levels/level_manager.hpp" +#include "game/map/map.hpp" #include "game/map/tilemap.hpp" #include "utils/defaults.hpp" #include "utils/defines.hpp" @@ -291,7 +295,7 @@ void Editor::handleHotkeys() { } } -void Editor::update(int /*delta_ms*/, Tilemap& tilemap) { +void Editor::update(int /*delta_ms*/, Tilemap& tilemap, EntityManager& entities) { if (!active_) { return; } handleHotkeys(); @@ -331,10 +335,10 @@ void Editor::update(int /*delta_ms*/, Tilemap& tilemap) { updateTileBrush(tilemap, CX, CY); break; case Mode::ENTITY: - updateEntityBrush(CX, CY); + updateEntityBrush(entities, CX, CY); break; case Mode::HAND: - updateHand(CX, CY); + updateHand(entities, CX, CY); break; } } @@ -353,18 +357,120 @@ void Editor::updateTileBrush(Tilemap& tilemap, int /*canvas_x*/, int /*canvas_y* } } -// NOLINTBEGIN(readability-convert-member-functions-to-static) -// Stubs: la lògica concreta es construeix als passos següents -// (entity placement, hand mode). Mantenim la signatura per estabilitat -// d'API i perquè update() pugui despatxar sense branques especials. -void Editor::updateEntityBrush(int /*canvas_x*/, int /*canvas_y*/) { - // Pas 5: col·locació amb snap per tipus + bloqueig de solapaments. +namespace { + + // Cel·la del tilemap → rect píxel d'una cel·la unitat. + auto tileCellRect(int col, int row, int tile_size) -> SDL_FRect { + return { + .x = static_cast((col * tile_size) + Map::tilemap_origin_x), + .y = static_cast((row * tile_size) + Map::tilemap_origin_y), + .w = static_cast(tile_size), + .h = static_cast(tile_size), + }; + } + + auto rectsOverlap(const SDL_FRect& a, const SDL_FRect& b) -> bool { + return a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h; + } + + // Per a un sarc viu, recupera la seva (sarc_col, sarc_row) invertint + // `getCollisionRect` (que coincideix exactament amb `Map::getSarcRect` + // — vegeu placeSarcophagus). No necessitem getters específics al + // Sarcophagus: la geometria és reversible. + auto sarcGridFromRect(const SDL_FRect& rect) -> std::pair { + if (Map::pitch_x <= 0 || Map::pitch_y <= 0) { return {-1, -1}; } + const int SC = (static_cast(rect.x) - Map::origin_x) / Map::pitch_x; + const int SR = (static_cast(rect.y) - Map::origin_y) / Map::pitch_y; + return {SC, SR}; + } + + // Cel·la del cursor → cel·la sarc (4×5). Retorna {-1,-1} si cau fora + // de la rejilla. Cap dependència de l'editor: pur càlcul de Map. + auto cursorToSarcCell(int cursor_col, int cursor_row) -> std::pair { + if (Map::pitch_x <= 0 || Map::pitch_y <= 0) { return {-1, -1}; } + const int CURSOR_PX = (cursor_col * Map::tile_size) + Map::tilemap_origin_x; + const int CURSOR_PY = (cursor_row * Map::tile_size) + Map::tilemap_origin_y; + const int SC = (CURSOR_PX - Map::origin_x) / Map::pitch_x; + const int SR = (CURSOR_PY - Map::origin_y) / Map::pitch_y; + if (SC < 0 || SC >= Map::grid_cols || SR < 0 || SR >= Map::grid_rows) { + return {-1, -1}; + } + return {SC, SR}; + } + + auto sarcAlreadyAt(const std::vector& existing, int sc, int sr) -> bool { + return std::ranges::any_of(existing, [sc, sr](const Entity* e) { + if (e->getName() != "sarcophagus") { return false; } + const auto [ESC, ESR] = sarcGridFromRect(e->getCollisionRect()); + return ESC == sc && ESR == sr; + }); + } + + auto doorExists(const std::vector& existing) -> bool { + return std::ranges::any_of(existing, [](const Entity* e) { return e->getName() == "door"; }); + } + + auto cellOccupied(const std::vector& existing, const SDL_FRect& dest) -> bool { + return std::ranges::any_of(existing, + [&dest](const Entity* e) { return rectsOverlap(dest, e->getCollisionRect()); }); + } + + // 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& existing) -> std::string { + std::ostringstream y; + y << "type: " << brush << '\n'; + if (brush == "sarcophagus") { + const auto [SC, SR] = cursorToSarcCell(cursor_col, cursor_row); + if (SC < 0 || sarcAlreadyAt(existing, SC, SR)) { return {}; } + y << "sarc_position: [" << SC << ", " << SR << "]\n"; + return y.str(); + } + if (brush == "door") { + if (doorExists(existing)) { return {}; } + return y.str(); + } + const auto DEST = tileCellRect(cursor_col, cursor_row, Map::tile_size); + if (cellOccupied(existing, DEST)) { return {}; } + y << "position: [" << cursor_col << ", " << cursor_row << "]\n"; + return y.str(); + } + +} // namespace + +void Editor::updateEntityBrush(EntityManager& entities, int /*canvas_x*/, int /*canvas_y*/) { + for (const auto& ev : EventBuffer::events) { + if (ev.type != SDL_EVENT_MOUSE_BUTTON_DOWN) { continue; } + if (ev.button.button == SDL_BUTTON_LEFT) { + tryPlaceEntity(entities); + } else if (ev.button.button == SDL_BUTTON_RIGHT) { + // Click dret: descarta el brush però es queda en Mode::ENTITY. + // Per sortir cap a un altre mode, utilitza T (tile picker) o + // H (mà): manté la simetria amb el comportament del brush + // de tiles. + brush_entity_.clear(); + } + } } -void Editor::updateHand(int /*canvas_x*/, int /*canvas_y*/) { +// 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. } -// NOLINTEND(readability-convert-member-functions-to-static) + +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()); + if (STR.empty()) { 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::updateEntityPicker(int canvas_x, int canvas_y) { const auto LAY = layoutEntityPicker(static_cast(editable_entities_.size())); diff --git a/source/game/editor/editor.hpp b/source/game/editor/editor.hpp index e99d645..6a48baf 100644 --- a/source/game/editor/editor.hpp +++ b/source/game/editor/editor.hpp @@ -7,6 +7,7 @@ #include #include +class EntityManager; class Surface; class Tilemap; @@ -49,7 +50,9 @@ class Editor { [[nodiscard]] auto active() const -> bool { return active_; } // Processa input propi (brush, picker). No-op si no actiu. - void update(int delta_ms, Tilemap& tilemap); + // `entities` cal perquè els modes ENTITY i HAND modifiquen el manager + // en directe (col·loca/mou/borra entitats); en TILE no s'usa. + void update(int delta_ms, Tilemap& tilemap, EntityManager& entities); // Pinta el border blau i, segons mode i picker, brush preview, marc // d'entitat seleccionada o el picker. No-op si no actiu. @@ -80,11 +83,18 @@ class Editor { // Branques d'update segons (mode, picker). Només té sentit cridar-les // des d'`update`. void updateTileBrush(Tilemap& tilemap, int canvas_x, int canvas_y); - void updateEntityBrush(int canvas_x, int canvas_y); - void updateHand(int canvas_x, int canvas_y); + void updateEntityBrush(EntityManager& entities, int canvas_x, int canvas_y); + void updateHand(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); + // 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 + // (sarc → rejilla 4×5; door → fila del mur i, de moment, columna + // canònica) i comprova solapaments. No-op si la cel·la està ocupada + // o el snap no és vàlid. Retorna true si s'ha col·locat. + auto tryPlaceEntity(EntityManager& entities) -> bool; + void renderTilePicker(const Tilemap& tilemap) const; void renderEntityPicker(const Tilemap& tilemap) const; diff --git a/source/game/entities/entity_manager.hpp b/source/game/entities/entity_manager.hpp index a2d2515..363ed1b 100644 --- a/source/game/entities/entity_manager.hpp +++ b/source/game/entities/entity_manager.hpp @@ -7,6 +7,7 @@ #include #include +#include "external/fkyaml_node.hpp" #include "game/entities/event_bus.hpp" class Entity; @@ -102,6 +103,15 @@ class EntityManager { // `Map::tile_size`, `tilemap_origin_*` i `getSarcRect`). void loadFromLevel(const std::string& level_yaml_path); + // Crea i afegeix una entitat a partir d'un node spawn (mateix format + // que els items del bloc `entities:` del YAML del nivell). Retorna el + // punter observacional a l'entitat creada, o `nullptr` si la plantilla + // no es pot carregar, el tipus no està registrat o el spawn no té els + // camps requerits (`position`, `sarc_position`, etc. segons el tipus). + // Pensat per al editor de Debug: construeix un node sintètic des + // d'una posició del cursor i el passa. + auto addSpawn(const fkyaml::node& spawn) -> Entity*; + // Reset complet del nivell: destrueix totes les entitats, reseteja // els comptadors interns (sarcòfags oberts, total, exited, flag de // level_complete) i torna a cridar `loadFromLevel` per regenerar les diff --git a/source/game/entities/entity_manager_loadlevel.cpp b/source/game/entities/entity_manager_loadlevel.cpp index 0eb4181..c562772 100644 --- a/source/game/entities/entity_manager_loadlevel.cpp +++ b/source/game/entities/entity_manager_loadlevel.cpp @@ -395,30 +395,31 @@ void EntityManager::reload(const std::string& level_yaml_path) { loadFromLevel(level_yaml_path); } +auto EntityManager::addSpawn(const fkyaml::node& spawn) -> Entity* { + if (!spawn.contains("type")) { return nullptr; } + const auto TYPE = spawn["type"].get_value(); + + SpriteInfo sprite; + std::string anchor; + auto entity = buildEntity(TYPE, sprite, anchor); + if (entity == nullptr) { return nullptr; } + Entity* observer = entity.get(); + + bool ok = false; + if (TYPE == "sarcophagus") { + ok = placeSarcophagus(*this, std::move(entity), sprite, spawn); + } else if (TYPE == "door") { + ok = placeDoor(*this, std::move(entity), sprite); + } else { + ok = placeGeneric(*this, std::move(entity), TYPE, sprite, anchor, spawn); + } + return ok ? observer : nullptr; +} + void EntityManager::loadFromLevel(const std::string& level_yaml_path) { const auto SPAWNS = EntityLoader::readSpawns(level_yaml_path); - int loaded = 0; - - for (const auto& spawn : SPAWNS) { - if (!spawn.contains("type")) { continue; } - const auto TYPE = spawn["type"].get_value(); - - SpriteInfo sprite; - std::string anchor; - auto entity = buildEntity(TYPE, sprite, anchor); - if (entity == nullptr) { continue; } - - bool ok = false; - if (TYPE == "sarcophagus") { - ok = placeSarcophagus(*this, std::move(entity), sprite, spawn); - } else if (TYPE == "door") { - ok = placeDoor(*this, std::move(entity), sprite); - } else { - ok = placeGeneric(*this, std::move(entity), TYPE, sprite, anchor, spawn); - } - const bool OK = ok; - if (OK) { ++loaded; } - } + const int LOADED = static_cast(std::ranges::count_if(SPAWNS, + [this](const fkyaml::node& spawn) { return addSpawn(spawn) != nullptr; })); // Broadcast inicial: marca com a "pintats" tots els tiles ocupats per // decals de sòl (Hole, i futurs pinchos plans, plaques, charcos...). @@ -446,6 +447,6 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) { << " tile_painted broadcast inicial (decals de sòl)\n"; } - std::cout << "EntityManager: " << loaded << " entitats carregades des de " + std::cout << "EntityManager: " << LOADED << " entitats carregades des de " << level_yaml_path << '\n'; } diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 549091b..f5489da 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -1228,7 +1228,7 @@ void GameScene::update(int delta_ms) { } #ifdef _DEBUG - editor_.update(delta_ms, tilemap_); + editor_.update(delta_ms, tilemap_, entity_manager_); #endif // El HUD continua avançant les seues animacions encara que el món