feat(editor): mou la porta i persisteix geometry.door_col
This commit is contained in:
1 parent
a254fdd669
commit
c003e6665e
8 files changed
+214
-20
No files matched your search
@@ -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`).
|
||||
|
||||
@@ -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
|
||||
@@ -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<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) {
|
||||
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<int>(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<Entity*> filtered;
|
||||
filtered.reserve(list.size() - 1);
|
||||
std::ranges::copy_if(list, std::back_inserter(filtered), [target](const Entity* e) { return e != target; });
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -179,6 +179,13 @@ class Player : public AnimatedSprite {
|
||||
// contra mur) s'emparen automàticament.
|
||||
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
|
||||
// és arrossegat per una empenta (no té input propi; es desplaça
|
||||
// perquè l'altre l'empeny). Determina que l'animació del frame siga
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -528,6 +528,12 @@ void GameScene::loadRoom(const std::string& yaml_path) {
|
||||
tilemap_ = Tilemap(yaml_path);
|
||||
paint_layer_ = std::make_unique<PaintLayer>(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);
|
||||
|
||||
Reference in new issue
Block a user