#include "dialogue/DialogueOverlay.h" #include "dialogue/DialogueTypes.h" #include "GameConstants.h" #include "Environment.h" #include #include 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); 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(); bodyRenderer = std::make_unique(); choiceRenderer = std::make_unique(); 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 choiceStartY = 270.f-35; const float choiceHeight = 121.f*0.9; const float choiceSpacing = -30.f; const float choiceWidth = 913.f * 0.9; 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(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(i) == hoveredChoiceIndex || static_cast(i) == model.selectedChoice; std::shared_ptr 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(i) == hoveredChoiceIndex || static_cast(i) == model.selectedChoice; const std::array color = (model.choices[i].kind == ChoiceKind::Optional) ? std::array{0.82f, 0.82f, 0.82f, 1.0f} : std::array{ 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{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(textboxChoiceTexture->getWidth()) / static_cast(textboxChoiceTexture->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, textboxChoiceTexture->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(choiceMainTexture->getWidth()) / static_cast(choiceMainTexture->getHeight()); const float choiceW = W - leftPad - rightPad; const float choiceH = choiceW / choiceTexAspect; const float choiceSpacing = 8.f; const float totalChoicesH = (choiceH + choiceSpacing) * static_cast(model.choices.size()) - choiceSpacing; const float choiceStartY = panelTopY - 34.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(model.choices.size() - 1 - i); UiRect rect{ leftPad, cy, choiceW, choiceH }; lastChoiceRects.push_back(rect); choiceQuads[i].rebuild(rect); const bool isHighlighted = static_cast(i) == hoveredChoiceIndex || static_cast(i) == model.selectedChoice; std::shared_ptr 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(i) == hoveredChoiceIndex || static_cast(i) == model.selectedChoice; const std::array color = (model.choices[i].kind == ChoiceKind::Optional) ? std::array{0.82f, 0.82f, 0.82f, 1.0f} : std::array{ 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, 1.5f, false, isHighlighted ? std::array{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(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(i); return true; } } return false; } if (model.mode == PresentationMode::Dialogue) { outAdvanceDialogue = true; return true; } return false; } std::shared_ptr 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