feat(levels): LevelManager amb índex de sales

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).
This commit is contained in:
JailDesigner committed 2026-04-26 14:48:15 +02:00
1 parent 8fd06887d1
commit fe17c35c67
4 files changed
+169

No files matched your search

+1
View File
@@ -101,6 +101,7 @@ set(APP_SOURCES
source/game/map/tilemap.cpp source/game/map/tilemap.cpp
source/game/map/tilemap_writer.cpp source/game/map/tilemap_writer.cpp
source/game/map/paint_layer.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`). # 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 # En Release el fitxer compila a un object buit i l'enllaçador no
# arrossega cap símbol del joc final. # arrossega cap símbol del joc final.
+89
View File
@@ -0,0 +1,89 @@
#include "game/levels/level_manager.hpp"
#include <algorithm>
#include <iostream>
#include <stdexcept>
#include <utility>
#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<const char*>(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<int>(),
.room = e["room"].get_value<int>(),
.file = e["file"].get_value<std::string>(),
};
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<int>(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
+74
View File
@@ -0,0 +1,74 @@
#pragma once
#include <string>
#include <vector>
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<Entry> entries_;
int index_{0};
};
} // namespace Levels
+5
View File
@@ -231,6 +231,11 @@ namespace Defaults {
// en contingui cap literal; afegir una piràmide nova serà un altre // en contingui cap literal; afegir una piràmide nova serà un altre
// conjunt de constants + un Config distint passat al ctor. // conjunt de constants + un Config distint passat al ctor.
namespace Pyramid { 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<N>/r<M>.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* MAP_YAML = "data/maps/pyramid.yaml";
constexpr const char* PLAYER_SPRITE = "data/player/sam.gif"; constexpr const char* PLAYER_SPRITE = "data/player/sam.gif";
constexpr const char* PLAYER_YAML = "data/player/sam.yaml"; constexpr const char* PLAYER_YAML = "data/player/sam.yaml";