368 lines
10 KiB
C++
368 lines
10 KiB
C++
#include "dialogue/DialogueRuntime.h"
|
|
|
|
#include <algorithm>
|
|
#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;
|
|
}
|
|
|
|
bool DialogueRuntime::startDialogue(const std::string& dialogueId) {
|
|
if (!database) {
|
|
std::cerr << "[dialogue] No database assigned to runtime\n";
|
|
return false;
|
|
}
|
|
|
|
if (mode != Mode::Inactive && activeDialogue && activeDialogue->uninterruptible) {
|
|
std::cerr << "[dialogue] Blocked: '" << activeDialogue->id << "' is uninterruptible\n";
|
|
return false;
|
|
}
|
|
|
|
const DialogueDefinition* dialogue = database->findDialogue(dialogueId);
|
|
if (!dialogue) {
|
|
std::cerr << "[dialogue] Dialogue not found: " << dialogueId << "\n";
|
|
return false;
|
|
}
|
|
|
|
activeDialogue = dialogue;
|
|
currentNodeId.clear();
|
|
visibleChoices.clear();
|
|
selectedChoice = -1;
|
|
revealCharacters = 0.0f;
|
|
presentation = {};
|
|
presentation.dialogueId = dialogue->id;
|
|
|
|
return enterNode(dialogue->startNode);
|
|
}
|
|
|
|
void DialogueRuntime::stop() {
|
|
activeDialogue = nullptr;
|
|
currentNodeId.clear();
|
|
visibleChoices.clear();
|
|
selectedChoice = -1;
|
|
revealCharacters = 0.0f;
|
|
mode = Mode::Inactive;
|
|
presentation = {};
|
|
}
|
|
|
|
void DialogueRuntime::resumeFromNode(const std::string& nodeId) {
|
|
if (mode != Mode::WaitingForCutscene) return;
|
|
if (nodeId.empty()) {
|
|
stop();
|
|
return;
|
|
}
|
|
enterNode(nodeId);
|
|
}
|
|
|
|
void DialogueRuntime::setOnCutsceneStartNeeded(
|
|
std::function<void(const std::string&, const std::string&)> cb)
|
|
{
|
|
onCutsceneStartNeeded = std::move(cb);
|
|
}
|
|
|
|
void DialogueRuntime::setOnDialogueLineStarted(std::function<void(const std::string&)> cb) {
|
|
onDialogueLineStarted = std::move(cb);
|
|
}
|
|
|
|
void DialogueRuntime::setOnChatBubbleReady(std::function<void(const std::string&, bool)> cb) {
|
|
onChatBubbleReady = std::move(cb);
|
|
}
|
|
|
|
void DialogueRuntime::update(int deltaMs) {
|
|
if (mode != Mode::PresentingLine) return;
|
|
|
|
if (!presentation.revealCompleted) {
|
|
revealCharacters += revealSpeedCharsPerSecond * (static_cast<float>(deltaMs) / 1000.0f);
|
|
const size_t fullLen = presentation.fullText.size();
|
|
const size_t visibleLen = static_cast<size_t>(std::min<float>(revealCharacters, static_cast<float>(fullLen)));
|
|
presentation.visibleText = presentation.fullText.substr(0, visibleLen);
|
|
presentation.revealCompleted = (visibleLen >= fullLen);
|
|
}
|
|
}
|
|
|
|
void DialogueRuntime::confirmAdvance() {
|
|
if (mode == Mode::PresentingLine) {
|
|
if (!presentation.revealCompleted) {
|
|
presentation.visibleText = presentation.fullText;
|
|
presentation.revealCompleted = true;
|
|
revealCharacters = static_cast<float>(presentation.fullText.size());
|
|
return;
|
|
}
|
|
|
|
if (!activeDialogue) {
|
|
stop();
|
|
return;
|
|
}
|
|
|
|
auto it = activeDialogue->nodes.find(currentNodeId);
|
|
if (it == activeDialogue->nodes.end()) {
|
|
stop();
|
|
return;
|
|
}
|
|
|
|
if (!it->second.next.empty()) {
|
|
enterNode(it->second.next);
|
|
}
|
|
else {
|
|
stop();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (mode == Mode::WaitingForChoice) {
|
|
if (visibleChoices.empty() || selectedChoice < 0 || selectedChoice >= static_cast<int>(visibleChoices.size())) {
|
|
return;
|
|
}
|
|
|
|
const Choice& choice = visibleChoices[selectedChoice];
|
|
if (choice.consumeOnce && !choice.id.empty()) {
|
|
consumedChoices.insert(choice.id);
|
|
}
|
|
applyEffects(choice.effects);
|
|
enterNode(choice.next);
|
|
return;
|
|
}
|
|
}
|
|
|
|
void DialogueRuntime::moveSelection(int delta) {
|
|
if (mode != Mode::WaitingForChoice || visibleChoices.empty()) return;
|
|
|
|
const int count = static_cast<int>(visibleChoices.size());
|
|
if (selectedChoice < 0 || selectedChoice >= count) {
|
|
selectedChoice = (delta >= 0) ? 0 : (count - 1);
|
|
}
|
|
else {
|
|
selectedChoice = (selectedChoice + delta) % count;
|
|
if (selectedChoice < 0) selectedChoice += count;
|
|
}
|
|
presentation.selectedChoice = selectedChoice;
|
|
}
|
|
|
|
void DialogueRuntime::selectChoice(int index) {
|
|
if (mode != Mode::WaitingForChoice || visibleChoices.empty()) return;
|
|
if (index < 0 || index >= static_cast<int>(visibleChoices.size())) return;
|
|
selectedChoice = index;
|
|
presentation.selectedChoice = selectedChoice;
|
|
}
|
|
|
|
void DialogueRuntime::setFlag(const std::string& name, int value) {
|
|
if (flagStore) (*flagStore)[name] = value;
|
|
}
|
|
|
|
int DialogueRuntime::getFlag(const std::string& name) const {
|
|
if (!flagStore) return 0;
|
|
auto it = flagStore->find(name);
|
|
return (it != flagStore->end()) ? it->second : 0;
|
|
}
|
|
|
|
void DialogueRuntime::setGlobalFlagStore(std::unordered_map<std::string, int>* store) {
|
|
flagStore = store;
|
|
}
|
|
|
|
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);
|
|
switch (condition.op) {
|
|
case ComparisonOp::Exists:
|
|
if (currentValue == 0) return false;
|
|
break;
|
|
case ComparisonOp::Equals:
|
|
if (currentValue != condition.value) return false;
|
|
break;
|
|
case ComparisonOp::NotEquals:
|
|
if (currentValue == condition.value) return false;
|
|
break;
|
|
case ComparisonOp::GreaterOrEqual:
|
|
if (currentValue < condition.value) return false;
|
|
break;
|
|
case ComparisonOp::LessOrEqual:
|
|
if (currentValue > condition.value) return false;
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void DialogueRuntime::applyEffects(const std::vector<Effect>& effects) {
|
|
for (const Effect& effect : effects) {
|
|
if (effect.flag.empty()) continue;
|
|
if (effect.relative) {
|
|
setFlag(effect.flag, getFlag(effect.flag) + effect.value);
|
|
}
|
|
else {
|
|
setFlag(effect.flag, effect.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool DialogueRuntime::enterNode(const std::string& nodeId) {
|
|
if (!activeDialogue) {
|
|
stop();
|
|
return false;
|
|
}
|
|
|
|
auto it = activeDialogue->nodes.find(nodeId);
|
|
if (it == activeDialogue->nodes.end()) {
|
|
std::cerr << "[dialogue] Node not found: " << nodeId << " in dialogue " << activeDialogue->id << "\n";
|
|
stop();
|
|
return false;
|
|
}
|
|
|
|
const Node& node = it->second;
|
|
currentNodeId = node.id;
|
|
presentation.dialogueId = activeDialogue->id;
|
|
|
|
switch (node.type) {
|
|
case NodeType::Line:
|
|
presentLine(node);
|
|
return true;
|
|
|
|
case NodeType::Choice:
|
|
presentChoices(node);
|
|
return true;
|
|
|
|
case NodeType::Condition:
|
|
if (evaluateConditions(node.conditions)) {
|
|
return enterNode(!node.trueNext.empty() ? node.trueNext : node.next);
|
|
}
|
|
return enterNode(node.falseNext);
|
|
|
|
case NodeType::SetFlag:
|
|
applyEffects(node.effects);
|
|
if (!node.next.empty()) {
|
|
return enterNode(node.next);
|
|
}
|
|
stop();
|
|
return true;
|
|
|
|
case NodeType::Jump:
|
|
if (!node.next.empty()) {
|
|
return enterNode(node.next);
|
|
}
|
|
stop();
|
|
return true;
|
|
|
|
case NodeType::End:
|
|
if (!node.luaCallback.empty() && onDialogueLineStarted)
|
|
onDialogueLineStarted(node.luaCallback);
|
|
stop();
|
|
return true;
|
|
|
|
case NodeType::CutsceneStart:
|
|
// Pause dialogue and hand off to the cutscene system via DialogueSystem.
|
|
mode = Mode::WaitingForCutscene;
|
|
if (onCutsceneStartNeeded)
|
|
onCutsceneStartNeeded(node.cutsceneId, node.next);
|
|
return true;
|
|
}
|
|
|
|
stop();
|
|
return false;
|
|
}
|
|
|
|
void DialogueRuntime::presentLine(const Node& node) {
|
|
mode = Mode::PresentingLine;
|
|
revealCharacters = 0.0f;
|
|
presentation.mode = PresentationMode::Dialogue;
|
|
presentation.speaker = node.speaker;
|
|
presentation.fullText = node.text;
|
|
presentation.visibleText.clear();
|
|
presentation.portraitPath = node.portrait;
|
|
presentation.backgroundPath.clear();
|
|
presentation.choices.clear();
|
|
presentation.selectedChoice = -1;
|
|
presentation.revealCompleted = node.text.empty();
|
|
presentation.showCutsceneSubtitle = false;
|
|
presentation.cutsceneSkippable = false;
|
|
presentation.cutsceneCamera = {};
|
|
presentation.cutsceneImages.clear();
|
|
presentation.cutsceneGlobalFadeAlpha = 1.0f;
|
|
presentation.cutsceneBlackAlpha = 0.0f;
|
|
|
|
if (presentation.revealCompleted) {
|
|
presentation.visibleText = node.text;
|
|
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);
|
|
}
|
|
if (!node.chatBubble.empty() && onChatBubbleReady) {
|
|
onChatBubbleReady(node.text, node.chatBubble == "in");
|
|
}
|
|
}
|
|
|
|
void DialogueRuntime::presentChoices(const Node& node) {
|
|
visibleChoices.clear();
|
|
presentation.choices.clear();
|
|
|
|
for (const Choice& choice : node.choices) {
|
|
if (!choice.id.empty() && consumedChoices.count(choice.id) > 0) continue;
|
|
if (!evaluateConditions(choice.conditions)) continue;
|
|
visibleChoices.push_back(choice);
|
|
presentation.choices.push_back({ choice.id, choice.text, choice.kind });
|
|
}
|
|
|
|
if (visibleChoices.empty()) {
|
|
if (!node.next.empty()) {
|
|
enterNode(node.next);
|
|
}
|
|
else {
|
|
stop();
|
|
}
|
|
return;
|
|
}
|
|
|
|
mode = Mode::WaitingForChoice;
|
|
selectedChoice = -1;
|
|
presentation.mode = PresentationMode::Choice;
|
|
presentation.speaker = node.speaker;
|
|
presentation.fullText = node.text;
|
|
presentation.visibleText = node.text;
|
|
presentation.portraitPath = node.portrait;
|
|
presentation.backgroundPath.clear();
|
|
presentation.selectedChoice = -1;
|
|
presentation.revealCompleted = true;
|
|
presentation.showCutsceneSubtitle = false;
|
|
presentation.cutsceneSkippable = false;
|
|
presentation.cutsceneCamera = {};
|
|
presentation.cutsceneImages.clear();
|
|
presentation.cutsceneGlobalFadeAlpha = 1.0f;
|
|
presentation.cutsceneBlackAlpha = 0.0f;
|
|
}
|
|
|
|
} // namespace ZL::Dialogue
|