Some improvements, added cutscene stub

This commit is contained in:
Vladislav Khorev committed 2026-05-09 23:31:49 +03:00
1 parent 72e05cdc28
commit be05643b70
27 files changed
+3480 -40

No files matched your search

+4
View File
@@ -197,6 +197,10 @@ StaticCutsceneDefinition DialogueDatabase::parseCutscene(const json& j) {
cutscene.music = j.value("music", "");
cutscene.skippable = j.value("skippable", true);
cutscene.durationMs = j.value("durationMs", 0);
cutscene.fadeOutMs = j.value("fadeOutMs", 0);
cutscene.fadeInMs = j.value("fadeInMs", 0);
cutscene.endFadeOutMs = j.value("endFadeOutMs", 0);
cutscene.endFadeInMs = j.value("endFadeInMs", 0);
if (j.contains("cameraTrack") && j["cameraTrack"].is_array()) {
for (const auto& item : j["cameraTrack"]) {
+20 -1
View File
@@ -461,7 +461,7 @@ void DialogueOverlay::drawCutscene(Renderer& renderer, const PresentationModel&
toUV(srcBR)
);
renderer.RenderUniform1f("uAlpha", std::clamp(layer.alpha, 0.0f, 1.0f));
renderer.RenderUniform1f("uAlpha", std::clamp(layer.alpha * model.cutsceneGlobalFadeAlpha, 0.0f, 1.0f));
drawQuad(renderer, backgroundQuad, texture);
}
@@ -469,6 +469,25 @@ void DialogueOverlay::drawCutscene(Renderer& renderer, const PresentationModel&
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
// Black overlay: fades IN over the game world (phase 1), then fades OUT to reveal cutscene (phase 2).
// Image renders at cutsceneGlobalFadeAlpha (0 during phase 1 so game world shows through for the
// fade-to-black effect, 1 during phase 2 so it blocks the world while black peels away).
if (model.cutsceneBlackAlpha > 0.001f) {
const float blackAlpha = model.cutsceneBlackAlpha;
renderer.shaderManager.PushShader("cutsceneBlack");
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
renderer.PushMatrix();
renderer.LoadIdentity();
backgroundQuad.rebuild(screenRect);
renderer.RenderUniform1f("uAlpha", blackAlpha);
renderer.DrawVertexRenderStruct(backgroundQuad.mesh);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
}
// UI quads over the image: subtitle panel and skip progress hint background.
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
+81 -6
View File
@@ -33,6 +33,7 @@ bool DialogueRuntime::startDialogue(const std::string& dialogueId) {
cutsceneTimerMs = 0;
cutsceneElapsedMs = 0;
cutsceneTotalDurationMs = 0;
cutsceneContentDurationMs = 0;
currentCutsceneBackground.clear();
presentation = {};
presentation.dialogueId = dialogue->id;
@@ -40,6 +41,27 @@ bool DialogueRuntime::startDialogue(const std::string& dialogueId) {
return enterNode(dialogue->startNode);
}
bool DialogueRuntime::startStandaloneCutscene(const std::string& cutsceneId) {
if (!database) {
std::cerr << "[dialogue] No database assigned to runtime\n";
return false;
}
const StaticCutsceneDefinition* def = database->findCutscene(cutsceneId);
if (!def) {
std::cerr << "[dialogue] Cutscene not found: " << cutsceneId << "\n";
return false;
}
activeDialogue = nullptr;
activeCutsceneId = cutsceneId;
presentation = {};
startCutscene(cutsceneId, "");
return true;
}
void DialogueRuntime::setOnCutsceneFinished(std::function<void(const std::string&)> cb) {
onCutsceneFinished = std::move(cb);
}
void DialogueRuntime::stop() {
activeDialogue = nullptr;
activeCutscene = nullptr;
@@ -52,6 +74,7 @@ void DialogueRuntime::stop() {
cutsceneTimerMs = 0;
cutsceneElapsedMs = 0;
cutsceneTotalDurationMs = 0;
cutsceneContentDurationMs = 0;
currentCutsceneBackground.clear();
mode = Mode::Inactive;
presentation = {};
@@ -340,6 +363,7 @@ bool DialogueRuntime::enterNode(const std::string& nodeId) {
return true;
case NodeType::CutsceneStart:
activeCutsceneId = node.cutsceneId;
startCutscene(node.cutsceneId, node.next);
return true;
}
@@ -364,6 +388,8 @@ void DialogueRuntime::presentLine(const Node& node) {
presentation.cutsceneSkippable = false;
presentation.cutsceneCamera = {};
presentation.cutsceneImages.clear();
presentation.cutsceneGlobalFadeAlpha = 1.0f;
presentation.cutsceneBlackAlpha = 0.0f;
if (presentation.revealCompleted) {
presentation.visibleText = node.text;
@@ -410,6 +436,8 @@ void DialogueRuntime::presentChoices(const Node& node) {
presentation.cutsceneSkippable = false;
presentation.cutsceneCamera = {};
presentation.cutsceneImages.clear();
presentation.cutsceneGlobalFadeAlpha = 1.0f;
presentation.cutsceneBlackAlpha = 0.0f;
}
void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::string& nextNodeAfterCutscene) {
@@ -451,10 +479,11 @@ void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::st
}
imageTrackDurationMs = std::max(imageTrackDurationMs, cueEnd);
}
cutsceneTotalDurationMs = std::max({ activeCutscene->durationMs, computeCameraTrackDurationMs(*activeCutscene), imageTrackDurationMs });
if (cutsceneTotalDurationMs <= 0 && activeCutscene->lines.empty()) {
cutsceneTotalDurationMs = 3000;
cutsceneContentDurationMs = std::max({ activeCutscene->durationMs, computeCameraTrackDurationMs(*activeCutscene), imageTrackDurationMs });
if (cutsceneContentDurationMs <= 0 && activeCutscene->lines.empty()) {
cutsceneContentDurationMs = 3000;
}
cutsceneTotalDurationMs = cutsceneContentDurationMs + activeCutscene->endFadeOutMs + activeCutscene->endFadeInMs;
refreshCutscenePresentation();
std::cout << "[CUTSCENE] start id=" << cutsceneId
@@ -466,12 +495,18 @@ void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::st
void DialogueRuntime::finishCutscene() {
std::cout << "[CUTSCENE] finish nextNode=" << pendingNodeAfterCutscene << std::endl;
const std::string finishedId = activeCutsceneId;
activeCutsceneId.clear();
activeCutscene = nullptr;
currentCutsceneLine = -1;
cutsceneTimerMs = 0;
cutsceneElapsedMs = 0;
cutsceneTotalDurationMs = 0;
cutsceneContentDurationMs = 0;
currentCutsceneBackground.clear();
if (onCutsceneFinished && !finishedId.empty())
onCutsceneFinished(finishedId);
if (!pendingNodeAfterCutscene.empty()) {
const std::string nextNode = pendingNodeAfterCutscene;
pendingNodeAfterCutscene.clear();
@@ -528,8 +563,11 @@ void DialogueRuntime::advanceCutsceneLine() {
if (currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size())) {
refreshCutscenePresentation();
if (cutsceneTotalDurationMs <= 0 || cutsceneElapsedMs >= cutsceneTotalDurationMs) {
finishCutscene();
if (cutsceneContentDurationMs <= 0 || cutsceneElapsedMs >= cutsceneContentDurationMs) {
// Only finish immediately if there's no end transition to play
if (activeCutscene->endFadeOutMs <= 0 && activeCutscene->endFadeInMs <= 0) {
finishCutscene();
}
}
return;
}
@@ -589,7 +627,7 @@ std::vector<PresentedCutsceneImage> DialogueRuntime::evaluateCutsceneImages() co
return result;
}
const int effectiveTotalDuration = (cutsceneTotalDurationMs > 0) ? cutsceneTotalDurationMs : std::max(activeCutscene->durationMs, 1);
const int effectiveTotalDuration = (cutsceneContentDurationMs > 0) ? cutsceneContentDurationMs : std::max(activeCutscene->durationMs, 1);
const int now = std::max(cutsceneElapsedMs, 0);
for (size_t i = 0; i < activeCutscene->images.size(); ++i) {
@@ -655,6 +693,43 @@ void DialogueRuntime::refreshCutscenePresentation() {
presentation.cutsceneImages = evaluateCutsceneImages();
presentation.cutsceneSkippable = activeCutscene->skippable;
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) {
// Start phase 1: game world fading to black
presentation.cutsceneGlobalFadeAlpha = 0.0f;
presentation.cutsceneBlackAlpha = std::clamp(
static_cast<float>(cutsceneElapsedMs) / static_cast<float>(fadeOutMs),
0.0f, 1.0f
);
} else if (cutsceneElapsedMs < endFadeOutStart) {
// Content playing (also covers start phase 2 fade-in)
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) {
// End phase 1: cutscene fading to black
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 {
// End phase 2: game world appearing from black (image hidden, world shows through)
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;
+7
View File
@@ -2,6 +2,7 @@
#include "dialogue/DialogueDatabase.h"
#include "external/nlohmann/json.hpp"
#include <functional>
#include <string>
#include <unordered_map>
#include <unordered_set>
@@ -16,6 +17,8 @@ public:
void setDatabase(const DialogueDatabase* value);
bool startDialogue(const std::string& dialogueId);
bool startStandaloneCutscene(const std::string& cutsceneId);
void setOnCutsceneFinished(std::function<void(const std::string&)> cb);
void stop();
void update(int deltaMs);
@@ -46,6 +49,9 @@ private:
PlayingCutscene
};
std::function<void(const std::string&)> onCutsceneFinished;
std::string activeCutsceneId;
const DialogueDatabase* database = nullptr;
const DialogueDefinition* activeDialogue = nullptr;
const StaticCutsceneDefinition* activeCutscene = nullptr;
@@ -68,6 +74,7 @@ private:
int cutsceneTimerMs = 0;
int cutsceneElapsedMs = 0;
int cutsceneTotalDurationMs = 0;
int cutsceneContentDurationMs = 0;
std::string currentCutsceneBackground;
+8
View File
@@ -119,6 +119,14 @@ bool DialogueSystem::startDialogue(const std::string& dialogueId) {
return runtime.startDialogue(dialogueId);
}
bool DialogueSystem::startCutscene(const std::string& cutsceneId) {
return runtime.startStandaloneCutscene(cutsceneId);
}
void DialogueSystem::setOnCutsceneFinished(std::function<void(const std::string&)> cb) {
runtime.setOnCutsceneFinished(std::move(cb));
}
void DialogueSystem::stopDialogue() {
runtime.stop();
}
+3
View File
@@ -3,6 +3,7 @@
#include "dialogue/DialogueOverlay.h"
#include "dialogue/DialogueRuntime.h"
#include <SDL.h>
#include <functional>
#include <string>
namespace ZL::Dialogue {
@@ -21,6 +22,8 @@ public:
bool handlePointerReleased(float x, float y);
bool startDialogue(const std::string& dialogueId);
bool startCutscene(const std::string& cutsceneId);
void setOnCutsceneFinished(std::function<void(const std::string&)> cb);
void stopDialogue();
bool isActive() const { return runtime.isActive(); }
+6
View File
@@ -149,6 +149,10 @@ struct StaticCutsceneDefinition {
std::string music;
bool skippable = true;
int durationMs = 0;
int fadeOutMs = 0;
int fadeInMs = 0;
int endFadeOutMs = 0;
int endFadeInMs = 0;
std::vector<CutsceneCameraSegment> cameraTrack;
std::vector<CutsceneImageCue> images;
std::vector<CutsceneLine> lines;
@@ -195,6 +199,8 @@ struct PresentationModel {
CutsceneCameraBlendState cutsceneCamera;
std::vector<PresentedCutsceneImage> cutsceneImages;
float cutsceneGlobalFadeAlpha = 1.0f;
float cutsceneBlackAlpha = 0.0f;
};
struct SaveState {