678 lines
23 KiB
C++
678 lines
23 KiB
C++
#include "dialogue/DialogueOverlay.h"
|
|
#include "dialogue/DialogueTypes.h"
|
|
|
|
#include "GameConstants.h"
|
|
#include "Environment.h"
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
|
|
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) {
|
|
rendererRef = &renderer;
|
|
zipFilename = zipFile;
|
|
|
|
textboxTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/dialogue/textbox_bg.png", zipFile));
|
|
portraitFrameTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/dialogue/portrait_frame.png", zipFile));
|
|
choiceMainTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/dialogue/choice_main.png", zipFile));
|
|
choiceOptionalTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/dialogue/choice_optional.png", zipFile));
|
|
choiceSelectedTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/dialogue/choice_selected.png", zipFile));
|
|
cutsceneSubtitleTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/dialogue/cutscene_subtitle_bg.png", zipFile));
|
|
|
|
nameRenderer = std::make_unique<TextRenderer>();
|
|
bodyRenderer = std::make_unique<TextRenderer>();
|
|
choiceRenderer = std::make_unique<TextRenderer>();
|
|
cutsceneRenderer = std::make_unique<TextRenderer>();
|
|
|
|
const bool ok =
|
|
nameRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 28, zipFile) &&
|
|
bodyRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 24, 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) {
|
|
if (model.mode == PresentationMode::Hidden) {
|
|
hoveredChoiceIndex = -1;
|
|
cutsceneSkipHintVisible = false;
|
|
cutsceneSkipArmed = false;
|
|
cutsceneSkipHolding = false;
|
|
cutsceneSkipTriggered = false;
|
|
cutsceneSkipHintRemainingMs = 0;
|
|
cutsceneSkipHoldElapsedMs = 0;
|
|
lastChoiceRects.clear();
|
|
lastDialogueAdvanceRect = {};
|
|
lastCutsceneAdvanceRect = {};
|
|
return;
|
|
}
|
|
|
|
if (model.mode != PresentationMode::Choice) {
|
|
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) {
|
|
if (model.mode == PresentationMode::Hidden) {
|
|
lastChoiceRects.clear();
|
|
lastDialogueAdvanceRect = {};
|
|
lastCutsceneAdvanceRect = {};
|
|
cutsceneSkipHintVisible = false;
|
|
cutsceneSkipArmed = false;
|
|
cutsceneSkipHolding = false;
|
|
cutsceneSkipHintRemainingMs = 0;
|
|
cutsceneSkipHoldElapsedMs = 0;
|
|
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 H = Environment::projectionHeight;
|
|
|
|
const UiRect portraitRect{ 24.0f, 24.0f, 182.0f, 182.0f };
|
|
const UiRect textboxRect{ 220.0f, 24.0f, max(200.0f, W - 244.0f), 182.0f };
|
|
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 ||
|
|
portraitQuad.rect.x != portraitRect.x || portraitQuad.rect.y != portraitRect.y) {
|
|
portraitQuad.rebuild(portraitRect);
|
|
}
|
|
if (!textboxQuad.initialized || textboxQuad.rect.w != textboxRect.w || textboxQuad.rect.h != textboxRect.h ||
|
|
textboxQuad.rect.x != textboxRect.x || textboxQuad.rect.y != textboxRect.y) {
|
|
textboxQuad.rebuild(textboxRect);
|
|
}
|
|
|
|
glEnable(GL_BLEND);
|
|
renderer.shaderManager.PushShader(defaultShaderName);
|
|
renderer.RenderUniform1i(textureUniformName, 0);
|
|
renderer.PushProjectionMatrix(0.0f, W, 0.0f, Environment::projectionHeight, -10.0f, 10.0f);
|
|
renderer.PushMatrix();
|
|
renderer.LoadIdentity();
|
|
|
|
drawQuad(renderer, textboxQuad, textboxTexture);
|
|
drawQuad(renderer, portraitQuad, model.portraitPath.empty() ? portraitFrameTexture : loadTextureCached(model.portraitPath));
|
|
drawQuad(renderer, portraitQuad, portraitFrameTexture);
|
|
|
|
renderer.PopMatrix();
|
|
renderer.PopProjectionMatrix();
|
|
renderer.shaderManager.PopShader();
|
|
|
|
const float nameX = textboxRect.x + 24.0f;
|
|
const float nameY = textboxRect.y + textboxRect.h - 38.0f;
|
|
const float bodyX = textboxRect.x + 24.0f;
|
|
const float bodyY = textboxRect.y + textboxRect.h - 78.0f;
|
|
|
|
if (!model.speaker.empty()) {
|
|
nameRenderer->drawText(model.speaker, nameX, nameY, 1.0f, false, { 1.0f, 0.88f, 0.45f, 1.0f });
|
|
}
|
|
|
|
const std::string wrappedBody = wrapText(model.visibleText, 56);
|
|
bodyRenderer->drawText(wrappedBody, bodyX, bodyY, 1.0f, false, { 1.0f, 1.0f, 1.0f, 1.0f });
|
|
|
|
lastChoiceRects.clear();
|
|
if (model.mode == PresentationMode::Choice) {
|
|
const float choiceStartY = textboxRect.y + 56.0f;
|
|
const float choiceHeight = 30.0f;
|
|
const float choiceSpacing = 8.0f;
|
|
const float choiceWidth = textboxRect.w - 48.0f;
|
|
|
|
if (choiceQuads.size() < model.choices.size()) {
|
|
choiceQuads.resize(model.choices.size());
|
|
}
|
|
|
|
renderer.shaderManager.PushShader(defaultShaderName);
|
|
renderer.RenderUniform1i(textureUniformName, 0);
|
|
renderer.PushProjectionMatrix(0.0f, W, 0.0f, Environment::projectionHeight, -10.0f, 10.0f);
|
|
renderer.PushMatrix();
|
|
renderer.LoadIdentity();
|
|
|
|
for (size_t i = 0; i < model.choices.size(); ++i) {
|
|
const float y = choiceStartY + (choiceHeight + choiceSpacing) * static_cast<float>(model.choices.size() - 1 - i);
|
|
UiRect rect{ textboxRect.x + 20.0f, y, choiceWidth, choiceHeight };
|
|
lastChoiceRects.push_back(rect);
|
|
choiceQuads[i].rebuild(rect);
|
|
|
|
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;
|
|
if (isHighlighted) {
|
|
choiceTexture = choiceSelectedTexture;
|
|
}
|
|
drawQuad(renderer, choiceQuads[i], choiceTexture);
|
|
}
|
|
|
|
renderer.PopMatrix();
|
|
renderer.PopProjectionMatrix();
|
|
renderer.shaderManager.PopShader();
|
|
|
|
for (size_t i = 0; i < model.choices.size(); ++i) {
|
|
const UiRect& rect = lastChoiceRects[i];
|
|
const bool isHighlighted = static_cast<int>(i) == hoveredChoiceIndex || static_cast<int>(i) == model.selectedChoice;
|
|
const std::array<float, 4> color = (model.choices[i].kind == ChoiceKind::Optional)
|
|
? std::array<float, 4>{0.82f, 0.82f, 0.82f, 1.0f}
|
|
: std::array<float, 4>{ 1.0f, 0.93f, 0.65f, 1.0f };
|
|
|
|
choiceRenderer->drawText(
|
|
wrapText(model.choices[i].text, 52),
|
|
rect.x + 14.0f,
|
|
rect.y + 9.0f,
|
|
1.0f,
|
|
false,
|
|
isHighlighted ? std::array<float, 4>{1.0f, 1.0f, 1.0f, 1.0f} : color
|
|
);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
const float texW = static_cast<float>(texture->getWidth());
|
|
const float texH = static_cast<float>(texture->getHeight());
|
|
|
|
ResolvedViewport layerViewport{};
|
|
|
|
if (model.cutsceneCamera.active) {
|
|
const ResolvedViewport fromViewport = resolveViewportPose(model.cutsceneCamera.from, texW, texH, W, H);
|
|
const ResolvedViewport toViewport = resolveViewportPose(model.cutsceneCamera.to, texW, texH, W, H);
|
|
layerViewport = blendViewport(
|
|
fromViewport,
|
|
toViewport,
|
|
std::clamp(model.cutsceneCamera.t, 0.0f, 1.0f)
|
|
);
|
|
}
|
|
else {
|
|
layerViewport = resolveViewportPose(CutsceneCameraPose{}, texW, texH, 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 image pixel space (origin = bottom-left)
|
|
const Eigen::Vector2f srcBL = rotatePoint(-halfW, -halfH);
|
|
const Eigen::Vector2f srcTL = rotatePoint(-halfW, +halfH);
|
|
const Eigen::Vector2f srcTR = rotatePoint(+halfW, +halfH);
|
|
const Eigen::Vector2f srcBR = rotatePoint(+halfW, -halfH);
|
|
|
|
auto toUV = [&](const Eigen::Vector2f& p) -> Eigen::Vector2f {
|
|
return {
|
|
std::clamp(p.x() / max(texW, 1.0f), 0.0f, 1.0f),
|
|
std::clamp(p.y() / max(texH, 1.0f), 0.0f, 1.0f)
|
|
};
|
|
};
|
|
|
|
backgroundQuad.rebuildWithUV(
|
|
screenRect,
|
|
toUV(srcBL),
|
|
toUV(srcTL),
|
|
toUV(srcTR),
|
|
toUV(srcBR)
|
|
);
|
|
|
|
renderer.RenderUniform1f("uAlpha", std::clamp(layer.alpha, 0.0f, 1.0f));
|
|
drawQuad(renderer, backgroundQuad, texture);
|
|
}
|
|
|
|
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 }
|
|
);
|
|
}
|
|
cutsceneRenderer->drawText(
|
|
wrapText(model.visibleText, 62),
|
|
subtitleRect.x + 24.0f,
|
|
subtitleRect.y + 30.0f,
|
|
1.0f,
|
|
false,
|
|
{ 1.0f, 1.0f, 1.0f, 1.0f }
|
|
);
|
|
}
|
|
|
|
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)x;
|
|
(void)y;
|
|
|
|
if (model.mode == PresentationMode::Choice) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
void DialogueOverlay::handlePointerMoved(float x, float y, const PresentationModel& model) {
|
|
if (model.mode == PresentationMode::Choice) {
|
|
hoveredChoiceIndex = -1;
|
|
for (size_t i = 0; i < lastChoiceRects.size(); ++i) {
|
|
if (rectContains(lastChoiceRects[i], x, y)) {
|
|
hoveredChoiceIndex = static_cast<int>(i);
|
|
break;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
hoveredChoiceIndex = -1;
|
|
}
|
|
|
|
bool DialogueOverlay::handlePointerReleased(float x, float y, const PresentationModel& model, int& outChoiceIndex, bool& outAdvanceDialogue) {
|
|
outChoiceIndex = -1;
|
|
outAdvanceDialogue = false;
|
|
|
|
if (model.mode == PresentationMode::Choice) {
|
|
for (size_t i = 0; i < lastChoiceRects.size(); ++i) {
|
|
if (rectContains(lastChoiceRects[i], x, y)) {
|
|
outChoiceIndex = static_cast<int>(i);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (model.mode == PresentationMode::Dialogue) {
|
|
outAdvanceDialogue = rectContains(lastDialogueAdvanceRect, x, y);
|
|
return outAdvanceDialogue;
|
|
}
|
|
|
|
if (model.mode == PresentationMode::Cutscene) {
|
|
if (cutsceneSkipHolding && cutsceneSkipHoldElapsedMs < CutsceneSkipHoldDurationMs) {
|
|
cutsceneSkipHolding = false;
|
|
cutsceneSkipHoldElapsedMs = 0;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
std::shared_ptr<Texture> DialogueOverlay::loadTextureCached(const std::string& path) {
|
|
if (path.empty()) {
|
|
return nullptr;
|
|
}
|
|
|
|
auto it = textureCache.find(path);
|
|
if (it != textureCache.end()) {
|
|
return it->second;
|
|
}
|
|
|
|
auto texture = std::make_shared<Texture>(CreateTextureDataFromPng(path, zipFilename));
|
|
textureCache[path] = texture;
|
|
return texture;
|
|
}
|
|
|
|
void DialogueOverlay::drawQuad(Renderer& renderer, const TexturedQuad& quad, const std::shared_ptr<Texture>& texture) const {
|
|
if (!texture) {
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
bool DialogueOverlay::rectContains(const UiRect& rect, float x, float y) {
|
|
return x >= rect.x && x <= rect.x + rect.w && y >= rect.y && y <= rect.y + rect.h;
|
|
}
|
|
|
|
} // namespace ZL::Dialogue
|