264 lines
8.6 KiB
C++
264 lines
8.6 KiB
C++
#include "quest/QuestJournal.h"
|
|
#include "external/nlohmann/json.hpp"
|
|
#include "utils/Utils.h"
|
|
#include "Localization.h"
|
|
#include <algorithm>
|
|
#include <iostream>
|
|
|
|
namespace ZL::Quest {
|
|
|
|
using json = nlohmann::json;
|
|
|
|
const char* toString(QuestStatus status) {
|
|
switch (status) {
|
|
case QuestStatus::Hidden: return "Hidden";
|
|
case QuestStatus::Available: return "Available";
|
|
case QuestStatus::Completed: return "Completed";
|
|
case QuestStatus::Failed: return "Failed";
|
|
default: return "Unknown";
|
|
}
|
|
}
|
|
|
|
static QuestStatus parseQuestStatus(const std::string& value) {
|
|
if (value == "Available") return QuestStatus::Available;
|
|
if (value == "Completed") return QuestStatus::Completed;
|
|
if (value == "Failed") return QuestStatus::Failed;
|
|
return QuestStatus::Hidden;
|
|
}
|
|
|
|
bool QuestJournal::loadFromFile(const std::string& path, const std::string& zipFile) {
|
|
const std::string content = ZL::readLocalizedConfigFile(path, zipFile);
|
|
if (content.empty()) {
|
|
std::cerr << "[quest] Failed to read " << path << std::endl;
|
|
return false;
|
|
}
|
|
|
|
json root;
|
|
try {
|
|
root = json::parse(content);
|
|
}
|
|
catch (const std::exception& e) {
|
|
std::cerr << "[quest] JSON parse error in " << path << ": " << e.what() << std::endl;
|
|
return false;
|
|
}
|
|
|
|
if (!root.contains("quests") || !root["quests"].is_array()) {
|
|
std::cerr << "[quest] Missing quests array in " << path << std::endl;
|
|
return false;
|
|
}
|
|
|
|
// Only clear existing definitions/state once the new file is confirmed
|
|
// valid, so a failed reload (e.g. a broken localized file) doesn't wipe
|
|
// the currently loaded quest journal.
|
|
quests.clear();
|
|
questOrder.clear();
|
|
|
|
for (const auto& item : root["quests"]) {
|
|
QuestDefinition def;
|
|
def.id = item.value("id", "");
|
|
def.title = item.value("title", def.id);
|
|
def.description = item.value("description", "");
|
|
def.initialStatus = parseQuestStatus(item.value("status", "Hidden"));
|
|
def.autoComplete = item.value("autoComplete", false);
|
|
|
|
if (item.contains("objectives") && item["objectives"].is_array()) {
|
|
for (const auto& obj : item["objectives"]) {
|
|
QuestObjective objective;
|
|
objective.id = obj.value("id", "");
|
|
objective.text = obj.value("text", "");
|
|
objective.completed = obj.value("completed", false);
|
|
objective.visible = obj.value("visible", true);
|
|
if (!objective.id.empty()) {
|
|
def.objectives.push_back(objective);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (def.id.empty()) {
|
|
continue;
|
|
}
|
|
|
|
QuestState state;
|
|
state.definition = def;
|
|
state.status = def.initialStatus;
|
|
state.activeObjectiveIndex = 0;
|
|
state.orderIndex = static_cast<int>(questOrder.size());
|
|
|
|
quests[def.id] = std::move(state);
|
|
questOrder.push_back(def.id);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool QuestJournal::reloadDefinitionsPreservingState(const std::string& path, const std::string& zipFile) {
|
|
json snapshot;
|
|
save(snapshot);
|
|
if (!loadFromFile(path, zipFile)) return false;
|
|
load(snapshot);
|
|
return true;
|
|
}
|
|
|
|
bool QuestJournal::setStatus(const std::string& questId, QuestStatus status) {
|
|
QuestState* quest = findQuest(questId);
|
|
if (!quest) return false;
|
|
quest->status = status;
|
|
return true;
|
|
}
|
|
|
|
bool QuestJournal::unlockQuest(const std::string& questId) {
|
|
QuestState* quest = findQuest(questId);
|
|
if (!quest) return false;
|
|
if (quest->status == QuestStatus::Hidden) {
|
|
quest->status = QuestStatus::Available;
|
|
if (onQuestUnlocked) onQuestUnlocked(questId);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool QuestJournal::completeQuest(const std::string& questId) {
|
|
bool changed = setStatus(questId, QuestStatus::Completed);
|
|
if (changed && onQuestCompleted) onQuestCompleted(questId);
|
|
return changed;
|
|
}
|
|
|
|
bool QuestJournal::failQuest(const std::string& questId) {
|
|
bool changed = setStatus(questId, QuestStatus::Failed);
|
|
if (changed && onQuestFailed) onQuestFailed(questId);
|
|
return changed;
|
|
}
|
|
|
|
bool QuestJournal::setObjectiveCompleted(const std::string& questId, const std::string& objectiveId, bool completed) {
|
|
QuestState* quest = findQuest(questId);
|
|
if (!quest) return false;
|
|
|
|
for (auto& objective : quest->definition.objectives) {
|
|
if (objective.id == objectiveId) {
|
|
objective.completed = completed;
|
|
|
|
if (completed) {
|
|
if (onObjectiveCompleted) onObjectiveCompleted(questId, objectiveId);
|
|
}
|
|
|
|
if (completed && quest->definition.autoComplete && quest->status == QuestStatus::Available) {
|
|
const bool allDone = std::all_of(
|
|
quest->definition.objectives.begin(),
|
|
quest->definition.objectives.end(),
|
|
[](const QuestObjective& o) { return o.completed; });
|
|
if (allDone) {
|
|
quest->status = QuestStatus::Completed;
|
|
if (onQuestCompleted) onQuestCompleted(questId);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool QuestJournal::setObjectiveVisible(const std::string& questId, const std::string& objectiveId, bool visible) {
|
|
QuestState* quest = findQuest(questId);
|
|
if (!quest) return false;
|
|
|
|
for (auto& objective : quest->definition.objectives) {
|
|
if (objective.id == objectiveId) {
|
|
objective.visible = visible;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool QuestJournal::setActiveObjective(const std::string& questId, int objectiveIndex) {
|
|
QuestState* quest = findQuest(questId);
|
|
if (!quest) return false;
|
|
|
|
if (objectiveIndex < 0 || objectiveIndex >= static_cast<int>(quest->definition.objectives.size())) {
|
|
return false;
|
|
}
|
|
|
|
quest->activeObjectiveIndex = objectiveIndex;
|
|
return true;
|
|
}
|
|
|
|
QuestState* QuestJournal::findQuest(const std::string& questId) {
|
|
auto it = quests.find(questId);
|
|
return (it != quests.end()) ? &it->second : nullptr;
|
|
}
|
|
|
|
const QuestState* QuestJournal::findQuest(const std::string& questId) const {
|
|
auto it = quests.find(questId);
|
|
return (it != quests.end()) ? &it->second : nullptr;
|
|
}
|
|
|
|
std::vector<QuestState*> QuestJournal::getVisibleQuests() {
|
|
std::vector<QuestState*> result;
|
|
for (const std::string& id : questOrder) {
|
|
QuestState* quest = findQuest(id);
|
|
if (!quest) continue;
|
|
if (quest->status != QuestStatus::Hidden) {
|
|
result.push_back(quest);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::vector<const QuestState*> QuestJournal::getVisibleQuests() const {
|
|
std::vector<const QuestState*> result;
|
|
for (const std::string& id : questOrder) {
|
|
const QuestState* quest = findQuest(id);
|
|
if (!quest) continue;
|
|
if (quest->status != QuestStatus::Hidden) {
|
|
result.push_back(quest);
|
|
}
|
|
}
|
|
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
|