shadow-over-bishkek001/src/MenuManager.cpp
2026-06-24 20:58:31 +03:00

1554 lines
53 KiB
C++

#include "MenuManager.h"
#include "Game.h"
#include "render/TextRenderer.h"
#include "utils/Utils.h"
#include <iostream>
#include <fstream>
#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::string trimChatPreview(const std::string& msg, int maxChars = 22) {
int charCount = 0;
int bytePos = 0;
const int len = static_cast<int>(msg.size());
while (bytePos < len && charCount < maxChars) {
const unsigned char c = static_cast<unsigned char>(msg[bytePos]);
if (c < 0x80) bytePos += 1;
else if (c < 0xE0) bytePos += 2;
else if (c < 0xF0) bytePos += 3;
else bytePos += 4;
++charCount;
}
if (bytePos >= len) return msg;
return msg.substr(0, bytePos) + "...";
}
static std::string formatMoney(int amount) {
bool negative = amount < 0;
std::string digits = std::to_string(negative ? -amount : amount);
std::string result;
const int len = static_cast<int>(digits.size());
for (int i = 0; i < len; ++i) {
if (i > 0 && (len - i) % 3 == 0) result += ' ';
result += digits[i];
}
if (negative) result = "-" + result;
return result + " сом";
}
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, GameState& gameState, AudioPlayerAsync& audioPlayer) :
renderer(iRenderer),
gameState_(gameState),
audioPlayer_(audioPlayer)
{
}
void MenuManager::setup(Inventory& inv, const std::string& zipFile) {
inventory = &inv;
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);
hudCutsceneRoot_ = loadUiFromFile("resources/w/ui/hud_cutscene.json", renderer, zipFile);
hudTopHintRoot_ = loadUiFromFile("resources/w/ui/hud_top_hint01.json", renderer, zipFile);
phoneMainRoot = loadUiFromFile("resources/w/ui/screen_phone.json", renderer, zipFile);
phoneMainHintARoot = loadUiFromFile("resources/w/ui/screen_phone_hint001.json", renderer, zipFile);
phoneMainHintBRoot = loadUiFromFile("resources/w/ui/screen_phone_hint002.json", renderer, zipFile);
phoneMainHintABRoot = loadUiFromFile("resources/w/ui/screen_phone_hint001_002.json", renderer, zipFile);
phoneBankRoot = loadUiFromFile("resources/w/ui/screen_phone_bank.json", renderer, zipFile);
phoneVideoRoot = loadUiFromFile("resources/w/ui/screen_phone_video.json", renderer, zipFile);
phoneMapDormRoot = loadUiFromFile("resources/w/ui/screen_phone_map_dorm.json", renderer, zipFile);
phoneMapUniRoot = loadUiFromFile("resources/w/ui/screen_phone_map_uni.json", renderer, zipFile);
phoneChatListRoot = loadUiFromFile("resources/w/ui/screen_phone_chat_list.json", renderer, zipFile);
phoneChatListHintRoot = loadUiFromFile("resources/w/ui/screen_phone_chat_list_hint001.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);
modalMenuRoot = loadUiFromFile("resources/w/ui/screen_modal_menu.json", renderer, zipFile);
newInventoryRoot = loadUiFromFile("resources/w/ui/screen_inventory.json", renderer, zipFile);
questJournalRoot = loadUiFromFile("resources/w/ui/screen_journal.json", renderer, zipFile);
mainMenuRoot = loadUiFromFile("resources/w/ui/screen_main_menu.json", renderer, zipFile);
aboutScreenRoot = loadUiFromFile("resources/w/ui/screen_about.json", renderer, zipFile);
settingsScreenRoot = loadUiFromFile("resources/w/ui/screen_settings.json", renderer, zipFile);
loadSavedGameScreenRoot = loadUiFromFile("resources/w/ui/screen_load.json", renderer, zipFile);
saveGameScreenRoot = loadUiFromFile("resources/w/ui/screen_save.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);
gameState_.questJournal.loadFromFile("resources/quests/quests.json", zipFile);
renderer.textureManager.LoadFromPng("resources/w/ui/img/toast/quest_new001.png", zipFile, true);
renderer.textureManager.LoadFromPng("resources/w/ui/img/toast/quest_completed001.png", zipFile, true);
renderer.textureManager.LoadFromPng("resources/w/ui/img/toast/quest_failed001.png", zipFile, true);
gameState_.questJournal.onQuestUnlocked = [this](const std::string& id) {
const Quest::QuestState* q = gameState_.questJournal.findQuest(id);
if (q) showToast("resources/w/ui/img/toast/quest_new001.png", q->definition.title);
};
gameState_.questJournal.onQuestCompleted = [this](const std::string& id) {
const Quest::QuestState* q = gameState_.questJournal.findQuest(id);
if (q) showToast("resources/w/ui/img/toast/quest_completed001.png", q->definition.title);
};
gameState_.questJournal.onQuestFailed = [this](const std::string& id) {
const Quest::QuestState* q = gameState_.questJournal.findQuest(id);
if (q) showToast("resources/w/ui/img/toast/quest_failed001.png", q->definition.title);
};
const std::string imgDir = "resources/w/ui/img/phone/";
texBubbleInCenter_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_in_center.png", zipFile, true);
texBubbleInLT_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_in_corner_left_top.png", zipFile, true);
texBubbleInLB_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_in_corner_left_bottom.png", zipFile, true);
texBubbleInRT_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_in_corner_right_top.png", zipFile, true);
texBubbleInRB_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_in_corner_right_bottom.png",zipFile, true);
texBubbleOutCenter_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_out_center.png", zipFile, true);
texBubbleOutLT_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_out_corner_left_top.png", zipFile, true);
texBubbleOutLB_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_out_corner_left_bottom.png",zipFile, true);
texBubbleOutRT_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_out_corner_right_top.png", zipFile, true);
texBubbleOutRB_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_out_corner_right_bottom.png",zipFile, true);
showMainMenu();
}
void MenuManager::enterGameplay(bool isFromLoad) {
if (!isFromLoad && uiState_ == GameUiState::MainMenu && startGameFunc) startGameFunc();
uiState_ = GameUiState::Gameplay;
uiManager.replaceRoot(hudRoot);
topUiManager.replaceRoot(hudTopHintRoot_);
if (isFromLoad) {
uiManager.clearMenuStack();
topUiManager.clearMenuStack();
}
applyCurrentHealthBar();
uiManager.setButtonCallback("inventoryButton", [this](const std::string&) {
openInventory();
});
if (uiManager.findButton("settingsButton")) {
uiManager.setButtonCallback("settingsButton", [this](const std::string&) {
showModalMenuScreen();
});
}
if (isFromLoad)
applyHudForCurrentState();
}
void MenuManager::applyHudForCurrentState() {
// Clear the step-0 dialogue hint unless we're actually at step 0.
if (gameState_.tutorialStep != TutorialStep::Step0)
topUiManager.replaceRoot(nullptr);
const std::string& loc = gameState_.currentLocationName;
// Non-dorm locations: reuse the existing location-change logic.
if (loc == "uni_exterior" || loc == "uni_interior") {
onLocationChanged(loc);
return;
}
// Dorm (or pre-first-teleport): select HUD root by tutorial step.
std::shared_ptr<UiNode> nextRoot;
switch (gameState_.tutorialStep) {
case TutorialStep::Step0: nextRoot = hudRoot; break;
case TutorialStep::Step1: nextRoot = hudStep1Root; break;
case TutorialStep::Step2: nextRoot = hudStep2Root; break;
case TutorialStep::Step3: nextRoot = hudStep3Root; break;
case TutorialStep::Step4: nextRoot = hudStep4Root; break;
default: break; // Step5/6 handled below
}
if (nextRoot) {
uiManager.replaceRoot(nextRoot);
applyCurrentHealthBar();
uiManager.setButtonCallback("inventoryButton", [this](const std::string&) { openInventory(); });
if (uiManager.findButton("settingsButton"))
uiManager.setButtonCallback("settingsButton", [this](const std::string&) { showModalMenuScreen(); });
} else {
// Step5 or Step6: pick the right variant and apply hint visibility.
refreshItemPickupHud();
}
}
void MenuManager::showMainMenu() {
if (onResetGame) onResetGame();
audioPlayer_.crossFadeMusicAsync("audio/main menu final.ogg");
uiState_ = GameUiState::MainMenu;
uiManager.clearMenuStack();
topUiManager.replaceRoot(nullptr);
topUiManager.clearMenuStack();
uiManager.replaceRoot(mainMenuRoot);
uiManager.setTextButtonCallback("menuStartButton", [this](const std::string&) {
enterGameplay();
});
uiManager.setTextButtonCallback("menuAboutButton", [this](const std::string&) {
showAboutScreen();
});
uiManager.setTextButtonCallback("menuSettingsButton", [this](const std::string&) {
showSettingsScreen();
});
uiManager.setTextButtonCallback("menuLoadButton", [this](const std::string&) {
showLoadGameScreen();
});
#ifndef EMSCRIPTEN
uiManager.setTextButtonCallback("menuExitButton", [this](const std::string&) {
Environment::exitGameLoop = true;
});
#else
uiManager.setNodeVisible("menuExitButton", false);
#endif
}
void MenuManager::showAboutScreen() {
uiManager.pushMenuFromSavedRoot(aboutScreenRoot);
uiManager.setTextButtonCallback("aboutBackButton", [this](const std::string&) {
uiManager.popMenu();
uiManager.updateAllLayouts();
});
}
void MenuManager::showSettingsScreen() {
uiManager.pushMenuFromSavedRoot(settingsScreenRoot);
uiManager.setTextButtonCallback("settingsBackButton", [this](const std::string&) {
saveSettings();
uiManager.popMenu();
uiManager.updateAllLayouts();
});
// Set initial slider positions before registering callbacks so
// setSliderValue does not fire an unregistered callback.
const float musicFrac = audioPlayer_.getMusicVolume() / 128.0f;
const float soundFrac = audioPlayer_.getSoundVolume() / 128.0f;
uiManager.setSliderValue("musicVolumeSlider", musicFrac);
uiManager.setSliderValue("soundVolumeSlider", soundFrac);
uiManager.setText("musicVolumeText",
"Music volume: " + std::to_string(audioPlayer_.getMusicVolume()));
uiManager.setText("soundVolumeText",
"Sound volume: " + std::to_string(audioPlayer_.getSoundVolume()));
uiManager.setSliderCallback("musicVolumeSlider",
[this](const std::string&, float value) {
const int vol = static_cast<int>(value * 128.0f + 0.5f);
audioPlayer_.setMusicVolume(vol);
uiManager.setText("musicVolumeText",
"Music volume: " + std::to_string(vol));
});
uiManager.setSliderCallback("soundVolumeSlider",
[this](const std::string&, float value) {
const int vol = static_cast<int>(value * 128.0f + 0.5f);
audioPlayer_.setSoundVolume(vol);
uiManager.setText("soundVolumeText",
"Sound volume: " + std::to_string(vol));
});
// Music on/off toggle — helper to sync button text and color from current state
auto refreshMusicToggle = [this]() {
const bool on = audioPlayer_.isMusicEnabled();
uiManager.setTextButtonText("musicToggleButton", on ? "Music: ON" : "Music: OFF");
uiManager.setTextButtonColor("musicToggleButton", on
? std::array<float, 4>{ 0.2f, 0.9f, 0.2f, 1.0f }
: std::array<float, 4>{ 0.9f, 0.2f, 0.2f, 1.0f });
};
auto refreshSoundToggle = [this]() {
const bool on = audioPlayer_.isSoundEnabled();
uiManager.setTextButtonText("soundToggleButton", on ? "Sound: ON" : "Sound: OFF");
uiManager.setTextButtonColor("soundToggleButton", on
? std::array<float, 4>{ 0.2f, 0.9f, 0.2f, 1.0f }
: std::array<float, 4>{ 0.9f, 0.2f, 0.2f, 1.0f });
};
refreshMusicToggle();
refreshSoundToggle();
uiManager.setTextButtonCallback("musicToggleButton", [this](const std::string&) {
audioPlayer_.setMusicEnabled(!audioPlayer_.isMusicEnabled());
const bool on = audioPlayer_.isMusicEnabled();
uiManager.setTextButtonText("musicToggleButton", on ? "Music: ON" : "Music: OFF");
uiManager.setTextButtonColor("musicToggleButton", on
? std::array<float, 4>{ 0.2f, 0.9f, 0.2f, 1.0f }
: std::array<float, 4>{ 0.9f, 0.2f, 0.2f, 1.0f });
});
uiManager.setTextButtonCallback("soundToggleButton", [this](const std::string&) {
audioPlayer_.setSoundEnabled(!audioPlayer_.isSoundEnabled());
const bool on = audioPlayer_.isSoundEnabled();
uiManager.setTextButtonText("soundToggleButton", on ? "Sound: ON" : "Sound: OFF");
uiManager.setTextButtonColor("soundToggleButton", on
? std::array<float, 4>{ 0.2f, 0.9f, 0.2f, 1.0f }
: std::array<float, 4>{ 0.9f, 0.2f, 0.2f, 1.0f });
});
}
void MenuManager::showLoadGameScreen()
{
uiManager.pushMenuFromSavedRoot(loadSavedGameScreenRoot);
uiManager.setTextButtonCallback("loadBackButton", [this](const std::string&) {
uiManager.popMenu();
uiManager.updateAllLayouts();
});
static const char* kSlotButtons[4] = {
"loadSlot1Button", "loadSlot2Button", "loadSlot3Button", "loadSlot4Button"
};
for (int i = 0; i < 4; ++i) {
int slot = i + 1;
if (getSlotInfoFunc) {
SaveSlotInfo info = getSlotInfoFunc(slot);
std::string label = info.empty
? "Empty"
: info.locationName + "\n" + info.savedAt;
uiManager.setTextButtonText(kSlotButtons[i], label);
}
uiManager.setTextButtonCallback(kSlotButtons[i], [this, slot](const std::string&) {
if (onLoadGame) onLoadGame(slot);
enterGameplay(true);
});
}
}
void MenuManager::showSaveGameScreen()
{
uiManager.pushMenuFromSavedRoot(saveGameScreenRoot);
uiManager.setTextButtonCallback("saveBackButton", [this](const std::string&) {
uiManager.popMenu();
uiManager.updateAllLayouts();
});
static const char* kSlotButtons[4] = {
"saveSlot1Button", "saveSlot2Button", "saveSlot3Button", "saveSlot4Button"
};
for (int i = 0; i < 4; ++i) {
int slot = i + 1;
const char* buttonName = kSlotButtons[i];
if (getSlotInfoFunc) {
SaveSlotInfo info = getSlotInfoFunc(slot);
std::string label = info.empty
? "Empty"
: info.locationName + "\n" + info.savedAt;
uiManager.setTextButtonText(buttonName, label);
}
uiManager.setTextButtonCallback(buttonName, [this, slot, buttonName](const std::string&) {
if (onSaveGame) onSaveGame(slot);
if (getSlotInfoFunc) {
SaveSlotInfo info = getSlotInfoFunc(slot);
std::string label = info.empty
? "Empty"
: info.locationName + "\n" + info.savedAt;
uiManager.setTextButtonText(buttonName, label);
}
});
}
}
void MenuManager::openInventory() {
uiState_ = GameUiState::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, CONST_ZIP_FILE, 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, CONST_ZIP_FILE, 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, CONST_ZIP_FILE, 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, CONST_ZIP_FILE, true);
}
uiManager.setText("selectedText", item.name);
uiManager.setText("selectedDescription", item.description);
}
void MenuManager::closeInventory() {
uiState_ = GameUiState::Gameplay;
uiManager.popMenu();
uiManager.updateAllLayouts();
}
void MenuManager::openQuestJournal() {
uiState_ = GameUiState::QuestJournal;
gameState_.tutorialJournalScreenOpened = true;
uiManager.setNodeVisible("hint6b", false);
uiManager.setNodeVisible("hint6barrow", 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() {
uiState_ = GameUiState::Gameplay;
selectedQuestIndex = -1;
visibleQuestIds.clear();
uiManager.popMenu();
uiManager.updateAllLayouts();
}
void MenuManager::toggleQuestJournal() {
std::cout << "[quest] toggleQuestJournal: " << (isQuestJournalOpen() ? "closing" : "opening") << std::endl;
if (uiState_ == GameUiState::QuestJournal) {
closeQuestJournal();
}
else {
if (uiState_ == GameUiState::Inventory) {
closeInventory();
}
openQuestJournal();
}
}
void MenuManager::openPhoneScreen() {
uiState_ = GameUiState::PhoneScreen;
if (gameState_.tutorialNeedOpenTaxiScreen && (gameState_.tutorialPhoneChatScreenOpened == false))
{
uiManager.pushMenuFromSavedRoot(phoneMainHintABRoot);
}
else if (gameState_.tutorialNeedOpenTaxiScreen)
{
uiManager.pushMenuFromSavedRoot(phoneMainHintBRoot);
}
else if (gameState_.tutorialPhoneChatScreenOpened == false)
{
uiManager.pushMenuFromSavedRoot(phoneMainHintARoot);
}
else
{
uiManager.pushMenuFromSavedRoot(phoneMainRoot);
}
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
closePhoneEntirely();
});
uiManager.setButtonCallback("phoneMessenger", [this](const std::string&) {
openPhoneMessenger();
});
uiManager.setButtonCallback("phoneBank", [this](const std::string&) {
openPhoneBank();
});
uiManager.setButtonCallback("phoneVideo", [this](const std::string&) {
openPhoneVideo();
});
uiManager.setButtonCallback("phoneTaxi", [this](const std::string&) {
openPhoneTaxi();
});
if (gameState_.isNight)
{
if (gameState_.isDawn)
{
uiManager.setNodeVisible("phoneTimeDay", false);
uiManager.setNodeVisible("phoneTimeNight", false);
uiManager.setNodeVisible("phoneTimeDawn", true);
}
else
{
uiManager.setNodeVisible("phoneTimeDay", false);
uiManager.setNodeVisible("phoneTimeNight", true);
uiManager.setNodeVisible("phoneTimeDawn", false);
}
}
else
{
uiManager.setNodeVisible("phoneTimeDay", true);
uiManager.setNodeVisible("phoneTimeNight", false);
uiManager.setNodeVisible("phoneTimeDawn", false);
}
}
void MenuManager::openPhoneMessenger() {
if (gameState_.tutorialPhoneChatScreenOpened)
{
uiManager.pushMenuFromSavedRoot(phoneChatListRoot);
}
else
{
uiManager.pushMenuFromSavedRoot(phoneChatListHintRoot);
}
refreshChatUnreadIndicators();
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
closePhoneEntirely();
});
uiManager.setButtonCallback("phoneMain", [this](const std::string&) {});
uiManager.setTextButtonCallback("chat1button", [this](const std::string&) {
gameState_.chatUnread[0] = false;
openPhoneChatFromList(0, phoneChat1Root);
});
uiManager.setTextButtonCallback("chat2button", [this](const std::string&) {
gameState_.chatUnread[1] = false;
openPhoneChatFromList(1, phoneChat2Root);
});
uiManager.setTextButtonCallback("chat3button", [this](const std::string&) {
gameState_.chatUnread[2] = false;
openPhoneChatFromList(2, phoneChat3Root);
});
}
void MenuManager::refreshChatUnreadIndicators() {
static const char* kUnreadNodes[3] = { "chat1Unread", "chat2Unread", "chat3Unread" };
static const char* kMsgNodes[3] = { "chat1msg", "chat2msg", "chat3msg" };
for (int i = 0; i < 3; ++i) {
uiManager.setNodeVisible(kUnreadNodes[i], gameState_.chatUnread[i]);
if (!gameState_.chatPreviewMsg[i].empty())
uiManager.setText(kMsgNodes[i], gameState_.chatPreviewMsg[i]);
}
}
void MenuManager::setChatUnread(int chatIndex, bool unread, const std::string& previewMsg) {
if (chatIndex < 0 || chatIndex > 2) return;
gameState_.chatUnread[chatIndex] = unread;
if (!previewMsg.empty()) {
gameState_.chatPreviewMsg[chatIndex] = trimChatPreview(previewMsg);
} else if (!unread && !gameState_.chatHistory[chatIndex].empty()) {
gameState_.chatPreviewMsg[chatIndex] = trimChatPreview(gameState_.chatHistory[chatIndex].back().text);
}
}
void MenuManager::spendMoney(int amount) {
gameState_.money -= amount;
}
void MenuManager::openPhoneBank() {
uiManager.pushMenuFromSavedRoot(phoneBankRoot);
uiManager.setText("balanceText", formatMoney(gameState_.money));
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
closePhoneEntirely();
});
uiManager.setButtonCallback("buttonBack", [this](const std::string&) {
uiManager.popMenu();
uiManager.updateAllLayouts();
});
}
void MenuManager::openPhoneVideo() {
uiManager.pushMenuFromSavedRoot(phoneVideoRoot);
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
closePhoneEntirely();
});
uiManager.setButtonCallback("buttonBack", [this](const std::string&) {
uiManager.popMenu();
uiManager.updateAllLayouts();
});
uiManager.setButtonCallback("videoSkip", [this](const std::string&) {
closePhoneEntirely();
if (gameState_.isNight)
{
startDialogueFunc("dialog_video002");
}
else
{
auto day = gameState_.globalInts["day"];
if (day == 0)
{
startDialogueFunc("dialog_video003");
}
else
{
startNightTransitionFunc();
}
}
});
}
void MenuManager::openPhoneTaxi() {
if (gameState_.currentLocationName == "uni_interior") {
closePhoneEntirely();
if (startDialogueFunc) startDialogueFunc("dialog_taxi003");
} else if (gameState_.currentLocationName == "uni_exterior") {
openPhoneMapScreen(phoneMapUniRoot);
} else {
openPhoneMapScreen(phoneMapDormRoot);
}
}
void MenuManager::openPhoneMapScreen(std::shared_ptr<UiNode> mapRoot) {
uiManager.pushMenuFromSavedRoot(mapRoot);
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
closePhoneEntirely();
});
uiManager.setButtonCallback("buttonBack", [this](const std::string&) {
uiManager.popMenu();
uiManager.updateAllLayouts();
});
uiManager.setButtonCallback("mapGo", [this](const std::string&) {
gameState_.tutorialNeedOpenTaxiScreen = false;
if (callTaxiFunc)
{
callTaxiFunc();
}
gameState_.money -= 500;
closePhoneEntirely();
if (startDialogueFunc) startDialogueFunc("dialog_taxi002");
});
}
void MenuManager::openPhoneChatFromList(int chatIndex, std::shared_ptr<UiNode> chatRoot) {
activeChatIndex_ = chatIndex;
phoneChatVisibleBubbles_.clear();
gameState_.tutorialPhoneChatScreenOpened = true;
uiManager.pushMenuFromSavedRoot(chatRoot);
rebuildChatBubblesFromHistory(chatIndex);
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
closePhoneScreenFromChat();
});
uiManager.setButtonCallback("phoneMain", [this](const std::string&) {});
uiManager.setTextButtonCallback("chatTitleButton", [this](const std::string&) {
returnToPhoneChatList();
});
if (chatOpenCallback) {
chatOpenCallback(chatIndex);
}
}
void MenuManager::returnToPhoneChatList() {
if (activeChatIndex_ >= 0 && activeChatIndex_ <= 2
&& !gameState_.chatHistory[activeChatIndex_].empty()) {
gameState_.chatPreviewMsg[activeChatIndex_] =
trimChatPreview(gameState_.chatHistory[activeChatIndex_].back().text);
}
activeChatIndex_ = -1;
phoneChatVisibleBubbles_.clear();
uiManager.popMenu();
uiManager.updateAllLayouts();
if (gameState_.tutorialPhoneChatScreenOpened)
{
uiManager.setNodeVisible("hint_m002", false);
}
else
{
uiManager.setNodeVisible("hint_m002", true);
}
refreshChatUnreadIndicators();
}
void MenuManager::closePhoneEntirely() {
if (activeChatIndex_ >= 0 && activeChatIndex_ <= 2
&& !gameState_.chatHistory[activeChatIndex_].empty()) {
gameState_.chatPreviewMsg[activeChatIndex_] =
trimChatPreview(gameState_.chatHistory[activeChatIndex_].back().text);
}
activeChatIndex_ = -1;
uiState_ = GameUiState::Gameplay;
phoneChatVisibleBubbles_.clear();
const int depth = uiManager.menuStackSize();
for (int i = 0; i < depth; ++i) uiManager.popMenu();
uiManager.updateAllLayouts();
if (gameState_.tutorialNeedOpenTaxiScreen)
{
uiManager.setNodeVisible("hint7", true);
uiManager.setNodeVisible("hint7arrow", true);
uiManager.setNodeVisible("hint6a", false);
uiManager.setNodeVisible("hint6aarrow", false);
}
else
{
uiManager.setNodeVisible("hint7", false);
uiManager.setNodeVisible("hint7arrow", false);
if (gameState_.tutorialPhoneChatScreenOpened)
{
uiManager.setNodeVisible("hint6a", false);
uiManager.setNodeVisible("hint6aarrow", false);
}
else
{
uiManager.setNodeVisible("hint6a", true);
uiManager.setNodeVisible("hint6aarrow", true);
}
}
}
void MenuManager::closePhoneScreenFromChat() {
closePhoneEntirely();
}
void MenuManager::closePhoneScreen() {
closePhoneEntirely();
}
void MenuManager::showModalMenuScreen() {
uiManager.pushMenuFromSavedRoot(modalMenuRoot);
uiManager.setButtonCallback("modalExitButton", [this](const std::string&) {
uiManager.popMenu();
uiManager.updateAllLayouts();
});
uiManager.setButtonCallback("menuCloseButton", [this](const std::string&) {
uiManager.popMenu();
uiManager.updateAllLayouts();
});
uiManager.setButtonCallback("menuSaveButton", [this](const std::string&) {
showSaveGameScreen();
});
uiManager.setButtonCallback("menuLoadButton", [this](const std::string&) {
showLoadGameScreen();
});
uiManager.setButtonCallback("menuSettingsButton", [this](const std::string&) {
showSettingsScreen();
});
uiManager.setButtonCallback("menuExitButton", [this](const std::string&) {
uiManager.popMenu();
showMainMenu();
});
}
void MenuManager::tutorialShowTaxiHint()
{
std::cout << "tutorialShowTaxiHint" << std::endl;
gameState_.tutorialNeedOpenTaxiScreen = true;
if (uiState_ == GameUiState::Gameplay)
{
uiManager.setNodeVisible("hint6a", false);
uiManager.setNodeVisible("hint6aarrow", false);
uiManager.setNodeVisible("hint7", true);
uiManager.setNodeVisible("hint7arrow", true);
}
}
// 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 (uiManager.findButton("settingsButton")) {
uiManager.setButtonCallback("settingsButton", [this](const std::string&) {
showModalMenuScreen();
});
}
if (gameState_.tutorialNeedOpenTaxiScreen)
{
uiManager.setNodeVisible("hint6a", false);
uiManager.setNodeVisible("hint6aarrow", false);
uiManager.setNodeVisible("hint7", true);
uiManager.setNodeVisible("hint7arrow", true);
}
else
{
uiManager.setNodeVisible("hint7", false);
uiManager.setNodeVisible("hint7arrow", false);
}
if (gameState_.tutorialPhoneChatScreenOpened) {
uiManager.setNodeVisible("hint6a", false);
uiManager.setNodeVisible("hint6aarrow", false);
}
if (gameState_.tutorialJournalScreenOpened)
{
uiManager.setNodeVisible("hint6b", false);
uiManager.setNodeVisible("hint6barrow", false);
}
if (gameState_.tutorialPhoneChatScreenOpened && gameState_.tutorialJournalScreenOpened) {
gameState_.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(); });
if (uiManager.findButton("settingsButton"))
uiManager.setButtonCallback("settingsButton", [this](const std::string&) { showModalMenuScreen(); });
hideAllToastWidgets();
applyToastsToUi();
}
void MenuManager::onLocationChanged(const std::string& locationName) {
if (uiState_ != GameUiState::Gameplay) return;
gameState_.currentLocationName = locationName;
if (locationName == "uni_exterior") {
uiManager.replaceRoot(gameState_.isDarklands ? hudUniExtDarkRoot : hudUniExtRoot);
applyCurrentHealthBar();
setupGameplayHudCallbacks();
if (gameState_.isDarklands)
{
audioPlayer_.crossFadeMusicAsync("audio/bishkek fight calm.ogg");
}
else if (gameState_.isNight)
{
audioPlayer_.crossFadeMusicAsync("audio/bishkek night.ogg");
}
else
{
audioPlayer_.crossFadeMusicAsync("audio/bishkek univer day.ogg");
}
} else if (locationName == "uni_interior") {
applyUniIntHud();
if (gameState_.isDarklands)
{
audioPlayer_.crossFadeMusicAsync("audio/bishkek fight calm.ogg");
}
else if (gameState_.isNight)
{
audioPlayer_.crossFadeMusicAsync("audio/bishkek night.ogg");
}
else
{
audioPlayer_.crossFadeMusicAsync("audio/bishkek univer day.ogg");
}
} else {
// Returning to dorm: reuse step5ab, suppress already-completed hints
uiManager.replaceRoot(hudStep5abRoot);
applyCurrentHealthBar();
setupStep5Callbacks();
if (gameState_.isNight)
{
audioPlayer_.crossFadeMusicAsync("audio/bishkek night.ogg");
}
else
{
audioPlayer_.crossFadeMusicAsync("audio/obshaga.ogg");
}
}
}
void MenuManager::advanceTutorialStep() {
std::shared_ptr<UiNode> nextRoot;
switch (gameState_.tutorialStep) {
case TutorialStep::Step0:
gameState_.tutorialStep = TutorialStep::Step1;
nextRoot = hudStep1Root;
topUiManager.replaceRoot(nullptr);
break;
case TutorialStep::Step1:
gameState_.tutorialStep = TutorialStep::Step2;
nextRoot = hudStep2Root;
break;
case TutorialStep::Step2:
gameState_.tutorialStep = TutorialStep::Step3;
nextRoot = hudStep3Root;
break;
case TutorialStep::Step3:
gameState_.tutorialStep = TutorialStep::Step4;
nextRoot = hudStep4Root;
if (tutorialUnlockInteractiveObjectsFunc)
{
tutorialUnlockInteractiveObjectsFunc();
tutorialUnlockInteractiveObjectsFunc = nullptr;
}
break;
default:
return; // Step4/Step5 transitions are driven by onItemPickedUp
}
if (uiState_ == GameUiState::Gameplay && nextRoot) {
uiManager.replaceRoot(nextRoot);
applyCurrentHealthBar();
uiManager.setButtonCallback("inventoryButton", [this](const std::string&) {
openInventory();
});
if (uiManager.findButton("settingsButton")) {
uiManager.setButtonCallback("settingsButton", [this](const std::string&) {
showModalMenuScreen();
});
}
}
}
void MenuManager::onItemPickedUp(const std::string& itemId) {
if (itemId == "note_spell" && gameState_.uniIntTutorialState == UniIntTutorialState::Step10) {
gameState_.uniIntTutorialState = UniIntTutorialState::Step11;
if (gameState_.currentLocationName == "uni_interior" && uiState_ == GameUiState::Gameplay)
applyUniIntHud();
}
if (gameState_.tutorialStep != TutorialStep::Step4 && gameState_.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 (!gameState_.currentLocationName.empty() && gameState_.currentLocationName != "location_dorm") {
return;
}
if (itemId == "phone") gameState_.tutorialPhonePickedUp = true;
if (itemId == "journal") gameState_.tutorialJournalPickedUp = true;
if (gameState_.tutorialStep == TutorialStep::Step4) {
gameState_.tutorialStep = TutorialStep::Step5;
}
refreshItemPickupHud();
}
void MenuManager::refreshItemPickupHud() {
if (uiState_ != GameUiState::Gameplay) return;
std::shared_ptr<UiNode> nextRoot;
if (gameState_.tutorialPhonePickedUp && gameState_.tutorialJournalPickedUp) {
nextRoot = hudStep5abRoot;
} else if (gameState_.tutorialPhonePickedUp) {
nextRoot = hudStep5aRoot;
} else if (gameState_.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 = gameState_.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 = gameState_.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::rebuildChatBubblesFromHistory(int chatIndex) {
uiManager.clearChatBubbles("chatMessagesContainer");
phoneChatVisibleBubbles_.clear();
if (chatIndex < 0 || chatIndex > 2) return;
for (const auto& msg : gameState_.chatHistory[chatIndex]) {
const bool inc = msg.incoming;
const std::string nodeName = uiManager.addChatBubble(
"chatMessagesContainer", msg.text, inc,
inc ? texBubbleInCenter_ : texBubbleOutCenter_,
inc ? texBubbleInLT_ : texBubbleOutLT_,
inc ? texBubbleInLB_ : texBubbleOutLB_,
inc ? texBubbleInRT_ : texBubbleOutRT_,
inc ? texBubbleInRB_ : texBubbleOutRB_,
renderer, "resources/fonts/DroidSans.ttf", 20, CONST_ZIP_FILE);
if (!nodeName.empty()) {
auto n = uiManager.findNode(nodeName);
phoneChatVisibleBubbles_.push_back({ nodeName, n ? n->height : 60.0f });
}
}
recomputePhoneChatPositions();
}
void MenuManager::onChatBubbleReady(const std::string& text, bool incoming) {
if (activeChatIndex_ < 0) return;
auto& history = gameState_.chatHistory[activeChatIndex_];
if (static_cast<int>(history.size()) >= 5) {
history.erase(history.begin());
}
history.push_back({ text, incoming });
if (uiState_ != GameUiState::PhoneScreen) return;
const std::string nodeName = uiManager.addChatBubble(
"chatMessagesContainer", text, incoming,
incoming ? texBubbleInCenter_ : texBubbleOutCenter_,
incoming ? texBubbleInLT_ : texBubbleOutLT_,
incoming ? texBubbleInLB_ : texBubbleOutLB_,
incoming ? texBubbleInRT_ : texBubbleOutRT_,
incoming ? texBubbleInRB_ : texBubbleOutRB_,
renderer, "resources/fonts/DroidSans.ttf", 20, CONST_ZIP_FILE);
if (nodeName.empty()) return;
auto node = uiManager.findNode(nodeName);
if (node) {
node->scaleX = 0.0f;
node->scaleY = 0.0f;
phoneChatVisibleBubbles_.push_back({ nodeName, node->height });
}
recomputePhoneChatPositions();
uiManager.startPopIn(nodeName, 300.0f);
}
void MenuManager::setDarklandsMode(bool enabled)
{
std::cout << "MenuManager::setDarklandsMode called" << std::endl;
if (gameState_.currentLocationName == "uni_interior") {
if (enabled && gameState_.uniIntTutorialState == UniIntTutorialState::Step11) {
gameState_.uniIntTutorialState = UniIntTutorialState::DarklandsActive;
}
applyUniIntHud();
} else if (gameState_.currentLocationName == "uni_exterior") {
if (uiState_ == GameUiState::Gameplay) {
uiManager.replaceRoot(enabled ? hudUniExtDarkRoot : hudUniExtRoot);
applyCurrentHealthBar();
setupGameplayHudCallbacks();
}
} else {
uiManager.setNodeVisible("darklandsButton", !enabled);
uiManager.setNodeVisible("phoneButton", !enabled);
}
}
void MenuManager::applyUniIntHud()
{
if (uiState_ != GameUiState::Gameplay) return;
std::shared_ptr<UiNode> root;
if (gameState_.isDarklands) {
switch (gameState_.uniIntTutorialState) {
case UniIntTutorialState::DarklandsStep13: root = hudUniIntStep13Root; break;
case UniIntTutorialState::DarklandsFull: root = hudUniIntDarkFullRoot; break;
default: root = hudUniIntStep12Root; break;
}
} else {
switch (gameState_.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 (gameState_.currentLocationName == "uni_interior"
&& gameState_.isDarklands
&& gameState_.uniIntTutorialState == UniIntTutorialState::DarklandsActive) {
uiManager.setNodeVisible("hint_darklands003", false);
uiManager.setNodeVisible("hint_darklands003_arrow", false);
}
}
void MenuManager::advanceUniIntDarklandsHud()
{
if (gameState_.uniIntTutorialState != UniIntTutorialState::DarklandsActive) return;
gameState_.uniIntTutorialState = UniIntTutorialState::DarklandsStep13;
if (gameState_.currentLocationName == "uni_interior" && gameState_.isDarklands && uiState_ == GameUiState::Gameplay)
applyUniIntHud();
}
void MenuManager::onEnemyKilledInUniInterior()
{
if (gameState_.uniIntTutorialState != UniIntTutorialState::DarklandsActive
&& gameState_.uniIntTutorialState != UniIntTutorialState::DarklandsStep13) return;
gameState_.uniIntTutorialState = UniIntTutorialState::DarklandsFull;
if (gameState_.currentLocationName == "uni_interior" && gameState_.isDarklands && uiState_ == GameUiState::Gameplay)
applyUniIntHud();
}
void MenuManager::updateHealthBar(float hp, float maxHp) {
gameState_.playerHp = hp;
gameState_.playerMaxHp = maxHp;
applyCurrentHealthBar();
}
void MenuManager::onCutsceneStarted() {
cutsceneHudActive_ = true;
topUiManager.replaceRoot(hudCutsceneRoot_);
topUiManager.setButtonCallback("skipButton", [this](const std::string&) {
if (skipCutsceneFunc) skipCutsceneFunc();
});
}
void MenuManager::onCutsceneFinished() {
cutsceneHudActive_ = false;
topUiManager.replaceRoot(nullptr);
if (uiState_ == GameUiState::Gameplay)
onLocationChanged(gameState_.currentLocationName);
}
void MenuManager::applyCurrentHealthBar() {
std::cout << "MenuManager::applyCurrentHealthBar called step 1" << std::endl;
std::cout << "currentPlayerMaxHp_ is " << gameState_.playerMaxHp << std::endl;
if (gameState_.playerMaxHp <= 0.f) return;
std::cout << "MenuManager::applyCurrentHealthBar called step 2" << std::endl;
std::cout << "currentPlayerHp_ is " << gameState_.playerHp << std::endl;
const float fraction = std::clamp(gameState_.playerHp / gameState_.playerMaxHp, 0.f, 1.f);
uiManager.setSliderValue("healthBarFill", fraction);
std::string hpText = std::to_string(static_cast<int>(gameState_.playerHp)) + "/" +
std::to_string(static_cast<int>(gameState_.playerMaxHp));
if (hpText.size() < 7) hpText.insert(0, 7 - hpText.size(), ' ');
uiManager.setText("healthValue", hpText);
}
// ---- Toast notification system ----
float MenuManager::ToastEntry::currentAlpha() const {
switch (state) {
case State::FadeIn: return min(timer / TOAST_FADE_MS, 1.0f);
case State::Visible: return 1.0f;
case State::FadeOut: return max(1.0f - timer / TOAST_FADE_MS, 0.0f);
}
return 0.0f;
}
void MenuManager::showToast(const std::string& iconPath, const std::string& text) {
if (activeToasts_.size() < 3) {
ToastEntry e;
e.iconPath = iconPath;
e.text = text;
activeToasts_.push_back(std::move(e));
} else {
toastQueue_.push_back({ iconPath, text });
}
applyToastsToUi();
}
void MenuManager::updateToasts(float deltaMs) {
// Advance timers and state transitions
for (auto& e : activeToasts_) {
e.timer += deltaMs;
if (e.state == ToastEntry::State::FadeIn && e.timer >= TOAST_FADE_MS) {
e.state = ToastEntry::State::Visible;
e.timer = 0.0f;
} else if (e.state == ToastEntry::State::Visible && e.timer >= TOAST_VISIBLE_MS) {
e.state = ToastEntry::State::FadeOut;
e.timer = 0.0f;
}
}
// Remove completed fade-outs
activeToasts_.erase(
std::remove_if(activeToasts_.begin(), activeToasts_.end(),
[](const ToastEntry& e) {
return e.state == ToastEntry::State::FadeOut && e.timer >= TOAST_FADE_MS;
}),
activeToasts_.end());
// Promote queued messages into free slots
while (!toastQueue_.empty() && activeToasts_.size() < 3) {
ToastEntry e;
e.iconPath = toastQueue_.front().iconPath;
e.text = toastQueue_.front().text;
toastQueue_.pop_front();
activeToasts_.push_back(std::move(e));
}
// Always sync UI — even with no active toasts, this hides stale widgets
// that couldn't be hidden while a pushed menu (inventory/journal/phone) was active.
applyToastsToUi();
}
void MenuManager::applyToastsToUi() {
static const char* imgNames[3] = { "toast001", "toast002", "toast003" };
static const char* txtNames[3] = { "toast001text", "toast002text", "toast003text" };
for (int i = 0; i < 3; ++i) {
if (i < static_cast<int>(activeToasts_.size())) {
const ToastEntry& e = activeToasts_[i];
const float a = e.currentAlpha();
auto img = uiManager.findStaticImage(imgNames[i]);
auto tv = uiManager.findTextView(txtNames[i]);
if (img) {
img->texture = renderer.textureManager.LoadFromPng(e.iconPath, CONST_ZIP_FILE, true);
img->alpha = a;
}
if (tv) {
tv->color[3] = a;
}
uiManager.setText(txtNames[i], e.text);
uiManager.setNodeVisible(imgNames[i], true);
uiManager.setNodeVisible(txtNames[i], true);
} else {
uiManager.setNodeVisible(imgNames[i], false);
uiManager.setNodeVisible(txtNames[i], false);
}
}
}
void MenuManager::hideAllToastWidgets() {
static const char* names[6] = {
"toast001", "toast001text",
"toast002", "toast002text",
"toast003", "toast003text"
};
for (const char* name : names)
uiManager.setNodeVisible(name, false);
}
void MenuManager::update(float deltaMs) {
updateToasts(deltaMs);
}
void MenuManager::saveSettings() {
nlohmann::json root;
root["musicVolume"] = audioPlayer_.getMusicVolume();
root["soundVolume"] = audioPlayer_.getSoundVolume();
root["musicEnabled"] = audioPlayer_.isMusicEnabled();
root["soundEnabled"] = audioPlayer_.isSoundEnabled();
std::ofstream file("settings.json");
if (file.is_open()) {
file << root.dump(2);
} else {
std::cerr << "[settings] Could not open settings.json for writing" << std::endl;
}
}
void MenuManager::loadSettings() {
const std::string content = ZL::readTextFile("settings.json");
if (content.empty()) return;
try {
const nlohmann::json root = nlohmann::json::parse(content);
if (root.contains("musicVolume"))
audioPlayer_.setMusicVolume(root["musicVolume"].get<int>());
if (root.contains("soundVolume"))
audioPlayer_.setSoundVolume(root["soundVolume"].get<int>());
if (root.contains("musicEnabled"))
audioPlayer_.setMusicEnabled(root["musicEnabled"].get<bool>());
if (root.contains("soundEnabled"))
audioPlayer_.setSoundEnabled(root["soundEnabled"].get<bool>());
} catch (const std::exception& e) {
std::cerr << "[settings] Failed to parse settings.json: " << e.what() << std::endl;
}
}
} // namespace ZL