Compare commits

...

2 Commits

11 changed files with 810 additions and 75 deletions

View File

@ -16,7 +16,7 @@
"id": "line_2", "id": "line_2",
"type": "Line", "type": "Line",
"speaker": "Hero", "speaker": "Hero",
"portrait": "", "portrait": "resources/hero.png",
"text": "I need answers.", "text": "I need answers.",
"next": "end_1" "next": "end_1"
}, },
@ -26,7 +26,6 @@
} }
] ]
}, },
{ {
"id": "test_choice_dialogue", "id": "test_choice_dialogue",
"start": "line_1", "start": "line_1",
@ -43,7 +42,7 @@
"id": "choice_1", "id": "choice_1",
"type": "Choice", "type": "Choice",
"speaker": "Hero", "speaker": "Hero",
"portrait": "", "portrait": "resources/hero.png",
"text": "Choose your answer.", "text": "Choose your answer.",
"choices": [ "choices": [
{ {
@ -74,7 +73,7 @@
"speaker": "Merchant", "speaker": "Merchant",
"portrait": "resources/ghost_avatar.png", "portrait": "resources/ghost_avatar.png",
"text": "Just a trader passing through.", "text": "Just a trader passing through.",
"next": "end_1" "next": "choice_1"
}, },
{ {
"id": "end_1", "id": "end_1",
@ -82,7 +81,6 @@
} }
] ]
}, },
{ {
"id": "test_condition_dialogue", "id": "test_condition_dialogue",
"start": "set_flag_1", "start": "set_flag_1",
@ -126,7 +124,6 @@
} }
] ]
}, },
{ {
"id": "test_cutscene_dialogue", "id": "test_cutscene_dialogue",
"start": "cutscene_start", "start": "cutscene_start",
@ -142,17 +139,85 @@
"type": "End" "type": "End"
} }
] ]
},
{
"id": "test_silent_cutscene_dialogue",
"start": "cutscene_start",
"nodes": [
{
"id": "cutscene_start",
"type": "CutsceneStart",
"cutsceneId": "test_cutscene_silent_01",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "test_cutscene_pan_dialogue",
"start": "cutscene_start",
"nodes": [
{
"id": "cutscene_start",
"type": "CutsceneStart",
"cutsceneId": "test_cutscene_pan_01",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "test_cutscene_pan_dialogue_silent",
"start": "cutscene_start",
"nodes": [
{
"id": "cutscene_start",
"type": "CutsceneStart",
"cutsceneId": "test_cutscene_pan_02",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
} }
], ],
"cutscenes": [ "cutscenes": [
{ {
"id": "test_cutscene_01", "id": "test_cutscene_01",
"background": "resources/first_cutscene.png", "background": "resources/first_cutscene.png",
"durationMs": 6800,
"cameraTrack": [
{
"durationMs": 2400,
"from": { "focusX": 0.50, "focusY": 0.55, "zoom": 1.00, "rotationDeg": 0.0 },
"to": { "focusX": 0.63, "focusY": 0.58, "zoom": 1.16, "rotationDeg": -1.0 },
"easing": "EaseInOutSine"
},
{
"durationMs": 2200,
"from": { "focusX": 0.63, "focusY": 0.58, "zoom": 1.16, "rotationDeg": -1.0 },
"to": { "focusX": 0.74, "focusY": 0.52, "zoom": 1.30, "rotationDeg": -2.4 },
"easing": "EaseInOutCubic"
},
{
"durationMs": 2200,
"from": { "focusX": 0.74, "focusY": 0.52, "zoom": 1.30, "rotationDeg": -2.4 },
"to": { "focusX": 0.58, "focusY": 0.46, "zoom": 1.10, "rotationDeg": -0.6 },
"easing": "EaseOutSine"
}
],
"lines": [ "lines": [
{ {
"speaker": "Narrator", "speaker": "Narrator",
"portrait": "", "portrait": "resources/hero.png",
"text": "The air in the room turned cold.", "text": "The air in the room turned cold.",
"durationMs": 2200 "durationMs": 2200
}, },
@ -163,6 +228,121 @@
"durationMs": 2600 "durationMs": 2600
} }
] ]
},
{
"id": "test_cutscene_silent_01",
"background": "resources/first_cutscene.png",
"durationMs": 5200,
"cameraTrack": [
{
"durationMs": 2600,
"from": { "focusX": 0.40, "focusY": 0.54, "zoom": 1.00, "rotationDeg": 0.0 },
"to": { "focusX": 0.58, "focusY": 0.54, "zoom": 1.22, "rotationDeg": 0.8 },
"easing": "EaseInOutSine"
},
{
"durationMs": 2600,
"from": { "focusX": 0.58, "focusY": 0.54, "zoom": 1.22, "rotationDeg": 0.8 },
"to": { "focusX": 0.72, "focusY": 0.48, "zoom": 1.34, "rotationDeg": -0.5 },
"easing": "EaseOutCubic"
}
],
"lines": []
},
{
"id": "test_cutscene_pan_01",
"background": "resources/first_cutscene.png",
"durationMs": 12000,
"cameraTrack": [
{
"durationMs": 1200,
"from": { "anchor": "Center", "zoom": 1.00, "rotationDeg": 0.0 },
"to": { "anchor": "Center", "zoom": 1.00, "rotationDeg": 0.0 },
"easing": "Linear"
},
{
"durationMs": 2500,
"from": { "anchor": "Center", "zoom": 1.00, "rotationDeg": 0.0 },
"to": { "anchor": "TopLeft", "zoom": 1.55, "rotationDeg": 0.0 },
"easing": "EaseInOutSine"
},
{
"durationMs": 2600,
"from": { "anchor": "TopLeft", "zoom": 1.55, "rotationDeg": 0.0 },
"to": { "anchor": "TopRight", "zoom": 1.55, "rotationDeg": 0.0 },
"easing": "EaseInOutSine"
},
{
"durationMs": 1800,
"from": { "anchor": "TopRight", "zoom": 1.55, "rotationDeg": 0.0 },
"to": { "anchor": "BottomRight", "zoom": 1.72, "rotationDeg": 0.0 },
"easing": "EaseInCubic"
},
{
"durationMs": 3900,
"from": { "anchor": "BottomRight", "zoom": 1.72, "rotationDeg": 0.0 },
"to": { "anchor": "BottomLeft", "zoom": 1.55, "rotationDeg": 0.0 },
"easing": "EaseInOutSine"
}
],
"lines": [
{
"speaker": "Narrator",
"portrait": "resources/hero.png",
"text": "The memory begins in silence.",
"durationMs": 2200
},
{
"speaker": "Narrator",
"portrait": "resources/hero.png",
"text": "Something is drawing your eyes across the whole scene.",
"durationMs": 2800
},
{
"speaker": "Ghost",
"portrait": "resources/ghost_avatar.png",
"text": "Do not look away.",
"durationMs": 2400
}
]
},
{
"id": "test_cutscene_pan_02",
"background": "resources/first_cutscene.png",
"durationMs": 12000,
"cameraTrack": [
{
"durationMs": 1200,
"from": { "anchor": "Center", "zoom": 1.00, "rotationDeg": 0.0 },
"to": { "anchor": "Center", "zoom": 1.00, "rotationDeg": 0.0 },
"easing": "Linear"
},
{
"durationMs": 2500,
"from": { "anchor": "Center", "zoom": 1.00, "rotationDeg": 0.0 },
"to": { "anchor": "TopLeft", "zoom": 1.55, "rotationDeg": 0.0 },
"easing": "EaseInOutSine"
},
{
"durationMs": 2600,
"from": { "anchor": "TopLeft", "zoom": 1.55, "rotationDeg": 0.0 },
"to": { "anchor": "TopRight", "zoom": 1.55, "rotationDeg": 0.0 },
"easing": "EaseInOutSine"
},
{
"durationMs": 1800,
"from": { "anchor": "TopRight", "zoom": 1.55, "rotationDeg": 0.0 },
"to": { "anchor": "BottomRight", "zoom": 1.72, "rotationDeg": 0.0 },
"easing": "EaseInCubic"
},
{
"durationMs": 3900,
"from": { "anchor": "BottomRight", "zoom": 1.72, "rotationDeg": 0.0 },
"to": { "anchor": "BottomLeft", "zoom": 1.55, "rotationDeg": 0.0 },
"easing": "EaseInOutSine"
}
],
"lines": []
} }
] ]
} }

BIN
resources/hero.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -516,14 +516,14 @@ namespace ZL
targetInteractiveObject = nullptr; targetInteractiveObject = nullptr;
} }
//for (auto& npc : npcs) npc->update(delta); //for (auto& npc : npcs) npc->update(delta);
}
if (player) { if (player) {
dialogueSystem.update(static_cast<int>(delta), player->position); dialogueSystem.update(static_cast<int>(delta), player->position);
} }
} }
} }
}
void Game::render() { void Game::render() {
ZL::CheckGlError(); ZL::CheckGlError();
@ -732,11 +732,11 @@ namespace ZL
if (event.type == SDL_KEYDOWN && event.key.repeat == 0) { if (event.type == SDL_KEYDOWN && event.key.repeat == 0) {
switch (event.key.keysym.sym) { switch (event.key.keysym.sym) {
case SDLK_f: case SDLK_f:
dialogueSystem.startDialogue("test_line_dialogue"); dialogueSystem.startDialogue("test_cutscene_pan_dialogue_silent");
break; break;
case SDLK_e: case SDLK_e:
dialogueSystem.startDialogue("test_cutscene_dialogue"); dialogueSystem.startDialogue("test_cutscene_pan_dialogue");
break; break;
case SDLK_RETURN: case SDLK_RETURN:

View File

@ -29,6 +29,28 @@ ComparisonOp DialogueDatabase::parseComparisonOp(const std::string& value) {
return ComparisonOp::Exists; return ComparisonOp::Exists;
} }
EasingType DialogueDatabase::parseEasingType(const std::string& value) {
if (value == "EaseInSine") return EasingType::EaseInSine;
if (value == "EaseOutSine") return EasingType::EaseOutSine;
if (value == "EaseInOutSine") return EasingType::EaseInOutSine;
if (value == "EaseInQuad") return EasingType::EaseInQuad;
if (value == "EaseOutQuad") return EasingType::EaseOutQuad;
if (value == "EaseInOutQuad") return EasingType::EaseInOutQuad;
if (value == "EaseInCubic") return EasingType::EaseInCubic;
if (value == "EaseOutCubic") return EasingType::EaseOutCubic;
if (value == "EaseInOutCubic") return EasingType::EaseInOutCubic;
return EasingType::Linear;
}
CutsceneAnchor DialogueDatabase::parseCutsceneAnchor(const std::string& value) {
if (value == "TopLeft") return CutsceneAnchor::TopLeft;
if (value == "TopRight") return CutsceneAnchor::TopRight;
if (value == "BottomRight") return CutsceneAnchor::BottomRight;
if (value == "BottomLeft") return CutsceneAnchor::BottomLeft;
if (value == "Custom") return CutsceneAnchor::Custom;
return CutsceneAnchor::Center;
}
Condition DialogueDatabase::parseCondition(const json& j) { Condition DialogueDatabase::parseCondition(const json& j) {
Condition c; Condition c;
c.flag = j.value("flag", ""); c.flag = j.value("flag", "");
@ -126,12 +148,45 @@ CutsceneLine DialogueDatabase::parseCutsceneLine(const json& j) {
return line; return line;
} }
CutsceneCameraPose DialogueDatabase::parseCutsceneCameraPose(const json& j) {
CutsceneCameraPose pose;
pose.anchor = parseCutsceneAnchor(j.value("anchor", "Center"));
pose.centerX = j.value("centerX", 0.5f);
pose.centerY = j.value("centerY", 0.5f);
pose.zoom = j.value("zoom", 1.0f);
pose.rotationDeg = j.value("rotationDeg", 0.0f);
return pose;
}
CutsceneCameraSegment DialogueDatabase::parseCutsceneCameraSegment(const json& j) {
CutsceneCameraSegment segment;
segment.durationMs = j.value("durationMs", 0);
segment.easing = parseEasingType(j.value("easing", "EaseInOutSine"));
if (j.contains("from") && j["from"].is_object()) {
segment.from = parseCutsceneCameraPose(j["from"]);
}
if (j.contains("to") && j["to"].is_object()) {
segment.to = parseCutsceneCameraPose(j["to"]);
}
else {
segment.to = segment.from;
}
return segment;
}
StaticCutsceneDefinition DialogueDatabase::parseCutscene(const json& j) { StaticCutsceneDefinition DialogueDatabase::parseCutscene(const json& j) {
StaticCutsceneDefinition cutscene; StaticCutsceneDefinition cutscene;
cutscene.id = j.value("id", ""); cutscene.id = j.value("id", "");
cutscene.background = j.value("background", ""); cutscene.background = j.value("background", "");
cutscene.music = j.value("music", ""); cutscene.music = j.value("music", "");
cutscene.skippable = j.value("skippable", true); cutscene.skippable = j.value("skippable", true);
cutscene.durationMs = j.value("durationMs", 0);
if (j.contains("cameraTrack") && j["cameraTrack"].is_array()) {
for (const auto& item : j["cameraTrack"]) {
cutscene.cameraTrack.push_back(parseCutsceneCameraSegment(item));
}
}
if (j.contains("lines") && j["lines"].is_array()) { if (j.contains("lines") && j["lines"].is_array()) {
for (const auto& item : j["lines"]) { for (const auto& item : j["lines"]) {

View File

@ -23,6 +23,8 @@ private:
static NodeType parseNodeType(const std::string& value); static NodeType parseNodeType(const std::string& value);
static ChoiceKind parseChoiceKind(const std::string& value); static ChoiceKind parseChoiceKind(const std::string& value);
static ComparisonOp parseComparisonOp(const std::string& value); static ComparisonOp parseComparisonOp(const std::string& value);
static EasingType parseEasingType(const std::string& value);
static CutsceneAnchor parseCutsceneAnchor(const std::string& value);
static Condition parseCondition(const json& j); static Condition parseCondition(const json& j);
static Effect parseEffect(const json& j); static Effect parseEffect(const json& j);
@ -30,9 +32,9 @@ private:
static Node parseNode(const json& j); static Node parseNode(const json& j);
static DialogueDefinition parseDialogue(const json& j); static DialogueDefinition parseDialogue(const json& j);
static CutsceneLine parseCutsceneLine(const json& j); static CutsceneLine parseCutsceneLine(const json& j);
static CutsceneCameraPose parseCutsceneCameraPose(const json& j);
static CutsceneCameraSegment parseCutsceneCameraSegment(const json& j);
static StaticCutsceneDefinition parseCutscene(const json& j); static StaticCutsceneDefinition parseCutscene(const json& j);
}; };
} // namespace ZL::Dialogue } // namespace ZL::Dialogue

View File

@ -4,6 +4,8 @@
#include "GameConstants.h" #include "GameConstants.h"
#include "Environment.h" #include "Environment.h"
#include <algorithm> #include <algorithm>
#include <array>
#include <cmath>
namespace ZL::Dialogue { namespace ZL::Dialogue {
@ -18,6 +20,45 @@ void DialogueOverlay::TexturedQuad::rebuild(const UiRect& newRect) {
initialized = true; initialized = true;
} }
void DialogueOverlay::TexturedQuad::rebuildWithUV(
const UiRect& newRect,
const Eigen::Vector2f& uvBottomLeft,
const Eigen::Vector2f& uvTopLeft,
const Eigen::Vector2f& uvTopRight,
const Eigen::Vector2f& uvBottomRight
) {
rect = newRect;
const float x0 = rect.x;
const float y0 = rect.y;
const float x1 = rect.x + rect.w;
const float y1 = rect.y + rect.h;
VertexDataStruct data;
data.PositionData = {
{ x0, y0, 0.0f }, // bottom-left
{ x0, y1, 0.0f }, // top-left
{ x1, y1, 0.0f }, // top-right
{ x1, y1, 0.0f }, // top-right
{ x1, y0, 0.0f }, // bottom-right
{ x0, y0, 0.0f } // bottom-left
};
data.TexCoordData = {
uvBottomLeft,
uvTopLeft,
uvTopRight,
uvTopRight,
uvBottomRight,
uvBottomLeft
};
mesh.AssignFrom(data);
initialized = true;
}
bool DialogueOverlay::init(Renderer& renderer, const std::string& zipFile) { bool DialogueOverlay::init(Renderer& renderer, const std::string& zipFile) {
rendererRef = &renderer; rendererRef = &renderer;
zipFilename = zipFile; zipFilename = zipFile;
@ -46,6 +87,9 @@ bool DialogueOverlay::init(Renderer& renderer, const std::string& zipFile) {
void DialogueOverlay::draw(Renderer& renderer, const PresentationModel& model) { void DialogueOverlay::draw(Renderer& renderer, const PresentationModel& model) {
if (model.mode == PresentationMode::Hidden) { if (model.mode == PresentationMode::Hidden) {
lastChoiceRects.clear(); lastChoiceRects.clear();
lastDialogueAdvanceRect = {};
lastCutsceneAdvanceRect = {};
cutsceneAdvanceEnabled = false;
return; return;
} }
@ -59,10 +103,13 @@ void DialogueOverlay::draw(Renderer& renderer, const PresentationModel& model) {
void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel& model) { void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel& model) {
const float W = Environment::projectionWidth; const float W = Environment::projectionWidth;
const float H = Environment::projectionHeight; // const float H = Environment::projectionHeight;
const UiRect portraitRect{ 24.0f, 24.0f, 182.0f, 182.0f }; const UiRect portraitRect{ 24.0f, 24.0f, 182.0f, 182.0f };
const UiRect textboxRect{ 220.0f, 24.0f, max(200.0f, W - 244.0f), 182.0f }; const UiRect textboxRect{ 220.0f, 24.0f, max(200.0f, W - 244.0f), 182.0f };
lastDialogueAdvanceRect = { portraitRect.x, portraitRect.y, textboxRect.x + textboxRect.w - portraitRect.x, textboxRect.h };
lastCutsceneAdvanceRect = {};
cutsceneAdvanceEnabled = false;
if (!portraitQuad.initialized || portraitQuad.rect.w != portraitRect.w || portraitQuad.rect.h != portraitRect.h || if (!portraitQuad.initialized || portraitQuad.rect.w != portraitRect.w || portraitQuad.rect.h != portraitRect.h ||
portraitQuad.rect.x != portraitRect.x || portraitQuad.rect.y != portraitRect.y) { portraitQuad.rect.x != portraitRect.x || portraitQuad.rect.y != portraitRect.y) {
@ -76,7 +123,7 @@ void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel&
glEnable(GL_BLEND); glEnable(GL_BLEND);
renderer.shaderManager.PushShader(defaultShaderName); renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform1i(textureUniformName, 0);
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f); renderer.PushProjectionMatrix(0.0f, W, 0.0f, Environment::projectionHeight, -10.0f, 10.0f);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.LoadIdentity(); renderer.LoadIdentity();
@ -113,7 +160,7 @@ void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel&
renderer.shaderManager.PushShader(defaultShaderName); renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform1i(textureUniformName, 0);
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f); renderer.PushProjectionMatrix(0.0f, W, 0.0f, Environment::projectionHeight, -10.0f, 10.0f);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.LoadIdentity(); renderer.LoadIdentity();
@ -159,14 +206,117 @@ void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel&
glDisable(GL_BLEND); glDisable(GL_BLEND);
} }
float DialogueOverlay::lerpFloat(float a, float b, float t) {
return a + (b - a) * t;
}
DialogueOverlay::ResolvedViewport DialogueOverlay::resolveViewportPose(
const CutsceneCameraPose& pose,
float texW,
float texH,
float screenW,
float screenH
) {
ResolvedViewport out{};
const float safeTexW = max(texW, 1.0f);
const float safeTexH = max(texH, 1.0f);
const float safeScreenW = max(screenW, 1.0f);
const float safeScreenH = max(screenH, 1.0f);
const float screenAspect = safeScreenW / safeScreenH;
const float imageAspect = safeTexW / safeTexH;
float baseViewportW = 0.0f;
float baseViewportH = 0.0f;
if (imageAspect >= screenAspect) {
baseViewportH = safeTexH;
baseViewportW = safeTexH * screenAspect;
}
else {
baseViewportW = safeTexW;
baseViewportH = safeTexW / screenAspect;
}
const float zoom = max(pose.zoom, 0.01f);
const float viewportW = baseViewportW / zoom;
const float viewportH = baseViewportH / zoom;
const float rotationRad = pose.rotationDeg * 3.14159265358979323846f / 180.0f;
const float c = std::cos(rotationRad);
const float s = std::sin(rotationRad);
// Bounding box повернутого viewport внутри source image.
const float halfRotatedW = std::abs((viewportW * 0.5f) * c) + std::abs((viewportH * 0.5f) * s);
const float halfRotatedH = std::abs((viewportW * 0.5f) * s) + std::abs((viewportH * 0.5f) * c);
float centerX = safeTexW * 0.5f;
float centerY = safeTexH * 0.5f;
switch (pose.anchor) {
case CutsceneAnchor::TopLeft:
centerX = halfRotatedW;
centerY = safeTexH - halfRotatedH;
break;
case CutsceneAnchor::TopRight:
centerX = safeTexW - halfRotatedW;
centerY = safeTexH - halfRotatedH;
break;
case CutsceneAnchor::BottomRight:
centerX = safeTexW - halfRotatedW;
centerY = halfRotatedH;
break;
case CutsceneAnchor::BottomLeft:
centerX = halfRotatedW;
centerY = halfRotatedH;
break;
case CutsceneAnchor::Custom:
// centerY: 0 = top, 1 = bottom
centerX = std::clamp(pose.centerX, 0.0f, 1.0f) * safeTexW;
centerY = (1.0f - std::clamp(pose.centerY, 0.0f, 1.0f)) * safeTexH;
centerX = std::clamp(centerX, halfRotatedW, safeTexW - halfRotatedW);
centerY = std::clamp(centerY, halfRotatedH, safeTexH - halfRotatedH);
break;
case CutsceneAnchor::Center:
default:
centerX = safeTexW * 0.5f;
centerY = safeTexH * 0.5f;
break;
}
out.centerXPx = centerX;
out.centerYPx = centerY;
out.widthPx = viewportW;
out.heightPx = viewportH;
out.rotationDeg = pose.rotationDeg;
return out;
}
DialogueOverlay::ResolvedViewport DialogueOverlay::blendViewport(
const ResolvedViewport& from,
const ResolvedViewport& to,
float t
) {
ResolvedViewport out;
out.centerXPx = lerpFloat(from.centerXPx, to.centerXPx, t);
out.centerYPx = lerpFloat(from.centerYPx, to.centerYPx, t);
out.widthPx = lerpFloat(from.widthPx, to.widthPx, t);
out.heightPx = lerpFloat(from.heightPx, to.heightPx, t);
out.rotationDeg = lerpFloat(from.rotationDeg, to.rotationDeg, t);
return out;
}
void DialogueOverlay::drawCutscene(Renderer& renderer, const PresentationModel& model) { void DialogueOverlay::drawCutscene(Renderer& renderer, const PresentationModel& model) {
const float W = Environment::projectionWidth; const float W = Environment::projectionWidth;
const float H = Environment::projectionHeight; const float H = Environment::projectionHeight;
const UiRect fullscreenRect{ 0.0f, 0.0f, W, H };
const UiRect subtitleRect{ W * 0.12f, 22.0f, W * 0.76f, 110.0f }; const UiRect subtitleRect{ W * 0.12f, 22.0f, W * 0.76f, 110.0f };
backgroundQuad.rebuild(fullscreenRect); lastDialogueAdvanceRect = {};
subtitleQuad.rebuild(subtitleRect); lastCutsceneAdvanceRect = subtitleRect;
cutsceneAdvanceEnabled = model.showCutsceneSubtitle;
std::shared_ptr<Texture> bgTexture = model.backgroundPath.empty() ? nullptr : loadTextureCached(model.backgroundPath);
glEnable(GL_BLEND); glEnable(GL_BLEND);
renderer.shaderManager.PushShader(defaultShaderName); renderer.shaderManager.PushShader(defaultShaderName);
@ -175,17 +325,84 @@ void DialogueOverlay::drawCutscene(Renderer& renderer, const PresentationModel&
renderer.PushMatrix(); renderer.PushMatrix();
renderer.LoadIdentity(); renderer.LoadIdentity();
if (!model.backgroundPath.empty()) { if (bgTexture) {
drawQuad(renderer, backgroundQuad, loadTextureCached(model.backgroundPath)); const float texW = static_cast<float>(bgTexture->getWidth());
const float texH = static_cast<float>(bgTexture->getHeight());
ResolvedViewport currentViewport{};
if (model.cutsceneCamera.active) {
const ResolvedViewport fromViewport = resolveViewportPose(model.cutsceneCamera.from, texW, texH, W, H);
const ResolvedViewport toViewport = resolveViewportPose(model.cutsceneCamera.to, texW, texH, W, H);
currentViewport = blendViewport(
fromViewport,
toViewport,
std::clamp(model.cutsceneCamera.t, 0.0f, 1.0f)
);
} }
else {
currentViewport = resolveViewportPose(CutsceneCameraPose{}, texW, texH, W, H);
}
const float halfW = currentViewport.widthPx * 0.5f;
const float halfH = currentViewport.heightPx * 0.5f;
const float rotationRad = currentViewport.rotationDeg * 3.14159265358979323846f / 180.0f;
const float c = std::cos(rotationRad);
const float s = std::sin(rotationRad);
auto rotatePoint = [&](float x, float y) -> Eigen::Vector2f {
return {
currentViewport.centerXPx + x * c - y * s,
currentViewport.centerYPx + x * s + y * c
};
};
// Source viewport corners in image pixel space (origin = bottom-left)
const Eigen::Vector2f srcBL = rotatePoint(-halfW, -halfH);
const Eigen::Vector2f srcTL = rotatePoint(-halfW, +halfH);
const Eigen::Vector2f srcTR = rotatePoint(+halfW, +halfH);
const Eigen::Vector2f srcBR = rotatePoint(+halfW, -halfH);
auto toUV = [&](const Eigen::Vector2f& p) -> Eigen::Vector2f {
return {
std::clamp(p.x() / max(texW, 1.0f), 0.0f, 1.0f),
std::clamp(p.y() / max(texH, 1.0f), 0.0f, 1.0f)
};
};
const UiRect screenRect{ 0.0f, 0.0f, W, H };
backgroundQuad.rebuildWithUV(
screenRect,
toUV(srcBL),
toUV(srcTL),
toUV(srcTR),
toUV(srcBR)
);
drawQuad(renderer, backgroundQuad, bgTexture);
}
if (model.showCutsceneSubtitle) {
subtitleQuad.rebuild(subtitleRect);
drawQuad(renderer, subtitleQuad, cutsceneSubtitleTexture); drawQuad(renderer, subtitleQuad, cutsceneSubtitleTexture);
}
renderer.PopMatrix(); renderer.PopMatrix();
renderer.PopProjectionMatrix(); renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader(); renderer.shaderManager.PopShader();
if (model.showCutsceneSubtitle) {
if (!model.speaker.empty()) { if (!model.speaker.empty()) {
nameRenderer->drawText(model.speaker, subtitleRect.x + 24.0f, subtitleRect.y + subtitleRect.h - 32.0f, 1.0f, false, { 1.0f, 0.88f, 0.45f, 1.0f }); nameRenderer->drawText(
model.speaker,
subtitleRect.x + 24.0f,
subtitleRect.y + subtitleRect.h - 32.0f,
1.0f,
false,
{ 1.0f, 0.88f, 0.45f, 1.0f }
);
} }
cutsceneRenderer->drawText( cutsceneRenderer->drawText(
wrapText(model.visibleText, 62), wrapText(model.visibleText, 62),
@ -195,22 +412,32 @@ void DialogueOverlay::drawCutscene(Renderer& renderer, const PresentationModel&
false, false,
{ 1.0f, 1.0f, 1.0f, 1.0f } { 1.0f, 1.0f, 1.0f, 1.0f }
); );
}
glDisable(GL_BLEND); glDisable(GL_BLEND);
} }
bool DialogueOverlay::handlePointerReleased(float x, float y, const PresentationModel& model, int& outChoiceIndex) const { bool DialogueOverlay::handlePointerReleased(float x, float y, const PresentationModel& model, int& outChoiceIndex) const {
outChoiceIndex = -1; outChoiceIndex = -1;
if (model.mode != PresentationMode::Choice) {
return false;
}
if (model.mode == PresentationMode::Choice) {
for (size_t i = 0; i < lastChoiceRects.size(); ++i) { for (size_t i = 0; i < lastChoiceRects.size(); ++i) {
if (lastChoiceRects[i].contains(x, y)) { if (lastChoiceRects[i].contains(x, y)) {
outChoiceIndex = static_cast<int>(i); outChoiceIndex = static_cast<int>(i);
return true; return true;
} }
} }
return lastDialogueAdvanceRect.contains(x, y);
}
if (model.mode == PresentationMode::Dialogue) {
return lastDialogueAdvanceRect.contains(x, y);
}
if (model.mode == PresentationMode::Cutscene) {
return cutsceneAdvanceEnabled && lastCutsceneAdvanceRect.contains(x, y);
}
return false; return false;
} }

View File

@ -18,6 +18,7 @@ public:
void draw(Renderer& renderer, const PresentationModel& model); void draw(Renderer& renderer, const PresentationModel& model);
// Coordinates are expected in the game's UI projection space // Coordinates are expected in the game's UI projection space
// Returns true only when the click should advance/select.
bool handlePointerReleased(float x, float y, const PresentationModel& model, int& outChoiceIndex) const; bool handlePointerReleased(float x, float y, const PresentationModel& model, int& outChoiceIndex) const;
private: private:
@ -27,6 +28,21 @@ private:
bool initialized = false; bool initialized = false;
void rebuild(const UiRect& newRect); void rebuild(const UiRect& newRect);
void rebuildWithUV(
const UiRect& newRect,
const Eigen::Vector2f& uvBottomLeft,
const Eigen::Vector2f& uvTopLeft,
const Eigen::Vector2f& uvTopRight,
const Eigen::Vector2f& uvBottomRight
);
};
struct ResolvedViewport {
float centerXPx = 0.0f;
float centerYPx = 0.0f;
float widthPx = 1.0f;
float heightPx = 1.0f;
float rotationDeg = 0.0f;
}; };
Renderer* rendererRef = nullptr; Renderer* rendererRef = nullptr;
@ -40,6 +56,9 @@ private:
std::shared_ptr<Texture> cutsceneSubtitleTexture; std::shared_ptr<Texture> cutsceneSubtitleTexture;
mutable std::vector<UiRect> lastChoiceRects; mutable std::vector<UiRect> lastChoiceRects;
mutable UiRect lastDialogueAdvanceRect{};
mutable UiRect lastCutsceneAdvanceRect{};
mutable bool cutsceneAdvanceEnabled = false;
std::unique_ptr<TextRenderer> nameRenderer; std::unique_ptr<TextRenderer> nameRenderer;
std::unique_ptr<TextRenderer> bodyRenderer; std::unique_ptr<TextRenderer> bodyRenderer;
@ -61,6 +80,20 @@ private:
void drawQuad(Renderer& renderer, const TexturedQuad& quad, const std::shared_ptr<Texture>& texture) const; void drawQuad(Renderer& renderer, const TexturedQuad& quad, const std::shared_ptr<Texture>& texture) const;
static std::string wrapText(const std::string& input, size_t maxLineLength); static std::string wrapText(const std::string& input, size_t maxLineLength);
static float lerpFloat(float a, float b, float t);
static ResolvedViewport resolveViewportPose(
const CutsceneCameraPose& pose,
float texW,
float texH,
float screenW,
float screenH
);
static ResolvedViewport blendViewport(
const ResolvedViewport& from,
const ResolvedViewport& to,
float t
);
}; };
} // namespace ZL::Dialogue } // namespace ZL::Dialogue

View File

@ -1,6 +1,7 @@
#include "dialogue/DialogueRuntime.h" #include "dialogue/DialogueRuntime.h"
#include <algorithm> #include <algorithm>
#include <cmath>
#include <iostream> #include <iostream>
namespace ZL::Dialogue { namespace ZL::Dialogue {
@ -30,6 +31,8 @@ bool DialogueRuntime::startDialogue(const std::string& dialogueId) {
revealCharacters = 0.0f; revealCharacters = 0.0f;
currentCutsceneLine = -1; currentCutsceneLine = -1;
cutsceneTimerMs = 0; cutsceneTimerMs = 0;
cutsceneElapsedMs = 0;
cutsceneTotalDurationMs = 0;
presentation = {}; presentation = {};
presentation.dialogueId = dialogue->id; presentation.dialogueId = dialogue->id;
@ -46,6 +49,8 @@ void DialogueRuntime::stop() {
revealCharacters = 0.0f; revealCharacters = 0.0f;
currentCutsceneLine = -1; currentCutsceneLine = -1;
cutsceneTimerMs = 0; cutsceneTimerMs = 0;
cutsceneElapsedMs = 0;
cutsceneTotalDurationMs = 0;
mode = Mode::Inactive; mode = Mode::Inactive;
presentation = {}; presentation = {};
} }
@ -63,20 +68,59 @@ void DialogueRuntime::update(int deltaMs) {
} }
if (mode == Mode::PlayingCutscene && activeCutscene) { if (mode == Mode::PlayingCutscene && activeCutscene) {
if (currentCutsceneLine < 0 || currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size())) { cutsceneElapsedMs += deltaMs;
advanceCutsceneLine();
return; if (!activeCutscene->lines.empty() &&
} currentCutsceneLine >= 0 &&
currentCutsceneLine < static_cast<int>(activeCutscene->lines.size())) {
const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine]; const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
if (line.waitForConfirm) { 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; return;
} }
cutsceneTimerMs += deltaMs; refreshCutscenePresentation();
const int durationMs = (line.durationMs > 0) ? line.durationMs : computeFallbackCutsceneDurationMs(line.text);
if (cutsceneTimerMs >= durationMs) { if (!activeCutscene || mode != Mode::PlayingCutscene) {
advanceCutsceneLine(); 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) {
finishCutscene();
}
return;
}
if (subtitlesFinished && (cutsceneTotalDurationMs <= 0 || durationFinished)) {
finishCutscene();
return;
} }
} }
} }
@ -125,8 +169,14 @@ void DialogueRuntime::confirmAdvance() {
} }
if (mode == Mode::PlayingCutscene) { if (mode == Mode::PlayingCutscene) {
if (!activeCutscene || activeCutscene->lines.empty()) {
return;
}
if (currentCutsceneLine >= 0 && currentCutsceneLine < static_cast<int>(activeCutscene->lines.size())) {
advanceCutsceneLine(); advanceCutsceneLine();
} }
}
} }
void DialogueRuntime::moveSelection(int delta) { void DialogueRuntime::moveSelection(int delta) {
@ -261,6 +311,8 @@ void DialogueRuntime::presentLine(const Node& node) {
presentation.choices.clear(); presentation.choices.clear();
presentation.selectedChoice = 0; presentation.selectedChoice = 0;
presentation.revealCompleted = node.text.empty(); presentation.revealCompleted = node.text.empty();
presentation.showCutsceneSubtitle = false;
presentation.cutsceneCamera = {};
if (presentation.revealCompleted) { if (presentation.revealCompleted) {
presentation.visibleText = node.text; presentation.visibleText = node.text;
@ -303,6 +355,8 @@ void DialogueRuntime::presentChoices(const Node& node) {
presentation.backgroundPath.clear(); presentation.backgroundPath.clear();
presentation.selectedChoice = 0; presentation.selectedChoice = 0;
presentation.revealCompleted = true; presentation.revealCompleted = true;
presentation.showCutsceneSubtitle = false;
presentation.cutsceneCamera = {};
} }
void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::string& nextNodeAfterCutscene) { void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::string& nextNodeAfterCutscene) {
@ -326,22 +380,29 @@ void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::st
activeCutscene = cutscene; activeCutscene = cutscene;
pendingNodeAfterCutscene = nextNodeAfterCutscene; pendingNodeAfterCutscene = nextNodeAfterCutscene;
mode = Mode::PlayingCutscene; mode = Mode::PlayingCutscene;
currentCutsceneLine = -1; cutsceneElapsedMs = 0;
cutsceneTimerMs = 0; cutsceneTimerMs = 0;
advanceCutsceneLine(); 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::advanceCutsceneLine() { void DialogueRuntime::finishCutscene() {
if (!activeCutscene) {
stop();
return;
}
++currentCutsceneLine; std::cout << "[CUTSCENE] finish nextNode=" << pendingNodeAfterCutscene << std::endl;
cutsceneTimerMs = 0;
if (currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size())) {
activeCutscene = nullptr; activeCutscene = nullptr;
currentCutsceneLine = -1;
cutsceneTimerMs = 0;
cutsceneElapsedMs = 0;
cutsceneTotalDurationMs = 0;
if (!pendingNodeAfterCutscene.empty()) { if (!pendingNodeAfterCutscene.empty()) {
const std::string nextNode = pendingNodeAfterCutscene; const std::string nextNode = pendingNodeAfterCutscene;
pendingNodeAfterCutscene.clear(); pendingNodeAfterCutscene.clear();
@ -350,28 +411,131 @@ void DialogueRuntime::advanceCutsceneLine() {
else { else {
stop(); stop();
} }
}
void DialogueRuntime::advanceCutsceneLine() {
if (!activeCutscene) {
stop();
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())) {
refreshCutscenePresentation();
if (cutsceneTotalDurationMs <= 0 || cutsceneElapsedMs >= cutsceneTotalDurationMs) {
finishCutscene();
}
return; return;
} }
refreshCutscenePresentation(); refreshCutscenePresentation();
} }
CutsceneCameraBlendState DialogueRuntime::evaluateCutsceneCameraBlend() const {
CutsceneCameraBlendState result;
result.active = false;
result.from = {};
result.to = {};
result.t = 1.0f;
if (!activeCutscene || activeCutscene->cameraTrack.empty()) {
return result;
}
int elapsed = cutsceneElapsedMs;
for (const CutsceneCameraSegment& segment : activeCutscene->cameraTrack) {
const int durationMs = std::max(segment.durationMs, 1);
if (elapsed <= durationMs) {
result.active = true;
result.from = segment.from;
result.to = segment.to;
result.t = applyEasing(
segment.easing,
std::clamp(static_cast<float>(elapsed) / static_cast<float>(durationMs), 0.0f, 1.0f)
);
return result;
}
elapsed -= durationMs;
}
result.active = true;
result.from = activeCutscene->cameraTrack.back().to;
result.to = activeCutscene->cameraTrack.back().to;
result.t = 1.0f;
return result;
}
void DialogueRuntime::refreshCutscenePresentation() { void DialogueRuntime::refreshCutscenePresentation() {
if (!activeCutscene || currentCutsceneLine < 0 || if (!activeCutscene) {
currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size())) { return;
}
presentation.mode = PresentationMode::Cutscene;
presentation.backgroundPath = activeCutscene->background;
presentation.cutsceneCamera = evaluateCutsceneCameraBlend();
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; return;
} }
const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine]; const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
presentation.mode = PresentationMode::Cutscene;
presentation.speaker = line.speaker; presentation.speaker = line.speaker;
presentation.fullText = line.text; presentation.fullText = line.text;
presentation.visibleText = line.text; presentation.visibleText = line.text;
presentation.portraitPath = line.portrait; presentation.portraitPath = line.portrait;
presentation.backgroundPath = activeCutscene->background;
presentation.choices.clear(); std::cout << "[CUTSCENE] lines=" << activeCutscene->lines.size()
presentation.selectedChoice = 0; << " current=" << currentCutsceneLine
presentation.revealCompleted = true; << 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) { int DialogueRuntime::computeFallbackCutsceneDurationMs(const std::string& text) {
@ -382,6 +546,14 @@ int DialogueRuntime::computeFallbackCutsceneDurationMs(const std::string& text)
return std::max(minDuration, calculated + linger); 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 { DialogueRuntime::json DialogueRuntime::buildSaveState() const {
json result; json result;
result["active"] = isActive(); result["active"] = isActive();

View File

@ -63,6 +63,8 @@ private:
int currentCutsceneLine = -1; int currentCutsceneLine = -1;
int cutsceneTimerMs = 0; int cutsceneTimerMs = 0;
int cutsceneElapsedMs = 0;
int cutsceneTotalDurationMs = 0;
bool evaluateConditions(const std::vector<Condition>& conditions) const; bool evaluateConditions(const std::vector<Condition>& conditions) const;
void applyEffects(const std::vector<Effect>& effects); void applyEffects(const std::vector<Effect>& effects);
@ -71,11 +73,15 @@ private:
void presentLine(const Node& node); void presentLine(const Node& node);
void presentChoices(const Node& node); void presentChoices(const Node& node);
void startCutscene(const std::string& cutsceneId, const std::string& nextNodeAfterCutscene); void startCutscene(const std::string& cutsceneId, const std::string& nextNodeAfterCutscene);
void finishCutscene();
void advanceCutsceneLine(); void advanceCutsceneLine();
void refreshCutscenePresentation(); void refreshCutscenePresentation();
CutsceneCameraBlendState evaluateCutsceneCameraBlend() const;
static float applyEasing(EasingType easing, float t);
static int computeFallbackCutsceneDurationMs(const std::string& text); static int computeFallbackCutsceneDurationMs(const std::string& text);
static int computeCameraTrackDurationMs(const StaticCutsceneDefinition& cutscene);
}; };
} // namespace ZL::Dialogue } // namespace ZL::Dialogue

View File

@ -73,12 +73,14 @@ bool DialogueSystem::handlePointerReleased(float x, float y) {
int choiceIndex = -1; int choiceIndex = -1;
const PresentationModel& model = runtime.getPresentation(); const PresentationModel& model = runtime.getPresentation();
if (overlay.handlePointerReleased(x, y, model, choiceIndex)) { if (!overlay.handlePointerReleased(x, y, model, choiceIndex)) {
while (model.selectedChoice != choiceIndex) { return false;
}
if (choiceIndex >= 0) {
while (runtime.getPresentation().selectedChoice != choiceIndex) {
runtime.moveSelection(1); runtime.moveSelection(1);
} }
runtime.confirmAdvance();
return true;
} }
runtime.confirmAdvance(); runtime.confirmAdvance();

View File

@ -31,6 +31,28 @@ enum class ComparisonOp {
LessOrEqual LessOrEqual
}; };
enum class EasingType {
Linear,
EaseInSine,
EaseOutSine,
EaseInOutSine,
EaseInQuad,
EaseOutQuad,
EaseInOutQuad,
EaseInCubic,
EaseOutCubic,
EaseInOutCubic
};
enum class CutsceneAnchor {
Center,
TopLeft,
TopRight,
BottomRight,
BottomLeft,
Custom
};
struct Condition { struct Condition {
std::string flag; std::string flag;
ComparisonOp op = ComparisonOp::Exists; ComparisonOp op = ComparisonOp::Exists;
@ -91,11 +113,34 @@ struct CutsceneLine {
bool waitForConfirm = false; bool waitForConfirm = false;
}; };
struct CutsceneCameraPose {
CutsceneAnchor anchor = CutsceneAnchor::Center;
// Используется только для Custom.
// Нормализованные координаты 0..1, где:
// centerX: 0 = левый край, 1 = правый край
// centerY: 0 = верхний край, 1 = нижний край
float centerX = 0.5f;
float centerY = 0.5f;
float zoom = 1.0f;
float rotationDeg = 0.0f;
};
struct CutsceneCameraSegment {
int durationMs = 0;
CutsceneCameraPose from;
CutsceneCameraPose to;
EasingType easing = EasingType::EaseInOutSine;
};
struct StaticCutsceneDefinition { struct StaticCutsceneDefinition {
std::string id; std::string id;
std::string background; std::string background;
std::string music; std::string music;
bool skippable = true; bool skippable = true;
int durationMs = 0;
std::vector<CutsceneCameraSegment> cameraTrack;
std::vector<CutsceneLine> lines; std::vector<CutsceneLine> lines;
}; };
@ -112,6 +157,13 @@ enum class PresentationMode {
Cutscene Cutscene
}; };
struct CutsceneCameraBlendState {
bool active = false;
CutsceneCameraPose from;
CutsceneCameraPose to;
float t = 1.0f;
};
struct PresentationModel { struct PresentationModel {
PresentationMode mode = PresentationMode::Hidden; PresentationMode mode = PresentationMode::Hidden;
std::string dialogueId; std::string dialogueId;
@ -123,6 +175,9 @@ struct PresentationModel {
std::vector<PresentedChoice> choices; std::vector<PresentedChoice> choices;
int selectedChoice = 0; int selectedChoice = 0;
bool revealCompleted = true; bool revealCompleted = true;
bool showCutsceneSubtitle = false;
CutsceneCameraBlendState cutsceneCamera;
}; };
struct SaveState { struct SaveState {