713 lines
24 KiB
C++
713 lines
24 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::Available: return 0;
|
|
case Quest::QuestStatus::Completed: return 1;
|
|
case Quest::QuestStatus::Failed: return 2;
|
|
default: return 3;
|
|
}
|
|
}
|
|
|
|
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::Available: return { 1.0f, 1.0f, 1.0f, 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);
|
|
hudUniExtRoot = loadUiFromFile("resources/w/ui/hud_uni_ext.json", renderer, zipFile);
|
|
hudUniIntStep10Root = loadUiFromFile("resources/w/ui/hud_uni_int_step10.json", renderer, zipFile);
|
|
hudUniIntStep11Root = loadUiFromFile("resources/w/ui/hud_uni_int_step11.json", renderer, zipFile);
|
|
hudUniIntStep12Root = loadUiFromFile("resources/w/ui/hud_uni_int_step12.json", renderer, zipFile);
|
|
hudUniIntStep13Root = loadUiFromFile("resources/w/ui/hud_uni_int_step13.json", renderer, zipFile);
|
|
hudUniIntFullRoot = loadUiFromFile("resources/w/ui/hud_uni_int_full.json", renderer, zipFile);
|
|
hudUniIntDarkFullRoot = loadUiFromFile("resources/w/ui/hud_uni_int_dark_full.json", renderer, zipFile);
|
|
hudUniExtDarkRoot = loadUiFromFile("resources/w/ui/hud_uni_ext_dark.json", renderer, zipFile);
|
|
phoneChatListRoot = loadUiFromFile("resources/w/ui/screen_phone_chat_list.json", renderer, zipFile);
|
|
phoneChat1Root = loadUiFromFile("resources/w/ui/screen_phone_chat1.json", renderer, zipFile);
|
|
phoneChat2Root = loadUiFromFile("resources/w/ui/screen_phone_chat2.json", renderer, zipFile);
|
|
phoneChat3Root = loadUiFromFile("resources/w/ui/screen_phone_chat3.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);
|
|
applyCurrentHealthBar();
|
|
/*
|
|
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();
|
|
});
|
|
|
|
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";
|
|
const int maxSlots = 9;
|
|
|
|
for (int i = 0; i < maxSlots; ++i) {
|
|
const std::string btnName = "item" + std::to_string(i + 1) + "Button";
|
|
if (i < static_cast<int>(items.size())) {
|
|
uiManager.setNodeVisible(btnName, true);
|
|
|
|
auto btn = uiManager.findButton(btnName);
|
|
if (btn) {
|
|
auto tex = renderer.textureManager.LoadFromPng(items[i].icon, zipFile_, true);
|
|
btn->texNormal = btn->texHover = btn->texPressed = tex;
|
|
}
|
|
|
|
uiManager.setButtonCallback(btnName, [this, i](const std::string&) {
|
|
selectInventoryItem(i);
|
|
});
|
|
}
|
|
else {
|
|
uiManager.setNodeVisible(btnName, false);
|
|
}
|
|
}
|
|
|
|
inventorySelectedIndex_ = -1;
|
|
if (!items.empty()) {
|
|
selectInventoryItem(0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void MenuManager::selectInventoryItem(int index) {
|
|
const auto& items = inventory->getItems();
|
|
if (index < 0 || index >= static_cast<int>(items.size())) return;
|
|
|
|
// Revert previously selected button to its regular icon
|
|
if (inventorySelectedIndex_ >= 0 && inventorySelectedIndex_ < static_cast<int>(items.size())) {
|
|
const std::string prevBtnName = "item" + std::to_string(inventorySelectedIndex_ + 1) + "Button";
|
|
auto prevBtn = uiManager.findButton(prevBtnName);
|
|
if (prevBtn) {
|
|
auto tex = renderer.textureManager.LoadFromPng(items[inventorySelectedIndex_].icon, zipFile_, true);
|
|
prevBtn->texNormal = prevBtn->texHover = prevBtn->texPressed = tex;
|
|
}
|
|
}
|
|
|
|
inventorySelectedIndex_ = index;
|
|
const auto& item = items[index];
|
|
|
|
// Highlight newly selected button with its selected icon
|
|
const std::string btnName = "item" + std::to_string(index + 1) + "Button";
|
|
auto btn = uiManager.findButton(btnName);
|
|
if (btn) {
|
|
const std::string& selPath = item.selectedIcon.empty() ? item.icon : item.selectedIcon;
|
|
auto tex = renderer.textureManager.LoadFromPng(selPath, zipFile_, true);
|
|
btn->texNormal = btn->texHover = btn->texPressed = tex;
|
|
}
|
|
|
|
// Update the large selected picture on the right panel
|
|
auto img = uiManager.findStaticImage("selectedItemPic");
|
|
if (img) {
|
|
const std::string& path = item.selectedIcon.empty() ? item.icon : item.selectedIcon;
|
|
img->texture = renderer.textureManager.LoadFromPng(path, zipFile_, true);
|
|
}
|
|
|
|
uiManager.setText("selectedText", item.name);
|
|
uiManager.setText("selectedDescription", item.description);
|
|
}
|
|
|
|
|
|
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;
|
|
uiManager.setNodeVisible("hint6a", false);
|
|
uiManager.pushMenuFromSavedRoot(phoneChatListRoot);
|
|
|
|
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
|
|
closePhoneScreen();
|
|
});
|
|
uiManager.setButtonCallback("phoneMain", [this](const std::string&) {});
|
|
uiManager.setTextButtonCallback("chat1button", [this](const std::string&) {
|
|
openPhoneChatFromList(phoneChat1Root, "dialog_chat_aiperi001");
|
|
});
|
|
uiManager.setTextButtonCallback("chat2button", [this](const std::string&) {
|
|
openPhoneChatFromList(phoneChat2Root, "dialog_chat_parents001");
|
|
});
|
|
uiManager.setTextButtonCallback("chat3button", [this](const std::string&) {
|
|
openPhoneChatFromList(phoneChat3Root, "dialog_chat_news001");
|
|
});
|
|
}
|
|
|
|
void MenuManager::openPhoneChatFromList(std::shared_ptr<UiNode> chatRoot, const std::string& dialogueId) {
|
|
phoneChatVisibleBubbles_.clear();
|
|
uiManager.pushMenuFromSavedRoot(chatRoot);
|
|
|
|
const bool firstOpen = dialogueId.empty() || startedDialogues_.find(dialogueId) == startedDialogues_.end();
|
|
if (firstOpen) {
|
|
resetPhoneChatNodes();
|
|
}
|
|
|
|
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
|
|
closePhoneScreenFromChat();
|
|
});
|
|
uiManager.setButtonCallback("phoneMain", [this](const std::string&) {});
|
|
uiManager.setTextButtonCallback("chatTitleButton", [this](const std::string&) {
|
|
returnToPhoneChatList();
|
|
});
|
|
|
|
if (firstOpen && startDialogueFunc && !dialogueId.empty()) {
|
|
startedDialogues_.insert(dialogueId);
|
|
startDialogueFunc(dialogueId);
|
|
}
|
|
}
|
|
|
|
void MenuManager::returnToPhoneChatList() {
|
|
phoneChatVisibleBubbles_.clear();
|
|
uiManager.popMenu();
|
|
}
|
|
|
|
void MenuManager::closePhoneScreenFromChat() {
|
|
state = GameState::Gameplay;
|
|
phoneChatVisibleBubbles_.clear();
|
|
uiManager.popMenu();
|
|
uiManager.popMenu();
|
|
}
|
|
|
|
void MenuManager::closePhoneScreen() {
|
|
state = GameState::Gameplay;
|
|
phoneChatVisibleBubbles_.clear();
|
|
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("inventoryButton")) {
|
|
uiManager.setButtonCallback("inventoryButton", [this](const std::string&) {
|
|
openInventory();
|
|
});
|
|
}
|
|
if (uiManager.findButton("journalButton")) {
|
|
uiManager.setButtonCallback("journalButton", [this](const std::string&) {
|
|
openQuestJournal();
|
|
});
|
|
}
|
|
if (tutorialPhoneScreenOpened) uiManager.setNodeVisible("hint6a", false);
|
|
if (tutorialJournalScreenOpened) uiManager.setNodeVisible("hint6b", false);
|
|
if (tutorialPhoneScreenOpened && tutorialJournalScreenOpened) {
|
|
tutorialStep = TutorialStep::Step6;
|
|
}
|
|
}
|
|
|
|
void MenuManager::setupGameplayHudCallbacks() {
|
|
if (uiManager.findButton("phoneButton"))
|
|
uiManager.setButtonCallback("phoneButton", [this](const std::string&) { openPhoneScreen(); });
|
|
if (uiManager.findButton("inventoryButton"))
|
|
uiManager.setButtonCallback("inventoryButton", [this](const std::string&) { openInventory(); });
|
|
if (uiManager.findButton("journalButton"))
|
|
uiManager.setButtonCallback("journalButton", [this](const std::string&) { openQuestJournal(); });
|
|
if (uiManager.findButton("darklandsButton"))
|
|
uiManager.setButtonCallback("darklandsButton", [this](const std::string&) { startDarklandsTransitionFunc(); });
|
|
}
|
|
|
|
void MenuManager::onLocationChanged(const std::string& locationName) {
|
|
if (state != GameState::Gameplay) return;
|
|
currentLocationName_ = locationName;
|
|
|
|
if (locationName == "uni_exterior") {
|
|
uiManager.replaceRoot(currentIsDarklands_ ? hudUniExtDarkRoot : hudUniExtRoot);
|
|
applyCurrentHealthBar();
|
|
setupGameplayHudCallbacks();
|
|
} else if (locationName == "uni_interior") {
|
|
applyUniIntHud();
|
|
} else {
|
|
// Returning to dorm: reuse step5ab, suppress already-completed hints
|
|
uiManager.replaceRoot(hudStep5abRoot);
|
|
applyCurrentHealthBar();
|
|
setupStep5Callbacks();
|
|
}
|
|
}
|
|
|
|
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);
|
|
applyCurrentHealthBar();
|
|
|
|
uiManager.setButtonCallback("inventoryButton", [this](const std::string&) {
|
|
openInventory();
|
|
});
|
|
}
|
|
}
|
|
|
|
void MenuManager::onItemPickedUp(const std::string& itemId) {
|
|
if (itemId == "note_spell" && uniIntTutorialState_ == UniIntTutorialState::Step10) {
|
|
uniIntTutorialState_ = UniIntTutorialState::Step11;
|
|
if (currentLocationName_ == "uni_interior" && state == GameState::Gameplay)
|
|
applyUniIntHud();
|
|
}
|
|
|
|
if (tutorialStep != TutorialStep::Step4 && tutorialStep != TutorialStep::Step5) {
|
|
return;
|
|
}
|
|
|
|
// Dorm tutorial HUD logic must not run in other locations.
|
|
// currentLocationName_ is empty only before the first teleport (still in dorm).
|
|
if (!currentLocationName_.empty() && currentLocationName_ != "location_dorm") {
|
|
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);
|
|
applyCurrentHealthBar();
|
|
// 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" };
|
|
|
|
std::vector<const Quest::QuestObjective*> visibleObjs;
|
|
for (const auto& obj : def.objectives)
|
|
if (obj.visible) visibleObjs.push_back(&obj);
|
|
|
|
for (int i = 0; i < 3; ++i) {
|
|
if (i < static_cast<int>(visibleObjs.size())) {
|
|
const auto& obj = *visibleObjs[i];
|
|
const bool isActive = (&obj - def.objectives.data() == 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();
|
|
}
|
|
|
|
void MenuManager::resetPhoneChatNodes() {
|
|
static const char* kChatNodes[] = {
|
|
"message01in", "message02out", "message03in", "message04out",
|
|
"message05in", "message06in", "message07in", "message08out",
|
|
"message09in", "message10in", "message11in", nullptr
|
|
};
|
|
for (int i = 0; kChatNodes[i]; ++i) {
|
|
uiManager.setNodeVisible(kChatNodes[i], false);
|
|
auto n = uiManager.findNode(kChatNodes[i]);
|
|
if (n) { n->scaleX = 1.0f; n->scaleY = 1.0f; }
|
|
}
|
|
}
|
|
|
|
void MenuManager::recomputePhoneChatPositions() {
|
|
float totalHeight = 0.0f;
|
|
for (size_t i = 0; i < phoneChatVisibleBubbles_.size(); ++i) {
|
|
totalHeight += phoneChatVisibleBubbles_[i].height;
|
|
if (i > 0) totalHeight += CHAT_SPACING;
|
|
}
|
|
|
|
const float available = CHAT_TOP_Y - CHAT_BOTTOM_Y;
|
|
const float topY = (totalHeight <= available)
|
|
? CHAT_TOP_Y
|
|
: CHAT_BOTTOM_Y + totalHeight;
|
|
|
|
float cursor = topY;
|
|
for (auto& bubble : phoneChatVisibleBubbles_) {
|
|
auto node = uiManager.findNode(bubble.nodeName);
|
|
if (!node) continue;
|
|
node->localY = cursor - bubble.height;
|
|
cursor -= bubble.height + CHAT_SPACING;
|
|
}
|
|
uiManager.updateAllLayouts();
|
|
}
|
|
|
|
void MenuManager::revealPhoneChatBubble(const std::string& slotName) {
|
|
if (state != GameState::PhoneScreen) return;
|
|
auto node = uiManager.findNode(slotName);
|
|
if (!node) return;
|
|
|
|
// 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);
|
|
recomputePhoneChatPositions();
|
|
uiManager.startPopIn(slotName, 300.0f);
|
|
}
|
|
|
|
void MenuManager::setDarklandsMode(bool enabled)
|
|
{
|
|
currentIsDarklands_ = enabled;
|
|
|
|
if (currentLocationName_ == "uni_interior") {
|
|
if (enabled && uniIntTutorialState_ == UniIntTutorialState::Step11) {
|
|
uniIntTutorialState_ = UniIntTutorialState::DarklandsActive;
|
|
}
|
|
applyUniIntHud();
|
|
} else if (currentLocationName_ == "uni_exterior") {
|
|
if (state == GameState::Gameplay) {
|
|
uiManager.replaceRoot(enabled ? hudUniExtDarkRoot : hudUniExtRoot);
|
|
applyCurrentHealthBar();
|
|
setupGameplayHudCallbacks();
|
|
}
|
|
} else {
|
|
uiManager.setNodeVisible("darklandsButton", !enabled);
|
|
uiManager.setNodeVisible("phoneButton", !enabled);
|
|
}
|
|
}
|
|
|
|
void MenuManager::applyUniIntHud()
|
|
{
|
|
if (state != GameState::Gameplay) return;
|
|
std::shared_ptr<UiNode> root;
|
|
if (currentIsDarklands_) {
|
|
switch (uniIntTutorialState_) {
|
|
case UniIntTutorialState::DarklandsStep13: root = hudUniIntStep13Root; break;
|
|
case UniIntTutorialState::DarklandsFull: root = hudUniIntDarkFullRoot; break;
|
|
default: root = hudUniIntStep12Root; break;
|
|
}
|
|
} else {
|
|
switch (uniIntTutorialState_) {
|
|
case UniIntTutorialState::Step10: root = hudUniIntStep10Root; break;
|
|
case UniIntTutorialState::Step11: root = hudUniIntStep11Root; break;
|
|
default: root = hudUniIntFullRoot; break;
|
|
}
|
|
}
|
|
uiManager.replaceRoot(root);
|
|
applyCurrentHealthBar();
|
|
setupGameplayHudCallbacks();
|
|
}
|
|
|
|
void MenuManager::onPlayerStartedWalking()
|
|
{
|
|
if (currentLocationName_ == "uni_interior"
|
|
&& currentIsDarklands_
|
|
&& uniIntTutorialState_ == UniIntTutorialState::DarklandsActive) {
|
|
uiManager.setNodeVisible("hint_darklands003", false);
|
|
uiManager.setNodeVisible("hint_darklands003_arrow", false);
|
|
}
|
|
}
|
|
|
|
void MenuManager::advanceUniIntDarklandsHud()
|
|
{
|
|
if (uniIntTutorialState_ != UniIntTutorialState::DarklandsActive) return;
|
|
uniIntTutorialState_ = UniIntTutorialState::DarklandsStep13;
|
|
if (currentLocationName_ == "uni_interior" && currentIsDarklands_ && state == GameState::Gameplay)
|
|
applyUniIntHud();
|
|
}
|
|
|
|
void MenuManager::onEnemyKilledInUniInterior()
|
|
{
|
|
if (uniIntTutorialState_ != UniIntTutorialState::DarklandsActive
|
|
&& uniIntTutorialState_ != UniIntTutorialState::DarklandsStep13) return;
|
|
uniIntTutorialState_ = UniIntTutorialState::DarklandsFull;
|
|
if (currentLocationName_ == "uni_interior" && currentIsDarklands_ && state == GameState::Gameplay)
|
|
applyUniIntHud();
|
|
}
|
|
|
|
void MenuManager::updateHealthBar(float hp, float maxHp) {
|
|
currentPlayerHp_ = hp;
|
|
currentPlayerMaxHp_ = maxHp;
|
|
applyCurrentHealthBar();
|
|
}
|
|
|
|
void MenuManager::applyCurrentHealthBar() {
|
|
if (currentPlayerMaxHp_ <= 0.f) return;
|
|
const float fraction = std::clamp(currentPlayerHp_ / currentPlayerMaxHp_, 0.f, 1.f);
|
|
uiManager.setSliderValue("healthBarFill", fraction);
|
|
std::string hpText = std::to_string(static_cast<int>(currentPlayerHp_)) + "/" +
|
|
std::to_string(static_cast<int>(currentPlayerMaxHp_));
|
|
if (hpText.size() < 7) hpText.insert(0, 7 - hpText.size(), ' ');
|
|
uiManager.setText("healthValue", hpText);
|
|
}
|
|
|
|
} // namespace ZL
|