shadow-over-bishkek001/src/MenuManager.cpp
2026-05-29 21:18:44 +03:00

416 lines
13 KiB
C++

#include "MenuManager.h"
#include "render/TextRenderer.h"
#include <iostream>
#include <algorithm>
#include <string>
namespace ZL {
static int questStatusPriority(Quest::QuestStatus status) {
switch (status) {
case Quest::QuestStatus::Active: return 0;
case Quest::QuestStatus::Available: return 1;
case Quest::QuestStatus::Completed: return 2;
case Quest::QuestStatus::Failed: return 3;
default: return 4;
}
}
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 };
case Quest::QuestStatus::Failed: return { 1.0f, 0.25f, 0.25f, 1.0f };
case Quest::QuestStatus::Active: return { 1.0f, 1.0f, 1.0f, 1.0f };
case Quest::QuestStatus::Available: return { 0.86f, 0.86f, 0.86f, 1.0f };
default: return { 0.45f, 0.45f, 0.45f, 1.0f };
}
}
MenuManager::MenuManager(Renderer& iRenderer) :
renderer(iRenderer)
{
}
void MenuManager::setup(Inventory& inv, const std::string& zipFile) {
inventory = &inv;
//hudRoot = loadUiFromFile("resources/config2/hud.json", renderer, zipFile);
hudRoot = loadUiFromFile("resources/w/ui/hud_step0.json", renderer, zipFile);
hudStep1Root = loadUiFromFile("resources/w/ui/hud_step1.json", renderer, zipFile);
hudStep2Root = loadUiFromFile("resources/w/ui/hud_step2.json", renderer, zipFile);
hudStep3Root = loadUiFromFile("resources/w/ui/hud_step3.json", renderer, zipFile);
hudStep4Root = loadUiFromFile("resources/w/ui/hud_step4.json", renderer, zipFile);
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_chat_list.json", renderer, zipFile);
phoneScreenRoot = loadUiFromFile("resources/w/ui/screen_phone_chat.json", renderer, zipFile);
newInventoryRoot = loadUiFromFile("resources/w/ui/screen_inventory.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);
enterGameplay();
}
void MenuManager::enterGameplay() {
state = GameState::Gameplay;
uiManager.replaceRoot(hudRoot);
/*
uiManager.setTextButtonCallback("inventory_button", [this](const std::string&) {
openInventory();
});
uiManager.setTextButtonCallback("quest_journal_button", [this](const std::string&) {
openQuestJournal();
});*/
uiManager.setButtonCallback("inventoryButton", [this](const std::string&) {
openInventory();
});
//openInventory()
}
void MenuManager::openInventory() {
state = GameState::Inventory;
uiManager.pushMenuFromSavedRoot(newInventoryRoot);
uiManager.setButtonCallback("inventoryExitButton", [this](const std::string&) {
closeInventory();
});
/*
uiManager.pushMenuFromSavedRoot(inventoryRoot);
uiManager.setTextButtonCallback("close_inventory_button", [this](const std::string&) {
closeInventory();
});
const auto& items = inventory->getItems();
std::string itemText;
if (items.empty()) {
itemText = "Inventory (Empty)";
}
else {
itemText = "Inventory (" + std::to_string(items.size()) + " items)\n\n";
for (size_t i = 0; i < items.size(); ++i) {
itemText += std::to_string(i + 1) + ". " + items[i].name + "\n";
}
}
uiManager.setText("inventory_items_text", itemText);*/
}
void MenuManager::closeInventory() {
state = GameState::Gameplay;
uiManager.popMenu();
}
void MenuManager::openQuestJournal() {
state = GameState::QuestJournal;
tutorialJournalScreenOpened = true;
uiManager.setNodeVisible("hint6b", false);
uiManager.pushMenuFromSavedRoot(questJournalRoot);
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) {
uiManager.setTextButtonCallback(kItemNames[i], [this, i](const std::string&) {
selectQuestByIndex(i);
});
}
refreshQuestJournalUi();
if (!visibleQuestIds.empty()) {
selectQuestByIndex(0);
}
}
void MenuManager::closeQuestJournal() {
state = GameState::Gameplay;
selectedQuestIndex = -1;
visibleQuestIds.clear();
uiManager.popMenu();
}
void MenuManager::toggleQuestJournal() {
std::cout << "[quest] toggleQuestJournal: " << (isQuestJournalOpen() ? "closing" : "opening") << std::endl;
if (state == GameState::QuestJournal) {
closeQuestJournal();
}
else {
if (state == GameState::Inventory) {
closeInventory();
}
openQuestJournal();
}
}
void MenuManager::openPhoneScreen() {
state = GameState::PhoneScreen;
tutorialPhoneScreenOpened = true;
// Hide the phone hint on the current HUD so it stays hidden when we return.
uiManager.setNodeVisible("hint6a", false);
uiManager.pushMenuFromSavedRoot(phoneScreenRoot);
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
closePhoneScreen();
});
uiManager.setButtonCallback("phoneMain", [this](const std::string&) {
//Keep the callback
});
uiManager.setTextButtonCallback("chat1button", [this](const std::string&) {
std::cout << "Hello test " << std::endl;
});
}
void MenuManager::closePhoneScreen() {
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.
void MenuManager::setupStep5Callbacks() {
if (uiManager.findButton("phoneButton")) {
uiManager.setButtonCallback("phoneButton", [this](const std::string&) {
openPhoneScreen();
});
}
if (uiManager.findButton("journalButton")) {
uiManager.setButtonCallback("journalButton", [this](const std::string&) {
openQuestJournal();
});
}
if (tutorialPhoneScreenOpened) uiManager.setNodeVisible("hint6a", false);
if (tutorialJournalScreenOpened) uiManager.setNodeVisible("hint6b", false);
}
void MenuManager::advanceTutorialStep() {
std::shared_ptr<UiNode> nextRoot;
switch (tutorialStep) {
case TutorialStep::Step0:
tutorialStep = TutorialStep::Step1;
nextRoot = hudStep1Root;
break;
case TutorialStep::Step1:
tutorialStep = TutorialStep::Step2;
nextRoot = hudStep2Root;
break;
case TutorialStep::Step2:
tutorialStep = TutorialStep::Step3;
nextRoot = hudStep3Root;
break;
case TutorialStep::Step3:
tutorialStep = TutorialStep::Step4;
nextRoot = hudStep4Root;
break;
default:
return; // Step4/Step5 transitions are driven by onItemPickedUp
}
if (state == GameState::Gameplay && nextRoot) {
uiManager.replaceRoot(nextRoot);
uiManager.setButtonCallback("inventoryButton", [this](const std::string&) {
openInventory();
});
}
}
void MenuManager::onItemPickedUp(const std::string& itemId) {
if (tutorialStep != TutorialStep::Step4 && tutorialStep != TutorialStep::Step5) {
return;
}
if (itemId == "phone") tutorialPhonePickedUp = true;
if (itemId == "journal") tutorialJournalPickedUp = true;
if (tutorialStep == TutorialStep::Step4) {
tutorialStep = TutorialStep::Step5;
}
refreshItemPickupHud();
}
void MenuManager::refreshItemPickupHud() {
if (state != GameState::Gameplay) return;
std::shared_ptr<UiNode> nextRoot;
if (tutorialPhonePickedUp && tutorialJournalPickedUp) {
nextRoot = hudStep5abRoot;
} else if (tutorialPhonePickedUp) {
nextRoot = hudStep5aRoot;
} else if (tutorialJournalPickedUp) {
nextRoot = hudStep5bRoot;
}
if (nextRoot) {
uiManager.replaceRoot(nextRoot);
// Register item-screen buttons and re-apply any already-completed hint visibility.
setupStep5Callbacks();
}
}
void MenuManager::refreshQuestJournalUi() {
visibleQuestIds.clear();
auto quests = questJournal.getVisibleQuests();
std::sort(quests.begin(), quests.end(), [](const Quest::QuestState* a, const Quest::QuestState* b) {
const int pa = questStatusPriority(a->status);
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) {
if (i < static_cast<int>(quests.size())) {
const auto* quest = quests[i];
visibleQuestIds.push_back(quest->definition.id);
const bool selected = (i == selectedQuestIndex);
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 };
}
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) {
if (index < 0 || index >= static_cast<int>(visibleQuestIds.size())) {
return;
}
selectedQuestIndex = index;
Quest::QuestState* quest = questJournal.findQuest(visibleQuestIds[index]);
if (!quest) {
return;
}
const auto& def = quest->definition;
uiManager.setText("quest_title", def.title);
static const char* kCheckboxes[3] = { "objective1checkbox", "objective2checkbox", "objective3checkbox" };
static const char* kObjNames[3] = { "objective1name", "objective2name", "objective3name" };
for (int i = 0; i < 3; ++i) {
if (i < static_cast<int>(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<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();
}
} // namespace ZL