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
+152 -25

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),
};
}
void Editor::updateHand(int /*canvas_x*/, int /*canvas_y*/) {
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();
}
}
}
// 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()));
+13 -3
View File
@@ -7,6 +7,7 @@
#include <string>
#include <vector>
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;
+10
View File
@@ -7,6 +7,7 @@
#include <string>
#include <vector>
#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
@@ -395,18 +395,15 @@ void EntityManager::reload(const std::string& level_yaml_path) {
loadFromLevel(level_yaml_path);
}
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; }
auto EntityManager::addSpawn(const fkyaml::node& spawn) -> Entity* {
if (!spawn.contains("type")) { return nullptr; }
const auto TYPE = spawn["type"].get_value<std::string>();
SpriteInfo sprite;
std::string anchor;
auto entity = buildEntity(TYPE, sprite, anchor);
if (entity == nullptr) { continue; }
if (entity == nullptr) { return nullptr; }
Entity* observer = entity.get();
bool ok = false;
if (TYPE == "sarcophagus") {
@@ -416,10 +413,14 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
} else {
ok = placeGeneric(*this, std::move(entity), TYPE, sprite, anchor, spawn);
}
const bool OK = ok;
if (OK) { ++loaded; }
return ok ? observer : nullptr;
}
void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
const auto SPAWNS = EntityLoader::readSpawns(level_yaml_path);
const int LOADED = static_cast<int>(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...).
// Imprescindible perquè el jugador no pot trepitjar aquests tiles
@@ -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';
}
+1 -1
View File
@@ -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