Merge
This commit is contained in:
commit
24851a89e8
@ -42,7 +42,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"id": "test_choice_dialogue",
|
"id": "test_choice_dialogue",
|
||||||
"start": "line_1",
|
"start": "line_1",
|
||||||
@ -59,7 +58,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": [
|
||||||
{
|
{
|
||||||
@ -90,7 +89,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",
|
||||||
@ -98,7 +97,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"id": "test_condition_dialogue",
|
"id": "test_condition_dialogue",
|
||||||
"start": "set_flag_1",
|
"start": "set_flag_1",
|
||||||
@ -142,7 +140,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"id": "test_cutscene_dialogue",
|
"id": "test_cutscene_dialogue",
|
||||||
"start": "cutscene_start",
|
"start": "cutscene_start",
|
||||||
@ -158,17 +155,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
|
||||||
},
|
},
|
||||||
@ -180,6 +245,121 @@
|
|||||||
"background": "resources/loading.png"
|
"background": "resources/loading.png"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"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
BIN
resources/hero.png
(Stored with Git LFS)
Normal file
Binary file not shown.
16
src/Game.cpp
16
src/Game.cpp
@ -773,7 +773,6 @@ namespace ZL
|
|||||||
|
|
||||||
lastTickCount = newTickCount;
|
lastTickCount = newTickCount;
|
||||||
|
|
||||||
//if (player) player->update(delta);
|
|
||||||
if (player) {
|
if (player) {
|
||||||
player->update(delta);
|
player->update(delta);
|
||||||
dialogueSystem.update(static_cast<int>(delta), player->position);
|
dialogueSystem.update(static_cast<int>(delta), player->position);
|
||||||
@ -808,18 +807,7 @@ namespace ZL
|
|||||||
|
|
||||||
targetInteractiveObject = nullptr;
|
targetInteractiveObject = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
//for (auto& npc : npcs) npc->update(delta);
|
|
||||||
//if (player) {
|
|
||||||
// dialogueSystem.update(static_cast<int>(delta), player->position);
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some AI stuff
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1054,11 +1042,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_p:
|
case SDLK_p:
|
||||||
|
|||||||
@ -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", "");
|
||||||
@ -127,12 +149,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"]) {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,42 +325,119 @@ 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.speaker.empty()) {
|
if (model.showCutsceneSubtitle) {
|
||||||
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 });
|
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
cutsceneRenderer->drawText(
|
||||||
|
wrapText(model.visibleText, 62),
|
||||||
|
subtitleRect.x + 24.0f,
|
||||||
|
subtitleRect.y + 30.0f,
|
||||||
|
1.0f,
|
||||||
|
false,
|
||||||
|
{ 1.0f, 1.0f, 1.0f, 1.0f }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
cutsceneRenderer->drawText(
|
|
||||||
wrapText(model.visibleText, 62),
|
|
||||||
subtitleRect.x + 24.0f,
|
|
||||||
subtitleRect.y + 30.0f,
|
|
||||||
1.0f,
|
|
||||||
false,
|
|
||||||
{ 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) {
|
||||||
|
if (lastChoiceRects[i].contains(x, y)) {
|
||||||
|
outChoiceIndex = static_cast<int>(i);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastDialogueAdvanceRect.contains(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.mode == PresentationMode::Dialogue) {
|
||||||
|
return lastDialogueAdvanceRect.contains(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (size_t i = 0; i < lastChoiceRects.size(); ++i) {
|
if (model.mode == PresentationMode::Cutscene) {
|
||||||
if (lastChoiceRects[i].contains(x, y)) {
|
return cutsceneAdvanceEnabled && lastCutsceneAdvanceRect.contains(x, y);
|
||||||
outChoiceIndex = static_cast<int>(i);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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();
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
|
refreshCutscenePresentation();
|
||||||
if (line.waitForConfirm) {
|
|
||||||
|
if (!activeCutscene || mode != Mode::PlayingCutscene) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cutsceneTimerMs += deltaMs;
|
const bool subtitlesFinished =
|
||||||
const int durationMs = (line.durationMs > 0) ? line.durationMs : computeFallbackCutsceneDurationMs(line.text);
|
activeCutscene->lines.empty() ||
|
||||||
if (cutsceneTimerMs >= durationMs) {
|
currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size());
|
||||||
advanceCutsceneLine();
|
|
||||||
|
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) {
|
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.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) {
|
||||||
@ -327,9 +381,37 @@ void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::st
|
|||||||
pendingNodeAfterCutscene = nextNodeAfterCutscene;
|
pendingNodeAfterCutscene = nextNodeAfterCutscene;
|
||||||
currentCutsceneBackground = cutscene->background;
|
currentCutsceneBackground = cutscene->background;
|
||||||
mode = Mode::PlayingCutscene;
|
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;
|
currentCutsceneLine = -1;
|
||||||
cutsceneTimerMs = 0;
|
cutsceneTimerMs = 0;
|
||||||
advanceCutsceneLine();
|
cutsceneElapsedMs = 0;
|
||||||
|
cutsceneTotalDurationMs = 0;
|
||||||
|
if (!pendingNodeAfterCutscene.empty()) {
|
||||||
|
const std::string nextNode = pendingNodeAfterCutscene;
|
||||||
|
pendingNodeAfterCutscene.clear();
|
||||||
|
enterNode(nextNode);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueRuntime::advanceCutsceneLine() {
|
void DialogueRuntime::advanceCutsceneLine() {
|
||||||
@ -338,18 +420,19 @@ void DialogueRuntime::advanceCutsceneLine() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (activeCutscene->lines.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "[CUTSCENE] advance before current=" << currentCutsceneLine << std::endl;
|
||||||
++currentCutsceneLine;
|
++currentCutsceneLine;
|
||||||
|
std::cout << "[CUTSCENE] advance after current=" << currentCutsceneLine << std::endl;
|
||||||
cutsceneTimerMs = 0;
|
cutsceneTimerMs = 0;
|
||||||
|
|
||||||
if (currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size())) {
|
if (currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size())) {
|
||||||
activeCutscene = nullptr;
|
refreshCutscenePresentation();
|
||||||
if (!pendingNodeAfterCutscene.empty()) {
|
if (cutsceneTotalDurationMs <= 0 || cutsceneElapsedMs >= cutsceneTotalDurationMs) {
|
||||||
const std::string nextNode = pendingNodeAfterCutscene;
|
finishCutscene();
|
||||||
pendingNodeAfterCutscene.clear();
|
|
||||||
enterNode(nextNode);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
stop();
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -357,28 +440,120 @@ void DialogueRuntime::advanceCutsceneLine() {
|
|||||||
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];
|
||||||
|
/*<<<<<<< HEAD
|
||||||
|
|
||||||
if (!line.background.empty()) {
|
if (!line.background.empty()) {
|
||||||
currentCutsceneBackground = line.background;
|
currentCutsceneBackground = line.background;
|
||||||
}
|
}
|
||||||
|
|
||||||
presentation.mode = PresentationMode::Cutscene;
|
presentation.mode = PresentationMode::Cutscene;
|
||||||
|
=======
|
||||||
|
>>>>>>> witcher001-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;
|
||||||
|
/*<<<<<<< HEAD
|
||||||
//presentation.backgroundPath = activeCutscene->background;
|
//presentation.backgroundPath = activeCutscene->background;
|
||||||
presentation.backgroundPath = currentCutsceneBackground;
|
presentation.backgroundPath = currentCutsceneBackground;
|
||||||
presentation.choices.clear();
|
presentation.choices.clear();
|
||||||
presentation.selectedChoice = 0;
|
presentation.selectedChoice = 0;
|
||||||
presentation.revealCompleted = true;
|
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;
|
||||||
|
}
|
||||||
|
//>>>>>>> witcher001-cutscene
|
||||||
}
|
}
|
||||||
|
|
||||||
int DialogueRuntime::computeFallbackCutsceneDurationMs(const std::string& text) {
|
int DialogueRuntime::computeFallbackCutsceneDurationMs(const std::string& text) {
|
||||||
@ -389,6 +564,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();
|
||||||
|
|||||||
@ -63,6 +63,8 @@ private:
|
|||||||
|
|
||||||
int currentCutsceneLine = -1;
|
int currentCutsceneLine = -1;
|
||||||
int cutsceneTimerMs = 0;
|
int cutsceneTimerMs = 0;
|
||||||
|
int cutsceneElapsedMs = 0;
|
||||||
|
int cutsceneTotalDurationMs = 0;
|
||||||
|
|
||||||
std::string currentCutsceneBackground;
|
std::string currentCutsceneBackground;
|
||||||
|
|
||||||
@ -73,11 +75,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
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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;
|
||||||
@ -92,11 +114,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;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -113,6 +158,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;
|
||||||
@ -124,6 +176,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 {
|
||||||
@ -138,4 +193,4 @@ struct SaveState {
|
|||||||
bool active = false;
|
bool active = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZL::Dialogue
|
} // namespace ZL::Dialogue
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user