space-game001/src/cutscene/CutsceneRuntime.cpp
2026-06-06 23:00:22 +03:00

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