Working on stuff

This commit is contained in:
Vladislav Khorev 2026-05-29 14:05:16 +03:00
parent c659293bf8
commit 31130ec248
5 changed files with 315 additions and 105 deletions

View File

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

View File

@ -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,

View File

@ -1,4 +1,5 @@
#include "MenuManager.h"
#include "render/TextRenderer.h"
#include <iostream>
#include <algorithm>
#include <string>
@ -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<float, 4> questStatusColor(Quest::QuestStatus status) {
switch (status) {
case Quest::QuestStatus::Completed: return { 0.25f, 0.95f, 0.35f, 1.0f };
@ -43,9 +82,13 @@ namespace ZL {
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);
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,15 +147,21 @@ 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);
});
}
@ -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);
@ -271,24 +304,55 @@ namespace ZL {
return a->orderIndex > b->orderIndex;
});
for (int i = 0; i < 9; ++i) {
const std::string slotName = "quest_slot_" + std::to_string(i);
static const char* kItemNames[9] = {
"item1name","item2name","item3name",
"item4name","item5name","item6name",
"item7name","item8name","item9name"
};
for (int i = 0; i < 9; ++i) {
if (i < static_cast<int>(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);
std::array<float, 4> 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 };
}
else {
uiManager.setTextButtonText(slotName, "");
uiManager.setNodeVisible(slotName, false);
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) {
for (int i = 0; i < 3; ++i) {
if (i < static_cast<int>(def.objectives.size())) {
const auto& obj = def.objectives[i];
const bool isActive = static_cast<int>(i) == quest->activeObjectiveIndex;
const std::string mark = obj.completed ? "[x] " : (isActive ? "> [ ] " : "[ ] ");
objectivesText += mark + obj.text;
if (i + 1 < def.objectives.size()) {
objectivesText += "\n";
}
}
uiManager.setText("quest_objectives_text", objectivesText);
const bool isActive = (i == quest->activeObjectiveIndex);
uiManager.setText("quest_lore_title_text", "Описание задания");
uiManager.setText("quest_description_text", def.description);
auto img = uiManager.findStaticImage(kCheckboxes[i]);
if (img) img->texture = obj.completed ? texObjectiveCompleted_ : texObjectiveBlank_;
std::array<float, 4> 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_description", def.description);
refreshQuestJournalUi();
}

View File

@ -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<UiNode> hudStep5bRoot;
std::shared_ptr<UiNode> hudStep5abRoot;
std::shared_ptr<UiNode> phoneScreenRoot;
std::shared_ptr<UiNode> journalScreenRoot;
std::shared_ptr<UiNode> newInventoryRoot;
std::shared_ptr<UiNode> questJournalRoot;
std::shared_ptr<Texture> texObjectiveCompleted_;
std::shared_ptr<Texture> texObjectiveBlank_;
std::shared_ptr<Texture> texItemSelected_;
std::shared_ptr<Texture> texItemTransparent_;
int selectedQuestIndex = -1;
std::vector<std::string> visibleQuestIds;
};

View File

@ -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);
}