feat(editor): col·locació d'entitats amb snap per tipus i bloqueig de solapaments

This commit is contained in:
JailDesigner committed 2026-05-02 11:21:05 +02:00
1 parent 94213f9fd6
commit 9b09ccb765
5 files changed
+165 -38

No files matched your search

+117 -11
View File
@@ -5,6 +5,7 @@
#include <SDL3/SDL.h>
#include <algorithm>
#include <sstream>
#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<float>((col * tile_size) + Map::tilemap_origin_x),
.y = static_cast<float>((row * tile_size) + Map::tilemap_origin_y),
.w = static_cast<float>(tile_size),
.h = static_cast<float>(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<int, int> {
if (Map::pitch_x <= 0 || Map::pitch_y <= 0) { return {-1, -1}; }
const int SC = (static_cast<int>(rect.x) - Map::origin_x) / Map::pitch_x;
const int SR = (static_cast<int>(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<int, int> {
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<Entity*>& 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<Entity*>& existing) -> bool {
return std::ranges::any_of(existing, [](const Entity* e) { return e->getName() == "door"; });
}
auto cellOccupied(const std::vector<Entity*>& 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<Entity*>& 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<int>(editable_entities_.size()));