From fe17c35c6731df9d8634b4de71723672f4313647 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 26 Apr 2026 14:48:15 +0200 Subject: [PATCH] =?UTF-8?q?feat(levels):=20LevelManager=20amb=20=C3=ADndex?= =?UTF-8?q?=20de=20sales?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nou singleton Levels::LevelManager que llig data/maps/levels.yaml a la primera get() i exposa currentPath/currentPyramid/currentRoom/ currentLabel + advance/restart/setIndex. És pur estat 'on sóc' — no orquestra cap reload; la GameScene i l'Editor li preguntaran al pas següent. Afegida constant Defaults::Pyramid::LEVELS_YAML al costat de la MAP_YAML antiga (la substitució completa va al Step 3). --- CMakeLists.txt | 1 + source/game/levels/level_manager.cpp | 89 ++++++++++++++++++++++++++++ source/game/levels/level_manager.hpp | 74 +++++++++++++++++++++++ source/utils/defaults.hpp | 5 ++ 4 files changed, 169 insertions(+) create mode 100644 source/game/levels/level_manager.cpp create mode 100644 source/game/levels/level_manager.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 70df771..b3dcc80 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,6 +101,7 @@ set(APP_SOURCES source/game/map/tilemap.cpp source/game/map/tilemap_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`). # En Release el fitxer compila a un object buit i l'enllaçador no # arrossega cap símbol del joc final. diff --git a/source/game/levels/level_manager.cpp b/source/game/levels/level_manager.cpp new file mode 100644 index 0000000..dc4eb92 --- /dev/null +++ b/source/game/levels/level_manager.cpp @@ -0,0 +1,89 @@ +#include "game/levels/level_manager.hpp" + +#include +#include +#include +#include + +#include "core/resources/resource_helper.hpp" +#include "external/fkyaml_node.hpp" +#include "utils/defaults.hpp" + +namespace Levels { + + auto LevelManager::get() -> LevelManager& { + static LevelManager instance_; + return instance_; + } + + LevelManager::LevelManager() { loadIndex(); } + + void LevelManager::loadIndex() { + const std::string PATH = Defaults::Pyramid::LEVELS_YAML; + auto bytes = Resource::Helper::loadFile(PATH); + if (bytes.empty()) { + throw std::runtime_error("LevelManager: no s'ha pogut obrir " + PATH); + } + const auto* begin = reinterpret_cast(bytes.data()); + const auto* end = begin + bytes.size(); + auto yaml = fkyaml::node::deserialize(begin, end); + + if (!yaml.contains("sequence") || !yaml["sequence"].is_sequence()) { + throw std::runtime_error("LevelManager: manca o és invàlid `sequence:` a " + PATH); + } + const auto& seq = yaml["sequence"]; + entries_.clear(); + entries_.reserve(seq.size()); + for (const auto& e : seq) { + Entry entry{ + .pyramid = e["pyramid"].get_value(), + .room = e["room"].get_value(), + .file = e["file"].get_value(), + }; + entries_.push_back(std::move(entry)); + } + if (entries_.empty()) { + throw std::runtime_error("LevelManager: la seqüència de " + PATH + " és buida"); + } + index_ = 0; + std::cout << "LevelManager: " << entries_.size() << " sales carregades de " + << PATH << " (començant a " << currentLabel() << ")\n"; + } + + auto LevelManager::currentPath() const -> const std::string& { + return entries_[index_].file; + } + + auto LevelManager::currentPyramid() const -> int { return entries_[index_].pyramid; } + auto LevelManager::currentRoom() const -> int { return entries_[index_].room; } + + auto LevelManager::currentLabel() const -> std::string { + return "P" + std::to_string(currentPyramid()) + "-R" + std::to_string(currentRoom()); + } + + auto LevelManager::index() const -> int { return index_; } + auto LevelManager::count() const -> int { return static_cast(entries_.size()); } + + auto LevelManager::advance() -> bool { + if (index_ + 1 >= count()) { return false; } + ++index_; + std::cout << "LevelManager: avança a " << currentLabel() + << " (" << currentPath() << ")\n"; + return true; + } + + void LevelManager::restart() { + index_ = 0; + std::cout << "LevelManager: restart a " << currentLabel() << '\n'; + } + + void LevelManager::setIndex(int new_index) { + const int CLAMPED = std::clamp(new_index, 0, count() - 1); + if (CLAMPED != index_) { + index_ = CLAMPED; + std::cout << "LevelManager: setIndex(" << new_index << ") → " + << currentLabel() << " (" << currentPath() << ")\n"; + } + } + +} // namespace Levels diff --git a/source/game/levels/level_manager.hpp b/source/game/levels/level_manager.hpp new file mode 100644 index 0000000..c6532f4 --- /dev/null +++ b/source/game/levels/level_manager.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include + +namespace Levels { + + // Font de veritat de la progressió de sales del joc. Carrega + // `data/maps/levels.yaml` a la primera crida a `get()` i exposa l'estat + // "on sóc ara" + les operacions per moure'm. La GameScene, l'Editor i + // els salts de debug consulten aquest singleton; ningú no manté el seu + // propi índex de progressió. + // + // Meyers singleton: la destrucció estàtica només toca std::string i + // std::vector, així que és segura sense ordenació explícita. + class LevelManager { + public: + static auto get() -> LevelManager&; + + LevelManager(const LevelManager&) = delete; + LevelManager(LevelManager&&) = delete; + auto operator=(const LevelManager&) -> LevelManager& = delete; + auto operator=(LevelManager&&) -> LevelManager& = delete; + + // Una entrada del `levels.yaml`. `file` és la ruta relativa al data + // root (mateixa convenció que la resta de YAML del joc); pyramid i + // room són purament informatius (logs i text d'intro). + struct Entry { + int pyramid{0}; + int room{0}; + std::string file; + }; + + // Ruta del nivell actualment apuntat. Sempre vàlida un cop carregat + // l'índex (entries_ no pot estar buit). Pensada per a `Map::loadFrom`, + // `EntityManager::reload` i l'Editor. + [[nodiscard]] auto currentPath() const -> const std::string&; + [[nodiscard]] auto currentPyramid() const -> int; + [[nodiscard]] auto currentRoom() const -> int; + // "P1-R3" — útil per a logs i el text d'intro de sala. + [[nodiscard]] auto currentLabel() const -> std::string; + + // Posició dins de la seqüència. `count()` és el total de sales + // carregades del YAML (no necessàriament 30: si afegeixes/treus + // entrades de `levels.yaml`, el manager s'adapta). + [[nodiscard]] auto index() const -> int; + [[nodiscard]] auto count() const -> int; + + // Avança al següent nivell. Retorna true si avança, false si ja + // estava a l'últim. NO fa loop — la GameScene decideix la política + // (loop a la primera, fi de joc, etc.) segons el context. + auto advance() -> bool; + + // Torna al primer nivell. + void restart(); + + // Salta a un índex concret. Clamp als bordes [0, count()-1]. + // Pensat per als hotkeys debug i per `debug.yaml.starting_level`. + void setIndex(int new_index); + + private: + LevelManager(); + ~LevelManager() = default; + + // Carrega `data/maps/levels.yaml` via `Resource::Helper::loadFile`. + // Si manca, és invàlid o té la seqüència buida, llança — no té sentit + // continuar sense progressió de sales. + void loadIndex(); + + std::vector entries_; + int index_{0}; + }; + +} // namespace Levels diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index 0b4149e..0d26232 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -231,6 +231,11 @@ namespace Defaults { // en contingui cap literal; afegir una piràmide nova serà un altre // conjunt de constants + un Config distint passat al ctor. namespace Pyramid { + // Índex ordenat de les 30 sales (5 piràmides × 6 sales). El consumeix + // `Levels::LevelManager` a la primera invocació; cada entrada apunta + // a un `data/maps/p/r.yaml`. Substituirà `MAP_YAML` quan el + // wiring del LevelManager estigui complet. + constexpr const char* LEVELS_YAML = "data/maps/levels.yaml"; constexpr const char* MAP_YAML = "data/maps/pyramid.yaml"; constexpr const char* PLAYER_SPRITE = "data/player/sam.gif"; constexpr const char* PLAYER_YAML = "data/player/sam.yaml";