Refactoring cutscenes
This commit is contained in:
parent
7011bbb754
commit
2e5988f8f4
@ -129,6 +129,14 @@ set(SOURCES
|
|||||||
../src/quest/QuestTypes.h
|
../src/quest/QuestTypes.h
|
||||||
../src/quest/QuestJournal.h
|
../src/quest/QuestJournal.h
|
||||||
../src/quest/QuestJournal.cpp
|
../src/quest/QuestJournal.cpp
|
||||||
|
../src/cutscene/CutsceneTypes.h
|
||||||
|
../src/cutscene/CutsceneDatabase.h
|
||||||
|
../src/cutscene/CutsceneDatabase.cpp
|
||||||
|
../src/cutscene/CutsceneRuntime.h
|
||||||
|
../src/cutscene/CutsceneRuntime.cpp
|
||||||
|
../src/cutscene/CutsceneOverlay.h
|
||||||
|
../src/cutscene/CutsceneOverlay.cpp
|
||||||
|
../src/render/UiQuad.h
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(bishkek-witcher ${SOURCES})
|
add_executable(bishkek-witcher ${SOURCES})
|
||||||
|
|||||||
@ -84,6 +84,14 @@ add_executable(witcher001
|
|||||||
../src/quest/QuestTypes.h
|
../src/quest/QuestTypes.h
|
||||||
../src/quest/QuestJournal.h
|
../src/quest/QuestJournal.h
|
||||||
../src/quest/QuestJournal.cpp
|
../src/quest/QuestJournal.cpp
|
||||||
|
../src/cutscene/CutsceneTypes.h
|
||||||
|
../src/cutscene/CutsceneDatabase.h
|
||||||
|
../src/cutscene/CutsceneDatabase.cpp
|
||||||
|
../src/cutscene/CutsceneRuntime.h
|
||||||
|
../src/cutscene/CutsceneRuntime.cpp
|
||||||
|
../src/cutscene/CutsceneOverlay.h
|
||||||
|
../src/cutscene/CutsceneOverlay.cpp
|
||||||
|
../src/render/UiQuad.h
|
||||||
)
|
)
|
||||||
|
|
||||||
# Установка проекта по умолчанию для Visual Studio
|
# Установка проекта по умолчанию для Visual Studio
|
||||||
|
|||||||
@ -130,6 +130,7 @@ namespace ZL
|
|||||||
|
|
||||||
dialogueSystem.init(renderer, CONST_ZIP_FILE);
|
dialogueSystem.init(renderer, CONST_ZIP_FILE);
|
||||||
dialogueSystem.loadDatabase(params.dialoguesJsonPath);
|
dialogueSystem.loadDatabase(params.dialoguesJsonPath);
|
||||||
|
dialogueSystem.loadCutsceneDatabase(params.dialoguesJsonPath);
|
||||||
dialogueSystem.setQuestJournal(journal);
|
dialogueSystem.setQuestJournal(journal);
|
||||||
|
|
||||||
npcNameText = std::make_unique<TextRenderer>();
|
npcNameText = std::make_unique<TextRenderer>();
|
||||||
|
|||||||
172
src/cutscene/CutsceneDatabase.cpp
Normal file
172
src/cutscene/CutsceneDatabase.cpp
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
#include "cutscene/CutsceneDatabase.h"
|
||||||
|
|
||||||
|
#include "utils/Utils.h"
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace ZL
|
||||||
|
{
|
||||||
|
extern const char* CONST_ZIP_FILE;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace ZL::Cutscene {
|
||||||
|
|
||||||
|
EasingType CutsceneDatabase::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 CutsceneDatabase::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;
|
||||||
|
}
|
||||||
|
|
||||||
|
CutsceneLine CutsceneDatabase::parseCutsceneLine(const json& j) {
|
||||||
|
CutsceneLine line;
|
||||||
|
line.speaker = j.value("speaker", "");
|
||||||
|
line.text = j.value("text", "");
|
||||||
|
line.portrait = j.value("portrait", "");
|
||||||
|
line.sfx = j.value("sfx", "");
|
||||||
|
line.background = j.value("background", "");
|
||||||
|
line.backgroundWidth = j.value("backgroundWidth", 0);
|
||||||
|
line.backgroundHeight= j.value("backgroundHeight", 0);
|
||||||
|
line.luaCallback = j.value("luaCallback", "");
|
||||||
|
line.durationMs = j.value("durationMs", 0);
|
||||||
|
line.waitForConfirm = j.value("waitForConfirm", false);
|
||||||
|
line.questUnlock = j.value("questUnlock", "");
|
||||||
|
line.questComplete = j.value("questComplete", "");
|
||||||
|
line.questFail = j.value("questFail", "");
|
||||||
|
line.objectiveComplete = j.value("objectiveComplete", "");
|
||||||
|
line.objectiveVisible = j.value("objectiveVisible", "");
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
CutsceneCameraPose CutsceneDatabase::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 CutsceneDatabase::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;
|
||||||
|
}
|
||||||
|
|
||||||
|
CutsceneImageCue CutsceneDatabase::parseCutsceneImageCue(const json& j) {
|
||||||
|
CutsceneImageCue cue;
|
||||||
|
cue.path = j.value("path", "");
|
||||||
|
cue.startMs = j.value("startMs", 0);
|
||||||
|
cue.endMs = j.value("endMs", 0);
|
||||||
|
cue.fadeInMs = j.value("fadeInMs", 0);
|
||||||
|
cue.fadeOutMs = j.value("fadeOutMs", 0);
|
||||||
|
return cue;
|
||||||
|
}
|
||||||
|
|
||||||
|
StaticCutsceneDefinition CutsceneDatabase::parseCutscene(const json& j) {
|
||||||
|
StaticCutsceneDefinition cutscene;
|
||||||
|
cutscene.id = j.value("id", "");
|
||||||
|
cutscene.background = j.value("background", "");
|
||||||
|
cutscene.backgroundWidth = j.value("backgroundWidth", 1280);
|
||||||
|
cutscene.backgroundHeight= j.value("backgroundHeight", 720);
|
||||||
|
cutscene.music = j.value("music", "");
|
||||||
|
cutscene.skippable = j.value("skippable", true);
|
||||||
|
cutscene.durationMs = j.value("durationMs", 0);
|
||||||
|
cutscene.fadeOutMs = j.value("fadeOutMs", 0);
|
||||||
|
cutscene.fadeInMs = j.value("fadeInMs", 0);
|
||||||
|
cutscene.endFadeOutMs = j.value("endFadeOutMs", 0);
|
||||||
|
cutscene.endFadeInMs = j.value("endFadeInMs", 0);
|
||||||
|
cutscene.onFadeInCallback= j.value("onFadeInCallback", "");
|
||||||
|
|
||||||
|
if (j.contains("cameraTrack") && j["cameraTrack"].is_array()) {
|
||||||
|
for (const auto& item : j["cameraTrack"]) {
|
||||||
|
cutscene.cameraTrack.push_back(parseCutsceneCameraSegment(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (j.contains("images") && j["images"].is_array()) {
|
||||||
|
for (const auto& item : j["images"]) {
|
||||||
|
cutscene.images.push_back(parseCutsceneImageCue(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (j.contains("lines") && j["lines"].is_array()) {
|
||||||
|
for (const auto& item : j["lines"]) {
|
||||||
|
cutscene.lines.push_back(parseCutsceneLine(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cutscene;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CutsceneDatabase::loadFromFile(const std::string& path) {
|
||||||
|
cutscenes.clear();
|
||||||
|
|
||||||
|
std::string raw;
|
||||||
|
try {
|
||||||
|
if (strlen(ZL::CONST_ZIP_FILE) == 0) {
|
||||||
|
raw = readTextFile(path);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
auto buf = readFileFromZIP(path, ZL::CONST_ZIP_FILE);
|
||||||
|
if (buf.empty()) {
|
||||||
|
std::cerr << "[cutscene] Failed to read " << path << " from zip\n";
|
||||||
|
throw std::runtime_error("Failed to load cutscene file: " + path);
|
||||||
|
}
|
||||||
|
raw.assign(buf.begin(), buf.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const std::exception& e) {
|
||||||
|
std::cerr << "[cutscene] Failed to open " << path << ": " << e.what() << "\n";
|
||||||
|
throw std::runtime_error("Failed to load cutscene file: " + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
json root;
|
||||||
|
try {
|
||||||
|
root = json::parse(raw);
|
||||||
|
}
|
||||||
|
catch (const std::exception& e) {
|
||||||
|
std::cerr << "[cutscene] JSON parse error in " << path << ": " << e.what() << "\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.contains("cutscenes") && root["cutscenes"].is_array()) {
|
||||||
|
for (const auto& item : root["cutscenes"]) {
|
||||||
|
StaticCutsceneDefinition cutscene = parseCutscene(item);
|
||||||
|
if (!cutscene.id.empty()) {
|
||||||
|
cutscenes[cutscene.id] = std::move(cutscene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StaticCutsceneDefinition* CutsceneDatabase::findCutscene(const std::string& id) const {
|
||||||
|
auto it = cutscenes.find(id);
|
||||||
|
return (it != cutscenes.end()) ? &it->second : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ZL::Cutscene
|
||||||
31
src/cutscene/CutsceneDatabase.h
Normal file
31
src/cutscene/CutsceneDatabase.h
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "cutscene/CutsceneTypes.h"
|
||||||
|
#include "external/nlohmann/json.hpp"
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace ZL::Cutscene {
|
||||||
|
|
||||||
|
class CutsceneDatabase {
|
||||||
|
public:
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
bool loadFromFile(const std::string& path);
|
||||||
|
|
||||||
|
const StaticCutsceneDefinition* findCutscene(const std::string& id) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<std::string, StaticCutsceneDefinition> cutscenes;
|
||||||
|
|
||||||
|
static EasingType parseEasingType(const std::string& value);
|
||||||
|
static CutsceneAnchor parseCutsceneAnchor(const std::string& value);
|
||||||
|
|
||||||
|
static CutsceneLine parseCutsceneLine(const json& j);
|
||||||
|
static CutsceneCameraPose parseCutsceneCameraPose(const json& j);
|
||||||
|
static CutsceneCameraSegment parseCutsceneCameraSegment(const json& j);
|
||||||
|
static CutsceneImageCue parseCutsceneImageCue(const json& j);
|
||||||
|
static StaticCutsceneDefinition parseCutscene(const json& j);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZL::Cutscene
|
||||||
410
src/cutscene/CutsceneOverlay.cpp
Normal file
410
src/cutscene/CutsceneOverlay.cpp
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
#include "cutscene/CutsceneOverlay.h"
|
||||||
|
#include "dialogue/DialogueTypes.h"
|
||||||
|
|
||||||
|
#include "Environment.h"
|
||||||
|
#include "GameConstants.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace ZL::Cutscene {
|
||||||
|
|
||||||
|
bool CutsceneOverlay::init(Renderer& renderer, const std::string& zipFile) {
|
||||||
|
rendererRef = &renderer;
|
||||||
|
zipFilename = zipFile;
|
||||||
|
|
||||||
|
choiceMainTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_main.png", zipFile);
|
||||||
|
choiceOptionalTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_optional.png", zipFile);
|
||||||
|
cutsceneSubtitleTexture = renderer.textureManager.LoadFromPng("resources/dialogue/cutscene_subtitle_bg.png", zipFile);
|
||||||
|
|
||||||
|
nameRenderer = std::make_unique<TextRenderer>();
|
||||||
|
cutsceneRenderer= std::make_unique<TextRenderer>();
|
||||||
|
choiceRenderer = std::make_unique<TextRenderer>();
|
||||||
|
|
||||||
|
return
|
||||||
|
nameRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 28, zipFile) &&
|
||||||
|
cutsceneRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 24, zipFile) &&
|
||||||
|
choiceRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 22, zipFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutsceneOverlay::update(const ZL::Dialogue::PresentationModel& model, int deltaMs) {
|
||||||
|
if (model.mode != ZL::Dialogue::PresentationMode::Cutscene || !model.cutsceneSkippable) {
|
||||||
|
cutsceneSkipHintVisible = false;
|
||||||
|
cutsceneSkipArmed = false;
|
||||||
|
cutsceneSkipHolding = false;
|
||||||
|
cutsceneSkipTriggered = false;
|
||||||
|
cutsceneSkipHintRemainingMs = 0;
|
||||||
|
cutsceneSkipHoldElapsedMs = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int safeDeltaMs = max(deltaMs, 0);
|
||||||
|
|
||||||
|
if (cutsceneSkipHintVisible) {
|
||||||
|
cutsceneSkipHintRemainingMs -= safeDeltaMs;
|
||||||
|
if (cutsceneSkipHintRemainingMs <= 0) {
|
||||||
|
cutsceneSkipHintVisible = false;
|
||||||
|
cutsceneSkipArmed = false;
|
||||||
|
cutsceneSkipHolding = false;
|
||||||
|
cutsceneSkipHintRemainingMs = 0;
|
||||||
|
cutsceneSkipHoldElapsedMs = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cutsceneSkipHolding && cutsceneSkipArmed) {
|
||||||
|
cutsceneSkipHoldElapsedMs += safeDeltaMs;
|
||||||
|
if (cutsceneSkipHoldElapsedMs >= CutsceneSkipHoldDurationMs) {
|
||||||
|
cutsceneSkipTriggered = true;
|
||||||
|
cutsceneSkipHolding = false;
|
||||||
|
cutsceneSkipHoldElapsedMs = CutsceneSkipHoldDurationMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutsceneOverlay::draw(Renderer& renderer, const ZL::Dialogue::PresentationModel& model) {
|
||||||
|
if (model.mode != ZL::Dialogue::PresentationMode::Cutscene) return;
|
||||||
|
|
||||||
|
const float W = Environment::projectionWidth;
|
||||||
|
const float H = Environment::projectionHeight;
|
||||||
|
const UiRect subtitleRect{ W * 0.12f, 22.0f, W * 0.76f, 110.0f };
|
||||||
|
|
||||||
|
lastCutsceneAdvanceRect = subtitleRect;
|
||||||
|
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
|
||||||
|
renderer.shaderManager.PushShader("cutsceneFade");
|
||||||
|
renderer.RenderUniform1i(textureUniformName, 0);
|
||||||
|
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
|
||||||
|
renderer.PushMatrix();
|
||||||
|
renderer.LoadIdentity();
|
||||||
|
|
||||||
|
const UiRect screenRect{ 0.0f, 0.0f, W, H };
|
||||||
|
|
||||||
|
std::vector<ZL::Cutscene::PresentedCutsceneImage> imageLayers = model.cutsceneImages;
|
||||||
|
if (imageLayers.empty() && !model.backgroundPath.empty()) {
|
||||||
|
imageLayers.push_back({ model.backgroundPath, 1.0f });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ZL::Cutscene::PresentedCutsceneImage& layer : imageLayers) {
|
||||||
|
const auto texture = loadTextureCached(layer.path);
|
||||||
|
if (!texture) continue;
|
||||||
|
|
||||||
|
const float imgW = (model.backgroundWidth > 0) ? static_cast<float>(model.backgroundWidth) : static_cast<float>(texture->getWidth());
|
||||||
|
const float imgH = (model.backgroundHeight > 0) ? static_cast<float>(model.backgroundHeight) : static_cast<float>(texture->getHeight());
|
||||||
|
|
||||||
|
ResolvedViewport layerViewport{};
|
||||||
|
if (model.cutsceneCamera.active) {
|
||||||
|
const ResolvedViewport fromVP = resolveViewportPose(model.cutsceneCamera.from, imgW, imgH, W, H);
|
||||||
|
const ResolvedViewport toVP = resolveViewportPose(model.cutsceneCamera.to, imgW, imgH, W, H);
|
||||||
|
layerViewport = blendViewport(fromVP, toVP, std::clamp(model.cutsceneCamera.t, 0.0f, 1.0f));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
layerViewport = resolveViewportPose(ZL::Cutscene::CutsceneCameraPose{}, imgW, imgH, W, H);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float halfW = layerViewport.widthPx * 0.5f;
|
||||||
|
const float halfH = layerViewport.heightPx * 0.5f;
|
||||||
|
const float rotRad = layerViewport.rotationDeg * 3.14159265358979323846f / 180.0f;
|
||||||
|
const float c = std::cos(rotRad);
|
||||||
|
const float s = std::sin(rotRad);
|
||||||
|
|
||||||
|
auto rotatePoint = [&](float x, float y) -> Eigen::Vector2f {
|
||||||
|
return {
|
||||||
|
layerViewport.centerXPx + x * c - y * s,
|
||||||
|
layerViewport.centerYPx + x * s + y * c
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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(imgW, 1.0f), 0.0f, 1.0f),
|
||||||
|
std::clamp(p.y() / max(imgH, 1.0f), 0.0f, 1.0f)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
backgroundQuad.rebuildWithUV(screenRect, toUV(srcBL), toUV(srcTL), toUV(srcTR), toUV(srcBR));
|
||||||
|
renderer.RenderUniform1f("uAlpha", std::clamp(layer.alpha * model.cutsceneGlobalFadeAlpha, 0.0f, 1.0f));
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture->getTexID());
|
||||||
|
renderer.DrawVertexRenderStruct(backgroundQuad.mesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.PopMatrix();
|
||||||
|
renderer.PopProjectionMatrix();
|
||||||
|
renderer.shaderManager.PopShader();
|
||||||
|
|
||||||
|
if (model.cutsceneBlackAlpha > 0.001f) {
|
||||||
|
renderer.shaderManager.PushShader("cutsceneBlack");
|
||||||
|
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
|
||||||
|
renderer.PushMatrix();
|
||||||
|
renderer.LoadIdentity();
|
||||||
|
|
||||||
|
backgroundQuad.rebuild(screenRect);
|
||||||
|
renderer.RenderUniform1f("uAlpha", model.cutsceneBlackAlpha);
|
||||||
|
renderer.DrawVertexRenderStruct(backgroundQuad.mesh);
|
||||||
|
|
||||||
|
renderer.PopMatrix();
|
||||||
|
renderer.PopProjectionMatrix();
|
||||||
|
renderer.shaderManager.PopShader();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.shaderManager.PushShader(defaultShaderName);
|
||||||
|
renderer.RenderUniform1i(textureUniformName, 0);
|
||||||
|
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
|
||||||
|
renderer.PushMatrix();
|
||||||
|
renderer.LoadIdentity();
|
||||||
|
|
||||||
|
if (model.showCutsceneSubtitle) {
|
||||||
|
subtitleQuad.rebuild(subtitleRect);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, cutsceneSubtitleTexture->getTexID());
|
||||||
|
renderer.DrawVertexRenderStruct(subtitleQuad.mesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.cutsceneSkippable && cutsceneSkipHintVisible) {
|
||||||
|
const UiRect hintBg{ W - 250.0f, H - 62.0f, 226.0f, 42.0f };
|
||||||
|
skipHintBgQuad.rebuild(hintBg);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, choiceOptionalTexture->getTexID());
|
||||||
|
renderer.DrawVertexRenderStruct(skipHintBgQuad.mesh);
|
||||||
|
|
||||||
|
const UiRect progressBg{ W - 232.0f, H - 34.0f, 190.0f, 7.0f };
|
||||||
|
skipProgressBgQuad.rebuild(progressBg);
|
||||||
|
renderer.DrawVertexRenderStruct(skipProgressBgQuad.mesh);
|
||||||
|
|
||||||
|
if (cutsceneSkipHolding) {
|
||||||
|
const float progress = std::clamp(
|
||||||
|
static_cast<float>(cutsceneSkipHoldElapsedMs) / static_cast<float>(CutsceneSkipHoldDurationMs),
|
||||||
|
0.0f, 1.0f
|
||||||
|
);
|
||||||
|
const UiRect progressFill{ progressBg.x, progressBg.y, progressBg.w * progress, progressBg.h };
|
||||||
|
skipProgressFillQuad.rebuild(progressFill);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, choiceMainTexture->getTexID());
|
||||||
|
renderer.DrawVertexRenderStruct(skipProgressFillQuad.mesh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.PopMatrix();
|
||||||
|
renderer.PopProjectionMatrix();
|
||||||
|
renderer.shaderManager.PopShader();
|
||||||
|
|
||||||
|
if (model.showCutsceneSubtitle) {
|
||||||
|
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const std::string wrapped = wrapTextToWidth(model.visibleText, *cutsceneRenderer, subtitleRect.w - 48.0f, 1.0f);
|
||||||
|
cutsceneRenderer->drawText(wrapped, subtitleRect.x + 24.0f, subtitleRect.y + 30.0f,
|
||||||
|
1.0f, false, { 1.0f, 1.0f, 1.0f, 1.0f });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.cutsceneSkippable && cutsceneSkipHintVisible) {
|
||||||
|
choiceRenderer->drawText(
|
||||||
|
cutsceneSkipHolding ? "Hold to skip..." : "Hold LMB to skip",
|
||||||
|
W - 232.0f, H - 50.0f,
|
||||||
|
0.85f, false, { 1.0f, 1.0f, 1.0f, 0.95f }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CutsceneOverlay::consumeSkipRequested() {
|
||||||
|
const bool result = cutsceneSkipTriggered;
|
||||||
|
cutsceneSkipTriggered = false;
|
||||||
|
if (result) {
|
||||||
|
cutsceneSkipHintVisible = false;
|
||||||
|
cutsceneSkipArmed = false;
|
||||||
|
cutsceneSkipHolding = false;
|
||||||
|
cutsceneSkipHintRemainingMs = 0;
|
||||||
|
cutsceneSkipHoldElapsedMs = 0;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutsceneOverlay::handlePointerDown(float x, float y, const ZL::Dialogue::PresentationModel& model) {
|
||||||
|
(void)x; (void)y;
|
||||||
|
if (model.mode != ZL::Dialogue::PresentationMode::Cutscene || !model.cutsceneSkippable) return;
|
||||||
|
|
||||||
|
if (!cutsceneSkipArmed) {
|
||||||
|
cutsceneSkipHintVisible = true;
|
||||||
|
cutsceneSkipArmed = true;
|
||||||
|
cutsceneSkipHolding = false;
|
||||||
|
cutsceneSkipTriggered = false;
|
||||||
|
cutsceneSkipHintRemainingMs = CutsceneSkipHintDurationMs;
|
||||||
|
cutsceneSkipHoldElapsedMs = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cutsceneSkipHintVisible = true;
|
||||||
|
cutsceneSkipHintRemainingMs = CutsceneSkipHintDurationMs;
|
||||||
|
cutsceneSkipHolding = true;
|
||||||
|
cutsceneSkipTriggered = false;
|
||||||
|
cutsceneSkipHoldElapsedMs = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutsceneOverlay::handlePointerMoved(float /*x*/, float /*y*/, const ZL::Dialogue::PresentationModel& /*model*/) {
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CutsceneOverlay::handlePointerReleased(float /*x*/, float /*y*/, const ZL::Dialogue::PresentationModel& model) {
|
||||||
|
if (model.mode != ZL::Dialogue::PresentationMode::Cutscene) return false;
|
||||||
|
if (cutsceneSkipHolding && cutsceneSkipHoldElapsedMs < CutsceneSkipHoldDurationMs) {
|
||||||
|
cutsceneSkipHolding = false;
|
||||||
|
cutsceneSkipHoldElapsedMs = 0;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<Texture> CutsceneOverlay::loadTextureCached(const std::string& path) {
|
||||||
|
if (path.empty()) return nullptr;
|
||||||
|
return rendererRef->textureManager.LoadFromPng(path, zipFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
float CutsceneOverlay::lerpFloat(float a, float b, float t) {
|
||||||
|
return a + (b - a) * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
CutsceneOverlay::ResolvedViewport CutsceneOverlay::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 rotRad = pose.rotationDeg * 3.14159265358979323846f / 180.0f;
|
||||||
|
const float c = std::cos(rotRad);
|
||||||
|
const float s = std::sin(rotRad);
|
||||||
|
|
||||||
|
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:
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
CutsceneOverlay::ResolvedViewport CutsceneOverlay::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;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string CutsceneOverlay::wrapTextToWidth(
|
||||||
|
const std::string& input,
|
||||||
|
const TextRenderer& textRenderer,
|
||||||
|
float maxWidthPx,
|
||||||
|
float scale)
|
||||||
|
{
|
||||||
|
if (input.empty() || maxWidthPx <= 1.0f) return input;
|
||||||
|
|
||||||
|
std::string output;
|
||||||
|
std::string currentLine;
|
||||||
|
std::string currentWord;
|
||||||
|
|
||||||
|
auto flushLine = [&]() {
|
||||||
|
if (!currentLine.empty()) {
|
||||||
|
if (!output.empty()) output.push_back('\n');
|
||||||
|
output += currentLine;
|
||||||
|
currentLine.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto pushWord = [&](const std::string& word) {
|
||||||
|
if (word.empty()) return;
|
||||||
|
if (currentLine.empty()) { currentLine = word; return; }
|
||||||
|
const std::string candidate = currentLine + " " + word;
|
||||||
|
if (textRenderer.measureTextWidth(candidate, scale) <= maxWidthPx) {
|
||||||
|
currentLine = candidate;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
flushLine();
|
||||||
|
currentLine = word;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (size_t i = 0; i < input.size(); ++i) {
|
||||||
|
const char ch = input[i];
|
||||||
|
if (ch == '\n') { pushWord(currentWord); currentWord.clear(); flushLine(); continue; }
|
||||||
|
if (ch == ' ' || ch == '\t' || ch == '\r') { pushWord(currentWord); currentWord.clear(); continue; }
|
||||||
|
currentWord.push_back(ch);
|
||||||
|
}
|
||||||
|
pushWord(currentWord);
|
||||||
|
flushLine();
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ZL::Cutscene
|
||||||
83
src/cutscene/CutsceneOverlay.h
Normal file
83
src/cutscene/CutsceneOverlay.h
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "cutscene/CutsceneTypes.h"
|
||||||
|
#include "dialogue/DialogueTypes.h"
|
||||||
|
#include "render/Renderer.h"
|
||||||
|
#include "render/TextRenderer.h"
|
||||||
|
#include "render/TextureManager.h"
|
||||||
|
#include "render/UiQuad.h"
|
||||||
|
#include "UiManager.h"
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ZL::Cutscene {
|
||||||
|
|
||||||
|
class CutsceneOverlay {
|
||||||
|
public:
|
||||||
|
bool init(Renderer& renderer, const std::string& zipFile = "");
|
||||||
|
void update(const ZL::Dialogue::PresentationModel& model, int deltaMs);
|
||||||
|
void draw(Renderer& renderer, const ZL::Dialogue::PresentationModel& model);
|
||||||
|
|
||||||
|
void handlePointerDown(float x, float y, const ZL::Dialogue::PresentationModel& model);
|
||||||
|
void handlePointerMoved(float x, float y, const ZL::Dialogue::PresentationModel& model);
|
||||||
|
bool handlePointerReleased(float x, float y, const ZL::Dialogue::PresentationModel& model);
|
||||||
|
bool consumeSkipRequested();
|
||||||
|
|
||||||
|
private:
|
||||||
|
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;
|
||||||
|
std::string zipFilename;
|
||||||
|
|
||||||
|
std::shared_ptr<Texture> choiceMainTexture;
|
||||||
|
std::shared_ptr<Texture> choiceOptionalTexture;
|
||||||
|
std::shared_ptr<Texture> cutsceneSubtitleTexture;
|
||||||
|
|
||||||
|
mutable UiRect lastCutsceneAdvanceRect{};
|
||||||
|
mutable UiRect lastCutsceneSkipRect{};
|
||||||
|
|
||||||
|
// Skip UX state
|
||||||
|
bool cutsceneSkipHintVisible = false;
|
||||||
|
bool cutsceneSkipArmed = false;
|
||||||
|
bool cutsceneSkipHolding = false;
|
||||||
|
bool cutsceneSkipTriggered = false;
|
||||||
|
int cutsceneSkipHintRemainingMs = 0;
|
||||||
|
int cutsceneSkipHoldElapsedMs = 0;
|
||||||
|
static constexpr int CutsceneSkipHintDurationMs = 5000;
|
||||||
|
static constexpr int CutsceneSkipHoldDurationMs = 3500;
|
||||||
|
|
||||||
|
std::unique_ptr<TextRenderer> nameRenderer;
|
||||||
|
std::unique_ptr<TextRenderer> cutsceneRenderer;
|
||||||
|
std::unique_ptr<TextRenderer> choiceRenderer;
|
||||||
|
|
||||||
|
UiQuad backgroundQuad;
|
||||||
|
UiQuad subtitleQuad;
|
||||||
|
UiQuad skipHintBgQuad;
|
||||||
|
UiQuad skipProgressBgQuad;
|
||||||
|
UiQuad skipProgressFillQuad;
|
||||||
|
|
||||||
|
std::shared_ptr<Texture> loadTextureCached(const std::string& path);
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
static std::string wrapTextToWidth(const std::string& input, const TextRenderer& textRenderer,
|
||||||
|
float maxWidthPx, float scale);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZL::Cutscene
|
||||||
509
src/cutscene/CutsceneRuntime.cpp
Normal file
509
src/cutscene/CutsceneRuntime.cpp
Normal file
@ -0,0 +1,509 @@
|
|||||||
|
#include "cutscene/CutsceneRuntime.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace ZL::Cutscene {
|
||||||
|
|
||||||
|
static std::pair<std::string, std::string> splitDot(const std::string& s) {
|
||||||
|
const auto dot = s.find('.');
|
||||||
|
if (dot == std::string::npos) return {s, ""};
|
||||||
|
return {s.substr(0, dot), s.substr(dot + 1)};
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutsceneRuntime::setDatabase(const CutsceneDatabase* value) {
|
||||||
|
database = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutsceneRuntime::setQuestJournal(Quest::QuestJournal* journal) {
|
||||||
|
questJournal = journal;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutsceneRuntime::setOnFinished(std::function<void(const std::string&)> cb) {
|
||||||
|
onFinished = std::move(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutsceneRuntime::setOnLineStarted(std::function<void(const std::string&)> cb) {
|
||||||
|
onLineStarted = std::move(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutsceneRuntime::setOnFadeInComplete(std::function<void(const std::string&)> cb) {
|
||||||
|
onFadeInComplete = std::move(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CutsceneRuntime::start(const std::string& cutsceneId) {
|
||||||
|
if (!database) {
|
||||||
|
std::cerr << "[cutscene] No database assigned to runtime\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const StaticCutsceneDefinition* def = database->findCutscene(cutsceneId);
|
||||||
|
if (!def) {
|
||||||
|
std::cerr << "[cutscene] Cutscene not found: " << cutsceneId << "\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeCutscene = def;
|
||||||
|
activeCutsceneId = cutsceneId;
|
||||||
|
active = true;
|
||||||
|
fadeInCallbackFired = false;
|
||||||
|
currentCutsceneBackground = def->background;
|
||||||
|
cutsceneElapsedMs = 0;
|
||||||
|
cutsceneTimerMs = 0;
|
||||||
|
currentCutsceneLine = def->lines.empty() ? -1 : 0;
|
||||||
|
|
||||||
|
int imageTrackDurationMs = 0;
|
||||||
|
for (size_t i = 0; i < def->images.size(); ++i) {
|
||||||
|
const CutsceneImageCue& cue = def->images[i];
|
||||||
|
int cueEnd = cue.endMs;
|
||||||
|
if (cueEnd <= cue.startMs) {
|
||||||
|
if (i + 1 < def->images.size()) {
|
||||||
|
cueEnd = std::max(def->images[i + 1].startMs, cue.startMs);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cueEnd = cue.startMs + std::max(cue.fadeInMs, 0) + std::max(cue.fadeOutMs, 0) + 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imageTrackDurationMs = std::max(imageTrackDurationMs, cueEnd);
|
||||||
|
}
|
||||||
|
cutsceneContentDurationMs = std::max({ def->durationMs, computeCameraTrackDurationMs(*def), imageTrackDurationMs });
|
||||||
|
if (cutsceneContentDurationMs <= 0 && def->lines.empty()) {
|
||||||
|
cutsceneContentDurationMs = 3000;
|
||||||
|
}
|
||||||
|
cutsceneTotalDurationMs = cutsceneContentDurationMs + def->endFadeOutMs + def->endFadeInMs;
|
||||||
|
|
||||||
|
presentation = {};
|
||||||
|
refreshPresentation();
|
||||||
|
|
||||||
|
if (!def->lines.empty()) {
|
||||||
|
const CutsceneLine& firstLine = def->lines[0];
|
||||||
|
applyQuestActions(firstLine.questUnlock, firstLine.questComplete,
|
||||||
|
firstLine.questFail, firstLine.objectiveComplete, firstLine.objectiveVisible);
|
||||||
|
if (onLineStarted && !firstLine.luaCallback.empty())
|
||||||
|
onLineStarted(firstLine.luaCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "[CUTSCENE] start id=" << cutsceneId
|
||||||
|
<< " lines=" << def->lines.size()
|
||||||
|
<< " totalDuration=" << cutsceneTotalDurationMs
|
||||||
|
<< std::endl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutsceneRuntime::stop() {
|
||||||
|
activeCutscene = nullptr;
|
||||||
|
activeCutsceneId.clear();
|
||||||
|
active = false;
|
||||||
|
fadeInCallbackFired = false;
|
||||||
|
currentCutsceneLine = -1;
|
||||||
|
cutsceneTimerMs = 0;
|
||||||
|
cutsceneElapsedMs = 0;
|
||||||
|
cutsceneTotalDurationMs = 0;
|
||||||
|
cutsceneContentDurationMs = 0;
|
||||||
|
currentCutsceneBackground.clear();
|
||||||
|
presentation = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutsceneRuntime::update(int deltaMs) {
|
||||||
|
if (!active || !activeCutscene) return;
|
||||||
|
|
||||||
|
cutsceneElapsedMs += deltaMs;
|
||||||
|
|
||||||
|
if (!activeCutscene->lines.empty() &&
|
||||||
|
currentCutsceneLine >= 0 &&
|
||||||
|
currentCutsceneLine < static_cast<int>(activeCutscene->lines.size())) {
|
||||||
|
|
||||||
|
const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
|
||||||
|
if (!line.waitForConfirm) {
|
||||||
|
cutsceneTimerMs += deltaMs;
|
||||||
|
const int durationMs = (line.durationMs > 0)
|
||||||
|
? line.durationMs
|
||||||
|
: computeFallbackDurationMs(line.text);
|
||||||
|
|
||||||
|
if (cutsceneTimerMs >= durationMs) {
|
||||||
|
advanceLine();
|
||||||
|
if (!active || !activeCutscene) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!active || !activeCutscene) return;
|
||||||
|
|
||||||
|
refreshPresentation();
|
||||||
|
|
||||||
|
if (!active || !activeCutscene) return;
|
||||||
|
|
||||||
|
if (!fadeInCallbackFired && onFadeInComplete && activeCutscene) {
|
||||||
|
const int fadeInCompleteMs = activeCutscene->fadeOutMs + activeCutscene->fadeInMs;
|
||||||
|
if (cutsceneElapsedMs >= fadeInCompleteMs && !activeCutscene->onFadeInCallback.empty()) {
|
||||||
|
fadeInCallbackFired = true;
|
||||||
|
onFadeInComplete(activeCutscene->onFadeInCallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!active || !activeCutscene) return;
|
||||||
|
|
||||||
|
const bool subtitlesFinished =
|
||||||
|
activeCutscene->lines.empty() ||
|
||||||
|
currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size());
|
||||||
|
|
||||||
|
const bool durationFinished =
|
||||||
|
cutsceneTotalDurationMs > 0 &&
|
||||||
|
cutsceneElapsedMs >= cutsceneTotalDurationMs;
|
||||||
|
|
||||||
|
if (activeCutscene->lines.empty()) {
|
||||||
|
if (durationFinished) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subtitlesFinished && (cutsceneTotalDurationMs <= 0 || durationFinished)) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CutsceneRuntime::canSkip() const {
|
||||||
|
return active && activeCutscene && activeCutscene->skippable;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutsceneRuntime::skip() {
|
||||||
|
if (!canSkip()) return;
|
||||||
|
|
||||||
|
if (!activeCutscene->images.empty()) {
|
||||||
|
int nextImageStartMs = -1;
|
||||||
|
for (const CutsceneImageCue& cue : activeCutscene->images) {
|
||||||
|
if (cue.path.empty()) continue;
|
||||||
|
if (cue.startMs > cutsceneElapsedMs) {
|
||||||
|
if (nextImageStartMs < 0 || cue.startMs < nextImageStartMs) {
|
||||||
|
nextImageStartMs = cue.startMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nextImageStartMs >= 0) {
|
||||||
|
cutsceneElapsedMs = nextImageStartMs;
|
||||||
|
syncLineToElapsedTime();
|
||||||
|
refreshPresentation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutsceneRuntime::applyQuestActions(
|
||||||
|
const std::string& questUnlock, const std::string& questComplete,
|
||||||
|
const std::string& questFail, const std::string& objectiveComplete,
|
||||||
|
const std::string& objectiveVisible)
|
||||||
|
{
|
||||||
|
if (!questJournal) return;
|
||||||
|
if (!questUnlock.empty()) questJournal->unlockQuest(questUnlock);
|
||||||
|
if (!questComplete.empty()) questJournal->completeQuest(questComplete);
|
||||||
|
if (!questFail.empty()) questJournal->failQuest(questFail);
|
||||||
|
if (!objectiveComplete.empty()) {
|
||||||
|
auto [qId, oId] = splitDot(objectiveComplete);
|
||||||
|
questJournal->setObjectiveCompleted(qId, oId);
|
||||||
|
}
|
||||||
|
if (!objectiveVisible.empty()) {
|
||||||
|
auto [qId, oId] = splitDot(objectiveVisible);
|
||||||
|
questJournal->setObjectiveVisible(qId, oId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutsceneRuntime::finish() {
|
||||||
|
std::cout << "[CUTSCENE] finish id=" << activeCutsceneId << std::endl;
|
||||||
|
const std::string finishedId = activeCutsceneId;
|
||||||
|
stop();
|
||||||
|
if (onFinished && !finishedId.empty()) {
|
||||||
|
onFinished(finishedId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutsceneRuntime::syncLineToElapsedTime() {
|
||||||
|
if (!activeCutscene || activeCutscene->lines.empty()) {
|
||||||
|
currentCutsceneLine = -1;
|
||||||
|
cutsceneTimerMs = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int elapsed = std::max(cutsceneElapsedMs, 0);
|
||||||
|
int accumulatedMs = 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < activeCutscene->lines.size(); ++i) {
|
||||||
|
const CutsceneLine& line = activeCutscene->lines[i];
|
||||||
|
const int durationMs = (line.durationMs > 0)
|
||||||
|
? line.durationMs
|
||||||
|
: computeFallbackDurationMs(line.text);
|
||||||
|
|
||||||
|
if (elapsed < accumulatedMs + durationMs) {
|
||||||
|
currentCutsceneLine = static_cast<int>(i);
|
||||||
|
cutsceneTimerMs = std::max(0, elapsed - accumulatedMs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
accumulatedMs += durationMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentCutsceneLine = -1;
|
||||||
|
cutsceneTimerMs = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutsceneRuntime::advanceLine() {
|
||||||
|
if (!activeCutscene) {
|
||||||
|
stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activeCutscene->lines.empty()) return;
|
||||||
|
|
||||||
|
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())) {
|
||||||
|
refreshPresentation();
|
||||||
|
if (cutsceneContentDurationMs <= 0 || cutsceneElapsedMs >= cutsceneContentDurationMs) {
|
||||||
|
if (activeCutscene->endFadeOutMs <= 0 && activeCutscene->endFadeInMs <= 0) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CutsceneLine& newLine = activeCutscene->lines[currentCutsceneLine];
|
||||||
|
applyQuestActions(newLine.questUnlock, newLine.questComplete,
|
||||||
|
newLine.questFail, newLine.objectiveComplete, newLine.objectiveVisible);
|
||||||
|
if (onLineStarted && !newLine.luaCallback.empty())
|
||||||
|
onLineStarted(newLine.luaCallback);
|
||||||
|
|
||||||
|
refreshPresentation();
|
||||||
|
}
|
||||||
|
|
||||||
|
CutsceneCameraBlendState CutsceneRuntime::evaluateCameraBlend() 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<PresentedCutsceneImage> CutsceneRuntime::evaluateImages() const {
|
||||||
|
std::vector<PresentedCutsceneImage> result;
|
||||||
|
if (!activeCutscene) return result;
|
||||||
|
|
||||||
|
const std::string& fallbackPath = !currentCutsceneBackground.empty()
|
||||||
|
? currentCutsceneBackground
|
||||||
|
: activeCutscene->background;
|
||||||
|
|
||||||
|
if (activeCutscene->images.empty()) {
|
||||||
|
if (!fallbackPath.empty()) {
|
||||||
|
result.push_back({ fallbackPath, 1.0f });
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int effectiveTotalDuration = (cutsceneContentDurationMs > 0)
|
||||||
|
? cutsceneContentDurationMs
|
||||||
|
: std::max(activeCutscene->durationMs, 1);
|
||||||
|
const int now = std::max(cutsceneElapsedMs, 0);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < activeCutscene->images.size(); ++i) {
|
||||||
|
const CutsceneImageCue& cue = activeCutscene->images[i];
|
||||||
|
if (cue.path.empty()) continue;
|
||||||
|
|
||||||
|
const int startMs = std::max(cue.startMs, 0);
|
||||||
|
int endMs = cue.endMs;
|
||||||
|
if (endMs <= startMs) {
|
||||||
|
if (i + 1 < activeCutscene->images.size()) {
|
||||||
|
endMs = std::max(activeCutscene->images[i + 1].startMs, startMs + 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
endMs = effectiveTotalDuration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (endMs <= startMs) {
|
||||||
|
endMs = startMs + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now < startMs || now > endMs) continue;
|
||||||
|
|
||||||
|
float alpha = 1.0f;
|
||||||
|
if (cue.fadeInMs > 0 && now < startMs + cue.fadeInMs) {
|
||||||
|
alpha = std::clamp(
|
||||||
|
static_cast<float>(now - startMs) / static_cast<float>(cue.fadeInMs),
|
||||||
|
0.0f, 1.0f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alpha > 0.0f) {
|
||||||
|
result.push_back({ cue.path, alpha });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.empty() && !fallbackPath.empty()) {
|
||||||
|
result.push_back({ fallbackPath, 1.0f });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.empty() && result.front().alpha < 0.999f &&
|
||||||
|
!fallbackPath.empty() && result.front().path != fallbackPath)
|
||||||
|
{
|
||||||
|
result.insert(result.begin(), { fallbackPath, 1.0f });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutsceneRuntime::refreshPresentation() {
|
||||||
|
if (!activeCutscene) return;
|
||||||
|
|
||||||
|
presentation.mode = ZL::Dialogue::PresentationMode::Cutscene;
|
||||||
|
presentation.backgroundPath = activeCutscene->background;
|
||||||
|
presentation.backgroundWidth = activeCutscene->backgroundWidth;
|
||||||
|
presentation.backgroundHeight = activeCutscene->backgroundHeight;
|
||||||
|
presentation.cutsceneCamera = evaluateCameraBlend();
|
||||||
|
presentation.cutsceneImages = evaluateImages();
|
||||||
|
presentation.cutsceneSkippable = activeCutscene->skippable;
|
||||||
|
|
||||||
|
const int fadeOutMs = activeCutscene->fadeOutMs;
|
||||||
|
const int fadeInMs = activeCutscene->fadeInMs;
|
||||||
|
const int endFadeOutMs = activeCutscene->endFadeOutMs;
|
||||||
|
const int endFadeInMs = activeCutscene->endFadeInMs;
|
||||||
|
const int endFadeOutStart = cutsceneContentDurationMs;
|
||||||
|
const int endFadeInStart = cutsceneContentDurationMs + endFadeOutMs;
|
||||||
|
|
||||||
|
if (cutsceneElapsedMs < fadeOutMs) {
|
||||||
|
presentation.cutsceneGlobalFadeAlpha = 0.0f;
|
||||||
|
presentation.cutsceneBlackAlpha = std::clamp(
|
||||||
|
static_cast<float>(cutsceneElapsedMs) / static_cast<float>(fadeOutMs),
|
||||||
|
0.0f, 1.0f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (cutsceneElapsedMs < endFadeOutStart) {
|
||||||
|
presentation.cutsceneGlobalFadeAlpha = 1.0f;
|
||||||
|
const int phase2elapsed = cutsceneElapsedMs - fadeOutMs;
|
||||||
|
presentation.cutsceneBlackAlpha = (fadeInMs > 0)
|
||||||
|
? std::clamp(1.0f - static_cast<float>(phase2elapsed) / static_cast<float>(fadeInMs), 0.0f, 1.0f)
|
||||||
|
: 0.0f;
|
||||||
|
}
|
||||||
|
else if (cutsceneElapsedMs < endFadeInStart) {
|
||||||
|
presentation.cutsceneGlobalFadeAlpha = 1.0f;
|
||||||
|
const int elapsed = cutsceneElapsedMs - endFadeOutStart;
|
||||||
|
presentation.cutsceneBlackAlpha = (endFadeOutMs > 0)
|
||||||
|
? std::clamp(static_cast<float>(elapsed) / static_cast<float>(endFadeOutMs), 0.0f, 1.0f)
|
||||||
|
: 1.0f;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
presentation.cutsceneGlobalFadeAlpha = 0.0f;
|
||||||
|
const int elapsed = cutsceneElapsedMs - endFadeInStart;
|
||||||
|
presentation.cutsceneBlackAlpha = (endFadeInMs > 0)
|
||||||
|
? std::clamp(1.0f - static_cast<float>(elapsed) / static_cast<float>(endFadeInMs), 0.0f, 1.0f)
|
||||||
|
: 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
presentation.choices.clear();
|
||||||
|
presentation.selectedChoice = -1;
|
||||||
|
presentation.revealCompleted = true;
|
||||||
|
|
||||||
|
const bool hasSubtitle = currentCutsceneLine >= 0 &&
|
||||||
|
currentCutsceneLine < static_cast<int>(activeCutscene->lines.size());
|
||||||
|
presentation.showCutsceneSubtitle = hasSubtitle;
|
||||||
|
|
||||||
|
if (!hasSubtitle) {
|
||||||
|
presentation.speaker.clear();
|
||||||
|
presentation.fullText.clear();
|
||||||
|
presentation.visibleText.clear();
|
||||||
|
presentation.portraitPath.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
|
||||||
|
|
||||||
|
if (!line.background.empty()) {
|
||||||
|
currentCutsceneBackground = line.background;
|
||||||
|
if (line.backgroundWidth > 0) presentation.backgroundWidth = line.backgroundWidth;
|
||||||
|
if (line.backgroundHeight > 0) presentation.backgroundHeight = line.backgroundHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
presentation.backgroundPath = currentCutsceneBackground;
|
||||||
|
presentation.speaker = line.speaker;
|
||||||
|
presentation.fullText = line.text;
|
||||||
|
presentation.visibleText = line.text;
|
||||||
|
presentation.portraitPath = line.portrait;
|
||||||
|
presentation.selectedChoice = 0;
|
||||||
|
|
||||||
|
std::cout << "[CUTSCENE] lines=" << activeCutscene->lines.size()
|
||||||
|
<< " current=" << currentCutsceneLine
|
||||||
|
<< std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
float CutsceneRuntime::applyEasing(EasingType easing, float t) {
|
||||||
|
t = std::clamp(t, 0.0f, 1.0f);
|
||||||
|
constexpr float PI = 3.14159265358979323846f;
|
||||||
|
|
||||||
|
switch (easing) {
|
||||||
|
case EasingType::EaseInSine:
|
||||||
|
return 1.0f - std::cos((t * PI) * 0.5f);
|
||||||
|
case EasingType::EaseOutSine:
|
||||||
|
return std::sin((t * PI) * 0.5f);
|
||||||
|
case EasingType::EaseInOutSine:
|
||||||
|
return -(std::cos(PI * t) - 1.0f) * 0.5f;
|
||||||
|
case EasingType::EaseInQuad:
|
||||||
|
return t * t;
|
||||||
|
case EasingType::EaseOutQuad:
|
||||||
|
return 1.0f - (1.0f - t) * (1.0f - t);
|
||||||
|
case EasingType::EaseInOutQuad:
|
||||||
|
return (t < 0.5f) ? 2.0f * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 2.0f) * 0.5f;
|
||||||
|
case EasingType::EaseInCubic:
|
||||||
|
return t * t * t;
|
||||||
|
case EasingType::EaseOutCubic:
|
||||||
|
return 1.0f - std::pow(1.0f - t, 3.0f);
|
||||||
|
case EasingType::EaseInOutCubic:
|
||||||
|
return (t < 0.5f) ? 4.0f * t * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 3.0f) * 0.5f;
|
||||||
|
case EasingType::Linear:
|
||||||
|
default:
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int CutsceneRuntime::computeFallbackDurationMs(const std::string& text) {
|
||||||
|
const int cps = 17;
|
||||||
|
const int minDuration = 1500;
|
||||||
|
const int linger = 450;
|
||||||
|
const int calculated = static_cast<int>((1000.0 * static_cast<double>(std::max<size_t>(text.size(), 1))) / cps);
|
||||||
|
return std::max(minDuration, calculated + linger);
|
||||||
|
}
|
||||||
|
|
||||||
|
int CutsceneRuntime::computeCameraTrackDurationMs(const StaticCutsceneDefinition& cutscene) {
|
||||||
|
int total = 0;
|
||||||
|
for (const CutsceneCameraSegment& segment : cutscene.cameraTrack) {
|
||||||
|
total += std::max(segment.durationMs, 0);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ZL::Cutscene
|
||||||
69
src/cutscene/CutsceneRuntime.h
Normal file
69
src/cutscene/CutsceneRuntime.h
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "cutscene/CutsceneDatabase.h"
|
||||||
|
#include "dialogue/DialogueTypes.h"
|
||||||
|
#include "quest/QuestJournal.h"
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ZL::Cutscene {
|
||||||
|
|
||||||
|
class CutsceneRuntime {
|
||||||
|
public:
|
||||||
|
void setDatabase(const CutsceneDatabase* value);
|
||||||
|
void setQuestJournal(Quest::QuestJournal* journal);
|
||||||
|
|
||||||
|
void setOnFinished(std::function<void(const std::string&)> cb);
|
||||||
|
void setOnLineStarted(std::function<void(const std::string&)> cb);
|
||||||
|
void setOnFadeInComplete(std::function<void(const std::string&)> cb);
|
||||||
|
|
||||||
|
bool start(const std::string& cutsceneId);
|
||||||
|
void stop();
|
||||||
|
void update(int deltaMs);
|
||||||
|
|
||||||
|
bool isActive() const { return active; }
|
||||||
|
bool canSkip() const;
|
||||||
|
void skip();
|
||||||
|
|
||||||
|
const ZL::Dialogue::PresentationModel& getPresentation() const { return presentation; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
const CutsceneDatabase* database = nullptr;
|
||||||
|
Quest::QuestJournal* questJournal = nullptr;
|
||||||
|
|
||||||
|
const StaticCutsceneDefinition* activeCutscene = nullptr;
|
||||||
|
std::string activeCutsceneId;
|
||||||
|
bool active = false;
|
||||||
|
bool fadeInCallbackFired = false;
|
||||||
|
|
||||||
|
int currentCutsceneLine = -1;
|
||||||
|
int cutsceneTimerMs = 0;
|
||||||
|
int cutsceneElapsedMs = 0;
|
||||||
|
int cutsceneTotalDurationMs = 0;
|
||||||
|
int cutsceneContentDurationMs = 0;
|
||||||
|
std::string currentCutsceneBackground;
|
||||||
|
|
||||||
|
ZL::Dialogue::PresentationModel presentation;
|
||||||
|
|
||||||
|
std::function<void(const std::string&)> onFinished;
|
||||||
|
std::function<void(const std::string&)> onLineStarted;
|
||||||
|
std::function<void(const std::string&)> onFadeInComplete;
|
||||||
|
|
||||||
|
void applyQuestActions(const std::string& questUnlock, const std::string& questComplete,
|
||||||
|
const std::string& questFail, const std::string& objectiveComplete,
|
||||||
|
const std::string& objectiveVisible);
|
||||||
|
|
||||||
|
void finish();
|
||||||
|
void syncLineToElapsedTime();
|
||||||
|
void advanceLine();
|
||||||
|
void refreshPresentation();
|
||||||
|
|
||||||
|
CutsceneCameraBlendState evaluateCameraBlend() const;
|
||||||
|
std::vector<PresentedCutsceneImage> evaluateImages() const;
|
||||||
|
|
||||||
|
static float applyEasing(EasingType easing, float t);
|
||||||
|
static int computeFallbackDurationMs(const std::string& text);
|
||||||
|
static int computeCameraTrackDurationMs(const StaticCutsceneDefinition& cutscene);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZL::Cutscene
|
||||||
102
src/cutscene/CutsceneTypes.h
Normal file
102
src/cutscene/CutsceneTypes.h
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ZL::Cutscene {
|
||||||
|
|
||||||
|
enum class EasingType {
|
||||||
|
Linear,
|
||||||
|
EaseInSine,
|
||||||
|
EaseOutSine,
|
||||||
|
EaseInOutSine,
|
||||||
|
EaseInQuad,
|
||||||
|
EaseOutQuad,
|
||||||
|
EaseInOutQuad,
|
||||||
|
EaseInCubic,
|
||||||
|
EaseOutCubic,
|
||||||
|
EaseInOutCubic
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class CutsceneAnchor {
|
||||||
|
Center,
|
||||||
|
TopLeft,
|
||||||
|
TopRight,
|
||||||
|
BottomRight,
|
||||||
|
BottomLeft,
|
||||||
|
Custom
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CutsceneLine {
|
||||||
|
std::string speaker;
|
||||||
|
std::string text;
|
||||||
|
std::string portrait;
|
||||||
|
std::string sfx;
|
||||||
|
std::string background;
|
||||||
|
std::string luaCallback;
|
||||||
|
int backgroundWidth = 0;
|
||||||
|
int backgroundHeight = 0;
|
||||||
|
int durationMs = 0;
|
||||||
|
bool waitForConfirm = false;
|
||||||
|
|
||||||
|
std::string questUnlock;
|
||||||
|
std::string questComplete;
|
||||||
|
std::string questFail;
|
||||||
|
std::string objectiveComplete; // "quest_id.objective_id"
|
||||||
|
std::string objectiveVisible; // "quest_id.objective_id"
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CutsceneCameraPose {
|
||||||
|
CutsceneAnchor anchor = CutsceneAnchor::Center;
|
||||||
|
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 CutsceneImageCue {
|
||||||
|
std::string path;
|
||||||
|
int startMs = 0;
|
||||||
|
int endMs = 0;
|
||||||
|
int fadeInMs = 0;
|
||||||
|
int fadeOutMs = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StaticCutsceneDefinition {
|
||||||
|
std::string id;
|
||||||
|
std::string background;
|
||||||
|
int backgroundWidth = 1280;
|
||||||
|
int backgroundHeight = 720;
|
||||||
|
std::string music;
|
||||||
|
std::string onFadeInCallback;
|
||||||
|
bool skippable = true;
|
||||||
|
int durationMs = 0;
|
||||||
|
int fadeOutMs = 0;
|
||||||
|
int fadeInMs = 0;
|
||||||
|
int endFadeOutMs = 0;
|
||||||
|
int endFadeInMs = 0;
|
||||||
|
std::vector<CutsceneCameraSegment> cameraTrack;
|
||||||
|
std::vector<CutsceneImageCue> images;
|
||||||
|
std::vector<CutsceneLine> lines;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PresentedCutsceneImage {
|
||||||
|
std::string path;
|
||||||
|
float alpha = 1.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CutsceneCameraBlendState {
|
||||||
|
bool active = false;
|
||||||
|
CutsceneCameraPose from;
|
||||||
|
CutsceneCameraPose to;
|
||||||
|
float t = 1.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZL::Cutscene
|
||||||
@ -34,28 +34,6 @@ 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,7 +105,6 @@ Node DialogueDatabase::parseNode(const json& j) {
|
|||||||
node.choices.push_back(parseChoice(item));
|
node.choices.push_back(parseChoice(item));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,105 +123,11 @@ DialogueDefinition DialogueDatabase::parseDialogue(const json& j) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
CutsceneLine DialogueDatabase::parseCutsceneLine(const json& j) {
|
|
||||||
CutsceneLine line;
|
|
||||||
line.speaker = j.value("speaker", "");
|
|
||||||
line.text = j.value("text", "");
|
|
||||||
line.portrait = j.value("portrait", "");
|
|
||||||
line.sfx = j.value("sfx", "");
|
|
||||||
line.background = j.value("background", "");
|
|
||||||
line.backgroundWidth = j.value("backgroundWidth", 0);
|
|
||||||
line.backgroundHeight = j.value("backgroundHeight", 0);
|
|
||||||
line.luaCallback = j.value("luaCallback", "");
|
|
||||||
line.durationMs = j.value("durationMs", 0);
|
|
||||||
line.waitForConfirm = j.value("waitForConfirm", false);
|
|
||||||
line.questUnlock = j.value("questUnlock", "");
|
|
||||||
line.questComplete = j.value("questComplete", "");
|
|
||||||
line.questFail = j.value("questFail", "");
|
|
||||||
line.objectiveComplete = j.value("objectiveComplete", "");
|
|
||||||
line.objectiveVisible = j.value("objectiveVisible", "");
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
CutsceneImageCue DialogueDatabase::parseCutsceneImageCue(const json& j) {
|
|
||||||
CutsceneImageCue cue;
|
|
||||||
cue.path = j.value("path", "");
|
|
||||||
cue.startMs = j.value("startMs", 0);
|
|
||||||
cue.endMs = j.value("endMs", 0);
|
|
||||||
cue.fadeInMs = j.value("fadeInMs", 0);
|
|
||||||
cue.fadeOutMs = j.value("fadeOutMs", 0);
|
|
||||||
return cue;
|
|
||||||
}
|
|
||||||
|
|
||||||
StaticCutsceneDefinition DialogueDatabase::parseCutscene(const json& j) {
|
|
||||||
StaticCutsceneDefinition cutscene;
|
|
||||||
cutscene.id = j.value("id", "");
|
|
||||||
cutscene.background = j.value("background", "");
|
|
||||||
cutscene.backgroundWidth = j.value("backgroundWidth", 1280);
|
|
||||||
cutscene.backgroundHeight = j.value("backgroundHeight", 720);
|
|
||||||
cutscene.music = j.value("music", "");
|
|
||||||
cutscene.skippable = j.value("skippable", true);
|
|
||||||
cutscene.durationMs = j.value("durationMs", 0);
|
|
||||||
cutscene.fadeOutMs = j.value("fadeOutMs", 0);
|
|
||||||
cutscene.fadeInMs = j.value("fadeInMs", 0);
|
|
||||||
cutscene.endFadeOutMs = j.value("endFadeOutMs", 0);
|
|
||||||
cutscene.endFadeInMs = j.value("endFadeInMs", 0);
|
|
||||||
cutscene.onFadeInCallback = j.value("onFadeInCallback", "");
|
|
||||||
|
|
||||||
if (j.contains("cameraTrack") && j["cameraTrack"].is_array()) {
|
|
||||||
for (const auto& item : j["cameraTrack"]) {
|
|
||||||
cutscene.cameraTrack.push_back(parseCutsceneCameraSegment(item));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (j.contains("images") && j["images"].is_array()) {
|
|
||||||
for (const auto& item : j["images"]) {
|
|
||||||
cutscene.images.push_back(parseCutsceneImageCue(item));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (j.contains("lines") && j["lines"].is_array()) {
|
|
||||||
for (const auto& item : j["lines"]) {
|
|
||||||
cutscene.lines.push_back(parseCutsceneLine(item));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cutscene;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DialogueDatabase::loadFromFile(const std::string& path) {
|
bool DialogueDatabase::loadFromFile(const std::string& path) {
|
||||||
dialogues.clear();
|
dialogues.clear();
|
||||||
cutscenes.clear();
|
|
||||||
|
|
||||||
std::string raw;
|
std::string raw;
|
||||||
try {
|
try {
|
||||||
@ -254,15 +137,15 @@ bool DialogueDatabase::loadFromFile(const std::string& path) {
|
|||||||
else {
|
else {
|
||||||
auto buf = readFileFromZIP(path, CONST_ZIP_FILE);
|
auto buf = readFileFromZIP(path, CONST_ZIP_FILE);
|
||||||
if (buf.empty()) {
|
if (buf.empty()) {
|
||||||
std::cerr << "UiManager: failed to read " << path << " from zip " << CONST_ZIP_FILE << std::endl;
|
std::cerr << "[dialogue] Failed to read " << path << " from zip\n";
|
||||||
throw std::runtime_error("Failed to load UI file: " + path);
|
throw std::runtime_error("Failed to load dialogue file: " + path);
|
||||||
}
|
}
|
||||||
raw.assign(buf.begin(), buf.end());
|
raw.assign(buf.begin(), buf.end());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (const std::exception& e) {
|
catch (const std::exception& e) {
|
||||||
std::cerr << "UiManager: failed to open " << path << " : " << e.what() << std::endl;
|
std::cerr << "[dialogue] Failed to open " << path << ": " << e.what() << "\n";
|
||||||
throw std::runtime_error("Failed to load UI file: " + path);
|
throw std::runtime_error("Failed to load dialogue file: " + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
json root;
|
json root;
|
||||||
@ -283,15 +166,6 @@ bool DialogueDatabase::loadFromFile(const std::string& path) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.contains("cutscenes") && root["cutscenes"].is_array()) {
|
|
||||||
for (const auto& item : root["cutscenes"]) {
|
|
||||||
StaticCutsceneDefinition cutscene = parseCutscene(item);
|
|
||||||
if (!cutscene.id.empty()) {
|
|
||||||
cutscenes[cutscene.id] = std::move(cutscene);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return !dialogues.empty();
|
return !dialogues.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,9 +174,4 @@ const DialogueDefinition* DialogueDatabase::findDialogue(const std::string& id)
|
|||||||
return (it != dialogues.end()) ? &it->second : nullptr;
|
return (it != dialogues.end()) ? &it->second : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StaticCutsceneDefinition* DialogueDatabase::findCutscene(const std::string& id) const {
|
|
||||||
auto it = cutscenes.find(id);
|
|
||||||
return (it != cutscenes.end()) ? &it->second : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace ZL::Dialogue
|
} // namespace ZL::Dialogue
|
||||||
|
|||||||
@ -14,28 +14,19 @@ public:
|
|||||||
bool loadFromFile(const std::string& path);
|
bool loadFromFile(const std::string& path);
|
||||||
|
|
||||||
const DialogueDefinition* findDialogue(const std::string& id) const;
|
const DialogueDefinition* findDialogue(const std::string& id) const;
|
||||||
const StaticCutsceneDefinition* findCutscene(const std::string& id) const;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unordered_map<std::string, DialogueDefinition> dialogues;
|
std::unordered_map<std::string, DialogueDefinition> dialogues;
|
||||||
std::unordered_map<std::string, StaticCutsceneDefinition> cutscenes;
|
|
||||||
|
|
||||||
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);
|
||||||
static Choice parseChoice(const json& j);
|
static Choice parseChoice(const json& j);
|
||||||
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 CutsceneCameraPose parseCutsceneCameraPose(const json& j);
|
|
||||||
static CutsceneCameraSegment parseCutsceneCameraSegment(const json& j);
|
|
||||||
static CutsceneImageCue parseCutsceneImageCue(const json& j);
|
|
||||||
static StaticCutsceneDefinition parseCutscene(const json& j);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZL::Dialogue
|
} // namespace ZL::Dialogue
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
#include "Environment.h"
|
#include "Environment.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
namespace ZL
|
namespace ZL
|
||||||
{
|
{
|
||||||
@ -15,170 +14,51 @@ namespace ZL
|
|||||||
|
|
||||||
namespace ZL::Dialogue {
|
namespace ZL::Dialogue {
|
||||||
|
|
||||||
void DialogueOverlay::TexturedQuad::rebuild(const UiRect& newRect) {
|
|
||||||
rect = newRect;
|
|
||||||
mesh.data = CreateRect2D(
|
|
||||||
{ rect.x + rect.w * 0.5f, rect.y + rect.h * 0.5f },
|
|
||||||
{ rect.w * 0.5f, rect.h * 0.5f },
|
|
||||||
0.0f
|
|
||||||
);
|
|
||||||
mesh.RefreshVBO();
|
|
||||||
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;
|
||||||
|
|
||||||
textboxTexture = renderer.textureManager.LoadFromPng("resources/dialogue/textbox_bg.png", zipFile);
|
textboxTexture = renderer.textureManager.LoadFromPng("resources/dialogue/textbox_bg.png", zipFile);
|
||||||
//portraitFrameTexture = renderer.textureManager.LoadFromPng("resources/dialogue/portrait_frame.png", zipFile);
|
|
||||||
choiceMainTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_main.png", zipFile);
|
choiceMainTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_main.png", zipFile);
|
||||||
choiceOptionalTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_optional.png", zipFile);
|
choiceOptionalTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_optional.png", zipFile);
|
||||||
choiceSelectedTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_selected.png", zipFile);
|
choiceSelectedTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_selected.png", zipFile);
|
||||||
cutsceneSubtitleTexture = renderer.textureManager.LoadFromPng("resources/dialogue/cutscene_subtitle_bg.png", zipFile);
|
|
||||||
|
|
||||||
nameRenderer = std::make_unique<TextRenderer>();
|
nameRenderer = std::make_unique<TextRenderer>();
|
||||||
bodyRenderer = std::make_unique<TextRenderer>();
|
bodyRenderer = std::make_unique<TextRenderer>();
|
||||||
choiceRenderer = std::make_unique<TextRenderer>();
|
choiceRenderer = std::make_unique<TextRenderer>();
|
||||||
cutsceneRenderer = std::make_unique<TextRenderer>();
|
|
||||||
|
|
||||||
const bool ok =
|
return
|
||||||
nameRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 28, zipFile) &&
|
nameRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 28, zipFile) &&
|
||||||
bodyRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 24, zipFile) &&
|
bodyRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 24, zipFile) &&
|
||||||
choiceRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 22, zipFile) &&
|
choiceRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 22, zipFile);
|
||||||
cutsceneRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 24, zipFile);
|
|
||||||
|
|
||||||
return ok;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueOverlay::update(const PresentationModel& model, int deltaMs) {
|
void DialogueOverlay::update(const PresentationModel& model, int deltaMs) {
|
||||||
|
(void)deltaMs;
|
||||||
if (model.mode == PresentationMode::Hidden) {
|
if (model.mode == PresentationMode::Hidden) {
|
||||||
hoveredChoiceIndex = -1;
|
hoveredChoiceIndex = -1;
|
||||||
cutsceneSkipHintVisible = false;
|
|
||||||
cutsceneSkipArmed = false;
|
|
||||||
cutsceneSkipHolding = false;
|
|
||||||
cutsceneSkipTriggered = false;
|
|
||||||
cutsceneSkipHintRemainingMs = 0;
|
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
lastChoiceRects.clear();
|
lastChoiceRects.clear();
|
||||||
lastDialogueAdvanceRect = {};
|
lastDialogueAdvanceRect = {};
|
||||||
lastCutsceneAdvanceRect = {};
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.mode != PresentationMode::Choice) {
|
if (model.mode != PresentationMode::Choice) {
|
||||||
hoveredChoiceIndex = -1;
|
hoveredChoiceIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.mode != PresentationMode::Cutscene || !model.cutsceneSkippable) {
|
|
||||||
cutsceneSkipHintVisible = false;
|
|
||||||
cutsceneSkipArmed = false;
|
|
||||||
cutsceneSkipHolding = false;
|
|
||||||
cutsceneSkipTriggered = false;
|
|
||||||
cutsceneSkipHintRemainingMs = 0;
|
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int safeDeltaMs = max(deltaMs, 0);
|
|
||||||
|
|
||||||
if (cutsceneSkipHintVisible) {
|
|
||||||
cutsceneSkipHintRemainingMs -= safeDeltaMs;
|
|
||||||
if (cutsceneSkipHintRemainingMs <= 0) {
|
|
||||||
cutsceneSkipHintVisible = false;
|
|
||||||
cutsceneSkipArmed = false;
|
|
||||||
cutsceneSkipHolding = false;
|
|
||||||
cutsceneSkipHintRemainingMs = 0;
|
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cutsceneSkipHolding && cutsceneSkipArmed) {
|
|
||||||
cutsceneSkipHoldElapsedMs += safeDeltaMs;
|
|
||||||
if (cutsceneSkipHoldElapsedMs >= CutsceneSkipHoldDurationMs) {
|
|
||||||
cutsceneSkipTriggered = true;
|
|
||||||
cutsceneSkipHolding = false;
|
|
||||||
cutsceneSkipHoldElapsedMs = CutsceneSkipHoldDurationMs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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::Dialogue && model.mode != PresentationMode::Choice) {
|
||||||
lastChoiceRects.clear();
|
lastChoiceRects.clear();
|
||||||
lastDialogueAdvanceRect = {};
|
lastDialogueAdvanceRect = {};
|
||||||
lastCutsceneAdvanceRect = {};
|
|
||||||
cutsceneSkipHintVisible = false;
|
|
||||||
cutsceneSkipArmed = false;
|
|
||||||
cutsceneSkipHolding = false;
|
|
||||||
cutsceneSkipHintRemainingMs = 0;
|
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.mode == PresentationMode::Cutscene) {
|
|
||||||
drawCutscene(renderer, model);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
drawDialogue(renderer, model);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel& model) {
|
|
||||||
const float W = Environment::projectionWidth;
|
const float W = Environment::projectionWidth;
|
||||||
// const float H = Environment::projectionHeight;
|
|
||||||
|
|
||||||
UiRect portraitRect{ 24.0f + 90, 24.0f + 16, 176.0f, 176.0f };
|
UiRect portraitRect{ 24.0f + 90, 24.0f + 16, 176.0f, 176.0f };
|
||||||
//const UiRect textboxRect{ 220.0f, 24.0f, max(200.0f, W - 244.0f), 182.0f };
|
|
||||||
UiRect textboxRect{ 30.f, -48.f, 1222.f, 340.0f };
|
UiRect textboxRect{ 30.f, -48.f, 1222.f, 340.0f };
|
||||||
|
|
||||||
|
|
||||||
lastDialogueAdvanceRect = { portraitRect.x, portraitRect.y, textboxRect.x + textboxRect.w - portraitRect.x, textboxRect.h };
|
lastDialogueAdvanceRect = { portraitRect.x, portraitRect.y, textboxRect.x + textboxRect.w - portraitRect.x, textboxRect.h };
|
||||||
lastCutsceneAdvanceRect = {};
|
|
||||||
cutsceneSkipHintVisible = false;
|
|
||||||
cutsceneSkipArmed = false;
|
|
||||||
cutsceneSkipHolding = false;
|
|
||||||
cutsceneSkipHintRemainingMs = 0;
|
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
|
|
||||||
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) {
|
||||||
@ -197,9 +77,16 @@ void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel&
|
|||||||
renderer.LoadIdentity();
|
renderer.LoadIdentity();
|
||||||
renderer.RenderUniform1f("uAlpha", 1.0f);
|
renderer.RenderUniform1f("uAlpha", 1.0f);
|
||||||
|
|
||||||
drawQuad(renderer, textboxQuad, textboxTexture);
|
glBindTexture(GL_TEXTURE_2D, textboxTexture->getTexID());
|
||||||
//drawQuad(renderer, portraitQuad, model.portraitPath.empty() ? portraitFrameTexture : loadTextureCached(model.portraitPath));
|
renderer.DrawVertexRenderStruct(textboxQuad.mesh);
|
||||||
drawQuad(renderer, portraitQuad, loadTextureCached(model.portraitPath));
|
|
||||||
|
{
|
||||||
|
auto portrait = loadTextureCached(model.portraitPath);
|
||||||
|
if (portrait) {
|
||||||
|
glBindTexture(GL_TEXTURE_2D, portrait->getTexID());
|
||||||
|
renderer.DrawVertexRenderStruct(portraitQuad.mesh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderer.PopMatrix();
|
renderer.PopMatrix();
|
||||||
renderer.PopProjectionMatrix();
|
renderer.PopProjectionMatrix();
|
||||||
@ -214,11 +101,9 @@ void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel&
|
|||||||
nameRenderer->drawText(model.speaker, nameX, nameY, 1.0f, false, { 1.0f, 0.88f, 0.45f, 1.0f });
|
nameRenderer->drawText(model.speaker, nameX, nameY, 1.0f, false, { 1.0f, 0.88f, 0.45f, 1.0f });
|
||||||
}
|
}
|
||||||
|
|
||||||
const float bodyTextScale = 1.0f;
|
const float bodyMaxWidthPx = W - nameX - 48.f - ZL::x - 60.f;
|
||||||
const float bodyMaxWidthPx = W - nameX - 48.f-x-60.f;
|
const std::string wrappedBody = wrapTextToWidth(model.visibleText, *bodyRenderer, bodyMaxWidthPx, 1.0f);
|
||||||
|
bodyRenderer->drawText(wrappedBody, bodyX, bodyY, 1.0f, false, { 1.0f, 1.0f, 1.0f, 1.0f });
|
||||||
const std::string wrappedBody = wrapTextToWidth(model.visibleText, *bodyRenderer, bodyMaxWidthPx, bodyTextScale);
|
|
||||||
bodyRenderer->drawText(wrappedBody, bodyX, bodyY, bodyTextScale, false, { 1.0f, 1.0f, 1.0f, 1.0f });
|
|
||||||
|
|
||||||
lastChoiceRects.clear();
|
lastChoiceRects.clear();
|
||||||
if (model.mode == PresentationMode::Choice) {
|
if (model.mode == PresentationMode::Choice) {
|
||||||
@ -238,17 +123,17 @@ void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel&
|
|||||||
renderer.LoadIdentity();
|
renderer.LoadIdentity();
|
||||||
|
|
||||||
for (size_t i = 0; i < model.choices.size(); ++i) {
|
for (size_t i = 0; i < model.choices.size(); ++i) {
|
||||||
const float y = choiceStartY + (choiceHeight + choiceSpacing) * static_cast<float>(model.choices.size() - 1 - i);
|
const float cy = choiceStartY + (choiceHeight + choiceSpacing) * static_cast<float>(model.choices.size() - 1 - i);
|
||||||
UiRect rect{ textboxRect.x + 20.0f, y, choiceWidth, choiceHeight };
|
UiRect rect{ textboxRect.x + 20.0f, cy, choiceWidth, choiceHeight };
|
||||||
lastChoiceRects.push_back(rect);
|
lastChoiceRects.push_back(rect);
|
||||||
choiceQuads[i].rebuild(rect);
|
choiceQuads[i].rebuild(rect);
|
||||||
|
|
||||||
const bool isHighlighted = static_cast<int>(i) == hoveredChoiceIndex || static_cast<int>(i) == model.selectedChoice;
|
const bool isHighlighted = static_cast<int>(i) == hoveredChoiceIndex || static_cast<int>(i) == model.selectedChoice;
|
||||||
std::shared_ptr<Texture> choiceTexture = (model.choices[i].kind == ChoiceKind::Optional) ? choiceOptionalTexture : choiceMainTexture;
|
std::shared_ptr<Texture> choiceTexture =
|
||||||
if (isHighlighted) {
|
(model.choices[i].kind == ChoiceKind::Optional) ? choiceOptionalTexture : choiceMainTexture;
|
||||||
choiceTexture = choiceSelectedTexture;
|
if (isHighlighted) choiceTexture = choiceSelectedTexture;
|
||||||
}
|
glBindTexture(GL_TEXTURE_2D, choiceTexture->getTexID());
|
||||||
drawQuad(renderer, choiceQuads[i], choiceTexture);
|
renderer.DrawVertexRenderStruct(choiceQuads[i].mesh);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.PopMatrix();
|
renderer.PopMatrix();
|
||||||
@ -262,22 +147,9 @@ void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel&
|
|||||||
? std::array<float, 4>{0.82f, 0.82f, 0.82f, 1.0f}
|
? std::array<float, 4>{0.82f, 0.82f, 0.82f, 1.0f}
|
||||||
: std::array<float, 4>{ 1.0f, 0.93f, 0.65f, 1.0f };
|
: std::array<float, 4>{ 1.0f, 0.93f, 0.65f, 1.0f };
|
||||||
|
|
||||||
const float choiceTextScale = 1.0f;
|
const std::string wrappedChoice = wrapTextToWidth(model.choices[i].text, *choiceRenderer, rect.w - 28.0f, 1.0f);
|
||||||
const float choiceMaxWidthPx = rect.w - 28.0f;
|
|
||||||
|
|
||||||
const std::string wrappedChoiceText = wrapTextToWidth(
|
|
||||||
model.choices[i].text,
|
|
||||||
*choiceRenderer,
|
|
||||||
choiceMaxWidthPx,
|
|
||||||
choiceTextScale
|
|
||||||
);
|
|
||||||
|
|
||||||
choiceRenderer->drawText(
|
choiceRenderer->drawText(
|
||||||
wrappedChoiceText,
|
wrappedChoice, rect.x + 14.0f, rect.y + 9.0f, 1.0f, false,
|
||||||
rect.x + 14.0f,
|
|
||||||
rect.y + 9.0f,
|
|
||||||
choiceTextScale,
|
|
||||||
false,
|
|
||||||
isHighlighted ? std::array<float, 4>{1.0f, 1.0f, 1.0f, 1.0f} : color
|
isHighlighted ? std::array<float, 4>{1.0f, 1.0f, 1.0f, 1.0f} : color
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -286,345 +158,9 @@ 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) {
|
|
||||||
const float W = Environment::projectionWidth;
|
|
||||||
const float H = Environment::projectionHeight;
|
|
||||||
const UiRect subtitleRect{ W * 0.12f, 22.0f, W * 0.76f, 110.0f };
|
|
||||||
|
|
||||||
lastDialogueAdvanceRect = {};
|
|
||||||
lastCutsceneAdvanceRect = subtitleRect;
|
|
||||||
|
|
||||||
glEnable(GL_BLEND);
|
|
||||||
|
|
||||||
renderer.shaderManager.PushShader("cutsceneFade");
|
|
||||||
renderer.RenderUniform1i(textureUniformName, 0);
|
|
||||||
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
|
|
||||||
renderer.PushMatrix();
|
|
||||||
renderer.LoadIdentity();
|
|
||||||
|
|
||||||
const UiRect screenRect{ 0.0f, 0.0f, W, H };
|
|
||||||
|
|
||||||
std::vector<PresentedCutsceneImage> imageLayers = model.cutsceneImages;
|
|
||||||
if (imageLayers.empty() && !model.backgroundPath.empty()) {
|
|
||||||
imageLayers.push_back({ model.backgroundPath, 1.0f });
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const PresentedCutsceneImage& layer : imageLayers) {
|
|
||||||
const auto texture = loadTextureCached(layer.path);
|
|
||||||
if (!texture) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logical content dimensions for camera and UV math.
|
|
||||||
// backgroundWidth/backgroundHeight define the coordinate space the camera track was authored in;
|
|
||||||
// the texture is assumed to fill this space entirely regardless of its actual pixel dimensions.
|
|
||||||
const float imgW = (model.backgroundWidth > 0) ? static_cast<float>(model.backgroundWidth) : static_cast<float>(texture->getWidth());
|
|
||||||
const float imgH = (model.backgroundHeight > 0) ? static_cast<float>(model.backgroundHeight) : static_cast<float>(texture->getHeight());
|
|
||||||
|
|
||||||
ResolvedViewport layerViewport{};
|
|
||||||
|
|
||||||
if (model.cutsceneCamera.active) {
|
|
||||||
const ResolvedViewport fromViewport = resolveViewportPose(model.cutsceneCamera.from, imgW, imgH, W, H);
|
|
||||||
const ResolvedViewport toViewport = resolveViewportPose(model.cutsceneCamera.to, imgW, imgH, W, H);
|
|
||||||
layerViewport = blendViewport(
|
|
||||||
fromViewport,
|
|
||||||
toViewport,
|
|
||||||
std::clamp(model.cutsceneCamera.t, 0.0f, 1.0f)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
layerViewport = resolveViewportPose(CutsceneCameraPose{}, imgW, imgH, W, H);
|
|
||||||
}
|
|
||||||
|
|
||||||
const float halfW = layerViewport.widthPx * 0.5f;
|
|
||||||
const float halfH = layerViewport.heightPx * 0.5f;
|
|
||||||
const float rotationRad = layerViewport.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 {
|
|
||||||
layerViewport.centerXPx + x * c - y * s,
|
|
||||||
layerViewport.centerYPx + x * s + y * c
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Source viewport corners in logical image space, then converted to UV via actual texture size.
|
|
||||||
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(imgW, 1.0f), 0.0f, 1.0f),
|
|
||||||
std::clamp(p.y() / max(imgH, 1.0f), 0.0f, 1.0f)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
backgroundQuad.rebuildWithUV(
|
|
||||||
screenRect,
|
|
||||||
toUV(srcBL),
|
|
||||||
toUV(srcTL),
|
|
||||||
toUV(srcTR),
|
|
||||||
toUV(srcBR)
|
|
||||||
);
|
|
||||||
|
|
||||||
renderer.RenderUniform1f("uAlpha", std::clamp(layer.alpha * model.cutsceneGlobalFadeAlpha, 0.0f, 1.0f));
|
|
||||||
drawQuad(renderer, backgroundQuad, texture);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.PopMatrix();
|
|
||||||
renderer.PopProjectionMatrix();
|
|
||||||
renderer.shaderManager.PopShader();
|
|
||||||
|
|
||||||
// Black overlay: fades IN over the game world (phase 1), then fades OUT to reveal cutscene (phase 2).
|
|
||||||
// Image renders at cutsceneGlobalFadeAlpha (0 during phase 1 so game world shows through for the
|
|
||||||
// fade-to-black effect, 1 during phase 2 so it blocks the world while black peels away).
|
|
||||||
if (model.cutsceneBlackAlpha > 0.001f) {
|
|
||||||
const float blackAlpha = model.cutsceneBlackAlpha;
|
|
||||||
renderer.shaderManager.PushShader("cutsceneBlack");
|
|
||||||
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
|
|
||||||
renderer.PushMatrix();
|
|
||||||
renderer.LoadIdentity();
|
|
||||||
|
|
||||||
backgroundQuad.rebuild(screenRect);
|
|
||||||
renderer.RenderUniform1f("uAlpha", blackAlpha);
|
|
||||||
renderer.DrawVertexRenderStruct(backgroundQuad.mesh);
|
|
||||||
|
|
||||||
renderer.PopMatrix();
|
|
||||||
renderer.PopProjectionMatrix();
|
|
||||||
renderer.shaderManager.PopShader();
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI quads over the image: subtitle panel and skip progress hint background.
|
|
||||||
renderer.shaderManager.PushShader(defaultShaderName);
|
|
||||||
renderer.RenderUniform1i(textureUniformName, 0);
|
|
||||||
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
|
|
||||||
renderer.PushMatrix();
|
|
||||||
renderer.LoadIdentity();
|
|
||||||
|
|
||||||
if (model.showCutsceneSubtitle) {
|
|
||||||
subtitleQuad.rebuild(subtitleRect);
|
|
||||||
drawQuad(renderer, subtitleQuad, cutsceneSubtitleTexture);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.cutsceneSkippable && cutsceneSkipHintVisible) {
|
|
||||||
const UiRect hintBg{ W - 250.0f, H - 62.0f, 226.0f, 42.0f };
|
|
||||||
skipHintBgQuad.rebuild(hintBg);
|
|
||||||
drawQuad(renderer, skipHintBgQuad, choiceOptionalTexture);
|
|
||||||
|
|
||||||
const UiRect progressBg{ W - 232.0f, H - 34.0f, 190.0f, 7.0f };
|
|
||||||
skipProgressBgQuad.rebuild(progressBg);
|
|
||||||
drawQuad(renderer, skipProgressBgQuad, choiceOptionalTexture);
|
|
||||||
|
|
||||||
if (cutsceneSkipHolding) {
|
|
||||||
const float progress = std::clamp(
|
|
||||||
static_cast<float>(cutsceneSkipHoldElapsedMs) / static_cast<float>(CutsceneSkipHoldDurationMs),
|
|
||||||
0.0f,
|
|
||||||
1.0f
|
|
||||||
);
|
|
||||||
const UiRect progressFill{ progressBg.x, progressBg.y, progressBg.w * progress, progressBg.h };
|
|
||||||
skipProgressFillQuad.rebuild(progressFill);
|
|
||||||
drawQuad(renderer, skipProgressFillQuad, choiceMainTexture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.PopMatrix();
|
|
||||||
renderer.PopProjectionMatrix();
|
|
||||||
renderer.shaderManager.PopShader();
|
|
||||||
|
|
||||||
if (model.showCutsceneSubtitle) {
|
|
||||||
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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const float subtitleTextScale = 1.0f;
|
|
||||||
const float subtitleMaxWidthPx = subtitleRect.w - 48.0f;
|
|
||||||
|
|
||||||
const std::string wrappedSubtitle = wrapTextToWidth(
|
|
||||||
model.visibleText,
|
|
||||||
*cutsceneRenderer,
|
|
||||||
subtitleMaxWidthPx,
|
|
||||||
subtitleTextScale
|
|
||||||
);
|
|
||||||
|
|
||||||
cutsceneRenderer->drawText(
|
|
||||||
wrappedSubtitle,
|
|
||||||
subtitleRect.x + 24.0f,
|
|
||||||
subtitleRect.y + 30.0f,
|
|
||||||
subtitleTextScale,
|
|
||||||
false,
|
|
||||||
{ 1.0f, 1.0f, 1.0f, 1.0f }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.cutsceneSkippable && cutsceneSkipHintVisible) {
|
|
||||||
choiceRenderer->drawText(
|
|
||||||
cutsceneSkipHolding ? "Hold to skip..." : "Hold LMB to skip",
|
|
||||||
W - 232.0f,
|
|
||||||
H - 50.0f,
|
|
||||||
0.85f,
|
|
||||||
false,
|
|
||||||
{ 1.0f, 1.0f, 1.0f, 0.95f }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
glDisable(GL_BLEND);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DialogueOverlay::consumeSkipRequested() {
|
|
||||||
const bool result = cutsceneSkipTriggered;
|
|
||||||
cutsceneSkipTriggered = false;
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
cutsceneSkipHintVisible = false;
|
|
||||||
cutsceneSkipArmed = false;
|
|
||||||
cutsceneSkipHolding = false;
|
|
||||||
cutsceneSkipHintRemainingMs = 0;
|
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DialogueOverlay::handlePointerDown(float x, float y, const PresentationModel& model) {
|
void DialogueOverlay::handlePointerDown(float x, float y, const PresentationModel& model) {
|
||||||
(void)x;
|
|
||||||
(void)y;
|
|
||||||
|
|
||||||
if (model.mode == PresentationMode::Choice) {
|
if (model.mode == PresentationMode::Choice) {
|
||||||
handlePointerMoved(x, y, model);
|
handlePointerMoved(x, y, model);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.mode == PresentationMode::Cutscene && model.cutsceneSkippable) {
|
|
||||||
// First click/tap only arms skip and shows the hint for a short time.
|
|
||||||
// It does not immediately start skipping, to avoid accidental skip.
|
|
||||||
if (!cutsceneSkipArmed) {
|
|
||||||
cutsceneSkipHintVisible = true;
|
|
||||||
cutsceneSkipArmed = true;
|
|
||||||
cutsceneSkipHolding = false;
|
|
||||||
cutsceneSkipTriggered = false;
|
|
||||||
cutsceneSkipHintRemainingMs = CutsceneSkipHintDurationMs;
|
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Once armed, holding anywhere on the screen starts skip progress.
|
|
||||||
cutsceneSkipHintVisible = true;
|
|
||||||
cutsceneSkipHintRemainingMs = CutsceneSkipHintDurationMs;
|
|
||||||
cutsceneSkipHolding = true;
|
|
||||||
cutsceneSkipTriggered = false;
|
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -639,11 +175,12 @@ void DialogueOverlay::handlePointerMoved(float x, float y, const PresentationMod
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
hoveredChoiceIndex = -1;
|
hoveredChoiceIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DialogueOverlay::handlePointerReleased(float x, float y, const PresentationModel& model, int& outChoiceIndex, bool& outAdvanceDialogue) {
|
bool DialogueOverlay::handlePointerReleased(float x, float y, const PresentationModel& model,
|
||||||
|
int& outChoiceIndex, bool& outAdvanceDialogue)
|
||||||
|
{
|
||||||
outChoiceIndex = -1;
|
outChoiceIndex = -1;
|
||||||
outAdvanceDialogue = false;
|
outAdvanceDialogue = false;
|
||||||
|
|
||||||
@ -662,68 +199,21 @@ bool DialogueOverlay::handlePointerReleased(float x, float y, const Presentation
|
|||||||
return outAdvanceDialogue;
|
return outAdvanceDialogue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.mode == PresentationMode::Cutscene) {
|
|
||||||
if (cutsceneSkipHolding && cutsceneSkipHoldElapsedMs < CutsceneSkipHoldDurationMs) {
|
|
||||||
cutsceneSkipHolding = false;
|
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<Texture> DialogueOverlay::loadTextureCached(const std::string& path) {
|
std::shared_ptr<Texture> DialogueOverlay::loadTextureCached(const std::string& path) {
|
||||||
if (path.empty()) {
|
if (path.empty()) return nullptr;
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return rendererRef->textureManager.LoadFromPng(path, zipFilename);
|
return rendererRef->textureManager.LoadFromPng(path, zipFilename);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueOverlay::drawQuad(Renderer& renderer, const TexturedQuad& quad, const std::shared_ptr<Texture>& texture) const {
|
std::string DialogueOverlay::wrapTextToWidth(
|
||||||
if (!texture) {
|
const std::string& input,
|
||||||
return;
|
const TextRenderer& textRenderer,
|
||||||
}
|
float maxWidthPx,
|
||||||
|
float scale)
|
||||||
glBindTexture(GL_TEXTURE_2D, texture->getTexID());
|
|
||||||
renderer.DrawVertexRenderStruct(quad.mesh);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string DialogueOverlay::wrapText(const std::string& input, size_t maxLineLength) {
|
|
||||||
if (input.size() <= maxLineLength) {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string output;
|
|
||||||
size_t lineStart = 0;
|
|
||||||
while (lineStart < input.size()) {
|
|
||||||
size_t lineEnd = min(lineStart + maxLineLength, input.size());
|
|
||||||
if (lineEnd < input.size()) {
|
|
||||||
const size_t split = input.rfind(' ', lineEnd);
|
|
||||||
if (split != std::string::npos && split > lineStart) {
|
|
||||||
lineEnd = split;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output.append(input.substr(lineStart, lineEnd - lineStart));
|
|
||||||
if (lineEnd < input.size()) {
|
|
||||||
output.push_back('\n');
|
|
||||||
lineStart = lineEnd + (input[lineEnd] == ' ' ? 1 : 0);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
lineStart = lineEnd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string DialogueOverlay::wrapTextToWidth(const std::string& input, const TextRenderer& textRenderer, float maxWidthPx, float scale)
|
|
||||||
{
|
{
|
||||||
if (input.empty() || maxWidthPx <= 1.0f) {
|
if (input.empty() || maxWidthPx <= 1.0f) return input;
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string output;
|
std::string output;
|
||||||
std::string currentLine;
|
std::string currentLine;
|
||||||
@ -731,24 +221,15 @@ std::string DialogueOverlay::wrapTextToWidth(const std::string& input, const Tex
|
|||||||
|
|
||||||
auto flushLine = [&]() {
|
auto flushLine = [&]() {
|
||||||
if (!currentLine.empty()) {
|
if (!currentLine.empty()) {
|
||||||
if (!output.empty()) {
|
if (!output.empty()) output.push_back('\n');
|
||||||
output.push_back('\n');
|
|
||||||
}
|
|
||||||
output += currentLine;
|
output += currentLine;
|
||||||
currentLine.clear();
|
currentLine.clear();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto pushWord = [&](const std::string& word) {
|
auto pushWord = [&](const std::string& word) {
|
||||||
if (word.empty()) {
|
if (word.empty()) return;
|
||||||
return;
|
if (currentLine.empty()) { currentLine = word; return; }
|
||||||
}
|
|
||||||
|
|
||||||
if (currentLine.empty()) {
|
|
||||||
currentLine = word;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string candidate = currentLine + " " + word;
|
const std::string candidate = currentLine + " " + word;
|
||||||
if (textRenderer.measureTextWidth(candidate, scale) <= maxWidthPx) {
|
if (textRenderer.measureTextWidth(candidate, scale) <= maxWidthPx) {
|
||||||
currentLine = candidate;
|
currentLine = candidate;
|
||||||
@ -761,23 +242,10 @@ std::string DialogueOverlay::wrapTextToWidth(const std::string& input, const Tex
|
|||||||
|
|
||||||
for (size_t i = 0; i < input.size(); ++i) {
|
for (size_t i = 0; i < input.size(); ++i) {
|
||||||
const char ch = input[i];
|
const char ch = input[i];
|
||||||
|
if (ch == '\n') { pushWord(currentWord); currentWord.clear(); flushLine(); continue; }
|
||||||
if (ch == '\n') {
|
if (ch == ' ' || ch == '\t' || ch == '\r') { pushWord(currentWord); currentWord.clear(); continue; }
|
||||||
pushWord(currentWord);
|
|
||||||
currentWord.clear();
|
|
||||||
flushLine();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ch == ' ' || ch == '\t' || ch == '\r') {
|
|
||||||
pushWord(currentWord);
|
|
||||||
currentWord.clear();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentWord.push_back(ch);
|
currentWord.push_back(ch);
|
||||||
}
|
}
|
||||||
|
|
||||||
pushWord(currentWord);
|
pushWord(currentWord);
|
||||||
flushLine();
|
flushLine();
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
#include "render/Renderer.h"
|
#include "render/Renderer.h"
|
||||||
#include "render/TextRenderer.h"
|
#include "render/TextRenderer.h"
|
||||||
#include "render/TextureManager.h"
|
#include "render/TextureManager.h"
|
||||||
|
#include "render/UiQuad.h"
|
||||||
#include "UiManager.h"
|
#include "UiManager.h"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
@ -20,98 +21,34 @@ public:
|
|||||||
void handlePointerDown(float x, float y, const PresentationModel& model);
|
void handlePointerDown(float x, float y, const PresentationModel& model);
|
||||||
void handlePointerMoved(float x, float y, const PresentationModel& model);
|
void handlePointerMoved(float x, float y, const PresentationModel& model);
|
||||||
bool handlePointerReleased(float x, float y, const PresentationModel& model, int& outChoiceIndex, bool& outAdvanceDialogue);
|
bool handlePointerReleased(float x, float y, const PresentationModel& model, int& outChoiceIndex, bool& outAdvanceDialogue);
|
||||||
bool consumeSkipRequested();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct TexturedQuad {
|
|
||||||
UiRect rect;
|
|
||||||
VertexRenderStruct mesh;
|
|
||||||
bool initialized = false;
|
|
||||||
|
|
||||||
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;
|
||||||
std::string zipFilename;
|
std::string zipFilename;
|
||||||
|
|
||||||
std::shared_ptr<Texture> textboxTexture;
|
std::shared_ptr<Texture> textboxTexture;
|
||||||
//std::shared_ptr<Texture> portraitFrameTexture;
|
|
||||||
std::shared_ptr<Texture> choiceMainTexture;
|
std::shared_ptr<Texture> choiceMainTexture;
|
||||||
std::shared_ptr<Texture> choiceOptionalTexture;
|
std::shared_ptr<Texture> choiceOptionalTexture;
|
||||||
std::shared_ptr<Texture> choiceSelectedTexture;
|
std::shared_ptr<Texture> choiceSelectedTexture;
|
||||||
std::shared_ptr<Texture> cutsceneSubtitleTexture;
|
|
||||||
|
|
||||||
mutable std::vector<UiRect> lastChoiceRects;
|
mutable std::vector<UiRect> lastChoiceRects;
|
||||||
mutable UiRect lastDialogueAdvanceRect{};
|
mutable UiRect lastDialogueAdvanceRect{};
|
||||||
mutable UiRect lastCutsceneAdvanceRect{};
|
|
||||||
mutable UiRect lastCutsceneSkipRect{};
|
|
||||||
|
|
||||||
int hoveredChoiceIndex = -1;
|
int hoveredChoiceIndex = -1;
|
||||||
|
|
||||||
// Cutscene skip UX:
|
|
||||||
// First LMB/tap anywhere arms skip and shows a hint for 5 seconds.
|
|
||||||
// While armed, holding LMB/touch anywhere for 3.5 seconds requests skip.
|
|
||||||
bool cutsceneSkipHintVisible = false;
|
|
||||||
bool cutsceneSkipArmed = false;
|
|
||||||
bool cutsceneSkipHolding = false;
|
|
||||||
bool cutsceneSkipTriggered = false;
|
|
||||||
int cutsceneSkipHintRemainingMs = 0;
|
|
||||||
int cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
static constexpr int CutsceneSkipHintDurationMs = 5000;
|
|
||||||
static constexpr int CutsceneSkipHoldDurationMs = 3500;
|
|
||||||
|
|
||||||
std::unique_ptr<TextRenderer> nameRenderer;
|
std::unique_ptr<TextRenderer> nameRenderer;
|
||||||
std::unique_ptr<TextRenderer> bodyRenderer;
|
std::unique_ptr<TextRenderer> bodyRenderer;
|
||||||
std::unique_ptr<TextRenderer> choiceRenderer;
|
std::unique_ptr<TextRenderer> choiceRenderer;
|
||||||
std::unique_ptr<TextRenderer> cutsceneRenderer;
|
|
||||||
|
|
||||||
TexturedQuad portraitQuad;
|
UiQuad portraitQuad;
|
||||||
TexturedQuad textboxQuad;
|
UiQuad textboxQuad;
|
||||||
TexturedQuad subtitleQuad;
|
mutable std::vector<UiQuad> choiceQuads;
|
||||||
TexturedQuad backgroundQuad;
|
|
||||||
TexturedQuad skipHintBgQuad;
|
|
||||||
TexturedQuad skipProgressBgQuad;
|
|
||||||
TexturedQuad skipProgressFillQuad;
|
|
||||||
mutable std::vector<TexturedQuad> choiceQuads;
|
|
||||||
|
|
||||||
void drawDialogue(Renderer& renderer, const PresentationModel& model);
|
|
||||||
void drawCutscene(Renderer& renderer, const PresentationModel& model);
|
|
||||||
|
|
||||||
std::shared_ptr<Texture> loadTextureCached(const std::string& path);
|
std::shared_ptr<Texture> loadTextureCached(const std::string& path);
|
||||||
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 wrapTextToWidth(const std::string& input, const TextRenderer& textRenderer,
|
||||||
static std::string wrapTextToWidth(const std::string& input, const TextRenderer& textRenderer, float maxWidthPx, float scale);
|
float maxWidthPx, float scale);
|
||||||
static bool rectContains(const UiRect& rect, float x, float y);
|
static bool rectContains(const UiRect& rect, float x, float y);
|
||||||
|
|
||||||
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,7 +1,6 @@
|
|||||||
#include "dialogue/DialogueRuntime.h"
|
#include "dialogue/DialogueRuntime.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
namespace ZL::Dialogue {
|
namespace ZL::Dialogue {
|
||||||
@ -34,84 +33,52 @@ bool DialogueRuntime::startDialogue(const std::string& dialogueId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
activeDialogue = dialogue;
|
activeDialogue = dialogue;
|
||||||
activeCutscene = nullptr;
|
|
||||||
currentNodeId.clear();
|
currentNodeId.clear();
|
||||||
pendingNodeAfterCutscene.clear();
|
|
||||||
visibleChoices.clear();
|
visibleChoices.clear();
|
||||||
selectedChoice = -1;
|
selectedChoice = -1;
|
||||||
revealCharacters = 0.0f;
|
revealCharacters = 0.0f;
|
||||||
currentCutsceneLine = -1;
|
|
||||||
cutsceneTimerMs = 0;
|
|
||||||
cutsceneElapsedMs = 0;
|
|
||||||
cutsceneTotalDurationMs = 0;
|
|
||||||
cutsceneContentDurationMs = 0;
|
|
||||||
currentCutsceneBackground.clear();
|
|
||||||
fadeInCallbackFired = false;
|
|
||||||
presentation = {};
|
presentation = {};
|
||||||
presentation.dialogueId = dialogue->id;
|
presentation.dialogueId = dialogue->id;
|
||||||
|
|
||||||
return enterNode(dialogue->startNode);
|
return enterNode(dialogue->startNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DialogueRuntime::startStandaloneCutscene(const std::string& cutsceneId) {
|
void DialogueRuntime::stop() {
|
||||||
if (!database) {
|
|
||||||
std::cerr << "[dialogue] No database assigned to runtime\n";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const StaticCutsceneDefinition* def = database->findCutscene(cutsceneId);
|
|
||||||
if (!def) {
|
|
||||||
std::cerr << "[dialogue] Cutscene not found: " << cutsceneId << "\n";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
activeDialogue = nullptr;
|
activeDialogue = nullptr;
|
||||||
activeCutsceneId = cutsceneId;
|
currentNodeId.clear();
|
||||||
fadeInCallbackFired = false;
|
visibleChoices.clear();
|
||||||
|
selectedChoice = -1;
|
||||||
|
revealCharacters = 0.0f;
|
||||||
|
mode = Mode::Inactive;
|
||||||
presentation = {};
|
presentation = {};
|
||||||
startCutscene(cutsceneId, "");
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueRuntime::setOnCutsceneFinished(std::function<void(const std::string&)> cb) {
|
void DialogueRuntime::resumeFromNode(const std::string& nodeId) {
|
||||||
onCutsceneFinished = std::move(cb);
|
if (mode != Mode::WaitingForCutscene) return;
|
||||||
|
if (nodeId.empty()) {
|
||||||
|
stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
enterNode(nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DialogueRuntime::setOnCutsceneStartNeeded(
|
||||||
|
std::function<void(const std::string&, const std::string&)> cb)
|
||||||
|
{
|
||||||
|
onCutsceneStartNeeded = std::move(cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueRuntime::setOnDialogueLineStarted(std::function<void(const std::string&)> cb) {
|
void DialogueRuntime::setOnDialogueLineStarted(std::function<void(const std::string&)> cb) {
|
||||||
onDialogueLineStarted = std::move(cb);
|
onDialogueLineStarted = std::move(cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueRuntime::setOnCutsceneLineStarted(std::function<void(const std::string&)> cb) {
|
|
||||||
onCutsceneLineStarted = std::move(cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DialogueRuntime::setOnCutsceneFadeInComplete(std::function<void(const std::string&)> cb) {
|
|
||||||
onCutsceneFadeInComplete = std::move(cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DialogueRuntime::setOnChatBubbleReady(std::function<void(const std::string&, bool)> cb) {
|
void DialogueRuntime::setOnChatBubbleReady(std::function<void(const std::string&, bool)> cb) {
|
||||||
onChatBubbleReady = std::move(cb);
|
onChatBubbleReady = std::move(cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueRuntime::stop() {
|
|
||||||
activeDialogue = nullptr;
|
|
||||||
activeCutscene = nullptr;
|
|
||||||
currentNodeId.clear();
|
|
||||||
pendingNodeAfterCutscene.clear();
|
|
||||||
visibleChoices.clear();
|
|
||||||
selectedChoice = -1;
|
|
||||||
revealCharacters = 0.0f;
|
|
||||||
currentCutsceneLine = -1;
|
|
||||||
cutsceneTimerMs = 0;
|
|
||||||
cutsceneElapsedMs = 0;
|
|
||||||
cutsceneTotalDurationMs = 0;
|
|
||||||
cutsceneContentDurationMs = 0;
|
|
||||||
currentCutsceneBackground.clear();
|
|
||||||
fadeInCallbackFired = false;
|
|
||||||
mode = Mode::Inactive;
|
|
||||||
presentation = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
void DialogueRuntime::update(int deltaMs) {
|
void DialogueRuntime::update(int deltaMs) {
|
||||||
if (mode == Mode::PresentingLine) {
|
if (mode != Mode::PresentingLine) return;
|
||||||
|
|
||||||
if (!presentation.revealCompleted) {
|
if (!presentation.revealCompleted) {
|
||||||
revealCharacters += revealSpeedCharsPerSecond * (static_cast<float>(deltaMs) / 1000.0f);
|
revealCharacters += revealSpeedCharsPerSecond * (static_cast<float>(deltaMs) / 1000.0f);
|
||||||
const size_t fullLen = presentation.fullText.size();
|
const size_t fullLen = presentation.fullText.size();
|
||||||
@ -119,77 +86,6 @@ void DialogueRuntime::update(int deltaMs) {
|
|||||||
presentation.visibleText = presentation.fullText.substr(0, visibleLen);
|
presentation.visibleText = presentation.fullText.substr(0, visibleLen);
|
||||||
presentation.revealCompleted = (visibleLen >= fullLen);
|
presentation.revealCompleted = (visibleLen >= fullLen);
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode == Mode::PlayingCutscene && activeCutscene) {
|
|
||||||
cutsceneElapsedMs += deltaMs;
|
|
||||||
|
|
||||||
if (!activeCutscene->lines.empty() &&
|
|
||||||
currentCutsceneLine >= 0 &&
|
|
||||||
currentCutsceneLine < static_cast<int>(activeCutscene->lines.size())) {
|
|
||||||
|
|
||||||
const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
|
|
||||||
if (!line.waitForConfirm) {
|
|
||||||
cutsceneTimerMs += deltaMs;
|
|
||||||
const int durationMs =
|
|
||||||
(line.durationMs > 0)
|
|
||||||
? line.durationMs
|
|
||||||
: computeFallbackCutsceneDurationMs(line.text);
|
|
||||||
|
|
||||||
if (cutsceneTimerMs >= durationMs) {
|
|
||||||
advanceCutsceneLine();
|
|
||||||
|
|
||||||
// ВАЖНО: после advance катсцена могла завершиться
|
|
||||||
if (!activeCutscene || mode != Mode::PlayingCutscene) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!activeCutscene || mode != Mode::PlayingCutscene) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshCutscenePresentation();
|
|
||||||
|
|
||||||
if (!activeCutscene || mode != Mode::PlayingCutscene) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fadeInCallbackFired && onCutsceneFadeInComplete && activeCutscene) {
|
|
||||||
const int fadeInCompleteMs = activeCutscene->fadeOutMs + activeCutscene->fadeInMs;
|
|
||||||
if (cutsceneElapsedMs >= fadeInCompleteMs && !activeCutscene->onFadeInCallback.empty()) {
|
|
||||||
fadeInCallbackFired = true;
|
|
||||||
onCutsceneFadeInComplete(activeCutscene->onFadeInCallback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!activeCutscene || mode != Mode::PlayingCutscene) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueRuntime::confirmAdvance() {
|
void DialogueRuntime::confirmAdvance() {
|
||||||
@ -234,16 +130,10 @@ void DialogueRuntime::confirmAdvance() {
|
|||||||
enterNode(choice.next);
|
enterNode(choice.next);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode == Mode::PlayingCutscene) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueRuntime::moveSelection(int delta) {
|
void DialogueRuntime::moveSelection(int delta) {
|
||||||
if (mode != Mode::WaitingForChoice || visibleChoices.empty()) {
|
if (mode != Mode::WaitingForChoice || visibleChoices.empty()) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int count = static_cast<int>(visibleChoices.size());
|
const int count = static_cast<int>(visibleChoices.size());
|
||||||
if (selectedChoice < 0 || selectedChoice >= count) {
|
if (selectedChoice < 0 || selectedChoice >= count) {
|
||||||
@ -251,61 +141,18 @@ void DialogueRuntime::moveSelection(int delta) {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
selectedChoice = (selectedChoice + delta) % count;
|
selectedChoice = (selectedChoice + delta) % count;
|
||||||
if (selectedChoice < 0) {
|
if (selectedChoice < 0) selectedChoice += count;
|
||||||
selectedChoice += count;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
presentation.selectedChoice = selectedChoice;
|
presentation.selectedChoice = selectedChoice;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueRuntime::selectChoice(int index) {
|
void DialogueRuntime::selectChoice(int index) {
|
||||||
if (mode != Mode::WaitingForChoice || visibleChoices.empty()) {
|
if (mode != Mode::WaitingForChoice || visibleChoices.empty()) return;
|
||||||
return;
|
if (index < 0 || index >= static_cast<int>(visibleChoices.size())) return;
|
||||||
}
|
|
||||||
if (index < 0 || index >= static_cast<int>(visibleChoices.size())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
selectedChoice = index;
|
selectedChoice = index;
|
||||||
presentation.selectedChoice = selectedChoice;
|
presentation.selectedChoice = selectedChoice;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DialogueRuntime::canSkipCurrentCutscene() const {
|
|
||||||
return mode == Mode::PlayingCutscene && activeCutscene && activeCutscene->skippable;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DialogueRuntime::skipCurrentCutscene() {
|
|
||||||
if (!canSkipCurrentCutscene()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multi-image cutscenes skip to the next image cue first.
|
|
||||||
// This matches the desired behavior: skip advances the visual chapter,
|
|
||||||
// not necessarily the whole cutscene immediately.
|
|
||||||
if (!activeCutscene->images.empty()) {
|
|
||||||
int nextImageStartMs = -1;
|
|
||||||
|
|
||||||
for (const CutsceneImageCue& cue : activeCutscene->images) {
|
|
||||||
if (cue.path.empty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (cue.startMs > cutsceneElapsedMs) {
|
|
||||||
if (nextImageStartMs < 0 || cue.startMs < nextImageStartMs) {
|
|
||||||
nextImageStartMs = cue.startMs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextImageStartMs >= 0) {
|
|
||||||
cutsceneElapsedMs = nextImageStartMs;
|
|
||||||
syncCutsceneLineToElapsedTime();
|
|
||||||
refreshCutscenePresentation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finishCutscene();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DialogueRuntime::setFlag(const std::string& name, int value) {
|
void DialogueRuntime::setFlag(const std::string& name, int value) {
|
||||||
if (flagStore) (*flagStore)[name] = value;
|
if (flagStore) (*flagStore)[name] = value;
|
||||||
}
|
}
|
||||||
@ -369,9 +216,7 @@ bool DialogueRuntime::evaluateConditions(const std::vector<Condition>& condition
|
|||||||
|
|
||||||
void DialogueRuntime::applyEffects(const std::vector<Effect>& effects) {
|
void DialogueRuntime::applyEffects(const std::vector<Effect>& effects) {
|
||||||
for (const Effect& effect : effects) {
|
for (const Effect& effect : effects) {
|
||||||
if (effect.flag.empty()) {
|
if (effect.flag.empty()) continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (effect.relative) {
|
if (effect.relative) {
|
||||||
setFlag(effect.flag, getFlag(effect.flag) + effect.value);
|
setFlag(effect.flag, getFlag(effect.flag) + effect.value);
|
||||||
}
|
}
|
||||||
@ -435,8 +280,10 @@ bool DialogueRuntime::enterNode(const std::string& nodeId) {
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
case NodeType::CutsceneStart:
|
case NodeType::CutsceneStart:
|
||||||
activeCutsceneId = node.cutsceneId;
|
// Pause dialogue and hand off to the cutscene system via DialogueSystem.
|
||||||
startCutscene(node.cutsceneId, node.next);
|
mode = Mode::WaitingForCutscene;
|
||||||
|
if (onCutsceneStartNeeded)
|
||||||
|
onCutsceneStartNeeded(node.cutsceneId, node.next);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -483,12 +330,8 @@ void DialogueRuntime::presentChoices(const Node& node) {
|
|||||||
presentation.choices.clear();
|
presentation.choices.clear();
|
||||||
|
|
||||||
for (const Choice& choice : node.choices) {
|
for (const Choice& choice : node.choices) {
|
||||||
if (!choice.id.empty() && consumedChoices.count(choice.id) > 0) {
|
if (!choice.id.empty() && consumedChoices.count(choice.id) > 0) continue;
|
||||||
continue;
|
if (!evaluateConditions(choice.conditions)) continue;
|
||||||
}
|
|
||||||
if (!evaluateConditions(choice.conditions)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
visibleChoices.push_back(choice);
|
visibleChoices.push_back(choice);
|
||||||
presentation.choices.push_back({ choice.id, choice.text, choice.kind });
|
presentation.choices.push_back({ choice.id, choice.text, choice.kind });
|
||||||
}
|
}
|
||||||
@ -521,450 +364,4 @@ void DialogueRuntime::presentChoices(const Node& node) {
|
|||||||
presentation.cutsceneBlackAlpha = 0.0f;
|
presentation.cutsceneBlackAlpha = 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::string& nextNodeAfterCutscene) {
|
|
||||||
if (!database) {
|
|
||||||
stop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StaticCutsceneDefinition* cutscene = database->findCutscene(cutsceneId);
|
|
||||||
if (!cutscene) {
|
|
||||||
std::cerr << "[dialogue] Cutscene not found: " << cutsceneId << "\n";
|
|
||||||
if (!nextNodeAfterCutscene.empty()) {
|
|
||||||
enterNode(nextNodeAfterCutscene);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
activeCutscene = cutscene;
|
|
||||||
pendingNodeAfterCutscene = nextNodeAfterCutscene;
|
|
||||||
currentCutsceneBackground = cutscene->background;
|
|
||||||
mode = Mode::PlayingCutscene;
|
|
||||||
cutsceneElapsedMs = 0;
|
|
||||||
cutsceneTimerMs = 0;
|
|
||||||
fadeInCallbackFired = false;
|
|
||||||
currentCutsceneLine = activeCutscene->lines.empty() ? -1 : 0;
|
|
||||||
int imageTrackDurationMs = 0;
|
|
||||||
for (size_t i = 0; i < activeCutscene->images.size(); ++i) {
|
|
||||||
const CutsceneImageCue& cue = activeCutscene->images[i];
|
|
||||||
int cueEnd = cue.endMs;
|
|
||||||
if (cueEnd <= cue.startMs) {
|
|
||||||
if (i + 1 < activeCutscene->images.size()) {
|
|
||||||
cueEnd = std::max(activeCutscene->images[i + 1].startMs, cue.startMs);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
cueEnd = cue.startMs + std::max(cue.fadeInMs, 0) + std::max(cue.fadeOutMs, 0) + 1000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
imageTrackDurationMs = std::max(imageTrackDurationMs, cueEnd);
|
|
||||||
}
|
|
||||||
cutsceneContentDurationMs = std::max({ activeCutscene->durationMs, computeCameraTrackDurationMs(*activeCutscene), imageTrackDurationMs });
|
|
||||||
if (cutsceneContentDurationMs <= 0 && activeCutscene->lines.empty()) {
|
|
||||||
cutsceneContentDurationMs = 3000;
|
|
||||||
}
|
|
||||||
cutsceneTotalDurationMs = cutsceneContentDurationMs + activeCutscene->endFadeOutMs + activeCutscene->endFadeInMs;
|
|
||||||
refreshCutscenePresentation();
|
|
||||||
|
|
||||||
if (!activeCutscene->lines.empty()) {
|
|
||||||
const CutsceneLine& firstLine = activeCutscene->lines[0];
|
|
||||||
applyQuestActions(firstLine.questUnlock, firstLine.questComplete,
|
|
||||||
firstLine.questFail, firstLine.objectiveComplete, firstLine.objectiveVisible);
|
|
||||||
if (onCutsceneLineStarted && !firstLine.luaCallback.empty())
|
|
||||||
onCutsceneLineStarted(firstLine.luaCallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
const std::string finishedId = activeCutsceneId;
|
|
||||||
activeCutsceneId.clear();
|
|
||||||
activeCutscene = nullptr;
|
|
||||||
currentCutsceneLine = -1;
|
|
||||||
cutsceneTimerMs = 0;
|
|
||||||
cutsceneElapsedMs = 0;
|
|
||||||
cutsceneTotalDurationMs = 0;
|
|
||||||
cutsceneContentDurationMs = 0;
|
|
||||||
currentCutsceneBackground.clear();
|
|
||||||
if (onCutsceneFinished && !finishedId.empty())
|
|
||||||
onCutsceneFinished(finishedId);
|
|
||||||
|
|
||||||
if (!pendingNodeAfterCutscene.empty()) {
|
|
||||||
const std::string nextNode = pendingNodeAfterCutscene;
|
|
||||||
pendingNodeAfterCutscene.clear();
|
|
||||||
enterNode(nextNode);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DialogueRuntime::syncCutsceneLineToElapsedTime() {
|
|
||||||
if (!activeCutscene || activeCutscene->lines.empty()) {
|
|
||||||
currentCutsceneLine = -1;
|
|
||||||
cutsceneTimerMs = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int elapsed = std::max(cutsceneElapsedMs, 0);
|
|
||||||
int accumulatedMs = 0;
|
|
||||||
|
|
||||||
for (size_t i = 0; i < activeCutscene->lines.size(); ++i) {
|
|
||||||
const CutsceneLine& line = activeCutscene->lines[i];
|
|
||||||
const int durationMs = (line.durationMs > 0)
|
|
||||||
? line.durationMs
|
|
||||||
: computeFallbackCutsceneDurationMs(line.text);
|
|
||||||
|
|
||||||
if (elapsed < accumulatedMs + durationMs) {
|
|
||||||
currentCutsceneLine = static_cast<int>(i);
|
|
||||||
cutsceneTimerMs = std::max(0, elapsed - accumulatedMs);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
accumulatedMs += durationMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentCutsceneLine = -1;
|
|
||||||
cutsceneTimerMs = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void 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 (cutsceneContentDurationMs <= 0 || cutsceneElapsedMs >= cutsceneContentDurationMs) {
|
|
||||||
// Only finish immediately if there's no end transition to play
|
|
||||||
if (activeCutscene->endFadeOutMs <= 0 && activeCutscene->endFadeInMs <= 0) {
|
|
||||||
finishCutscene();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CutsceneLine& newLine = activeCutscene->lines[currentCutsceneLine];
|
|
||||||
applyQuestActions(newLine.questUnlock, newLine.questComplete,
|
|
||||||
newLine.questFail, newLine.objectiveComplete, newLine.objectiveVisible);
|
|
||||||
if (onCutsceneLineStarted && !newLine.luaCallback.empty())
|
|
||||||
onCutsceneLineStarted(newLine.luaCallback);
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<PresentedCutsceneImage> DialogueRuntime::evaluateCutsceneImages() const {
|
|
||||||
std::vector<PresentedCutsceneImage> result;
|
|
||||||
if (!activeCutscene) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& fallbackPath = !currentCutsceneBackground.empty()
|
|
||||||
? currentCutsceneBackground
|
|
||||||
: activeCutscene->background;
|
|
||||||
|
|
||||||
if (activeCutscene->images.empty()) {
|
|
||||||
if (!fallbackPath.empty()) {
|
|
||||||
result.push_back({ fallbackPath, 1.0f });
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int effectiveTotalDuration = (cutsceneContentDurationMs > 0) ? cutsceneContentDurationMs : std::max(activeCutscene->durationMs, 1);
|
|
||||||
const int now = std::max(cutsceneElapsedMs, 0);
|
|
||||||
|
|
||||||
for (size_t i = 0; i < activeCutscene->images.size(); ++i) {
|
|
||||||
const CutsceneImageCue& cue = activeCutscene->images[i];
|
|
||||||
if (cue.path.empty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int startMs = std::max(cue.startMs, 0);
|
|
||||||
int endMs = cue.endMs;
|
|
||||||
if (endMs <= startMs) {
|
|
||||||
if (i + 1 < activeCutscene->images.size()) {
|
|
||||||
endMs = std::max(activeCutscene->images[i + 1].startMs, startMs + 1);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
endMs = effectiveTotalDuration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (endMs <= startMs) {
|
|
||||||
endMs = startMs + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (now < startMs || now > endMs) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
float alpha = 1.0f;
|
|
||||||
if (cue.fadeInMs > 0 && now < startMs + cue.fadeInMs) {
|
|
||||||
alpha = std::clamp(
|
|
||||||
static_cast<float>(now - startMs) / static_cast<float>(cue.fadeInMs),
|
|
||||||
0.0f,
|
|
||||||
1.0f
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (alpha > 0.0f) {
|
|
||||||
result.push_back({ cue.path, alpha });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Safety fallback: never leave the cutscene without an opaque image layer.
|
|
||||||
if (result.empty() && !fallbackPath.empty()) {
|
|
||||||
result.push_back({ fallbackPath, 1.0f });
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the first active layer is still fading in, put an opaque fallback/base below it.
|
|
||||||
// This prevents the world from becoming visible behind the cutscene.
|
|
||||||
if (!result.empty() && result.front().alpha < 0.999f && !fallbackPath.empty() && result.front().path != fallbackPath) {
|
|
||||||
result.insert(result.begin(), { fallbackPath, 1.0f });
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DialogueRuntime::refreshCutscenePresentation() {
|
|
||||||
if (!activeCutscene) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
presentation.mode = PresentationMode::Cutscene;
|
|
||||||
presentation.backgroundPath = activeCutscene->background;
|
|
||||||
presentation.backgroundWidth = activeCutscene->backgroundWidth;
|
|
||||||
presentation.backgroundHeight = activeCutscene->backgroundHeight;
|
|
||||||
presentation.cutsceneCamera = evaluateCutsceneCameraBlend();
|
|
||||||
presentation.cutsceneImages = evaluateCutsceneImages();
|
|
||||||
presentation.cutsceneSkippable = activeCutscene->skippable;
|
|
||||||
|
|
||||||
const int fadeOutMs = activeCutscene->fadeOutMs;
|
|
||||||
const int fadeInMs = activeCutscene->fadeInMs;
|
|
||||||
const int endFadeOutMs = activeCutscene->endFadeOutMs;
|
|
||||||
const int endFadeInMs = activeCutscene->endFadeInMs;
|
|
||||||
const int endFadeOutStart = cutsceneContentDurationMs;
|
|
||||||
const int endFadeInStart = cutsceneContentDurationMs + endFadeOutMs;
|
|
||||||
|
|
||||||
if (cutsceneElapsedMs < fadeOutMs) {
|
|
||||||
// Start phase 1: game world fading to black
|
|
||||||
presentation.cutsceneGlobalFadeAlpha = 0.0f;
|
|
||||||
presentation.cutsceneBlackAlpha = std::clamp(
|
|
||||||
static_cast<float>(cutsceneElapsedMs) / static_cast<float>(fadeOutMs),
|
|
||||||
0.0f, 1.0f
|
|
||||||
);
|
|
||||||
} else if (cutsceneElapsedMs < endFadeOutStart) {
|
|
||||||
// Content playing (also covers start phase 2 fade-in)
|
|
||||||
presentation.cutsceneGlobalFadeAlpha = 1.0f;
|
|
||||||
const int phase2elapsed = cutsceneElapsedMs - fadeOutMs;
|
|
||||||
presentation.cutsceneBlackAlpha = (fadeInMs > 0)
|
|
||||||
? std::clamp(1.0f - static_cast<float>(phase2elapsed) / static_cast<float>(fadeInMs), 0.0f, 1.0f)
|
|
||||||
: 0.0f;
|
|
||||||
} else if (cutsceneElapsedMs < endFadeInStart) {
|
|
||||||
// End phase 1: cutscene fading to black
|
|
||||||
presentation.cutsceneGlobalFadeAlpha = 1.0f;
|
|
||||||
const int elapsed = cutsceneElapsedMs - endFadeOutStart;
|
|
||||||
presentation.cutsceneBlackAlpha = (endFadeOutMs > 0)
|
|
||||||
? std::clamp(static_cast<float>(elapsed) / static_cast<float>(endFadeOutMs), 0.0f, 1.0f)
|
|
||||||
: 1.0f;
|
|
||||||
} else {
|
|
||||||
// End phase 2: game world appearing from black (image hidden, world shows through)
|
|
||||||
presentation.cutsceneGlobalFadeAlpha = 0.0f;
|
|
||||||
const int elapsed = cutsceneElapsedMs - endFadeInStart;
|
|
||||||
presentation.cutsceneBlackAlpha = (endFadeInMs > 0)
|
|
||||||
? std::clamp(1.0f - static_cast<float>(elapsed) / static_cast<float>(endFadeInMs), 0.0f, 1.0f)
|
|
||||||
: 0.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
presentation.choices.clear();
|
|
||||||
presentation.selectedChoice = -1;
|
|
||||||
presentation.revealCompleted = true;
|
|
||||||
|
|
||||||
const bool hasSubtitle = currentCutsceneLine >= 0 && currentCutsceneLine < static_cast<int>(activeCutscene->lines.size());
|
|
||||||
presentation.showCutsceneSubtitle = hasSubtitle;
|
|
||||||
|
|
||||||
if (!hasSubtitle) {
|
|
||||||
presentation.speaker.clear();
|
|
||||||
presentation.fullText.clear();
|
|
||||||
presentation.visibleText.clear();
|
|
||||||
presentation.portraitPath.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
|
|
||||||
|
|
||||||
|
|
||||||
if (!line.background.empty()) {
|
|
||||||
currentCutsceneBackground = line.background;
|
|
||||||
if (line.backgroundWidth > 0) presentation.backgroundWidth = line.backgroundWidth;
|
|
||||||
if (line.backgroundHeight > 0) presentation.backgroundHeight = line.backgroundHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
presentation.mode = PresentationMode::Cutscene;
|
|
||||||
presentation.speaker = line.speaker;
|
|
||||||
presentation.fullText = line.text;
|
|
||||||
presentation.visibleText = line.text;
|
|
||||||
presentation.portraitPath = line.portrait;
|
|
||||||
|
|
||||||
//presentation.backgroundPath = activeCutscene->background;
|
|
||||||
presentation.backgroundPath = currentCutsceneBackground;
|
|
||||||
presentation.choices.clear();
|
|
||||||
presentation.selectedChoice = 0;
|
|
||||||
presentation.revealCompleted = true;
|
|
||||||
|
|
||||||
|
|
||||||
std::cout << "[CUTSCENE] lines=" << activeCutscene->lines.size()
|
|
||||||
<< " current=" << currentCutsceneLine
|
|
||||||
<< std::endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
float DialogueRuntime::applyEasing(EasingType easing, float t) {
|
|
||||||
t = std::clamp(t, 0.0f, 1.0f);
|
|
||||||
constexpr float PI = 3.14159265358979323846f;
|
|
||||||
|
|
||||||
switch (easing) {
|
|
||||||
case EasingType::EaseInSine:
|
|
||||||
return 1.0f - std::cos((t * PI) * 0.5f);
|
|
||||||
case EasingType::EaseOutSine:
|
|
||||||
return std::sin((t * PI) * 0.5f);
|
|
||||||
case EasingType::EaseInOutSine:
|
|
||||||
return -(std::cos(PI * t) - 1.0f) * 0.5f;
|
|
||||||
case EasingType::EaseInQuad:
|
|
||||||
return t * t;
|
|
||||||
case EasingType::EaseOutQuad:
|
|
||||||
return 1.0f - (1.0f - t) * (1.0f - t);
|
|
||||||
case EasingType::EaseInOutQuad:
|
|
||||||
return (t < 0.5f) ? 2.0f * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 2.0f) * 0.5f;
|
|
||||||
case EasingType::EaseInCubic:
|
|
||||||
return t * t * t;
|
|
||||||
case EasingType::EaseOutCubic:
|
|
||||||
return 1.0f - std::pow(1.0f - t, 3.0f);
|
|
||||||
case EasingType::EaseInOutCubic:
|
|
||||||
return (t < 0.5f) ? 4.0f * t * t * t : 1.0f - std::pow(-2.0f * t + 2.0f, 3.0f) * 0.5f;
|
|
||||||
case EasingType::Linear:
|
|
||||||
default:
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int DialogueRuntime::computeFallbackCutsceneDurationMs(const std::string& text) {
|
|
||||||
const int cps = 17;
|
|
||||||
const int minDuration = 1500;
|
|
||||||
const int linger = 450;
|
|
||||||
const int calculated = static_cast<int>((1000.0 * static_cast<double>(std::max<size_t>(text.size(), 1))) / cps);
|
|
||||||
return std::max(minDuration, calculated + linger);
|
|
||||||
}
|
|
||||||
|
|
||||||
int DialogueRuntime::computeCameraTrackDurationMs(const StaticCutsceneDefinition& cutscene) {
|
|
||||||
int total = 0;
|
|
||||||
for (const CutsceneCameraSegment& segment : cutscene.cameraTrack) {
|
|
||||||
total += std::max(segment.durationMs, 0);
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
DialogueRuntime::json DialogueRuntime::buildSaveState() const {
|
|
||||||
json result;
|
|
||||||
result["active"] = isActive();
|
|
||||||
result["dialogueId"] = activeDialogue ? activeDialogue->id : "";
|
|
||||||
result["currentNodeId"] = currentNodeId;
|
|
||||||
result["pendingNodeAfterCutscene"] = pendingNodeAfterCutscene;
|
|
||||||
result["selectedChoice"] = selectedChoice;
|
|
||||||
result["currentCutsceneLine"] = currentCutsceneLine;
|
|
||||||
result["cutsceneTimerMs"] = cutsceneTimerMs;
|
|
||||||
result["consumedChoices"] = consumedChoices;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DialogueRuntime::restoreSaveState(const json& state) {
|
|
||||||
if (!database) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
flags.clear();
|
|
||||||
consumedChoices.clear();
|
|
||||||
|
|
||||||
if (state.contains("consumedChoices")) {
|
|
||||||
consumedChoices = state["consumedChoices"].get<std::unordered_set<std::string>>();
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool active = state.value("active", false);
|
|
||||||
if (!active) {
|
|
||||||
stop();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string dialogueId = state.value("dialogueId", "");
|
|
||||||
if (!startDialogue(dialogueId)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string nodeId = state.value("currentNodeId", "");
|
|
||||||
pendingNodeAfterCutscene = state.value("pendingNodeAfterCutscene", "");
|
|
||||||
selectedChoice = state.value("selectedChoice", -1);
|
|
||||||
currentCutsceneLine = state.value("currentCutsceneLine", -1);
|
|
||||||
cutsceneTimerMs = state.value("cutsceneTimerMs", 0);
|
|
||||||
|
|
||||||
const bool ok = nodeId.empty() ? true : enterNode(nodeId);
|
|
||||||
if (mode == Mode::WaitingForChoice && !visibleChoices.empty()) {
|
|
||||||
if (selectedChoice >= 0) {
|
|
||||||
selectedChoice = std::clamp(selectedChoice, 0, static_cast<int>(visibleChoices.size()) - 1);
|
|
||||||
}
|
|
||||||
presentation.selectedChoice = selectedChoice;
|
|
||||||
}
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
} // namespace ZL::Dialogue
|
} // namespace ZL::Dialogue
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#include "dialogue/DialogueDatabase.h"
|
#include "dialogue/DialogueDatabase.h"
|
||||||
#include "quest/QuestJournal.h"
|
#include "quest/QuestJournal.h"
|
||||||
#include "external/nlohmann/json.hpp"
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@ -13,70 +12,54 @@ namespace ZL::Dialogue {
|
|||||||
|
|
||||||
class DialogueRuntime {
|
class DialogueRuntime {
|
||||||
public:
|
public:
|
||||||
using json = nlohmann::json;
|
|
||||||
|
|
||||||
void setDatabase(const DialogueDatabase* value);
|
void setDatabase(const DialogueDatabase* value);
|
||||||
|
|
||||||
bool startDialogue(const std::string& dialogueId);
|
bool startDialogue(const std::string& dialogueId);
|
||||||
bool startStandaloneCutscene(const std::string& cutsceneId);
|
|
||||||
void setOnCutsceneFinished(std::function<void(const std::string&)> cb);
|
|
||||||
void setOnDialogueLineStarted(std::function<void(const std::string&)> cb);
|
|
||||||
void setOnCutsceneLineStarted(std::function<void(const std::string&)> cb);
|
|
||||||
void setOnCutsceneFadeInComplete(std::function<void(const std::string&)> cb);
|
|
||||||
void setOnChatBubbleReady(std::function<void(const std::string&, bool)> cb);
|
|
||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
|
// Called by DialogueSystem after the inline cutscene finishes.
|
||||||
|
void resumeFromNode(const std::string& nodeId);
|
||||||
|
|
||||||
|
void setOnCutsceneStartNeeded(std::function<void(const std::string& cutsceneId, const std::string& nextNodeId)> cb);
|
||||||
|
void setOnDialogueLineStarted(std::function<void(const std::string&)> cb);
|
||||||
|
void setOnChatBubbleReady(std::function<void(const std::string&, bool)> cb);
|
||||||
|
|
||||||
void update(int deltaMs);
|
void update(int deltaMs);
|
||||||
|
|
||||||
bool isActive() const { return mode != Mode::Inactive; }
|
bool isActive() const { return mode != Mode::Inactive; }
|
||||||
bool isInChoice() const { return mode == Mode::WaitingForChoice; }
|
bool isInChoice() const { return mode == Mode::WaitingForChoice; }
|
||||||
bool isPlayingCutscene() const { return mode == Mode::PlayingCutscene; }
|
|
||||||
|
|
||||||
void confirmAdvance();
|
void confirmAdvance();
|
||||||
void moveSelection(int delta);
|
void moveSelection(int delta);
|
||||||
void selectChoice(int index);
|
void selectChoice(int index);
|
||||||
bool canSkipCurrentCutscene() const;
|
|
||||||
void skipCurrentCutscene();
|
|
||||||
|
|
||||||
const PresentationModel& getPresentation() const { return presentation; }
|
const PresentationModel& getPresentation() const { return presentation; }
|
||||||
|
|
||||||
void setFlag(const std::string& name, int value);
|
void setFlag(const std::string& name, int value);
|
||||||
int getFlag(const std::string& name) const;
|
int getFlag(const std::string& name) const;
|
||||||
|
|
||||||
void setGlobalFlagStore(std::unordered_map<std::string, int>* store);
|
void setGlobalFlagStore(std::unordered_map<std::string, int>* store);
|
||||||
|
|
||||||
void setQuestJournal(Quest::QuestJournal* journal);
|
void setQuestJournal(Quest::QuestJournal* journal);
|
||||||
|
|
||||||
//json buildSaveState() const;
|
|
||||||
//bool restoreSaveState(const json& state);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class Mode {
|
enum class Mode {
|
||||||
Inactive,
|
Inactive,
|
||||||
PresentingLine,
|
PresentingLine,
|
||||||
WaitingForChoice,
|
WaitingForChoice,
|
||||||
PlayingCutscene
|
WaitingForCutscene // paused while an inline cutscene plays
|
||||||
};
|
};
|
||||||
|
|
||||||
std::function<void(const std::string&)> onCutsceneFinished;
|
std::function<void(const std::string&, const std::string&)> onCutsceneStartNeeded;
|
||||||
std::function<void(const std::string&)> onDialogueLineStarted;
|
std::function<void(const std::string&)> onDialogueLineStarted;
|
||||||
std::function<void(const std::string&)> onCutsceneLineStarted;
|
|
||||||
std::function<void(const std::string&)> onCutsceneFadeInComplete;
|
|
||||||
std::function<void(const std::string&, bool)> onChatBubbleReady;
|
std::function<void(const std::string&, bool)> onChatBubbleReady;
|
||||||
std::string activeCutsceneId;
|
|
||||||
bool fadeInCallbackFired = false;
|
|
||||||
|
|
||||||
const DialogueDatabase* database = nullptr;
|
const DialogueDatabase* database = nullptr;
|
||||||
Quest::QuestJournal* questJournal = nullptr;
|
Quest::QuestJournal* questJournal = nullptr;
|
||||||
const DialogueDefinition* activeDialogue = nullptr;
|
const DialogueDefinition* activeDialogue = nullptr;
|
||||||
const StaticCutsceneDefinition* activeCutscene = nullptr;
|
|
||||||
|
|
||||||
std::unordered_map<std::string, int>* flagStore = nullptr;
|
std::unordered_map<std::string, int>* flagStore = nullptr;
|
||||||
std::unordered_set<std::string> consumedChoices;
|
std::unordered_set<std::string> consumedChoices;
|
||||||
|
|
||||||
std::string currentNodeId;
|
std::string currentNodeId;
|
||||||
std::string pendingNodeAfterCutscene;
|
|
||||||
|
|
||||||
std::vector<Choice> visibleChoices;
|
std::vector<Choice> visibleChoices;
|
||||||
PresentationModel presentation;
|
PresentationModel presentation;
|
||||||
Mode mode = Mode::Inactive;
|
Mode mode = Mode::Inactive;
|
||||||
@ -85,14 +68,6 @@ private:
|
|||||||
float revealCharacters = 0.0f;
|
float revealCharacters = 0.0f;
|
||||||
float revealSpeedCharsPerSecond = 52.0f;
|
float revealSpeedCharsPerSecond = 52.0f;
|
||||||
|
|
||||||
int currentCutsceneLine = -1;
|
|
||||||
int cutsceneTimerMs = 0;
|
|
||||||
int cutsceneElapsedMs = 0;
|
|
||||||
int cutsceneTotalDurationMs = 0;
|
|
||||||
int cutsceneContentDurationMs = 0;
|
|
||||||
|
|
||||||
std::string currentCutsceneBackground;
|
|
||||||
|
|
||||||
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);
|
||||||
void applyQuestActions(const std::string& questUnlock, const std::string& questComplete,
|
void applyQuestActions(const std::string& questUnlock, const std::string& questComplete,
|
||||||
@ -102,18 +77,6 @@ private:
|
|||||||
bool enterNode(const std::string& nodeId);
|
bool enterNode(const std::string& nodeId);
|
||||||
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 finishCutscene();
|
|
||||||
void syncCutsceneLineToElapsedTime();
|
|
||||||
|
|
||||||
void advanceCutsceneLine();
|
|
||||||
void refreshCutscenePresentation();
|
|
||||||
CutsceneCameraBlendState evaluateCutsceneCameraBlend() const;
|
|
||||||
std::vector<PresentedCutsceneImage> evaluateCutsceneImages() const;
|
|
||||||
|
|
||||||
static float applyEasing(EasingType easing, float t);
|
|
||||||
static int computeFallbackCutsceneDurationMs(const std::string& text);
|
|
||||||
static int computeCameraTrackDurationMs(const StaticCutsceneDefinition& cutscene);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZL::Dialogue
|
} // namespace ZL::Dialogue
|
||||||
|
|||||||
@ -3,36 +3,57 @@
|
|||||||
namespace ZL::Dialogue {
|
namespace ZL::Dialogue {
|
||||||
|
|
||||||
bool DialogueSystem::init(Renderer& renderer, const std::string& zipFile) {
|
bool DialogueSystem::init(Renderer& renderer, const std::string& zipFile) {
|
||||||
runtime.setDatabase(&database);
|
dialogueRuntime.setDatabase(&database);
|
||||||
runtime.setOnCutsceneFinished([this](const std::string& id) {
|
cutsceneRuntime.setDatabase(&cutsceneDatabase);
|
||||||
if (onCutsceneFinishedCallback) onCutsceneFinishedCallback(id);
|
|
||||||
if (onCutsceneFinishedExtraCallback) onCutsceneFinishedExtraCallback(id);
|
// When dialogue hits a CutsceneStart node, hand off to cutsceneRuntime.
|
||||||
|
dialogueRuntime.setOnCutsceneStartNeeded([this](const std::string& cutsceneId, const std::string& nextNodeId) {
|
||||||
|
pendingNodeAfterCutscene = nextNodeId;
|
||||||
|
if (onCutsceneStartedCallback) onCutsceneStartedCallback();
|
||||||
|
cutsceneRuntime.start(cutsceneId);
|
||||||
});
|
});
|
||||||
return overlay.init(renderer, zipFile);
|
|
||||||
|
cutsceneRuntime.setOnFinished([this](const std::string& id) {
|
||||||
|
onCutsceneFinishedInternal(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return
|
||||||
|
dialogueOverlay.init(renderer, zipFile) &&
|
||||||
|
cutsceneOverlay.init(renderer, zipFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DialogueSystem::loadDatabase(const std::string& path) {
|
bool DialogueSystem::loadDatabase(const std::string& path) {
|
||||||
return database.loadFromFile(path);
|
return database.loadFromFile(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool DialogueSystem::loadCutsceneDatabase(const std::string& path) {
|
||||||
|
return cutsceneDatabase.loadFromFile(path);
|
||||||
|
}
|
||||||
|
|
||||||
void DialogueSystem::update(int deltaMs) {
|
void DialogueSystem::update(int deltaMs) {
|
||||||
runtime.update(deltaMs);
|
dialogueRuntime.update(deltaMs);
|
||||||
overlay.update(runtime.getPresentation(), deltaMs);
|
cutsceneRuntime.update(deltaMs);
|
||||||
if (overlay.consumeSkipRequested()) {
|
dialogueOverlay.update(dialogueRuntime.getPresentation(), deltaMs);
|
||||||
runtime.skipCurrentCutscene();
|
cutsceneOverlay.update(cutsceneRuntime.getPresentation(), deltaMs);
|
||||||
|
|
||||||
|
if (cutsceneOverlay.consumeSkipRequested()) {
|
||||||
|
cutsceneRuntime.skip();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueSystem::draw(Renderer& renderer) {
|
void DialogueSystem::draw(Renderer& renderer) {
|
||||||
overlay.draw(renderer, runtime.getPresentation());
|
if (cutsceneRuntime.isActive()) {
|
||||||
|
cutsceneOverlay.draw(renderer, cutsceneRuntime.getPresentation());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dialogueOverlay.draw(renderer, dialogueRuntime.getPresentation());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DialogueSystem::handleKeyDown(SDL_Keycode key) {
|
bool DialogueSystem::handleKeyDown(SDL_Keycode key) {
|
||||||
if (!runtime.isActive()) {
|
if (!isActive()) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (runtime.isPlayingCutscene()) {
|
if (cutsceneRuntime.isActive()) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case SDLK_RETURN:
|
case SDLK_RETURN:
|
||||||
case SDLK_SPACE:
|
case SDLK_SPACE:
|
||||||
@ -48,17 +69,17 @@ bool DialogueSystem::handleKeyDown(SDL_Keycode key) {
|
|||||||
case SDLK_RETURN:
|
case SDLK_RETURN:
|
||||||
case SDLK_SPACE:
|
case SDLK_SPACE:
|
||||||
case SDLK_e:
|
case SDLK_e:
|
||||||
runtime.confirmAdvance();
|
dialogueRuntime.confirmAdvance();
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case SDLK_UP:
|
case SDLK_UP:
|
||||||
case SDLK_w:
|
case SDLK_w:
|
||||||
runtime.moveSelection(-1);
|
dialogueRuntime.moveSelection(-1);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case SDLK_DOWN:
|
case SDLK_DOWN:
|
||||||
case SDLK_s:
|
case SDLK_s:
|
||||||
runtime.moveSelection(1);
|
dialogueRuntime.moveSelection(1);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case SDLK_ESCAPE:
|
case SDLK_ESCAPE:
|
||||||
@ -71,67 +92,91 @@ bool DialogueSystem::handleKeyDown(SDL_Keycode key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DialogueSystem::handlePointerDown(float x, float y) {
|
void DialogueSystem::handlePointerDown(float x, float y) {
|
||||||
if (!runtime.isActive()) {
|
if (!isActive()) return;
|
||||||
return;
|
|
||||||
|
if (cutsceneRuntime.isActive()) {
|
||||||
|
cutsceneOverlay.handlePointerDown(x, y, cutsceneRuntime.getPresentation());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dialogueOverlay.handlePointerDown(x, y, dialogueRuntime.getPresentation());
|
||||||
}
|
}
|
||||||
overlay.handlePointerDown(x, y, runtime.getPresentation());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueSystem::handlePointerMoved(float x, float y) {
|
void DialogueSystem::handlePointerMoved(float x, float y) {
|
||||||
if (!runtime.isActive()) {
|
if (!isActive()) return;
|
||||||
return;
|
|
||||||
|
if (cutsceneRuntime.isActive()) {
|
||||||
|
cutsceneOverlay.handlePointerMoved(x, y, cutsceneRuntime.getPresentation());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dialogueOverlay.handlePointerMoved(x, y, dialogueRuntime.getPresentation());
|
||||||
}
|
}
|
||||||
overlay.handlePointerMoved(x, y, runtime.getPresentation());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DialogueSystem::handlePointerReleased(float x, float y) {
|
bool DialogueSystem::handlePointerReleased(float x, float y) {
|
||||||
if (!runtime.isActive()) {
|
if (!isActive()) return false;
|
||||||
return false;
|
|
||||||
|
if (cutsceneRuntime.isActive()) {
|
||||||
|
if (cutsceneOverlay.consumeSkipRequested()) {
|
||||||
|
cutsceneRuntime.skip();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return cutsceneOverlay.handlePointerReleased(x, y, cutsceneRuntime.getPresentation());
|
||||||
}
|
}
|
||||||
|
|
||||||
int choiceIndex = -1;
|
int choiceIndex = -1;
|
||||||
bool advanceDialogue = false;
|
bool advanceDialogue = false;
|
||||||
const PresentationModel& model = runtime.getPresentation();
|
if (!dialogueOverlay.handlePointerReleased(x, y, dialogueRuntime.getPresentation(), choiceIndex, advanceDialogue)) {
|
||||||
if (!overlay.handlePointerReleased(x, y, model, choiceIndex, advanceDialogue)) {
|
return false;
|
||||||
if (overlay.consumeSkipRequested()) {
|
|
||||||
runtime.skipCurrentCutscene();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return runtime.isPlayingCutscene();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (choiceIndex >= 0) {
|
if (choiceIndex >= 0) {
|
||||||
runtime.selectChoice(choiceIndex);
|
dialogueRuntime.selectChoice(choiceIndex);
|
||||||
runtime.confirmAdvance();
|
dialogueRuntime.confirmAdvance();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (advanceDialogue) {
|
if (advanceDialogue) {
|
||||||
runtime.confirmAdvance();
|
dialogueRuntime.confirmAdvance();
|
||||||
if (onDialogueAdvancedCallback) onDialogueAdvancedCallback();
|
if (onDialogueAdvancedCallback) onDialogueAdvancedCallback();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (overlay.consumeSkipRequested()) {
|
|
||||||
runtime.skipCurrentCutscene();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DialogueSystem::startDialogue(const std::string& dialogueId) {
|
bool DialogueSystem::startDialogue(const std::string& dialogueId) {
|
||||||
return runtime.startDialogue(dialogueId);
|
return dialogueRuntime.startDialogue(dialogueId);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DialogueSystem::startCutscene(const std::string& cutsceneId) {
|
bool DialogueSystem::startCutscene(const std::string& cutsceneId) {
|
||||||
bool result = runtime.startStandaloneCutscene(cutsceneId);
|
pendingNodeAfterCutscene.clear();
|
||||||
|
const bool result = cutsceneRuntime.start(cutsceneId);
|
||||||
if (result && onCutsceneStartedCallback) onCutsceneStartedCallback();
|
if (result && onCutsceneStartedCallback) onCutsceneStartedCallback();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueSystem::skipCutscene() {
|
void DialogueSystem::skipCutscene() {
|
||||||
runtime.skipCurrentCutscene();
|
cutsceneRuntime.skip();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DialogueSystem::stopDialogue() {
|
||||||
|
dialogueRuntime.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DialogueSystem::onCutsceneFinishedInternal(const std::string& id) {
|
||||||
|
if (onCutsceneFinishedCallback) onCutsceneFinishedCallback(id);
|
||||||
|
if (onCutsceneFinishedExtraCallback) onCutsceneFinishedExtraCallback(id);
|
||||||
|
|
||||||
|
// Resume dialogue if this cutscene was triggered by a CutsceneStart node.
|
||||||
|
if (!pendingNodeAfterCutscene.empty()) {
|
||||||
|
const std::string nextNode = pendingNodeAfterCutscene;
|
||||||
|
pendingNodeAfterCutscene.clear();
|
||||||
|
dialogueRuntime.resumeFromNode(nextNode);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dialogueRuntime.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueSystem::setOnCutsceneStarted(std::function<void()> cb) {
|
void DialogueSystem::setOnCutsceneStarted(std::function<void()> cb) {
|
||||||
@ -147,23 +192,19 @@ void DialogueSystem::setOnCutsceneFinishedExtra(std::function<void(const std::st
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DialogueSystem::setOnDialogueLineStarted(std::function<void(const std::string&)> cb) {
|
void DialogueSystem::setOnDialogueLineStarted(std::function<void(const std::string&)> cb) {
|
||||||
runtime.setOnDialogueLineStarted(std::move(cb));
|
dialogueRuntime.setOnDialogueLineStarted(std::move(cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueSystem::setOnCutsceneLineStarted(std::function<void(const std::string&)> cb) {
|
void DialogueSystem::setOnCutsceneLineStarted(std::function<void(const std::string&)> cb) {
|
||||||
runtime.setOnCutsceneLineStarted(std::move(cb));
|
cutsceneRuntime.setOnLineStarted(std::move(cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueSystem::setOnCutsceneFadeInComplete(std::function<void(const std::string&)> cb) {
|
void DialogueSystem::setOnCutsceneFadeInComplete(std::function<void(const std::string&)> cb) {
|
||||||
runtime.setOnCutsceneFadeInComplete(std::move(cb));
|
cutsceneRuntime.setOnFadeInComplete(std::move(cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueSystem::setOnChatBubbleReady(std::function<void(const std::string&, bool)> cb) {
|
void DialogueSystem::setOnChatBubbleReady(std::function<void(const std::string&, bool)> cb) {
|
||||||
runtime.setOnChatBubbleReady(std::move(cb));
|
dialogueRuntime.setOnChatBubbleReady(std::move(cb));
|
||||||
}
|
|
||||||
|
|
||||||
void DialogueSystem::stopDialogue() {
|
|
||||||
runtime.stop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueSystem::setOnDialogueAdvanced(std::function<void()> cb) {
|
void DialogueSystem::setOnDialogueAdvanced(std::function<void()> cb) {
|
||||||
|
|||||||
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
#include "dialogue/DialogueOverlay.h"
|
#include "dialogue/DialogueOverlay.h"
|
||||||
#include "dialogue/DialogueRuntime.h"
|
#include "dialogue/DialogueRuntime.h"
|
||||||
|
#include "cutscene/CutsceneDatabase.h"
|
||||||
|
#include "cutscene/CutsceneOverlay.h"
|
||||||
|
#include "cutscene/CutsceneRuntime.h"
|
||||||
#include "quest/QuestJournal.h"
|
#include "quest/QuestJournal.h"
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
@ -12,7 +15,9 @@ namespace ZL::Dialogue {
|
|||||||
class DialogueSystem {
|
class DialogueSystem {
|
||||||
public:
|
public:
|
||||||
bool init(Renderer& renderer, const std::string& zipFile = "");
|
bool init(Renderer& renderer, const std::string& zipFile = "");
|
||||||
|
|
||||||
bool loadDatabase(const std::string& path);
|
bool loadDatabase(const std::string& path);
|
||||||
|
bool loadCutsceneDatabase(const std::string& path);
|
||||||
|
|
||||||
void update(int deltaMs);
|
void update(int deltaMs);
|
||||||
void draw(Renderer& renderer);
|
void draw(Renderer& renderer);
|
||||||
@ -25,6 +30,7 @@ public:
|
|||||||
bool startDialogue(const std::string& dialogueId);
|
bool startDialogue(const std::string& dialogueId);
|
||||||
bool startCutscene(const std::string& cutsceneId);
|
bool startCutscene(const std::string& cutsceneId);
|
||||||
void skipCutscene();
|
void skipCutscene();
|
||||||
|
|
||||||
void setOnCutsceneStarted(std::function<void()> cb);
|
void setOnCutsceneStarted(std::function<void()> cb);
|
||||||
void setOnCutsceneFinished(std::function<void(const std::string&)> cb);
|
void setOnCutsceneFinished(std::function<void(const std::string&)> cb);
|
||||||
void setOnCutsceneFinishedExtra(std::function<void(const std::string&)> cb);
|
void setOnCutsceneFinishedExtra(std::function<void(const std::string&)> cb);
|
||||||
@ -35,24 +41,38 @@ public:
|
|||||||
void setOnDialogueAdvanced(std::function<void()> cb);
|
void setOnDialogueAdvanced(std::function<void()> cb);
|
||||||
void stopDialogue();
|
void stopDialogue();
|
||||||
|
|
||||||
bool isActive() const { return runtime.isActive(); }
|
bool isActive() const { return dialogueRuntime.isActive() || cutsceneRuntime.isActive(); }
|
||||||
bool blocksGameplayInput() const { return runtime.isActive(); }
|
bool blocksGameplayInput() const { return isActive(); }
|
||||||
|
|
||||||
void setFlag(const std::string& name, int value) { runtime.setFlag(name, value); }
|
void setFlag(const std::string& name, int value) { dialogueRuntime.setFlag(name, value); }
|
||||||
int getFlag(const std::string& name) const { return runtime.getFlag(name); }
|
int getFlag(const std::string& name) const { return dialogueRuntime.getFlag(name); }
|
||||||
|
|
||||||
void setGlobalFlagStore(std::unordered_map<std::string, int>* store) { runtime.setGlobalFlagStore(store); }
|
void setGlobalFlagStore(std::unordered_map<std::string, int>* store) {
|
||||||
|
dialogueRuntime.setGlobalFlagStore(store);
|
||||||
void setQuestJournal(Quest::QuestJournal* journal) { runtime.setQuestJournal(journal); }
|
}
|
||||||
|
void setQuestJournal(Quest::QuestJournal* journal) {
|
||||||
|
dialogueRuntime.setQuestJournal(journal);
|
||||||
|
cutsceneRuntime.setQuestJournal(journal);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
DialogueDatabase database;
|
DialogueDatabase database;
|
||||||
DialogueRuntime runtime;
|
DialogueRuntime dialogueRuntime;
|
||||||
DialogueOverlay overlay;
|
DialogueOverlay dialogueOverlay;
|
||||||
|
|
||||||
|
ZL::Cutscene::CutsceneDatabase cutsceneDatabase;
|
||||||
|
ZL::Cutscene::CutsceneRuntime cutsceneRuntime;
|
||||||
|
ZL::Cutscene::CutsceneOverlay cutsceneOverlay;
|
||||||
|
|
||||||
std::function<void()> onDialogueAdvancedCallback;
|
std::function<void()> onDialogueAdvancedCallback;
|
||||||
std::function<void()> onCutsceneStartedCallback;
|
std::function<void()> onCutsceneStartedCallback;
|
||||||
std::function<void(const std::string&)> onCutsceneFinishedCallback;
|
std::function<void(const std::string&)> onCutsceneFinishedCallback;
|
||||||
std::function<void(const std::string&)> onCutsceneFinishedExtraCallback;
|
std::function<void(const std::string&)> onCutsceneFinishedExtraCallback;
|
||||||
|
|
||||||
|
// Pending node to resume after an inline cutscene finishes.
|
||||||
|
std::string pendingNodeAfterCutscene;
|
||||||
|
|
||||||
|
void onCutsceneFinishedInternal(const std::string& id);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZL::Dialogue
|
} // namespace ZL::Dialogue
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "cutscene/CutsceneTypes.h"
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
@ -31,28 +32,6 @@ 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;
|
||||||
@ -116,84 +95,12 @@ struct DialogueDefinition {
|
|||||||
std::unordered_map<std::string, Node> nodes;
|
std::unordered_map<std::string, Node> nodes;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct CutsceneLine {
|
|
||||||
std::string speaker;
|
|
||||||
std::string text;
|
|
||||||
std::string portrait;
|
|
||||||
std::string sfx;
|
|
||||||
std::string background;
|
|
||||||
std::string luaCallback;
|
|
||||||
int backgroundWidth = 0; // 0 = inherit from cutscene
|
|
||||||
int backgroundHeight = 0; // 0 = inherit from cutscene
|
|
||||||
int durationMs = 0;
|
|
||||||
bool waitForConfirm = false;
|
|
||||||
|
|
||||||
// Quest actions fired when this line is shown (empty = no action)
|
|
||||||
std::string questUnlock;
|
|
||||||
std::string questComplete;
|
|
||||||
std::string questFail;
|
|
||||||
std::string objectiveComplete; // "quest_id.objective_id"
|
|
||||||
std::string objectiveVisible; // "quest_id.objective_id"
|
|
||||||
};
|
|
||||||
|
|
||||||
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 CutsceneImageCue {
|
|
||||||
std::string path;
|
|
||||||
int startMs = 0;
|
|
||||||
int endMs = 0;
|
|
||||||
int fadeInMs = 0;
|
|
||||||
int fadeOutMs = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct StaticCutsceneDefinition {
|
|
||||||
std::string id;
|
|
||||||
std::string background;
|
|
||||||
int backgroundWidth = 1280;
|
|
||||||
int backgroundHeight = 720;
|
|
||||||
std::string music;
|
|
||||||
std::string onFadeInCallback;
|
|
||||||
bool skippable = true;
|
|
||||||
int durationMs = 0;
|
|
||||||
int fadeOutMs = 0;
|
|
||||||
int fadeInMs = 0;
|
|
||||||
int endFadeOutMs = 0;
|
|
||||||
int endFadeInMs = 0;
|
|
||||||
std::vector<CutsceneCameraSegment> cameraTrack;
|
|
||||||
std::vector<CutsceneImageCue> images;
|
|
||||||
std::vector<CutsceneLine> lines;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct PresentedChoice {
|
struct PresentedChoice {
|
||||||
std::string id;
|
std::string id;
|
||||||
std::string text;
|
std::string text;
|
||||||
ChoiceKind kind = ChoiceKind::Main;
|
ChoiceKind kind = ChoiceKind::Main;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PresentedCutsceneImage {
|
|
||||||
std::string path;
|
|
||||||
float alpha = 1.0f;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class PresentationMode {
|
enum class PresentationMode {
|
||||||
Hidden,
|
Hidden,
|
||||||
Dialogue,
|
Dialogue,
|
||||||
@ -201,13 +108,6 @@ 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;
|
||||||
@ -222,24 +122,12 @@ struct PresentationModel {
|
|||||||
bool showCutsceneSubtitle = false;
|
bool showCutsceneSubtitle = false;
|
||||||
bool cutsceneSkippable = false;
|
bool cutsceneSkippable = false;
|
||||||
|
|
||||||
CutsceneCameraBlendState cutsceneCamera;
|
ZL::Cutscene::CutsceneCameraBlendState cutsceneCamera;
|
||||||
std::vector<PresentedCutsceneImage> cutsceneImages;
|
std::vector<ZL::Cutscene::PresentedCutsceneImage> cutsceneImages;
|
||||||
float cutsceneGlobalFadeAlpha = 1.0f;
|
float cutsceneGlobalFadeAlpha = 1.0f;
|
||||||
float cutsceneBlackAlpha = 0.0f;
|
float cutsceneBlackAlpha = 0.0f;
|
||||||
int backgroundWidth = 1280;
|
int backgroundWidth = 1280;
|
||||||
int backgroundHeight = 720;
|
int backgroundHeight = 720;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SaveState {
|
|
||||||
std::string dialogueId;
|
|
||||||
std::string currentNodeId;
|
|
||||||
std::string pendingNodeAfterCutscene;
|
|
||||||
std::unordered_map<std::string, int> flags;
|
|
||||||
std::unordered_set<std::string> consumedChoices;
|
|
||||||
int selectedChoice = -1;
|
|
||||||
int currentCutsceneLine = -1;
|
|
||||||
int cutsceneTimerMs = 0;
|
|
||||||
bool active = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace ZL::Dialogue
|
} // namespace ZL::Dialogue
|
||||||
|
|||||||
65
src/render/UiQuad.h
Normal file
65
src/render/UiQuad.h
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "render/Renderer.h"
|
||||||
|
#include "UiManager.h"
|
||||||
|
#include <Eigen/Core>
|
||||||
|
|
||||||
|
namespace ZL
|
||||||
|
{
|
||||||
|
// Axis-aligned textured quad with cached mesh. Rebuild only when rect changes.
|
||||||
|
struct UiQuad {
|
||||||
|
UiRect rect{};
|
||||||
|
VertexRenderStruct mesh;
|
||||||
|
bool initialized = false;
|
||||||
|
|
||||||
|
void rebuild(const UiRect& newRect) {
|
||||||
|
rect = newRect;
|
||||||
|
mesh.data = CreateRect2D(
|
||||||
|
{ rect.x + rect.w * 0.5f, rect.y + rect.h * 0.5f },
|
||||||
|
{ rect.w * 0.5f, rect.h * 0.5f },
|
||||||
|
0.0f
|
||||||
|
);
|
||||||
|
mesh.RefreshVBO();
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void 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 },
|
||||||
|
{ x0, y1, 0.0f },
|
||||||
|
{ x1, y1, 0.0f },
|
||||||
|
|
||||||
|
{ x1, y1, 0.0f },
|
||||||
|
{ x1, y0, 0.0f },
|
||||||
|
{ x0, y0, 0.0f }
|
||||||
|
};
|
||||||
|
data.TexCoordData = {
|
||||||
|
uvBottomLeft,
|
||||||
|
uvTopLeft,
|
||||||
|
uvTopRight,
|
||||||
|
|
||||||
|
uvTopRight,
|
||||||
|
uvBottomRight,
|
||||||
|
uvBottomLeft
|
||||||
|
};
|
||||||
|
|
||||||
|
mesh.AssignFrom(data);
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user