#include "quest/QuestJournal.h" #include "external/nlohmann/json.hpp" #include "utils/Utils.h" #include "Localization.h" #include #include 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(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(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 QuestJournal::getVisibleQuests() { std::vector 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 QuestJournal::getVisibleQuests() const { std::vector 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(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