pantalla de carrega no bloquejant

streaming de audio per evitar precárrega i descompresió a memoria
This commit is contained in:
2026-04-13 19:29:05 +02:00
parent 585c93054e
commit 9b8820ffa3
9 changed files with 587 additions and 261 deletions

View File

@@ -2,10 +2,6 @@
#include <SDL3/SDL.h>
#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h> // Para emscripten_sleep
#endif
#include <algorithm> // Para find_if
#include <cstdlib> // Para exit, size_t
#include <fstream> // Para ifstream, istreambuf_iterator
@@ -42,10 +38,10 @@ namespace Resource {
// [SINGLETON] Con este método obtenemos el objeto cache y podemos trabajar con él
auto Cache::get() -> Cache* { return Cache::cache; }
// Constructor
// Constructor — no dispara la carga. Director llama a beginLoad() + loadStep()
// desde iterate() para que el bucle SDL3 esté vivo durante la carga.
Cache::Cache()
: loading_text_(Screen::get()->getText()) {
load();
}
// Vacia todos los vectores de recursos
@@ -57,12 +53,11 @@ namespace Resource {
text_files_.clear();
texts_.clear();
animations_.clear();
rooms_.clear();
}
// Carga todos los recursos
// Carga todos los recursos de golpe (usado solo por reload() en hot-reload de debug)
void Cache::load() {
// Nota: el overlay de debug (RenderInfo) se inicializa después de esta carga,
// por lo que updateZoomFactor() se llamará correctamente en RenderInfo::init().
calculateTotal();
Screen::get()->setBorderColor(static_cast<Uint8>(PaletteColor::BLACK));
std::cout << "\n** LOADING RESOURCES" << '\n';
@@ -77,7 +72,162 @@ namespace Resource {
std::cout << "\n** RESOURCES LOADED" << '\n';
}
// Recarga todos los recursos
// Prepara el loader incremental. Director lo llama una vez tras Cache::init().
void Cache::beginLoad() {
calculateTotal();
Screen::get()->setBorderColor(static_cast<Uint8>(PaletteColor::BLACK));
std::cout << "\n** LOADING RESOURCES (incremental)" << '\n';
stage_ = LoadStage::SOUNDS;
stage_index_ = 0;
}
auto Cache::isLoadDone() const -> bool {
return stage_ == LoadStage::DONE;
}
// Carga assets hasta agotar el presupuesto de tiempo o completar todas las etapas.
// Devuelve true cuando ya no queda nada por cargar.
auto Cache::loadStep(int budget_ms) -> bool {
if (stage_ == LoadStage::DONE) return true;
const Uint64 start_ns = SDL_GetTicksNS();
const Uint64 budget_ns = static_cast<Uint64>(budget_ms) * 1'000'000ULL;
auto listOf = [](List::Type t) { return List::get()->getListByType(t); };
while (stage_ != LoadStage::DONE) {
switch (stage_) {
case LoadStage::SOUNDS: {
auto list = listOf(List::Type::SOUND);
if (stage_index_ == 0) {
std::cout << "\n>> SOUND FILES" << '\n';
sounds_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::MUSICS;
stage_index_ = 0;
break;
}
loadOneSound(stage_index_++);
break;
}
case LoadStage::MUSICS: {
auto list = listOf(List::Type::MUSIC);
if (stage_index_ == 0) {
std::cout << "\n>> MUSIC FILES" << '\n';
musics_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::SURFACES;
stage_index_ = 0;
break;
}
loadOneMusic(stage_index_++);
break;
}
case LoadStage::SURFACES: {
auto list = listOf(List::Type::BITMAP);
if (stage_index_ == 0) {
std::cout << "\n>> SURFACES" << '\n';
surfaces_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::SURFACES_POST;
stage_index_ = 0;
break;
}
loadOneSurface(stage_index_++);
break;
}
case LoadStage::SURFACES_POST: {
finalizeSurfaces();
stage_ = LoadStage::PALETTES;
stage_index_ = 0;
break;
}
case LoadStage::PALETTES: {
auto list = listOf(List::Type::PALETTE);
if (stage_index_ == 0) {
std::cout << "\n>> PALETTES" << '\n';
palettes_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::TEXT_FILES;
stage_index_ = 0;
break;
}
loadOnePalette(stage_index_++);
break;
}
case LoadStage::TEXT_FILES: {
auto list = listOf(List::Type::FONT);
if (stage_index_ == 0) {
std::cout << "\n>> TEXT FILES" << '\n';
text_files_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::ANIMATIONS;
stage_index_ = 0;
break;
}
loadOneTextFile(stage_index_++);
break;
}
case LoadStage::ANIMATIONS: {
auto list = listOf(List::Type::ANIMATION);
if (stage_index_ == 0) {
std::cout << "\n>> ANIMATIONS" << '\n';
animations_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::ROOMS;
stage_index_ = 0;
break;
}
loadOneAnimation(stage_index_++);
break;
}
case LoadStage::ROOMS: {
auto list = listOf(List::Type::ROOM);
if (stage_index_ == 0) {
std::cout << "\n>> ROOMS" << '\n';
rooms_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::TEXTS;
stage_index_ = 0;
break;
}
loadOneRoom(stage_index_++);
break;
}
case LoadStage::TEXTS: {
// createText itera sobre una lista fija de 5 fuentes
constexpr size_t TEXT_COUNT = 5;
if (stage_index_ == 0) {
std::cout << "\n>> CREATING TEXT_OBJECTS" << '\n';
texts_.clear();
}
if (stage_index_ >= TEXT_COUNT) {
stage_ = LoadStage::DONE;
stage_index_ = 0;
std::cout << "\n** RESOURCES LOADED" << '\n';
break;
}
createOneText(stage_index_++);
break;
}
case LoadStage::DONE:
break;
}
if ((SDL_GetTicksNS() - start_ns) >= budget_ns) break;
}
return stage_ == LoadStage::DONE;
}
// Recarga todos los recursos (síncrono, solo para hot-reload de debug)
void Cache::reload() {
clear();
load();
@@ -221,96 +371,96 @@ namespace Resource {
throw;
}
// Carga los sonidos
void Cache::loadSounds() { // NOLINT(readability-convert-member-functions-to-static)
std::cout << "\n>> SOUND FILES" << '\n';
// Lista fija de text objects. Compartida entre createText() y createOneText(i).
namespace {
struct TextObjectInfo {
std::string key; // Identificador del recurso
std::string texture_file; // Nombre del archivo de textura
std::string text_file; // Nombre del archivo de texto
};
const std::vector<TextObjectInfo>& getTextObjectInfos() {
static const std::vector<TextObjectInfo> info = {
{.key = "aseprite", .texture_file = "aseprite.gif", .text_file = "aseprite.fnt"},
{.key = "gauntlet", .texture_file = "gauntlet.gif", .text_file = "gauntlet.fnt"},
{.key = "smb2", .texture_file = "smb2.gif", .text_file = "smb2.fnt"},
{.key = "subatomic", .texture_file = "subatomic.gif", .text_file = "subatomic.fnt"},
{.key = "8bithud", .texture_file = "8bithud.gif", .text_file = "8bithud.fnt"}};
return info;
}
} // namespace
// --- Helpers incrementales (un asset por llamada) ---
void Cache::loadOneSound(size_t index) {
auto list = List::get()->getListByType(List::Type::SOUND);
sounds_.clear();
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
JA_Sound_t* sound = nullptr;
for (const auto& l : list) {
try {
auto name = getFileName(l);
setCurrentLoading(name);
JA_Sound_t* sound = nullptr;
// Try loading from resource pack first
auto audio_data = Helper::loadFile(l);
if (!audio_data.empty()) {
sound = JA_LoadSound(audio_data.data(), static_cast<Uint32>(audio_data.size()));
}
// Fallback to file path if memory loading failed
if (sound == nullptr) {
sound = JA_LoadSound(l.c_str());
}
if (sound == nullptr) {
throw std::runtime_error("Failed to decode audio file");
}
sounds_.emplace_back(SoundResource{.name = name, .sound = sound});
printWithDots("Sound : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("SOUND", l, e);
auto audio_data = Helper::loadFile(l);
if (!audio_data.empty()) {
sound = JA_LoadSound(audio_data.data(), static_cast<Uint32>(audio_data.size()));
}
if (sound == nullptr) {
sound = JA_LoadSound(l.c_str());
}
if (sound == nullptr) {
throw std::runtime_error("Failed to decode audio file");
}
sounds_.emplace_back(SoundResource{.name = name, .sound = sound});
printWithDots("Sound : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("SOUND", l, e);
}
}
// Carga las musicas
void Cache::loadMusics() { // NOLINT(readability-convert-member-functions-to-static)
std::cout << "\n>> MUSIC FILES" << '\n';
void Cache::loadOneMusic(size_t index) {
auto list = List::get()->getListByType(List::Type::MUSIC);
musics_.clear();
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
JA_Music_t* music = nullptr;
for (const auto& l : list) {
try {
auto name = getFileName(l);
setCurrentLoading(name);
JA_Music_t* music = nullptr;
// Try loading from resource pack first
auto audio_data = Helper::loadFile(l);
if (!audio_data.empty()) {
music = JA_LoadMusic(audio_data.data(), static_cast<Uint32>(audio_data.size()));
}
// Fallback to file path if memory loading failed
if (music == nullptr) {
music = JA_LoadMusic(l.c_str());
}
if (music == nullptr) {
throw std::runtime_error("Failed to decode music file");
}
musics_.emplace_back(MusicResource{.name = name, .music = music});
printWithDots("Music : ", name, "[ LOADED ]");
updateLoadingProgress(1);
} catch (const std::exception& e) {
throwLoadError("MUSIC", l, e);
auto audio_data = Helper::loadFile(l);
if (!audio_data.empty()) {
music = JA_LoadMusic(audio_data.data(), static_cast<Uint32>(audio_data.size()));
}
if (music == nullptr) {
music = JA_LoadMusic(l.c_str());
}
if (music == nullptr) {
throw std::runtime_error("Failed to decode music file");
}
musics_.emplace_back(MusicResource{.name = name, .music = music});
printWithDots("Music : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("MUSIC", l, e);
}
}
// Carga las texturas
void Cache::loadSurfaces() { // NOLINT(readability-convert-member-functions-to-static)
std::cout << "\n>> SURFACES" << '\n';
void Cache::loadOneSurface(size_t index) {
auto list = List::get()->getListByType(List::Type::BITMAP);
surfaces_.clear();
for (const auto& l : list) {
try {
auto name = getFileName(l);
setCurrentLoading(name);
surfaces_.emplace_back(SurfaceResource{.name = name, .surface = std::make_shared<Surface>(l)});
surfaces_.back().surface->setTransparentColor(0);
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("BITMAP", l, e);
}
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
surfaces_.emplace_back(SurfaceResource{.name = name, .surface = std::make_shared<Surface>(l)});
surfaces_.back().surface->setTransparentColor(0);
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("BITMAP", l, e);
}
}
void Cache::finalizeSurfaces() {
// Reconfigura el color transparente de algunas surfaces
getSurface("loading_screen_color.gif")->setTransparentColor();
getSurface("ending1.gif")->setTransparentColor();
@@ -321,108 +471,132 @@ namespace Resource {
getSurface("standard.gif")->setTransparentColor(16);
}
// Carga las paletas
void Cache::loadPalettes() { // NOLINT(readability-convert-member-functions-to-static)
void Cache::loadOnePalette(size_t index) {
auto list = List::get()->getListByType(List::Type::PALETTE);
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
palettes_.emplace_back(ResourcePalette{.name = name, .palette = readPalFile(l)});
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("PALETTE", l, e);
}
}
void Cache::loadOneTextFile(size_t index) {
auto list = List::get()->getListByType(List::Type::FONT);
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
text_files_.emplace_back(TextFileResource{.name = name, .text_file = Text::loadTextFile(l)});
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("FONT", l, e);
}
}
void Cache::loadOneAnimation(size_t index) {
auto list = List::get()->getListByType(List::Type::ANIMATION);
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
auto yaml_bytes = Helper::loadFile(l);
if (yaml_bytes.empty()) {
throw std::runtime_error("File is empty or could not be loaded");
}
animations_.emplace_back(AnimationResource{.name = name, .yaml_data = yaml_bytes});
printWithDots("Animation : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("ANIMATION", l, e);
}
}
void Cache::loadOneRoom(size_t index) {
auto list = List::get()->getListByType(List::Type::ROOM);
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
rooms_.emplace_back(RoomResource{.name = name, .room = std::make_shared<Room::Data>(Room::loadYAML(l))});
printWithDots("Room : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("ROOM", l, e);
}
}
void Cache::createOneText(size_t index) {
const auto& infos = getTextObjectInfos();
const auto& res_info = infos[index];
texts_.emplace_back(TextResource{
.name = res_info.key,
.text = std::make_shared<Text>(getSurface(res_info.texture_file), getTextFile(res_info.text_file))});
printWithDots("Text : ", res_info.key, "[ DONE ]");
}
// --- Bucles completos (solo usados por reload() síncrono) ---
void Cache::loadSounds() {
std::cout << "\n>> SOUND FILES" << '\n';
auto list = List::get()->getListByType(List::Type::SOUND);
sounds_.clear();
for (size_t i = 0; i < list.size(); ++i) loadOneSound(i);
}
void Cache::loadMusics() {
std::cout << "\n>> MUSIC FILES" << '\n';
auto list = List::get()->getListByType(List::Type::MUSIC);
musics_.clear();
for (size_t i = 0; i < list.size(); ++i) loadOneMusic(i);
}
void Cache::loadSurfaces() {
std::cout << "\n>> SURFACES" << '\n';
auto list = List::get()->getListByType(List::Type::BITMAP);
surfaces_.clear();
for (size_t i = 0; i < list.size(); ++i) loadOneSurface(i);
finalizeSurfaces();
}
void Cache::loadPalettes() {
std::cout << "\n>> PALETTES" << '\n';
auto list = List::get()->getListByType(List::Type::PALETTE);
palettes_.clear();
for (const auto& l : list) {
try {
auto name = getFileName(l);
setCurrentLoading(name);
palettes_.emplace_back(ResourcePalette{.name = name, .palette = readPalFile(l)});
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("PALETTE", l, e);
}
}
for (size_t i = 0; i < list.size(); ++i) loadOnePalette(i);
}
// Carga los ficheros de texto
void Cache::loadTextFiles() { // NOLINT(readability-convert-member-functions-to-static)
void Cache::loadTextFiles() {
std::cout << "\n>> TEXT FILES" << '\n';
auto list = List::get()->getListByType(List::Type::FONT);
text_files_.clear();
for (const auto& l : list) {
try {
auto name = getFileName(l);
setCurrentLoading(name);
text_files_.emplace_back(TextFileResource{.name = name, .text_file = Text::loadTextFile(l)});
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("FONT", l, e);
}
}
for (size_t i = 0; i < list.size(); ++i) loadOneTextFile(i);
}
// Carga las animaciones
void Cache::loadAnimations() { // NOLINT(readability-convert-member-functions-to-static)
void Cache::loadAnimations() {
std::cout << "\n>> ANIMATIONS" << '\n';
auto list = List::get()->getListByType(List::Type::ANIMATION);
animations_.clear();
for (const auto& l : list) {
try {
auto name = getFileName(l);
setCurrentLoading(name);
// Cargar bytes del archivo YAML sin parsear (carga lazy)
auto yaml_bytes = Helper::loadFile(l);
if (yaml_bytes.empty()) {
throw std::runtime_error("File is empty or could not be loaded");
}
animations_.emplace_back(AnimationResource{.name = name, .yaml_data = yaml_bytes});
printWithDots("Animation : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("ANIMATION", l, e);
}
}
for (size_t i = 0; i < list.size(); ++i) loadOneAnimation(i);
}
// Carga las habitaciones desde archivos YAML
void Cache::loadRooms() { // NOLINT(readability-convert-member-functions-to-static)
void Cache::loadRooms() {
std::cout << "\n>> ROOMS" << '\n';
auto list = List::get()->getListByType(List::Type::ROOM);
rooms_.clear();
for (const auto& l : list) {
try {
auto name = getFileName(l);
setCurrentLoading(name);
rooms_.emplace_back(RoomResource{.name = name, .room = std::make_shared<Room::Data>(Room::loadYAML(l))});
printWithDots("Room : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("ROOM", l, e);
}
}
for (size_t i = 0; i < list.size(); ++i) loadOneRoom(i);
}
void Cache::createText() { // NOLINT(readability-convert-member-functions-to-static)
struct ResourceInfo {
std::string key; // Identificador del recurso
std::string texture_file; // Nombre del archivo de textura
std::string text_file; // Nombre del archivo de texto
};
void Cache::createText() {
std::cout << "\n>> CREATING TEXT_OBJECTS" << '\n';
std::vector<ResourceInfo> resources = {
{.key = "aseprite", .texture_file = "aseprite.gif", .text_file = "aseprite.fnt"},
{.key = "gauntlet", .texture_file = "gauntlet.gif", .text_file = "gauntlet.fnt"},
{.key = "smb2", .texture_file = "smb2.gif", .text_file = "smb2.fnt"},
{.key = "subatomic", .texture_file = "subatomic.gif", .text_file = "subatomic.fnt"},
{.key = "8bithud", .texture_file = "8bithud.gif", .text_file = "8bithud.fnt"}};
for (const auto& res_info : resources) {
texts_.emplace_back(TextResource{.name = res_info.key, .text = std::make_shared<Text>(getSurface(res_info.texture_file), getTextFile(res_info.text_file))});
printWithDots("Text : ", res_info.key, "[ DONE ]");
}
texts_.clear();
const auto& infos = getTextObjectInfos();
for (size_t i = 0; i < infos.size(); ++i) createOneText(i);
}
// Vacía el vector de sonidos
@@ -512,7 +686,6 @@ namespace Resource {
SDL_FRect rect_full = {.x = X_PADDING, .y = BAR_POSITION, .w = FULL_BAR_WIDTH, .h = BAR_HEIGHT};
surface->fillRect(&rect_full, BAR_COLOR);
#if defined(__EMSCRIPTEN__) || defined(_DEBUG)
// Mostra el nom del recurs que està a punt de carregar-se, centrat sobre la barra
if (!current_loading_name_.empty()) {
const float TEXT_Y = BAR_POSITION - static_cast<float>(TEXT_HEIGHT) - 2.0F;
@@ -522,51 +695,19 @@ namespace Resource {
current_loading_name_,
LOADING_TEXT_COLOR);
}
#endif
Screen::get()->render();
}
// Desa el nom del recurs que s'està a punt de carregar i repinta immediatament.
// A wasm/debug serveix per veure exactament en quin fitxer es penja la càrrega.
// Guarda el nombre del recurso que se está a punto de cargar. El repintado
// lo hace el BootLoader (una vez por frame) — aquí solo se actualiza el estado.
void Cache::setCurrentLoading(const std::string& name) {
current_loading_name_ = name;
#if defined(__EMSCRIPTEN__) || defined(_DEBUG)
renderProgress();
checkEvents();
#endif
#ifdef __EMSCRIPTEN__
// Cedeix el control al navegador perquè pinte el canvas i processe
// events. Sense això, el thread principal queda bloquejat durant tota
// la precàrrega i el jugador només veu pantalla negra.
emscripten_sleep(0);
#endif
}
// Comprueba los eventos de la pantalla de carga
void Cache::checkEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_EVENT_QUIT:
exit(0);
break;
case SDL_EVENT_KEY_DOWN:
if (event.key.key == SDLK_ESCAPE) {
exit(0);
}
break;
}
}
}
// Actualiza el progreso de carga
void Cache::updateLoadingProgress(int steps) {
// Incrementa el contador de recursos cargados
void Cache::updateLoadingProgress() {
count_.add(1);
if (count_.loaded % steps == 0 || count_.loaded == count_.total) {
renderProgress();
}
checkEvents();
}
} // namespace Resource

View File

@@ -25,7 +25,13 @@ namespace Resource {
auto getRoom(const std::string& name) -> std::shared_ptr<Room::Data>;
auto getRooms() -> std::vector<RoomResource>&;
void reload(); // Recarga todos los recursos
// --- Incremental loading (Director drives this from iterate()) ---
void beginLoad(); // Prepara el estado del loader incremental
auto loadStep(int budget_ms) -> bool; // Carga assets durante budget_ms; devuelve true si ha terminado
void renderProgress(); // Dibuja la barra de progreso (usada por BootLoader)
[[nodiscard]] auto isLoadDone() const -> bool;
void reload(); // Recarga todos los recursos (síncrono, usado en hot-reload de debug)
#ifdef _DEBUG
void reloadRoom(const std::string& name); // Recarga una habitación desde disco
#endif
@@ -47,7 +53,21 @@ namespace Resource {
}
};
// Métodos de carga de recursos
// Etapas del loader incremental
enum class LoadStage {
SOUNDS,
MUSICS,
SURFACES,
SURFACES_POST, // Ajuste de transparent colors tras cargar todas las surfaces
PALETTES,
TEXT_FILES,
ANIMATIONS,
ROOMS,
TEXTS,
DONE
};
// Métodos de carga de recursos (bucle completo, usados por reload() síncrono)
void loadSounds();
void loadMusics();
void loadSurfaces();
@@ -57,18 +77,27 @@ namespace Resource {
void loadRooms();
void createText();
// Helpers incrementales: cargan un único asset de la categoría correspondiente
void loadOneSound(size_t index);
void loadOneMusic(size_t index);
void loadOneSurface(size_t index);
void finalizeSurfaces(); // Ajuste de transparent colors tras cargar surfaces
void loadOnePalette(size_t index);
void loadOneTextFile(size_t index);
void loadOneAnimation(size_t index);
void loadOneRoom(size_t index);
void createOneText(size_t index);
// Métodos de limpieza
void clear();
void clearSounds();
void clearMusics();
// Métodos de gestión de carga
void load();
void load(); // Carga completa síncrona (usado solo por reload())
void calculateTotal();
void renderProgress();
static void checkEvents();
void updateLoadingProgress(int steps = 5);
void setCurrentLoading(const std::string& name); // Desa el nom del recurs en curs i repinta (wasm/debug)
void updateLoadingProgress();
void setCurrentLoading(const std::string& name); // Desa el nom del recurs en curs
// Helper para mensajes de error de carga
[[noreturn]] static void throwLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e);
@@ -92,7 +121,11 @@ namespace Resource {
ResourceCount count_{}; // Contador de recursos
std::shared_ptr<Text> loading_text_; // Texto para la pantalla de carga
std::string current_loading_name_; // Nom del recurs que s'està a punt de carregar (debug/wasm)
std::string current_loading_name_; // Nom del recurs que s'està a punt de carregar
// Estado del loader incremental
LoadStage stage_{LoadStage::DONE}; // Arranca en DONE hasta que beginLoad() lo cambie
size_t stage_index_{0}; // Cursor dentro de la categoría actual
};
} // namespace Resource