#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); choiceMainTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_main.png", zipFile); choiceOptionalTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_optional.png", zipFile); choiceSelectedTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_selected.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; } const float W = Environment::projectionWidth; UiRect portraitRect{ 24.0f + 90, 24.0f + 16, 176.0f, 176.0f }; UiRect textboxRect{ 30.f, -48.f, 1222.f, 340.0f }; 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); } 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); glBindTexture(GL_TEXTURE_2D, textboxTexture->getTexID()); renderer.DrawVertexRenderStruct(textboxQuad.mesh); { auto portrait = loadTextureCached(model.portraitPath); if (portrait) { glBindTexture(GL_TEXTURE_2D, portrait->getTexID()); renderer.DrawVertexRenderStruct(portraitQuad.mesh); } } renderer.PopMatrix(); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); const float nameX = 312; const float nameY = 232 - 38.0f; const float bodyX = 312; const float bodyY = 232 - 78.0f; if (!model.speaker.empty()) { nameRenderer->drawText(model.speaker, nameX, nameY, 1.0f, false, { 1.0f, 0.88f, 0.45f, 1.0f }); } const float bodyMaxWidthPx = W - nameX - 48.f - ZL::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 }); 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 cy = choiceStartY + (choiceHeight + choiceSpacing) * static_cast(model.choices.size() - 1 - i); UiRect rect{ textboxRect.x + 20.0f, 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 + 14.0f, rect.y + 9.0f, 1.0f, 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 = rectContains(lastDialogueAdvanceRect, x, y); return outAdvanceDialogue; } 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