#include "dialogue/DialogueRuntime.h" #include #include #include namespace ZL::Dialogue { 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; } const DialogueDefinition* dialogue = database->findDialogue(dialogueId); if (!dialogue) { std::cerr << "[dialogue] Dialogue not found: " << dialogueId << "\n"; return false; } activeDialogue = dialogue; activeCutscene = nullptr; currentNodeId.clear(); pendingNodeAfterCutscene.clear(); visibleChoices.clear(); selectedChoice = 0; revealCharacters = 0.0f; currentCutsceneLine = -1; cutsceneTimerMs = 0; cutsceneElapsedMs = 0; cutsceneTotalDurationMs = 0; presentation = {}; presentation.dialogueId = dialogue->id; return enterNode(dialogue->startNode); } void DialogueRuntime::stop() { activeDialogue = nullptr; activeCutscene = nullptr; currentNodeId.clear(); pendingNodeAfterCutscene.clear(); visibleChoices.clear(); selectedChoice = 0; revealCharacters = 0.0f; currentCutsceneLine = -1; cutsceneTimerMs = 0; cutsceneElapsedMs = 0; cutsceneTotalDurationMs = 0; mode = Mode::Inactive; presentation = {}; } void DialogueRuntime::update(int deltaMs) { if (mode == Mode::PresentingLine) { 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); } return; } if (mode == Mode::PlayingCutscene && activeCutscene) { cutsceneElapsedMs += deltaMs; if (!activeCutscene->lines.empty() && currentCutsceneLine >= 0 && currentCutsceneLine < static_cast(activeCutscene->lines.size())) { const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine]; if (!line.waitForConfirm) { cutsceneTimerMs += deltaMs; const int durationMs = (line.durationMs > 0) ? line.durationMs : computeFallbackCutsceneDurationMs(line.text); if (cutsceneTimerMs >= durationMs) { advanceCutsceneLine(); // ВАЖНО: после advance катсцена могла завершиться if (!activeCutscene || mode != Mode::PlayingCutscene) { return; } } } } if (!activeCutscene || mode != Mode::PlayingCutscene) { return; } refreshCutscenePresentation(); if (!activeCutscene || mode != Mode::PlayingCutscene) { return; } const bool subtitlesFinished = activeCutscene->lines.empty() || currentCutsceneLine >= static_cast(activeCutscene->lines.size()); const bool durationFinished = cutsceneTotalDurationMs > 0 && cutsceneElapsedMs >= cutsceneTotalDurationMs; if (activeCutscene->lines.empty()) { if (durationFinished) { finishCutscene(); } return; } if (subtitlesFinished && (cutsceneTotalDurationMs <= 0 || durationFinished)) { finishCutscene(); return; } } } 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()) { return; } const Choice& choice = visibleChoices[std::clamp(selectedChoice, 0, static_cast(visibleChoices.size()) - 1)]; if (choice.consumeOnce && !choice.id.empty()) { consumedChoices.insert(choice.id); } applyEffects(choice.effects); enterNode(choice.next); return; } if (mode == Mode::PlayingCutscene) { if (!activeCutscene || activeCutscene->lines.empty()) { return; } if (currentCutsceneLine >= 0 && currentCutsceneLine < static_cast(activeCutscene->lines.size())) { advanceCutsceneLine(); } } } void DialogueRuntime::moveSelection(int delta) { if (mode != Mode::WaitingForChoice || visibleChoices.empty()) { return; } const int count = static_cast(visibleChoices.size()); selectedChoice = (selectedChoice + delta) % count; if (selectedChoice < 0) { selectedChoice += count; } presentation.selectedChoice = selectedChoice; } void DialogueRuntime::setFlag(const std::string& name, int value) { flags[name] = value; } int DialogueRuntime::getFlag(const std::string& name) const { auto it = flags.find(name); return (it != flags.end()) ? it->second : 0; } 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) { flags[effect.flag] += effect.value; } else { flags[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: stop(); return true; case NodeType::CutsceneStart: startCutscene(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 = 0; presentation.revealCompleted = node.text.empty(); presentation.showCutsceneSubtitle = false; presentation.cutsceneCamera = {}; if (presentation.revealCompleted) { presentation.visibleText = node.text; revealCharacters = static_cast(node.text.size()); } } 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 = 0; 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 = 0; presentation.revealCompleted = true; presentation.showCutsceneSubtitle = false; presentation.cutsceneCamera = {}; } void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::string& nextNodeAfterCutscene) { if (!database) { stop(); return; } const StaticCutsceneDefinition* cutscene = database->findCutscene(cutsceneId); if (!cutscene) { std::cerr << "[dialogue] Cutscene not found: " << cutsceneId << "\n"; if (!nextNodeAfterCutscene.empty()) { enterNode(nextNodeAfterCutscene); } else { stop(); } return; } activeCutscene = cutscene; pendingNodeAfterCutscene = nextNodeAfterCutscene; mode = Mode::PlayingCutscene; cutsceneElapsedMs = 0; cutsceneTimerMs = 0; currentCutsceneLine = activeCutscene->lines.empty() ? -1 : 0; cutsceneTotalDurationMs = std::max(activeCutscene->durationMs, computeCameraTrackDurationMs(*activeCutscene)); if (cutsceneTotalDurationMs <= 0 && activeCutscene->lines.empty()) { cutsceneTotalDurationMs = 3000; } refreshCutscenePresentation(); std::cout << "[CUTSCENE] start id=" << cutsceneId << " lines=" << activeCutscene->lines.size() << " totalDuration=" << cutsceneTotalDurationMs << std::endl; } void DialogueRuntime::finishCutscene() { std::cout << "[CUTSCENE] finish nextNode=" << pendingNodeAfterCutscene << std::endl; activeCutscene = nullptr; currentCutsceneLine = -1; cutsceneTimerMs = 0; cutsceneElapsedMs = 0; cutsceneTotalDurationMs = 0; if (!pendingNodeAfterCutscene.empty()) { const std::string nextNode = pendingNodeAfterCutscene; pendingNodeAfterCutscene.clear(); enterNode(nextNode); } else { stop(); } } void DialogueRuntime::advanceCutsceneLine() { if (!activeCutscene) { stop(); return; } if (activeCutscene->lines.empty()) { return; } std::cout << "[CUTSCENE] advance before current=" << currentCutsceneLine << std::endl; ++currentCutsceneLine; std::cout << "[CUTSCENE] advance after current=" << currentCutsceneLine << std::endl; cutsceneTimerMs = 0; if (currentCutsceneLine >= static_cast(activeCutscene->lines.size())) { refreshCutscenePresentation(); if (cutsceneTotalDurationMs <= 0 || cutsceneElapsedMs >= cutsceneTotalDurationMs) { finishCutscene(); } return; } refreshCutscenePresentation(); } CutsceneCameraPose DialogueRuntime::evaluateCutsceneCameraPose() const { CutsceneCameraPose defaultPose{}; if (!activeCutscene || activeCutscene->cameraTrack.empty()) { return defaultPose; } int elapsed = cutsceneElapsedMs; for (const CutsceneCameraSegment& segment : activeCutscene->cameraTrack) { const int durationMs = std::max(segment.durationMs, 1); if (elapsed <= durationMs) { const float rawT = static_cast(elapsed) / static_cast(durationMs); const float t = applyEasing(segment.easing, std::clamp(rawT, 0.0f, 1.0f)); CutsceneCameraPose pose; pose.focusX = segment.from.focusX + (segment.to.focusX - segment.from.focusX) * t; pose.focusY = segment.from.focusY + (segment.to.focusY - segment.from.focusY) * t; pose.zoom = segment.from.zoom + (segment.to.zoom - segment.from.zoom) * t; pose.rotationDeg = segment.from.rotationDeg + (segment.to.rotationDeg - segment.from.rotationDeg) * t; return pose; } elapsed -= durationMs; } return activeCutscene->cameraTrack.back().to; } void DialogueRuntime::refreshCutscenePresentation() { if (!activeCutscene) { return; } presentation.mode = PresentationMode::Cutscene; presentation.backgroundPath = activeCutscene->background; presentation.cutsceneCamera = evaluateCutsceneCameraPose(); std::cout << "[CUTSCENE] pose focus=(" << presentation.cutsceneCamera.focusX << ", " << presentation.cutsceneCamera.focusY << ") zoom=" << presentation.cutsceneCamera.zoom << " rot=" << presentation.cutsceneCamera.rotationDeg << " line=" << currentCutsceneLine << std::endl; presentation.choices.clear(); presentation.selectedChoice = 0; presentation.revealCompleted = true; const bool hasSubtitle = currentCutsceneLine >= 0 && currentCutsceneLine < static_cast(activeCutscene->lines.size()); presentation.showCutsceneSubtitle = hasSubtitle; if (!hasSubtitle) { presentation.speaker.clear(); presentation.fullText.clear(); presentation.visibleText.clear(); presentation.portraitPath.clear(); return; } const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine]; presentation.speaker = line.speaker; presentation.fullText = line.text; presentation.visibleText = line.text; presentation.portraitPath = line.portrait; std::cout << "[CUTSCENE] lines=" << activeCutscene->lines.size() << " current=" << currentCutsceneLine << std::endl; } float DialogueRuntime::applyEasing(EasingType easing, float t) { t = std::clamp(t, 0.0f, 1.0f); constexpr float PI = 3.14159265358979323846f; switch (easing) { case EasingType::EaseInSine: return 1.0f - std::cos((t * PI) * 0.5f); case EasingType::EaseOutSine: return std::sin((t * PI) * 0.5f); case EasingType::EaseInOutSine: return -(std::cos(PI * t) - 1.0f) * 0.5f; case EasingType::EaseInQuad: return t * t; case EasingType::EaseOutQuad: return 1.0f - (1.0f - t) * (1.0f - t); case EasingType::EaseInOutQuad: return (t < 0.5f) ? 2.0f * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 2.0f) * 0.5f; case EasingType::EaseInCubic: return t * t * t; case EasingType::EaseOutCubic: return 1.0f - std::pow(1.0f - t, 3.0f); case EasingType::EaseInOutCubic: return (t < 0.5f) ? 4.0f * t * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 3.0f) * 0.5f; case EasingType::Linear: default: return t; } } int DialogueRuntime::computeFallbackCutsceneDurationMs(const std::string& text) { const int cps = 17; const int minDuration = 1500; const int linger = 450; const int calculated = static_cast((1000.0 * static_cast(std::max(text.size(), 1))) / cps); return std::max(minDuration, calculated + linger); } int DialogueRuntime::computeCameraTrackDurationMs(const StaticCutsceneDefinition& cutscene) { int total = 0; for (const CutsceneCameraSegment& segment : cutscene.cameraTrack) { total += std::max(segment.durationMs, 0); } return total; } DialogueRuntime::json DialogueRuntime::buildSaveState() const { json result; result["active"] = isActive(); result["dialogueId"] = activeDialogue ? activeDialogue->id : ""; result["currentNodeId"] = currentNodeId; result["pendingNodeAfterCutscene"] = pendingNodeAfterCutscene; result["selectedChoice"] = selectedChoice; result["currentCutsceneLine"] = currentCutsceneLine; result["cutsceneTimerMs"] = cutsceneTimerMs; result["flags"] = flags; result["consumedChoices"] = consumedChoices; return result; } bool DialogueRuntime::restoreSaveState(const json& state) { if (!database) { return false; } flags.clear(); consumedChoices.clear(); if (state.contains("flags")) { flags = state["flags"].get>(); } if (state.contains("consumedChoices")) { consumedChoices = state["consumedChoices"].get>(); } const bool active = state.value("active", false); if (!active) { stop(); return true; } const std::string dialogueId = state.value("dialogueId", ""); if (!startDialogue(dialogueId)) { return false; } const std::string nodeId = state.value("currentNodeId", ""); pendingNodeAfterCutscene = state.value("pendingNodeAfterCutscene", ""); selectedChoice = state.value("selectedChoice", 0); currentCutsceneLine = state.value("currentCutsceneLine", -1); cutsceneTimerMs = state.value("cutsceneTimerMs", 0); const bool ok = nodeId.empty() ? true : enterNode(nodeId); if (mode == Mode::WaitingForChoice && !visibleChoices.empty()) { selectedChoice = std::clamp(selectedChoice, 0, static_cast(visibleChoices.size()) - 1); presentation.selectedChoice = selectedChoice; } return ok; } } // namespace ZL::Dialogue