From 31130ec248783cd469f2e635dfa838543b5cec80 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Fri, 29 May 2026 14:05:16 +0300 Subject: [PATCH] Working on stuff --- resources/quests/quests.json | 38 ------ resources/w/ui/screen_journal.json | 187 +++++++++++++++++++++++++++-- src/MenuManager.cpp | 180 +++++++++++++++++++-------- src/MenuManager.h | 11 +- src/UiManager.cpp | 4 + 5 files changed, 315 insertions(+), 105 deletions(-) diff --git a/resources/quests/quests.json b/resources/quests/quests.json index f54a982..9c8d997 100644 --- a/resources/quests/quests.json +++ b/resources/quests/quests.json @@ -47,44 +47,6 @@ { "id": "search_tracks", "text": "Найти следы возле каменной ограды", "completed": false }, { "id": "report_elder", "text": "Вернуться к старосте", "completed": false } ] - }, - { - "id": "main_find_ghost", - "title": "Следы призрака", - "category": "Main", - "status": "Active", - "recommendedLevel": 1, - "description": "В заброшенной части деревни появился странный призрак. Местные жители говорят, что он появляется только ночью и исчезает возле старого колодца. Похоже, дух пытается привести кого-то к месту, где много лет назад случилась трагедия.", - "objectives": [ - { "id": "talk_to_ghost", "text": "Поговорить с призраком возле старого дома", "completed": false }, - { "id": "inspect_well", "text": "Осмотреть старый колодец", "completed": false }, - { "id": "return_to_elder", "text": "Вернуться к старосте", "completed": false } - ] - }, - { - "id": "main_black_letter", - "title": "Письмо с чёрной печатью", - "category": "Main", - "status": "Active", - "recommendedLevel": 3, - "description": "В руки героя попало письмо с чёрной восковой печатью. Имя отправителя стёрто, но на бумаге остался запах дыма и дорогих трав. Письмо упоминает встречу у северных ворот и человека, который знает правду о призраке.", - "objectives": [ - { "id": "read_letter", "text": "Прочитать письмо", "completed": true }, - { "id": "go_north_gate", "text": "Добраться до северных ворот", "completed": false }, - { "id": "find_contact", "text": "Найти человека с серебряным кольцом", "completed": false } - ] - }, - { - "id": "side_herbalist_help", - "title": "Травы для лекаря", - "category": "Side", - "status": "Available", - "recommendedLevel": 1, - "description": "Лекарю нужны редкие болотные травы, чтобы приготовить настой для больных детей. Он предупредил, что растение раскрывается только на рассвете, а рядом часто появляются дикие звери.", - "objectives": [ - { "id": "collect_herbs", "text": "Собрать болотные травы на рассвете", "completed": false }, - { "id": "bring_herbs", "text": "Отнести травы лекарю", "completed": false } - ] } ] } diff --git a/resources/w/ui/screen_journal.json b/resources/w/ui/screen_journal.json index ac099ec..8c49f3e 100644 --- a/resources/w/ui/screen_journal.json +++ b/resources/w/ui/screen_journal.json @@ -18,7 +18,7 @@ }, { "type": "Button", - "name": "inventoryExitButton", + "name": "journalExitButton", "horizontal_gravity": "center", "vertical_gravity": "center", "width": 90, @@ -52,7 +52,7 @@ "text": "Главное Задание 1", "textPaddingY": 4.0, "textPaddingX": 16.0, - "fontSize": 32, + "fontSize": 24, "fontPath": "resources/fonts/DroidSans.ttf", "textCentered": false, "topAligned": true, @@ -79,7 +79,7 @@ "text": "Задание 2", "textPaddingY": 4.0, "textPaddingX": 16.0, - "fontSize": 32, + "fontSize": 24, "fontPath": "resources/fonts/DroidSans.ttf", "textCentered": false, "topAligned": true, @@ -106,7 +106,7 @@ "text": "Задание 3", "textPaddingY": 4.0, "textPaddingX": 16.0, - "fontSize": 32, + "fontSize": 24, "fontPath": "resources/fonts/DroidSans.ttf", "textCentered": false, "topAligned": true, @@ -122,6 +122,138 @@ "hover": "resources/w/ui/img/journal/ButtonBkgTransparent001.png", "pressed": "resources/w/ui/img/journal/ButtonBkgTransparent001.png" } + }, + { + "type": "TextButton", + "name": "item4name", + "x": 0.0, + "y": 0.0, + "width": 270.0, + "height": 60.0, + "text": "", + "textPaddingY": 4.0, + "textPaddingX": 16.0, + "fontSize": 24, + "fontPath": "resources/fonts/DroidSans.ttf", + "textCentered": false, + "topAligned": true, + "wrap": true, + "color": [0.996, 0.977, 0.761, 1.0], + "textures": { + "normal": "resources/w/ui/img/journal/ButtonBkgTransparent001.png", + "hover": "resources/w/ui/img/journal/ButtonBkgTransparent001.png", + "pressed": "resources/w/ui/img/journal/ButtonBkgTransparent001.png" + } + }, + { + "type": "TextButton", + "name": "item5name", + "x": 0.0, + "y": 0.0, + "width": 270.0, + "height": 60.0, + "text": "", + "textPaddingY": 4.0, + "textPaddingX": 16.0, + "fontSize": 24, + "fontPath": "resources/fonts/DroidSans.ttf", + "textCentered": false, + "topAligned": true, + "wrap": true, + "color": [0.996, 0.977, 0.761, 1.0], + "textures": { + "normal": "resources/w/ui/img/journal/ButtonBkgTransparent001.png", + "hover": "resources/w/ui/img/journal/ButtonBkgTransparent001.png", + "pressed": "resources/w/ui/img/journal/ButtonBkgTransparent001.png" + } + }, + { + "type": "TextButton", + "name": "item6name", + "x": 0.0, + "y": 0.0, + "width": 270.0, + "height": 60.0, + "text": "", + "textPaddingY": 4.0, + "textPaddingX": 16.0, + "fontSize": 24, + "fontPath": "resources/fonts/DroidSans.ttf", + "textCentered": false, + "topAligned": true, + "wrap": true, + "color": [0.996, 0.977, 0.761, 1.0], + "textures": { + "normal": "resources/w/ui/img/journal/ButtonBkgTransparent001.png", + "hover": "resources/w/ui/img/journal/ButtonBkgTransparent001.png", + "pressed": "resources/w/ui/img/journal/ButtonBkgTransparent001.png" + } + }, + { + "type": "TextButton", + "name": "item7name", + "x": 0.0, + "y": 0.0, + "width": 270.0, + "height": 60.0, + "text": "", + "textPaddingY": 4.0, + "textPaddingX": 16.0, + "fontSize": 24, + "fontPath": "resources/fonts/DroidSans.ttf", + "textCentered": false, + "topAligned": true, + "wrap": true, + "color": [0.996, 0.977, 0.761, 1.0], + "textures": { + "normal": "resources/w/ui/img/journal/ButtonBkgTransparent001.png", + "hover": "resources/w/ui/img/journal/ButtonBkgTransparent001.png", + "pressed": "resources/w/ui/img/journal/ButtonBkgTransparent001.png" + } + }, + { + "type": "TextButton", + "name": "item8name", + "x": 0.0, + "y": 0.0, + "width": 270.0, + "height": 60.0, + "text": "", + "textPaddingY": 4.0, + "textPaddingX": 16.0, + "fontSize": 24, + "fontPath": "resources/fonts/DroidSans.ttf", + "textCentered": false, + "topAligned": true, + "wrap": true, + "color": [0.996, 0.977, 0.761, 1.0], + "textures": { + "normal": "resources/w/ui/img/journal/ButtonBkgTransparent001.png", + "hover": "resources/w/ui/img/journal/ButtonBkgTransparent001.png", + "pressed": "resources/w/ui/img/journal/ButtonBkgTransparent001.png" + } + }, + { + "type": "TextButton", + "name": "item9name", + "x": 0.0, + "y": 0.0, + "width": 270.0, + "height": 60.0, + "text": "", + "textPaddingY": 4.0, + "textPaddingX": 16.0, + "fontSize": 24, + "fontPath": "resources/fonts/DroidSans.ttf", + "textCentered": false, + "topAligned": true, + "wrap": true, + "color": [0.996, 0.977, 0.761, 1.0], + "textures": { + "normal": "resources/w/ui/img/journal/ButtonBkgTransparent001.png", + "hover": "resources/w/ui/img/journal/ButtonBkgTransparent001.png", + "pressed": "resources/w/ui/img/journal/ButtonBkgTransparent001.png" + } } ] }, @@ -169,7 +301,7 @@ "children": [ { "type": "StaticImage", - "name": "background", + "name": "objective1checkbox", "width": 40, "height": 40, "texture": "resources/w/ui/img/journal/quest_objective_completed.png" @@ -206,7 +338,7 @@ "children": [ { "type": "StaticImage", - "name": "background", + "name": "objective2checkbox", "width": 40, "height": 40, "texture": "resources/w/ui/img/journal/quest_objective_blank.png" @@ -231,20 +363,57 @@ ] } ] + }, + { + "type": "LinearLayout", + "orientation": "horizontal", + "spacing": 10, + "x": 0, + "y": 0, + "width": 800, + "height": 40, + "children": [ + { + "type": "StaticImage", + "name": "objective3checkbox", + "width": 40, + "height": 40, + "texture": "resources/w/ui/img/journal/quest_objective_blank.png" + }, + { + "type": "TextView", + "name": "objective3name", + "paddingY": 20, + "width": 740.0, + "height": 60.0, + "text": "Цель 3", + "fontSize": 32, + "fontPath": "resources/fonts/DroidSans.ttf", + "textCentered": false, + "topAligned": false, + "wrap": true, + "color": [ + 0.996, + 0.977, + 0.761, + 0.9 + ] + } + ] } ] }, { "type": "TextView", "name": "quest_description", - "x": 170.0, + "x": 140.0, "y": 390.0, - "width": 1000.0, + "width": 950.0, "height": 300.0, "horizontal_gravity": "center", "vertical_gravity": "top", "text": "А все бегут бегут бегут бегут бегут бегут бегут бегут бегут бегут бегут а мы идем.", - "fontSize": 32, + "fontSize": 20, "fontPath": "resources/fonts/DroidSans.ttf", "textCentered": false, "topAligned": false, diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index aebbb42..10b464c 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -1,4 +1,5 @@ #include "MenuManager.h" +#include "render/TextRenderer.h" #include #include #include @@ -15,6 +16,44 @@ namespace ZL { } } + static int countWrappedLines(const std::string& text, const TextRenderer& tr, float maxWidth) { + if (text.empty()) return 1; + int lines = 0; + std::string currentLine; + + auto flushLine = [&]() { ++lines; currentLine.clear(); }; + + auto pushWord = [&](const std::string& word) { + if (word.empty()) return; + if (currentLine.empty()) { + currentLine = word; + } else { + const std::string candidate = currentLine + " " + word; + if (tr.measureTextWidth(candidate) <= maxWidth) { + currentLine = candidate; + } else { + flushLine(); + currentLine = word; + } + } + }; + + std::string currentWord; + for (char ch : text) { + if (ch == '\n') { + pushWord(currentWord); currentWord.clear(); flushLine(); + } else if (ch == ' ' || ch == '\t' || ch == '\r') { + pushWord(currentWord); currentWord.clear(); + } else { + currentWord.push_back(ch); + } + } + pushWord(currentWord); + if (!currentLine.empty()) flushLine(); + + return max(1, lines); + } + static std::array questStatusColor(Quest::QuestStatus status) { switch (status) { case Quest::QuestStatus::Completed: return { 0.25f, 0.95f, 0.35f, 1.0f }; @@ -42,10 +81,14 @@ namespace ZL { hudStep5aRoot = loadUiFromFile("resources/w/ui/hud_step5a.json", renderer, zipFile); hudStep5bRoot = loadUiFromFile("resources/w/ui/hud_step5b.json", renderer, zipFile); hudStep5abRoot = loadUiFromFile("resources/w/ui/hud_step5ab.json", renderer, zipFile); - phoneScreenRoot = loadUiFromFile("resources/w/ui/screen_phone.json", renderer, zipFile); - journalScreenRoot= loadUiFromFile("resources/w/ui/screen_journal.json", renderer, zipFile); + phoneScreenRoot = loadUiFromFile("resources/w/ui/screen_phone.json", renderer, zipFile); newInventoryRoot = loadUiFromFile("resources/w/ui/screen_inventory.json", renderer, zipFile); - questJournalRoot = loadUiFromFile("resources/config2/ui_quest_journal.json", renderer, zipFile); + questJournalRoot = loadUiFromFile("resources/w/ui/screen_journal.json", renderer, zipFile); + + texObjectiveCompleted_ = renderer.textureManager.LoadFromPng("resources/w/ui/img/journal/quest_objective_completed.png", zipFile, true); + texObjectiveBlank_ = renderer.textureManager.LoadFromPng("resources/w/ui/img/journal/quest_objective_blank.png", zipFile, true); + texItemSelected_ = renderer.textureManager.LoadFromPng("resources/w/ui/img/journal/ButtonBkg001.png", zipFile, true); + texItemTransparent_ = renderer.textureManager.LoadFromPng("resources/w/ui/img/journal/ButtonBkgTransparent001.png", zipFile, true); questJournal.loadFromFile("resources/quests/quests.json", zipFile); @@ -104,17 +147,23 @@ namespace ZL { void MenuManager::openQuestJournal() { state = GameState::QuestJournal; + tutorialJournalScreenOpened = true; + uiManager.setNodeVisible("hint6b", false); uiManager.pushMenuFromSavedRoot(questJournalRoot); - uiManager.setTextButtonCallback("quest_close_button", [this](const std::string&) { + uiManager.setButtonCallback("journalExitButton", [this](const std::string&) { closeQuestJournal(); - }); + }); + static const char* kItemNames[9] = { + "item1name","item2name","item3name", + "item4name","item5name","item6name", + "item7name","item8name","item9name" + }; for (int i = 0; i < 9; ++i) { - const std::string slotName = "quest_slot_" + std::to_string(i); - uiManager.setTextButtonCallback(slotName, [this, i](const std::string&) { + uiManager.setTextButtonCallback(kItemNames[i], [this, i](const std::string&) { selectQuestByIndex(i); - }); + }); } refreshQuestJournalUi(); @@ -159,22 +208,6 @@ namespace ZL { uiManager.popMenu(); } - void MenuManager::openJournalScreen() { - state = GameState::JournalScreen; - tutorialJournalScreenOpened = true; - // Hide the journal hint on the current HUD so it stays hidden when we return. - uiManager.setNodeVisible("hint6b", false); - uiManager.pushMenuFromSavedRoot(journalScreenRoot); - uiManager.setButtonCallback("journalExitButton", [this](const std::string&) { - closeJournalScreen(); - }); - } - - void MenuManager::closeJournalScreen() { - state = GameState::Gameplay; - uiManager.popMenu(); - } - // Registers phoneButton / journalButton callbacks on the current HUD root // and hides any hints that have already been completed. // Called after every replaceRoot during step 5. @@ -186,7 +219,7 @@ namespace ZL { } if (uiManager.findButton("journalButton")) { uiManager.setButtonCallback("journalButton", [this](const std::string&) { - openJournalScreen(); + openQuestJournal(); }); } if (tutorialPhoneScreenOpened) uiManager.setNodeVisible("hint6a", false); @@ -269,26 +302,57 @@ namespace ZL { const int pb = questStatusPriority(b->status); if (pa != pb) return pa < pb; return a->orderIndex > b->orderIndex; - }); + }); + + static const char* kItemNames[9] = { + "item1name","item2name","item3name", + "item4name","item5name","item6name", + "item7name","item8name","item9name" + }; for (int i = 0; i < 9; ++i) { - const std::string slotName = "quest_slot_" + std::to_string(i); - if (i < static_cast(quests.size())) { const auto* quest = quests[i]; visibleQuestIds.push_back(quest->definition.id); const bool selected = (i == selectedQuestIndex); - const std::string prefix = selected ? "> " : " "; - uiManager.setTextButtonText(slotName, prefix + quest->definition.title); - uiManager.setTextButtonColor(slotName, questStatusColor(quest->status)); - uiManager.setNodeVisible(slotName, true); - } - else { - uiManager.setTextButtonText(slotName, ""); - uiManager.setNodeVisible(slotName, false); + + std::array color; + if (selected) { + color = { 0.996f, 0.977f, 0.761f, 1.0f }; + } else if (quest->status == Quest::QuestStatus::Completed) { + color = { 0.02f, 0.875f, 0.447f, 0.6f }; + } else if (quest->status == Quest::QuestStatus::Failed) { + color = { 1.0f, 0.25f, 0.25f, 0.6f }; + } else { + color = { 0.996f, 0.977f, 0.761f, 0.7f }; + } + + auto tb = uiManager.findTextButton(kItemNames[i]); + if (tb) { + auto tex = selected ? texItemSelected_ : texItemTransparent_; + tb->texNormal = tb->texHover = tb->texPressed = tex; + } + uiManager.setTextButtonText(kItemNames[i], quest->definition.title); + uiManager.setTextButtonColor(kItemNames[i], color); + uiManager.setNodeVisible(kItemNames[i], true); + + auto node = uiManager.findNode(kItemNames[i]); + if (node && tb && tb->textRenderer) { + const float availW = node->width - 2.0f * tb->textPaddingX; + const int numLines = countWrappedLines(quest->definition.title, *tb->textRenderer, availW); + node->height = numLines * 30.0f + 30.0f; + } + } else { + uiManager.setTextButtonText(kItemNames[i], ""); + uiManager.setNodeVisible(kItemNames[i], false); + + auto node = uiManager.findNode(kItemNames[i]); + if (node) node->height = 60.0f; } } + + uiManager.updateAllLayouts(); } void MenuManager::selectQuestByIndex(int index) { @@ -304,28 +368,38 @@ namespace ZL { const auto& def = quest->definition; - uiManager.setText("quest_middle_title_text", def.title); - uiManager.setTextColor("quest_middle_title_text", questStatusColor(quest->status)); + uiManager.setText("quest_title", def.title); - const std::string meta = std::string("Category: ") + Quest::toString(def.category) - + " | Status: " + Quest::toString(quest->status) - + " | Level: " + std::to_string(def.recommendedLevel); - uiManager.setText("quest_meta_text", meta); + static const char* kCheckboxes[3] = { "objective1checkbox", "objective2checkbox", "objective3checkbox" }; + static const char* kObjNames[3] = { "objective1name", "objective2name", "objective3name" }; - std::string objectivesText; - for (size_t i = 0; i < def.objectives.size(); ++i) { - const auto& obj = def.objectives[i]; - const bool isActive = static_cast(i) == quest->activeObjectiveIndex; - const std::string mark = obj.completed ? "[x] " : (isActive ? "> [ ] " : "[ ] "); - objectivesText += mark + obj.text; - if (i + 1 < def.objectives.size()) { - objectivesText += "\n"; + for (int i = 0; i < 3; ++i) { + if (i < static_cast(def.objectives.size())) { + const auto& obj = def.objectives[i]; + const bool isActive = (i == quest->activeObjectiveIndex); + + auto img = uiManager.findStaticImage(kCheckboxes[i]); + if (img) img->texture = obj.completed ? texObjectiveCompleted_ : texObjectiveBlank_; + + std::array color; + if (obj.completed) { + color = { 0.02f, 0.875f, 0.447f, 0.6f }; + } else if (isActive) { + color = { 0.996f, 0.977f, 0.761f, 1.0f }; + } else { + color = { 0.996f, 0.977f, 0.761f, 0.9f }; + } + uiManager.setText(kObjNames[i], obj.text); + uiManager.setTextColor(kObjNames[i], color); + uiManager.setNodeVisible(kCheckboxes[i], true); + uiManager.setNodeVisible(kObjNames[i], true); + } else { + uiManager.setNodeVisible(kCheckboxes[i], false); + uiManager.setNodeVisible(kObjNames[i], false); } } - uiManager.setText("quest_objectives_text", objectivesText); - uiManager.setText("quest_lore_title_text", "Описание задания"); - uiManager.setText("quest_description_text", def.description); + uiManager.setText("quest_description", def.description); refreshQuestJournalUi(); } diff --git a/src/MenuManager.h b/src/MenuManager.h index 5efa547..abe1735 100644 --- a/src/MenuManager.h +++ b/src/MenuManager.h @@ -17,8 +17,7 @@ namespace ZL { Gameplay, Inventory, QuestJournal, - PhoneScreen, - JournalScreen + PhoneScreen }; enum class TutorialStep { @@ -51,8 +50,6 @@ namespace ZL { void openPhoneScreen(); void closePhoneScreen(); - void openJournalScreen(); - void closeJournalScreen(); void advanceTutorialStep(); void onItemPickedUp(const std::string& itemId); @@ -89,10 +86,14 @@ namespace ZL { std::shared_ptr hudStep5bRoot; std::shared_ptr hudStep5abRoot; std::shared_ptr phoneScreenRoot; - std::shared_ptr journalScreenRoot; std::shared_ptr newInventoryRoot; std::shared_ptr questJournalRoot; + std::shared_ptr texObjectiveCompleted_; + std::shared_ptr texObjectiveBlank_; + std::shared_ptr texItemSelected_; + std::shared_ptr texItemTransparent_; + int selectedQuestIndex = -1; std::vector visibleQuestIds; }; diff --git a/src/UiManager.cpp b/src/UiManager.cpp index f9506fc..707104e 100644 --- a/src/UiManager.cpp +++ b/src/UiManager.cpp @@ -194,6 +194,9 @@ namespace ZL { if (tex && *tex) { renderer.shaderManager.PushShader(defaultShaderName); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + renderer.PushMatrix(); renderer.TranslateMatrix({ animOffsetX, animOffsetY, 0.0f }); renderer.ScaleMatrix({ animScaleX, animScaleY, 1.0f }); @@ -204,6 +207,7 @@ namespace ZL { renderer.PopMatrix(); renderer.shaderManager.PopShader(); + glDisable(GL_BLEND); }