#include "dialogue/DialogueRuntime.h" #include #include namespace ZL::Dialogue { static std::pair 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 cb) { onCutsceneStartNeeded = std::move(cb); } void DialogueRuntime::setOnDialogueLineStarted(std::function cb) { onDialogueLineStarted = std::move(cb); } void DialogueRuntime::setOnChatBubbleReady(std::function cb) { onChatBubbleReady = std::move(cb); } void DialogueRuntime::update(int deltaMs) { if (mode != Mode::PresentingLine) return; if (!presentation.revealCompleted) { revealCharacters += revealSpeedCharsPerSecond * (static_cast(deltaMs) / 1000.0f); const size_t fullLen = presentation.fullText.size(); const size_t visibleLen = static_cast(std::min(revealCharacters, static_cast(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(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(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(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(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* 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& 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& 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(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