From 1fc8120ee065c71065af189de136de63c88f8853 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Fri, 5 Jun 2026 12:13:02 +0300 Subject: [PATCH] Working on messenger --- resources/dialogue/dorm_dialogues.json | 28 +-- resources/w/ui/img/phone/bubble_in_center.png | 3 + .../phone/bubble_in_corner_left_bottom.png | 3 + .../img/phone/bubble_in_corner_left_top.png | 3 + .../phone/bubble_in_corner_right_bottom.png | 3 + .../img/phone/bubble_in_corner_right_top.png | 3 + .../w/ui/img/phone/bubble_out_center.png | 3 + .../phone/bubble_out_corner_left_bottom.png | 3 + .../img/phone/bubble_out_corner_left_top.png | 3 + .../phone/bubble_out_corner_right_bottom.png | 3 + .../img/phone/bubble_out_corner_right_top.png | 3 + resources/w/ui/screen_phone_chat1.json | 155 ++------------ resources/w/ui/screen_phone_chat2.json | 49 ++--- resources/w/ui/screen_phone_chat3.json | 49 ++--- src/Game.cpp | 6 +- src/MenuManager.cpp | 90 ++++++-- src/MenuManager.h | 16 +- src/UiManager.cpp | 202 +++++++++++++++++- src/UiManager.h | 55 +++++ src/dialogue/DialogueDatabase.cpp | 2 +- src/dialogue/DialogueRuntime.cpp | 8 +- src/dialogue/DialogueRuntime.h | 4 +- src/dialogue/DialogueSystem.cpp | 4 +- src/dialogue/DialogueSystem.h | 2 +- src/dialogue/DialogueTypes.h | 4 +- 25 files changed, 445 insertions(+), 259 deletions(-) create mode 100644 resources/w/ui/img/phone/bubble_in_center.png create mode 100644 resources/w/ui/img/phone/bubble_in_corner_left_bottom.png create mode 100644 resources/w/ui/img/phone/bubble_in_corner_left_top.png create mode 100644 resources/w/ui/img/phone/bubble_in_corner_right_bottom.png create mode 100644 resources/w/ui/img/phone/bubble_in_corner_right_top.png create mode 100644 resources/w/ui/img/phone/bubble_out_center.png create mode 100644 resources/w/ui/img/phone/bubble_out_corner_left_bottom.png create mode 100644 resources/w/ui/img/phone/bubble_out_corner_left_top.png create mode 100644 resources/w/ui/img/phone/bubble_out_corner_right_bottom.png create mode 100644 resources/w/ui/img/phone/bubble_out_corner_right_top.png diff --git a/resources/dialogue/dorm_dialogues.json b/resources/dialogue/dorm_dialogues.json index b587321..8eb372b 100644 --- a/resources/dialogue/dorm_dialogues.json +++ b/resources/dialogue/dorm_dialogues.json @@ -47,7 +47,7 @@ "portrait": "resources/dialogue/portrait_phone.png", "text": "Бекзат, сынок, мы c мамой тебе отправили немного денег, постарайся прожить на эти деньги до конца недели!", "next": "line_2", - "bubbleSlot": "message01in" + "chatBubble": "in" }, { "id": "line_2", @@ -56,7 +56,7 @@ "portrait": "resources/dialogue/portrait_phone.png", "text": "Спасибо!", "next": "end_1", - "bubbleSlot": "message02out" + "chatBubble": "out" }, { "id": "end_1", @@ -75,7 +75,7 @@ "portrait": "resources/dialogue/portrait_phone.png", "text": "Жители Бишкека все чаще жалуются на депрессию и апатию. Смотрите свежее видео об этом на нашем канале!", "next": "end_1", - "bubbleSlot": "message01in" + "chatBubble": "in" }, { "id": "end_1", @@ -94,7 +94,7 @@ "portrait": "resources/dialogue/portrait_phone.png", "text": "Бекзат, помнишь мы скидывались на торт для Аиды Джаныбековой? Я тогда еще приносила скатерть, тарелки и нож для торта. И я до сих пор не получила назад ничего.", "next": "line_2", - "bubbleSlot": "message01in" + "chatBubble": "in" }, { "id": "line_2", @@ -103,7 +103,7 @@ "portrait": "resources/dialogue/portrait_phone.png", "text": "Скатерть и тарелки вроде бы лежат в студзоне.", "next": "line_3", - "bubbleSlot": "message02out" + "chatBubble": "out" }, { "id": "line_3", @@ -112,7 +112,7 @@ "portrait": "resources/dialogue/portrait_phone.png", "text": "А нож?", "next": "line_4", - "bubbleSlot": "message03in" + "chatBubble": "in" }, { "id": "line_4", @@ -121,7 +121,7 @@ "portrait": "resources/dialogue/portrait_phone.png", "text": "Нож, наверное, так и остался в учительской.", "next": "line_5", - "bubbleSlot": "message04out" + "chatBubble": "out" }, { "id": "line_5", @@ -130,7 +130,7 @@ "portrait": "resources/dialogue/portrait_phone.png", "text": "А давай не \"наверное\"?", "next": "line_6", - "bubbleSlot": "message05in" + "chatBubble": "in" }, { "id": "line_6", @@ -139,7 +139,7 @@ "portrait": "resources/dialogue/portrait_phone.png", "text": "А давай ты приедешь в универ, зайдешь в учительскую, заберешь нож и отдашь мне?", "next": "line_7", - "bubbleSlot": "message06in" + "chatBubble": "in" }, { "id": "line_7", @@ -148,7 +148,7 @@ "portrait": "resources/dialogue/portrait_phone.png", "text": "У вас сегодня как раз Аида ведет лекцию. После лекции попросишь у нее ключи от учительской и заберешь нож.", "next": "line_8", - "bubbleSlot": "message07in" + "chatBubble": "in" }, { "id": "line_8", @@ -157,7 +157,7 @@ "portrait": "resources/dialogue/portrait_phone.png", "text": "Почему ты сама не можешь забрать?", "next": "line_9", - "bubbleSlot": "message08out" + "chatBubble": "out" }, { "id": "line_9", @@ -166,7 +166,7 @@ "portrait": "resources/dialogue/portrait_phone.png", "text": "Ты же знаешь, если я встречу Аиду, она 100% даст мне какое-нибудь сложное задание.", "next": "line_10", - "bubbleSlot": "message09in" + "chatBubble": "in" }, { "id": "line_10", @@ -175,7 +175,7 @@ "portrait": "resources/dialogue/portrait_phone.png", "text": "И потом, это ты у меня брал нож, с чего я должна ходить искать его по всему универу?", "next": "line_11", - "bubbleSlot": "message10in" + "chatBubble": "in" }, { "id": "line_11", @@ -184,7 +184,7 @@ "portrait": "resources/dialogue/portrait_phone.png", "text": "Так что жду тебя в универе! Не вздумай прогулять!", "next": "end_1", - "bubbleSlot": "message11in", + "chatBubble": "in", "questUnlock" : "aiperi_knife" }, { diff --git a/resources/w/ui/img/phone/bubble_in_center.png b/resources/w/ui/img/phone/bubble_in_center.png new file mode 100644 index 0000000..15aab82 --- /dev/null +++ b/resources/w/ui/img/phone/bubble_in_center.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8fbb975263bf30d0ac53a98bf66b83395cd17024989f0b285bbfbbf0338b695d +size 678 diff --git a/resources/w/ui/img/phone/bubble_in_corner_left_bottom.png b/resources/w/ui/img/phone/bubble_in_corner_left_bottom.png new file mode 100644 index 0000000..96e06c1 --- /dev/null +++ b/resources/w/ui/img/phone/bubble_in_corner_left_bottom.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e9bad79019ed14a41d471c8c6569f704fe1ae8fb14aad9439605d332cbfcdc3 +size 1048 diff --git a/resources/w/ui/img/phone/bubble_in_corner_left_top.png b/resources/w/ui/img/phone/bubble_in_corner_left_top.png new file mode 100644 index 0000000..c461335 --- /dev/null +++ b/resources/w/ui/img/phone/bubble_in_corner_left_top.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7711cb9bc7e74816d3481dcba3f83199c0292871eaed4f028cd18ae2cdcbbda9 +size 1495 diff --git a/resources/w/ui/img/phone/bubble_in_corner_right_bottom.png b/resources/w/ui/img/phone/bubble_in_corner_right_bottom.png new file mode 100644 index 0000000..3e938d4 --- /dev/null +++ b/resources/w/ui/img/phone/bubble_in_corner_right_bottom.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:867b41ba1685dccfc4e6295f389b06e437d6c4cdf55b30fecad902ad393f15dd +size 1498 diff --git a/resources/w/ui/img/phone/bubble_in_corner_right_top.png b/resources/w/ui/img/phone/bubble_in_corner_right_top.png new file mode 100644 index 0000000..eed36ca --- /dev/null +++ b/resources/w/ui/img/phone/bubble_in_corner_right_top.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:931c4517e5ec26b8179aa6665ead52b8924eb888271d48abae1670d05739d11a +size 1445 diff --git a/resources/w/ui/img/phone/bubble_out_center.png b/resources/w/ui/img/phone/bubble_out_center.png new file mode 100644 index 0000000..f5228eb --- /dev/null +++ b/resources/w/ui/img/phone/bubble_out_center.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78df06f0e791cea8fb2012a2090cdd666772bd76e1832d7e77a62a6dc1ce72eb +size 386 diff --git a/resources/w/ui/img/phone/bubble_out_corner_left_bottom.png b/resources/w/ui/img/phone/bubble_out_corner_left_bottom.png new file mode 100644 index 0000000..1a4a33c --- /dev/null +++ b/resources/w/ui/img/phone/bubble_out_corner_left_bottom.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:916158690a09d324b986b2cf0cf13cf8b85ab0de54742e091d133da692e1d931 +size 1531 diff --git a/resources/w/ui/img/phone/bubble_out_corner_left_top.png b/resources/w/ui/img/phone/bubble_out_corner_left_top.png new file mode 100644 index 0000000..57aedd3 --- /dev/null +++ b/resources/w/ui/img/phone/bubble_out_corner_left_top.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e1d0662f38d52c17378d3f43fce0950d684bb87bde2182becfb10de965dee08 +size 1503 diff --git a/resources/w/ui/img/phone/bubble_out_corner_right_bottom.png b/resources/w/ui/img/phone/bubble_out_corner_right_bottom.png new file mode 100644 index 0000000..8eae632 --- /dev/null +++ b/resources/w/ui/img/phone/bubble_out_corner_right_bottom.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4be607d89f9ba00b35fba081f54f65d4dad7b6d8b754bbdba1fcac5057862b04 +size 965 diff --git a/resources/w/ui/img/phone/bubble_out_corner_right_top.png b/resources/w/ui/img/phone/bubble_out_corner_right_top.png new file mode 100644 index 0000000..5cda059 --- /dev/null +++ b/resources/w/ui/img/phone/bubble_out_corner_right_top.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8f6d2d35304a20f6f9a31d4bcae9b91922e06d8696bb9b581f9eb038dea2f02 +size 1504 diff --git a/resources/w/ui/screen_phone_chat1.json b/resources/w/ui/screen_phone_chat1.json index 011d781..696261d 100644 --- a/resources/w/ui/screen_phone_chat1.json +++ b/resources/w/ui/screen_phone_chat1.json @@ -35,152 +35,26 @@ "pressed": "resources/w/ui/img/phone/PhoneChat001.png" } }, - { - "type": "StaticImage", - "name": "message01in", - "width": 320.6, - "height": 148.4, - "x" : 430, - "y" : 1097, - "horizontal_gravity": "left", - "vertical_gravity": "bottom", - "visible": false, - "texture": "resources/w/ui/img/phone/chat02_01in.png" + { + "type": "FrameLayout", + "name": "chatMessagesContainer", + "width": "match_parent", + "height": "match_parent" }, - { - "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", "name": "chatTopCover", "width": 446.25, "height": 70.82, - "x" : 0, - "y" : -50.82, - "horizontal_gravity": "center", + "x": 0, + "y": -50.82, + "horizontal_gravity": "center", "texture": "resources/w/ui/img/phone/chat_top_cover001.png" }, { "type": "TextButton", "name": "chatTitleButton", - "horizontal_gravity": "center", + "horizontal_gravity": "center", "x": 0.0, "y": 20.0, "width": 446.25, @@ -193,12 +67,7 @@ "textCentered": false, "topAligned": true, "wrap": true, - "color": [ - 1.0, - 1.0, - 1.0, - 1.0 - ], + "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", @@ -207,4 +76,4 @@ } ] } -} \ No newline at end of file +} diff --git a/resources/w/ui/screen_phone_chat2.json b/resources/w/ui/screen_phone_chat2.json index 92753e9..2089201 100644 --- a/resources/w/ui/screen_phone_chat2.json +++ b/resources/w/ui/screen_phone_chat2.json @@ -35,20 +35,26 @@ "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", + "x": 0, + "y": -50.82, + "horizontal_gravity": "center", "texture": "resources/w/ui/img/phone/chat_top_cover001.png" }, { "type": "TextButton", "name": "chatTitleButton", - "horizontal_gravity": "center", + "horizontal_gravity": "center", "x": 0.0, "y": 20.0, "width": 446.25, @@ -61,42 +67,13 @@ "textCentered": false, "topAligned": true, "wrap": true, - "color": [ - 1.0, - 1.0, - 1.0, - 1.0 - ], + "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" } - }, - { - "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" } ] } -} \ No newline at end of file +} diff --git a/resources/w/ui/screen_phone_chat3.json b/resources/w/ui/screen_phone_chat3.json index 0f25d3d..5f14616 100644 --- a/resources/w/ui/screen_phone_chat3.json +++ b/resources/w/ui/screen_phone_chat3.json @@ -35,10 +35,26 @@ "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", "name": "chatTitleButton", - "horizontal_gravity": "center", + "horizontal_gravity": "center", "x": 0.0, "y": 20.0, "width": 446.25, @@ -51,40 +67,13 @@ "textCentered": false, "topAligned": true, "wrap": true, - "color": [ - 1.0, - 1.0, - 1.0, - 1.0 - ], + "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" } - }, - { - "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" } ] } -} \ No newline at end of file +} diff --git a/src/Game.cpp b/src/Game.cpp index 66b7657..5302db7 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -423,10 +423,10 @@ namespace ZL 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) { - loc->dialogueSystem.setOnBubbleSlotReady([this](const std::string& bubbleSlot) { - menuManager.revealPhoneChatBubble(bubbleSlot); + loc->dialogueSystem.setOnChatBubbleReady([this](const std::string& text, bool incoming) { + menuManager.onChatBubbleReady(text, incoming); }); } diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index 66a47a5..c37b5dd 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -122,6 +122,18 @@ namespace ZL { 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(); } @@ -335,15 +347,15 @@ namespace ZL { uiManager.setButtonCallback("phoneMain", [this](const std::string&) {}); uiManager.setTextButtonCallback("chat1button", [this](const std::string&) { chatUnread_[0] = false; - openPhoneChatFromList(phoneChat1Root, "dialog_chat_aiperi001"); + openPhoneChatFromList(0, phoneChat1Root, "dialog_chat_aiperi001"); }); uiManager.setTextButtonCallback("chat2button", [this](const std::string&) { chatUnread_[1] = false; - openPhoneChatFromList(phoneChat2Root, "dialog_chat_parents001"); + openPhoneChatFromList(1, phoneChat2Root, "dialog_chat_parents001"); }); uiManager.setTextButtonCallback("chat3button", [this](const std::string&) { 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 chatRoot, const std::string& dialogueId) { + void MenuManager::openPhoneChatFromList(int chatIndex, std::shared_ptr chatRoot, const std::string& dialogueId) { + activeChatIndex_ = chatIndex; phoneChatVisibleBubbles_.clear(); uiManager.pushMenuFromSavedRoot(chatRoot); const bool firstOpen = dialogueId.empty() || startedDialogues_.find(dialogueId) == startedDialogues_.end(); - if (firstOpen) { - resetPhoneChatNodes(); - } + + rebuildChatBubblesFromHistory(chatIndex); uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) { closePhoneScreenFromChat(); @@ -448,12 +460,14 @@ namespace ZL { } void MenuManager::returnToPhoneChatList() { + activeChatIndex_ = -1; phoneChatVisibleBubbles_.clear(); uiManager.popMenu(); refreshChatUnreadIndicators(); } void MenuManager::closePhoneEntirely() { + activeChatIndex_ = -1; state = GameState::Gameplay; phoneChatVisibleBubbles_.clear(); const int depth = uiManager.menuStackSize(); @@ -754,19 +768,59 @@ namespace ZL { uiManager.updateAllLayouts(); } - void MenuManager::revealPhoneChatBubble(const std::string& slotName) { - if (state != GameState::PhoneScreen) return; - auto node = uiManager.findNode(slotName); - if (!node) return; + void MenuManager::rebuildChatBubblesFromHistory(int chatIndex) { + uiManager.clearChatBubbles("chatMessagesContainer"); + phoneChatVisibleBubbles_.clear(); - // Zero scale before making visible to avoid a one-frame flash at full size - node->scaleX = 0.0f; - node->scaleY = 0.0f; - - phoneChatVisibleBubbles_.push_back({slotName, node->height}); - uiManager.setNodeVisible(slotName, true); + if (chatIndex < 0 || chatIndex > 2) return; + for (const auto& msg : chatHistory_[chatIndex]) { + const bool inc = msg.incoming; + const std::string nodeName = uiManager.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, zipFile_); + if (!nodeName.empty()) { + auto n = uiManager.findNode(nodeName); + phoneChatVisibleBubbles_.push_back({ nodeName, n ? n->height : 60.0f }); + } + } 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(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) diff --git a/src/MenuManager.h b/src/MenuManager.h index 2764579..5e54e46 100644 --- a/src/MenuManager.h +++ b/src/MenuManager.h @@ -58,7 +58,6 @@ namespace ZL { void openPhoneScreen(); void closePhoneScreen(); - void revealPhoneChatBubble(const std::string& slotName); bool isPhoneScreenOpen() const { return state == GameState::PhoneScreen; } void closePhoneEntirely(); @@ -71,6 +70,9 @@ namespace ZL { std::function startDarklandsTransitionFunc; std::function 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 advanceTutorialStep(); void onItemPickedUp(const std::string& itemId); @@ -102,10 +104,11 @@ namespace ZL { void refreshChatUnreadIndicators(); void resetPhoneChatNodes(); void recomputePhoneChatPositions(); - void openPhoneChatFromList(std::shared_ptr chatRoot, const std::string& dialogueId); + void openPhoneChatFromList(int chatIndex, std::shared_ptr chatRoot, const std::string& dialogueId); void returnToPhoneChatList(); void closePhoneScreenFromChat(); void applyUniIntHud(); + void rebuildChatBubblesFromHistory(int chatIndex); GameState state = GameState::Gameplay; Inventory* inventory = nullptr; @@ -176,6 +179,15 @@ namespace ZL { }; std::vector phoneChatVisibleBubbles_; + // Per-chat message history (max 5 messages each) + struct StoredChatMessage { std::string text; bool incoming; }; + std::vector chatHistory_[3]; + int activeChatIndex_ = -1; + + // Preloaded bubble textures + std::shared_ptr texBubbleInCenter_, texBubbleInLT_, texBubbleInLB_, texBubbleInRT_, texBubbleInRB_; + std::shared_ptr texBubbleOutCenter_, texBubbleOutLT_, texBubbleOutLB_, texBubbleOutRT_, texBubbleOutRB_; + static constexpr float CHAT_TOP_Y = 610.0f; static constexpr float CHAT_BOTTOM_Y = 290.0f; static constexpr float CHAT_SPACING = 10.0f; diff --git a/src/UiManager.cpp b/src/UiManager.cpp index 111c69b..05cb65c 100644 --- a/src/UiManager.cpp +++ b/src/UiManager.cpp @@ -824,6 +824,107 @@ namespace ZL { 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(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(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& 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(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 newRoot) { root = newRoot; layoutNode( @@ -841,6 +942,7 @@ namespace ZL { textViews.clear(); textFields.clear(); staticImages.clear(); + chatBubbles.clear(); pulsingNodes.clear(); popInNodes.clear(); collectButtonsAndSliders(root); @@ -1052,6 +1154,12 @@ namespace ZL { node->staticImage->rect = node->screenRect; node->staticImage->buildMesh(); } + + // 6. Chat bubble + if (node->chatBubble) { + node->chatBubble->rect = node->screenRect; + node->chatBubble->buildMeshes(); + } } void UiManager::updateAllLayouts() { @@ -1106,6 +1214,9 @@ namespace ZL { if (node->pulseEnabled && node->staticImage) { pulsingNodes.push_back(node); } + if (node->chatBubble) { + chatBubbles.push_back(node->chatBubble); + } for (auto& c : node->children) collectButtonsAndSliders(c); } @@ -1215,6 +1326,7 @@ namespace ZL { prev.textViews = textViews; prev.textFields = textFields; prev.staticImages = staticImages; + prev.chatBubbles = chatBubbles; prev.pulsingNodes = pulsingNodes; prev.pressedButtons = pressedButtons; prev.pressedTextButtons = pressedTextButtons; @@ -1280,6 +1392,7 @@ namespace ZL { textViews = s.textViews; textFields = s.textFields; staticImages = s.staticImages; + chatBubbles = s.chatBubbles; pulsingNodes = s.pulsingNodes; popInNodes = s.popInNodes; pressedButtons = s.pressedButtons; @@ -1347,7 +1460,12 @@ namespace ZL { if (node->staticImage) { node->staticImage->draw(renderer); } - + + // 1b. Chat bubble (composite widget) + if (node->chatBubble) { + node->chatBubble->draw(renderer, node->scaleX, node->scaleY); + } + // 2. Потом кнопки if (node->button) { node->button->draw(renderer); @@ -1994,4 +2112,86 @@ namespace ZL { if (!node) return false; return node->visible; } + + std::string UiManager::addChatBubble( + const std::string& parentNodeName, + const std::string& text, bool incoming, + std::shared_ptr texCenter, + std::shared_ptr texLT, std::shared_ptr texLB, + std::shared_ptr texRT, std::shared_ptr 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(); + 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(); + if (!bubble->textRenderer->init(renderer, fontPath, fontSize, zipFile)) { + std::cerr << "UiManager::addChatBubble: TextRenderer init failed\n"; + } + + // Build the UiNode + auto node = std::make_shared(); + 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& n) { return n && n->chatBubble != nullptr; }), + ch.end()); + + // Remove from chatBubbles collection + chatBubbles.clear(); + } + } // namespace ZL \ No newline at end of file diff --git a/src/UiManager.h b/src/UiManager.h index 29385c9..6be976c 100644 --- a/src/UiManager.h +++ b/src/UiManager.h @@ -245,6 +245,45 @@ namespace ZL { void draw(Renderer& renderer) const; }; + struct UiChatBubble { + std::string name; + UiRect rect; + std::string text; + bool incoming = true; + + std::shared_ptr texCenter; + std::shared_ptr texCornerLT; + std::shared_ptr texCornerLB; + std::shared_ptr texCornerRT; + std::shared_ptr 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; + 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 { std::string name; LayoutType layoutType = LayoutType::Frame; @@ -290,6 +329,7 @@ namespace ZL { std::shared_ptr textView; std::shared_ptr textField; std::shared_ptr staticImage; + std::shared_ptr chatBubble; // Анимации struct AnimStep { @@ -409,6 +449,19 @@ namespace ZL { void clearMenuStack(); 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 texCenter, + std::shared_ptr texLT, std::shared_ptr texLB, + std::shared_ptr texRT, std::shared_ptr 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 startAnimation(const std::string& animName); bool startAnimationOnNode(const std::string& nodeName, const std::string& animName); @@ -462,6 +515,7 @@ namespace ZL { std::vector> textViews; std::vector> textFields; std::vector> staticImages; + std::vector> chatBubbles; std::vector> pulsingNodes; std::vector> popInNodes; @@ -483,6 +537,7 @@ namespace ZL { std::vector> textViews; std::vector> textFields; std::vector> staticImages; + std::vector> chatBubbles; std::vector> pulsingNodes; std::vector> popInNodes; std::map> pressedButtons; diff --git a/src/dialogue/DialogueDatabase.cpp b/src/dialogue/DialogueDatabase.cpp index eddb4ab..47e3a20 100644 --- a/src/dialogue/DialogueDatabase.cpp +++ b/src/dialogue/DialogueDatabase.cpp @@ -105,7 +105,7 @@ Node DialogueDatabase::parseNode(const json& j) { node.falseNext = j.value("falseNext", ""); node.cutsceneId = j.value("cutsceneId", ""); node.luaCallback = j.value("luaCallback", ""); - node.bubbleSlot = j.value("bubbleSlot", ""); + node.chatBubble = j.value("chatBubble", ""); node.questUnlock = j.value("questUnlock", ""); node.questComplete = j.value("questComplete", ""); node.questFail = j.value("questFail", ""); diff --git a/src/dialogue/DialogueRuntime.cpp b/src/dialogue/DialogueRuntime.cpp index f22cd90..ddf4e6b 100644 --- a/src/dialogue/DialogueRuntime.cpp +++ b/src/dialogue/DialogueRuntime.cpp @@ -87,8 +87,8 @@ void DialogueRuntime::setOnCutsceneFadeInComplete(std::function cb) { - onBubbleSlotReady = std::move(cb); +void DialogueRuntime::setOnChatBubbleReady(std::function cb) { + onChatBubbleReady = std::move(cb); } void DialogueRuntime::stop() { @@ -466,8 +466,8 @@ void DialogueRuntime::presentLine(const Node& node) { if (!node.luaCallback.empty() && onDialogueLineStarted) { onDialogueLineStarted(node.luaCallback); } - if (!node.bubbleSlot.empty() && onBubbleSlotReady) { - onBubbleSlotReady(node.bubbleSlot); + if (!node.chatBubble.empty() && onChatBubbleReady) { + onChatBubbleReady(node.text, node.chatBubble == "in"); } } diff --git a/src/dialogue/DialogueRuntime.h b/src/dialogue/DialogueRuntime.h index 0c8ab09..908db16 100644 --- a/src/dialogue/DialogueRuntime.h +++ b/src/dialogue/DialogueRuntime.h @@ -23,7 +23,7 @@ public: void setOnDialogueLineStarted(std::function cb); void setOnCutsceneLineStarted(std::function cb); void setOnCutsceneFadeInComplete(std::function cb); - void setOnBubbleSlotReady(std::function cb); + void setOnChatBubbleReady(std::function cb); void stop(); void update(int deltaMs); @@ -60,7 +60,7 @@ private: std::function onDialogueLineStarted; std::function onCutsceneLineStarted; std::function onCutsceneFadeInComplete; - std::function onBubbleSlotReady; + std::function onChatBubbleReady; std::string activeCutsceneId; bool fadeInCallbackFired = false; diff --git a/src/dialogue/DialogueSystem.cpp b/src/dialogue/DialogueSystem.cpp index 3b9c6d9..9604d53 100644 --- a/src/dialogue/DialogueSystem.cpp +++ b/src/dialogue/DialogueSystem.cpp @@ -140,8 +140,8 @@ void DialogueSystem::setOnCutsceneFadeInComplete(std::function cb) { - runtime.setOnBubbleSlotReady(std::move(cb)); +void DialogueSystem::setOnChatBubbleReady(std::function cb) { + runtime.setOnChatBubbleReady(std::move(cb)); } void DialogueSystem::stopDialogue() { diff --git a/src/dialogue/DialogueSystem.h b/src/dialogue/DialogueSystem.h index 85f1683..4f8ec8e 100644 --- a/src/dialogue/DialogueSystem.h +++ b/src/dialogue/DialogueSystem.h @@ -28,7 +28,7 @@ public: void setOnDialogueLineStarted(std::function cb); void setOnCutsceneLineStarted(std::function cb); void setOnCutsceneFadeInComplete(std::function cb); - void setOnBubbleSlotReady(std::function cb); + void setOnChatBubbleReady(std::function cb); void setOnDialogueAdvanced(std::function cb); void stopDialogue(); diff --git a/src/dialogue/DialogueTypes.h b/src/dialogue/DialogueTypes.h index 1a372e4..d1adaf1 100644 --- a/src/dialogue/DialogueTypes.h +++ b/src/dialogue/DialogueTypes.h @@ -97,8 +97,8 @@ struct Node { // For CutsceneStart std::string cutsceneId; - // Name of the UI node (StaticImage) to reveal in the phone chat when this line is shown - std::string bubbleSlot; + // "in" or "out" — creates a dynamic chat bubble; empty = not a chat message + std::string chatBubble; // Quest actions fired when this line is presented (empty = no action) std::string questUnlock;