357 lines
10 KiB
C++
357 lines
10 KiB
C++
#include "cutscene/CutsceneRuntime.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <iostream>
|
|
|
|
namespace ZL::Cutscene {
|
|
|
|
void CutsceneRuntime::setDatabase(const CutsceneDatabase* value) {
|
|
database = value;
|
|
}
|
|
|
|
void CutsceneRuntime::setOnFinished(std::function<void(const std::string&)> cb) {
|
|
onFinished = std::move(cb);
|
|
}
|
|
|
|
void CutsceneRuntime::setOnLineStarted(std::function<void(const std::string&)> cb) {
|
|
onLineStarted = std::move(cb);
|
|
}
|
|
|
|
void CutsceneRuntime::setOnFadeInComplete(std::function<void(const std::string&)> 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<int>(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<int>(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<int>(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<int>(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<PresentedCutsceneImage> CutsceneRuntime::evaluateImages() const {
|
|
std::vector<PresentedCutsceneImage> 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<float>(now - seg.startMs) / static_cast<float>(seg.fadeInMs),
|
|
0.0f, 1.0f
|
|
);
|
|
}
|
|
if (seg.fadeOutMs > 0 && now > seg.endMs - seg.fadeOutMs) {
|
|
const float fadeOutAlpha = std::clamp(
|
|
static_cast<float>(seg.endMs - now) / static_cast<float>(seg.fadeOutMs),
|
|
0.0f, 1.0f
|
|
);
|
|
alpha = std::min(alpha, fadeOutAlpha);
|
|
}
|
|
|
|
// Interpolated pose
|
|
const float segDuration = static_cast<float>(std::max(seg.endMs - seg.startMs, 1));
|
|
const float rawT = static_cast<float>(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<float>(cutsceneElapsedMs) / static_cast<float>(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<float>(phase2elapsed) / static_cast<float>(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<float>(elapsed) / static_cast<float>(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<float>(elapsed) / static_cast<float>(endFadeInMs), 0.0f, 1.0f)
|
|
: 0.0f;
|
|
}
|
|
|
|
presentation.choices.clear();
|
|
presentation.selectedChoice = -1;
|
|
presentation.revealCompleted = true;
|
|
|
|
const bool hasSubtitle = currentCutsceneLine >= 0 &&
|
|
currentCutsceneLine < static_cast<int>(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<int>((1000.0 * static_cast<double>(std::max<size_t>(text.size(), 1))) / cps);
|
|
return std::max(minDuration, calculated + linger);
|
|
}
|
|
|
|
} // namespace ZL::Cutscene
|