add quest journal (QuestJournal + split UI view)

This commit is contained in:
vottozi committed 2026-04-30 03:20:52 +06:00
1 parent dbf0af2cb2
commit d78286a9c2
13 files changed
+1148 -32

No files matched your search

+211
View File
@@ -0,0 +1,211 @@
#include "quest/QuestJournal.h"
#include "external/nlohmann/json.hpp"
#include "utils/Utils.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::Active: return "Active";
case QuestStatus::Completed: return "Completed";
case QuestStatus::Failed: return "Failed";
default: return "Unknown";
}
}
const char* toString(QuestCategory category) {
switch (category) {
case QuestCategory::Main: return "Main";
case QuestCategory::Side: return "Side";
case QuestCategory::Contract: return "Contract";
case QuestCategory::Tutorial: return "Tutorial";
default: return "Unknown";
}
}
static QuestStatus parseQuestStatus(const std::string& value) {
if (value == "Available") return QuestStatus::Available;
if (value == "Active") return QuestStatus::Active;
if (value == "Completed") return QuestStatus::Completed;
if (value == "Failed") return QuestStatus::Failed;
return QuestStatus::Hidden;
}
static QuestCategory parseQuestCategory(const std::string& value) {
if (value == "Main") return QuestCategory::Main;
if (value == "Contract") return QuestCategory::Contract;
if (value == "Tutorial") return QuestCategory::Tutorial;
return QuestCategory::Side;
}
bool QuestJournal::loadFromFile(const std::string& path, const std::string& zipFile) {
quests.clear();
questOrder.clear();
std::string content;
try {
if (zipFile.empty()) {
content = ZL::readTextFile(path);
}
else {
auto data = ZL::readFileFromZIP(path, zipFile);
content.assign(data.begin(), data.end());
}
}
catch (const std::exception& e) {
std::cerr << "[quest] Failed to read " << path << ": " << e.what() << std::endl;
return false;
}
if (content.empty()) {
std::cerr << "[quest] Empty quest file: " << 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;
}
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.category = parseQuestCategory(item.value("category", "Side"));
def.initialStatus = parseQuestStatus(item.value("status", "Hidden"));
def.recommendedLevel = item.value("recommendedLevel", 0);
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);
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::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;
}
return true;
}
bool QuestJournal::startQuest(const std::string& questId) {
return setStatus(questId, QuestStatus::Active);
}
bool QuestJournal::completeQuest(const std::string& questId) {
return setStatus(questId, QuestStatus::Completed);
}
bool QuestJournal::failQuest(const std::string& questId) {
return setStatus(questId, QuestStatus::Failed);
}
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;
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;
}
} // namespace ZL::Quest
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include "quest/QuestTypes.h"
#include <string>
#include <unordered_map>
#include <vector>
namespace ZL::Quest {
class QuestJournal {
public:
bool loadFromFile(const std::string& path, const std::string& zipFile = "");
bool unlockQuest(const std::string& questId);
bool startQuest(const std::string& questId);
bool completeQuest(const std::string& questId);
bool failQuest(const std::string& questId);
bool setObjectiveCompleted(const std::string& questId, const std::string& objectiveId, bool completed = true);
bool setActiveObjective(const std::string& questId, int objectiveIndex);
QuestState* findQuest(const std::string& questId);
const QuestState* findQuest(const std::string& questId) const;
std::vector<QuestState*> getVisibleQuests();
std::vector<const QuestState*> getVisibleQuests() const;
private:
std::unordered_map<std::string, QuestState> quests;
std::vector<std::string> questOrder;
bool setStatus(const std::string& questId, QuestStatus status);
};
} // namespace ZL::Quest
+49
View File
@@ -0,0 +1,49 @@
#pragma once
#include <string>
#include <vector>
namespace ZL::Quest {
enum class QuestStatus {
Hidden,
Available,
Active,
Completed,
Failed
};
enum class QuestCategory {
Main,
Side,
Contract,
Tutorial
};
struct QuestObjective {
std::string id;
std::string text;
bool completed = false;
};
struct QuestDefinition {
std::string id;
std::string title;
std::string description;
QuestCategory category = QuestCategory::Side;
QuestStatus initialStatus = QuestStatus::Hidden;
int recommendedLevel = 0;
std::vector<QuestObjective> objectives;
};
struct QuestState {
QuestDefinition definition;
QuestStatus status = QuestStatus::Hidden;
int activeObjectiveIndex = 0;
int orderIndex = 0; // acquisition/load order; larger value means newer quest
};
const char* toString(QuestStatus status);
const char* toString(QuestCategory category);
} // namespace ZL::Quest