feat(editor): mou la porta i persisteix geometry.door_col

This commit is contained in:
JailDesigner committed 2026-05-02 13:35:01 +02:00
1 parent a254fdd669
commit c003e6665e
8 files changed
+214 -20

No files matched your search

+3
View File
@@ -116,6 +116,9 @@ set(APP_SOURCES
source/game/map/map.cpp source/game/map/map.cpp
source/game/map/tilemap.cpp source/game/map/tilemap.cpp
source/game/map/tilemap_writer.cpp source/game/map/tilemap_writer.cpp
# Writer surgical de claus dins de `geometry:` (door_col), paral·lel
# al TilemapWriter. Només Debug.
source/game/map/map_writer.cpp
source/game/map/paint_layer.cpp source/game/map/paint_layer.cpp
source/game/levels/level_manager.cpp source/game/levels/level_manager.cpp
# Mòdul editor (només activa contingut en Debug via `#ifdef _DEBUG`). # Mòdul editor (només activa contingut en Debug via `#ifdef _DEBUG`).
+2 -4
View File
@@ -49,7 +49,7 @@ geometry:
# 16 px centrals (just aquesta columna) són el "forat" del mur trasser per # 16 px centrals (just aquesta columna) són el "forat" del mur trasser per
# on el jugador surt de la sala. Les 2 columnes flanquejants segueixen sent # on el jugador surt de la sala. Les 2 columnes flanquejants segueixen sent
# mur visible. El spawn dels jugadors també s'alinea amb aquesta columna. # mur visible. El spawn dels jugadors també s'alinea amb aquesta columna.
door_col: 10 door_col: 14
data: data:
- [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13] - [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13]
@@ -82,8 +82,6 @@ data:
# d'obertura (262/263). La momia al corredor central. L'antorxa al costat # d'obertura (262/263). La momia al corredor central. L'antorxa al costat
# superior esquerre per verificar render d'una entitat decorativa. # superior esquerre per verificar render d'una entitat decorativa.
entities: entities:
- type: torch
position: [4, 2]
- type: torch - type: torch
position: [8, 2] position: [8, 2]
- type: torch - type: torch
@@ -130,7 +128,6 @@ entities:
sarc_position: [2, 4] sarc_position: [2, 4]
- type: sarcophagus - type: sarcophagus
sarc_position: [3, 4] sarc_position: [3, 4]
- type: door
- type: mummy - type: mummy
position: [10, 7] position: [10, 7]
- type: hole - type: hole
@@ -145,3 +142,4 @@ entities:
position: [8, 5] position: [8, 5]
- type: mummy - type: mummy
position: [8, 9] position: [8, 9]
- type: door
+40 -12
View File
@@ -28,6 +28,7 @@
#include "game/entities/entity_writer.hpp" #include "game/entities/entity_writer.hpp"
#include "game/levels/level_manager.hpp" #include "game/levels/level_manager.hpp"
#include "game/map/map.hpp" #include "game/map/map.hpp"
#include "game/map/map_writer.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"
@@ -332,7 +333,11 @@ void Editor::exit(Tilemap& tilemap, const EntityManager& entities) {
const auto ABS_PATH = Resource::Helper::resolve(Levels::LevelManager::get().currentPath()); const auto ABS_PATH = Resource::Helper::resolve(Levels::LevelManager::get().currentPath());
const bool TILES_OK = tilemap.save(ABS_PATH); const bool TILES_OK = tilemap.save(ABS_PATH);
const bool ENTITIES_OK = EntityWriter::saveEntitiesBlock(ABS_PATH, gatherSpawns(entities)); const bool ENTITIES_OK = EntityWriter::saveEntitiesBlock(ABS_PATH, gatherSpawns(entities));
const bool SAVED = TILES_OK && ENTITIES_OK; // Persistim també `geometry.door_col`: l'editor permet canviar la
// columna de la porta, així que la geometry del YAML ha de
// reflectir-ho a la pròxima càrrega.
const bool GEO_OK = MapWriter::saveDoorCol(ABS_PATH, Map::door_col);
const bool SAVED = TILES_OK && ENTITIES_OK && GEO_OK;
Notification::notify(Locale::get().text(SAVED ? "input.editor_off" : "input.editor_save_error")); Notification::notify(Locale::get().text(SAVED ? "input.editor_off" : "input.editor_save_error"));
} }
@@ -430,7 +435,7 @@ void Editor::update(int /*delta_ms*/, Tilemap& tilemap, EntityManager& entities)
updateEntityBrush(entities, CX, CY); updateEntityBrush(entities, CX, CY);
break; break;
case Mode::HAND: case Mode::HAND:
updateHand(entities, CX, CY); updateHand(tilemap, entities, CX, CY);
break; break;
} }
} }
@@ -574,7 +579,7 @@ namespace {
} // namespace } // namespace
void Editor::updateHand(EntityManager& entities, int canvas_x, int canvas_y) { void Editor::updateHand(const Tilemap& tilemap, EntityManager& entities, int canvas_x, int canvas_y) {
const auto LIST = entities.getEntities(); const auto LIST = entities.getEntities();
hand_hover_idx_ = entityAtPixel(LIST, canvas_x, canvas_y); hand_hover_idx_ = entityAtPixel(LIST, canvas_x, canvas_y);
if (hand_selected_idx_ >= static_cast<int>(LIST.size())) { hand_selected_idx_ = -1; } if (hand_selected_idx_ >= static_cast<int>(LIST.size())) { hand_selected_idx_ = -1; }
@@ -582,7 +587,7 @@ void Editor::updateHand(EntityManager& entities, int canvas_x, int canvas_y) {
for (const auto& ev : EventBuffer::events) { for (const auto& ev : EventBuffer::events) {
const bool IS_MOUSE_DOWN = (ev.type == SDL_EVENT_MOUSE_BUTTON_DOWN); const bool IS_MOUSE_DOWN = (ev.type == SDL_EVENT_MOUSE_BUTTON_DOWN);
if (IS_MOUSE_DOWN && ev.button.button == SDL_BUTTON_LEFT) { if (IS_MOUSE_DOWN && ev.button.button == SDL_BUTTON_LEFT) {
handleHandLeftClick(entities); handleHandLeftClick(tilemap, entities);
} else if (IS_MOUSE_DOWN && ev.button.button == SDL_BUTTON_RIGHT) { } else if (IS_MOUSE_DOWN && ev.button.button == SDL_BUTTON_RIGHT) {
handleHandRightClick(); handleHandRightClick();
} else if (isDeleteKey(ev) && hand_selected_idx_ >= 0) { } else if (isDeleteKey(ev) && hand_selected_idx_ >= 0) {
@@ -591,7 +596,7 @@ void Editor::updateHand(EntityManager& entities, int canvas_x, int canvas_y) {
} }
} }
void Editor::handleHandLeftClick(EntityManager& entities) { void Editor::handleHandLeftClick(const Tilemap& tilemap, EntityManager& entities) {
if (hand_selected_idx_ < 0) { if (hand_selected_idx_ < 0) {
// Sense selecció: si pulsem sobre una entitat, seleccionem. // Sense selecció: si pulsem sobre una entitat, seleccionem.
if (hand_hover_idx_ >= 0) { hand_selected_idx_ = hand_hover_idx_; } if (hand_hover_idx_ >= 0) { hand_selected_idx_ = hand_hover_idx_; }
@@ -608,7 +613,7 @@ void Editor::handleHandLeftClick(EntityManager& entities) {
return; return;
} }
// Cel·la sense entitat: intenta moure-hi la seleccionada. // Cel·la sense entitat: intenta moure-hi la seleccionada.
if (tryMoveSelected(entities)) { hand_selected_idx_ = -1; } if (tryMoveSelected(tilemap, entities)) { hand_selected_idx_ = -1; }
} }
void Editor::handleHandRightClick() { void Editor::handleHandRightClick() {
@@ -619,7 +624,7 @@ void Editor::handleHandRightClick() {
} }
} }
auto Editor::tryMoveSelected(EntityManager& entities) const -> bool { auto Editor::tryMoveSelected(const Tilemap& tilemap, EntityManager& entities) const -> bool {
if (hand_selected_idx_ < 0 || cursor_col_ < 0 || cursor_row_ < 0) { return false; } if (hand_selected_idx_ < 0 || cursor_col_ < 0 || cursor_row_ < 0) { return false; }
auto list = entities.getEntities(); auto list = entities.getEntities();
if (hand_selected_idx_ >= static_cast<int>(list.size())) { return false; } if (hand_selected_idx_ >= static_cast<int>(list.size())) { return false; }
@@ -627,12 +632,35 @@ auto Editor::tryMoveSelected(EntityManager& entities) const -> bool {
auto* target = list[hand_selected_idx_]; auto* target = list[hand_selected_idx_];
const std::string TYPE = target->getName(); const std::string TYPE = target->getName();
// Construïm el nou spawn excloent l'entitat que mou del check de if (TYPE == "door") {
// solapaments: si no, sempre xocaria amb ella mateixa. Per a la // La porta viu sempre al mur trasser; només varia la columna.
// porta, el "moviment" actualment és no-op (no portem encara la // El sprite ocupa 3 cel·les (col-1, col, col+1), així que la
// columna a YAML), així que el descartem amb claredat. // columna central vàlida és [1, cols-2]. La fila no influeix
if (TYPE == "door") { return false; } // (l'usuari pot clicar a qualsevol fila): només importa la
// columna del cursor.
const int COLS = tilemap.getCols();
if (cursor_col_ < 1 || cursor_col_ > COLS - 2) { return false; }
if (cursor_col_ == Map::door_col) { return false; }
// Recolocació: actualitzem `Map::door_col` (i el seu derivat
// `door_corridor_x`), borrem l'entitat porta actual i en
// creem una de nova: `placeDoor` la posicionarà segons el
// nou `Map::door_col`. La persistència al YAML
// (`geometry.door_col`) es fa a `Editor::exit` a banda dels
// blocs `data:` i `entities:`.
Map::door_col = cursor_col_;
Map::door_corridor_x = (Map::door_col * Map::tile_size) + Map::tilemap_origin_x;
if (!entities.removeEntity(target)) { return false; }
try {
const std::string STR = "type: door\n";
auto node = fkyaml::node::deserialize(STR.begin(), STR.end());
return entities.addSpawn(node) != nullptr;
} catch (const std::exception&) {
return false;
}
}
// Resta d'entitats: snap a cel·la del cursor amb check de
// solapaments excloent la pròpia.
std::vector<Entity*> filtered; std::vector<Entity*> filtered;
filtered.reserve(list.size() - 1); filtered.reserve(list.size() - 1);
std::ranges::copy_if(list, std::back_inserter(filtered), [target](const Entity* e) { return e != target; }); std::ranges::copy_if(list, std::back_inserter(filtered), [target](const Entity* e) { return e != target; });
+7 -4
View File
@@ -97,7 +97,7 @@ class Editor {
// 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(EntityManager& entities, 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 updateHand(const Tilemap& tilemap, 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);
@@ -111,12 +111,15 @@ class Editor {
// Mou l'entitat seleccionada a la cel·la del cursor: borra l'antiga // 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 // i crida `addSpawn` amb la nova posició. Reusa la mateixa lògica
// de snap/solapament que la col·locació nova, excloent l'entitat // de snap/solapament que la col·locació nova, excloent l'entitat
// que es mou del check de solapament. // que es mou del check de solapament. Cas especial per a la porta:
auto tryMoveSelected(EntityManager& entities) const -> bool; // el sprite ocupa 3 cols (col-1, col, col+1) sobre el mur superior;
// per tant la columna central vàlida és [1, cols-2] — `tilemap` és
// necessari per saber `cols`.
auto tryMoveSelected(const Tilemap& tilemap, EntityManager& entities) const -> bool;
// Sub-handlers de `updateHand`. Aïllats per mantenir baixa la // Sub-handlers de `updateHand`. Aïllats per mantenir baixa la
// complexitat cognitiva del bucle d'esdeveniments. // complexitat cognitiva del bucle d'esdeveniments.
void handleHandLeftClick(EntityManager& entities); void handleHandLeftClick(const Tilemap& tilemap, EntityManager& entities);
void handleHandRightClick(); void handleHandRightClick();
// Borra l'entitat seleccionada. La validació de constraints // Borra l'entitat seleccionada. La validació de constraints
+7
View File
@@ -179,6 +179,13 @@ class Player : public AnimatedSprite {
// contra mur) s'emparen automàticament. // contra mur) s'emparen automàticament.
void setOtherPlayers(const std::vector<Player*>* all) { all_players_ = all; } void setOtherPlayers(const std::vector<Player*>* all) { all_players_ = all; }
// Refresca la còpia local de `Map::Info`. Necessari quan el mapa
// (geometria, p.ex. `door_col`) canvia entre carregaments —
// típicament després del editor in-game. Sense aquest refresc, el
// `clampToPlayfield` segueix usant el corridor antic i el jugador
// no troba la nova "obertura" del mur trasser.
void setMapInfo(const Map::Info& info) { map_ = info; }
// Marcat pel resolver jugador↔jugador quan aquest frame el jugador // Marcat pel resolver jugador↔jugador quan aquest frame el jugador
// és arrossegat per una empenta (no té input propi; es desplaça // és arrossegat per una empenta (no té input propi; es desplaça
// perquè l'altre l'empeny). Determina que l'animació del frame siga // perquè l'altre l'empeny). Determina que l'animació del frame siga
+123
View File
@@ -0,0 +1,123 @@
#include "game/map/map_writer.hpp"
#ifdef _DEBUG
#include <cstddef>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include <system_error>
namespace MapWriter {
namespace {
// Cerca la línia que comença per `<key>:` a columna 0 (top-level)
// o, si `min_indent>0`, a una indentació mínima donada — això ens
// permet trobar tant `geometry:` com una clau interna del bloc.
auto findKeyLine(const std::string& content, const std::string& key, std::size_t start_pos, std::size_t min_indent) -> std::size_t {
const std::string PREFIX = key + ":";
std::size_t pos = start_pos;
while (pos < content.size()) {
// Compta la indentació de la línia actual.
std::size_t indent = 0;
while (pos + indent < content.size() && (content[pos + indent] == ' ' || content[pos + indent] == '\t')) {
++indent;
}
const bool INDENT_OK = (min_indent == 0) ? (indent == 0) : (indent >= min_indent);
if (INDENT_OK && pos + indent + PREFIX.size() <= content.size() && content.compare(pos + indent, PREFIX.size(), PREFIX) == 0) {
const std::size_t AFTER = pos + indent + PREFIX.size();
if (AFTER >= content.size() || content[AFTER] == '\n' || content[AFTER] == '\r' || content[AFTER] == ' ' || content[AFTER] == '\t' || content[AFTER] == '#') {
return pos + indent;
}
}
const auto NL = content.find('\n', pos);
if (NL == std::string::npos) { break; }
pos = NL + 1;
// Si la línia actual és de columna 0 i no és la que busquem,
// i estem dins d'un bloc indentat, sortim (no és part del bloc).
if (min_indent > 0 && indent == 0 && content.compare(pos - 1, 1, "\n") == 0) {
// No usem aquest break agressiu perquè línies en blanc
// (indent=0) no han de tallar la cerca dins del bloc.
// Tractades implícitament pel control superior.
}
}
return std::string::npos;
}
auto atomicWrite(const std::string& path, const std::string& content) -> bool {
const auto TMP_PATH = path + ".tmp";
{
std::ofstream out(TMP_PATH, std::ios::binary | std::ios::trunc);
if (!out) { return false; }
out.write(content.data(), static_cast<std::streamsize>(content.size()));
out.close();
if (!out) {
std::error_code ec;
std::filesystem::remove(TMP_PATH, ec);
return false;
}
}
std::error_code ec;
std::filesystem::rename(TMP_PATH, path, ec);
if (ec) {
std::filesystem::remove(TMP_PATH, ec);
return false;
}
return true;
}
} // namespace
auto saveDoorCol(const std::string& path, int door_col) -> bool {
std::ifstream in(path, std::ios::binary);
if (!in) { return false; }
std::ostringstream ss;
ss << in.rdbuf();
const std::string CONTENT = ss.str();
in.close();
// 1) Localitza el bloc `geometry:` a columna 0.
const auto GEO_POS = findKeyLine(CONTENT, "geometry", 0, 0);
if (GEO_POS == std::string::npos) { return false; }
const auto GEO_LINE_END = CONTENT.find('\n', GEO_POS);
if (GEO_LINE_END == std::string::npos) { return false; }
const std::size_t BLOCK_START = GEO_LINE_END + 1;
// 2) Dins del bloc, busca `door_col:` indentada (>=1 espai/tab).
const auto KEY_POS = findKeyLine(CONTENT, "door_col", BLOCK_START, 1);
const std::string NEW_LINE = " door_col: " + std::to_string(door_col);
if (KEY_POS != std::string::npos) {
// Substitueix la línia sencera (manté indentació original
// recalculant el start de la línia).
std::size_t line_start = KEY_POS;
while (line_start > 0 && CONTENT[line_start - 1] != '\n') { --line_start; }
const auto LINE_END = CONTENT.find('\n', KEY_POS);
const auto END = (LINE_END == std::string::npos) ? CONTENT.size() : LINE_END;
const std::string NEW_CONTENT =
CONTENT.substr(0, line_start) + NEW_LINE + CONTENT.substr(END);
return atomicWrite(path, NEW_CONTENT);
}
// Si manca, l'afegim al final del bloc geometry (just abans
// de la primera línia top-level que segueix). Trobem la fi
// del bloc avançant mentre les línies són indentades o buides.
std::size_t pos = BLOCK_START;
while (pos < CONTENT.size()) {
const char C = CONTENT[pos];
const bool INDENTED = (C == ' ' || C == '\t');
const bool BLANK = (C == '\n' || C == '\r');
if (!INDENTED && !BLANK) { break; }
const auto NL = CONTENT.find('\n', pos);
pos = (NL == std::string::npos) ? CONTENT.size() : NL + 1;
}
const std::string NEW_CONTENT =
CONTENT.substr(0, pos) + NEW_LINE + "\n" + CONTENT.substr(pos);
return atomicWrite(path, NEW_CONTENT);
}
} // namespace MapWriter
#endif // _DEBUG
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#ifdef _DEBUG
#include <string>
// Writer surgical de claus dins del bloc `geometry:` d'un YAML de nivell.
// Reescriu un escalar (p.ex. `door_col: N`) preservant byte a byte la
// resta del fitxer: comentaris, ordre de claus, format i indentació de
// la resta del `geometry:` i la resta de seccions.
//
// Pensat per al editor (Debug): quan el dissenyador mou la porta, només
// canvia `door_col`; tota la resta del YAML segueix igual.
//
// Si `geometry:` no existeix al fitxer, retorna false. Si la clau no
// existeix dins del bloc, l'afegeix al final del bloc, indentada com
// les altres entrades del mateix nivell.
//
// Escriptura atòmica via `<path>.tmp` + `std::filesystem::rename`.
namespace MapWriter {
auto saveDoorCol(const std::string& path, int door_col) -> bool;
} // namespace MapWriter
#endif // _DEBUG
+6
View File
@@ -528,6 +528,12 @@ void GameScene::loadRoom(const std::string& yaml_path) {
tilemap_ = Tilemap(yaml_path); tilemap_ = Tilemap(yaml_path);
paint_layer_ = std::make_unique<PaintLayer>(tilemap_, &entity_manager_.getBus()); paint_layer_ = std::make_unique<PaintLayer>(tilemap_, &entity_manager_.getBus());
entity_manager_.reload(yaml_path); entity_manager_.reload(yaml_path);
// Refresquem la còpia local de Map::Info als players: la geometria
// pot haver canviat (p.ex. door_col del editor), i sense això
// `clampToPlayfield` continuaria usant el corridor antic i el
// jugador no podria sortir per la nova posició de la porta.
const auto INFO = Map::makeInfo();
for (auto& p : players_) { p->setMapInfo(INFO); }
// Tornem a fixar el comptador de "esperats" amb només els slots // Tornem a fixar el comptador de "esperats" amb només els slots
// actius — un slot DISABLED permanent (single-player) no compta. // actius — un slot DISABLED permanent (single-player) no compta.
const int ACTIVE_COUNT = std::ranges::count(players_starts_active_, true); const int ACTIVE_COUNT = std::ranges::count(players_starts_active_, true);