Working on quest system

This commit is contained in:
Vladislav Khorev committed 2026-06-03 14:37:09 +03:00
1 parent aaae368a07
commit e7ae148ead
22 files changed
+357 -103

No files matched your search

+3 -4
View File
@@ -15,7 +15,6 @@
#endif
#ifdef EMSCRIPTEN
#include <emscripten.h>
#endif
@@ -259,7 +258,7 @@ namespace ZL
uniInteriorParams.playerPosition = Eigen::Vector3f(0.942694, 0, -9.63104);
locations["uni_interior"] = std::make_shared<Location>(renderer, inventory);
locations["uni_interior"]->setup(uniInteriorParams);
locations["uni_interior"]->setup(uniInteriorParams, &menuManager.questJournal);
locations["uni_interior"]->scriptEngine.setGlobalStore(&globalInts);
locations["uni_interior"]->scriptEngine.setGlobalFloatStore(&globalFloats);
locations["uni_interior"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
@@ -286,7 +285,7 @@ namespace ZL
uniExteriorParams.dialoguesJsonPath = "resources/dialogue/uni_exterior_dialogues.json";
locations["uni_exterior"] = std::make_shared<Location>(renderer, inventory);
locations["uni_exterior"]->setup(uniExteriorParams);
locations["uni_exterior"]->setup(uniExteriorParams, &menuManager.questJournal);
locations["uni_exterior"]->scriptEngine.setGlobalStore(&globalInts);
locations["uni_exterior"]->scriptEngine.setGlobalFloatStore(&globalFloats);
locations["uni_exterior"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
@@ -346,7 +345,7 @@ namespace ZL
params_dorm.playerPosition = Eigen::Vector3f(6.76345, 0, -14.6022);
locations["location_dorm"] = std::make_shared<Location>(renderer, inventory);
locations["location_dorm"]->setup(params_dorm);
locations["location_dorm"]->setup(params_dorm, &menuManager.questJournal);
locations["location_dorm"]->scriptEngine.setGlobalStore(&globalInts);
locations["location_dorm"]->scriptEngine.setGlobalFloatStore(&globalFloats);
locations["location_dorm"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
+3 -1
View File
@@ -48,7 +48,7 @@ namespace ZL
{
}
void Location::setup(const LocationSetup& params)
void Location::setup(const LocationSetup& params, Quest::QuestJournal* journal)
{
// Load static game objects
@@ -130,6 +130,7 @@ namespace ZL
dialogueSystem.init(renderer, CONST_ZIP_FILE);
dialogueSystem.loadDatabase(params.dialoguesJsonPath);
dialogueSystem.setQuestJournal(journal);
npcNameText = std::make_unique<TextRenderer>();
if (!npcNameText->init(renderer, "resources/fonts/DroidSans.ttf", 24, CONST_ZIP_FILE)) {
@@ -138,6 +139,7 @@ namespace ZL
}
scriptEngine.init(this, &inventory, params.scriptPath);
scriptEngine.setQuestJournal(journal);
dialogueSystem.setOnCutsceneFinished([this](const std::string& cutsceneId) {
scriptEngine.callCutsceneCompleteCallback(cutsceneId);
+1 -1
View File
@@ -112,7 +112,7 @@ namespace ZL
int lastMouseY = 0;
void setup(const LocationSetup& params);
void setup(const LocationSetup& params, Quest::QuestJournal* journal);
void setupNavigation(const std::vector<std::string>& paths);
bool switchNavigation(int index);
InteractiveObject* raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir);
+13 -11
View File
@@ -8,11 +8,10 @@ namespace ZL {
static int questStatusPriority(Quest::QuestStatus status) {
switch (status) {
case Quest::QuestStatus::Active: return 0;
case Quest::QuestStatus::Available: return 1;
case Quest::QuestStatus::Completed: return 2;
case Quest::QuestStatus::Failed: return 3;
default: return 4;
case Quest::QuestStatus::Available: return 0;
case Quest::QuestStatus::Completed: return 1;
case Quest::QuestStatus::Failed: return 2;
default: return 3;
}
}
@@ -58,8 +57,7 @@ namespace ZL {
switch (status) {
case Quest::QuestStatus::Completed: return { 0.25f, 0.95f, 0.35f, 1.0f };
case Quest::QuestStatus::Failed: return { 1.0f, 0.25f, 0.25f, 1.0f };
case Quest::QuestStatus::Active: return { 1.0f, 1.0f, 1.0f, 1.0f };
case Quest::QuestStatus::Available: return { 0.86f, 0.86f, 0.86f, 1.0f };
case Quest::QuestStatus::Available: return { 1.0f, 1.0f, 1.0f, 1.0f };
default: return { 0.45f, 0.45f, 0.45f, 1.0f };
}
}
@@ -468,7 +466,7 @@ namespace ZL {
const int pa = questStatusPriority(a->status);
const int pb = questStatusPriority(b->status);
if (pa != pb) return pa < pb;
return a->orderIndex > b->orderIndex;
return a->orderIndex < b->orderIndex;
});
static const char* kItemNames[9] = {
@@ -540,10 +538,14 @@ namespace ZL {
static const char* kCheckboxes[3] = { "objective1checkbox", "objective2checkbox", "objective3checkbox" };
static const char* kObjNames[3] = { "objective1name", "objective2name", "objective3name" };
std::vector<const Quest::QuestObjective*> visibleObjs;
for (const auto& obj : def.objectives)
if (obj.visible) visibleObjs.push_back(&obj);
for (int i = 0; i < 3; ++i) {
if (i < static_cast<int>(def.objectives.size())) {
const auto& obj = def.objectives[i];
const bool isActive = (i == quest->activeObjectiveIndex);
if (i < static_cast<int>(visibleObjs.size())) {
const auto& obj = *visibleObjs[i];
const bool isActive = (&obj - def.objectives.data() == quest->activeObjectiveIndex);
auto img = uiManager.findStaticImage(kCheckboxes[i]);
if (img) img->texture = obj.completed ? texObjectiveCompleted_ : texObjectiveBlank_;
+67
View File
@@ -20,6 +20,7 @@ namespace ZL {
std::unordered_map<int, sol::protected_function> npcBumpsPlayerCallbacks;
std::unordered_map<std::string, int>* globalInts = nullptr;
std::unordered_map<std::string, float>* globalFloats = nullptr;
Quest::QuestJournal* questJournal = nullptr;
sol::protected_function locationEnterCallback;
sol::protected_function locationExitCallback;
sol::protected_function darklandsEnterCallback;
@@ -505,6 +506,68 @@ namespace ZL {
this_impl->npcBumpsPlayerCallbacks[index] = onNpcBumpsPlayer.as<sol::protected_function>();
});
// quest_unlock(quest_id)
api.set_function("quest_unlock",
[this_impl = impl.get()](const std::string& questId) -> bool {
if (!this_impl->questJournal) {
std::cerr << "[script] quest_unlock: QuestJournal not set\n";
return false;
}
return this_impl->questJournal->unlockQuest(questId);
});
// quest_complete(quest_id)
api.set_function("quest_complete",
[this_impl = impl.get()](const std::string& questId) -> bool {
if (!this_impl->questJournal) {
std::cerr << "[script] quest_complete: QuestJournal not set\n";
return false;
}
return this_impl->questJournal->completeQuest(questId);
});
// quest_fail(quest_id)
api.set_function("quest_fail",
[this_impl = impl.get()](const std::string& questId) -> bool {
if (!this_impl->questJournal) {
std::cerr << "[script] quest_fail: QuestJournal not set\n";
return false;
}
return this_impl->questJournal->failQuest(questId);
});
// quest_set_objective_completed(quest_id, objective_id [, completed])
api.set_function("quest_set_objective_completed",
[this_impl = impl.get()](const std::string& questId, const std::string& objId, sol::object completed) -> bool {
if (!this_impl->questJournal) {
std::cerr << "[script] quest_set_objective_completed: QuestJournal not set\n";
return false;
}
const bool val = completed.is<bool>() ? completed.as<bool>() : true;
return this_impl->questJournal->setObjectiveCompleted(questId, objId, val);
});
// quest_set_objective_visible(quest_id, objective_id [, visible])
api.set_function("quest_set_objective_visible",
[this_impl = impl.get()](const std::string& questId, const std::string& objId, sol::object visible) -> bool {
if (!this_impl->questJournal) {
std::cerr << "[script] quest_set_objective_visible: QuestJournal not set\n";
return false;
}
const bool val = visible.is<bool>() ? visible.as<bool>() : true;
return this_impl->questJournal->setObjectiveVisible(questId, objId, val);
});
// quest_set_active_objective(quest_id, objective_index)
api.set_function("quest_set_active_objective",
[this_impl = impl.get()](const std::string& questId, int index) -> bool {
if (!this_impl->questJournal) {
std::cerr << "[script] quest_set_active_objective: QuestJournal not set\n";
return false;
}
return this_impl->questJournal->setActiveObjective(questId, index);
});
lua.script_file(scriptPath);
}
@@ -624,6 +687,10 @@ namespace ZL {
if (impl) impl->globalFloats = store;
}
void ScriptEngine::setQuestJournal(Quest::QuestJournal* journal) {
if (impl) impl->questJournal = journal;
}
void ScriptEngine::callLocationEnterCallback() {
if (!impl || !impl->locationEnterCallback.valid()) return;
auto result = impl->locationEnterCallback();
+2
View File
@@ -2,6 +2,7 @@
#include <string>
#include <memory>
#include <unordered_map>
#include "quest/QuestJournal.h"
namespace ZL {
@@ -35,6 +36,7 @@ public:
void setGlobalStore(std::unordered_map<std::string, int>* store);
void setGlobalFloatStore(std::unordered_map<std::string, float>* store);
void setQuestJournal(Quest::QuestJournal* journal);
void callLocationEnterCallback();
void callLocationExitCallback();
+10
View File
@@ -106,6 +106,11 @@ Node DialogueDatabase::parseNode(const json& j) {
node.cutsceneId = j.value("cutsceneId", "");
node.luaCallback = j.value("luaCallback", "");
node.bubbleSlot = j.value("bubbleSlot", "");
node.questUnlock = j.value("questUnlock", "");
node.questComplete = j.value("questComplete", "");
node.questFail = j.value("questFail", "");
node.objectiveComplete = j.value("objectiveComplete", "");
node.objectiveVisible = j.value("objectiveVisible", "");
if (j.contains("conditions") && j["conditions"].is_array()) {
for (const auto& item : j["conditions"]) {
@@ -157,6 +162,11 @@ CutsceneLine DialogueDatabase::parseCutsceneLine(const json& j) {
line.luaCallback = j.value("luaCallback", "");
line.durationMs = j.value("durationMs", 0);
line.waitForConfirm = j.value("waitForConfirm", false);
line.questUnlock = j.value("questUnlock", "");
line.questComplete = j.value("questComplete", "");
line.questFail = j.value("questFail", "");
line.objectiveComplete = j.value("objectiveComplete", "");
line.objectiveVisible = j.value("objectiveVisible", "");
return line;
}
+43 -8
View File
@@ -5,7 +5,13 @@
#include <iostream>
namespace ZL::Dialogue {
static std::pair<std::string, std::string> splitDot(const std::string& s) {
const auto dot = s.find('.');
if (dot == std::string::npos) return {s, ""};
return {s.substr(0, dot), s.substr(dot + 1)};
}
void DialogueRuntime::setDatabase(const DialogueDatabase* value) {
database = value;
}
@@ -309,6 +315,29 @@ int DialogueRuntime::getFlag(const std::string& name) const {
return (it != flags.end()) ? it->second : 0;
}
void DialogueRuntime::setQuestJournal(Quest::QuestJournal* journal) {
questJournal = journal;
}
void DialogueRuntime::applyQuestActions(
const std::string& questUnlock, const std::string& questComplete,
const std::string& questFail, const std::string& objectiveComplete,
const std::string& objectiveVisible)
{
if (!questJournal) return;
if (!questUnlock.empty()) questJournal->unlockQuest(questUnlock);
if (!questComplete.empty()) questJournal->completeQuest(questComplete);
if (!questFail.empty()) questJournal->failQuest(questFail);
if (!objectiveComplete.empty()) {
auto [qId, oId] = splitDot(objectiveComplete);
questJournal->setObjectiveCompleted(qId, oId);
}
if (!objectiveVisible.empty()) {
auto [qId, oId] = splitDot(objectiveVisible);
questJournal->setObjectiveVisible(qId, oId);
}
}
bool DialogueRuntime::evaluateConditions(const std::vector<Condition>& conditions) const {
for (const Condition& condition : conditions) {
const int currentValue = getFlag(condition.flag);
@@ -432,6 +461,8 @@ void DialogueRuntime::presentLine(const Node& node) {
revealCharacters = static_cast<float>(node.text.size());
}
applyQuestActions(node.questUnlock, node.questComplete, node.questFail,
node.objectiveComplete, node.objectiveVisible);
if (!node.luaCallback.empty() && onDialogueLineStarted) {
onDialogueLineStarted(node.luaCallback);
}
@@ -530,9 +561,12 @@ void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::st
cutsceneTotalDurationMs = cutsceneContentDurationMs + activeCutscene->endFadeOutMs + activeCutscene->endFadeInMs;
refreshCutscenePresentation();
if (!activeCutscene->lines.empty() && onCutsceneLineStarted) {
const std::string& cb = activeCutscene->lines[0].luaCallback;
if (!cb.empty()) onCutsceneLineStarted(cb);
if (!activeCutscene->lines.empty()) {
const CutsceneLine& firstLine = activeCutscene->lines[0];
applyQuestActions(firstLine.questUnlock, firstLine.questComplete,
firstLine.questFail, firstLine.objectiveComplete, firstLine.objectiveVisible);
if (onCutsceneLineStarted && !firstLine.luaCallback.empty())
onCutsceneLineStarted(firstLine.luaCallback);
}
std::cout << "[CUTSCENE] start id=" << cutsceneId
@@ -621,10 +655,11 @@ void DialogueRuntime::advanceCutsceneLine() {
return;
}
if (onCutsceneLineStarted) {
const std::string& cb = activeCutscene->lines[currentCutsceneLine].luaCallback;
if (!cb.empty()) onCutsceneLineStarted(cb);
}
const CutsceneLine& newLine = activeCutscene->lines[currentCutsceneLine];
applyQuestActions(newLine.questUnlock, newLine.questComplete,
newLine.questFail, newLine.objectiveComplete, newLine.objectiveVisible);
if (onCutsceneLineStarted && !newLine.luaCallback.empty())
onCutsceneLineStarted(newLine.luaCallback);
refreshCutscenePresentation();
}
+7
View File
@@ -1,6 +1,7 @@
#pragma once
#include "dialogue/DialogueDatabase.h"
#include "quest/QuestJournal.h"
#include "external/nlohmann/json.hpp"
#include <functional>
#include <string>
@@ -42,6 +43,8 @@ public:
void setFlag(const std::string& name, int value);
int getFlag(const std::string& name) const;
void setQuestJournal(Quest::QuestJournal* journal);
json buildSaveState() const;
bool restoreSaveState(const json& state);
@@ -62,6 +65,7 @@ private:
bool fadeInCallbackFired = false;
const DialogueDatabase* database = nullptr;
Quest::QuestJournal* questJournal = nullptr;
const DialogueDefinition* activeDialogue = nullptr;
const StaticCutsceneDefinition* activeCutscene = nullptr;
@@ -89,6 +93,9 @@ private:
bool evaluateConditions(const std::vector<Condition>& conditions) const;
void applyEffects(const std::vector<Effect>& effects);
void applyQuestActions(const std::string& questUnlock, const std::string& questComplete,
const std::string& questFail, const std::string& objectiveComplete,
const std::string& objectiveVisible);
bool enterNode(const std::string& nodeId);
void presentLine(const Node& node);
+3
View File
@@ -2,6 +2,7 @@
#include "dialogue/DialogueOverlay.h"
#include "dialogue/DialogueRuntime.h"
#include "quest/QuestJournal.h"
#include <SDL.h>
#include <functional>
#include <string>
@@ -37,6 +38,8 @@ public:
void setFlag(const std::string& name, int value) { runtime.setFlag(name, value); }
int getFlag(const std::string& name) const { return runtime.getFlag(name); }
void setQuestJournal(Quest::QuestJournal* journal) { runtime.setQuestJournal(journal); }
private:
DialogueDatabase database;
DialogueRuntime runtime;
+14
View File
@@ -99,6 +99,13 @@ struct Node {
// Name of the UI node (StaticImage) to reveal in the phone chat when this line is shown
std::string bubbleSlot;
// Quest actions fired when this line is presented (empty = no action)
std::string questUnlock;
std::string questComplete;
std::string questFail;
std::string objectiveComplete; // "quest_id.objective_id"
std::string objectiveVisible; // "quest_id.objective_id"
};
struct DialogueDefinition {
@@ -120,6 +127,13 @@ struct CutsceneLine {
int backgroundHeight = 0; // 0 = inherit from cutscene
int durationMs = 0;
bool waitForConfirm = false;
// Quest actions fired when this line is shown (empty = no action)
std::string questUnlock;
std::string questComplete;
std::string questFail;
std::string objectiveComplete; // "quest_id.objective_id"
std::string objectiveVisible; // "quest_id.objective_id"
};
struct CutsceneCameraPose {
+26 -25
View File
@@ -12,38 +12,19 @@ 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();
@@ -87,9 +68,8 @@ bool QuestJournal::loadFromFile(const std::string& path, const std::string& zipF
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);
def.autoComplete = item.value("autoComplete", false);
if (item.contains("objectives") && item["objectives"].is_array()) {
for (const auto& obj : item["objectives"]) {
@@ -97,6 +77,7 @@ bool QuestJournal::loadFromFile(const std::string& path, const std::string& zipF
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);
}
@@ -136,10 +117,6 @@ bool QuestJournal::unlockQuest(const std::string& questId) {
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);
}
@@ -155,6 +132,30 @@ bool QuestJournal::setObjectiveCompleted(const std::string& questId, const std::
for (auto& objective : quest->definition.objectives) {
if (objective.id == objectiveId) {
objective.completed = completed;
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;
}
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;
}
}
+1 -1
View File
@@ -12,11 +12,11 @@ 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 setObjectiveVisible(const std::string& questId, const std::string& objectiveId, bool visible = true);
bool setActiveObjective(const std::string& questId, int objectiveIndex);
QuestState* findQuest(const std::string& questId);
+2 -11
View File
@@ -8,31 +8,23 @@ 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;
bool visible = true;
};
struct QuestDefinition {
std::string id;
std::string title;
std::string description;
QuestCategory category = QuestCategory::Side;
QuestStatus initialStatus = QuestStatus::Hidden;
int recommendedLevel = 0;
bool autoComplete = false;
std::vector<QuestObjective> objectives;
};
@@ -44,6 +36,5 @@ struct QuestState {
};
const char* toString(QuestStatus status);
const char* toString(QuestCategory category);
} // namespace ZL::Quest