diff --git a/resources/w/ui/portrait_screen_phone_chat1.json b/resources/w/ui/portrait_screen_phone_chat1.json new file mode 100644 index 0000000..8306e0c --- /dev/null +++ b/resources/w/ui/portrait_screen_phone_chat1.json @@ -0,0 +1,83 @@ +{ + "root": { + "type": "FrameLayout", + "name": "hud_root", + "width": "match_parent", + "height": "match_parent", + "vertical_align": "center", + "horizontal_align": "center", + "children": [ + { + "type": "Button", + "name": "phoneExitButton", + "horizontal_gravity": "center", + "vertical_gravity": "center", + "y": 0, + "width": "match_parent", + "height": "match_parent", + "textures": { + "normal": "resources/transparent.png", + "hover": "resources/transparent.png", + "pressed": "resources/transparent.png" + } + }, + { + "type": "Button", + "name": "phoneMain", + "horizontal_gravity": "center", + "vertical_gravity": "center", + "y": -60, + "width": 882, + "height": 1416, + "textures": { + "normal": "resources/w/ui/img/phone/PhoneChat001.png", + "hover": "resources/w/ui/img/phone/PhoneChat001.png", + "pressed": "resources/w/ui/img/phone/PhoneChat001.png" + } + }, + { + "type": "FrameLayout", + "name": "chatMessagesContainer", + "horizontal_gravity": "center", + "vertical_gravity": "center", + "width": "match_parent", + "height": 1000 + }, + { + "type": "StaticImage", + "name": "chatTopCover", + "width": 638, + "height": 101.25, + "x": 0, + "y": 536, + "horizontal_gravity": "center", + "vertical_gravity": "center", + "texture": "resources/w/ui/img/phone/chat_top_cover001.png" + }, + { + "type": "TextButton", + "name": "chatTitleButton", + "horizontal_gravity": "center", + "vertical_gravity": "center", + "x": 0.0, + "y": 424, + "width": 638, + "height": 112, + "text": "Айпери", + "textPaddingY": 28.0, + "textPaddingX": 216.0, + "fontSize": 40, + "fontPath": "resources/fonts/DroidSans.ttf", + "textCentered": false, + "topAligned": true, + "wrap": true, + "color": [1.0, 1.0, 1.0, 1.0], + "textures": { + "normal": "resources/w/ui/img/phone/CharHeader001.png", + "hover": "resources/w/ui/img/phone/CharHeader001.png", + "pressed": "resources/w/ui/img/phone/CharHeader001.png" + } + } + ] + } +} diff --git a/resources/w/ui/portrait_screen_phone_chat2.json b/resources/w/ui/portrait_screen_phone_chat2.json new file mode 100644 index 0000000..54258f4 --- /dev/null +++ b/resources/w/ui/portrait_screen_phone_chat2.json @@ -0,0 +1,83 @@ +{ + "root": { + "type": "FrameLayout", + "name": "hud_root", + "width": "match_parent", + "height": "match_parent", + "vertical_align": "center", + "horizontal_align": "center", + "children": [ + { + "type": "Button", + "name": "phoneExitButton", + "horizontal_gravity": "center", + "vertical_gravity": "center", + "y": 0, + "width": "match_parent", + "height": "match_parent", + "textures": { + "normal": "resources/transparent.png", + "hover": "resources/transparent.png", + "pressed": "resources/transparent.png" + } + }, + { + "type": "Button", + "name": "phoneMain", + "horizontal_gravity": "center", + "vertical_gravity": "center", + "y": -60, + "width": 882, + "height": 1416, + "textures": { + "normal": "resources/w/ui/img/phone/PhoneChat001.png", + "hover": "resources/w/ui/img/phone/PhoneChat001.png", + "pressed": "resources/w/ui/img/phone/PhoneChat001.png" + } + }, + { + "type": "FrameLayout", + "name": "chatMessagesContainer", + "horizontal_gravity": "center", + "vertical_gravity": "center", + "width": "match_parent", + "height": 1000 + }, + { + "type": "StaticImage", + "name": "chatTopCover", + "width": 638, + "height": 101.25, + "x": 0, + "y": 536, + "horizontal_gravity": "center", + "vertical_gravity": "center", + "texture": "resources/w/ui/img/phone/chat_top_cover001.png" + }, + { + "type": "TextButton", + "name": "chatTitleButton", + "horizontal_gravity": "center", + "vertical_gravity": "center", + "x": 0.0, + "y": 424, + "width": 638, + "height": 112, + "text": "Атам", + "textPaddingY": 28.0, + "textPaddingX": 216.0, + "fontSize": 40, + "fontPath": "resources/fonts/DroidSans.ttf", + "textCentered": false, + "topAligned": true, + "wrap": true, + "color": [1.0, 1.0, 1.0, 1.0], + "textures": { + "normal": "resources/w/ui/img/phone/CharHeader002.png", + "hover": "resources/w/ui/img/phone/CharHeader002.png", + "pressed": "resources/w/ui/img/phone/CharHeader002.png" + } + } + ] + } +} diff --git a/resources/w/ui/portrait_screen_phone_chat3.json b/resources/w/ui/portrait_screen_phone_chat3.json new file mode 100644 index 0000000..acac120 --- /dev/null +++ b/resources/w/ui/portrait_screen_phone_chat3.json @@ -0,0 +1,83 @@ +{ + "root": { + "type": "FrameLayout", + "name": "hud_root", + "width": "match_parent", + "height": "match_parent", + "vertical_align": "center", + "horizontal_align": "center", + "children": [ + { + "type": "Button", + "name": "phoneExitButton", + "horizontal_gravity": "center", + "vertical_gravity": "center", + "y": 0, + "width": "match_parent", + "height": "match_parent", + "textures": { + "normal": "resources/transparent.png", + "hover": "resources/transparent.png", + "pressed": "resources/transparent.png" + } + }, + { + "type": "Button", + "name": "phoneMain", + "horizontal_gravity": "center", + "vertical_gravity": "center", + "y": -60, + "width": 882, + "height": 1416, + "textures": { + "normal": "resources/w/ui/img/phone/PhoneChat001.png", + "hover": "resources/w/ui/img/phone/PhoneChat001.png", + "pressed": "resources/w/ui/img/phone/PhoneChat001.png" + } + }, + { + "type": "FrameLayout", + "name": "chatMessagesContainer", + "horizontal_gravity": "center", + "vertical_gravity": "center", + "width": "match_parent", + "height": 1000 + }, + { + "type": "StaticImage", + "name": "chatTopCover", + "width": 638, + "height": 101.25, + "x": 0, + "y": 536, + "horizontal_gravity": "center", + "vertical_gravity": "center", + "texture": "resources/w/ui/img/phone/chat_top_cover001.png" + }, + { + "type": "TextButton", + "name": "chatTitleButton", + "horizontal_gravity": "center", + "vertical_gravity": "center", + "x": 0.0, + "y": 424, + "width": 638, + "height": 112, + "text": "БишкекНовости", + "textPaddingY": 28.0, + "textPaddingX": 216.0, + "fontSize": 40, + "fontPath": "resources/fonts/DroidSans.ttf", + "textCentered": false, + "topAligned": true, + "wrap": true, + "color": [1.0, 1.0, 1.0, 1.0], + "textures": { + "normal": "resources/w/ui/img/phone/CharHeader003.png", + "hover": "resources/w/ui/img/phone/CharHeader003.png", + "pressed": "resources/w/ui/img/phone/CharHeader003.png" + } + } + ] + } +} diff --git a/src/Game.cpp b/src/Game.cpp index 9957f78..03b8b3e 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -672,6 +672,7 @@ namespace ZL if (!menuManager.cutsceneHudActive_) menuManager.uiManager.draw(renderer); + menuManager.drawChatFbo(renderer); if (currentLocation()) { currentLocation()->dialogueSystem.draw(renderer); diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index 0ba7ae4..dc6f719 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -7,6 +7,7 @@ #include #include + namespace ZL { static int questStatusPriority(Quest::QuestStatus status) { @@ -149,9 +150,12 @@ namespace ZL { phoneChatListHintRoot = loadUiFromFile("resources/w/ui/screen_phone_chat_list_hint001.json", renderer, zipFile); verticalPhoneChatListRoot = loadUiFromFile("resources/w/ui/portrait_screen_phone_chat_list.json", renderer, zipFile); verticalPhoneChatListHintRoot = loadUiFromFile("resources/w/ui/portrait_screen_phone_chat_list_hint001.json", renderer, zipFile); - phoneChat1Root = loadUiFromFile("resources/w/ui/screen_phone_chat1.json", renderer, zipFile); - phoneChat2Root = loadUiFromFile("resources/w/ui/screen_phone_chat2.json", renderer, zipFile); - phoneChat3Root = loadUiFromFile("resources/w/ui/screen_phone_chat3.json", renderer, zipFile); + phoneChat1Root = loadUiFromFile("resources/w/ui/screen_phone_chat1.json", renderer, zipFile); + phoneChat2Root = loadUiFromFile("resources/w/ui/screen_phone_chat2.json", renderer, zipFile); + phoneChat3Root = loadUiFromFile("resources/w/ui/screen_phone_chat3.json", renderer, zipFile); + verticalPhoneChat1Root = loadUiFromFile("resources/w/ui/portrait_screen_phone_chat1.json", renderer, zipFile); + verticalPhoneChat2Root = loadUiFromFile("resources/w/ui/portrait_screen_phone_chat2.json", renderer, zipFile); + verticalPhoneChat3Root = loadUiFromFile("resources/w/ui/portrait_screen_phone_chat3.json", renderer, zipFile); modalMenuRoot = loadUiFromFile("resources/w/ui/screen_modal_menu.json", renderer, zipFile); newInventoryRoot = loadUiFromFile("resources/w/ui/screen_inventory.json", renderer, zipFile); @@ -657,15 +661,15 @@ namespace ZL { uiManager.setButtonCallback("phoneMain", [this](const std::string&) {}); uiManager.setTextButtonCallback("chat1button", [this](const std::string&) { gameState_.chatUnread[0] = false; - openPhoneChatFromList(0, phoneChat1Root); + openPhoneChatFromList(0, isPortraitMode() ? verticalPhoneChat1Root : phoneChat1Root); }); uiManager.setTextButtonCallback("chat2button", [this](const std::string&) { gameState_.chatUnread[1] = false; - openPhoneChatFromList(1, phoneChat2Root); + openPhoneChatFromList(1, isPortraitMode() ? verticalPhoneChat2Root : phoneChat2Root); }); uiManager.setTextButtonCallback("chat3button", [this](const std::string&) { gameState_.chatUnread[2] = false; - openPhoneChatFromList(2, phoneChat3Root); + openPhoneChatFromList(2, isPortraitMode() ? verticalPhoneChat3Root : phoneChat3Root); }); } @@ -784,7 +788,33 @@ namespace ZL { gameState_.tutorialPhoneChatScreenOpened = true; uiManager.pushMenuFromSavedRoot(chatRoot); - rebuildChatBubblesFromHistory(chatIndex); + // Build a dedicated chatUiManager root sized to the FBO chat area + { + const bool portrait = isPortraitMode(); + const float fboW = portrait ? CHAT_FBO_WIDTH_P : CHAT_FBO_WIDTH_L; + const float fboH = portrait ? (CHAT_FBO_OFFSET_UP_P + CHAT_FBO_OFFSET_DOWN_P) + : (CHAT_FBO_OFFSET_UP_L + CHAT_FBO_OFFSET_DOWN_L); + + auto fboRoot = std::make_shared(); + fboRoot->name = "chatFboRoot"; + fboRoot->width = fboW; + fboRoot->height = fboH; + fboRoot->screenRect = { 0.0f, 0.0f, fboW, fboH }; + + auto fboContainer = std::make_shared(); + fboContainer->name = "chatMessagesContainer"; + fboContainer->width = fboW; + fboContainer->height = fboH; + fboContainer->screenRect = { 0.0f, 0.0f, fboW, fboH }; + fboRoot->children.push_back(fboContainer); + + chatUiManager.replaceRoot(fboRoot); + chatFrameBuffer_ = std::make_unique( + static_cast(fboW), static_cast(fboH), false); + chatFboQuadLastProjW_ = -1.0f; + } + + rebuildChatBubblesFromHistory(chatIndex, isPortraitMode()); uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) { closePhoneScreenFromChat(); @@ -808,6 +838,8 @@ namespace ZL { activeChatIndex_ = -1; currentPhoneSubScreen_ = PhoneSubScreen::ChatList; phoneChatVisibleBubbles_.clear(); + chatUiManager.replaceRoot(nullptr); + chatFrameBuffer_.reset(); uiManager.popMenu(); uiManager.updateAllLayouts(); if (gameState_.tutorialPhoneChatScreenOpened) @@ -832,6 +864,8 @@ namespace ZL { currentPhoneSubScreen_ = PhoneSubScreen::None; uiState_ = GameUiState::Gameplay; phoneChatVisibleBubbles_.clear(); + chatUiManager.replaceRoot(nullptr); + chatFrameBuffer_.reset(); const int depth = uiManager.menuStackSize(); for (int i = 0; i < depth; ++i) uiManager.popMenu(); uiManager.updateAllLayouts(); @@ -1261,42 +1295,69 @@ namespace ZL { if (i > 0) totalHeight += CHAT_SPACING; } - const float available = CHAT_TOP_Y - CHAT_BOTTOM_Y; - const float topY = (totalHeight <= available) - ? CHAT_TOP_Y - : CHAT_BOTTOM_Y + totalHeight; + const float fboH = CHAT_FBO_OFFSET_UP_L + CHAT_FBO_OFFSET_DOWN_L; + const float topY = (totalHeight <= fboH) ? fboH : totalHeight; float cursor = topY; for (auto& bubble : phoneChatVisibleBubbles_) { - auto node = uiManager.findNode(bubble.nodeName); + auto node = chatUiManager.findNode(bubble.nodeName); if (!node) continue; node->localY = cursor - bubble.height; cursor -= bubble.height + CHAT_SPACING; } - uiManager.updateAllLayouts(); + chatUiManager.updateAllLayouts(); } - void MenuManager::rebuildChatBubblesFromHistory(int chatIndex) { - uiManager.clearChatBubbles("chatMessagesContainer"); + void MenuManager::recomputePhoneChatPositionsPortrait() { + float totalHeight = 0.0f; + for (size_t i = 0; i < phoneChatVisibleBubbles_.size(); ++i) { + totalHeight += phoneChatVisibleBubbles_[i].height; + if (i > 0) totalHeight += CHAT_SPACING; + } + + const float fboH = CHAT_FBO_OFFSET_UP_P + CHAT_FBO_OFFSET_DOWN_P; + const float topY = (totalHeight <= fboH) ? fboH : totalHeight; + + float cursor = topY; + for (auto& bubble : phoneChatVisibleBubbles_) { + auto node = chatUiManager.findNode(bubble.nodeName); + if (!node) continue; + node->localY = cursor - bubble.height; + cursor -= bubble.height + CHAT_SPACING; + } + chatUiManager.updateAllLayouts(); + } + + void MenuManager::rebuildChatBubblesFromHistory(int chatIndex, bool isPortrait) { + chatUiManager.clearChatBubbles("chatMessagesContainer"); phoneChatVisibleBubbles_.clear(); if (chatIndex < 0 || chatIndex > 2) return; for (const auto& msg : gameState_.chatHistory[chatIndex]) { const bool inc = msg.incoming; - const std::string nodeName = uiManager.addChatBubble( + const std::string nodeName = chatUiManager.addChatBubble( "chatMessagesContainer", msg.text, inc, inc ? texBubbleInCenter_ : texBubbleOutCenter_, inc ? texBubbleInLT_ : texBubbleOutLT_, inc ? texBubbleInLB_ : texBubbleOutLB_, inc ? texBubbleInRT_ : texBubbleOutRT_, inc ? texBubbleInRB_ : texBubbleOutRB_, - renderer, "resources/fonts/DroidSans.ttf", 20, CONST_ZIP_FILE); + renderer, "resources/fonts/DroidSans.ttf", isPortrait ? 32 : 20, isPortrait, CONST_ZIP_FILE); if (!nodeName.empty()) { - auto n = uiManager.findNode(nodeName); + auto n = chatUiManager.findNode(nodeName); phoneChatVisibleBubbles_.push_back({ nodeName, n ? n->height : 60.0f }); } } - recomputePhoneChatPositions(); + + if (isPortrait) + { + recomputePhoneChatPositionsPortrait(); + } + else + { + recomputePhoneChatPositions(); + } + } void MenuManager::onChatBubbleReady(const std::string& text, bool incoming) { @@ -1310,25 +1371,34 @@ namespace ZL { if (uiState_ != GameUiState::PhoneScreen) return; - const std::string nodeName = uiManager.addChatBubble( + bool isPortrait = isPortraitMode(); + + const std::string nodeName = chatUiManager.addChatBubble( "chatMessagesContainer", text, incoming, incoming ? texBubbleInCenter_ : texBubbleOutCenter_, incoming ? texBubbleInLT_ : texBubbleOutLT_, incoming ? texBubbleInLB_ : texBubbleOutLB_, incoming ? texBubbleInRT_ : texBubbleOutRT_, incoming ? texBubbleInRB_ : texBubbleOutRB_, - renderer, "resources/fonts/DroidSans.ttf", 20, CONST_ZIP_FILE); + renderer, "resources/fonts/DroidSans.ttf", isPortrait ? 32 : 20, isPortrait, CONST_ZIP_FILE); if (nodeName.empty()) return; - auto node = uiManager.findNode(nodeName); + auto node = chatUiManager.findNode(nodeName); if (node) { node->scaleX = 0.0f; node->scaleY = 0.0f; phoneChatVisibleBubbles_.push_back({ nodeName, node->height }); } - recomputePhoneChatPositions(); - uiManager.startPopIn(nodeName, 300.0f); + if (isPortrait) + { + recomputePhoneChatPositionsPortrait(); + } + else + { + recomputePhoneChatPositions(); + } + chatUiManager.startPopIn(nodeName, 300.0f); } void MenuManager::setDarklandsMode(bool enabled) @@ -1537,6 +1607,57 @@ namespace ZL { void MenuManager::update(float deltaMs) { updateToasts(deltaMs); + chatUiManager.update(deltaMs); + } + + void MenuManager::drawChatFbo(Renderer& renderer) { + if (!chatFrameBuffer_) return; + + const bool portrait = isPortraitMode(); + const float fboW = portrait ? CHAT_FBO_WIDTH_P : CHAT_FBO_WIDTH_L; + const float offsetUp = portrait ? CHAT_FBO_OFFSET_UP_P : CHAT_FBO_OFFSET_UP_L; + const float offsetDown= portrait ? CHAT_FBO_OFFSET_DOWN_P : CHAT_FBO_OFFSET_DOWN_L; + const float fboH = offsetUp + offsetDown; + const float projW = Environment::projectionWidth; + const float projH = Environment::projectionHeight; + + // Pass 1: render chatUiManager into FBO + chatFrameBuffer_->Bind(); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + chatUiManager.draw(renderer, fboW, fboH); + chatFrameBuffer_->Unbind(); + + // Pass 2: blit FBO texture onto screen, centered with up/down offsets from screen center + if (chatFboQuadLastProjW_ != projW || chatFboQuadLastPortrait_ != portrait || + chatFboQuadLastProjH_ != projH) { + const float x0 = (projW - fboW) * 0.5f; + const float y0 = projH * 0.5f - offsetDown; + const float x1 = x0 + fboW; + const float y1 = y0 + fboH; + VertexDataStruct vd; + vd.PositionData = { {x0,y0,0},{x0,y1,0},{x1,y1,0}, + {x0,y0,0},{x1,y1,0},{x1,y0,0} }; + vd.TexCoordData = { {0,0},{0,1},{1,1}, + {0,0},{1,1},{1,0} }; + chatFboQuad_.AssignFrom(vd); + chatFboQuadLastProjW_ = projW; + chatFboQuadLastProjH_ = projH; + chatFboQuadLastPortrait_ = portrait; + } + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + renderer.PushProjectionMatrix(projW, projH, -1, 1); + renderer.PushMatrix(); + renderer.LoadIdentity(); + renderer.RenderUniform1f("uAlpha", 1.0f); + glBindTexture(GL_TEXTURE_2D, chatFrameBuffer_->getTextureID()); + renderer.DrawVertexRenderStruct(chatFboQuad_); + renderer.PopMatrix(); + renderer.PopProjectionMatrix(); } void MenuManager::saveSettings() { @@ -1609,10 +1730,19 @@ namespace ZL { currentPhoneSubScreen_ = PhoneSubScreen::MapUni; break; case PhoneSubScreen::ChatList: - case PhoneSubScreen::Chat: openPhoneScreen(); openPhoneMessenger(); break; + case PhoneSubScreen::Chat: { + openPhoneScreen(); + openPhoneMessenger(); + std::shared_ptr chatRoot; + if (savedChatIndex == 0) chatRoot = isPortraitMode() ? verticalPhoneChat1Root : phoneChat1Root; + else if (savedChatIndex == 1) chatRoot = isPortraitMode() ? verticalPhoneChat2Root : phoneChat2Root; + else if (savedChatIndex == 2) chatRoot = isPortraitMode() ? verticalPhoneChat3Root : phoneChat3Root; + if (chatRoot) openPhoneChatFromList(savedChatIndex, chatRoot); + break; + } default: openPhoneScreen(); break; diff --git a/src/MenuManager.h b/src/MenuManager.h index a5dba17..6a37016 100644 --- a/src/MenuManager.h +++ b/src/MenuManager.h @@ -10,6 +10,7 @@ #include #include #include "AudioPlayerAsync.h" +#include "render/FrameBuffer.h" // Forward-declared here to avoid pulling Game.h into MenuManager.h. namespace ZL { struct SaveSlotInfo; } @@ -31,6 +32,7 @@ namespace ZL { public: UiManager uiManager; UiManager topUiManager; + UiManager chatUiManager; MenuManager(Renderer& iRenderer, GameState& gameState, AudioPlayerAsync& audioPlayer); @@ -96,6 +98,9 @@ namespace ZL { // Called by Game when the window is resized and orientation has changed. void onOrientationChanged(); + // Called from Game::drawUI() to render chat bubbles through an FBO. + void drawChatFbo(Renderer& renderer); + void saveSettings(); void loadSettings(); @@ -125,11 +130,12 @@ namespace ZL { void refreshChatUnreadIndicators(); void resetPhoneChatNodes(); void recomputePhoneChatPositions(); + void recomputePhoneChatPositionsPortrait(); void openPhoneChatFromList(int chatIndex, std::shared_ptr chatRoot); void returnToPhoneChatList(); void closePhoneScreenFromChat(); void applyUniIntHud(); - void rebuildChatBubblesFromHistory(int chatIndex); + void rebuildChatBubblesFromHistory(int chatIndex, bool isPortrait); void showModalMenuScreen(); void showAboutScreen(); @@ -210,6 +216,9 @@ namespace ZL { std::shared_ptr phoneChat1Root; std::shared_ptr phoneChat2Root; std::shared_ptr phoneChat3Root; + std::shared_ptr verticalPhoneChat1Root; + std::shared_ptr verticalPhoneChat2Root; + std::shared_ptr verticalPhoneChat3Root; std::shared_ptr modalMenuRoot; @@ -253,6 +262,27 @@ namespace ZL { static constexpr float CHAT_TOP_Y = 610.0f; static constexpr float CHAT_BOTTOM_Y = 290.0f; static constexpr float CHAT_SPACING = 10.0f; + + static constexpr float P_CHAT_TOP_Y = 610.0f + 246; + static constexpr float P_CHAT_BOTTOM_Y = 290.0f + 126; + + // FBO dimensions for chat clipping. + // OFFSET_UP / OFFSET_DOWN = how far the FBO extends above / below screen center. + // Height = UP + DOWN. Quad: y0 = projH/2 - DOWN, y1 = projH/2 + UP. + static constexpr float CHAT_FBO_WIDTH_L = 469.0f; + static constexpr float CHAT_FBO_OFFSET_UP_L = 260.0f; + static constexpr float CHAT_FBO_OFFSET_DOWN_L = 70.0f; + + static constexpr float CHAT_FBO_WIDTH_P = 670.0f; + static constexpr float CHAT_FBO_OFFSET_UP_P = 360.0f; + static constexpr float CHAT_FBO_OFFSET_DOWN_P = 70.0f; + + std::unique_ptr chatFrameBuffer_; + VertexRenderStruct chatFboQuad_; + float chatFboQuadLastProjW_ = -1.0f; + float chatFboQuadLastProjH_ = -1.0f; + bool chatFboQuadLastPortrait_ = false; + }; } // namespace ZL diff --git a/src/UiManager.cpp b/src/UiManager.cpp index 699950e..5a1212e 100644 --- a/src/UiManager.cpp +++ b/src/UiManager.cpp @@ -854,6 +854,22 @@ namespace ZL { mesh.RefreshVBO(); } + float UiChatBubble::computeWidthPortrait() const { + if (!textRenderer) return P_MAX_WIDTH; + const float measuredTextW = textRenderer->measureTextWidth(text, 1.0f); + const float required = measuredTextW + P_CORNER_SIZE * 2.0f + P_PAD_X * 2.0f; + return min(required, P_MAX_WIDTH); + } + + float UiChatBubble::computeHeightPortrait() const { + if (!textRenderer) return P_CORNER_SIZE * 2 + P_PAD_Y * 2 + static_cast(fontSize); + const float textW = computeWidthPortrait() - P_CORNER_SIZE * 2.0f - P_PAD_X * 2.0f; + const std::string wrapped = wrapTextByPixels(text, *textRenderer, textW, 1.0f); + int lines = 1; + for (char c : wrapped) if (c == '\n') ++lines; + return P_CORNER_SIZE * 2.0f + P_PAD_Y * 2.0f + lines * static_cast(fontSize) * 1.25f; + } + float UiChatBubble::computeWidth() const { if (!textRenderer) return MAX_WIDTH; const float measuredTextW = textRenderer->measureTextWidth(text, 1.0f); @@ -892,7 +908,7 @@ namespace ZL { buildQuadMesh(meshCornerRB, x + w - cs, y, x + w, y + cs); } - void UiChatBubble::draw(Renderer& renderer, float nodeScaleX, float nodeScaleY) const { + void UiChatBubble::draw(Renderer& renderer, float nodeScaleX, float nodeScaleY, float projW, float projH) const { renderer.RenderUniform1i(textureUniformName, 0); @@ -936,7 +952,7 @@ namespace ZL { const float tx = cx + (tx0 - cx) * nodeScaleX; const float ty = cy + (ty0 - cy) * nodeScaleY; - textRenderer->drawText(wrapped, tx, ty, nodeScaleX, false, { 1.f, 1.f, 1.f, 1.f }); + textRenderer->drawText(wrapped, tx, ty, nodeScaleX, false, { 1.f, 1.f, 1.f, 1.f }, projW, projH); } } @@ -1456,8 +1472,10 @@ namespace ZL { return static_cast(menuStack.size()); } - void UiManager::draw(Renderer& renderer) { - renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, -1, 1); + void UiManager::draw(Renderer& renderer, float projW, float projH) { + if (projW <= 0.0f) projW = Environment::projectionWidth; + if (projH <= 0.0f) projH = Environment::projectionHeight; + renderer.PushProjectionMatrix(projW, projH, -1, 1); renderer.PushMatrix(); renderer.LoadIdentity(); renderer.RenderUniform1f("uAlpha", 1.0f); // ensure alpha is fully opaque unless a widget overrides it @@ -1481,7 +1499,7 @@ namespace ZL { // 1b. Chat bubble (composite widget) if (node->chatBubble) { - node->chatBubble->draw(renderer, node->scaleX, node->scaleY); + node->chatBubble->draw(renderer, node->scaleX, node->scaleY, projW, projH); } // 2. Потом кнопки @@ -2136,6 +2154,7 @@ namespace ZL { std::shared_ptr texLT, std::shared_ptr texLB, std::shared_ptr texRT, std::shared_ptr texRB, Renderer& renderer, const std::string& fontPath, int fontSize, + bool isPortrait, const std::string& zipFile) { if (!root) return ""; @@ -2168,39 +2187,37 @@ namespace ZL { // Build the UiNode auto node = std::make_shared(); node->name = nodeName; - node->width = bubble->computeWidth(); - node->height = bubble->computeHeight(); + node->width = isPortrait? bubble->computeWidthPortrait() : bubble->computeWidth(); + node->height = isPortrait? bubble->computeHeightPortrait() : bubble->computeHeight(); - if (incoming) { - node->localX = -node->width * 0.5+110 - (UiChatBubble::MAX_WIDTH - node->width);// +(425.f - node->width); + + if (isPortrait) + { + if (incoming) { + node->localX = -300 + node->width * 0.5; + } + else + { + node->localX = 300 - node->width * 0.5; + } } else { - /*if (node->width == UiChatBubble::MAX_WIDTH) - { - node->localX = node->width * 0.5 - 110 - (UiChatBubble::MAX_WIDTH - node->width);// -(425.f - node->width); + if (incoming) { + node->localX = -210 + node->width * 0.5; } else - {*/ - //node->localX = node->width * 0.5 - 110; - //node->localX = - 110 +x; - node->localX = 210 - node->width * 0.5; - //} + { + node->localX = 210 - node->width * 0.5; + } } - //430 - //1280 node->localY = 0.0f; node->visible = true; node->chatBubble = bubble; node->layoutSettings.hGravity = HorizontalGravity::Center; - /* - if (incoming) { - node->layoutSettings.hGravity = HorizontalGravity::Left; - } else { - node->layoutSettings.hGravity = HorizontalGravity::Right; - }*/ + // Bottom gravity means localY is used as-is for screenRect.y (no parentH transform). // The original static chat nodes all had "vertical_gravity": "bottom" for the same reason. node->layoutSettings.vGravity = VerticalGravity::Bottom; diff --git a/src/UiManager.h b/src/UiManager.h index a11486f..384aba1 100644 --- a/src/UiManager.h +++ b/src/UiManager.h @@ -262,11 +262,14 @@ namespace ZL { static constexpr float CORNER_SIZE = 22.4f; static constexpr float MAX_WIDTH = 320.6f; - //static constexpr float PAD_X = 7.0f; - //static constexpr float PAD_Y = 5.0f; static constexpr float PAD_X = -10.0f; static constexpr float PAD_Y = -10.0f; + static constexpr float P_CORNER_SIZE = 32; + static constexpr float P_MAX_WIDTH = 456; + static constexpr float P_PAD_X = -14.0f; + static constexpr float P_PAD_Y = -14.0f; + // 7 meshes: middle-center, top-strip, bottom-strip, LT, RT, LB, RB VertexRenderStruct meshMiddle; VertexRenderStruct meshTopStrip; @@ -282,10 +285,12 @@ namespace ZL { // Must be called after textRenderer is initialised float computeWidth() const; // capped at MAX_WIDTH float computeHeight() const; // depends on computeWidth() + float computeWidthPortrait() const; + float computeHeightPortrait() const; void buildMeshes(); // nodeScaleX/Y: the owning UiNode's current scale (for pop-in / animations). // Text position and glyph size are transformed to match the renderer's scale matrix. - void draw(Renderer& renderer, float nodeScaleX = 1.0f, float nodeScaleY = 1.0f) const; + void draw(Renderer& renderer, float nodeScaleX = 1.0f, float nodeScaleY = 1.0f, float projW = 0.0f, float projH = 0.0f) const; }; struct UiNode { @@ -366,7 +371,7 @@ namespace ZL { void loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = ""); void appendFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = ""); - void draw(Renderer& renderer); + void draw(Renderer& renderer, float projW = 0.0f, float projH = 0.0f); // Multi-touch methods (used directly for touch events with per-finger IDs) void onTouchDown(int64_t fingerId, int x, int y); @@ -461,6 +466,7 @@ namespace ZL { std::shared_ptr texLT, std::shared_ptr texLB, std::shared_ptr texRT, std::shared_ptr texRB, Renderer& renderer, const std::string& fontPath, int fontSize, + bool isPortrait, const std::string& zipFile); // Remove all chat-bubble children from named parent and clear chatBubbles collection. diff --git a/src/render/TextRenderer.cpp b/src/render/TextRenderer.cpp index 7dd9547..cf2224e 100644 --- a/src/render/TextRenderer.cpp +++ b/src/render/TextRenderer.cpp @@ -380,9 +380,11 @@ float TextRenderer::measureTextWidth(const std::string& text, float scale) const return maxLineWidth; } -void TextRenderer::drawText(const std::string& text, float x, float y, float scale, bool centered, std::array color) +void TextRenderer::drawText(const std::string& text, float x, float y, float scale, bool centered, std::array color, float projW, float projH) { if (!r || text.empty() || !atlasTexture) return; + if (projW <= 0.0f) projW = Environment::projectionWidth; + if (projH <= 0.0f) projH = Environment::projectionHeight; // формируем ключ кеша std::string key = text + "|" + std::to_string(scale) + "|" + (centered ? "1" : "0"); @@ -471,16 +473,14 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca // 4. Рендеринг r->shaderManager.PushShader(shaderName); - // Матрица проекции — используем виртуальные проекционные размеры, - // чтобы координаты текста были независимы от физического разрешения экрана. - float W = Environment::projectionWidth; - float H = Environment::projectionHeight; + // Матрица проекции — используем переданные проекционные размеры. + // Это позволяет корректно рендерить текст как в основной сцене, так и внутри FBO. Eigen::Matrix4f proj = Eigen::Matrix4f::Identity(); - proj(0, 0) = 2.0f / W; - proj(1, 1) = 2.0f / H; + proj(0, 0) = 2.0f / projW; + proj(1, 1) = 2.0f / projH; // Сдвигаем проекцию так, чтобы локальные координаты меша (pen-origin=0,0) оказались в (tx,ty) - proj(0, 3) = -1.0f + 2.0f * (tx) / W; - proj(1, 3) = -1.0f + 2.0f * (ty) / H; + proj(0, 3) = -1.0f + 2.0f * (tx) / projW; + proj(1, 3) = -1.0f + 2.0f * (ty) / projH; //cached.mesh.RefreshVBO(); diff --git a/src/render/TextRenderer.h b/src/render/TextRenderer.h index 7e9422c..29d7d68 100644 --- a/src/render/TextRenderer.h +++ b/src/render/TextRenderer.h @@ -26,7 +26,7 @@ public: ~TextRenderer(); bool init(Renderer& renderer, const std::string& ttfPath, int pixelSize, const std::string& zipfilename); - void drawText(const std::string& text, float x, float y, float scale, bool centered, std::array color = { 1.f,1.f,1.f,1.f }); + void drawText(const std::string& text, float x, float y, float scale, bool centered, std::array color = { 1.f,1.f,1.f,1.f }, float projW = 0.0f, float projH = 0.0f); float measureTextWidth(const std::string& text, float scale = 1.0f) const; float getLineHeight(float scale = 1.0f) const { return lineHeight * scale; }