diff --git a/proj-windows/CMakeLists.txt b/proj-windows/CMakeLists.txt index 3f20311..a4799f1 100644 --- a/proj-windows/CMakeLists.txt +++ b/proj-windows/CMakeLists.txt @@ -172,30 +172,6 @@ if (WIN32) add_custom_command(TARGET witcher001 POST_BUILD COMMAND ${CMAKE_COMMAND} -E echo "Copying DLLs to output folder..." - # Копируем SDL2 (целевое имя всегда SDL2.dll) - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${SDL2_DLL_SRC}" - "${SDL2_DLL_DST}" - - # Копируем LIBZIP (целевое имя всегда zip.dll) - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${LIBZIP_DLL_SRC}" - "$/zip.dll" - - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${ZLIB_DLL_SRC}" - "${ZLIB_DLL_DST}" - - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${SDL2TTF_DLL_SRC}" - "$" - - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/SDL_mixer-release-2.8.0/install-$/bin/SDL2_mixer$<$:d>.dll" - - # This is only for profiling: - #"${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/SDL_mixer-release-2.8.0/install-Release/bin/SDL2_mixer$<$:d>.dll" - "$/SDL2_mixer$<$:d>.dll" ) endif() diff --git a/resources/start_uni_interior.lua b/resources/start_uni_interior.lua index 83191f6..930cbfc 100644 --- a/resources/start_uni_interior.lua +++ b/resources/start_uni_interior.lua @@ -1343,4 +1343,4 @@ end) setDay0setup() game_api.switch_navigation(0) --debugAllOpen() -print("Lua script loaded successfully!") +print("Lua script loaded successfully!---!") diff --git a/resources/w/ui/screen_load.json b/resources/w/ui/screen_load.json index dc079fd..9099cba 100644 --- a/resources/w/ui/screen_load.json +++ b/resources/w/ui/screen_load.json @@ -32,37 +32,37 @@ { "type": "TextButton", "name": "loadSlot1Button", - "width": 300, + "width": 600, "height": 70, "text": "(пусто)", - "fontSize": 48, + "fontSize": 24, "textCentered": true }, { "type": "TextButton", "name": "loadSlot2Button", - "width": 300, + "width": 600, "height": 70, "text": "(пусто)", - "fontSize": 48, + "fontSize": 24, "textCentered": true }, { "type": "TextButton", "name": "loadSlot3Button", - "width": 300, + "width": 600, "height": 70, "text": "(пусто)", - "fontSize": 48, + "fontSize": 24, "textCentered": true }, { "type": "TextButton", "name": "loadSlot4Button", - "width": 300, + "width": 600, "height": 70, "text": "(пусто)", - "fontSize": 48, + "fontSize": 24, "textCentered": true } ] diff --git a/resources/w/ui/screen_save.json b/resources/w/ui/screen_save.json index 76a3f5a..8677984 100644 --- a/resources/w/ui/screen_save.json +++ b/resources/w/ui/screen_save.json @@ -32,37 +32,37 @@ { "type": "TextButton", "name": "saveSlot1Button", - "width": 300, + "width": 600, "height": 70, "text": "(пусто)", - "fontSize": 48, + "fontSize": 24, "textCentered": true }, { "type": "TextButton", "name": "saveSlot2Button", - "width": 300, + "width": 600, "height": 70, "text": "(пусто)", - "fontSize": 48, + "fontSize": 24, "textCentered": true }, { "type": "TextButton", "name": "saveSlot3Button", - "width": 300, + "width": 600, "height": 70, "text": "(пусто)", - "fontSize": 48, + "fontSize": 24, "textCentered": true }, { "type": "TextButton", "name": "saveSlot4Button", - "width": 300, + "width": 600, "height": 70, "text": "(пусто)", - "fontSize": 48, + "fontSize": 24, "textCentered": true } ] diff --git a/src/BoneAnimatedModelNew.cpp b/src/BoneAnimatedModelNew.cpp index ef7709c..d516ba4 100644 --- a/src/BoneAnimatedModelNew.cpp +++ b/src/BoneAnimatedModelNew.cpp @@ -13,6 +13,16 @@ namespace ZL using std::max; #endif + static std::unordered_map s_boneSystemCache; + + static std::string BoneCacheKey(const std::string& fileName, const std::string& ZIPFileName) + { + std::string base = fileName; + if (base.size() > 4 && base.compare(base.size() - 4, 4, ".bin") == 0) + base = base.substr(0, base.size() - 4); + return ZIPFileName.empty() ? base : ZIPFileName + ":" + base; + } + int getIndexByValue(const std::string& name, const std::vector& words) { for (int i = 0; i < words.size(); i++) @@ -75,6 +85,10 @@ namespace ZL void BoneSystemNew::LoadFromFile(const std::string& fileName, const std::string& ZIPFileName) { + std::string key = BoneCacheKey(fileName, ZIPFileName); + auto cacheIt = s_boneSystemCache.find(key); + if (cacheIt != s_boneSystemCache.end()) { *this = cacheIt->second; return; } + std::ifstream filestream; std::istringstream zipStream; @@ -487,6 +501,8 @@ namespace ZL } } + s_boneSystemCache[key] = *this; + if (startBones.size() > MAX_GPU_BONES) { std::cout << "Warning: model has " << startBones.size() @@ -496,6 +512,10 @@ namespace ZL void BoneSystemNew::LoadFromBinaryFile(const std::string& fileName, const std::string& ZIPFileName) { + std::string key = BoneCacheKey(fileName, ZIPFileName); + auto cacheIt = s_boneSystemCache.find(key); + if (cacheIt != s_boneSystemCache.end()) { *this = cacheIt->second; return; } + std::vector fileData; if (!ZIPFileName.empty()) @@ -678,6 +698,8 @@ namespace ZL } } + s_boneSystemCache[key] = *this; + if (startBones.size() > MAX_GPU_BONES) { std::cout << "Warning: model has " << startBones.size() diff --git a/src/Character.cpp b/src/Character.cpp index 39844de..fe0f8ec 100644 --- a/src/Character.cpp +++ b/src/Character.cpp @@ -10,7 +10,6 @@ #include "utils/Utils.h" #include "TextModel.h" - namespace ZL { const float ATTACK_COOLDOWN_TIME = 1.6f; @@ -63,7 +62,8 @@ std::unique_ptr Character::createFromState(CharacterState st, Rendere auto loadTex = [&](const std::string& path) -> std::shared_ptr { if (path.empty()) return nullptr; try { - return std::make_shared(CreateTextureDataFromPng(path, zip)); + //return std::make_shared(CreateTextureDataFromPng(path, zip)); + return renderer.textureManager.LoadFromPng(path, zip); } catch (const std::exception& e) { std::cerr << "Character::createFromState: failed to load texture '" << path << "': " << e.what() << std::endl; return nullptr; diff --git a/src/CharacterState.h b/src/CharacterState.h index 33a346a..39ee497 100644 --- a/src/CharacterState.h +++ b/src/CharacterState.h @@ -6,6 +6,7 @@ #include #include #include "external/nlohmann/json.hpp" +#include "ISaveable.h" namespace ZL { @@ -44,7 +45,7 @@ struct CharacterCreationInfo { // All serializable game state for a character. // This is a member of Character; rendering-only data lives in Character itself. -class CharacterState { +class CharacterState : public ISaveable { public: // Special index values for attackTargetIndex / faceTargetIndex static constexpr int kNoTarget = -1; // no target (was nullptr) @@ -127,8 +128,8 @@ public: void setHp(float newHp); // --- Serialisation (runtime mutable fields only; creation info is reloaded from config) --- - void save(nlohmann::json& out) const; - void load(const nlohmann::json& in); + void save(nlohmann::json& out) const override; + void load(const nlohmann::json& in) override; }; } // namespace ZL diff --git a/src/Game.cpp b/src/Game.cpp index 1e10aa0..d915cc6 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -1,6 +1,7 @@ #include "Game.h" #include "AnimatedModel.h" #include "utils/Utils.h" +#include #include "items/ItemRegistry.h" #include "render/OpenGlExtensions.h" #include @@ -242,6 +243,113 @@ namespace ZL ItemRegistry::instance().loadFromJson("resources/config2/items.json", CONST_ZIP_FILE); + createLocations(); + + renderer.textureManager.LoadFromPng("resources/w/ui/img/toast/item_received001.png", CONST_ZIP_FILE, true); + renderer.textureManager.LoadFromPng("resources/w/ui/img/toast/item_removed001.png", CONST_ZIP_FILE, true); + + mobileRotateTexture = renderer.textureManager.LoadFromPng( + "resources/rotateYourDevice.png", CONST_ZIP_FILE, true); + + + + // Wire inventory callbacks: tutorial tracking + toast notifications. + gameState.inventory.onItemAdded = [this](const std::string& itemId) { + menuManager.onItemPickedUp(itemId); + const Item* item = ItemRegistry::instance().findById(itemId); + if (item) menuManager.showToast("resources/w/ui/img/toast/item_received001.png", item->name); + }; + gameState.inventory.onItemRemoved = [this](const std::string& itemId) { + const Item* item = ItemRegistry::instance().findById(itemId); + if (item) menuManager.showToast("resources/w/ui/img/toast/item_removed001.png", item->name); + }; + + // Wire phone dialogue start function so MenuManager can trigger dialogues. + menuManager.startDialogueFunc = [this](const std::string& id) { + if (currentLocation()) currentLocation()->dialogueSystem.startDialogue(id); + }; + + menuManager.startDarklandsTransitionFunc = [this]() { + startDarklandsTransition(); + }; + + menuManager.startNightTransitionFunc = [this]() { + startNightTransition(); + }; + + menuManager.chatOpenCallback = [this](int chatIndex) { + if (currentLocation()) + currentLocation()->scriptEngine.callChatOpenCallback(chatIndex); + }; + + menuManager.skipCutsceneFunc = [this]() { + if (currentLocation()) currentLocation()->dialogueSystem.skipCutscene(); + }; + + std::cout << "Load resurces step 13" << std::endl; + + menuManager.onResetGame = [this]() { + gameState.currentLocationName.clear(); + gameState.isDarklands = false; + gameState.isNight = false; + gameState.isDawn = false; + gameState.playerHp = 200.f; + gameState.playerMaxHp = 200.f; + gameState.money = 5500; + gameState.globalInts.clear(); + gameState.globalFloats.clear(); + gameState.tutorialStep = TutorialStep::Step0; + gameState.uniIntTutorialState = UniIntTutorialState::Step10; + gameState.tutorialPhonePickedUp = false; + gameState.tutorialJournalPickedUp = false; + gameState.tutorialPhoneChatScreenOpened = false; + gameState.tutorialJournalScreenOpened = false; + gameState.tutorialNeedOpenTaxiScreen = false; + for (int i = 0; i < 3; ++i) { + gameState.chatUnread[i] = true; + gameState.chatPreviewMsg[i].clear(); + gameState.chatHistory[i].clear(); + } + gameState.inventory.items.clear(); + gameState.questJournal.loadFromFile("resources/quests/quests.json", CONST_ZIP_FILE); + createLocations(); + }; + + try { + menuManager.setup(gameState.inventory, CONST_ZIP_FILE); + std::cout << "UI loaded successfully" << std::endl; + } + catch (const std::exception& e) { + + std::cerr << "Failed to load UI: " << e.what() << std::endl; + } + + menuManager.startGameFunc = [this]() { + gameState.currentLocationName = "location_dorm"; + currentLocation()->scriptEngine.callLocationEnterCallback(); + }; + + menuManager.onSaveGame = [this](int slot) { saveGame(slot); }; + menuManager.onLoadGame = [this](int slot) { loadGame(slot); }; + menuManager.getSlotInfoFunc = [this](int slot) { return readSlotInfo(slot); }; + + loadingCompleted = true; + + if (audioPlayer->init()) { + audioPlayer->setMusicVolume(100); + audioPlayer->setSoundVolume(80); + std::cout << "Audio initialized successfully" << std::endl; + } + else { + std::cout << "Audio initialization failed" << std::endl; + } + + } + + void Game::createLocations() + { + gameState.locations.clear(); + gameState.globalFloats["player_hp"] = 200; LocationSetup uniInteriorParams; @@ -280,12 +388,12 @@ namespace ZL "resources/navigation/uni_interior3_n2_lr_tr_hall_aiperi.json", "resources/navigation/uni_interior3_darklands_all_open.json", //5 "resources/navigation/uni_interior4_unlocked_hall.json", - "resources/navigation/uni_interior4_unlocked_n3.json", + "resources/navigation/uni_interior4_unlocked_n3.json", "resources/navigation/uni_interior4_unlocked_s1.json", "resources/navigation/uni_interior4_unlocked_s2.json", "resources/navigation/uni_interior4_unlocked_s3.json", "resources/navigation/uni_interior4_unlocked_s3_s1.json", //11 - "resources/navigation/uni_interior4_unlocked_s3_hall.json", + "resources/navigation/uni_interior4_unlocked_s3_hall.json", "resources/navigation/uni_interior4_unlocked_s3_n3.json", "resources/navigation/uni_interior4_unlocked_s3_n2.json", "resources/navigation/uni_interior4_unlocked_s3_s2.json", @@ -375,6 +483,7 @@ namespace ZL //params_dorm.gameObjectsJsonPath = "resources/config2/gameobjects_dorm.json"; //params_dorm.gameObjectsJsonPath = "resources/config2/gameobjects_dorm_trees001.json"; params_dorm.gameObjectsJsonPath = "resources/config2/gameobjects_dorm_new_x.json"; + params_dorm.npcsJsonPath = "resources/config2/npcs_dorm.json"; params_dorm.dialoguesJsonPath = "resources/dialogue/dorm_dialogues.json"; @@ -387,7 +496,7 @@ namespace ZL "resources/navigation/dorm3_b.json", "resources/navigation/dorm3_all_open.json", };*/ - /* + params_dorm.navigationJsonPaths = { "resources/navigation/dorm3_bca.txt", //0 "resources/navigation/dorm3_ca.txt", //1 @@ -396,15 +505,15 @@ namespace ZL "resources/navigation/dorm3_b.txt", "resources/navigation/dorm3_all_open.txt", //5 }; - */ - params_dorm.navigationJsonPaths = { + + /*params_dorm.navigationJsonPaths = { "resources/navigation/dorm0_large.json", "resources/navigation/dorm0_large.json", "resources/navigation/dorm0_large.json", "resources/navigation/dorm0_large.json", "resources/navigation/dorm0_large.json", "resources/navigation/dorm0_large.json", - }; + };*/ params_dorm.teleportsJsonPath = "resources/config2/teleports_dorm.json"; params_dorm.triggerZonesJsonPath = "resources/config2/trigger_zones_dorm.json"; params_dorm.lightsJsonPath = "resources/config2/lights_dorm.json"; @@ -509,43 +618,6 @@ namespace ZL }; } - renderer.textureManager.LoadFromPng("resources/w/ui/img/toast/item_received001.png", CONST_ZIP_FILE, true); - renderer.textureManager.LoadFromPng("resources/w/ui/img/toast/item_removed001.png", CONST_ZIP_FILE, true); - - mobileRotateTexture = renderer.textureManager.LoadFromPng( - "resources/rotateYourDevice.png", CONST_ZIP_FILE, true); - - - - // Wire inventory callbacks: tutorial tracking + toast notifications. - gameState.inventory.onItemAdded = [this](const std::string& itemId) { - menuManager.onItemPickedUp(itemId); - const Item* item = ItemRegistry::instance().findById(itemId); - if (item) menuManager.showToast("resources/w/ui/img/toast/item_received001.png", item->name); - }; - gameState.inventory.onItemRemoved = [this](const std::string& itemId) { - const Item* item = ItemRegistry::instance().findById(itemId); - if (item) menuManager.showToast("resources/w/ui/img/toast/item_removed001.png", item->name); - }; - - // Wire phone dialogue start function so MenuManager can trigger dialogues. - menuManager.startDialogueFunc = [this](const std::string& id) { - if (currentLocation()) currentLocation()->dialogueSystem.startDialogue(id); - }; - - menuManager.startDarklandsTransitionFunc = [this]() { - startDarklandsTransition(); - }; - - menuManager.startNightTransitionFunc = [this]() { - startNightTransition(); - }; - - menuManager.chatOpenCallback = [this](int chatIndex) { - if (currentLocation()) - currentLocation()->scriptEngine.callChatOpenCallback(chatIndex); - }; - // Wire chat-bubble callback so dynamic bubbles appear as dialogue lines are shown. for (auto& [name, loc] : gameState.locations) { loc->dialogueSystem.setOnChatBubbleReady([this](const std::string& text, bool incoming) { @@ -562,25 +634,6 @@ namespace ZL menuManager.onCutsceneFinished(); }); } - menuManager.skipCutsceneFunc = [this]() { - if (currentLocation()) currentLocation()->dialogueSystem.skipCutscene(); - }; - - std::cout << "Load resurces step 13" << std::endl; - - try { - menuManager.setup(gameState.inventory, CONST_ZIP_FILE); - std::cout << "UI loaded successfully" << std::endl; - } - catch (const std::exception& e) { - - std::cerr << "Failed to load UI: " << e.what() << std::endl; - } - - menuManager.startGameFunc = [this]() { - gameState.currentLocationName = "location_dorm"; - currentLocation()->scriptEngine.callLocationEnterCallback(); - }; // Wire HP-change callbacks so all player instances update the health bar HUD. for (auto& [name, loc] : gameState.locations) { @@ -590,18 +643,6 @@ namespace ZL }; } } - - loadingCompleted = true; - - if (audioPlayer->init()) { - audioPlayer->setMusicVolume(100); - audioPlayer->setSoundVolume(80); - std::cout << "Audio initialized successfully" << std::endl; - } - else { - std::cout << "Audio initialization failed" << std::endl; - } - } void Game::drawUI() @@ -1515,4 +1556,50 @@ namespace ZL glDisable(GL_BLEND); } + SaveSlotInfo Game::readSlotInfo(int slot) const + { + std::string path = "save_slot" + std::to_string(slot) + ".json"; + std::string content = ZL::readTextFile(path); + if (content.empty()) return {}; + try { + nlohmann::json root = nlohmann::json::parse(content); + SaveSlotInfo info; + info.empty = false; + info.locationName = root.value("currentLocationName", ""); + info.savedAt = root.value("savedAt", ""); + return info; + } catch (...) { + return {}; + } + } + + void Game::saveGame(int slot) + { + nlohmann::json root; + gameState.save(root); + std::string path = "save_slot" + std::to_string(slot) + ".json"; + std::ofstream file(path); + if (file.is_open()) { + file << root.dump(2); + } else { + std::cerr << "[save] Could not open " << path << " for writing" << std::endl; + } + } + + void Game::loadGame(int slot) + { + std::string path = "save_slot" + std::to_string(slot) + ".json"; + std::string content = ZL::readTextFile(path); + if (content.empty()) { + std::cerr << "[save] Save file not found or empty: " << path << std::endl; + return; + } + try { + nlohmann::json root = nlohmann::json::parse(content); + gameState.load(root); + } catch (const std::exception& e) { + std::cerr << "[save] Failed to parse save file " << path << ": " << e.what() << std::endl; + } + } + } // namespace ZL \ No newline at end of file diff --git a/src/Game.h b/src/Game.h index 87c805e..c165d55 100644 --- a/src/Game.h +++ b/src/Game.h @@ -23,6 +23,12 @@ #include "GameState.h" namespace ZL { + struct SaveSlotInfo { + std::string locationName; + std::string savedAt; + bool empty = true; + }; + class Game { public: Game(); @@ -89,6 +95,11 @@ namespace ZL { Location* currentLocation() const; + void saveGame(int slot); + void loadGame(int slot); + SaveSlotInfo readSlotInfo(int slot) const; + void createLocations(); + int64_t getSyncTimeMs(); void processTickCount(); void drawScene(); diff --git a/src/GameState.cpp b/src/GameState.cpp index caf3e7a..555cd84 100644 --- a/src/GameState.cpp +++ b/src/GameState.cpp @@ -1 +1,154 @@ #include "GameState.h" +#include +#include +#include + +namespace ZL { + +void GameState::save(nlohmann::json& out) const +{ + // Timestamp + std::time_t now = std::time(nullptr); + std::tm* tm = std::localtime(&now); + std::ostringstream ts; + ts << std::put_time(tm, "%Y-%m-%d %H:%M"); + out["savedAt"] = ts.str(); + + // --- World --- + out["currentLocationName"] = currentLocationName; + out["isDarklands"] = isDarklands; + out["isNight"] = isNight; + out["isDawn"] = isDawn; + + // --- Player resources --- + out["playerHp"] = playerHp; + out["playerMaxHp"] = playerMaxHp; + out["money"] = money; + + // --- Script globals --- + nlohmann::json gi = nlohmann::json::object(); + for (const auto& [k, v] : globalInts) gi[k] = v; + out["globalInts"] = std::move(gi); + + nlohmann::json gf = nlohmann::json::object(); + for (const auto& [k, v] : globalFloats) gf[k] = v; + out["globalFloats"] = std::move(gf); + + // --- Tutorial --- + out["tutorialStep"] = static_cast(tutorialStep); + out["uniIntTutorialState"] = static_cast(uniIntTutorialState); + out["tutorialPhonePickedUp"] = tutorialPhonePickedUp; + out["tutorialJournalPickedUp"] = tutorialJournalPickedUp; + out["tutorialPhoneChatScreenOpened"] = tutorialPhoneChatScreenOpened; + out["tutorialJournalScreenOpened"] = tutorialJournalScreenOpened; + out["tutorialNeedOpenTaxiScreen"] = tutorialNeedOpenTaxiScreen; + + // --- Phone / chat --- + out["chatUnread"] = { chatUnread[0], chatUnread[1], chatUnread[2] }; + out["chatPreviewMsg"] = { chatPreviewMsg[0], chatPreviewMsg[1], chatPreviewMsg[2] }; + + nlohmann::json histories = nlohmann::json::array(); + for (int i = 0; i < 3; ++i) { + nlohmann::json msgs = nlohmann::json::array(); + for (const auto& msg : chatHistory[i]) { + msgs.push_back({ {"text", msg.text}, {"incoming", msg.incoming} }); + } + histories.push_back(std::move(msgs)); + } + out["chatHistory"] = std::move(histories); + + // --- Inventory --- + nlohmann::json invJson; + inventory.save(invJson); + out["inventory"] = std::move(invJson); + + // --- Quest journal --- + nlohmann::json questJson; + questJournal.save(questJson); + out["questJournal"] = std::move(questJson); + + // --- Locations --- + nlohmann::json locsJson = nlohmann::json::object(); + for (const auto& [name, loc] : locations) { + if (loc) { + nlohmann::json locJson; + loc->save(locJson); + locsJson[name] = std::move(locJson); + } + } + out["locations"] = std::move(locsJson); +} + +void GameState::load(const nlohmann::json& in) +{ + // --- World --- + currentLocationName = in.value("currentLocationName", currentLocationName); + isDarklands = in.value("isDarklands", isDarklands); + isNight = in.value("isNight", isNight); + isDawn = in.value("isDawn", isDawn); + + // --- Player resources --- + playerHp = in.value("playerHp", playerHp); + playerMaxHp = in.value("playerMaxHp", playerMaxHp); + money = in.value("money", money); + + // --- Script globals --- + if (in.contains("globalInts") && in["globalInts"].is_object()) { + for (const auto& [k, v] : in["globalInts"].items()) + globalInts[k] = v.get(); + } + if (in.contains("globalFloats") && in["globalFloats"].is_object()) { + for (const auto& [k, v] : in["globalFloats"].items()) + globalFloats[k] = v.get(); + } + + // --- Tutorial --- + tutorialStep = static_cast(in.value("tutorialStep", static_cast(tutorialStep))); + uniIntTutorialState = static_cast(in.value("uniIntTutorialState", static_cast(uniIntTutorialState))); + tutorialPhonePickedUp = in.value("tutorialPhonePickedUp", tutorialPhonePickedUp); + tutorialJournalPickedUp = in.value("tutorialJournalPickedUp", tutorialJournalPickedUp); + tutorialPhoneChatScreenOpened = in.value("tutorialPhoneChatScreenOpened", tutorialPhoneChatScreenOpened); + tutorialJournalScreenOpened = in.value("tutorialJournalScreenOpened", tutorialJournalScreenOpened); + tutorialNeedOpenTaxiScreen = in.value("tutorialNeedOpenTaxiScreen", tutorialNeedOpenTaxiScreen); + + // --- Phone / chat --- + if (in.contains("chatUnread") && in["chatUnread"].is_array()) { + for (int i = 0; i < 3 && i < static_cast(in["chatUnread"].size()); ++i) + chatUnread[i] = in["chatUnread"][i].get(); + } + if (in.contains("chatPreviewMsg") && in["chatPreviewMsg"].is_array()) { + for (int i = 0; i < 3 && i < static_cast(in["chatPreviewMsg"].size()); ++i) + chatPreviewMsg[i] = in["chatPreviewMsg"][i].get(); + } + if (in.contains("chatHistory") && in["chatHistory"].is_array()) { + for (int i = 0; i < 3 && i < static_cast(in["chatHistory"].size()); ++i) { + chatHistory[i].clear(); + if (in["chatHistory"][i].is_array()) { + for (const auto& msg : in["chatHistory"][i]) { + StoredChatMessage m; + m.text = msg.value("text", ""); + m.incoming = msg.value("incoming", false); + chatHistory[i].push_back(std::move(m)); + } + } + } + } + + // --- Inventory --- + if (in.contains("inventory")) inventory.load(in["inventory"]); + + // --- Quest journal --- + if (in.contains("questJournal")) questJournal.load(in["questJournal"]); + + // --- Locations --- + if (in.contains("locations") && in["locations"].is_object()) { + for (const auto& [name, locJson] : in["locations"].items()) { + auto it = locations.find(name); + if (it != locations.end() && it->second) { + it->second->load(locJson); + } + } + } +} + +} // namespace ZL diff --git a/src/GameState.h b/src/GameState.h index c8d7407..b21fc56 100644 --- a/src/GameState.h +++ b/src/GameState.h @@ -2,6 +2,8 @@ #include "items/Item.h" #include "quest/QuestJournal.h" #include "Location.h" +#include "ISaveable.h" +#include "external/nlohmann/json.hpp" #include #include #include @@ -32,7 +34,7 @@ struct StoredChatMessage { bool incoming; }; -struct GameState { +struct GameState : public ISaveable { // --- World --- std::unordered_map> locations; std::string currentLocationName; @@ -66,6 +68,9 @@ struct GameState { bool chatUnread[3] = { true, true, true }; std::string chatPreviewMsg[3]; std::vector chatHistory[3]; + + void save(nlohmann::json& out) const override; + void load(const nlohmann::json& in) override; }; } // namespace ZL diff --git a/src/ISaveable.h b/src/ISaveable.h new file mode 100644 index 0000000..4f25b14 --- /dev/null +++ b/src/ISaveable.h @@ -0,0 +1,12 @@ +#pragma once +#include "external/nlohmann/json.hpp" + +namespace ZL { + +struct ISaveable { + virtual void save(nlohmann::json& out) const = 0; + virtual void load(const nlohmann::json& in) = 0; + virtual ~ISaveable() = default; +}; + +} // namespace ZL diff --git a/src/Location.cpp b/src/Location.cpp index 6e7d9e4..48b91e1 100644 --- a/src/Location.cpp +++ b/src/Location.cpp @@ -312,8 +312,6 @@ namespace ZL triggerZones.push_back(std::move(tz)); } - // Resize the parallel playerInside vector in state to match - state.triggerZonePlayerInside.assign(triggerZones.size(), false); std::cout << "[TRIGGER] Loaded " << triggerZones.size() << " trigger zone(s) from " << jsonPath << std::endl; } @@ -388,11 +386,9 @@ namespace ZL const float dist = (playerPos - tz.position).norm(); if (!tz.playerInside && dist <= tz.radius) { tz.playerInside = true; - state.triggerZonePlayerInside[i] = true; scriptEngine.callTriggerEnterCallback(tz.id); } else if (tz.playerInside && dist > tz.radius + tz.hysteresis) { tz.playerInside = false; - state.triggerZonePlayerInside[i] = false; scriptEngine.callTriggerExitCallback(tz.id); } } @@ -1833,7 +1829,7 @@ namespace ZL // ---- Save / Load ---- - void Location::saveFullState(nlohmann::json& out) const + void Location::save(nlohmann::json& out) const { // Location-level state nlohmann::json locState; @@ -1878,9 +1874,23 @@ namespace ZL nlohmann::json scriptState; scriptEngine.saveScriptGlobals(scriptState); out["scriptGlobals"] = std::move(scriptState); + + // Trigger zone runtime state (by id) + nlohmann::json tzState = nlohmann::json::object(); + for (const auto& tz : triggerZones) { + tzState[tz.id] = { {"enabled", tz.enabled}, {"playerInside", tz.playerInside} }; + } + out["triggerZoneState"] = std::move(tzState); + + // Teleport zone runtime state (by id) + nlohmann::json tpState = nlohmann::json::object(); + for (const auto& tp : teleportZones) { + tpState[tp.id] = tp.active; + } + out["teleportZoneState"] = std::move(tpState); } - void Location::loadFullState(const nlohmann::json& in) + void Location::load(const nlohmann::json& in) { // Location-level state if (in.contains("locationState")) { @@ -1894,10 +1904,24 @@ namespace ZL navigation = &navigationMaps[state.activeNavigationIndex]; } - // Re-sync triggerZones[i].playerInside from state - for (int i = 0; i < static_cast(triggerZones.size()); ++i) { - if (i < static_cast(state.triggerZonePlayerInside.size())) { - triggerZones[i].playerInside = state.triggerZonePlayerInside[i]; + // Restore trigger zone runtime state (by id) + if (in.contains("triggerZoneState") && in["triggerZoneState"].is_object()) { + const auto& tzs = in["triggerZoneState"]; + for (auto& tz : triggerZones) { + if (tzs.contains(tz.id)) { + tz.enabled = tzs[tz.id].value("enabled", tz.enabled); + tz.playerInside = tzs[tz.id].value("playerInside", tz.playerInside); + } + } + } + + // Restore teleport zone runtime state (by id) + if (in.contains("teleportZoneState") && in["teleportZoneState"].is_object()) { + const auto& tps = in["teleportZoneState"]; + for (auto& tp : teleportZones) { + if (tps.contains(tp.id)) { + tp.active = tps[tp.id].get(); + } } } diff --git a/src/Location.h b/src/Location.h index 38c1c0b..8de3287 100644 --- a/src/Location.h +++ b/src/Location.h @@ -15,6 +15,7 @@ #include "TeleportZone.h" #include "LocationEditor.h" #include "LocationState.h" +#include "ISaveable.h" #include #include #include @@ -60,7 +61,7 @@ namespace ZL Eigen::Vector3f playerPosition = Eigen::Vector3f::Zero(); }; - class Location + class Location : public ISaveable { public: Location(Renderer& iRenderer, Inventory& iInventory); @@ -155,11 +156,10 @@ namespace ZL // ---- Save / Load ---- // Serialises LocationState + all Character/InteractiveObject sub-states + Lua globals. - void saveFullState(nlohmann::json& out) const; - // Restores state from JSON written by saveFullState. + void save(nlohmann::json& out) const override; + // Restores state from JSON written by save. // Navigation pointer and triggerZone.playerInside are re-synced automatically. - void loadFullState(const nlohmann::json& in); - + void load(const nlohmann::json& in) override; protected: friend class LocationEditor; Renderer& renderer; diff --git a/src/LocationState.cpp b/src/LocationState.cpp index 96c4d3f..7168969 100644 --- a/src/LocationState.cpp +++ b/src/LocationState.cpp @@ -18,10 +18,6 @@ void LocationState::save(nlohmann::json& out) const out["isDawn"] = isDawn; out["tutorialInteractiveObjectsLocked"] = tutorialInteractiveObjectsLocked; - - nlohmann::json tzArr = nlohmann::json::array(); - for (bool v : triggerZonePlayerInside) tzArr.push_back(v); - out["triggerZonePlayerInside"] = std::move(tzArr); } void LocationState::load(const nlohmann::json& in) @@ -40,13 +36,6 @@ void LocationState::load(const nlohmann::json& in) isDawn = in.value("isDawn", false); tutorialInteractiveObjectsLocked = in.value("tutorialInteractiveObjectsLocked", false); - - triggerZonePlayerInside.clear(); - if (in.contains("triggerZonePlayerInside") && in["triggerZonePlayerInside"].is_array()) { - for (const auto& v : in["triggerZonePlayerInside"]) { - triggerZonePlayerInside.push_back(v.get()); - } - } } } // namespace ZL diff --git a/src/LocationState.h b/src/LocationState.h index 6c97b5b..2421714 100644 --- a/src/LocationState.h +++ b/src/LocationState.h @@ -1,10 +1,10 @@ #pragma once -#include #include "external/nlohmann/json.hpp" +#include "ISaveable.h" namespace ZL { -struct LocationState { +struct LocationState : public ISaveable { // ---- Camera ---- float cameraAzimuth = -2.35f; float cameraInclination = 1.1036f; @@ -26,15 +26,11 @@ struct LocationState { // ---- Tutorial ---- bool tutorialInteractiveObjectsLocked = false; - // ---- Trigger-zone runtime flags ---- - // Indexed parallel to Location::triggerZones. - // The zones themselves are static config reloaded from JSON; - // only playerInside is live state that must be saved. - std::vector triggerZonePlayerInside; + // (trigger/teleport zone runtime flags are serialised by id directly in Location::save/load) // ---- Serialisation ---- - void save(nlohmann::json& out) const; - void load(const nlohmann::json& in); + void save(nlohmann::json& out) const override; + void load(const nlohmann::json& in) override; }; } // namespace ZL diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index bace555..c04ab09 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -1,4 +1,5 @@ #include "MenuManager.h" +#include "Game.h" #include "render/TextRenderer.h" #include #include @@ -186,11 +187,15 @@ namespace ZL { showMainMenu(); } - void MenuManager::enterGameplay() { - if (uiState_ == GameUiState::MainMenu && startGameFunc) startGameFunc(); + void MenuManager::enterGameplay(bool isFromLoad) { + if (!isFromLoad && uiState_ == GameUiState::MainMenu && startGameFunc) startGameFunc(); uiState_ = GameUiState::Gameplay; uiManager.replaceRoot(hudRoot); topUiManager.replaceRoot(hudTopHintRoot_); + if (isFromLoad) { + uiManager.clearMenuStack(); + topUiManager.clearMenuStack(); + } applyCurrentHealthBar(); @@ -202,10 +207,53 @@ namespace ZL { showModalMenuScreen(); }); } + + if (isFromLoad) + applyHudForCurrentState(); + } + + void MenuManager::applyHudForCurrentState() { + // Clear the step-0 dialogue hint unless we're actually at step 0. + if (gameState_.tutorialStep != TutorialStep::Step0) + topUiManager.replaceRoot(nullptr); + + const std::string& loc = gameState_.currentLocationName; + + // Non-dorm locations: reuse the existing location-change logic. + if (loc == "uni_exterior" || loc == "uni_interior") { + onLocationChanged(loc); + return; + } + + // Dorm (or pre-first-teleport): select HUD root by tutorial step. + std::shared_ptr nextRoot; + switch (gameState_.tutorialStep) { + case TutorialStep::Step0: nextRoot = hudRoot; break; + case TutorialStep::Step1: nextRoot = hudStep1Root; break; + case TutorialStep::Step2: nextRoot = hudStep2Root; break; + case TutorialStep::Step3: nextRoot = hudStep3Root; break; + case TutorialStep::Step4: nextRoot = hudStep4Root; break; + default: break; // Step5/6 handled below + } + + if (nextRoot) { + uiManager.replaceRoot(nextRoot); + applyCurrentHealthBar(); + uiManager.setButtonCallback("inventoryButton", [this](const std::string&) { openInventory(); }); + if (uiManager.findButton("settingsButton")) + uiManager.setButtonCallback("settingsButton", [this](const std::string&) { showModalMenuScreen(); }); + } else { + // Step5 or Step6: pick the right variant and apply hint visibility. + refreshItemPickupHud(); + } } void MenuManager::showMainMenu() { + if (onResetGame) onResetGame(); uiState_ = GameUiState::MainMenu; + uiManager.clearMenuStack(); + topUiManager.replaceRoot(nullptr); + topUiManager.clearMenuStack(); uiManager.replaceRoot(mainMenuRoot); uiManager.setTextButtonCallback("menuStartButton", [this](const std::string&) { @@ -254,6 +302,24 @@ namespace ZL { uiManager.popMenu(); uiManager.updateAllLayouts(); }); + + static const char* kSlotButtons[4] = { + "loadSlot1Button", "loadSlot2Button", "loadSlot3Button", "loadSlot4Button" + }; + for (int i = 0; i < 4; ++i) { + int slot = i + 1; + if (getSlotInfoFunc) { + SaveSlotInfo info = getSlotInfoFunc(slot); + std::string label = info.empty + ? "Empty" + : info.locationName + "\n" + info.savedAt; + uiManager.setTextButtonText(kSlotButtons[i], label); + } + uiManager.setTextButtonCallback(kSlotButtons[i], [this, slot](const std::string&) { + if (onLoadGame) onLoadGame(slot); + enterGameplay(true); + }); + } } void MenuManager::showSaveGameScreen() @@ -263,6 +329,31 @@ namespace ZL { uiManager.popMenu(); uiManager.updateAllLayouts(); }); + + static const char* kSlotButtons[4] = { + "saveSlot1Button", "saveSlot2Button", "saveSlot3Button", "saveSlot4Button" + }; + for (int i = 0; i < 4; ++i) { + int slot = i + 1; + const char* buttonName = kSlotButtons[i]; + if (getSlotInfoFunc) { + SaveSlotInfo info = getSlotInfoFunc(slot); + std::string label = info.empty + ? "Empty" + : info.locationName + "\n" + info.savedAt; + uiManager.setTextButtonText(buttonName, label); + } + uiManager.setTextButtonCallback(buttonName, [this, slot, buttonName](const std::string&) { + if (onSaveGame) onSaveGame(slot); + if (getSlotInfoFunc) { + SaveSlotInfo info = getSlotInfoFunc(slot); + std::string label = info.empty + ? "Empty" + : info.locationName + "\n" + info.savedAt; + uiManager.setTextButtonText(buttonName, label); + } + }); + } } void MenuManager::openInventory() { @@ -711,6 +802,11 @@ namespace ZL { uiManager.setButtonCallback("menuSettingsButton", [this](const std::string&) { showSettingsScreen(); }); + + uiManager.setButtonCallback("menuExitButton", [this](const std::string&) { + uiManager.popMenu(); + showMainMenu(); + }); } void MenuManager::tutorialShowTaxiHint() diff --git a/src/MenuManager.h b/src/MenuManager.h index 07da0c0..c3a712b 100644 --- a/src/MenuManager.h +++ b/src/MenuManager.h @@ -10,6 +10,9 @@ #include #include +// Forward-declared here to avoid pulling Game.h into MenuManager.h. +namespace ZL { struct SaveSlotInfo; } + namespace ZL { extern const char* CONST_ZIP_FILE; @@ -66,6 +69,10 @@ namespace ZL { std::function skipCutsceneFunc; std::function callTaxiFunc; std::function tutorialUnlockInteractiveObjectsFunc; + std::function onSaveGame; + std::function onLoadGame; + std::function getSlotInfoFunc; + std::function onResetGame; // Called when a chat message bubble should be shown (text + direction) void onChatBubbleReady(const std::string& text, bool incoming); @@ -95,7 +102,8 @@ namespace ZL { private: GameState& gameState_; - void enterGameplay(); + void enterGameplay(bool isFromLoad = false); + void applyHudForCurrentState(); void refreshQuestJournalUi(); void selectQuestByIndex(int index); void refreshItemPickupHud(); diff --git a/src/ScriptEngine.h b/src/ScriptEngine.h index 142760d..6ad34ef 100644 --- a/src/ScriptEngine.h +++ b/src/ScriptEngine.h @@ -4,13 +4,14 @@ #include #include "quest/QuestJournal.h" #include "external/nlohmann/json.hpp" +#include "ISaveable.h" namespace ZL { class Location; class Inventory; -class ScriptEngine { +class ScriptEngine : public ISaveable { public: ScriptEngine(); ~ScriptEngine(); @@ -60,6 +61,9 @@ public: // has already run and registered all callbacks. void loadScriptGlobals(const nlohmann::json& in); + void save(nlohmann::json& out) const override { saveScriptGlobals(out); } + void load(const nlohmann::json& in) override { loadScriptGlobals(in); } + private: struct Impl; std::unique_ptr impl; diff --git a/src/dialogue/DialogueRuntime.h b/src/dialogue/DialogueRuntime.h index 775a64b..793c26b 100644 --- a/src/dialogue/DialogueRuntime.h +++ b/src/dialogue/DialogueRuntime.h @@ -3,6 +3,7 @@ #include "dialogue/DialogueDatabase.h" #include "quest/QuestJournal.h" #include "external/nlohmann/json.hpp" +#include "ISaveable.h" #include #include #include @@ -11,7 +12,7 @@ namespace ZL::Dialogue { -class DialogueRuntime { +class DialogueRuntime : public ZL::ISaveable { public: void setDatabase(const DialogueDatabase* value); @@ -43,10 +44,10 @@ public: // Saves whether a dialogue is active, its id, and current node. // Cutscene state is not saved. - void save(nlohmann::json& out) const; + void save(nlohmann::json& out) const override; // Restores saved dialogue state. Text reveal snaps to completion so the // typing animation doesn't replay. No callbacks are fired on restore. - void load(const nlohmann::json& in); + void load(const nlohmann::json& in) override; private: enum class Mode { diff --git a/src/dialogue/DialogueSystem.h b/src/dialogue/DialogueSystem.h index bf5d712..848edc3 100644 --- a/src/dialogue/DialogueSystem.h +++ b/src/dialogue/DialogueSystem.h @@ -7,13 +7,14 @@ #include "cutscene/CutsceneOverlay.h" #include "cutscene/CutsceneRuntime.h" #include "quest/QuestJournal.h" +#include "ISaveable.h" #include #include #include namespace ZL::Dialogue { -class DialogueSystem { +class DialogueSystem : public ZL::ISaveable { public: bool init(Renderer& renderer, const std::string& zipFile = ""); @@ -45,6 +46,9 @@ public: void saveDialogueState(nlohmann::json& out) const { dialogueRuntime.save(out); } void loadDialogueState(const nlohmann::json& in) { dialogueRuntime.load(in); } + void save(nlohmann::json& out) const override { saveDialogueState(out); } + void load(const nlohmann::json& in) override { loadDialogueState(in); } + bool isActive() const { return dialogueRuntime.isActive() || cutsceneRuntime.isActive(); } bool blocksGameplayInput() const { return isActive(); } diff --git a/src/items/InteractiveObject.cpp b/src/items/InteractiveObject.cpp index 560b9ef..8bc8af9 100644 --- a/src/items/InteractiveObject.cpp +++ b/src/items/InteractiveObject.cpp @@ -67,7 +67,6 @@ namespace ZL { obj.state.jsonPositionY, obj.state.jsonPositionZ); } - return obj; } @@ -239,8 +238,25 @@ namespace ZL { out["scale"] = scale; out["alpha"] = alpha; out["isActive"] = isActive; - // isAnimating and animTask are not saved: animations snap to their end - // state on load (the final position/scale/alpha is already in the fields above). + out["isAnimating"] = isAnimating; + + if (isAnimating && animTask.has_value()) { + nlohmann::json at; + at["type"] = static_cast(animTask->type); + at["startPosX"] = animTask->startPos.x(); + at["startPosY"] = animTask->startPos.y(); + at["startPosZ"] = animTask->startPos.z(); + at["startRotY"] = animTask->startRotY; + at["startScale"] = animTask->startScale; + at["targetPosX"] = animTask->targetPos.x(); + at["targetPosY"] = animTask->targetPos.y(); + at["targetPosZ"] = animTask->targetPos.z(); + at["targetRotY"] = animTask->targetRotY; + at["targetScale"] = animTask->targetScale; + at["durationMs"] = animTask->durationMs; + at["elapsedMs"] = animTask->elapsedMs; + out["animTask"] = std::move(at); + } } void InteractiveObjectState::load(const nlohmann::json& in) @@ -252,9 +268,24 @@ namespace ZL { scale = in.value("scale", scale); alpha = in.value("alpha", alpha); isActive = in.value("isActive", isActive); - // Cancel any in-flight animation; state was restored to final values above. - isAnimating = false; + isAnimating = in.value("isAnimating", false); animTask.reset(); + + if (isAnimating && in.contains("animTask") && in["animTask"].is_object()) { + const auto& at = in["animTask"]; + AnimTask task; + task.type = static_cast(at.value("type", 0)); + task.startPos = { at.value("startPosX", 0.f), at.value("startPosY", 0.f), at.value("startPosZ", 0.f) }; + task.startRotY = at.value("startRotY", 0.f); + task.startScale = at.value("startScale", 1.f); + task.targetPos = { at.value("targetPosX", 0.f), at.value("targetPosY", 0.f), at.value("targetPosZ", 0.f) }; + task.targetRotY = at.value("targetRotY", 0.f); + task.targetScale = at.value("targetScale", 1.f); + task.durationMs = at.value("durationMs", 1000.f); + task.elapsedMs = at.value("elapsedMs", 0.f); + // onComplete is non-serializable; re-wired at runtime by Lua script callbacks. + animTask = std::move(task); + } } } // namespace ZL diff --git a/src/items/InteractiveObjectState.h b/src/items/InteractiveObjectState.h index f6f04ff..52cdccd 100644 --- a/src/items/InteractiveObjectState.h +++ b/src/items/InteractiveObjectState.h @@ -5,6 +5,7 @@ #include #include #include "external/nlohmann/json.hpp" +#include "ISaveable.h" namespace ZL { @@ -20,7 +21,7 @@ struct AnimTask { float targetScale = 1.f; float durationMs = 1000.f; float elapsedMs = 0.f; - std::function onComplete; // non-serializable + std::function onComplete; // non-serializable; re-wired at runtime by Lua callbacks }; // Paths and baked mesh transforms needed to recreate a LoadedGameObject from disk. @@ -37,7 +38,7 @@ struct LoadedGameObjectState { // All serializable state for an InteractiveObject: creation info + runtime mutable state. // Mirrors CharacterState / CharacterCreationInfo but kept as a single flat class // since interactive objects don't need the two-level separation. -class InteractiveObjectState { +class InteractiveObjectState : public ISaveable { public: // --- Creation info (fixed at load time) --- LoadedGameObjectState objectInfo; @@ -66,8 +67,8 @@ public: std::optional animTask; // --- Serialisation (runtime mutable fields only) --- - void save(nlohmann::json& out) const; - void load(const nlohmann::json& in); + void save(nlohmann::json& out) const override; + void load(const nlohmann::json& in) override; }; } // namespace ZL diff --git a/src/items/Item.cpp b/src/items/Item.cpp index 4b878b4..604355c 100644 --- a/src/items/Item.cpp +++ b/src/items/Item.cpp @@ -1,4 +1,5 @@ #include "Item.h" +#include "external/nlohmann/json.hpp" #include #include @@ -25,5 +26,32 @@ namespace ZL { [&itemId](const Item& item) { return item.id == itemId; }) != items.end(); } + void Inventory::save(nlohmann::json& out) const { + nlohmann::json arr = nlohmann::json::array(); + for (const auto& item : items) { + arr.push_back({ + {"id", item.id}, + {"name", item.name}, + {"description", item.description}, + {"icon", item.icon}, + {"selectedIcon", item.selectedIcon} + }); + } + out["items"] = std::move(arr); + } + + void Inventory::load(const nlohmann::json& in) { + items.clear(); + if (!in.contains("items") || !in["items"].is_array()) return; + for (const auto& j : in["items"]) { + Item item; + item.id = j.value("id", ""); + item.name = j.value("name", ""); + item.description = j.value("description", ""); + item.icon = j.value("icon", ""); + item.selectedIcon = j.value("selectedIcon", ""); + items.push_back(std::move(item)); + } + } } // namespace ZL \ No newline at end of file diff --git a/src/items/Item.h b/src/items/Item.h index 56f21da..551c06a 100644 --- a/src/items/Item.h +++ b/src/items/Item.h @@ -3,6 +3,7 @@ #include #include #include +#include "ISaveable.h" namespace ZL { @@ -19,11 +20,9 @@ namespace ZL { } }; - class Inventory { - private: - std::vector items; - + class Inventory : public ISaveable { public: + std::vector items; void addItem(const Item& item); void removeItem(const std::string& itemId); const std::vector& getItems() const { return items; } @@ -31,6 +30,9 @@ namespace ZL { void clear() { items.clear(); } size_t getCount() const { return items.size(); } + void save(nlohmann::json& out) const override; + void load(const nlohmann::json& in) override; + // Called whenever an item is added/removed; receives the item id. std::function onItemAdded; std::function onItemRemoved; diff --git a/src/quest/QuestJournal.cpp b/src/quest/QuestJournal.cpp index cef1715..f18056d 100644 --- a/src/quest/QuestJournal.cpp +++ b/src/quest/QuestJournal.cpp @@ -220,4 +220,46 @@ std::vector QuestJournal::getVisibleQuests() const { return result; } +void QuestJournal::save(nlohmann::json& out) const { + json arr = json::array(); + for (const auto& id : questOrder) { + const auto& qs = quests.at(id); + json qj; + qj["id"] = id; + qj["status"] = static_cast(qs.status); + qj["activeObjectiveIndex"] = qs.activeObjectiveIndex; + json objArr = json::array(); + for (const auto& obj : qs.definition.objectives) { + objArr.push_back({ {"id", obj.id}, {"completed", obj.completed}, {"visible", obj.visible} }); + } + qj["objectives"] = std::move(objArr); + arr.push_back(std::move(qj)); + } + out["quests"] = std::move(arr); +} + +void QuestJournal::load(const nlohmann::json& in) { + if (!in.contains("quests") || !in["quests"].is_array()) return; + for (const auto& qj : in["quests"]) { + const std::string id = qj.value("id", ""); + auto it = quests.find(id); + if (it == quests.end()) continue; + auto& qs = it->second; + qs.status = static_cast(qj.value("status", 0)); + qs.activeObjectiveIndex = qj.value("activeObjectiveIndex", 0); + if (qj.contains("objectives") && qj["objectives"].is_array()) { + for (const auto& oj : qj["objectives"]) { + const std::string objId = oj.value("id", ""); + for (auto& obj : qs.definition.objectives) { + if (obj.id == objId) { + obj.completed = oj.value("completed", obj.completed); + obj.visible = oj.value("visible", obj.visible); + break; + } + } + } + } + } +} + } // namespace ZL::Quest diff --git a/src/quest/QuestJournal.h b/src/quest/QuestJournal.h index 84be965..99aa7fe 100644 --- a/src/quest/QuestJournal.h +++ b/src/quest/QuestJournal.h @@ -1,6 +1,8 @@ #pragma once #include "quest/QuestTypes.h" +#include "ISaveable.h" +#include "external/nlohmann/json.hpp" #include #include #include @@ -8,10 +10,13 @@ namespace ZL::Quest { -class QuestJournal { +class QuestJournal : public ZL::ISaveable { public: bool loadFromFile(const std::string& path, const std::string& zipFile = ""); + void save(nlohmann::json& out) const override; + void load(const nlohmann::json& in) override; + // Event callbacks — set by the owner (MenuManager) to receive notifications. std::function onQuestUnlocked; std::function onQuestCompleted; diff --git a/src/render/TextRenderer.cpp b/src/render/TextRenderer.cpp index 0b7cbeb..7dd9547 100644 --- a/src/render/TextRenderer.cpp +++ b/src/render/TextRenderer.cpp @@ -12,6 +12,15 @@ namespace ZL { +struct GlyphAtlasData { + std::unordered_map glyphs; + std::shared_ptr atlasTexture; + size_t atlasWidth = 0; + size_t atlasHeight = 0; + float lineHeight = 32.f; +}; +static std::unordered_map s_glyphAtlasCache; + // Decode one UTF-8 codepoint from str at byte position i; advances i past the sequence. static uint32_t nextUtf8Codepoint(const std::string& str, size_t& i) { @@ -71,6 +80,18 @@ void TextRenderer::ClearCache() bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const std::string& zipfilename) { + std::string cacheKey = ttfPath + "|" + zipfilename + "|" + std::to_string(pixelSize); + auto cacheIt = s_glyphAtlasCache.find(cacheKey); + if (cacheIt != s_glyphAtlasCache.end()) { + const GlyphAtlasData& cached = cacheIt->second; + glyphs = cached.glyphs; + atlasTexture = cached.atlasTexture; + atlasWidth = cached.atlasWidth; + atlasHeight = cached.atlasHeight; + lineHeight = cached.lineHeight; + return true; + } + // 1. Загружаем сырые данные из ZIP std::vector fontData; try { @@ -312,10 +333,18 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s FT_Done_Face(face); FT_Done_FreeType(ft); - // После FT_Done_Face данные из fontData больше не нужны, + // После FT_Done_Face данные из fontData больше не нужны, // вектор сам очистится при выходе из функции. glBindTexture(GL_TEXTURE_2D, 0); + + GlyphAtlasData& entry = s_glyphAtlasCache[cacheKey]; + entry.glyphs = glyphs; + entry.atlasTexture = atlasTexture; + entry.atlasWidth = atlasWidth; + entry.atlasHeight = atlasHeight; + entry.lineHeight = lineHeight; + return true; } diff --git a/src/render/TextureManager.cpp b/src/render/TextureManager.cpp index 505efed..f1eb77f 100644 --- a/src/render/TextureManager.cpp +++ b/src/render/TextureManager.cpp @@ -528,7 +528,8 @@ namespace ZL void TextureManager::Unload(const std::string& fileName, const std::string& zipFile) { - textureMap.erase(MakeKey(fileName, zipFile)); + //For now, we should keep all textures in memory, to make game load faster after restart + //textureMap.erase(MakeKey(fileName, zipFile)); } void TextureManager::UnloadAll()