#include "cutscene/CutsceneRuntime.h" #include #include #include namespace ZL::Cutscene { void CutsceneRuntime::setDatabase(const CutsceneDatabase* value) { database = value; } void CutsceneRuntime::setOnFinished(std::function cb) { onFinished = std::move(cb); } void CutsceneRuntime::setOnLineStarted(std::function cb) { onLineStarted = std::move(cb); } void CutsceneRuntime::setOnFadeInComplete(std::function cb) { onFadeInComplete = std::move(cb); } bool CutsceneRuntime::start(const std::string& cutsceneId) { if (!database) { std::cerr << "[cutscene] No database assigned to runtime\n"; return false; } const StaticCutsceneDefinition* def = database->findCutscene(cutsceneId); if (!def) { std::cerr << "[cutscene] Cutscene not found: " << cutsceneId << "\n"; return false; } activeCutscene = def; activeCutsceneId = cutsceneId; active = true; fadeInCallbackFired = false; cutsceneElapsedMs = 0; cutsceneTimerMs = 0; currentCutsceneLine = def->lines.empty() ? -1 : 0; int maxSegmentEndMs = 0; for (const CutsceneImageSegment& seg : def->imageSegments) { maxSegmentEndMs = std::max(maxSegmentEndMs, seg.endMs); } cutsceneContentDurationMs = std::max(def->durationMs, maxSegmentEndMs); if (cutsceneContentDurationMs <= 0 && def->lines.empty()) { cutsceneContentDurationMs = 3000; } cutsceneTotalDurationMs = cutsceneContentDurationMs + def->endFadeOutMs + def->endFadeInMs; presentation = {}; refreshPresentation(); if (!def->lines.empty()) { const CutsceneLine& firstLine = def->lines[0]; if (onLineStarted && !firstLine.luaCallback.empty()) onLineStarted(firstLine.luaCallback); } std::cout << "[CUTSCENE] start id=" << cutsceneId << " lines=" << def->lines.size() << " totalDuration=" << cutsceneTotalDurationMs << std::endl; return true; } void CutsceneRuntime::stop() { activeCutscene = nullptr; activeCutsceneId.clear(); active = false; fadeInCallbackFired = false; currentCutsceneLine = -1; cutsceneTimerMs = 0; cutsceneElapsedMs = 0; cutsceneTotalDurationMs = 0; cutsceneContentDurationMs = 0; presentation = {}; } void CutsceneRuntime::update(int deltaMs) { if (!active || !activeCutscene) return; 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 : computeFallbackDurationMs(line.text); if (cutsceneTimerMs >= durationMs) { advanceLine(); if (!active || !activeCutscene) return; } } } if (!active || !activeCutscene) return; refreshPresentation(); if (!active || !activeCutscene) return; if (!fadeInCallbackFired && onFadeInComplete && activeCutscene) { const int fadeInCompleteMs = activeCutscene->fadeOutMs + activeCutscene->fadeInMs; if (cutsceneElapsedMs >= fadeInCompleteMs && !activeCutscene->onFadeInCallback.empty()) { fadeInCallbackFired = true; onFadeInComplete(activeCutscene->onFadeInCallback); } } if (!active || !activeCutscene) 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) { finish(); } return; } if (subtitlesFinished && (cutsceneTotalDurationMs <= 0 || durationFinished)) { finish(); } } bool CutsceneRuntime::canSkip() const { return active && activeCutscene && activeCutscene->skippable; } void CutsceneRuntime::skip() { if (!canSkip()) return; finish(); } void CutsceneRuntime::finish() { std::cout << "[CUTSCENE] finish id=" << activeCutsceneId << std::endl; const std::string finishedId = activeCutsceneId; stop(); if (onFinished && !finishedId.empty()) { onFinished(finishedId); } } void CutsceneRuntime::syncLineToElapsedTime() { if (!activeCutscene || activeCutscene->lines.empty()) { currentCutsceneLine = -1; cutsceneTimerMs = 0; return; } int elapsed = std::max(cutsceneElapsedMs, 0); int accumulatedMs = 0; for (size_t i = 0; i < activeCutscene->lines.size(); ++i) { const CutsceneLine& line = activeCutscene->lines[i]; const int durationMs = (line.durationMs > 0) ? line.durationMs : computeFallbackDurationMs(line.text); if (elapsed < accumulatedMs + durationMs) { currentCutsceneLine = static_cast(i); cutsceneTimerMs = std::max(0, elapsed - accumulatedMs); return; } accumulatedMs += durationMs; } currentCutsceneLine = -1; cutsceneTimerMs = 0; } void CutsceneRuntime::advanceLine() { if (!activeCutscene) { stop(); return; } if (activeCutscene->lines.empty()) return; ++currentCutsceneLine; cutsceneTimerMs = 0; if (currentCutsceneLine >= static_cast(activeCutscene->lines.size())) { refreshPresentation(); if (cutsceneContentDurationMs <= 0 || cutsceneElapsedMs >= cutsceneContentDurationMs) { if (activeCutscene->endFadeOutMs <= 0 && activeCutscene->endFadeInMs <= 0) { finish(); } } return; } const CutsceneLine& newLine = activeCutscene->lines[currentCutsceneLine]; if (onLineStarted && !newLine.luaCallback.empty()) onLineStarted(newLine.luaCallback); refreshPresentation(); } std::vector CutsceneRuntime::evaluateImages() const { std::vector result; if (!activeCutscene) return result; const int now = std::max(cutsceneElapsedMs, 0); for (const CutsceneImageSegment& seg : activeCutscene->imageSegments) { if (seg.path.empty()) continue; if (now < seg.startMs || now > seg.endMs) continue; // Fade-in / fade-out alpha float alpha = 1.0f; if (seg.fadeInMs > 0 && now < seg.startMs + seg.fadeInMs) { alpha = std::clamp( static_cast(now - seg.startMs) / static_cast(seg.fadeInMs), 0.0f, 1.0f ); } if (seg.fadeOutMs > 0 && now > seg.endMs - seg.fadeOutMs) { const float fadeOutAlpha = std::clamp( static_cast(seg.endMs - now) / static_cast(seg.fadeOutMs), 0.0f, 1.0f ); alpha = std::min(alpha, fadeOutAlpha); } // Interpolated pose const float segDuration = static_cast(std::max(seg.endMs - seg.startMs, 1)); const float rawT = static_cast(now - seg.startMs) / segDuration; const float easedT = applyEasing(seg.easing, std::clamp(rawT, 0.0f, 1.0f)); CutsceneImagePose pose; pose.centerX = seg.from.centerX + (seg.to.centerX - seg.from.centerX) * easedT; pose.centerY = seg.from.centerY + (seg.to.centerY - seg.from.centerY) * easedT; pose.scale = seg.from.scale + (seg.to.scale - seg.from.scale) * easedT; result.push_back({ seg.path, alpha, pose, seg.width, seg.height }); } return result; } void CutsceneRuntime::refreshPresentation() { if (!activeCutscene) return; presentation.mode = ZL::Dialogue::PresentationMode::Cutscene; presentation.cutsceneSkippable = activeCutscene->skippable; presentation.cutsceneImages = evaluateImages(); const int fadeOutMs = activeCutscene->fadeOutMs; const int fadeInMs = activeCutscene->fadeInMs; const int endFadeOutMs = activeCutscene->endFadeOutMs; const int endFadeInMs = activeCutscene->endFadeInMs; const int endFadeOutStart = cutsceneContentDurationMs; const int endFadeInStart = cutsceneContentDurationMs + endFadeOutMs; if (cutsceneElapsedMs < fadeOutMs) { presentation.cutsceneGlobalFadeAlpha = 0.0f; presentation.cutsceneBlackAlpha = std::clamp( static_cast(cutsceneElapsedMs) / static_cast(fadeOutMs), 0.0f, 1.0f ); } else if (cutsceneElapsedMs < endFadeOutStart) { presentation.cutsceneGlobalFadeAlpha = 1.0f; const int phase2elapsed = cutsceneElapsedMs - fadeOutMs; presentation.cutsceneBlackAlpha = (fadeInMs > 0) ? std::clamp(1.0f - static_cast(phase2elapsed) / static_cast(fadeInMs), 0.0f, 1.0f) : 0.0f; } else if (cutsceneElapsedMs < endFadeInStart) { presentation.cutsceneGlobalFadeAlpha = 1.0f; const int elapsed = cutsceneElapsedMs - endFadeOutStart; presentation.cutsceneBlackAlpha = (endFadeOutMs > 0) ? std::clamp(static_cast(elapsed) / static_cast(endFadeOutMs), 0.0f, 1.0f) : 1.0f; } else { presentation.cutsceneGlobalFadeAlpha = 0.0f; const int elapsed = cutsceneElapsedMs - endFadeInStart; presentation.cutsceneBlackAlpha = (endFadeInMs > 0) ? std::clamp(1.0f - static_cast(elapsed) / static_cast(endFadeInMs), 0.0f, 1.0f) : 0.0f; } presentation.choices.clear(); presentation.selectedChoice = -1; 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(); return; } const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine]; presentation.speaker = line.speaker; presentation.fullText = line.text; presentation.visibleText = line.text; presentation.selectedChoice = 0; } float CutsceneRuntime::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 CutsceneRuntime::computeFallbackDurationMs(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); } } // namespace ZL::Cutscene