diff --git a/CMakeLists.txt b/CMakeLists.txt index ca7cb1c..ca81c35 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -116,6 +116,9 @@ set(APP_SOURCES source/game/map/map.cpp source/game/map/tilemap.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/levels/level_manager.cpp # Mòdul editor (només activa contingut en Debug via `#ifdef _DEBUG`). diff --git a/data/maps/p1/r1.yaml b/data/maps/p1/r1.yaml index 96e3874..10e028f 100644 --- a/data/maps/p1/r1.yaml +++ b/data/maps/p1/r1.yaml @@ -49,7 +49,7 @@ geometry: # 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 # mur visible. El spawn dels jugadors també s'alinea amb aquesta columna. - door_col: 10 + door_col: 14 data: - [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 # superior esquerre per verificar render d'una entitat decorativa. entities: - - type: torch - position: [4, 2] - type: torch position: [8, 2] - type: torch @@ -130,7 +128,6 @@ entities: sarc_position: [2, 4] - type: sarcophagus sarc_position: [3, 4] - - type: door - type: mummy position: [10, 7] - type: hole @@ -145,3 +142,4 @@ entities: position: [8, 5] - type: mummy position: [8, 9] + - type: door diff --git a/source/game/editor/editor.cpp b/source/game/editor/editor.cpp index 7d27bd4..2e05696 100644 --- a/source/game/editor/editor.cpp +++ b/source/game/editor/editor.cpp @@ -28,6 +28,7 @@ #include "game/entities/entity_writer.hpp" #include "game/levels/level_manager.hpp" #include "game/map/map.hpp" +#include "game/map/map_writer.hpp" #include "game/map/tilemap.hpp" #include "utils/defaults.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 bool TILES_OK = tilemap.save(ABS_PATH); 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")); } @@ -430,7 +435,7 @@ void Editor::update(int /*delta_ms*/, Tilemap& tilemap, EntityManager& entities) updateEntityBrush(entities, CX, CY); break; case Mode::HAND: - updateHand(entities, CX, CY); + updateHand(tilemap, entities, CX, CY); break; } } @@ -574,7 +579,7 @@ 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(); hand_hover_idx_ = entityAtPixel(LIST, canvas_x, canvas_y); if (hand_selected_idx_ >= static_cast(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) { const bool IS_MOUSE_DOWN = (ev.type == SDL_EVENT_MOUSE_BUTTON_DOWN); 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) { handleHandRightClick(); } 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) { // Sense selecció: si pulsem sobre una entitat, seleccionem. if (hand_hover_idx_ >= 0) { hand_selected_idx_ = hand_hover_idx_; } @@ -608,7 +613,7 @@ void Editor::handleHandLeftClick(EntityManager& entities) { return; } // 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() { @@ -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; } auto list = entities.getEntities(); if (hand_selected_idx_ >= static_cast(list.size())) { return false; } @@ -627,12 +632,35 @@ auto Editor::tryMoveSelected(EntityManager& entities) const -> bool { auto* target = list[hand_selected_idx_]; const std::string TYPE = target->getName(); - // Construïm el nou spawn excloent l'entitat que mou del check de - // solapaments: si no, sempre xocaria amb ella mateixa. Per a la - // porta, el "moviment" actualment és no-op (no portem encara la - // columna a YAML), així que el descartem amb claredat. - if (TYPE == "door") { return false; } + if (TYPE == "door") { + // La porta viu sempre al mur trasser; només varia la columna. + // El sprite ocupa 3 cel·les (col-1, col, col+1), així que la + // columna central vàlida és [1, cols-2]. La fila no influeix + // (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 filtered; filtered.reserve(list.size() - 1); std::ranges::copy_if(list, std::back_inserter(filtered), [target](const Entity* e) { return e != target; }); diff --git a/source/game/editor/editor.hpp b/source/game/editor/editor.hpp index e3ff0f4..6e7d814 100644 --- a/source/game/editor/editor.hpp +++ b/source/game/editor/editor.hpp @@ -97,7 +97,7 @@ class Editor { // des d'`update`. void updateTileBrush(Tilemap& tilemap, 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 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 // i crida `addSpawn` amb la nova posició. Reusa la mateixa lògica // de snap/solapament que la col·locació nova, excloent l'entitat - // que es mou del check de solapament. - auto tryMoveSelected(EntityManager& entities) const -> bool; + // que es mou del check de solapament. Cas especial per a la porta: + // 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 // complexitat cognitiva del bucle d'esdeveniments. - void handleHandLeftClick(EntityManager& entities); + void handleHandLeftClick(const Tilemap& tilemap, EntityManager& entities); void handleHandRightClick(); // Borra l'entitat seleccionada. La validació de constraints diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index c1a8617..150b94d 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -179,6 +179,13 @@ class Player : public AnimatedSprite { // contra mur) s'emparen automàticament. void setOtherPlayers(const std::vector* 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 // és arrossegat per una empenta (no té input propi; es desplaça // perquè l'altre l'empeny). Determina que l'animació del frame siga diff --git a/source/game/map/map_writer.cpp b/source/game/map/map_writer.cpp new file mode 100644 index 0000000..9e6e26e --- /dev/null +++ b/source/game/map/map_writer.cpp @@ -0,0 +1,123 @@ +#include "game/map/map_writer.hpp" + +#ifdef _DEBUG + +#include +#include +#include +#include +#include +#include + +namespace MapWriter { + + namespace { + + // Cerca la línia que comença per `:` 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(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 diff --git a/source/game/map/map_writer.hpp b/source/game/map/map_writer.hpp new file mode 100644 index 0000000..c248b17 --- /dev/null +++ b/source/game/map/map_writer.hpp @@ -0,0 +1,26 @@ +#pragma once + +#ifdef _DEBUG + +#include + +// 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 `.tmp` + `std::filesystem::rename`. +namespace MapWriter { + + auto saveDoorCol(const std::string& path, int door_col) -> bool; + +} // namespace MapWriter + +#endif // _DEBUG diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 47550ad..07b4610 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -528,6 +528,12 @@ void GameScene::loadRoom(const std::string& yaml_path) { tilemap_ = Tilemap(yaml_path); paint_layer_ = std::make_unique(tilemap_, &entity_manager_.getBus()); 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 // actius — un slot DISABLED permanent (single-player) no compta. const int ACTIVE_COUNT = std::ranges::count(players_starts_active_, true);