Working on save-load system and UI
This commit is contained in:
parent
901142385d
commit
d7db0faea3
@ -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}"
|
||||
"$<TARGET_FILE_DIR:witcher001>/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}"
|
||||
"$<TARGET_FILE_DIR:witcher001>"
|
||||
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/SDL_mixer-release-2.8.0/install-$<CONFIG>/bin/SDL2_mixer$<$<CONFIG:Debug>:d>.dll"
|
||||
|
||||
# This is only for profiling:
|
||||
#"${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/SDL_mixer-release-2.8.0/install-Release/bin/SDL2_mixer$<$<CONFIG:Debug>:d>.dll"
|
||||
"$<TARGET_FILE_DIR:witcher001>/SDL2_mixer$<$<CONFIG:Debug>:d>.dll"
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
@ -1343,4 +1343,4 @@ end)
|
||||
setDay0setup()
|
||||
game_api.switch_navigation(0)
|
||||
--debugAllOpen()
|
||||
print("Lua script loaded successfully!")
|
||||
print("Lua script loaded successfully!---!")
|
||||
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
|
||||
@ -13,6 +13,16 @@ namespace ZL
|
||||
using std::max;
|
||||
#endif
|
||||
|
||||
static std::unordered_map<std::string, BoneSystemNew> 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<std::string>& 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<char> 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()
|
||||
|
||||
@ -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> Character::createFromState(CharacterState st, Rendere
|
||||
auto loadTex = [&](const std::string& path) -> std::shared_ptr<Texture> {
|
||||
if (path.empty()) return nullptr;
|
||||
try {
|
||||
return std::make_shared<Texture>(CreateTextureDataFromPng(path, zip));
|
||||
//return std::make_shared<Texture>(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;
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
#include <Eigen/Core>
|
||||
#include <Eigen/Geometry>
|
||||
#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
|
||||
|
||||
235
src/Game.cpp
235
src/Game.cpp
@ -1,6 +1,7 @@
|
||||
#include "Game.h"
|
||||
#include "AnimatedModel.h"
|
||||
#include "utils/Utils.h"
|
||||
#include <fstream>
|
||||
#include "items/ItemRegistry.h"
|
||||
#include "render/OpenGlExtensions.h"
|
||||
#include <iostream>
|
||||
@ -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
|
||||
11
src/Game.h
11
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();
|
||||
|
||||
@ -1 +1,154 @@
|
||||
#include "GameState.h"
|
||||
#include <ctime>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
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<int>(tutorialStep);
|
||||
out["uniIntTutorialState"] = static_cast<int>(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<int>();
|
||||
}
|
||||
if (in.contains("globalFloats") && in["globalFloats"].is_object()) {
|
||||
for (const auto& [k, v] : in["globalFloats"].items())
|
||||
globalFloats[k] = v.get<float>();
|
||||
}
|
||||
|
||||
// --- Tutorial ---
|
||||
tutorialStep = static_cast<TutorialStep>(in.value("tutorialStep", static_cast<int>(tutorialStep)));
|
||||
uniIntTutorialState = static_cast<UniIntTutorialState>(in.value("uniIntTutorialState", static_cast<int>(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<int>(in["chatUnread"].size()); ++i)
|
||||
chatUnread[i] = in["chatUnread"][i].get<bool>();
|
||||
}
|
||||
if (in.contains("chatPreviewMsg") && in["chatPreviewMsg"].is_array()) {
|
||||
for (int i = 0; i < 3 && i < static_cast<int>(in["chatPreviewMsg"].size()); ++i)
|
||||
chatPreviewMsg[i] = in["chatPreviewMsg"][i].get<std::string>();
|
||||
}
|
||||
if (in.contains("chatHistory") && in["chatHistory"].is_array()) {
|
||||
for (int i = 0; i < 3 && i < static_cast<int>(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
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
#include "items/Item.h"
|
||||
#include "quest/QuestJournal.h"
|
||||
#include "Location.h"
|
||||
#include "ISaveable.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
@ -32,7 +34,7 @@ struct StoredChatMessage {
|
||||
bool incoming;
|
||||
};
|
||||
|
||||
struct GameState {
|
||||
struct GameState : public ISaveable {
|
||||
// --- World ---
|
||||
std::unordered_map<std::string, std::shared_ptr<Location>> locations;
|
||||
std::string currentLocationName;
|
||||
@ -66,6 +68,9 @@ struct GameState {
|
||||
bool chatUnread[3] = { true, true, true };
|
||||
std::string chatPreviewMsg[3];
|
||||
std::vector<StoredChatMessage> chatHistory[3];
|
||||
|
||||
void save(nlohmann::json& out) const override;
|
||||
void load(const nlohmann::json& in) override;
|
||||
};
|
||||
|
||||
} // namespace ZL
|
||||
|
||||
12
src/ISaveable.h
Normal file
12
src/ISaveable.h
Normal file
@ -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
|
||||
@ -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<int>(triggerZones.size()); ++i) {
|
||||
if (i < static_cast<int>(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<bool>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
#include "TeleportZone.h"
|
||||
#include "LocationEditor.h"
|
||||
#include "LocationState.h"
|
||||
#include "ISaveable.h"
|
||||
#include <functional>
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
@ -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;
|
||||
|
||||
@ -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<bool>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ZL
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
#pragma once
|
||||
#include <vector>
|
||||
#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<bool> 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
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#include "MenuManager.h"
|
||||
#include "Game.h"
|
||||
#include "render/TextRenderer.h"
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
@ -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<UiNode> 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()
|
||||
|
||||
@ -10,6 +10,9 @@
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
// 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<void()> skipCutsceneFunc;
|
||||
std::function<void()> callTaxiFunc;
|
||||
std::function<void()> tutorialUnlockInteractiveObjectsFunc;
|
||||
std::function<void(int)> onSaveGame;
|
||||
std::function<void(int)> onLoadGame;
|
||||
std::function<SaveSlotInfo(int)> getSlotInfoFunc;
|
||||
std::function<void()> 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();
|
||||
|
||||
@ -4,13 +4,14 @@
|
||||
#include <unordered_map>
|
||||
#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> impl;
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
#include "dialogue/DialogueDatabase.h"
|
||||
#include "quest/QuestJournal.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include "ISaveable.h"
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@ -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 {
|
||||
|
||||
@ -7,13 +7,14 @@
|
||||
#include "cutscene/CutsceneOverlay.h"
|
||||
#include "cutscene/CutsceneRuntime.h"
|
||||
#include "quest/QuestJournal.h"
|
||||
#include "ISaveable.h"
|
||||
#include <SDL.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
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(); }
|
||||
|
||||
|
||||
@ -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<int>(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<AnimTask::Type>(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
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
#include <Eigen/Core>
|
||||
#include <Eigen/Geometry>
|
||||
#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<void()> onComplete; // non-serializable
|
||||
std::function<void()> 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> 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
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#include "Item.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
|
||||
@ -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
|
||||
@ -3,6 +3,7 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include "ISaveable.h"
|
||||
|
||||
namespace ZL {
|
||||
|
||||
@ -19,11 +20,9 @@ namespace ZL {
|
||||
}
|
||||
};
|
||||
|
||||
class Inventory {
|
||||
private:
|
||||
std::vector<Item> items;
|
||||
|
||||
class Inventory : public ISaveable {
|
||||
public:
|
||||
std::vector<Item> items;
|
||||
void addItem(const Item& item);
|
||||
void removeItem(const std::string& itemId);
|
||||
const std::vector<Item>& 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<void(const std::string&)> onItemAdded;
|
||||
std::function<void(const std::string&)> onItemRemoved;
|
||||
|
||||
@ -220,4 +220,46 @@ std::vector<const QuestState*> 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<int>(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<QuestStatus>(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
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "quest/QuestTypes.h"
|
||||
#include "ISaveable.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@ -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<void(const std::string& questId)> onQuestUnlocked;
|
||||
std::function<void(const std::string& questId)> onQuestCompleted;
|
||||
|
||||
@ -12,6 +12,15 @@
|
||||
|
||||
namespace ZL {
|
||||
|
||||
struct GlyphAtlasData {
|
||||
std::unordered_map<uint32_t, GlyphInfo> glyphs;
|
||||
std::shared_ptr<Texture> atlasTexture;
|
||||
size_t atlasWidth = 0;
|
||||
size_t atlasHeight = 0;
|
||||
float lineHeight = 32.f;
|
||||
};
|
||||
static std::unordered_map<std::string, GlyphAtlasData> 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<char> 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;
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user