#include "MenuManager.h" #include "render/TextRenderer.h" #include #include #include 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 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 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 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(quests.size())) { const auto* quest = quests[i]; visibleQuestIds.push_back(quest->definition.id); const bool selected = (i == selectedQuestIndex); 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) { if (index < 0 || index >= static_cast(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(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_description", def.description); refreshQuestJournalUi(); } } // namespace ZL