improve dialogue advance and add flexible cutscene camera system

This commit is contained in:
vottozi committed 2026-04-15 20:42:41 +06:00
1 parent 422ff1fbe3
commit ceebb13719
10 files changed
+502 -74

No files matched your search

+195 -25
View File
@@ -1,6 +1,7 @@
#include "dialogue/DialogueRuntime.h"
#include <algorithm>
#include <cmath>
#include <iostream>
namespace ZL::Dialogue {
@@ -30,6 +31,8 @@ bool DialogueRuntime::startDialogue(const std::string& dialogueId) {
revealCharacters = 0.0f;
currentCutsceneLine = -1;
cutsceneTimerMs = 0;
cutsceneElapsedMs = 0;
cutsceneTotalDurationMs = 0;
presentation = {};
presentation.dialogueId = dialogue->id;
@@ -46,6 +49,8 @@ void DialogueRuntime::stop() {
revealCharacters = 0.0f;
currentCutsceneLine = -1;
cutsceneTimerMs = 0;
cutsceneElapsedMs = 0;
cutsceneTotalDurationMs = 0;
mode = Mode::Inactive;
presentation = {};
}
@@ -63,20 +68,59 @@ void DialogueRuntime::update(int deltaMs) {
}
if (mode == Mode::PlayingCutscene && activeCutscene) {
if (currentCutsceneLine < 0 || currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size())) {
advanceCutsceneLine();
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
: computeFallbackCutsceneDurationMs(line.text);
if (cutsceneTimerMs >= durationMs) {
advanceCutsceneLine();
// ВАЖНО: после advance катсцена могла завершиться
if (!activeCutscene || mode != Mode::PlayingCutscene) {
return;
}
}
}
}
if (!activeCutscene || mode != Mode::PlayingCutscene) {
return;
}
const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
if (line.waitForConfirm) {
refreshCutscenePresentation();
if (!activeCutscene || mode != Mode::PlayingCutscene) {
return;
}
cutsceneTimerMs += deltaMs;
const int durationMs = (line.durationMs > 0) ? line.durationMs : computeFallbackCutsceneDurationMs(line.text);
if (cutsceneTimerMs >= durationMs) {
advanceCutsceneLine();
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) {
finishCutscene();
}
return;
}
if (subtitlesFinished && (cutsceneTotalDurationMs <= 0 || durationFinished)) {
finishCutscene();
return;
}
}
}
@@ -125,7 +169,13 @@ void DialogueRuntime::confirmAdvance() {
}
if (mode == Mode::PlayingCutscene) {
advanceCutsceneLine();
if (!activeCutscene || activeCutscene->lines.empty()) {
return;
}
if (currentCutsceneLine >= 0 && currentCutsceneLine < static_cast<int>(activeCutscene->lines.size())) {
advanceCutsceneLine();
}
}
}
@@ -261,6 +311,8 @@ void DialogueRuntime::presentLine(const Node& node) {
presentation.choices.clear();
presentation.selectedChoice = 0;
presentation.revealCompleted = node.text.empty();
presentation.showCutsceneSubtitle = false;
presentation.cutsceneCamera = {};
if (presentation.revealCompleted) {
presentation.visibleText = node.text;
@@ -303,6 +355,8 @@ void DialogueRuntime::presentChoices(const Node& node) {
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) {
@@ -326,9 +380,37 @@ void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::st
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;
advanceCutsceneLine();
cutsceneElapsedMs = 0;
cutsceneTotalDurationMs = 0;
if (!pendingNodeAfterCutscene.empty()) {
const std::string nextNode = pendingNodeAfterCutscene;
pendingNodeAfterCutscene.clear();
enterNode(nextNode);
}
else {
stop();
}
}
void DialogueRuntime::advanceCutsceneLine() {
@@ -337,18 +419,19 @@ void DialogueRuntime::advanceCutsceneLine() {
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<int>(activeCutscene->lines.size())) {
activeCutscene = nullptr;
if (!pendingNodeAfterCutscene.empty()) {
const std::string nextNode = pendingNodeAfterCutscene;
pendingNodeAfterCutscene.clear();
enterNode(nextNode);
}
else {
stop();
refreshCutscenePresentation();
if (cutsceneTotalDurationMs <= 0 || cutsceneElapsedMs >= cutsceneTotalDurationMs) {
finishCutscene();
}
return;
}
@@ -356,22 +439,101 @@ void DialogueRuntime::advanceCutsceneLine() {
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<float>(elapsed) / static_cast<float>(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 || currentCutsceneLine < 0 ||
currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size())) {
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<int>(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.mode = PresentationMode::Cutscene;
presentation.speaker = line.speaker;
presentation.fullText = line.text;
presentation.visibleText = line.text;
presentation.portraitPath = line.portrait;
presentation.backgroundPath = activeCutscene->background;
presentation.choices.clear();
presentation.selectedChoice = 0;
presentation.revealCompleted = true;
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) {
@@ -382,6 +544,14 @@ int DialogueRuntime::computeFallbackCutsceneDurationMs(const std::string& text)
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();