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 <SDL3/SDL.h>
#include <algorithm> #include <algorithm>
#include <sstream>
#include "core/input/key_config.hpp" #include "core/input/key_config.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
@@ -16,9 +17,12 @@
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "game/editor/editor_config.hpp" #include "game/editor/editor_config.hpp"
#include "game/entities/entity.hpp"
#include "game/entities/entity_factory.hpp" #include "game/entities/entity_factory.hpp"
#include "game/entities/entity_loader.hpp" #include "game/entities/entity_loader.hpp"
#include "game/entities/entity_manager.hpp"
#include "game/levels/level_manager.hpp" #include "game/levels/level_manager.hpp"
#include "game/map/map.hpp"
#include "game/map/tilemap.hpp" #include "game/map/tilemap.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/defines.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; } if (!active_) { return; }
handleHotkeys(); handleHotkeys();
@@ -331,10 +335,10 @@ void Editor::update(int /*delta_ms*/, Tilemap& tilemap) {
updateTileBrush(tilemap, CX, CY); updateTileBrush(tilemap, CX, CY);
break; break;
case Mode::ENTITY: case Mode::ENTITY:
updateEntityBrush(CX, CY); updateEntityBrush(entities, CX, CY);
break; break;
case Mode::HAND: case Mode::HAND:
updateHand(CX, CY); updateHand(entities, CX, CY);
break; break;
} }
} }
@@ -353,18 +357,120 @@ void Editor::updateTileBrush(Tilemap& tilemap, int /*canvas_x*/, int /*canvas_y*
} }
} }
// NOLINTBEGIN(readability-convert-member-functions-to-static) namespace {
// Stubs: la lògica concreta es construeix als passos següents
// (entity placement, hand mode). Mantenim la signatura per estabilitat // Cel·la del tilemap → rect píxel d'una cel·la unitat.
// d'API i perquè update() pugui despatxar sense branques especials. auto tileCellRect(int col, int row, int tile_size) -> SDL_FRect {
void Editor::updateEntityBrush(int /*canvas_x*/, int /*canvas_y*/) { return {
// Pas 5: col·locació amb snap per tipus + bloqueig de solapaments. .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. // 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) { void Editor::updateEntityPicker(int canvas_x, int canvas_y) {
const auto LAY = layoutEntityPicker(static_cast<int>(editable_entities_.size())); const auto LAY = layoutEntityPicker(static_cast<int>(editable_entities_.size()));
+13 -3
View File
@@ -7,6 +7,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
class EntityManager;
class Surface; class Surface;
class Tilemap; class Tilemap;
@@ -49,7 +50,9 @@ class Editor {
[[nodiscard]] auto active() const -> bool { return active_; } [[nodiscard]] auto active() const -> bool { return active_; }
// Processa input propi (brush, picker). No-op si no actiu. // 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 // Pinta el border blau i, segons mode i picker, brush preview, marc
// d'entitat seleccionada o el picker. No-op si no actiu. // 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 // Branques d'update segons (mode, picker). Només té sentit cridar-les
// des d'`update`. // des d'`update`.
void updateTileBrush(Tilemap& tilemap, int canvas_x, int canvas_y); void updateTileBrush(Tilemap& tilemap, int canvas_x, int canvas_y);
void updateEntityBrush(int canvas_x, int canvas_y); void updateEntityBrush(EntityManager& entities, int canvas_x, int canvas_y);
void updateHand(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 updateTilePicker(const Tilemap& tilemap, int canvas_x, int canvas_y);
void updateEntityPicker(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 renderTilePicker(const Tilemap& tilemap) const;
void renderEntityPicker(const Tilemap& tilemap) const; void renderEntityPicker(const Tilemap& tilemap) const;
+10
View File
@@ -7,6 +7,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "external/fkyaml_node.hpp"
#include "game/entities/event_bus.hpp" #include "game/entities/event_bus.hpp"
class Entity; class Entity;
@@ -102,6 +103,15 @@ class EntityManager {
// `Map::tile_size`, `tilemap_origin_*` i `getSarcRect`). // `Map::tile_size`, `tilemap_origin_*` i `getSarcRect`).
void loadFromLevel(const std::string& level_yaml_path); 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 // Reset complet del nivell: destrueix totes les entitats, reseteja
// els comptadors interns (sarcòfags oberts, total, exited, flag de // els comptadors interns (sarcòfags oberts, total, exited, flag de
// level_complete) i torna a cridar `loadFromLevel` per regenerar les // level_complete) i torna a cridar `loadFromLevel` per regenerar les
@@ -395,30 +395,31 @@ void EntityManager::reload(const std::string& level_yaml_path) {
loadFromLevel(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<std::string>();
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) { void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
const auto SPAWNS = EntityLoader::readSpawns(level_yaml_path); const auto SPAWNS = EntityLoader::readSpawns(level_yaml_path);
int loaded = 0; const int LOADED = static_cast<int>(std::ranges::count_if(SPAWNS,
[this](const fkyaml::node& spawn) { return addSpawn(spawn) != nullptr; }));
for (const auto& spawn : SPAWNS) {
if (!spawn.contains("type")) { continue; }
const auto TYPE = spawn["type"].get_value<std::string>();
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; }
}
// Broadcast inicial: marca com a "pintats" tots els tiles ocupats per // Broadcast inicial: marca com a "pintats" tots els tiles ocupats per
// decals de sòl (Hole, i futurs pinchos plans, plaques, charcos...). // 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"; << " 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'; << level_yaml_path << '\n';
} }
+1 -1
View File
@@ -1228,7 +1228,7 @@ void GameScene::update(int delta_ms) {
} }
#ifdef _DEBUG #ifdef _DEBUG
editor_.update(delta_ms, tilemap_); editor_.update(delta_ms, tilemap_, entity_manager_);
#endif #endif
// El HUD continua avançant les seues animacions encara que el món // El HUD continua avançant les seues animacions encara que el món