Working on messenger

This commit is contained in:
Vladislav Khorev 2026-06-05 12:13:02 +03:00
parent 9221ab68ac
commit 1fc8120ee0
25 changed files with 445 additions and 259 deletions

View File

@ -47,7 +47,7 @@
"portrait": "resources/dialogue/portrait_phone.png", "portrait": "resources/dialogue/portrait_phone.png",
"text": "Бекзат, сынок, мы c мамой тебе отправили немного денег, постарайся прожить на эти деньги до конца недели!", "text": "Бекзат, сынок, мы c мамой тебе отправили немного денег, постарайся прожить на эти деньги до конца недели!",
"next": "line_2", "next": "line_2",
"bubbleSlot": "message01in" "chatBubble": "in"
}, },
{ {
"id": "line_2", "id": "line_2",
@ -56,7 +56,7 @@
"portrait": "resources/dialogue/portrait_phone.png", "portrait": "resources/dialogue/portrait_phone.png",
"text": "Спасибо!", "text": "Спасибо!",
"next": "end_1", "next": "end_1",
"bubbleSlot": "message02out" "chatBubble": "out"
}, },
{ {
"id": "end_1", "id": "end_1",
@ -75,7 +75,7 @@
"portrait": "resources/dialogue/portrait_phone.png", "portrait": "resources/dialogue/portrait_phone.png",
"text": "Жители Бишкека все чаще жалуются на депрессию и апатию. Смотрите свежее видео об этом на нашем канале!", "text": "Жители Бишкека все чаще жалуются на депрессию и апатию. Смотрите свежее видео об этом на нашем канале!",
"next": "end_1", "next": "end_1",
"bubbleSlot": "message01in" "chatBubble": "in"
}, },
{ {
"id": "end_1", "id": "end_1",
@ -94,7 +94,7 @@
"portrait": "resources/dialogue/portrait_phone.png", "portrait": "resources/dialogue/portrait_phone.png",
"text": "Бекзат, помнишь мы скидывались на торт для Аиды Джаныбековой? Я тогда еще приносила скатерть, тарелки и нож для торта. И я до сих пор не получила назад ничего.", "text": "Бекзат, помнишь мы скидывались на торт для Аиды Джаныбековой? Я тогда еще приносила скатерть, тарелки и нож для торта. И я до сих пор не получила назад ничего.",
"next": "line_2", "next": "line_2",
"bubbleSlot": "message01in" "chatBubble": "in"
}, },
{ {
"id": "line_2", "id": "line_2",
@ -103,7 +103,7 @@
"portrait": "resources/dialogue/portrait_phone.png", "portrait": "resources/dialogue/portrait_phone.png",
"text": "Скатерть и тарелки вроде бы лежат в студзоне.", "text": "Скатерть и тарелки вроде бы лежат в студзоне.",
"next": "line_3", "next": "line_3",
"bubbleSlot": "message02out" "chatBubble": "out"
}, },
{ {
"id": "line_3", "id": "line_3",
@ -112,7 +112,7 @@
"portrait": "resources/dialogue/portrait_phone.png", "portrait": "resources/dialogue/portrait_phone.png",
"text": "А нож?", "text": "А нож?",
"next": "line_4", "next": "line_4",
"bubbleSlot": "message03in" "chatBubble": "in"
}, },
{ {
"id": "line_4", "id": "line_4",
@ -121,7 +121,7 @@
"portrait": "resources/dialogue/portrait_phone.png", "portrait": "resources/dialogue/portrait_phone.png",
"text": "Нож, наверное, так и остался в учительской.", "text": "Нож, наверное, так и остался в учительской.",
"next": "line_5", "next": "line_5",
"bubbleSlot": "message04out" "chatBubble": "out"
}, },
{ {
"id": "line_5", "id": "line_5",
@ -130,7 +130,7 @@
"portrait": "resources/dialogue/portrait_phone.png", "portrait": "resources/dialogue/portrait_phone.png",
"text": "А давай не \"наверное\"?", "text": "А давай не \"наверное\"?",
"next": "line_6", "next": "line_6",
"bubbleSlot": "message05in" "chatBubble": "in"
}, },
{ {
"id": "line_6", "id": "line_6",
@ -139,7 +139,7 @@
"portrait": "resources/dialogue/portrait_phone.png", "portrait": "resources/dialogue/portrait_phone.png",
"text": "А давай ты приедешь в универ, зайдешь в учительскую, заберешь нож и отдашь мне?", "text": "А давай ты приедешь в универ, зайдешь в учительскую, заберешь нож и отдашь мне?",
"next": "line_7", "next": "line_7",
"bubbleSlot": "message06in" "chatBubble": "in"
}, },
{ {
"id": "line_7", "id": "line_7",
@ -148,7 +148,7 @@
"portrait": "resources/dialogue/portrait_phone.png", "portrait": "resources/dialogue/portrait_phone.png",
"text": "У вас сегодня как раз Аида ведет лекцию. После лекции попросишь у нее ключи от учительской и заберешь нож.", "text": "У вас сегодня как раз Аида ведет лекцию. После лекции попросишь у нее ключи от учительской и заберешь нож.",
"next": "line_8", "next": "line_8",
"bubbleSlot": "message07in" "chatBubble": "in"
}, },
{ {
"id": "line_8", "id": "line_8",
@ -157,7 +157,7 @@
"portrait": "resources/dialogue/portrait_phone.png", "portrait": "resources/dialogue/portrait_phone.png",
"text": "Почему ты сама не можешь забрать?", "text": "Почему ты сама не можешь забрать?",
"next": "line_9", "next": "line_9",
"bubbleSlot": "message08out" "chatBubble": "out"
}, },
{ {
"id": "line_9", "id": "line_9",
@ -166,7 +166,7 @@
"portrait": "resources/dialogue/portrait_phone.png", "portrait": "resources/dialogue/portrait_phone.png",
"text": "Ты же знаешь, если я встречу Аиду, она 100% даст мне какое-нибудь сложное задание.", "text": "Ты же знаешь, если я встречу Аиду, она 100% даст мне какое-нибудь сложное задание.",
"next": "line_10", "next": "line_10",
"bubbleSlot": "message09in" "chatBubble": "in"
}, },
{ {
"id": "line_10", "id": "line_10",
@ -175,7 +175,7 @@
"portrait": "resources/dialogue/portrait_phone.png", "portrait": "resources/dialogue/portrait_phone.png",
"text": "И потом, это ты у меня брал нож, с чего я должна ходить искать его по всему универу?", "text": "И потом, это ты у меня брал нож, с чего я должна ходить искать его по всему универу?",
"next": "line_11", "next": "line_11",
"bubbleSlot": "message10in" "chatBubble": "in"
}, },
{ {
"id": "line_11", "id": "line_11",
@ -184,7 +184,7 @@
"portrait": "resources/dialogue/portrait_phone.png", "portrait": "resources/dialogue/portrait_phone.png",
"text": "Так что жду тебя в универе! Не вздумай прогулять!", "text": "Так что жду тебя в универе! Не вздумай прогулять!",
"next": "end_1", "next": "end_1",
"bubbleSlot": "message11in", "chatBubble": "in",
"questUnlock" : "aiperi_knife" "questUnlock" : "aiperi_knife"
}, },
{ {

BIN
resources/w/ui/img/phone/bubble_in_center.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/w/ui/img/phone/bubble_in_corner_left_bottom.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/w/ui/img/phone/bubble_in_corner_left_top.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/w/ui/img/phone/bubble_in_corner_right_bottom.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/w/ui/img/phone/bubble_in_corner_right_top.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/w/ui/img/phone/bubble_out_center.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/w/ui/img/phone/bubble_out_corner_left_bottom.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/w/ui/img/phone/bubble_out_corner_left_top.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/w/ui/img/phone/bubble_out_corner_right_bottom.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/w/ui/img/phone/bubble_out_corner_right_top.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -35,152 +35,26 @@
"pressed": "resources/w/ui/img/phone/PhoneChat001.png" "pressed": "resources/w/ui/img/phone/PhoneChat001.png"
} }
}, },
{ {
"type": "StaticImage", "type": "FrameLayout",
"name": "message01in", "name": "chatMessagesContainer",
"width": 320.6, "width": "match_parent",
"height": 148.4, "height": "match_parent"
"x" : 430,
"y" : 1097,
"horizontal_gravity": "left",
"vertical_gravity": "bottom",
"visible": false,
"texture": "resources/w/ui/img/phone/chat02_01in.png"
}, },
{ {
"type": "StaticImage",
"name": "message02out",
"width": 320.6,
"height": 64.4,
"x" : 430,
"y" : 1022.6,
"horizontal_gravity": "right",
"vertical_gravity": "bottom",
"visible": false,
"texture": "resources/w/ui/img/phone/chat02_02out.png"
},
{
"type": "StaticImage",
"name": "message03in",
"width": 103.6,
"height": 43.4,
"x" : 430,
"y" : 969.2,
"horizontal_gravity": "left",
"vertical_gravity": "bottom",
"visible": false,
"texture": "resources/w/ui/img/phone/chat02_03in.png"
},
{
"type": "StaticImage",
"name": "message04out",
"width": 320.6,
"height": 64.4,
"x" : 430,
"y" : 894.8,
"horizontal_gravity": "right",
"vertical_gravity": "bottom",
"visible": false,
"texture": "resources/w/ui/img/phone/chat02_04out.png"
},
{
"type": "StaticImage",
"name": "message05in",
"width": 243.6,
"height": 43.4,
"x" : 430,
"y" : 841.4,
"horizontal_gravity": "left",
"vertical_gravity": "bottom",
"visible": false,
"texture": "resources/w/ui/img/phone/chat02_05in.png"
},
{
"type": "StaticImage",
"name": "message06in",
"width": 320.6,
"height": 85.4,
"x" : 430,
"y" : 746,
"horizontal_gravity": "left",
"vertical_gravity": "bottom",
"visible": false,
"texture": "resources/w/ui/img/phone/chat02_06in.png"
},
{
"type": "StaticImage",
"name": "message07in",
"width": 320.6,
"height": 106.4,
"x" : 430,
"y" : 629.6,
"horizontal_gravity": "left",
"vertical_gravity": "bottom",
"visible": false,
"texture": "resources/w/ui/img/phone/chat02_07in.png"
},
{
"type": "StaticImage",
"name": "message08out",
"width": 320.6,
"height": 64.4,
"x" : 430,
"y" : 555.2,
"horizontal_gravity": "right",
"vertical_gravity": "bottom",
"visible": false,
"texture": "resources/w/ui/img/phone/chat02_08out.png"
},
{
"type": "StaticImage",
"name": "message09in",
"width": 320.6,
"height": 85.4,
"x" : 430,
"y" : 459.8,
"horizontal_gravity": "left",
"vertical_gravity": "bottom",
"visible": false,
"texture": "resources/w/ui/img/phone/chat02_09in.png"
},
{
"type": "StaticImage",
"name": "message10in",
"width": 320.6,
"height": 85.4,
"x" : 430,
"y" : 364.4,
"horizontal_gravity": "left",
"vertical_gravity": "bottom",
"visible": false,
"texture": "resources/w/ui/img/phone/chat02_10in.png"
},
{
"type": "StaticImage",
"name": "message11in",
"width": 320.6,
"height": 64.4,
"x" : 430,
"y" : 290,
"horizontal_gravity": "left",
"vertical_gravity": "bottom",
"visible": false,
"texture": "resources/w/ui/img/phone/chat02_11in.png"
},
{
"type": "StaticImage", "type": "StaticImage",
"name": "chatTopCover", "name": "chatTopCover",
"width": 446.25, "width": 446.25,
"height": 70.82, "height": 70.82,
"x" : 0, "x": 0,
"y" : -50.82, "y": -50.82,
"horizontal_gravity": "center", "horizontal_gravity": "center",
"texture": "resources/w/ui/img/phone/chat_top_cover001.png" "texture": "resources/w/ui/img/phone/chat_top_cover001.png"
}, },
{ {
"type": "TextButton", "type": "TextButton",
"name": "chatTitleButton", "name": "chatTitleButton",
"horizontal_gravity": "center", "horizontal_gravity": "center",
"x": 0.0, "x": 0.0,
"y": 20.0, "y": 20.0,
"width": 446.25, "width": 446.25,
@ -193,12 +67,7 @@
"textCentered": false, "textCentered": false,
"topAligned": true, "topAligned": true,
"wrap": true, "wrap": true,
"color": [ "color": [1.0, 1.0, 1.0, 1.0],
1.0,
1.0,
1.0,
1.0
],
"textures": { "textures": {
"normal": "resources/w/ui/img/phone/CharHeader001.png", "normal": "resources/w/ui/img/phone/CharHeader001.png",
"hover": "resources/w/ui/img/phone/CharHeader001.png", "hover": "resources/w/ui/img/phone/CharHeader001.png",

View File

@ -35,20 +35,26 @@
"pressed": "resources/w/ui/img/phone/PhoneChat001.png" "pressed": "resources/w/ui/img/phone/PhoneChat001.png"
} }
}, },
{ {
"type": "FrameLayout",
"name": "chatMessagesContainer",
"width": "match_parent",
"height": "match_parent"
},
{
"type": "StaticImage", "type": "StaticImage",
"name": "chatTopCover", "name": "chatTopCover",
"width": 446.25, "width": 446.25,
"height": 70.82, "height": 70.82,
"x" : 0, "x": 0,
"y" : -50.82, "y": -50.82,
"horizontal_gravity": "center", "horizontal_gravity": "center",
"texture": "resources/w/ui/img/phone/chat_top_cover001.png" "texture": "resources/w/ui/img/phone/chat_top_cover001.png"
}, },
{ {
"type": "TextButton", "type": "TextButton",
"name": "chatTitleButton", "name": "chatTitleButton",
"horizontal_gravity": "center", "horizontal_gravity": "center",
"x": 0.0, "x": 0.0,
"y": 20.0, "y": 20.0,
"width": 446.25, "width": 446.25,
@ -61,41 +67,12 @@
"textCentered": false, "textCentered": false,
"topAligned": true, "topAligned": true,
"wrap": true, "wrap": true,
"color": [ "color": [1.0, 1.0, 1.0, 1.0],
1.0,
1.0,
1.0,
1.0
],
"textures": { "textures": {
"normal": "resources/w/ui/img/phone/CharHeader002.png", "normal": "resources/w/ui/img/phone/CharHeader002.png",
"hover": "resources/w/ui/img/phone/CharHeader002.png", "hover": "resources/w/ui/img/phone/CharHeader002.png",
"pressed": "resources/w/ui/img/phone/CharHeader002.png" "pressed": "resources/w/ui/img/phone/CharHeader002.png"
} }
},
{
"type": "StaticImage",
"name": "message01in",
"width": 320.6,
"height": 103.6,
"x" : 430,
"y" : 506.4,
"horizontal_gravity": "left",
"vertical_gravity": "bottom",
"visible": false,
"texture": "resources/w/ui/img/phone/chat01_01in.png"
},
{
"type": "StaticImage",
"name": "message02out",
"width": 116.2,
"height": 43.4,
"x" : 430,
"y" : 453,
"horizontal_gravity": "right",
"vertical_gravity": "bottom",
"visible": false,
"texture": "resources/w/ui/img/phone/chat01_02out.png"
} }
] ]
} }

View File

@ -35,10 +35,26 @@
"pressed": "resources/w/ui/img/phone/PhoneChat001.png" "pressed": "resources/w/ui/img/phone/PhoneChat001.png"
} }
}, },
{
"type": "FrameLayout",
"name": "chatMessagesContainer",
"width": "match_parent",
"height": "match_parent"
},
{
"type": "StaticImage",
"name": "chatTopCover",
"width": 446.25,
"height": 70.82,
"x": 0,
"y": -50.82,
"horizontal_gravity": "center",
"texture": "resources/w/ui/img/phone/chat_top_cover001.png"
},
{ {
"type": "TextButton", "type": "TextButton",
"name": "chatTitleButton", "name": "chatTitleButton",
"horizontal_gravity": "center", "horizontal_gravity": "center",
"x": 0.0, "x": 0.0,
"y": 20.0, "y": 20.0,
"width": 446.25, "width": 446.25,
@ -51,39 +67,12 @@
"textCentered": false, "textCentered": false,
"topAligned": true, "topAligned": true,
"wrap": true, "wrap": true,
"color": [ "color": [1.0, 1.0, 1.0, 1.0],
1.0,
1.0,
1.0,
1.0
],
"textures": { "textures": {
"normal": "resources/w/ui/img/phone/CharHeader003.png", "normal": "resources/w/ui/img/phone/CharHeader003.png",
"hover": "resources/w/ui/img/phone/CharHeader003.png", "hover": "resources/w/ui/img/phone/CharHeader003.png",
"pressed": "resources/w/ui/img/phone/CharHeader003.png" "pressed": "resources/w/ui/img/phone/CharHeader003.png"
} }
},
{
"type": "StaticImage",
"name": "chatTopCover",
"width": 446.25,
"height": 70.82,
"x" : 0,
"y" : -50.82,
"horizontal_gravity": "center",
"texture": "resources/w/ui/img/phone/chat_top_cover001.png"
},
{
"type": "StaticImage",
"name": "message01in",
"width": 320.6,
"height": 103.6,
"x" : 430,
"y" : 506.4,
"horizontal_gravity": "left",
"vertical_gravity": "bottom",
"visible": false,
"texture": "resources/w/ui/img/phone/chat03_01in.png"
} }
] ]
} }

View File

@ -423,10 +423,10 @@ namespace ZL
startNightTransition(); startNightTransition();
}; };
// Wire bubble-slot callback so chat bubbles appear as dialogue lines are shown. // Wire chat-bubble callback so dynamic bubbles appear as dialogue lines are shown.
for (auto& [name, loc] : locations) { for (auto& [name, loc] : locations) {
loc->dialogueSystem.setOnBubbleSlotReady([this](const std::string& bubbleSlot) { loc->dialogueSystem.setOnChatBubbleReady([this](const std::string& text, bool incoming) {
menuManager.revealPhoneChatBubble(bubbleSlot); menuManager.onChatBubbleReady(text, incoming);
}); });
} }

View File

@ -122,6 +122,18 @@ namespace ZL {
questJournal.loadFromFile("resources/quests/quests.json", zipFile); questJournal.loadFromFile("resources/quests/quests.json", zipFile);
const std::string imgDir = "resources/w/ui/img/phone/";
texBubbleInCenter_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_in_center.png", zipFile, true);
texBubbleInLT_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_in_corner_left_top.png", zipFile, true);
texBubbleInLB_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_in_corner_left_bottom.png", zipFile, true);
texBubbleInRT_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_in_corner_right_top.png", zipFile, true);
texBubbleInRB_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_in_corner_right_bottom.png",zipFile, true);
texBubbleOutCenter_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_out_center.png", zipFile, true);
texBubbleOutLT_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_out_corner_left_top.png", zipFile, true);
texBubbleOutLB_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_out_corner_left_bottom.png",zipFile, true);
texBubbleOutRT_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_out_corner_right_top.png", zipFile, true);
texBubbleOutRB_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_out_corner_right_bottom.png",zipFile, true);
enterGameplay(); enterGameplay();
} }
@ -335,15 +347,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&) {
chatUnread_[0] = false; chatUnread_[0] = false;
openPhoneChatFromList(phoneChat1Root, "dialog_chat_aiperi001"); openPhoneChatFromList(0, phoneChat1Root, "dialog_chat_aiperi001");
}); });
uiManager.setTextButtonCallback("chat2button", [this](const std::string&) { uiManager.setTextButtonCallback("chat2button", [this](const std::string&) {
chatUnread_[1] = false; chatUnread_[1] = false;
openPhoneChatFromList(phoneChat2Root, "dialog_chat_parents001"); openPhoneChatFromList(1, phoneChat2Root, "dialog_chat_parents001");
}); });
uiManager.setTextButtonCallback("chat3button", [this](const std::string&) { uiManager.setTextButtonCallback("chat3button", [this](const std::string&) {
chatUnread_[2] = false; chatUnread_[2] = false;
openPhoneChatFromList(phoneChat3Root, "dialog_chat_news001"); openPhoneChatFromList(2, phoneChat3Root, "dialog_chat_news001");
}); });
} }
@ -424,14 +436,14 @@ namespace ZL {
}); });
} }
void MenuManager::openPhoneChatFromList(std::shared_ptr<UiNode> chatRoot, const std::string& dialogueId) { void MenuManager::openPhoneChatFromList(int chatIndex, std::shared_ptr<UiNode> chatRoot, const std::string& dialogueId) {
activeChatIndex_ = chatIndex;
phoneChatVisibleBubbles_.clear(); phoneChatVisibleBubbles_.clear();
uiManager.pushMenuFromSavedRoot(chatRoot); uiManager.pushMenuFromSavedRoot(chatRoot);
const bool firstOpen = dialogueId.empty() || startedDialogues_.find(dialogueId) == startedDialogues_.end(); const bool firstOpen = dialogueId.empty() || startedDialogues_.find(dialogueId) == startedDialogues_.end();
if (firstOpen) {
resetPhoneChatNodes(); rebuildChatBubblesFromHistory(chatIndex);
}
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) { uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
closePhoneScreenFromChat(); closePhoneScreenFromChat();
@ -448,12 +460,14 @@ namespace ZL {
} }
void MenuManager::returnToPhoneChatList() { void MenuManager::returnToPhoneChatList() {
activeChatIndex_ = -1;
phoneChatVisibleBubbles_.clear(); phoneChatVisibleBubbles_.clear();
uiManager.popMenu(); uiManager.popMenu();
refreshChatUnreadIndicators(); refreshChatUnreadIndicators();
} }
void MenuManager::closePhoneEntirely() { void MenuManager::closePhoneEntirely() {
activeChatIndex_ = -1;
state = GameState::Gameplay; state = GameState::Gameplay;
phoneChatVisibleBubbles_.clear(); phoneChatVisibleBubbles_.clear();
const int depth = uiManager.menuStackSize(); const int depth = uiManager.menuStackSize();
@ -754,19 +768,59 @@ namespace ZL {
uiManager.updateAllLayouts(); uiManager.updateAllLayouts();
} }
void MenuManager::revealPhoneChatBubble(const std::string& slotName) { void MenuManager::rebuildChatBubblesFromHistory(int chatIndex) {
if (state != GameState::PhoneScreen) return; uiManager.clearChatBubbles("chatMessagesContainer");
auto node = uiManager.findNode(slotName); phoneChatVisibleBubbles_.clear();
if (!node) return;
// Zero scale before making visible to avoid a one-frame flash at full size if (chatIndex < 0 || chatIndex > 2) return;
node->scaleX = 0.0f; for (const auto& msg : chatHistory_[chatIndex]) {
node->scaleY = 0.0f; const bool inc = msg.incoming;
const std::string nodeName = uiManager.addChatBubble(
phoneChatVisibleBubbles_.push_back({slotName, node->height}); "chatMessagesContainer", msg.text, inc,
uiManager.setNodeVisible(slotName, true); inc ? texBubbleInCenter_ : texBubbleOutCenter_,
inc ? texBubbleInLT_ : texBubbleOutLT_,
inc ? texBubbleInLB_ : texBubbleOutLB_,
inc ? texBubbleInRT_ : texBubbleOutRT_,
inc ? texBubbleInRB_ : texBubbleOutRB_,
renderer, "resources/fonts/DroidSans.ttf", 20, zipFile_);
if (!nodeName.empty()) {
auto n = uiManager.findNode(nodeName);
phoneChatVisibleBubbles_.push_back({ nodeName, n ? n->height : 60.0f });
}
}
recomputePhoneChatPositions(); recomputePhoneChatPositions();
uiManager.startPopIn(slotName, 300.0f); }
void MenuManager::onChatBubbleReady(const std::string& text, bool incoming) {
if (activeChatIndex_ < 0) return;
auto& history = chatHistory_[activeChatIndex_];
if (static_cast<int>(history.size()) >= 5) {
history.erase(history.begin());
}
history.push_back({ text, incoming });
if (state != GameState::PhoneScreen) return;
const std::string nodeName = uiManager.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, zipFile_);
if (nodeName.empty()) return;
auto node = uiManager.findNode(nodeName);
if (node) {
node->scaleX = 0.0f;
node->scaleY = 0.0f;
phoneChatVisibleBubbles_.push_back({ nodeName, node->height });
}
recomputePhoneChatPositions();
uiManager.startPopIn(nodeName, 300.0f);
} }
void MenuManager::setDarklandsMode(bool enabled) void MenuManager::setDarklandsMode(bool enabled)

View File

@ -58,7 +58,6 @@ namespace ZL {
void openPhoneScreen(); void openPhoneScreen();
void closePhoneScreen(); void closePhoneScreen();
void revealPhoneChatBubble(const std::string& slotName);
bool isPhoneScreenOpen() const { return state == GameState::PhoneScreen; } bool isPhoneScreenOpen() const { return state == GameState::PhoneScreen; }
void closePhoneEntirely(); void closePhoneEntirely();
@ -71,6 +70,9 @@ namespace ZL {
std::function<void()> startDarklandsTransitionFunc; std::function<void()> startDarklandsTransitionFunc;
std::function<void()> startNightTransitionFunc; std::function<void()> startNightTransitionFunc;
// Called when a chat message bubble should be shown (text + direction)
void onChatBubbleReady(const std::string& text, bool incoming);
void setDarklandsMode(bool enabled); void setDarklandsMode(bool enabled);
void advanceTutorialStep(); void advanceTutorialStep();
void onItemPickedUp(const std::string& itemId); void onItemPickedUp(const std::string& itemId);
@ -102,10 +104,11 @@ namespace ZL {
void refreshChatUnreadIndicators(); void refreshChatUnreadIndicators();
void resetPhoneChatNodes(); void resetPhoneChatNodes();
void recomputePhoneChatPositions(); void recomputePhoneChatPositions();
void openPhoneChatFromList(std::shared_ptr<UiNode> chatRoot, const std::string& dialogueId); void openPhoneChatFromList(int chatIndex, std::shared_ptr<UiNode> chatRoot, const std::string& dialogueId);
void returnToPhoneChatList(); void returnToPhoneChatList();
void closePhoneScreenFromChat(); void closePhoneScreenFromChat();
void applyUniIntHud(); void applyUniIntHud();
void rebuildChatBubblesFromHistory(int chatIndex);
GameState state = GameState::Gameplay; GameState state = GameState::Gameplay;
Inventory* inventory = nullptr; Inventory* inventory = nullptr;
@ -176,6 +179,15 @@ namespace ZL {
}; };
std::vector<PhoneChatBubbleInfo> phoneChatVisibleBubbles_; std::vector<PhoneChatBubbleInfo> phoneChatVisibleBubbles_;
// Per-chat message history (max 5 messages each)
struct StoredChatMessage { std::string text; bool incoming; };
std::vector<StoredChatMessage> chatHistory_[3];
int activeChatIndex_ = -1;
// Preloaded bubble textures
std::shared_ptr<Texture> texBubbleInCenter_, texBubbleInLT_, texBubbleInLB_, texBubbleInRT_, texBubbleInRB_;
std::shared_ptr<Texture> texBubbleOutCenter_, texBubbleOutLT_, texBubbleOutLB_, texBubbleOutRT_, texBubbleOutRB_;
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;

View File

@ -824,6 +824,107 @@ namespace ZL {
return root; return root;
} }
// ---- UiChatBubble helpers ----
static void buildQuadMesh(VertexRenderStruct& mesh, float x0, float y0, float x1, float y1) {
mesh.data.PositionData.clear();
mesh.data.TexCoordData.clear();
mesh.data.PositionData.push_back({ x0, y0, 0 });
mesh.data.TexCoordData.push_back({ 0, 0 });
mesh.data.PositionData.push_back({ x0, y1, 0 });
mesh.data.TexCoordData.push_back({ 0, 1 });
mesh.data.PositionData.push_back({ x1, y1, 0 });
mesh.data.TexCoordData.push_back({ 1, 1 });
mesh.data.PositionData.push_back({ x0, y0, 0 });
mesh.data.TexCoordData.push_back({ 0, 0 });
mesh.data.PositionData.push_back({ x1, y1, 0 });
mesh.data.TexCoordData.push_back({ 1, 1 });
mesh.data.PositionData.push_back({ x1, y0, 0 });
mesh.data.TexCoordData.push_back({ 1, 0 });
mesh.RefreshVBO();
}
float UiChatBubble::computeHeight() const {
if (!textRenderer) return CORNER_SIZE * 2 + PAD_Y * 2 + static_cast<float>(fontSize);
const float textW = MAX_WIDTH - CORNER_SIZE * 2.0f - 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 CORNER_SIZE * 2.0f + PAD_Y * 2.0f + lines * static_cast<float>(fontSize) * 1.25f;
}
void UiChatBubble::buildMeshes() {
const float x = rect.x;
const float y = rect.y;
const float w = rect.w;
const float h = rect.h;
const float cs = CORNER_SIZE;
const float OFFSET = 0.5f;
// 1. Middle center (full width, between top and bottom corner rows)
buildQuadMesh(meshMiddle, x, y + cs- OFFSET, x + w, y + h - cs +OFFSET);
// 2. Top strip (between corners)
buildQuadMesh(meshTopStrip, x + cs- OFFSET, y + h - cs, x + w - cs+ OFFSET, y + h);
// 3. Bottom strip
buildQuadMesh(meshBotStrip, x + cs- OFFSET, y, x + w - cs+ OFFSET, y + cs);
// Corners
buildQuadMesh(meshCornerLT, x, y + h - cs-OFFSET, x + cs, y + h);
buildQuadMesh(meshCornerRT, x + w - cs, y + h - cs - OFFSET, x + w, y + h);
buildQuadMesh(meshCornerLB, x, y, x + cs, y + cs);
buildQuadMesh(meshCornerRB, x + w - cs, y, x + w, y + cs);
}
void UiChatBubble::draw(Renderer& renderer, float nodeScaleX, float nodeScaleY) const {
renderer.RenderUniform1i(textureUniformName, 0);
// Corners with blending (semi-transparent rounded corners)
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
auto drawCorner = [&](const std::shared_ptr<Texture>& tex, const VertexRenderStruct& mesh) {
if (tex) {
glBindTexture(GL_TEXTURE_2D, tex->getTexID());
renderer.DrawVertexRenderStruct(mesh);
}
};
drawCorner(texCornerLT, meshCornerLT);
drawCorner(texCornerRT, meshCornerRT);
drawCorner(texCornerLB, meshCornerLB);
drawCorner(texCornerRB, meshCornerRB);
glDisable(GL_BLEND);
// Center parts (no blending needed — opaque fill)
if (texCenter) {
glBindTexture(GL_TEXTURE_2D, texCenter->getTexID());
renderer.DrawVertexRenderStruct(meshMiddle);
renderer.DrawVertexRenderStruct(meshTopStrip);
renderer.DrawVertexRenderStruct(meshBotStrip);
}
// Text — position is transformed around the bubble centre to match the renderer's
// scale matrix (which scales meshes around the same centre point).
if (textRenderer && !text.empty()) {
const float textW = rect.w - CORNER_SIZE * 2.0f - PAD_X * 2.0f;
const std::string wrapped = wrapTextByPixels(text, *textRenderer, textW, 1.0f);
const float tx0 = rect.x + CORNER_SIZE + PAD_X;
const float ty0 = rect.y + rect.h - CORNER_SIZE - PAD_Y - static_cast<float>(fontSize);
// Scale around bubble centre, matching how the renderer's ScaleMatrix works
const float cx = rect.x + rect.w * 0.5f;
const float cy = rect.y + rect.h * 0.5f;
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 });
}
}
// ---- end UiChatBubble helpers ----
void UiManager::replaceRoot(std::shared_ptr<UiNode> newRoot) { void UiManager::replaceRoot(std::shared_ptr<UiNode> newRoot) {
root = newRoot; root = newRoot;
layoutNode( layoutNode(
@ -841,6 +942,7 @@ namespace ZL {
textViews.clear(); textViews.clear();
textFields.clear(); textFields.clear();
staticImages.clear(); staticImages.clear();
chatBubbles.clear();
pulsingNodes.clear(); pulsingNodes.clear();
popInNodes.clear(); popInNodes.clear();
collectButtonsAndSliders(root); collectButtonsAndSliders(root);
@ -1052,6 +1154,12 @@ namespace ZL {
node->staticImage->rect = node->screenRect; node->staticImage->rect = node->screenRect;
node->staticImage->buildMesh(); node->staticImage->buildMesh();
} }
// 6. Chat bubble
if (node->chatBubble) {
node->chatBubble->rect = node->screenRect;
node->chatBubble->buildMeshes();
}
} }
void UiManager::updateAllLayouts() { void UiManager::updateAllLayouts() {
@ -1106,6 +1214,9 @@ namespace ZL {
if (node->pulseEnabled && node->staticImage) { if (node->pulseEnabled && node->staticImage) {
pulsingNodes.push_back(node); pulsingNodes.push_back(node);
} }
if (node->chatBubble) {
chatBubbles.push_back(node->chatBubble);
}
for (auto& c : node->children) collectButtonsAndSliders(c); for (auto& c : node->children) collectButtonsAndSliders(c);
} }
@ -1215,6 +1326,7 @@ namespace ZL {
prev.textViews = textViews; prev.textViews = textViews;
prev.textFields = textFields; prev.textFields = textFields;
prev.staticImages = staticImages; prev.staticImages = staticImages;
prev.chatBubbles = chatBubbles;
prev.pulsingNodes = pulsingNodes; prev.pulsingNodes = pulsingNodes;
prev.pressedButtons = pressedButtons; prev.pressedButtons = pressedButtons;
prev.pressedTextButtons = pressedTextButtons; prev.pressedTextButtons = pressedTextButtons;
@ -1280,6 +1392,7 @@ namespace ZL {
textViews = s.textViews; textViews = s.textViews;
textFields = s.textFields; textFields = s.textFields;
staticImages = s.staticImages; staticImages = s.staticImages;
chatBubbles = s.chatBubbles;
pulsingNodes = s.pulsingNodes; pulsingNodes = s.pulsingNodes;
popInNodes = s.popInNodes; popInNodes = s.popInNodes;
pressedButtons = s.pressedButtons; pressedButtons = s.pressedButtons;
@ -1348,6 +1461,11 @@ namespace ZL {
node->staticImage->draw(renderer); node->staticImage->draw(renderer);
} }
// 1b. Chat bubble (composite widget)
if (node->chatBubble) {
node->chatBubble->draw(renderer, node->scaleX, node->scaleY);
}
// 2. Потом кнопки // 2. Потом кнопки
if (node->button) { if (node->button) {
node->button->draw(renderer); node->button->draw(renderer);
@ -1994,4 +2112,86 @@ namespace ZL {
if (!node) return false; if (!node) return false;
return node->visible; return node->visible;
} }
std::string UiManager::addChatBubble(
const std::string& parentNodeName,
const std::string& text, bool incoming,
std::shared_ptr<Texture> texCenter,
std::shared_ptr<Texture> texLT, std::shared_ptr<Texture> texLB,
std::shared_ptr<Texture> texRT, std::shared_ptr<Texture> texRB,
Renderer& renderer, const std::string& fontPath, int fontSize,
const std::string& zipFile)
{
if (!root) return "";
auto parent = findNodeByName(root, parentNodeName);
if (!parent) {
std::cerr << "UiManager::addChatBubble: parent '" << parentNodeName << "' not found\n";
return "";
}
static int bubbleCounter = 0;
const std::string nodeName = "chat_bubble_" + std::to_string(bubbleCounter++);
// Build the chat bubble component
auto bubble = std::make_shared<UiChatBubble>();
bubble->name = nodeName;
bubble->text = text;
bubble->incoming = incoming;
bubble->fontSize = fontSize;
bubble->texCenter = texCenter;
bubble->texCornerLT = texLT;
bubble->texCornerLB = texLB;
bubble->texCornerRT = texRT;
bubble->texCornerRB = texRB;
bubble->textRenderer = std::make_unique<TextRenderer>();
if (!bubble->textRenderer->init(renderer, fontPath, fontSize, zipFile)) {
std::cerr << "UiManager::addChatBubble: TextRenderer init failed\n";
}
// Build the UiNode
auto node = std::make_shared<UiNode>();
node->name = nodeName;
node->width = UiChatBubble::MAX_WIDTH;
node->height = bubble->computeHeight();
node->localX = 430.0f;
node->localY = 0.0f;
node->visible = true;
node->chatBubble = bubble;
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;
parent->children.push_back(node);
// Re-layout and collect the new node
updateAllLayouts();
bubble->rect = node->screenRect;
bubble->buildMeshes();
chatBubbles.push_back(bubble);
return nodeName;
}
void UiManager::clearChatBubbles(const std::string& parentNodeName) {
if (!root) return;
auto parent = findNodeByName(root, parentNodeName);
if (!parent) return;
// Remove children that have a chatBubble component
auto& ch = parent->children;
ch.erase(std::remove_if(ch.begin(), ch.end(),
[](const std::shared_ptr<UiNode>& n) { return n && n->chatBubble != nullptr; }),
ch.end());
// Remove from chatBubbles collection
chatBubbles.clear();
}
} // namespace ZL } // namespace ZL

View File

@ -245,6 +245,45 @@ namespace ZL {
void draw(Renderer& renderer) const; void draw(Renderer& renderer) const;
}; };
struct UiChatBubble {
std::string name;
UiRect rect;
std::string text;
bool incoming = true;
std::shared_ptr<Texture> texCenter;
std::shared_ptr<Texture> texCornerLT;
std::shared_ptr<Texture> texCornerLB;
std::shared_ptr<Texture> texCornerRT;
std::shared_ptr<Texture> texCornerRB;
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 = -15.0f;
static constexpr float PAD_Y = -15.0f;
// 7 meshes: middle-center, top-strip, bottom-strip, LT, RT, LB, RB
VertexRenderStruct meshMiddle;
VertexRenderStruct meshTopStrip;
VertexRenderStruct meshBotStrip;
VertexRenderStruct meshCornerLT;
VertexRenderStruct meshCornerRT;
VertexRenderStruct meshCornerLB;
VertexRenderStruct meshCornerRB;
std::unique_ptr<TextRenderer> textRenderer;
int fontSize = 20;
// Must be called after rect is set and textRenderer is initialised
float computeHeight() 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;
};
struct UiNode { struct UiNode {
std::string name; std::string name;
LayoutType layoutType = LayoutType::Frame; LayoutType layoutType = LayoutType::Frame;
@ -290,6 +329,7 @@ namespace ZL {
std::shared_ptr<UiTextView> textView; std::shared_ptr<UiTextView> textView;
std::shared_ptr<UiTextField> textField; std::shared_ptr<UiTextField> textField;
std::shared_ptr<UiStaticImage> staticImage; std::shared_ptr<UiStaticImage> staticImage;
std::shared_ptr<UiChatBubble> chatBubble;
// Анимации // Анимации
struct AnimStep { struct AnimStep {
@ -409,6 +449,19 @@ namespace ZL {
void clearMenuStack(); void clearMenuStack();
int menuStackSize() const; int menuStackSize() const;
// Dynamically create a chat bubble node and append it to a named parent node.
// Returns the generated node name, or "" on failure.
std::string addChatBubble(const std::string& parentNodeName,
const std::string& text, bool incoming,
std::shared_ptr<Texture> texCenter,
std::shared_ptr<Texture> texLT, std::shared_ptr<Texture> texLB,
std::shared_ptr<Texture> texRT, std::shared_ptr<Texture> texRB,
Renderer& renderer, const std::string& fontPath, int fontSize,
const std::string& zipFile);
// Remove all chat-bubble children from named parent and clear chatBubbles collection.
void clearChatBubbles(const std::string& parentNodeName);
void update(float deltaMs); void update(float deltaMs);
void startAnimation(const std::string& animName); void startAnimation(const std::string& animName);
bool startAnimationOnNode(const std::string& nodeName, const std::string& animName); bool startAnimationOnNode(const std::string& nodeName, const std::string& animName);
@ -462,6 +515,7 @@ namespace ZL {
std::vector<std::shared_ptr<UiTextView>> textViews; std::vector<std::shared_ptr<UiTextView>> textViews;
std::vector<std::shared_ptr<UiTextField>> textFields; std::vector<std::shared_ptr<UiTextField>> textFields;
std::vector<std::shared_ptr<UiStaticImage>> staticImages; std::vector<std::shared_ptr<UiStaticImage>> staticImages;
std::vector<std::shared_ptr<UiChatBubble>> chatBubbles;
std::vector<std::shared_ptr<UiNode>> pulsingNodes; std::vector<std::shared_ptr<UiNode>> pulsingNodes;
std::vector<std::shared_ptr<UiNode>> popInNodes; std::vector<std::shared_ptr<UiNode>> popInNodes;
@ -483,6 +537,7 @@ namespace ZL {
std::vector<std::shared_ptr<UiTextView>> textViews; std::vector<std::shared_ptr<UiTextView>> textViews;
std::vector<std::shared_ptr<UiTextField>> textFields; std::vector<std::shared_ptr<UiTextField>> textFields;
std::vector<std::shared_ptr<UiStaticImage>> staticImages; std::vector<std::shared_ptr<UiStaticImage>> staticImages;
std::vector<std::shared_ptr<UiChatBubble>> chatBubbles;
std::vector<std::shared_ptr<UiNode>> pulsingNodes; std::vector<std::shared_ptr<UiNode>> pulsingNodes;
std::vector<std::shared_ptr<UiNode>> popInNodes; std::vector<std::shared_ptr<UiNode>> popInNodes;
std::map<int64_t, std::shared_ptr<UiButton>> pressedButtons; std::map<int64_t, std::shared_ptr<UiButton>> pressedButtons;

View File

@ -105,7 +105,7 @@ Node DialogueDatabase::parseNode(const json& j) {
node.falseNext = j.value("falseNext", ""); node.falseNext = j.value("falseNext", "");
node.cutsceneId = j.value("cutsceneId", ""); node.cutsceneId = j.value("cutsceneId", "");
node.luaCallback = j.value("luaCallback", ""); node.luaCallback = j.value("luaCallback", "");
node.bubbleSlot = j.value("bubbleSlot", ""); node.chatBubble = j.value("chatBubble", "");
node.questUnlock = j.value("questUnlock", ""); node.questUnlock = j.value("questUnlock", "");
node.questComplete = j.value("questComplete", ""); node.questComplete = j.value("questComplete", "");
node.questFail = j.value("questFail", ""); node.questFail = j.value("questFail", "");

View File

@ -87,8 +87,8 @@ void DialogueRuntime::setOnCutsceneFadeInComplete(std::function<void(const std::
onCutsceneFadeInComplete = std::move(cb); onCutsceneFadeInComplete = std::move(cb);
} }
void DialogueRuntime::setOnBubbleSlotReady(std::function<void(const std::string&)> cb) { void DialogueRuntime::setOnChatBubbleReady(std::function<void(const std::string&, bool)> cb) {
onBubbleSlotReady = std::move(cb); onChatBubbleReady = std::move(cb);
} }
void DialogueRuntime::stop() { void DialogueRuntime::stop() {
@ -466,8 +466,8 @@ void DialogueRuntime::presentLine(const Node& node) {
if (!node.luaCallback.empty() && onDialogueLineStarted) { if (!node.luaCallback.empty() && onDialogueLineStarted) {
onDialogueLineStarted(node.luaCallback); onDialogueLineStarted(node.luaCallback);
} }
if (!node.bubbleSlot.empty() && onBubbleSlotReady) { if (!node.chatBubble.empty() && onChatBubbleReady) {
onBubbleSlotReady(node.bubbleSlot); onChatBubbleReady(node.text, node.chatBubble == "in");
} }
} }

View File

@ -23,7 +23,7 @@ public:
void setOnDialogueLineStarted(std::function<void(const std::string&)> cb); void setOnDialogueLineStarted(std::function<void(const std::string&)> cb);
void setOnCutsceneLineStarted(std::function<void(const std::string&)> cb); void setOnCutsceneLineStarted(std::function<void(const std::string&)> cb);
void setOnCutsceneFadeInComplete(std::function<void(const std::string&)> cb); void setOnCutsceneFadeInComplete(std::function<void(const std::string&)> cb);
void setOnBubbleSlotReady(std::function<void(const std::string&)> cb); void setOnChatBubbleReady(std::function<void(const std::string&, bool)> cb);
void stop(); void stop();
void update(int deltaMs); void update(int deltaMs);
@ -60,7 +60,7 @@ private:
std::function<void(const std::string&)> onDialogueLineStarted; std::function<void(const std::string&)> onDialogueLineStarted;
std::function<void(const std::string&)> onCutsceneLineStarted; std::function<void(const std::string&)> onCutsceneLineStarted;
std::function<void(const std::string&)> onCutsceneFadeInComplete; std::function<void(const std::string&)> onCutsceneFadeInComplete;
std::function<void(const std::string&)> onBubbleSlotReady; std::function<void(const std::string&, bool)> onChatBubbleReady;
std::string activeCutsceneId; std::string activeCutsceneId;
bool fadeInCallbackFired = false; bool fadeInCallbackFired = false;

View File

@ -140,8 +140,8 @@ void DialogueSystem::setOnCutsceneFadeInComplete(std::function<void(const std::s
runtime.setOnCutsceneFadeInComplete(std::move(cb)); runtime.setOnCutsceneFadeInComplete(std::move(cb));
} }
void DialogueSystem::setOnBubbleSlotReady(std::function<void(const std::string&)> cb) { void DialogueSystem::setOnChatBubbleReady(std::function<void(const std::string&, bool)> cb) {
runtime.setOnBubbleSlotReady(std::move(cb)); runtime.setOnChatBubbleReady(std::move(cb));
} }
void DialogueSystem::stopDialogue() { void DialogueSystem::stopDialogue() {

View File

@ -28,7 +28,7 @@ public:
void setOnDialogueLineStarted(std::function<void(const std::string&)> cb); void setOnDialogueLineStarted(std::function<void(const std::string&)> cb);
void setOnCutsceneLineStarted(std::function<void(const std::string&)> cb); void setOnCutsceneLineStarted(std::function<void(const std::string&)> cb);
void setOnCutsceneFadeInComplete(std::function<void(const std::string&)> cb); void setOnCutsceneFadeInComplete(std::function<void(const std::string&)> cb);
void setOnBubbleSlotReady(std::function<void(const std::string&)> cb); void setOnChatBubbleReady(std::function<void(const std::string&, bool)> cb);
void setOnDialogueAdvanced(std::function<void()> cb); void setOnDialogueAdvanced(std::function<void()> cb);
void stopDialogue(); void stopDialogue();

View File

@ -97,8 +97,8 @@ struct Node {
// For CutsceneStart // For CutsceneStart
std::string cutsceneId; std::string cutsceneId;
// Name of the UI node (StaticImage) to reveal in the phone chat when this line is shown // "in" or "out" — creates a dynamic chat bubble; empty = not a chat message
std::string bubbleSlot; std::string chatBubble;
// Quest actions fired when this line is presented (empty = no action) // Quest actions fired when this line is presented (empty = no action)
std::string questUnlock; std::string questUnlock;