#include "core/resources/resource.hpp" #include // Para SDL_LogInfo, SDL_LogCategory, SDL_LogError, SDL_SetRenderDrawColor, SDL_EventType, SDL_PollEvent, SDL_RenderFillRect, SDL_RenderRect, SDLK_ESCAPE, SDL_Event #include // Para ranges::transform, ranges::find_if #include // Para array #include // Para exit #include // Para std::cout #include // Para back_inserter #include // Para runtime_error #include // Para move #include "core/audio/jail_audio.hpp" // Para Ja::loadMusic, Ja::loadSound, Ja::deleteMusic, Ja::deleteSound #include "core/locale/lang.hpp" // Para getText #include "core/rendering/screen.hpp" // Para Screen #include "core/rendering/text.hpp" // Para Text #include "core/resources/asset.hpp" // Para Asset #include "core/resources/resource_helper.hpp" // Para loadFile #include "game/options.hpp" // Para Options::loading #include "utils/color.hpp" // Para Color, NO_COLOR_MOD #include "utils/defines.hpp" // Para Texts::VERSION #include "utils/param.hpp" // Para Param, param, ParamPlayer, ParamResource, ParamGame #include "utils/utils.hpp" // Para getFileName #include "version.h" // Para APP_NAME, GIT_HASH // Helper para cargar archivos de audio desde pack o filesystem en memoria namespace { struct AudioData { std::vector data; std::string filepath; }; auto loadAudioData(const std::string& file_path) -> AudioData { auto resource_data = ResourceHelper::loadFile(file_path); return AudioData{.data = std::move(resource_data), .filepath = file_path}; } } // namespace // Declaraciones de funciones que necesitas implementar en otros archivos // Singleton Resource* Resource::instance = nullptr; // Inicializa la instancia única del singleton con modo de carga void Resource::init(LoadingMode mode) { Resource::instance = new Resource(mode); } // Libera la instancia void Resource::destroy() { delete Resource::instance; Resource::instance = nullptr; } // Obtiene la instancia auto Resource::get() -> Resource* { return Resource::instance; } // Constructor con modo de carga Resource::Resource(LoadingMode mode) : loading_mode_(mode), loading_text_(nullptr) { Screen::get()->show(); if (loading_mode_ == LoadingMode::PRELOAD) { loading_text_ = Screen::get()->getText(); // Ya NO llamamos load() aquí: Director bombea beginLoad() + loadStep() // desde iterate() para mantener vivo el bucle SDL3 durante la carga. } else { // En modo lazy, cargamos lo mínimo indispensable initResourceLists(); loadEssentialResources(); } } // Destructor Resource::~Resource() { clear(); } // Carga los recursos esenciales que siempre se necesitan (modo lazy) void Resource::loadEssentialResources() { // Cargar recursos de texto básicos que se usan para crear texturas loadTextFilesQuiet(); // <- VERSIÓN SILENCIOSA loadEssentialTextures(); // Ya es silenciosa createText(); // Crear objetos de texto createTextTextures(); // Crear texturas generadas (game_text_xxx) createPlayerTextures(); // Crea las texturas de jugadores con todas sus variantes de paleta } // Carga los ficheros de texto del juego (versión silenciosa) void Resource::loadTextFilesQuiet() { auto list = Asset::get()->getListByType(Asset::Type::FONT); for (const auto& l : list) { auto name = getFileName(l); // Buscar en nuestra lista y cargar directamente auto it = std::ranges::find_if(text_files_, [&name](const auto& t) -> auto { return t.name == name; }); if (it != text_files_.end()) { it->text_file = Text::loadFile(l); } } } // Carga solo las texturas esenciales (fuentes) void Resource::loadEssentialTextures() { const std::vector ESSENTIAL_TEXTURES = { "04b_25.png", "04b_25_2x.png", "04b_25_metal.png", "04b_25_grey.png", "04b_25_flat.png", "04b_25_reversed.png", "04b_25_flat_2x.png", "04b_25_reversed_2x.png", "8bithud.png", "aseprite.png", "smb2.png", "smb2_grad.png"}; auto texture_list = Asset::get()->getListByType(Asset::Type::BITMAP); for (const auto& file : texture_list) { auto name = getFileName(file); // Solo cargar texturas esenciales if (std::ranges::find(ESSENTIAL_TEXTURES, name) != ESSENTIAL_TEXTURES.end()) { // Buscar en nuestra lista y cargar auto it = std::ranges::find_if(textures_, [&name](const auto& t) -> auto { return t.name == name; }); if (it != textures_.end()) { it->texture = std::make_shared(Screen::get()->getRenderer(), file); } } } } // Inicializa las listas de recursos sin cargar el contenido (modo lazy) void Resource::initResourceLists() { const auto FILE_TO_NAME = [](const auto& file) { return getFileName(file); }; // Inicializa lista de sonidos const auto SOUND_LIST = Asset::get()->getListByType(Asset::Type::SOUND); sounds_.clear(); sounds_.reserve(SOUND_LIST.size()); std::ranges::transform(SOUND_LIST, std::back_inserter(sounds_), [&](const auto& file) { return ResourceSound(FILE_TO_NAME(file)); }); // Inicializa lista de músicas const auto MUSIC_LIST = Asset::get()->getListByType(Asset::Type::MUSIC); musics_.clear(); musics_.reserve(MUSIC_LIST.size()); std::ranges::transform(MUSIC_LIST, std::back_inserter(musics_), [&](const auto& file) { return ResourceMusic(FILE_TO_NAME(file)); }); // Inicializa lista de texturas const auto TEXTURE_LIST = Asset::get()->getListByType(Asset::Type::BITMAP); textures_.clear(); textures_.reserve(TEXTURE_LIST.size()); std::ranges::transform(TEXTURE_LIST, std::back_inserter(textures_), [&](const auto& file) { return ResourceTexture(FILE_TO_NAME(file)); }); // Inicializa lista de ficheros de texto const auto TEXT_FILE_LIST = Asset::get()->getListByType(Asset::Type::FONT); text_files_.clear(); text_files_.reserve(TEXT_FILE_LIST.size()); std::ranges::transform(TEXT_FILE_LIST, std::back_inserter(text_files_), [&](const auto& file) { return ResourceTextFile(FILE_TO_NAME(file)); }); // Inicializa lista de animaciones const auto ANIMATION_LIST = Asset::get()->getListByType(Asset::Type::ANIMATION); animations_.clear(); animations_.reserve(ANIMATION_LIST.size()); std::ranges::transform(ANIMATION_LIST, std::back_inserter(animations_), [&](const auto& file) { return ResourceAnimation(FILE_TO_NAME(file)); }); // Los demos se cargan directamente sin mostrar progreso (son pocos y pequeños) loadDemoDataQuiet(); // Inicializa lista de objetos de texto (sin cargar el contenido) const std::vector TEXT_OBJECTS = { "04b_25", "04b_25_2x", "04b_25_metal", "04b_25_grey", "04b_25_flat", "04b_25_reversed", "04b_25_flat_2x", "04b_25_reversed_2x", "8bithud", "aseprite", "smb2", "smb2_grad"}; texts_.clear(); texts_.reserve(TEXT_OBJECTS.size()); std::ranges::transform(TEXT_OBJECTS, std::back_inserter(texts_), [](const auto& text_name) { return ResourceText(text_name); }); } // Obtiene el sonido a partir de un nombre (con carga perezosa) auto Resource::getSound(const std::string& name) -> Ja::Sound* { auto it = std::ranges::find_if(sounds_, [&name](const auto& s) -> auto { return s.name == name; }); if (it != sounds_.end()) { // Si está en modo lazy y no se ha cargado aún, lo carga ahora if (loading_mode_ == LoadingMode::LAZY_LOAD && it->sound == nullptr) { it->sound = loadSoundLazy(name); } return it->sound; } std::cout << "Error: Sonido no encontrado " << name << '\n'; throw std::runtime_error("Sonido no encontrado: " + name); } // Obtiene la música a partir de un nombre (con carga perezosa) auto Resource::getMusic(const std::string& name) -> Ja::Music* { auto it = std::ranges::find_if(musics_, [&name](const auto& m) -> auto { return m.name == name; }); if (it != musics_.end()) { // Si está en modo lazy y no se ha cargado aún, lo carga ahora if (loading_mode_ == LoadingMode::LAZY_LOAD && it->music == nullptr) { it->music = loadMusicLazy(name); } return it->music; } std::cout << "Error: Música no encontrada " << name << '\n'; throw std::runtime_error("Música no encontrada: " + name); } // Obtiene la textura a partir de un nombre (con carga perezosa) auto Resource::getTexture(const std::string& name) -> std::shared_ptr { auto it = std::ranges::find_if(textures_, [&name](const auto& t) -> auto { return t.name == name; }); if (it != textures_.end()) { // Si está en modo lazy y no se ha cargado aún, lo carga ahora if (loading_mode_ == LoadingMode::LAZY_LOAD && it->texture == nullptr) { it->texture = loadTextureLazy(name); } return it->texture; } std::cout << "Error: Imagen no encontrada " << name << '\n'; throw std::runtime_error("Imagen no encontrada: " + name); } // Obtiene el fichero de texto a partir de un nombre (con carga perezosa) auto Resource::getTextFile(const std::string& name) -> std::shared_ptr { auto it = std::ranges::find_if(text_files_, [&name](const auto& t) -> auto { return t.name == name; }); if (it != text_files_.end()) { // Si está en modo lazy y no se ha cargado aún, lo carga ahora if (loading_mode_ == LoadingMode::LAZY_LOAD && it->text_file == nullptr) { it->text_file = loadTextFileLazy(name); } return it->text_file; } std::cout << "Error: TextFile no encontrado " << name << '\n'; throw std::runtime_error("TextFile no encontrado: " + name); } // Obtiene el objeto de texto a partir de un nombre (con carga perezosa) auto Resource::getText(const std::string& name) -> std::shared_ptr { auto it = std::ranges::find_if(texts_, [&name](const auto& t) -> auto { return t.name == name; }); if (it != texts_.end()) { // Si está en modo lazy y no se ha cargado aún, lo carga ahora if (loading_mode_ == LoadingMode::LAZY_LOAD && it->text == nullptr) { it->text = loadTextLazy(name); } return it->text; } std::cout << "Error: Text no encontrado " << name << '\n'; throw std::runtime_error("Text no encontrado: " + name); } // Obtiene la animación a partir de un nombre (con carga perezosa) auto Resource::getAnimation(const std::string& name) -> AnimationsFileBuffer& { auto it = std::ranges::find_if(animations_, [&name](const auto& a) -> auto { return a.name == name; }); if (it != animations_.end()) { // Si está en modo lazy y no se ha cargado aún (vector vacío), lo carga ahora if (loading_mode_ == LoadingMode::LAZY_LOAD && it->animation.empty()) { it->animation = loadAnimationLazy(name); } return it->animation; } std::cout << "Error: Animación no encontrada " << name << '\n'; throw std::runtime_error("Animación no encontrada: " + name); } // Obtiene el fichero con los datos para el modo demostración a partir de un índice auto Resource::getDemoData(int index) -> DemoData& { if (index < 0 || std::cmp_greater_equal(index, demos_.size())) { std::cout << "Index " << index << " out of range for demo data (size: " << static_cast(demos_.size()) << ")" << '\n'; static DemoData empty_demo_; return empty_demo_; } return demos_.at(index); } // --- Métodos de carga perezosa --- auto Resource::loadSoundLazy(const std::string& name) -> Ja::Sound* { auto sound_list = Asset::get()->getListByType(Asset::Type::SOUND); for (const auto& file : sound_list) { if (getFileName(file) == name) { auto audio_data = loadAudioData(file); if (!audio_data.data.empty()) { return Ja::loadSound(audio_data.data.data(), audio_data.data.size()); } // Fallback a cargar desde disco si no está en pack return Ja::loadSound(file.c_str()); } } return nullptr; } auto Resource::loadMusicLazy(const std::string& name) -> Ja::Music* { auto music_list = Asset::get()->getListByType(Asset::Type::MUSIC); for (const auto& file : music_list) { if (getFileName(file) == name) { auto audio_data = loadAudioData(file); if (!audio_data.data.empty()) { return Ja::loadMusic(audio_data.data.data(), audio_data.data.size()); } // Fallback a cargar desde disco si no está en pack return Ja::loadMusic(file.c_str()); } } return nullptr; } auto Resource::loadTextureLazy(const std::string& name) -> std::shared_ptr { const auto TEXTURE_LIST = Asset::get()->getListByType(Asset::Type::BITMAP); const auto IT = std::ranges::find_if(TEXTURE_LIST, [&name](const auto& file) { return getFileName(file) == name; }); return IT != TEXTURE_LIST.end() ? std::make_shared(Screen::get()->getRenderer(), *IT) : nullptr; } auto Resource::loadTextFileLazy(const std::string& name) -> std::shared_ptr { const auto TEXT_FILE_LIST = Asset::get()->getListByType(Asset::Type::FONT); const auto IT = std::ranges::find_if(TEXT_FILE_LIST, [&name](const auto& file) { return getFileName(file) == name; }); return IT != TEXT_FILE_LIST.end() ? Text::loadFile(*IT) : nullptr; } auto Resource::loadTextLazy(const std::string& name) -> std::shared_ptr { // Mapeo de objetos de texto a sus recursos struct TextMapping { std::string key; std::string texture_file; std::string text_file; }; const std::vector TEXT_MAPPINGS = { {.key = "04b_25", .texture_file = "04b_25.png", .text_file = "04b_25.txt"}, {.key = "04b_25_2x", .texture_file = "04b_25_2x.png", .text_file = "04b_25_2x.txt"}, {.key = "04b_25_metal", .texture_file = "04b_25_metal.png", .text_file = "04b_25.txt"}, {.key = "04b_25_grey", .texture_file = "04b_25_grey.png", .text_file = "04b_25.txt"}, {.key = "04b_25_flat", .texture_file = "04b_25_flat.png", .text_file = "04b_25.txt"}, {.key = "04b_25_reversed", .texture_file = "04b_25_reversed.png", .text_file = "04b_25.txt"}, {.key = "04b_25_flat_2x", .texture_file = "04b_25_flat_2x.png", .text_file = "04b_25_2x.txt"}, {.key = "04b_25_reversed_2x", .texture_file = "04b_25_reversed_2x.png", .text_file = "04b_25_2x.txt"}, {.key = "8bithud", .texture_file = "8bithud.png", .text_file = "8bithud.txt"}, {.key = "aseprite", .texture_file = "aseprite.png", .text_file = "aseprite.txt"}, {.key = "smb2", .texture_file = "smb2.png", .text_file = "smb2.txt"}, {.key = "smb2_grad", .texture_file = "smb2_grad.png", .text_file = "smb2.txt"}}; const auto IT = std::ranges::find_if(TEXT_MAPPINGS, [&name](const auto& mapping) { return mapping.key == name; }); if (IT == TEXT_MAPPINGS.end()) { return nullptr; } auto texture = getTexture(IT->texture_file); // Esto cargará la textura si no está cargada auto text_file = getTextFile(IT->text_file); // Esto cargará el archivo de texto si no está cargado return (texture && text_file) ? std::make_shared(texture, text_file) : nullptr; } auto Resource::loadAnimationLazy(const std::string& name) -> AnimationsFileBuffer { const auto ANIMATION_LIST = Asset::get()->getListByType(Asset::Type::ANIMATION); const auto IT = std::ranges::find_if(ANIMATION_LIST, [&name](const auto& file) { return getFileName(file) == name; }); if (IT != ANIMATION_LIST.end()) { return loadAnimationsFromFile(*IT); } // Si no se encuentra, retorna vector vacío return AnimationsFileBuffer{}; } // Vacia todos los vectores de recursos void Resource::clear() { clearSounds(); clearMusics(); textures_.clear(); text_files_.clear(); texts_.clear(); animations_.clear(); demos_.clear(); } // Carga síncrona completa: usado por Resource::reload() (hot-reload en debug). // En arranque normal la carga la bombea Director::iterate() vía loadStep(). void Resource::load() { beginLoad(); while (!loadStep(INT_MAX)) { // Presupuesto infinito: una sola pasada carga todo } Screen::get()->setVSync(saved_vsync_); } // Prepara el estado del cargador incremental. No carga nada todavía. void Resource::beginLoad() { calculateTotalResources(); initProgressBar(); saved_vsync_ = Screen::getVSync(); Screen::get()->setVSync(false); // Maximiza FPS durante el preload stage_ = LoadStage::SOUNDS; stage_index_ = 0; } auto Resource::isLoadDone() const -> bool { return stage_ == LoadStage::DONE; } // Avança una etapa que descarrega una llista d'assets. void Resource::advanceListLoadStage(const std::vector& list, void (Resource::*load_one)(size_t), LoadStage next_stage) { if (stage_index_ >= list.size()) { stage_ = next_stage; stage_index_ = 0; return; } (this->*load_one)(stage_index_++); } // Bombea la máquina de etapas hasta agotar el presupuesto de tiempo o completar la carga. // Devuelve true cuando ya no queda nada por cargar. auto Resource::loadStep(int budget_ms) -> bool { if (stage_ == LoadStage::DONE) { return true; } const Uint64 START_NS = SDL_GetTicksNS(); const Uint64 BUDGET_NS = static_cast(budget_ms) * 1'000'000ULL; while (stage_ != LoadStage::DONE) { switch (stage_) { case LoadStage::SOUNDS: { if (stage_index_ == 0) { sounds_.clear(); } advanceListLoadStage(Asset::get()->getListByType(Asset::Type::SOUND), &Resource::loadOneSound, LoadStage::MUSICS); break; } case LoadStage::MUSICS: { if (stage_index_ == 0) { musics_.clear(); } advanceListLoadStage(Asset::get()->getListByType(Asset::Type::MUSIC), &Resource::loadOneMusic, LoadStage::TEXTURES); break; } case LoadStage::TEXTURES: { if (stage_index_ == 0) { textures_.clear(); } advanceListLoadStage(Asset::get()->getListByType(Asset::Type::BITMAP), &Resource::loadOneTexture, LoadStage::TEXT_FILES); break; } case LoadStage::TEXT_FILES: { if (stage_index_ == 0) { text_files_.clear(); } advanceListLoadStage(Asset::get()->getListByType(Asset::Type::FONT), &Resource::loadOneTextFile, LoadStage::ANIMATIONS); break; } case LoadStage::ANIMATIONS: { if (stage_index_ == 0) { animations_.clear(); } advanceListLoadStage(Asset::get()->getListByType(Asset::Type::ANIMATION), &Resource::loadOneAnimation, LoadStage::DEMO_DATA); break; } case LoadStage::DEMO_DATA: { if (stage_index_ == 0) { demos_.clear(); } advanceListLoadStage(Asset::get()->getListByType(Asset::Type::DEMODATA), &Resource::loadOneDemoData, LoadStage::CREATE_TEXT); break; } case LoadStage::CREATE_TEXT: createText(); stage_ = LoadStage::CREATE_TEXT_TEXTURES; break; case LoadStage::CREATE_TEXT_TEXTURES: createTextTextures(); stage_ = LoadStage::CREATE_PLAYER_TEXTURES; break; case LoadStage::CREATE_PLAYER_TEXTURES: createPlayerTextures(); stage_ = LoadStage::DONE; break; case LoadStage::DONE: break; } if ((SDL_GetTicksNS() - START_NS) >= BUDGET_NS) { break; } } return stage_ == LoadStage::DONE; } // Recarga todos los recursos (limpia y vuelve a cargar) void Resource::reload() { clear(); if (loading_mode_ == LoadingMode::PRELOAD) { load(); } else { initResourceLists(); } } // Carga un sonido concreto desde la lista de assets void Resource::loadOneSound(size_t idx) { auto list = Asset::get()->getListByType(Asset::Type::SOUND); const auto& path = list[idx]; auto name = getFileName(path); updateLoadingProgress(name); auto audio_data = loadAudioData(path); Ja::Sound* sound = nullptr; if (!audio_data.data.empty()) { sound = Ja::loadSound(audio_data.data.data(), audio_data.data.size()); } else { sound = Ja::loadSound(path.c_str()); } if (sound == nullptr) { std::cout << "Sound load failed: " << name << '\n'; } sounds_.emplace_back(name, sound); } // Carga una música concreta desde la lista de assets void Resource::loadOneMusic(size_t idx) { auto list = Asset::get()->getListByType(Asset::Type::MUSIC); const auto& path = list[idx]; auto name = getFileName(path); updateLoadingProgress(name); auto audio_data = loadAudioData(path); Ja::Music* music = nullptr; if (!audio_data.data.empty()) { music = Ja::loadMusic(audio_data.data.data(), audio_data.data.size()); } else { music = Ja::loadMusic(path.c_str()); } if (music == nullptr) { std::cout << "Music load failed: " << name << '\n'; } musics_.emplace_back(name, music); } // Carga una textura concreta desde la lista de assets void Resource::loadOneTexture(size_t idx) { auto list = Asset::get()->getListByType(Asset::Type::BITMAP); const auto& path = list[idx]; auto name = getFileName(path); updateLoadingProgress(name); textures_.emplace_back(name, std::make_shared(Screen::get()->getRenderer(), path)); } // Carga un fichero de texto concreto desde la lista de assets void Resource::loadOneTextFile(size_t idx) { auto list = Asset::get()->getListByType(Asset::Type::FONT); const auto& path = list[idx]; auto name = getFileName(path); updateLoadingProgress(name); text_files_.emplace_back(name, Text::loadFile(path)); } // Carga una animación concreta desde la lista de assets void Resource::loadOneAnimation(size_t idx) { auto list = Asset::get()->getListByType(Asset::Type::ANIMATION); const auto& path = list[idx]; auto name = getFileName(path); updateLoadingProgress(name); animations_.emplace_back(name, loadAnimationsFromFile(path)); } // Carga un fichero de datos de demo concreto desde la lista de assets void Resource::loadOneDemoData(size_t idx) { auto list = Asset::get()->getListByType(Asset::Type::DEMODATA); const auto& path = list[idx]; auto name = getFileName(path); updateLoadingProgress(name); demos_.emplace_back(loadDemoDataFromFile(path)); } // Crea las texturas de jugadores con todas sus variantes de paleta void Resource::createPlayerTextures() { // Configuración de jugadores y sus paletas struct PlayerConfig { std::string base_texture; std::vector palette_files; std::string name_prefix; }; std::vector players = { {.base_texture = "player1.gif", .palette_files = {"player1_coffee1.pal", "player1_coffee2.pal", "player1_invencible.pal"}, .name_prefix = "player1"}, {.base_texture = "player2.gif", .palette_files = {"player2_coffee1.pal", "player2_coffee2.pal", "player2_invencible.pal"}, .name_prefix = "player2"}}; // Bucle principal for (size_t player_idx = 0; player_idx < players.size(); ++player_idx) { const auto& player = players[player_idx]; // Obtenemos el jugador actual // Encontrar el archivo original de la textura const auto TEXTURE_LIST = Asset::get()->getListByType(Asset::Type::BITMAP); const auto IT = std::ranges::find_if(TEXTURE_LIST, [&player](const auto& file) { return getFileName(file) == player.base_texture; }); const std::string TEXTURE_FILE_PATH = (IT != TEXTURE_LIST.end()) ? *IT : std::string{}; // Crear las 4 texturas con sus respectivas paletas for (int palette_idx = 0; palette_idx < 4; ++palette_idx) { std::shared_ptr texture; if (palette_idx == 0) { // Textura 0 - usar la ya cargada y modificar solo paleta 0 (default_shirt) texture = getTexture(player.base_texture); texture->setPaletteColor(0, 16, param.player.default_shirt[player_idx].darkest.TO_UINT32()); texture->setPaletteColor(0, 17, param.player.default_shirt[player_idx].dark.TO_UINT32()); texture->setPaletteColor(0, 18, param.player.default_shirt[player_idx].base.TO_UINT32()); texture->setPaletteColor(0, 19, param.player.default_shirt[player_idx].light.TO_UINT32()); texture->setPaletteColor(0, 56, param.player.outline_color[player_idx].TO_UINT32()); } else { // Crear textura nueva desde archivo usando ResourceHelper texture = std::make_shared(Screen::get()->getRenderer(), TEXTURE_FILE_PATH); // Añadir todas las paletas texture->addPaletteFromPalFile(Asset::get()->getPath(player.palette_files[0])); texture->addPaletteFromPalFile(Asset::get()->getPath(player.palette_files[1])); texture->addPaletteFromPalFile(Asset::get()->getPath(player.palette_files[2])); if (palette_idx == 1) { // Textura 1 - modificar solo paleta 1 (one_coffee_shirt) texture->setPaletteColor(1, 16, param.player.one_coffee_shirt[player_idx].darkest.TO_UINT32()); texture->setPaletteColor(1, 17, param.player.one_coffee_shirt[player_idx].dark.TO_UINT32()); texture->setPaletteColor(1, 18, param.player.one_coffee_shirt[player_idx].base.TO_UINT32()); texture->setPaletteColor(1, 19, param.player.one_coffee_shirt[player_idx].light.TO_UINT32()); texture->setPaletteColor(1, 56, param.player.outline_color[player_idx].TO_UINT32()); } else if (palette_idx == 2) { // Textura 2 - modificar solo paleta 2 (two_coffee_shirt) texture->setPaletteColor(2, 16, param.player.two_coffee_shirt[player_idx].darkest.TO_UINT32()); texture->setPaletteColor(2, 17, param.player.two_coffee_shirt[player_idx].dark.TO_UINT32()); texture->setPaletteColor(2, 18, param.player.two_coffee_shirt[player_idx].base.TO_UINT32()); texture->setPaletteColor(2, 19, param.player.two_coffee_shirt[player_idx].light.TO_UINT32()); texture->setPaletteColor(2, 56, param.player.outline_color[player_idx].TO_UINT32()); } // Textura 3 (palette_idx == 3) - no modificar nada, usar colores originales } // Asignar la paleta correspondiente texture->setPalette(palette_idx); // Guardar con nombre específico std::string texture_name = player.name_prefix + "_pal" + std::to_string(palette_idx); textures_.emplace_back(texture_name, texture); } } } // Crea texturas a partir de textos para mostrar puntuaciones y mensajes void Resource::createTextTextures() { struct NameAndText { std::string name; std::string text; NameAndText(std::string name_init, std::string text_init) : name(std::move(name_init)), text(std::move(text_init)) {} }; // Texturas de tamaño normal con outline std::vector strings1 = { {"game_text_1000_points", "1.000"}, {"game_text_2500_points", "2.500"}, {"game_text_5000_points", "5.000"}, {"game_text_powerup", Lang::getText("[GAME_TEXT] 4")}, {"game_text_one_hit", Lang::getText("[GAME_TEXT] 5")}, {"game_text_stop", Lang::getText("[GAME_TEXT] 6")}, {"game_text_1000000_points", Lang::getText("[GAME_TEXT] 8")}}; auto text1 = getText("04b_25_enhanced"); std::ranges::transform(strings1, std::back_inserter(textures_), [&](const auto& s) { return ResourceTexture(s.name, text1->writeDXToTexture(Text::STROKE, s.text, -2, Colors::NO_COLOR_MOD, 1, param.game.item_text_outline_color)); }); // Texturas de tamaño doble std::vector strings2 = { {"game_text_100000_points", "100.000"}, {"game_text_get_ready", Lang::getText("[GAME_TEXT] 7")}, {"game_text_last_stage", Lang::getText("[GAME_TEXT] 3")}, {"game_text_congratulations", Lang::getText("[GAME_TEXT] 1")}, {"game_text_new_record", Lang::getText("[GAME_TEXT] NEW_RECORD")}, {"game_text_game_over", "Game Over"}}; auto text2 = getText("04b_25_2x_enhanced"); std::ranges::transform(strings2, std::back_inserter(textures_), [&](const auto& s) { return ResourceTexture(s.name, text2->writeDXToTexture(Text::STROKE, s.text, -4, Colors::NO_COLOR_MOD, 1, param.game.item_text_outline_color)); }); } // Crea los objetos de texto a partir de los archivos de textura y texto void Resource::createText() { struct ResourceInfo { std::string key; std::string texture_file; std::string text_file; std::string white_texture_file; // Textura blanca opcional ResourceInfo(std::string k, std::string t_file, std::string txt_file, std::string w_file = "") : key(std::move(k)), texture_file(std::move(t_file)), text_file(std::move(txt_file)), white_texture_file(std::move(w_file)) {} }; std::vector resources = { {"04b_25", "04b_25.png", "04b_25.txt"}, {"04b_25_enhanced", "04b_25.png", "04b_25.txt", "04b_25_white.png"}, // Nueva fuente con textura blanca {"04b_25_white", "04b_25_white.png", "04b_25.txt"}, {"04b_25_2x", "04b_25_2x.png", "04b_25_2x.txt"}, {"04b_25_2x_enhanced", "04b_25_2x.png", "04b_25_2x.txt", "04b_25_2x_white.png"}, // Nueva fuente con textura blanca {"04b_25_metal", "04b_25_metal.png", "04b_25.txt"}, {"04b_25_grey", "04b_25_grey.png", "04b_25.txt"}, {"04b_25_flat", "04b_25_flat.png", "04b_25.txt"}, {"04b_25_reversed", "04b_25_reversed.png", "04b_25.txt"}, {"04b_25_flat_2x", "04b_25_flat_2x.png", "04b_25_2x.txt"}, {"04b_25_reversed_2x", "04b_25_reversed_2x.png", "04b_25_2x.txt"}, {"8bithud", "8bithud.png", "8bithud.txt"}, {"aseprite", "aseprite.png", "aseprite.txt"}, {"smb2", "smb2.png", "smb2.txt"}, {"smb2_grad", "smb2_grad.png", "smb2.txt"}}; for (const auto& resource : resources) { if (!resource.white_texture_file.empty()) { // Crear texto con textura blanca texts_.emplace_back(resource.key, std::make_shared(getTexture(resource.texture_file), getTexture(resource.white_texture_file), getTextFile(resource.text_file))); } else { // Crear texto normal texts_.emplace_back(resource.key, std::make_shared(getTexture(resource.texture_file), getTextFile(resource.text_file))); } } } // Vacía el vector de sonidos y libera la memoria asociada void Resource::clearSounds() { for (auto& sound : sounds_) { if (sound.sound != nullptr) { Ja::deleteSound(sound.sound); sound.sound = nullptr; } } sounds_.clear(); } // Vacía el vector de músicas y libera la memoria asociada void Resource::clearMusics() { for (auto& music : musics_) { if (music.music != nullptr) { Ja::deleteMusic(music.music); music.music = nullptr; } } musics_.clear(); } // Calcula el número total de recursos a cargar y reinicia el contador de carga void Resource::calculateTotalResources() { const std::array ASSET_TYPES = { Asset::Type::SOUND, Asset::Type::MUSIC, Asset::Type::BITMAP, Asset::Type::FONT, Asset::Type::ANIMATION, Asset::Type::DEMODATA}; size_t total = 0; for (const auto& asset_type : ASSET_TYPES) { auto list = Asset::get()->getListByType(asset_type); total += list.size(); } loading_count_ = ResourceCount(total); } // Muestra el progreso de carga en pantalla (barra y texto) void Resource::renderProgress() { // Obtiene la pantalla y el renderer auto* screen = Screen::get(); auto* renderer = screen->getRenderer(); // Actualiza la lógica principal de la pantalla (input, etc.) screen->coreUpdate(); // Inicia el frame y limpia la pantalla screen->start(); screen->clean(); // Si la pantalla de carga está desactivada, dejamos todo en negro. // wait_for_input solo tiene efecto cuando la pantalla está visible. if (!Options::loading.show) { screen->coreRender(); return; } // Estamos en la fase de espera explícita al usuario tras terminar la carga const bool WAITING_FOR_INPUT = isLoadDone() && Options::loading.wait_for_input; auto text_color = param.resource.color; auto bar_color = param.resource.color.DARKEN(100); const auto TEXT_HEIGHT = loading_text_->getCharacterSize(); // Dibuja el interior de la barra de progreso SDL_SetRenderDrawColor(renderer, bar_color.r, bar_color.g, bar_color.b, bar_color.a); SDL_RenderFillRect(renderer, &loading_full_rect_); // Dibuja el marco de la barra de progreso SDL_SetRenderDrawColor(renderer, bar_color.r, bar_color.g, bar_color.b, bar_color.a); SDL_RenderRect(renderer, &loading_wired_rect_); // Texto centrado sobre la barra: mientras carga, el nombre del recurso; // al terminar en modo wait_for_input, el prompt traducido. const std::string OVER_BAR_TEXT = WAITING_FOR_INPUT ? Lang::getText("[RESOURCE] PRESS_TO_CONTINUE") : loading_resource_name_; if ((Options::loading.show_resource_name || WAITING_FOR_INPUT) && !OVER_BAR_TEXT.empty()) { loading_text_->writeDX( Text::CENTER | Text::COLOR, loading_wired_rect_.x + (loading_wired_rect_.w / 2), loading_wired_rect_.y - TEXT_HEIGHT - 2, OVER_BAR_TEXT, 1, text_color); } // Muestra nombre de la aplicación loading_text_->writeDX( Text::CENTER | Text::COLOR, param.game.game_area.center_x, param.game.game_area.center_y - TEXT_HEIGHT, spaceBetweenLetters(std::string(Version::APP_NAME)), 1, text_color); // Muestra la versión y el hash del commit loading_text_->writeDX( Text::CENTER | Text::COLOR, param.game.game_area.center_x, param.game.game_area.center_y + TEXT_HEIGHT, "ver. " + std::string(Texts::VERSION) + " (" + std::string(Version::GIT_HASH) + ")", 1, text_color); // Renderiza el frame en pantalla screen->coreRender(); } // Carga los datos para el modo demostración (sin mostrar progreso) void Resource::loadDemoDataQuiet() { const auto LIST = Asset::get()->getListByType(Asset::Type::DEMODATA); demos_.clear(); demos_.reserve(LIST.size()); std::ranges::transform(LIST, std::back_inserter(demos_), [](const auto& l) { return loadDemoDataFromFile(l); }); } // Inicializa los rectangulos que definen la barra de progreso void Resource::initProgressBar() { const float WIRED_BAR_WIDTH = param.game.width * BAR_WIDTH_RATIO; const float BAR_X_POSITION = (param.game.width - WIRED_BAR_WIDTH) / 2.0F; const float BAR_Y_POSITION = (param.game.height * BAR_Y_RATIO) - (BAR_HEIGHT / 2.0F); loading_wired_rect_ = {.x = BAR_X_POSITION, .y = BAR_Y_POSITION, .w = WIRED_BAR_WIDTH, .h = BAR_HEIGHT}; const float FULL_BAR_WIDTH = WIRED_BAR_WIDTH * loading_count_.getPercentage(); loading_full_rect_ = {.x = BAR_X_POSITION, .y = BAR_Y_POSITION, .w = FULL_BAR_WIDTH, .h = BAR_HEIGHT}; } // Actualiza el estado del progreso. No renderiza: el repintado lo hace // Preload::iterate una vez por frame llamando a renderProgress(). void Resource::updateLoadingProgress(std::string name) { loading_resource_name_ = std::move(name); loading_count_.increase(); updateProgressBar(); } // Actualiza la barra de estado void Resource::updateProgressBar() { loading_full_rect_.w = loading_wired_rect_.w * loading_count_.getPercentage(); }