Working on messenger in portrait mode

This commit is contained in:
Vladislav Khorev 2026-06-26 20:04:08 +03:00
parent 0b4df8ab2d
commit 8c42179576
10 changed files with 498 additions and 65 deletions

View File

@ -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"
}
}
]
}
}

View File

@ -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"
}
}
]
}
}

View File

@ -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"
}
}
]
}
}

View File

@ -672,6 +672,7 @@ namespace ZL
if (!menuManager.cutsceneHudActive_) if (!menuManager.cutsceneHudActive_)
menuManager.uiManager.draw(renderer); menuManager.uiManager.draw(renderer);
menuManager.drawChatFbo(renderer);
if (currentLocation()) if (currentLocation())
{ {
currentLocation()->dialogueSystem.draw(renderer); currentLocation()->dialogueSystem.draw(renderer);

View File

@ -7,6 +7,7 @@
#include <algorithm> #include <algorithm>
#include <string> #include <string>
namespace ZL { namespace ZL {
static int questStatusPriority(Quest::QuestStatus status) { static int questStatusPriority(Quest::QuestStatus status) {
@ -152,6 +153,9 @@ namespace ZL {
phoneChat1Root = loadUiFromFile("resources/w/ui/screen_phone_chat1.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); phoneChat2Root = loadUiFromFile("resources/w/ui/screen_phone_chat2.json", renderer, zipFile);
phoneChat3Root = loadUiFromFile("resources/w/ui/screen_phone_chat3.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); modalMenuRoot = loadUiFromFile("resources/w/ui/screen_modal_menu.json", renderer, zipFile);
newInventoryRoot = loadUiFromFile("resources/w/ui/screen_inventory.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.setButtonCallback("phoneMain", [this](const std::string&) {});
uiManager.setTextButtonCallback("chat1button", [this](const std::string&) { uiManager.setTextButtonCallback("chat1button", [this](const std::string&) {
gameState_.chatUnread[0] = false; gameState_.chatUnread[0] = false;
openPhoneChatFromList(0, phoneChat1Root); openPhoneChatFromList(0, isPortraitMode() ? verticalPhoneChat1Root : phoneChat1Root);
}); });
uiManager.setTextButtonCallback("chat2button", [this](const std::string&) { uiManager.setTextButtonCallback("chat2button", [this](const std::string&) {
gameState_.chatUnread[1] = false; gameState_.chatUnread[1] = false;
openPhoneChatFromList(1, phoneChat2Root); openPhoneChatFromList(1, isPortraitMode() ? verticalPhoneChat2Root : phoneChat2Root);
}); });
uiManager.setTextButtonCallback("chat3button", [this](const std::string&) { uiManager.setTextButtonCallback("chat3button", [this](const std::string&) {
gameState_.chatUnread[2] = false; gameState_.chatUnread[2] = false;
openPhoneChatFromList(2, phoneChat3Root); openPhoneChatFromList(2, isPortraitMode() ? verticalPhoneChat3Root : phoneChat3Root);
}); });
} }
@ -784,7 +788,33 @@ namespace ZL {
gameState_.tutorialPhoneChatScreenOpened = true; gameState_.tutorialPhoneChatScreenOpened = true;
uiManager.pushMenuFromSavedRoot(chatRoot); 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<UiNode>();
fboRoot->name = "chatFboRoot";
fboRoot->width = fboW;
fboRoot->height = fboH;
fboRoot->screenRect = { 0.0f, 0.0f, fboW, fboH };
auto fboContainer = std::make_shared<UiNode>();
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<FrameBuffer>(
static_cast<int>(fboW), static_cast<int>(fboH), false);
chatFboQuadLastProjW_ = -1.0f;
}
rebuildChatBubblesFromHistory(chatIndex, isPortraitMode());
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) { uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
closePhoneScreenFromChat(); closePhoneScreenFromChat();
@ -808,6 +838,8 @@ namespace ZL {
activeChatIndex_ = -1; activeChatIndex_ = -1;
currentPhoneSubScreen_ = PhoneSubScreen::ChatList; currentPhoneSubScreen_ = PhoneSubScreen::ChatList;
phoneChatVisibleBubbles_.clear(); phoneChatVisibleBubbles_.clear();
chatUiManager.replaceRoot(nullptr);
chatFrameBuffer_.reset();
uiManager.popMenu(); uiManager.popMenu();
uiManager.updateAllLayouts(); uiManager.updateAllLayouts();
if (gameState_.tutorialPhoneChatScreenOpened) if (gameState_.tutorialPhoneChatScreenOpened)
@ -832,6 +864,8 @@ namespace ZL {
currentPhoneSubScreen_ = PhoneSubScreen::None; currentPhoneSubScreen_ = PhoneSubScreen::None;
uiState_ = GameUiState::Gameplay; uiState_ = GameUiState::Gameplay;
phoneChatVisibleBubbles_.clear(); phoneChatVisibleBubbles_.clear();
chatUiManager.replaceRoot(nullptr);
chatFrameBuffer_.reset();
const int depth = uiManager.menuStackSize(); const int depth = uiManager.menuStackSize();
for (int i = 0; i < depth; ++i) uiManager.popMenu(); for (int i = 0; i < depth; ++i) uiManager.popMenu();
uiManager.updateAllLayouts(); uiManager.updateAllLayouts();
@ -1261,44 +1295,71 @@ namespace ZL {
if (i > 0) totalHeight += CHAT_SPACING; if (i > 0) totalHeight += CHAT_SPACING;
} }
const float available = CHAT_TOP_Y - CHAT_BOTTOM_Y; const float fboH = CHAT_FBO_OFFSET_UP_L + CHAT_FBO_OFFSET_DOWN_L;
const float topY = (totalHeight <= available) const float topY = (totalHeight <= fboH) ? fboH : totalHeight;
? CHAT_TOP_Y
: CHAT_BOTTOM_Y + totalHeight;
float cursor = topY; float cursor = topY;
for (auto& bubble : phoneChatVisibleBubbles_) { for (auto& bubble : phoneChatVisibleBubbles_) {
auto node = uiManager.findNode(bubble.nodeName); auto node = chatUiManager.findNode(bubble.nodeName);
if (!node) continue; if (!node) continue;
node->localY = cursor - bubble.height; node->localY = cursor - bubble.height;
cursor -= bubble.height + CHAT_SPACING; cursor -= bubble.height + CHAT_SPACING;
} }
uiManager.updateAllLayouts(); chatUiManager.updateAllLayouts();
} }
void MenuManager::rebuildChatBubblesFromHistory(int chatIndex) { void MenuManager::recomputePhoneChatPositionsPortrait() {
uiManager.clearChatBubbles("chatMessagesContainer"); 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(); phoneChatVisibleBubbles_.clear();
if (chatIndex < 0 || chatIndex > 2) return; if (chatIndex < 0 || chatIndex > 2) return;
for (const auto& msg : gameState_.chatHistory[chatIndex]) { for (const auto& msg : gameState_.chatHistory[chatIndex]) {
const bool inc = msg.incoming; const bool inc = msg.incoming;
const std::string nodeName = uiManager.addChatBubble( const std::string nodeName = chatUiManager.addChatBubble(
"chatMessagesContainer", msg.text, inc, "chatMessagesContainer", msg.text, inc,
inc ? texBubbleInCenter_ : texBubbleOutCenter_, inc ? texBubbleInCenter_ : texBubbleOutCenter_,
inc ? texBubbleInLT_ : texBubbleOutLT_, inc ? texBubbleInLT_ : texBubbleOutLT_,
inc ? texBubbleInLB_ : texBubbleOutLB_, inc ? texBubbleInLB_ : texBubbleOutLB_,
inc ? texBubbleInRT_ : texBubbleOutRT_, inc ? texBubbleInRT_ : texBubbleOutRT_,
inc ? texBubbleInRB_ : texBubbleOutRB_, 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()) { if (!nodeName.empty()) {
auto n = uiManager.findNode(nodeName); auto n = chatUiManager.findNode(nodeName);
phoneChatVisibleBubbles_.push_back({ nodeName, n ? n->height : 60.0f }); phoneChatVisibleBubbles_.push_back({ nodeName, n ? n->height : 60.0f });
} }
} }
if (isPortrait)
{
recomputePhoneChatPositionsPortrait();
}
else
{
recomputePhoneChatPositions(); recomputePhoneChatPositions();
} }
}
void MenuManager::onChatBubbleReady(const std::string& text, bool incoming) { void MenuManager::onChatBubbleReady(const std::string& text, bool incoming) {
if (activeChatIndex_ < 0) return; if (activeChatIndex_ < 0) return;
@ -1310,25 +1371,34 @@ namespace ZL {
if (uiState_ != GameUiState::PhoneScreen) return; if (uiState_ != GameUiState::PhoneScreen) return;
const std::string nodeName = uiManager.addChatBubble( bool isPortrait = isPortraitMode();
const std::string nodeName = chatUiManager.addChatBubble(
"chatMessagesContainer", text, incoming, "chatMessagesContainer", text, incoming,
incoming ? texBubbleInCenter_ : texBubbleOutCenter_, incoming ? texBubbleInCenter_ : texBubbleOutCenter_,
incoming ? texBubbleInLT_ : texBubbleOutLT_, incoming ? texBubbleInLT_ : texBubbleOutLT_,
incoming ? texBubbleInLB_ : texBubbleOutLB_, incoming ? texBubbleInLB_ : texBubbleOutLB_,
incoming ? texBubbleInRT_ : texBubbleOutRT_, incoming ? texBubbleInRT_ : texBubbleOutRT_,
incoming ? texBubbleInRB_ : texBubbleOutRB_, 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; if (nodeName.empty()) return;
auto node = uiManager.findNode(nodeName); auto node = chatUiManager.findNode(nodeName);
if (node) { if (node) {
node->scaleX = 0.0f; node->scaleX = 0.0f;
node->scaleY = 0.0f; node->scaleY = 0.0f;
phoneChatVisibleBubbles_.push_back({ nodeName, node->height }); phoneChatVisibleBubbles_.push_back({ nodeName, node->height });
} }
if (isPortrait)
{
recomputePhoneChatPositionsPortrait();
}
else
{
recomputePhoneChatPositions(); recomputePhoneChatPositions();
uiManager.startPopIn(nodeName, 300.0f); }
chatUiManager.startPopIn(nodeName, 300.0f);
} }
void MenuManager::setDarklandsMode(bool enabled) void MenuManager::setDarklandsMode(bool enabled)
@ -1537,6 +1607,57 @@ namespace ZL {
void MenuManager::update(float deltaMs) { void MenuManager::update(float deltaMs) {
updateToasts(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() { void MenuManager::saveSettings() {
@ -1609,10 +1730,19 @@ namespace ZL {
currentPhoneSubScreen_ = PhoneSubScreen::MapUni; currentPhoneSubScreen_ = PhoneSubScreen::MapUni;
break; break;
case PhoneSubScreen::ChatList: case PhoneSubScreen::ChatList:
case PhoneSubScreen::Chat:
openPhoneScreen(); openPhoneScreen();
openPhoneMessenger(); openPhoneMessenger();
break; break;
case PhoneSubScreen::Chat: {
openPhoneScreen();
openPhoneMessenger();
std::shared_ptr<UiNode> 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: default:
openPhoneScreen(); openPhoneScreen();
break; break;

View File

@ -10,6 +10,7 @@
#include <string> #include <string>
#include <memory> #include <memory>
#include "AudioPlayerAsync.h" #include "AudioPlayerAsync.h"
#include "render/FrameBuffer.h"
// Forward-declared here to avoid pulling Game.h into MenuManager.h. // Forward-declared here to avoid pulling Game.h into MenuManager.h.
namespace ZL { struct SaveSlotInfo; } namespace ZL { struct SaveSlotInfo; }
@ -31,6 +32,7 @@ namespace ZL {
public: public:
UiManager uiManager; UiManager uiManager;
UiManager topUiManager; UiManager topUiManager;
UiManager chatUiManager;
MenuManager(Renderer& iRenderer, GameState& gameState, AudioPlayerAsync& audioPlayer); 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. // Called by Game when the window is resized and orientation has changed.
void onOrientationChanged(); void onOrientationChanged();
// Called from Game::drawUI() to render chat bubbles through an FBO.
void drawChatFbo(Renderer& renderer);
void saveSettings(); void saveSettings();
void loadSettings(); void loadSettings();
@ -125,11 +130,12 @@ namespace ZL {
void refreshChatUnreadIndicators(); void refreshChatUnreadIndicators();
void resetPhoneChatNodes(); void resetPhoneChatNodes();
void recomputePhoneChatPositions(); void recomputePhoneChatPositions();
void recomputePhoneChatPositionsPortrait();
void openPhoneChatFromList(int chatIndex, std::shared_ptr<UiNode> chatRoot); void openPhoneChatFromList(int chatIndex, std::shared_ptr<UiNode> chatRoot);
void returnToPhoneChatList(); void returnToPhoneChatList();
void closePhoneScreenFromChat(); void closePhoneScreenFromChat();
void applyUniIntHud(); void applyUniIntHud();
void rebuildChatBubblesFromHistory(int chatIndex); void rebuildChatBubblesFromHistory(int chatIndex, bool isPortrait);
void showModalMenuScreen(); void showModalMenuScreen();
void showAboutScreen(); void showAboutScreen();
@ -210,6 +216,9 @@ namespace ZL {
std::shared_ptr<UiNode> phoneChat1Root; std::shared_ptr<UiNode> phoneChat1Root;
std::shared_ptr<UiNode> phoneChat2Root; std::shared_ptr<UiNode> phoneChat2Root;
std::shared_ptr<UiNode> phoneChat3Root; std::shared_ptr<UiNode> phoneChat3Root;
std::shared_ptr<UiNode> verticalPhoneChat1Root;
std::shared_ptr<UiNode> verticalPhoneChat2Root;
std::shared_ptr<UiNode> verticalPhoneChat3Root;
std::shared_ptr<UiNode> modalMenuRoot; std::shared_ptr<UiNode> modalMenuRoot;
@ -253,6 +262,27 @@ namespace ZL {
static constexpr float CHAT_TOP_Y = 610.0f; static constexpr float CHAT_TOP_Y = 610.0f;
static constexpr float CHAT_BOTTOM_Y = 290.0f; static constexpr float CHAT_BOTTOM_Y = 290.0f;
static constexpr float CHAT_SPACING = 10.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<FrameBuffer> chatFrameBuffer_;
VertexRenderStruct chatFboQuad_;
float chatFboQuadLastProjW_ = -1.0f;
float chatFboQuadLastProjH_ = -1.0f;
bool chatFboQuadLastPortrait_ = false;
}; };
} // namespace ZL } // namespace ZL

View File

@ -854,6 +854,22 @@ namespace ZL {
mesh.RefreshVBO(); 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<float>(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<float>(fontSize) * 1.25f;
}
float UiChatBubble::computeWidth() const { float UiChatBubble::computeWidth() const {
if (!textRenderer) return MAX_WIDTH; if (!textRenderer) return MAX_WIDTH;
const float measuredTextW = textRenderer->measureTextWidth(text, 1.0f); const float measuredTextW = textRenderer->measureTextWidth(text, 1.0f);
@ -892,7 +908,7 @@ namespace ZL {
buildQuadMesh(meshCornerRB, x + w - cs, y, x + w, y + cs); 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); renderer.RenderUniform1i(textureUniformName, 0);
@ -936,7 +952,7 @@ namespace ZL {
const float tx = cx + (tx0 - cx) * nodeScaleX; const float tx = cx + (tx0 - cx) * nodeScaleX;
const float ty = cy + (ty0 - cy) * nodeScaleY; 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<int>(menuStack.size()); return static_cast<int>(menuStack.size());
} }
void UiManager::draw(Renderer& renderer) { void UiManager::draw(Renderer& renderer, float projW, float projH) {
renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, -1, 1); if (projW <= 0.0f) projW = Environment::projectionWidth;
if (projH <= 0.0f) projH = Environment::projectionHeight;
renderer.PushProjectionMatrix(projW, projH, -1, 1);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.LoadIdentity(); renderer.LoadIdentity();
renderer.RenderUniform1f("uAlpha", 1.0f); // ensure alpha is fully opaque unless a widget overrides it 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) // 1b. Chat bubble (composite widget)
if (node->chatBubble) { if (node->chatBubble) {
node->chatBubble->draw(renderer, node->scaleX, node->scaleY); node->chatBubble->draw(renderer, node->scaleX, node->scaleY, projW, projH);
} }
// 2. Потом кнопки // 2. Потом кнопки
@ -2136,6 +2154,7 @@ namespace ZL {
std::shared_ptr<Texture> texLT, std::shared_ptr<Texture> texLB, std::shared_ptr<Texture> texLT, std::shared_ptr<Texture> texLB,
std::shared_ptr<Texture> texRT, std::shared_ptr<Texture> texRB, std::shared_ptr<Texture> texRT, std::shared_ptr<Texture> texRB,
Renderer& renderer, const std::string& fontPath, int fontSize, Renderer& renderer, const std::string& fontPath, int fontSize,
bool isPortrait,
const std::string& zipFile) const std::string& zipFile)
{ {
if (!root) return ""; if (!root) return "";
@ -2168,39 +2187,37 @@ namespace ZL {
// Build the UiNode // Build the UiNode
auto node = std::make_shared<UiNode>(); auto node = std::make_shared<UiNode>();
node->name = nodeName; node->name = nodeName;
node->width = bubble->computeWidth(); node->width = isPortrait? bubble->computeWidthPortrait() : bubble->computeWidth();
node->height = bubble->computeHeight(); node->height = isPortrait? bubble->computeHeightPortrait() : bubble->computeHeight();
if (isPortrait)
{
if (incoming) { if (incoming) {
node->localX = -node->width * 0.5+110 - (UiChatBubble::MAX_WIDTH - node->width);// +(425.f - node->width); node->localX = -300 + node->width * 0.5;
} }
else else
{ {
/*if (node->width == UiChatBubble::MAX_WIDTH) node->localX = 300 - node->width * 0.5;
{ }
node->localX = node->width * 0.5 - 110 - (UiChatBubble::MAX_WIDTH - node->width);// -(425.f - node->width);
} }
else else
{*/ {
//node->localX = node->width * 0.5 - 110; if (incoming) {
//node->localX = - 110 +x; node->localX = -210 + node->width * 0.5;
}
else
{
node->localX = 210 - node->width * 0.5; node->localX = 210 - node->width * 0.5;
//}
} }
//430 }
//1280
node->localY = 0.0f; node->localY = 0.0f;
node->visible = true; node->visible = true;
node->chatBubble = bubble; node->chatBubble = bubble;
node->layoutSettings.hGravity = HorizontalGravity::Center; 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). // 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. // The original static chat nodes all had "vertical_gravity": "bottom" for the same reason.
node->layoutSettings.vGravity = VerticalGravity::Bottom; node->layoutSettings.vGravity = VerticalGravity::Bottom;

View File

@ -262,11 +262,14 @@ namespace ZL {
static constexpr float CORNER_SIZE = 22.4f; static constexpr float CORNER_SIZE = 22.4f;
static constexpr float MAX_WIDTH = 320.6f; 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_X = -10.0f;
static constexpr float PAD_Y = -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 // 7 meshes: middle-center, top-strip, bottom-strip, LT, RT, LB, RB
VertexRenderStruct meshMiddle; VertexRenderStruct meshMiddle;
VertexRenderStruct meshTopStrip; VertexRenderStruct meshTopStrip;
@ -282,10 +285,12 @@ namespace ZL {
// Must be called after textRenderer is initialised // Must be called after textRenderer is initialised
float computeWidth() const; // capped at MAX_WIDTH float computeWidth() const; // capped at MAX_WIDTH
float computeHeight() const; // depends on computeWidth() float computeHeight() const; // depends on computeWidth()
float computeWidthPortrait() const;
float computeHeightPortrait() const;
void buildMeshes(); void buildMeshes();
// nodeScaleX/Y: the owning UiNode's current scale (for pop-in / animations). // 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. // 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 { struct UiNode {
@ -366,7 +371,7 @@ namespace ZL {
void loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = ""); 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 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) // Multi-touch methods (used directly for touch events with per-finger IDs)
void onTouchDown(int64_t fingerId, int x, int y); void onTouchDown(int64_t fingerId, int x, int y);
@ -461,6 +466,7 @@ namespace ZL {
std::shared_ptr<Texture> texLT, std::shared_ptr<Texture> texLB, std::shared_ptr<Texture> texLT, std::shared_ptr<Texture> texLB,
std::shared_ptr<Texture> texRT, std::shared_ptr<Texture> texRB, std::shared_ptr<Texture> texRT, std::shared_ptr<Texture> texRB,
Renderer& renderer, const std::string& fontPath, int fontSize, Renderer& renderer, const std::string& fontPath, int fontSize,
bool isPortrait,
const std::string& zipFile); const std::string& zipFile);
// Remove all chat-bubble children from named parent and clear chatBubbles collection. // Remove all chat-bubble children from named parent and clear chatBubbles collection.

View File

@ -380,9 +380,11 @@ float TextRenderer::measureTextWidth(const std::string& text, float scale) const
return maxLineWidth; return maxLineWidth;
} }
void TextRenderer::drawText(const std::string& text, float x, float y, float scale, bool centered, std::array<float, 4> color) void TextRenderer::drawText(const std::string& text, float x, float y, float scale, bool centered, std::array<float, 4> color, float projW, float projH)
{ {
if (!r || text.empty() || !atlasTexture) return; 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"); 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. Рендеринг // 4. Рендеринг
r->shaderManager.PushShader(shaderName); r->shaderManager.PushShader(shaderName);
// Матрица проекции — используем виртуальные проекционные размеры, // Матрица проекции — используем переданные проекционные размеры.
// чтобы координаты текста были независимы от физического разрешения экрана. // Это позволяет корректно рендерить текст как в основной сцене, так и внутри FBO.
float W = Environment::projectionWidth;
float H = Environment::projectionHeight;
Eigen::Matrix4f proj = Eigen::Matrix4f::Identity(); Eigen::Matrix4f proj = Eigen::Matrix4f::Identity();
proj(0, 0) = 2.0f / W; proj(0, 0) = 2.0f / projW;
proj(1, 1) = 2.0f / H; proj(1, 1) = 2.0f / projH;
// Сдвигаем проекцию так, чтобы локальные координаты меша (pen-origin=0,0) оказались в (tx,ty) // Сдвигаем проекцию так, чтобы локальные координаты меша (pen-origin=0,0) оказались в (tx,ty)
proj(0, 3) = -1.0f + 2.0f * (tx) / W; proj(0, 3) = -1.0f + 2.0f * (tx) / projW;
proj(1, 3) = -1.0f + 2.0f * (ty) / H; proj(1, 3) = -1.0f + 2.0f * (ty) / projH;
//cached.mesh.RefreshVBO(); //cached.mesh.RefreshVBO();

View File

@ -26,7 +26,7 @@ public:
~TextRenderer(); ~TextRenderer();
bool init(Renderer& renderer, const std::string& ttfPath, int pixelSize, const std::string& zipfilename); 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<float, 4> 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<float, 4> 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 measureTextWidth(const std::string& text, float scale = 1.0f) const;
float getLineHeight(float scale = 1.0f) const { return lineHeight * scale; } float getLineHeight(float scale = 1.0f) const { return lineHeight * scale; }