451 lines
16 KiB
C++
451 lines
16 KiB
C++
#include "dialogue/DialogueOverlay.h"
|
|
#include "dialogue/DialogueTypes.h"
|
|
|
|
#include "GameConstants.h"
|
|
#include "Environment.h"
|
|
#include <algorithm>
|
|
#include <array>
|
|
|
|
namespace ZL
|
|
{
|
|
extern float x;
|
|
extern float y;
|
|
}
|
|
|
|
namespace ZL::Dialogue {
|
|
|
|
bool DialogueOverlay::init(Renderer& renderer, const std::string& zipFile) {
|
|
rendererRef = &renderer;
|
|
zipFilename = zipFile;
|
|
|
|
textboxTexture = renderer.textureManager.LoadFromPng("resources/dialogue/textbox_bg.png", zipFile);
|
|
textboxChoiceTexture = renderer.textureManager.LoadFromPng("resources/dialogue/DialogChoice001.png", zipFile);
|
|
textboxChoicePortraitTexture = renderer.textureManager.LoadFromPng("resources/dialogue/DialogChoicePortrait001.png", zipFile);
|
|
|
|
choiceMainTexture = renderer.textureManager.LoadFromPng("resources/dialogue/ChoiceNormal001.png", zipFile);
|
|
choiceOptionalTexture = renderer.textureManager.LoadFromPng("resources/dialogue/ChoiceNormal001.png", zipFile);
|
|
choiceSelectedTexture = renderer.textureManager.LoadFromPng("resources/dialogue/ChoiceSelected001.png", zipFile);
|
|
|
|
nameRenderer = std::make_unique<TextRenderer>();
|
|
bodyRenderer = std::make_unique<TextRenderer>();
|
|
choiceRenderer = std::make_unique<TextRenderer>();
|
|
|
|
return
|
|
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);
|
|
}
|
|
|
|
void DialogueOverlay::update(const PresentationModel& model, int deltaMs) {
|
|
(void)deltaMs;
|
|
if (model.mode == PresentationMode::Hidden) {
|
|
hoveredChoiceIndex = -1;
|
|
lastChoiceRects.clear();
|
|
lastDialogueAdvanceRect = {};
|
|
return;
|
|
}
|
|
if (model.mode != PresentationMode::Choice) {
|
|
hoveredChoiceIndex = -1;
|
|
}
|
|
}
|
|
|
|
|
|
void DialogueOverlay::draw(Renderer& renderer, const PresentationModel& model) {
|
|
if (model.mode != PresentationMode::Dialogue && model.mode != PresentationMode::Choice) {
|
|
lastChoiceRects.clear();
|
|
lastDialogueAdvanceRect = {};
|
|
return;
|
|
}
|
|
|
|
if (Environment::projectionWidth < Environment::projectionHeight)
|
|
drawPortrait(renderer, model);
|
|
else
|
|
drawLandscape(renderer, model);
|
|
}
|
|
|
|
void DialogueOverlay::drawLandscape(Renderer& renderer, const PresentationModel& model) {
|
|
const float W = Environment::projectionWidth;
|
|
|
|
const float nameX = 312 + 8;
|
|
const float nameY = 232 - 38.0f;
|
|
const float bodyX = 312 + 8;
|
|
const float bodyY = 232 - 78.0f;
|
|
|
|
const float bodyMaxWidthPx = 1280.0 - nameX - 48.f - 60.f;
|
|
|
|
|
|
float centeredShiftX = ((W - nameX - 48.f - 60.f) - bodyMaxWidthPx)*0.5;
|
|
|
|
UiRect portraitRect{ 24.0f + 90 + centeredShiftX, 24.0f + 16, 176.0f, 176.0f };
|
|
UiRect textboxRect{ 30.f + centeredShiftX, -48.f, 1222.f, 340.0f };
|
|
UiRect textboxChoiceRect{ 30.f + centeredShiftX, -90.f, 1200.f, 619.5f };
|
|
|
|
lastDialogueAdvanceRect = { portraitRect.x, portraitRect.y, textboxRect.x + textboxRect.w - portraitRect.x, textboxRect.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.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);
|
|
}
|
|
|
|
if (!textboxChoiceQuad.initialized || textboxChoiceQuad.rect.w != textboxChoiceRect.w || textboxChoiceQuad.rect.h != textboxChoiceRect.h ||
|
|
textboxChoiceQuad.rect.x != textboxChoiceRect.x || textboxChoiceQuad.rect.y != textboxChoiceRect.y) {
|
|
textboxChoiceQuad.rebuild(textboxChoiceRect);
|
|
}
|
|
|
|
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();
|
|
renderer.RenderUniform1f("uAlpha", 1.0f);
|
|
|
|
if (model.mode == PresentationMode::Choice)
|
|
{
|
|
glBindTexture(GL_TEXTURE_2D, textboxChoiceTexture->getTexID());
|
|
renderer.DrawVertexRenderStruct(textboxChoiceQuad.mesh);
|
|
}
|
|
else
|
|
{
|
|
glBindTexture(GL_TEXTURE_2D, textboxTexture->getTexID());
|
|
renderer.DrawVertexRenderStruct(textboxQuad.mesh);
|
|
}
|
|
|
|
{
|
|
renderer.PushMatrix();
|
|
|
|
if (model.mode == PresentationMode::Choice)
|
|
{
|
|
renderer.TranslateMatrix(Vector3f{ 20, 230, 0 });
|
|
}
|
|
|
|
auto portrait = loadTextureCached(model.portraitPath);
|
|
if (portrait) {
|
|
glBindTexture(GL_TEXTURE_2D, portrait->getTexID());
|
|
renderer.DrawVertexRenderStruct(portraitQuad.mesh);
|
|
}
|
|
renderer.PopMatrix();
|
|
}
|
|
|
|
renderer.PopMatrix();
|
|
renderer.PopProjectionMatrix();
|
|
renderer.shaderManager.PopShader();
|
|
|
|
|
|
|
|
lastChoiceRects.clear();
|
|
|
|
if (!model.speaker.empty()) {
|
|
|
|
if (model.mode == PresentationMode::Choice)
|
|
{
|
|
nameRenderer->drawText(model.speaker, nameX+20 + centeredShiftX, nameY+230, 1.0f, false, { 1.0f, 0.88f, 0.45f, 1.0f });
|
|
}
|
|
else
|
|
{
|
|
|
|
nameRenderer->drawText(model.speaker, nameX + centeredShiftX, nameY, 1.0f, false, { 1.0f, 0.88f, 0.45f, 1.0f });
|
|
}
|
|
}
|
|
|
|
const std::string wrappedBody = wrapTextToWidth(model.visibleText, *bodyRenderer, bodyMaxWidthPx, 1.0f);
|
|
bodyRenderer->drawText(wrappedBody, bodyX + centeredShiftX, bodyY, 1.0f, false, { 1.0f, 1.0f, 1.0f, 1.0f });
|
|
|
|
|
|
if (model.mode == PresentationMode::Choice) {
|
|
const float choiceHeight = 121.f*0.9;
|
|
const float choiceSpacing = -30.f;
|
|
const float choiceWidth = 913.f * 0.9;
|
|
float choiceStartY = 400.f - (choiceHeight + choiceSpacing) * (model.choices.size());
|
|
|
|
|
|
if (choiceQuads.size() < model.choices.size()) {
|
|
choiceQuads.resize(model.choices.size());
|
|
}
|
|
|
|
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();
|
|
|
|
for (size_t i = 0; i < model.choices.size(); ++i) {
|
|
const float cy = choiceStartY + (choiceHeight + choiceSpacing) * static_cast<float>(model.choices.size() - 1 - i);
|
|
UiRect rect{ 320.f + centeredShiftX , cy, 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;
|
|
glBindTexture(GL_TEXTURE_2D, choiceTexture->getTexID());
|
|
renderer.DrawVertexRenderStruct(choiceQuads[i].mesh);
|
|
}
|
|
|
|
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 };
|
|
|
|
const std::string wrappedChoice = wrapTextToWidth(model.choices[i].text, *choiceRenderer, rect.w - 28.0f, 1.0f);
|
|
choiceRenderer->drawText(
|
|
wrappedChoice, rect.x + 32.0f, rect.y + 50.f, 1.5f, false,
|
|
isHighlighted ? std::array<float, 4>{1.0f, 1.0f, 1.0f, 1.0f} : color
|
|
);
|
|
}
|
|
}
|
|
|
|
glDisable(GL_BLEND);
|
|
}
|
|
|
|
void DialogueOverlay::drawPortrait(Renderer& renderer, const PresentationModel& model) {
|
|
const float W = Environment::projectionWidth;
|
|
const float H = Environment::projectionHeight;
|
|
|
|
// Panel: DialogChoice001.png scaled to full screen width, aspect ratio preserved.
|
|
const float texAspect = static_cast<float>(textboxChoicePortraitTexture->getWidth())
|
|
/ static_cast<float>(textboxChoicePortraitTexture->getHeight());
|
|
const float panelW = W;
|
|
const float panelH = panelW / texAspect;
|
|
const float clipAtBottom = 18.f;
|
|
const float panelTopY = panelH - clipAtBottom;
|
|
|
|
// Character portrait sits above the panel, shifted right and slightly overlapping.
|
|
const float portraitSize = 200.f;
|
|
const float portraitX = 56.f;
|
|
const float portraitY = panelTopY + 6.f - 40.f;
|
|
|
|
// Text sits inside the panel near the top.
|
|
const float leftPad = 60.f;
|
|
const float rightPad = 60.f;
|
|
const float textMaxWidth = W - leftPad - rightPad;
|
|
const float nameY = panelTopY - 32.f - 50.f;
|
|
const float bodyY = nameY - 38.f;
|
|
|
|
// --- Rebuild quads only when geometry changes ---
|
|
{
|
|
UiRect panelRect{ 0.f, -clipAtBottom, panelW, panelH };
|
|
if (!portraitPanelQuad.initialized
|
|
|| portraitPanelQuad.rect.x != panelRect.x || portraitPanelQuad.rect.y != panelRect.y
|
|
|| portraitPanelQuad.rect.w != panelRect.w || portraitPanelQuad.rect.h != panelRect.h) {
|
|
portraitPanelQuad.rebuild(panelRect);
|
|
}
|
|
}
|
|
{
|
|
UiRect portraitRect{ portraitX, portraitY, portraitSize, portraitSize };
|
|
if (!portraitAbovePortraitQuad.initialized
|
|
|| portraitAbovePortraitQuad.rect.x != portraitRect.x || portraitAbovePortraitQuad.rect.y != portraitRect.y
|
|
|| portraitAbovePortraitQuad.rect.w != portraitRect.w || portraitAbovePortraitQuad.rect.h != portraitRect.h) {
|
|
portraitAbovePortraitQuad.rebuild(portraitRect);
|
|
}
|
|
}
|
|
|
|
// --- Draw panel and portrait image ---
|
|
glEnable(GL_BLEND);
|
|
renderer.shaderManager.PushShader(defaultShaderName);
|
|
renderer.RenderUniform1i(textureUniformName, 0);
|
|
renderer.PushProjectionMatrix(0.f, W, 0.f, H, -10.f, 10.f);
|
|
renderer.PushMatrix();
|
|
renderer.LoadIdentity();
|
|
renderer.RenderUniform1f("uAlpha", 1.0f);
|
|
|
|
glBindTexture(GL_TEXTURE_2D, textboxChoicePortraitTexture->getTexID());
|
|
renderer.DrawVertexRenderStruct(portraitPanelQuad.mesh);
|
|
|
|
if (model.portraitPath != "resources/dialogue/portrait_phone.png")
|
|
{
|
|
|
|
auto portrait = loadTextureCached(model.portraitPath);
|
|
if (portrait) {
|
|
glBindTexture(GL_TEXTURE_2D, portrait->getTexID());
|
|
renderer.DrawVertexRenderStruct(portraitAbovePortraitQuad.mesh);
|
|
}
|
|
}
|
|
|
|
renderer.PopMatrix();
|
|
renderer.PopProjectionMatrix();
|
|
renderer.shaderManager.PopShader();
|
|
|
|
// --- Name and body text ---
|
|
lastChoiceRects.clear();
|
|
|
|
if (!model.speaker.empty()) {
|
|
nameRenderer->drawText(model.speaker, leftPad, nameY, 1.0f, false, { 1.0f, 0.88f, 0.45f, 1.0f });
|
|
}
|
|
|
|
const std::string wrappedBody = wrapTextToWidth(model.visibleText, *bodyRenderer, textMaxWidth, 1.0f);
|
|
bodyRenderer->drawText(wrappedBody, leftPad, bodyY, 1.0f, false, { 1.0f, 1.0f, 1.0f, 1.0f });
|
|
|
|
// Hit rect for tap-to-advance.
|
|
lastDialogueAdvanceRect = { 0.f, 0.f, W, panelTopY };
|
|
|
|
// --- Choice mode: stack choices inside the panel ---
|
|
if (model.mode == PresentationMode::Choice) {
|
|
const float choiceTexAspect = static_cast<float>(choiceMainTexture->getWidth())
|
|
/ static_cast<float>(choiceMainTexture->getHeight());
|
|
const float choiceW = W - leftPad - rightPad + 32;
|
|
const float choiceH = choiceW / choiceTexAspect;
|
|
const float choiceSpacing = -18.f;
|
|
|
|
const float totalChoicesH = (choiceH + choiceSpacing) * static_cast<float>(model.choices.size()) - choiceSpacing;
|
|
const float choiceStartY = panelTopY - 80.f - totalChoicesH; // below name row
|
|
|
|
if (choiceQuads.size() < model.choices.size()) {
|
|
choiceQuads.resize(model.choices.size());
|
|
}
|
|
|
|
glEnable(GL_BLEND);
|
|
renderer.shaderManager.PushShader(defaultShaderName);
|
|
renderer.RenderUniform1i(textureUniformName, 0);
|
|
renderer.PushProjectionMatrix(0.f, W, 0.f, H, -10.f, 10.f);
|
|
renderer.PushMatrix();
|
|
renderer.LoadIdentity();
|
|
|
|
for (size_t i = 0; i < model.choices.size(); ++i) {
|
|
const float cy = choiceStartY + (choiceH + choiceSpacing) * static_cast<float>(model.choices.size() - 1 - i);
|
|
UiRect rect{ leftPad-16.f, cy, choiceW, choiceH };
|
|
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;
|
|
glBindTexture(GL_TEXTURE_2D, choiceTexture->getTexID());
|
|
renderer.DrawVertexRenderStruct(choiceQuads[i].mesh);
|
|
}
|
|
|
|
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 };
|
|
|
|
const std::string wrappedChoice = wrapTextToWidth(model.choices[i].text, *choiceRenderer, rect.w - 20.0f, 1.0f);
|
|
choiceRenderer->drawText(
|
|
wrappedChoice, rect.x + 20.f, rect.y + choiceH * 0.35f+5.f, 1.4f, false,
|
|
isHighlighted ? std::array<float, 4>{1.0f, 1.0f, 1.0f, 1.0f} : color
|
|
);
|
|
}
|
|
}
|
|
|
|
glDisable(GL_BLEND);
|
|
}
|
|
|
|
void DialogueOverlay::handlePointerDown(float x, float y, const PresentationModel& model) {
|
|
if (model.mode == PresentationMode::Choice) {
|
|
handlePointerMoved(x, y, model);
|
|
}
|
|
}
|
|
|
|
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 = true;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
std::shared_ptr<Texture> DialogueOverlay::loadTextureCached(const std::string& path) {
|
|
if (path.empty()) return nullptr;
|
|
return rendererRef->textureManager.LoadFromPng(path, zipFilename);
|
|
}
|
|
|
|
std::string DialogueOverlay::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;
|
|
}
|
|
|
|
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
|