#include "MenuManager.h" #include "Game.h" #include "Localization.h" #include "items/ItemRegistry.h" #include "Campaign.h" #include "render/TextRenderer.h" #include "utils/Utils.h" #include #include #include #include #include #ifdef __EMSCRIPTEN__ #include #endif namespace FRG { #ifdef __EMSCRIPTEN__ //extern Game* Game::s_instance; #endif // Localization static const std::string EMPTY_LANGUAGE_RU = u8"(пусто)"; static const std::string EMPTY_LANGUAGE_EN = "(empty)"; static const std::string BACK_TO_MAIN_MENU_RU = u8"Вернуться в меню"; static const std::string BACK_TO_MAIN_MENU_EN = "Back to Main Menu"; static const std::string MUSIC_DOWNLOAD_ERROR_RU = u8"Ошибка скачивания музыки"; static const std::string MUSIC_DOWNLOAD_NOT_STARTED_RU = u8"Скачать пакет с музыкой (27 MB)."; static const std::string MUSIC_DOWNLOAD_PROGRESS_PREFIX_RU = u8"Загрузка музыки: "; static const std::string MUSIC_DOWNLOAD_ERROR_EN = u8"Music download error"; static const std::string MUSIC_DOWNLOAD_NOT_STARTED_EN = u8"Download music package (27 MB)."; static const std::string MUSIC_DOWNLOAD_PROGRESS_PREFIX_EN = u8"Music download: "; 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(msg.size()); while (bytePos < len && charCount < maxChars) { const unsigned char c = static_cast(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(digits.size()); for (int i = 0; i < len; ++i) { if (i > 0 && (len - i) % 3 == 0) result += ' '; result += digits[i]; } if (negative) result = "-" + result; //Localization if (g_currentLanguage == Language::English) { return result + " som"; } return result + u8" сом"; } 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::Available: return { 1.0f, 1.0f, 1.0f, 1.0f }; default: return { 0.45f, 0.45f, 0.45f, 1.0f }; } } #ifdef __EMSCRIPTEN__ // Коллбэк успешной загрузки music.zip void MenuManager::onMusicZipLoaded(unsigned /*handle*/, void* /*userData*/, const char* /*filename*/) { logger() << "File music.zip downloaded successfully to VFS" << std::endl; FRG::emscriptenSyncFs(); if (Game::s_instance != nullptr) { Game::s_instance->menuManager.musicDownloadProgress = 1.f; Game::s_instance->menuManager.musicDownloadStarted = false; Game::s_instance->menuManager.musicDownloadError = false; logger() << "onMusicZipLoaded step 2: " << Game::s_instance->menuManager.lastMusicTrack << std::endl; if (Game::s_instance->menuManager.lastMusicTrack != "") { logger() << "onMusicZipLoaded step 3" << std::endl; Game::s_instance->menuManager.audioPlayer_.crossFadeMusicAsync(Game::s_instance->menuManager.lastMusicTrack); } logger() << "onMusicZipLoaded step 4" << std::endl; if (Game::s_instance->menuManager.playerInSettingsNoMusic) { if (Game::s_instance->menuManager.playerInSettingsNoMusicGraphics) { Game::s_instance->menuManager.uiManager.popMenu(); Game::s_instance->menuManager.uiManager.popMenu(); Game::s_instance->menuManager.showSettingsScreen(); Game::s_instance->menuManager.showSettingsGraphicsScreen(); Game::s_instance->menuManager.uiManager.updateAllLayouts(); Game::s_instance->menuManager.topUiManager.updateAllLayouts(); } else { Game::s_instance->menuManager.uiManager.popMenu(); Game::s_instance->menuManager.showSettingsScreen(); Game::s_instance->menuManager.uiManager.updateAllLayouts(); Game::s_instance->menuManager.topUiManager.updateAllLayouts(); } } Game::s_instance->menuManager.playerInSettingsNoMusic = false; Game::s_instance->menuManager.playerInSettingsNoMusicGraphics = false; //Game::s_instance->menuManager.uiManager.setText("musicDownloadButton", musicDownloadCompleted); } } void MenuManager::onMusicZipError(unsigned /*handle*/, void* /*userData*/, int httpStatus) { logger() << "Failed to download music.zip, HTTP status: " << httpStatus << std::endl; if (Game::s_instance != nullptr) { std::string musicDownloadError = MUSIC_DOWNLOAD_ERROR_RU; Game::s_instance->menuManager.musicDownloadStarted = false; Game::s_instance->menuManager.musicDownloadError = true; Game::s_instance->menuManager.uiManager.setText("musicDownloadButton", musicDownloadError); } } void MenuManager::onMusicZipProgress(unsigned /*handle*/, void* /*userData*/, int percentComplete) { // Прогресс для музыки при необходимости if (Game::s_instance != nullptr) { std::string musicDownloadProgressPrefix = MUSIC_DOWNLOAD_PROGRESS_PREFIX_RU; Game::s_instance->menuManager.musicDownloadProgress = percentComplete / 100.0f; Game::s_instance->menuManager.uiManager.setText("musicDownloadButton", musicDownloadProgressPrefix + std::to_string(percentComplete) + "%"); } } #endif MenuManager::MenuManager(Renderer& iRenderer, GameState& gameState, AudioPlayerAsync& audioPlayer) : renderer(iRenderer), gameState_(gameState), audioPlayer_(audioPlayer) { //Localization localizedLocationName["uni_interior"][Language::Russian] = u8"Университет, 3 этаж"; localizedLocationName["uni_interior"][Language::English] = "University, 3rd floor"; localizedLocationName["uni_exterior"][Language::Russian] = u8"Университет, двор"; localizedLocationName["uni_exterior"][Language::English] = "University, exterior"; localizedLocationName["location_dorm"][Language::Russian] = u8"Общежитие, 1 этаж"; localizedLocationName["location_dorm"][Language::English] = "Dormitory, 1st floor"; } std::shared_ptr MenuManager::loadLocalizedUi(const std::string& basePath, const std::string& zipFile) { // Service the OS message queue between UI screens so the window doesn't // get flagged as "not responding" while loading the ~50 UI layout files. SDL_PumpEvents(); static const std::string marker = "resources/w/ui/"; if (basePath.rfind(marker, 0) == 0) { const std::string localizedPath = "resources/w/ui_" + languageToCode(g_currentLanguage) + "/" + basePath.substr(marker.size()); try { return loadUiFromFile(localizedPath, renderer, zipFile); } catch (const std::exception&) { // Localized screen not available — fall back to the base path below. } } return loadUiFromFile(basePath, renderer, zipFile); } std::shared_ptr MenuManager::loadLocalizedTexture(const std::string& basePath, const std::string& zipFile) { static const std::string marker = "resources/w/ui/"; if (basePath.rfind(marker, 0) == 0) { const std::string localizedPath = "resources/w/ui_" + languageToCode(g_currentLanguage) + "/" + basePath.substr(marker.size()); try { return renderer.textureManager.LoadFromPng(localizedPath, zipFile, true); } catch (const std::exception&) { // Localized screen not available — fall back to the base path below. } } return renderer.textureManager.LoadFromPng(basePath, zipFile, true); } void MenuManager::loadUiRoots(const std::string& zipFile) { #ifdef __ANDROID__ hudDormRoot = loadLocalizedUi("resources/w/ui/hud_dorm_android.json", zipFile); hudUniIntRoot = loadLocalizedUi("resources/w/ui/hud_uni_int_android.json", zipFile); hudUniIntDarkRoot = loadLocalizedUi("resources/w/ui/hud_uni_int_dark_android.json", zipFile); #else hudDormRoot = loadLocalizedUi("resources/w/ui/hud_dorm.json", zipFile); hudUniIntRoot = loadLocalizedUi("resources/w/ui/hud_uni_int.json", zipFile); hudUniIntDarkRoot = loadLocalizedUi("resources/w/ui/hud_uni_int_dark.json", zipFile); #endif hudUniExtRoot = loadLocalizedUi("resources/w/ui/hud_uni_ext.json", zipFile); hudUniExtDarkRoot = loadLocalizedUi("resources/w/ui/hud_uni_ext_dark.json", zipFile); //hudCutsceneRoot_ = loadLocalizedUi("resources/w/ui/hud_cutscene.json", zipFile); hudCutsceneRoot_ = loadUiFromFile("resources/w/ui/hud_cutscene.json", renderer, zipFile); hudTopHintRoot_ = loadLocalizedUi("resources/w/ui/hud_top_hint01.json", zipFile); phoneMainRoot = loadLocalizedUi("resources/w/ui/screen_phone.json", zipFile); phoneBankRoot = loadLocalizedUi("resources/w/ui/screen_phone_bank.json", zipFile); phoneVideoRoot = loadLocalizedUi("resources/w/ui/screen_phone_video.json", zipFile); phoneMapDormRoot = loadLocalizedUi("resources/w/ui/screen_phone_map_dorm.json", zipFile); phoneMapUniRoot = loadLocalizedUi("resources/w/ui/screen_phone_map_uni.json", zipFile); verticalPhoneMainRoot = loadLocalizedUi("resources/w/ui/portrait_screen_phone.json", zipFile); verticalPhoneBankRoot = loadLocalizedUi("resources/w/ui/portrait_screen_phone_bank.json", zipFile); verticalPhoneVideoRoot = loadLocalizedUi("resources/w/ui/portrait_screen_phone_video.json", zipFile); verticalPhoneMapDormRoot = loadLocalizedUi("resources/w/ui/portrait_screen_phone_map_dorm.json", zipFile); verticalPhoneMapUniRoot = loadLocalizedUi("resources/w/ui/portrait_screen_phone_map_uni.json", zipFile); phoneChatListRoot = loadLocalizedUi("resources/w/ui/screen_phone_chat_list.json", zipFile); verticalPhoneChatListRoot = loadLocalizedUi("resources/w/ui/portrait_screen_phone_chat_list.json", zipFile); phoneChat1Root = loadLocalizedUi("resources/w/ui/screen_phone_chat1.json", zipFile); phoneChat2Root = loadLocalizedUi("resources/w/ui/screen_phone_chat2.json", zipFile); phoneChat3Root = loadLocalizedUi("resources/w/ui/screen_phone_chat3.json", zipFile); verticalPhoneChat1Root = loadLocalizedUi("resources/w/ui/portrait_screen_phone_chat1.json", zipFile); verticalPhoneChat2Root = loadLocalizedUi("resources/w/ui/portrait_screen_phone_chat2.json", zipFile); verticalPhoneChat3Root = loadLocalizedUi("resources/w/ui/portrait_screen_phone_chat3.json", zipFile); modalMenuRoot = loadLocalizedUi("resources/w/ui/screen_modal_menu.json", zipFile); newInventoryRoot = loadLocalizedUi("resources/w/ui/screen_inventory.json", zipFile); portraitInventoryListRoot = loadLocalizedUi("resources/w/ui/portrait_screen_inventory.json", zipFile); portraitInventoryItemRoot = loadLocalizedUi("resources/w/ui/portrait_screen_inventory_item.json", zipFile); questJournalRoot = loadLocalizedUi("resources/w/ui/screen_journal.json", zipFile); portraitQuestJournalListRoot = loadLocalizedUi("resources/w/ui/portrait_screen_journal.json", zipFile); portraitQuestJournalItemRoot = loadLocalizedUi("resources/w/ui/portrait_screen_journal_item.json", zipFile); #if defined(__ANDROID__) || defined(__EMSCRIPTEN__) mainMenuRoot = loadLocalizedUi("resources/w/ui/screen_main_menu_android.json", zipFile); #else mainMenuRoot = loadLocalizedUi("resources/w/ui/screen_main_menu.json", zipFile); #endif #if defined(__ANDROID__) aboutScreenRoot = loadLocalizedUi("resources/w/ui/screen_about_android.json", zipFile); #elif defined(__EMSCRIPTEN__) aboutScreenRoot = loadLocalizedUi("resources/w/ui/screen_about_web.json", zipFile); #elif defined(STEAMSDK) aboutScreenRoot = loadLocalizedUi("resources/w/ui/screen_about.json", zipFile); #else aboutScreenRoot = loadLocalizedUi("resources/w/ui/screen_about_web.json", zipFile); //Windows and Linux non-steam #endif creditsScreenRoot = loadLocalizedUi("resources/w/ui/screen_credits.json", zipFile); portraitCreditsScreenRoot = loadLocalizedUi("resources/w/ui/portrait_screen_credits.json", zipFile); #ifdef __EMSCRIPTEN__ settingsScreenNoMusicWebRoot = loadLocalizedUi("resources/w/ui/screen_settings_nomusic_web.json", zipFile); #endif settingsScreenRoot = loadLocalizedUi("resources/w/ui/screen_settings.json", zipFile); settingsGraphicsScreenRoot = loadLocalizedUi("resources/w/ui/screen_settings_graphics.json", zipFile); languageScreenRoot = loadLocalizedUi("resources/w/ui/screen_language.json", zipFile); loadSavedGameScreenRoot = loadLocalizedUi("resources/w/ui/screen_load.json", zipFile); campaignScreenRoot = loadLocalizedUi("resources/w/ui/screen_campaign.json", zipFile); loadingScreenRoots_.clear(); auto preloadLoadingScreen = [this, &zipFile](const std::string& path) { if (!path.empty() && !loadingScreenRoots_.count(path)) loadingScreenRoots_[path] = loadLocalizedUi(path, zipFile); }; for (const CampaignConfig& campaign : CampaignRegistry::instance().all()) { preloadLoadingScreen(campaign.loadingScreenPath); for (const LocationConfig& location : campaign.locations) preloadLoadingScreen(location.loadingScreenPath); } saveGameScreenRoot = loadLocalizedUi("resources/w/ui/screen_save.json", zipFile); } void MenuManager::setup(Inventory& inv, const std::string& zipFile) { inventory = &inv; loadUiRoots(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); // Quest definitions are loaded per campaign by Game (see Game::activateCampaign). toastTextureQuestNew = loadLocalizedTexture("resources/w/ui/img/toast/quest_new001.png", zipFile); toastTextureQuestCompleted = loadLocalizedTexture("resources/w/ui/img/toast/quest_completed001.png", zipFile); toastTextureQuestFailed = loadLocalizedTexture("resources/w/ui/img/toast/quest_failed001.png", zipFile); toastTextureItemAdded = loadLocalizedTexture("resources/w/ui/img/toast/item_received001.png", zipFile); toastTextureItemRemoved = loadLocalizedTexture("resources/w/ui/img/toast/item_removed001.png", zipFile); languageLoadingRu = renderer.textureManager.LoadFromPng("resources/loading_land003_ru.png", zipFile); languageLoadingEn = renderer.textureManager.LoadFromPng("resources/loading_land003_en.png", zipFile); gameState_.questJournal.onQuestUnlocked = [this](const std::string& id) { const Quest::QuestState* q = gameState_.questJournal.findQuest(id); if (q) showToast(toastTextureQuestNew, q->definition.title); }; gameState_.questJournal.onQuestCompleted = [this](const std::string& id) { const Quest::QuestState* q = gameState_.questJournal.findQuest(id); if (q) showToast(toastTextureQuestCompleted, q->definition.title); }; gameState_.questJournal.onQuestFailed = [this](const std::string& id) { const Quest::QuestState* q = gameState_.questJournal.findQuest(id); if (q) showToast(toastTextureQuestFailed, 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::startNewGame(const std::string& campaignId) { // The reset runs on a later main-thread task, which then shows the HUD itself; // the main menu stays up until then. Showing the HUD now would draw one frame // from the previous game's state (e.g. its phone/journal buttons). if (startGameFunc) startGameFunc(campaignId); } void MenuManager::showLoadingScreen(const std::string& layoutPath) { auto it = loadingScreenRoots_.find(layoutPath); if (it == loadingScreenRoots_.end() || !it->second) { std::cerr << "[menu] Loading screen not preloaded: " << layoutPath << std::endl; return; // Game falls back to the startup loading image } loadingUiManager.replaceRoot(it->second); loadingScreenShown_ = true; } void MenuManager::hideLoadingScreen() { loadingUiManager.replaceRoot(nullptr); loadingScreenShown_ = false; } void MenuManager::enterGameplay(bool isFromLoad) { if (isFromLoad) { uiManager.clearMenuStack(); topUiManager.clearMenuStack(); } showGameplayHud(); } std::shared_ptr MenuManager::gameplayHudRoot() const { const std::string& loc = gameState_.currentLocationName; if (loc == "uni_exterior") return gameState_.isDarklands ? hudUniExtDarkRoot : hudUniExtRoot; if (loc == "uni_interior") return gameState_.isDarklands ? hudUniIntDarkRoot : hudUniIntRoot; return hudDormRoot; // dorm, or a new game before the location is set } void MenuManager::showGameplayHud() { uiState_ = GameUiState::Gameplay; uiManager.replaceRoot(gameplayHudRoot()); applyCurrentHealthBar(); setupGameplayHudCallbacks(); refreshHudVisibility(); } void MenuManager::refreshHudVisibility() { if (!inventory) return; // update() already ticks during loading, before setup() const TutorialStep step = gameState_.tutorialStep; const bool tutorial = gameState_.isTutorialActive(); const bool hasPhone = inventory->hasItem("phone"); const bool hasJournal = inventory->hasItem("journal"); const bool hasNote = inventory->hasItem("note_spell"); const bool dark = gameState_.isDarklands; auto show = [this](std::initializer_list names, bool visible) { for (const char* name : names) uiManager.setNodeVisible(name, visible); }; // --- HUD buttons: present while the matching item is in the inventory --- show({ "inventoryButton" }, inventory->getCount() > 0); show({ "phoneButton" }, hasPhone && !dark); show({ "journalButton" }, hasJournal); show({ "darklandsButton" }, hasNote && !dark); // --- Tutorial hints --- // The dialogue hint lives in topUiManager so it is drawn above the dialogue. if (!cutsceneHudActive_) { auto topRoot = (step == TutorialStep::AdvanceDialogue) ? hudTopHintRoot_ : nullptr; if (topUiManager.getRoot() != topRoot) topUiManager.replaceRoot(topRoot); } // Dorm HUD: one hint per linear step... show({ "hint2" }, step == TutorialStep::RotateCamera); show({ "hint3" }, step == TutorialStep::Walk); show({ "hint4" }, step == TutorialStep::Zoom); show({ "hint5" }, step == TutorialStep::PickUpItem); // ...then the hints for the picked-up items, which can be done in any order. const bool chatHint = tutorial && !gameState_.tutorialPhoneChatScreenOpened; const bool journalHint = tutorial && !gameState_.tutorialJournalScreenOpened; const bool taxiHint = tutorial && gameState_.tutorialNeedOpenTaxiScreen; show({ "hint6a", "hint6aarrow" }, hasPhone && chatHint && !taxiHint); show({ "hint6b", "hint6barrow" }, hasJournal && journalHint); show({ "hint7", "hint7arrow" }, taxiHint); // Phone screens. show({ "hint_m001", "hint_m001arrow" }, chatHint); // main screen: open the messenger show({ "hint_m003", "hint_m003arrow" }, taxiHint); // main screen: open the taxi app show({ "hint_m002" }, chatHint); // chat list: open a chat // University interior HUDs. show({ "hint_darklands001", "hint_darklands001_arrow" }, step == TutorialStep::EnterDarklands && hasNote); show({ "hint_darklands003", "hint_darklands003_arrow" }, step == TutorialStep::DarklandsHealth); show({ "hint_darklands002" }, step == TutorialStep::DarklandsAttack); } void MenuManager::showMainMenu() { gameState_.currentLocationName.clear(); audioPlayer_.crossFadeMusicAsync("music/main menu final.ogg"); lastMusicTrack = "music/main menu final.ogg"; uiState_ = GameUiState::MainMenu; uiManager.clearMenuStack(); topUiManager.replaceRoot(nullptr); topUiManager.clearMenuStack(); uiManager.replaceRoot(mainMenuRoot); uiManager.setTextButtonCallback("menuStartButton", [this](const std::string&) { audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); showCampaignScreen(); }); uiManager.setTextButtonCallback("menuAboutButton", [this](const std::string&) { showAboutScreen(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); uiManager.setTextButtonCallback("menuCreditsButton", [this](const std::string&) { showCreditsScreen(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); uiManager.setTextButtonCallback("menuSettingsButton", [this](const std::string&) { #ifdef __EMSCRIPTEN__ std::string musicPath = FRG::getPersistentResourcePath("music.zip"); if (FRG::isZipValid(musicPath)) { showSettingsScreen(); } else { showSettingsNoMusicWebScreen(); } #else showSettingsScreen(); #endif audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); uiManager.setTextButtonCallback("menuLanguageButton", [this](const std::string&) { showLanguageScreen(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); uiManager.setTextButtonCallback("menuLoadButton", [this](const std::string&) { showLoadGameScreen(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); #if !defined(EMSCRIPTEN) && !defined(__ANDROID__) uiManager.setTextButtonCallback("menuExitButton", [this](const std::string&) { audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); Environment::exitGameLoop = true; }); #else uiManager.setNodeVisible("menuExitButton", false); #endif } void MenuManager::showMenuAfterGameCompleted() { //gameState_.currentLocationName.clear(); audioPlayer_.crossFadeMusicAsync("music/main menu final.ogg"); lastMusicTrack = "music/main menu final.ogg"; uiState_ = GameUiState::MainMenu; uiManager.clearMenuStack(); topUiManager.replaceRoot(nullptr); topUiManager.clearMenuStack(); uiManager.replaceRoot(mainMenuRoot); uiManager.setTextButtonCallback("menuStartButton", [this](const std::string&) { audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); showCampaignScreen(); }); uiManager.setTextButtonCallback("menuAboutButton", [this](const std::string&) { showAboutScreen(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); uiManager.setTextButtonCallback("menuCreditsButton", [this](const std::string&) { showCreditsScreen(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); uiManager.setTextButtonCallback("menuSettingsButton", [this](const std::string&) { showSettingsScreen(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); uiManager.setTextButtonCallback("menuLanguageButton", [this](const std::string&) { showLanguageScreen(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); uiManager.setTextButtonCallback("menuLoadButton", [this](const std::string&) { showLoadGameScreen(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); #ifndef EMSCRIPTEN uiManager.setTextButtonCallback("menuExitButton", [this](const std::string&) { audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); Environment::exitGameLoop = true; }); #else uiManager.setNodeVisible("menuExitButton", false); #endif //Now open the about screen: showAboutScreen(); } void MenuManager::showAboutScreen() { uiManager.pushMenuFromSavedRoot(aboutScreenRoot); uiManager.setTextButtonCallback("aboutBackButton", [this](const std::string&) { uiManager.popMenu(); uiManager.updateAllLayouts(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); uiManager.setTextButtonCallback("steamButton", [this](const std::string&) { audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); SDL_OpenURL("https://store.steampowered.com/app/4801010/"); }); uiManager.setTextButtonCallback("websiteButton", [this](const std::string&) { audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); SDL_OpenURL("https://shadowoverbishkek.com/"); }); #ifdef __ANDROID__ uiManager.setTextButtonCallback("reviewButton", [this](const std::string&) { audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); SDL_OpenURL("https://play.google.com/store/apps/details?id=fishrungames.shadowoverbishkek"); }); #endif uiManager.setTextButtonCallback("telegramButton", [this](const std::string&) { audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); SDL_OpenURL("https://telegram.me/fishrungames"); }); uiManager.setTextButtonCallback("discordButton", [this](const std::string&) { audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); SDL_OpenURL("https://discord.gg/S8jgGHXjw"); }); } void MenuManager::showCreditsScreen() { isPlayerInCreditsMenu = true; if (isPortraitMode()) { uiManager.pushMenuFromSavedRoot(portraitCreditsScreenRoot); } else { uiManager.pushMenuFromSavedRoot(creditsScreenRoot); } static std::string gameTitle; #ifdef __ANDROID__ if (g_currentLanguage == Language::English) { gameTitle = "Shadow over Bishkek"; } else if (g_currentLanguage == Language::Russian) { gameTitle = u8"Тень над Бишкеком"; } uiManager.setText("creditsTitleText", gameTitle); #endif uiManager.setButtonCallback("creditsBackButton", [this](const std::string&) { isPlayerInCreditsMenu = false; uiManager.popMenu(); uiManager.updateAllLayouts(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); uiManager.updateAllLayouts(); uiManager.stopAnimationOnNode("creditsLinearLayout", "slowScroll"); uiManager.resetAnimationOnNode("creditsLinearLayout"); uiManager.startAnimationOnNode("creditsLinearLayout", "slowScroll"); } void MenuManager::showSettingsScreen() { uiManager.pushMenuFromSavedRoot(settingsScreenRoot); uiManager.setTextButtonCallback("settingsBackButton", [this](const std::string&) { saveSettings(); uiManager.popMenu(); uiManager.updateAllLayouts(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); uiManager.setTextButtonCallback("graphicsButton", [this](const std::string&) { showSettingsGraphicsScreen(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); // 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); // Localization if (g_currentLanguage == Language::English) { musicVolumePrefix = "Music volume: "; soundVolumePrefix = "Sound volume: "; musicToggleOn = "Music: ON"; musicToggleOff = "Music: OFF"; soundToggleOn = "Sound: ON"; soundToggleOff = "Sound: OFF"; } else if (g_currentLanguage == Language::Russian) { musicVolumePrefix = u8"Громкость музыки: "; soundVolumePrefix = u8"Громкость звука: "; musicToggleOn = u8"Музыка: ВКЛ"; musicToggleOff = u8"Музыка: ВЫКЛ"; soundToggleOn = u8"Звук: ВКЛ"; soundToggleOff = u8"Звук: ВЫКЛ"; } uiManager.setText("musicVolumeText", musicVolumePrefix + std::to_string(audioPlayer_.getMusicVolume())); uiManager.setText("soundVolumeText", soundVolumePrefix + std::to_string(audioPlayer_.getSoundVolume())); uiManager.setSliderCallback("musicVolumeSlider", [this](const std::string&, float value) { const int vol = static_cast(value * 128.0f + 0.5f); audioPlayer_.setMusicVolume(vol); uiManager.setText("musicVolumeText", musicVolumePrefix + std::to_string(vol)); }); uiManager.setSliderCallback("soundVolumeSlider", [this](const std::string&, float value) { const int vol = static_cast(value * 128.0f + 0.5f); audioPlayer_.setSoundVolume(vol); uiManager.setText("soundVolumeText", soundVolumePrefix + 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 ? musicToggleOn : musicToggleOff); /*uiManager.setTextButtonColor("musicToggleButton", on ? std::array{ 0.2f, 0.9f, 0.2f, 1.0f } : std::array{ 0.9f, 0.2f, 0.2f, 1.0f });*/ }; auto refreshSoundToggle = [this]() { const bool on = audioPlayer_.isSoundEnabled(); uiManager.setTextButtonText("soundToggleButton", on ? soundToggleOn : soundToggleOff); /*uiManager.setTextButtonColor("soundToggleButton", on ? std::array{ 0.2f, 0.9f, 0.2f, 1.0f } : std::array{ 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 ? musicToggleOn : musicToggleOff); /*uiManager.setTextButtonColor("musicToggleButton", on ? std::array{ 0.2f, 0.9f, 0.2f, 1.0f } : std::array{ 0.9f, 0.2f, 0.2f, 1.0f });*/ audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); uiManager.setTextButtonCallback("soundToggleButton", [this](const std::string&) { audioPlayer_.setSoundEnabled(!audioPlayer_.isSoundEnabled()); const bool on = audioPlayer_.isSoundEnabled(); uiManager.setTextButtonText("soundToggleButton", on ? soundToggleOn : soundToggleOff); /*uiManager.setTextButtonColor("soundToggleButton", on ? std::array{ 0.2f, 0.9f, 0.2f, 1.0f } : std::array{ 0.9f, 0.2f, 0.2f, 1.0f });*/ audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); setupTutorialToggle(); } // Tutorial on/off button shared by the settings screens — takes effect on the next new game only. void MenuManager::setupTutorialToggle() { if (g_currentLanguage == Language::English) { tutorialToggleOn = "Tutorial: ON"; tutorialToggleOff = "Tutorial: OFF"; } else if (g_currentLanguage == Language::Russian) { tutorialToggleOn = u8"Обучение: ВКЛ"; tutorialToggleOff = u8"Обучение: ВЫКЛ"; } uiManager.setTextButtonText("tutorialToggleButton", tutorialEnabledSetting ? tutorialToggleOn : tutorialToggleOff); uiManager.setTextButtonCallback("tutorialToggleButton", [this](const std::string&) { tutorialEnabledSetting = !tutorialEnabledSetting; uiManager.setTextButtonText("tutorialToggleButton", tutorialEnabledSetting ? tutorialToggleOn : tutorialToggleOff); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); } void MenuManager::showSettingsGraphicsScreen() { uiManager.pushMenuFromSavedRoot(settingsGraphicsScreenRoot); uiManager.setTextButtonCallback("settingsBackButton", [this](const std::string&) { saveSettings(); uiManager.popMenu(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); uiManager.updateAllLayouts(); topUiManager.updateAllLayouts(); #ifdef __EMSCRIPTEN__ playerInSettingsNoMusicGraphics = false; #endif }); // Localization if (g_currentLanguage == Language::English) { fullScreenToggleOn = "Full Screen: ON"; fullScreenToggleOff = "Full Screen: OFF"; shadowToggleOn = "Shadows: ON"; shadowToggleOff = "Shadows: OFF"; } else if (g_currentLanguage == Language::Russian) { fullScreenToggleOn = u8"Полноэкранный режим: ВКЛ"; fullScreenToggleOff = u8"Полноэкранный режим: ВЫКЛ"; shadowToggleOn = u8"Тени: ВКЛ"; shadowToggleOff = u8"Тени: ВЫКЛ"; } auto refreshFullscreenToggle = [this]() { const bool on = Environment::isFullscreen; uiManager.setTextButtonText("fullScreenToggleButton", on ? fullScreenToggleOn : fullScreenToggleOff); }; auto refreshShadowToggle = [this]() { const bool on = this->shadowsEnabled; uiManager.setTextButtonText("shadowToggleButton", on ? shadowToggleOn : shadowToggleOff); }; refreshFullscreenToggle(); refreshShadowToggle(); #ifdef __ANDROID__ uiManager.setNodeVisible("fullScreenToggleButton", false); #else uiManager.setTextButtonCallback("fullScreenToggleButton", [this, refreshFullscreenToggle](const std::string&) { Environment::setFullscreen(!Environment::isFullscreen); refreshFullscreenToggle(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); uiManager.updateAllLayouts(); topUiManager.updateAllLayouts(); }); #endif uiManager.setTextButtonCallback("shadowToggleButton", [this, refreshShadowToggle](const std::string&) { bool newValue = !this->shadowsEnabled; shadowMapSettingsChangedFunc(newValue); refreshShadowToggle(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); } void MenuManager::refreshFullScreenToggleButton() { // Localization if (g_currentLanguage == Language::English) { fullScreenToggleOn = "Full Screen: ON"; fullScreenToggleOff = "Full Screen: OFF"; } else if (g_currentLanguage == Language::Russian) { fullScreenToggleOn = u8"Полноэкранный режим: ВКЛ"; fullScreenToggleOff = u8"Полноэкранный режим: ВЫКЛ"; } const bool on = Environment::isFullscreen; uiManager.setTextButtonText("fullScreenToggleButton", on ? fullScreenToggleOn : fullScreenToggleOff); } #ifdef __EMSCRIPTEN__ void MenuManager::showSettingsNoMusicWebScreen() { playerInSettingsNoMusic = true; uiManager.pushMenuFromSavedRoot(settingsScreenNoMusicWebRoot); uiManager.setTextButtonCallback("settingsBackButton", [this](const std::string&) { saveSettings(); uiManager.popMenu(); uiManager.updateAllLayouts(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); playerInSettingsNoMusic = false; }); uiManager.setTextButtonCallback("graphicsButton", [this](const std::string&) { playerInSettingsNoMusicGraphics = true; showSettingsGraphicsScreen(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); // Set initial slider positions before registering callbacks so // setSliderValue does not fire an unregistered callback. const float soundFrac = audioPlayer_.getSoundVolume() / 128.0f; uiManager.setSliderValue("soundVolumeSlider", soundFrac); static std::string musicDownloadErrorString;// = MUSIC_DOWNLOAD_ERROR_RU; static std::string musicDownloadNotStartedString;// = MUSIC_DOWNLOAD_NOT_STARTED_RU; static std::string musicDownloadProgressPrefix;// = MUSIC_DOWNLOAD_PROGRESS_PREFIX_RU; // Localization if (g_currentLanguage == Language::English) { soundVolumePrefix = "Sound volume: "; soundToggleOn = "Sound: ON"; soundToggleOff = "Sound: OFF"; musicDownloadErrorString = MUSIC_DOWNLOAD_ERROR_EN; musicDownloadNotStartedString = MUSIC_DOWNLOAD_NOT_STARTED_EN; musicDownloadProgressPrefix = MUSIC_DOWNLOAD_PROGRESS_PREFIX_EN; } else if (g_currentLanguage == Language::Russian) { soundVolumePrefix = u8"Громкость звука: "; soundToggleOn = u8"Звук: ВКЛ"; soundToggleOff = u8"Звук: ВЫКЛ"; musicDownloadErrorString = MUSIC_DOWNLOAD_ERROR_RU; musicDownloadNotStartedString = MUSIC_DOWNLOAD_NOT_STARTED_RU; musicDownloadProgressPrefix = MUSIC_DOWNLOAD_PROGRESS_PREFIX_RU; } // musicDownloadStarted = false; //musicDownloadError = false; if (musicDownloadError) { uiManager.setText("musicDownloadButton", musicDownloadErrorString); } else { if (musicDownloadStarted == false) { uiManager.setText("musicDownloadButton", musicDownloadNotStartedString); } else { //uiManager.setText("musicDownloadButton", musicDownloadProgressString); uiManager.setText("musicDownloadButton", musicDownloadProgressPrefix + std::to_string(round(musicDownloadProgress*100)) + "%"); } } uiManager.setText("soundVolumeText", soundVolumePrefix + std::to_string(audioPlayer_.getSoundVolume())); uiManager.setSliderCallback("soundVolumeSlider", [this](const std::string&, float value) { const int vol = static_cast(value * 128.0f + 0.5f); audioPlayer_.setSoundVolume(vol); uiManager.setText("soundVolumeText", soundVolumePrefix + std::to_string(vol)); }); // Music on/off toggle — helper to sync button text and color from current state auto refreshSoundToggle = [this]() { const bool on = audioPlayer_.isSoundEnabled(); uiManager.setTextButtonText("soundToggleButton", on ? soundToggleOn : soundToggleOff); /*uiManager.setTextButtonColor("soundToggleButton", on ? std::array{ 0.2f, 0.9f, 0.2f, 1.0f } : std::array{ 0.9f, 0.2f, 0.2f, 1.0f });*/ }; refreshSoundToggle(); uiManager.setTextButtonCallback("soundToggleButton", [this](const std::string&) { audioPlayer_.setSoundEnabled(!audioPlayer_.isSoundEnabled()); const bool on = audioPlayer_.isSoundEnabled(); uiManager.setTextButtonText("soundToggleButton", on ? soundToggleOn : soundToggleOff); /*uiManager.setTextButtonColor("soundToggleButton", on ? std::array{ 0.2f, 0.9f, 0.2f, 1.0f } : std::array{ 0.9f, 0.2f, 0.2f, 1.0f });*/ audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); setupTutorialToggle(); uiManager.setTextButtonCallback("musicDownloadButton", [this](const std::string&) { if (musicDownloadStarted) return; // Already downloading, ignore further clicks musicDownloadStarted = true; musicDownloadError = false; std::string musicPath = FRG::getPersistentResourcePath("music.zip"); emscripten_async_wget2( "music.zip", musicPath.c_str(), "GET", nullptr, nullptr, onMusicZipLoaded, onMusicZipError, onMusicZipProgress ); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); } #endif void MenuManager::showLanguageScreen() { uiManager.pushMenuFromSavedRoot(languageScreenRoot); uiManager.setTextButtonCallback("languageBackButton", [this](const std::string&) { saveSettings(); uiManager.popMenu(); uiManager.updateAllLayouts(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); uiManager.setTextButtonCallback("languageRussianButton", [this](const std::string&) { #ifndef __EMSCRIPTEN__ audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); #endif FRG::g_currentLanguage = FRG::Language::Russian; reloadLocalizedGameContent(); saveSettings(); }); uiManager.setTextButtonCallback("languageEnglishButton", [this](const std::string&) { #ifndef __EMSCRIPTEN__ audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); #endif FRG::g_currentLanguage = FRG::Language::English; reloadLocalizedGameContent(); saveSettings(); }); } void MenuManager::reloadLocalizedGameContent() { // Screens below the Language screen on the menu stack hold onto their old // (pre-reload) UiNode instances, so simply reopening Language on top of // them would leave Main Menu / Settings / the pause menu showing stale // text once the player steps back. Instead, unwind the whole chain and // replay it with freshly (re)loaded, localized roots. const bool wasInMainMenu = (uiState_ == GameUiState::MainMenu); if (!wasInMainMenu) { // Reached via the in-game pause menu (Modal -> Settings -> Language). // Fully unwind back to the untouched gameplay HUD underneath — it // isn't rebuilt here, only the pause/settings/language chain above it. const int depth = uiManager.menuStackSize(); for (int i = 0; i < depth; ++i) uiManager.popMenu(); } // Unloading/reloading ~50 UI screens and their textures is heavy enough to // freeze the window for a noticeable moment, so run it the same way the // initial resource load runs: a queue of closures drained one per frame // with the loading screen shown, instead of one big blocking call here. std::vector> steps; steps.push_back([this]() { ItemRegistry::instance().loadFromJson("resources/config/items.json", CONST_ZIP_FILE); if (const CampaignConfig* campaign = CampaignRegistry::instance().find(gameState_.campaignId)) gameState_.questJournal.reloadDefinitionsPreservingState(campaign->questsPath, CONST_ZIP_FILE); }); steps.push_back([this]() { renderer.textureManager.UnloadByPrefix("resources/w/ui"); loadUiRoots(CONST_ZIP_FILE); toastTextureQuestNew = loadLocalizedTexture("resources/w/ui/img/toast/quest_new001.png", CONST_ZIP_FILE); toastTextureQuestCompleted = loadLocalizedTexture("resources/w/ui/img/toast/quest_completed001.png", CONST_ZIP_FILE); toastTextureQuestFailed = loadLocalizedTexture("resources/w/ui/img/toast/quest_failed001.png", CONST_ZIP_FILE); toastTextureItemAdded = loadLocalizedTexture("resources/w/ui/img/toast/item_received001.png", CONST_ZIP_FILE); toastTextureItemRemoved = loadLocalizedTexture("resources/w/ui/img/toast/item_removed001.png", CONST_ZIP_FILE); }); steps.push_back([this]() { showMainMenu(); }); if (runAsLoadingSequence) { runAsLoadingSequence(std::move(steps)); } else { for (auto& step : steps) step(); } } void MenuManager::showLoadGameScreen() { uiManager.pushMenuFromSavedRoot(loadSavedGameScreenRoot); uiManager.setTextButtonCallback("loadBackButton", [this](const std::string&) { uiManager.popMenu(); uiManager.updateAllLayouts(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); static const char* kSlotButtons[4] = { "loadSlot1Button", "loadSlot2Button", "loadSlot3Button", "loadSlot4Button" }; for (int i = 0; i < 4; ++i) { int slot = i + 1; if (getSlotInfoFunc) { uiManager.setTextButtonText(kSlotButtons[i], slotLabel(getSlotInfoFunc(slot))); } uiManager.setTextButtonCallback(kSlotButtons[i], [this, slot](const std::string&) { audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); // Game enters gameplay itself once the save (and its campaign) is loaded. if (onLoadGame) onLoadGame(slot); }); } } std::string MenuManager::slotLabel(const SaveSlotInfo& info) { if (info.empty) return FRG::g_currentLanguage == FRG::Language::English ? EMPTY_LANGUAGE_EN : EMPTY_LANGUAGE_RU; std::string label = localizedLocationName[info.locationName][FRG::g_currentLanguage] + " " + info.savedAt; // Name the campaign unless it's the main game, so saves of different campaigns are told apart. const CampaignConfig* campaign = CampaignRegistry::instance().find(info.campaignId); if (campaign && campaign->id != CampaignRegistry::kDefaultCampaignId) label = campaign->title(FRG::g_currentLanguage) + ": " + label; return label; } void MenuManager::showCampaignScreen() { uiManager.pushMenuFromSavedRoot(campaignScreenRoot); uiManager.setTextButtonCallback("campaignBackButton", [this](const std::string&) { uiManager.popMenu(); uiManager.updateAllLayouts(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); // One button per campaign in campaigns.json order; spare buttons are hidden. static const char* kCampaignButtons[4] = { "campaignSlot1Button", "campaignSlot2Button", "campaignSlot3Button", "campaignSlot4Button" }; const auto& campaigns = CampaignRegistry::instance().all(); for (int i = 0; i < 4; ++i) { const bool used = i < static_cast(campaigns.size()); uiManager.setNodeVisible(kCampaignButtons[i], used); if (!used) continue; const std::string campaignId = campaigns[i].id; uiManager.setTextButtonText(kCampaignButtons[i], campaigns[i].title(FRG::g_currentLanguage)); uiManager.setTextButtonCallback(kCampaignButtons[i], [this, campaignId](const std::string&) { audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); uiManager.popMenu(); startNewGame(campaignId); }); } } void MenuManager::showSaveGameScreen() { uiManager.pushMenuFromSavedRoot(saveGameScreenRoot); uiManager.setTextButtonCallback("saveBackButton", [this](const std::string&) { uiManager.popMenu(); uiManager.updateAllLayouts(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); 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) { uiManager.setTextButtonText(buttonName, slotLabel(getSlotInfoFunc(slot))); } uiManager.setTextButtonCallback(buttonName, [this, slot, buttonName](const std::string&) { if (onSaveGame) onSaveGame(slot); if (getSlotInfoFunc) { uiManager.setTextButtonText(buttonName, slotLabel(getSlotInfoFunc(slot))); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); } }); } } void MenuManager::openInventory() { uiState_ = GameUiState::Inventory; inventoryMenuStackDepth_ = 1; const bool portrait = isPortraitMode(); uiManager.pushMenuFromSavedRoot(portrait ? portraitInventoryListRoot : newInventoryRoot); uiManager.setButtonCallback("inventoryExitButton2", [this](const std::string&) { closeInventory(); }); uiManager.setButtonCallback("inventoryExitButton", [this](const std::string&) { closeInventory(); }); uiManager.setButtonCallback("inventoryMain", [this](const std::string&) {}); const auto& items = inventory->getItems(); /*logger() << "Item count: " << items.size() << std::endl; for (int x = 0; x < items.size(); x++) { std::cout << "item " << x << ": " << items[x].id << std::endl; }*/ const int maxSlots = 9; for (int i = 0; i < maxSlots; ++i) { std::string btnName = "item" + std::to_string(i + 1) + "Button"; if (i < static_cast(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; } //logger() << "Set button ballback to button: " << btnName << std::endl; uiManager.setButtonCallback(btnName, [this, i](const std::string&) { //logger() << "Callback called for button number " << i << std::endl; selectInventoryItem(i); //logger() << "Callback called for button number " << i << " END" << std::endl; audioPlayer_.playSoundAsync("resources/audio/sound_bag001.ogg"); }); } else { uiManager.setNodeVisible(btnName, false); } } inventorySelectedIndex_ = -1; if (!portrait && !items.empty()) { selectInventoryItem(0); } audioPlayer_.playSoundAsync("resources/audio/sound_bag_open.ogg"); } void MenuManager::selectInventoryItem(int index) { logger() << "MenuManager::selectInventoryItem: " << index << std::endl; const auto& items = inventory->getItems(); if (index < 0 || index >= static_cast(items.size())) return; if (isPortraitMode() && inventoryMenuStackDepth_ == 1) { openPortraitInventoryItem(index); return; } // Revert previously selected button to its regular icon if (inventorySelectedIndex_ >= 0 && inventorySelectedIndex_ < static_cast(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; for (int i = 0; i < inventoryMenuStackDepth_; ++i) uiManager.popMenu(); inventoryMenuStackDepth_ = 0; uiManager.updateAllLayouts(); audioPlayer_.playSoundAsync("resources/audio/sound_bag_close.ogg"); } void MenuManager::openQuestJournal() { uiState_ = GameUiState::QuestJournal; gameState_.tutorialJournalScreenOpened = true; journalMenuStackDepth_ = 1; const bool portrait = isPortraitMode(); uiManager.pushMenuFromSavedRoot(portrait ? portraitQuestJournalListRoot : questJournalRoot); uiManager.setButtonCallback("journalExitButton", [this](const std::string&) { closeQuestJournal(); audioPlayer_.playSoundAsync("resources/audio/656126__itsthegoodstuff__paging-through-book.ogg"); }); uiManager.setButtonCallback("journalExitButton2", [this](const std::string&) { closeQuestJournal(); audioPlayer_.playSoundAsync("resources/audio/656126__itsthegoodstuff__paging-through-book.ogg"); }); uiManager.setButtonCallback("journalMain", [this](const std::string&) {}); 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); audioPlayer_.playSoundAsync("resources/audio/255571__stradie__flipping-paper.ogg"); }); } refreshQuestJournalUi(); if (!portrait && !visibleQuestIds.empty()) { selectQuestByIndex(0); } audioPlayer_.playSoundAsync("resources/audio/255571__stradie__flipping-paper.ogg"); } void MenuManager::closeQuestJournal() { uiState_ = GameUiState::Gameplay; selectedQuestIndex = -1; visibleQuestIds.clear(); for (int i = 0; i < journalMenuStackDepth_; ++i) uiManager.popMenu(); journalMenuStackDepth_ = 0; uiManager.updateAllLayouts(); } void MenuManager::toggleQuestJournal() { logger() << "[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; currentPhoneSubScreen_ = PhoneSubScreen::Main; uiManager.pushMenuFromSavedRoot(isPortraitMode() ? verticalPhoneMainRoot : phoneMainRoot); refreshHudVisibility(); uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) { closePhoneEntirely(); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); }); uiManager.setButtonCallback("phoneMain", [this](const std::string&) {}); uiManager.setButtonCallback("phoneMessenger", [this](const std::string&) { openPhoneMessenger(); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); }); uiManager.setButtonCallback("phoneBank", [this](const std::string&) { openPhoneBank(); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); }); uiManager.setButtonCallback("phoneVideo", [this](const std::string&) { openPhoneVideo(); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); }); uiManager.setButtonCallback("phoneTaxi", [this](const std::string&) { openPhoneTaxi(); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); }); 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); } audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); } void MenuManager::openPhoneMessenger() { currentPhoneSubScreen_ = PhoneSubScreen::ChatList; uiManager.pushMenuFromSavedRoot(isPortraitMode() ? verticalPhoneChatListRoot : phoneChatListRoot); refreshHudVisibility(); refreshChatUnreadIndicators(); uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) { closePhoneEntirely(); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); }); uiManager.setButtonCallback("phoneMain", [this](const std::string&) {}); uiManager.setTextButtonCallback("chat1button", [this](const std::string&) { gameState_.chatUnread[0] = false; openPhoneChatFromList(0, isPortraitMode() ? verticalPhoneChat1Root : phoneChat1Root); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); }); uiManager.setTextButtonCallback("chat2button", [this](const std::string&) { gameState_.chatUnread[1] = false; openPhoneChatFromList(1, isPortraitMode() ? verticalPhoneChat2Root : phoneChat2Root); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); }); uiManager.setTextButtonCallback("chat3button", [this](const std::string&) { gameState_.chatUnread[2] = false; openPhoneChatFromList(2, isPortraitMode() ? verticalPhoneChat3Root : phoneChat3Root); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); }); } 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() { currentPhoneSubScreen_ = PhoneSubScreen::Bank; uiManager.pushMenuFromSavedRoot(isPortraitMode() ? verticalPhoneBankRoot : phoneBankRoot); uiManager.setText("balanceText", formatMoney(gameState_.money)); uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) { closePhoneEntirely(); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); }); uiManager.setButtonCallback("phoneMain", [this](const std::string&) {}); uiManager.setButtonCallback("buttonBack", [this](const std::string&) { uiManager.popMenu(); uiManager.updateAllLayouts(); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); }); } void MenuManager::openPhoneVideo() { currentPhoneSubScreen_ = PhoneSubScreen::Video; uiManager.pushMenuFromSavedRoot(isPortraitMode() ? verticalPhoneVideoRoot : phoneVideoRoot); uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) { closePhoneEntirely(); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); }); uiManager.setButtonCallback("phoneMain", [this](const std::string&) {}); uiManager.setButtonCallback("buttonBack", [this](const std::string&) { uiManager.popMenu(); uiManager.updateAllLayouts(); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); }); 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 { audioPlayer_.playSoundAsync("resources/audio/78564__joedeshon__alarm_clock_ticking_02.ogg"); startNightTransitionFunc(); } } }); } void MenuManager::openPhoneTaxi() { if (gameState_.currentLocationName == "uni_interior") { closePhoneEntirely(); if (startDialogueFunc) startDialogueFunc("dialog_taxi003"); } else if (gameState_.currentLocationName == "uni_exterior") { currentPhoneSubScreen_ = PhoneSubScreen::MapUni; openPhoneMapScreen(isPortraitMode() ? verticalPhoneMapUniRoot : phoneMapUniRoot); } else { currentPhoneSubScreen_ = PhoneSubScreen::MapDorm; openPhoneMapScreen(isPortraitMode() ? verticalPhoneMapDormRoot : phoneMapDormRoot); } } void MenuManager::openPhoneMapScreen(std::shared_ptr mapRoot) { uiManager.pushMenuFromSavedRoot(mapRoot); uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) { closePhoneEntirely(); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); }); uiManager.setButtonCallback("phoneMain", [this](const std::string&) {}); uiManager.setButtonCallback("buttonBack", [this](const std::string&) { uiManager.popMenu(); uiManager.updateAllLayouts(); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); }); uiManager.setButtonCallback("mapGo", [this](const std::string&) { gameState_.tutorialNeedOpenTaxiScreen = false; gameState_.tutorialTaxiHintDone = true; if (gameState_.taxiIsCalled == false) { gameState_.taxiIsCalled = true; if (callTaxiFunc) { callTaxiFunc(); } gameState_.money -= 500; closePhoneEntirely(); if (startDialogueFunc) startDialogueFunc("dialog_taxi002"); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); } else { closePhoneEntirely(); if (startDialogueFunc) startDialogueFunc("dialog_taxi004"); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); } }); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); } void MenuManager::openPhoneChatFromList(int chatIndex, std::shared_ptr chatRoot) { activeChatIndex_ = chatIndex; currentPhoneSubScreen_ = PhoneSubScreen::Chat; phoneChatVisibleBubbles_.clear(); gameState_.tutorialPhoneChatScreenOpened = true; uiManager.pushMenuFromSavedRoot(chatRoot); // Build a dedicated chatUiManager root sized to the FBO chat area { const bool portrait = isPortraitMode(); const float fboW = portrait ? CHAT_FBO_WIDTH_P : CHAT_FBO_WIDTH_L; const float fboH = portrait ? (CHAT_FBO_OFFSET_UP_P + CHAT_FBO_OFFSET_DOWN_P) : (CHAT_FBO_OFFSET_UP_L + CHAT_FBO_OFFSET_DOWN_L); auto fboRoot = std::make_shared(); fboRoot->name = "chatFboRoot"; fboRoot->width = fboW; fboRoot->height = fboH; fboRoot->screenRect = { 0.0f, 0.0f, fboW, fboH }; auto fboContainer = std::make_shared(); fboContainer->name = "chatMessagesContainer"; fboContainer->width = fboW; fboContainer->height = fboH; fboContainer->screenRect = { 0.0f, 0.0f, fboW, fboH }; fboRoot->children.push_back(fboContainer); chatUiManager.replaceRoot(fboRoot); chatFrameBuffer_ = std::make_unique( static_cast(fboW), static_cast(fboH), false); chatFboQuadLastProjW_ = -1.0f; } rebuildChatBubblesFromHistory(chatIndex, isPortraitMode()); uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) { closePhoneScreenFromChat(); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); }); uiManager.setButtonCallback("phoneMain", [this](const std::string&) {}); uiManager.setTextButtonCallback("chatTitleButton", [this](const std::string&) { returnToPhoneChatList(); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); }); 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; currentPhoneSubScreen_ = PhoneSubScreen::ChatList; phoneChatVisibleBubbles_.clear(); chatUiManager.replaceRoot(nullptr); chatFrameBuffer_.reset(); uiManager.popMenu(); uiManager.updateAllLayouts(); refreshHudVisibility(); refreshChatUnreadIndicators(); } void MenuManager::closePhoneEntirely() { if (activeChatIndex_ >= 0 && activeChatIndex_ <= 2 && !gameState_.chatHistory[activeChatIndex_].empty()) { gameState_.chatPreviewMsg[activeChatIndex_] = trimChatPreview(gameState_.chatHistory[activeChatIndex_].back().text); } activeChatIndex_ = -1; currentPhoneSubScreen_ = PhoneSubScreen::None; uiState_ = GameUiState::Gameplay; phoneChatVisibleBubbles_.clear(); chatUiManager.replaceRoot(nullptr); chatFrameBuffer_.reset(); const int depth = uiManager.menuStackSize(); for (int i = 0; i < depth; ++i) uiManager.popMenu(); uiManager.updateAllLayouts(); refreshHudVisibility(); } void MenuManager::closePhoneScreenFromChat() { closePhoneEntirely(); } void MenuManager::closePhoneScreen() { closePhoneEntirely(); } void MenuManager::showModalMenuScreen() { audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); uiManager.pushMenuFromSavedRoot(modalMenuRoot); uiManager.setButtonCallback("modalExitButton", [this](const std::string&) { audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); uiManager.popMenu(); uiManager.updateAllLayouts(); }); uiManager.setButtonCallback("menuCloseButton", [this](const std::string&) { audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); uiManager.popMenu(); uiManager.updateAllLayouts(); }); uiManager.setButtonCallback("menuSaveButton", [this](const std::string&) { showSaveGameScreen(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); uiManager.setButtonCallback("menuLoadButton", [this](const std::string&) { showLoadGameScreen(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); uiManager.setButtonCallback("menuSettingsButton", [this](const std::string&) { showSettingsScreen(); audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); }); uiManager.setButtonCallback("menuExitButton", [this](const std::string&) { audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg"); uiManager.popMenu(); showMainMenu(); }); } void MenuManager::tutorialShowTaxiHint() { logger() << "tutorialShowTaxiHint" << std::endl; if (!gameState_.isTutorialActive() || gameState_.tutorialTaxiHintDone) return; gameState_.tutorialNeedOpenTaxiScreen = true; // cleared when the taxi is ordered refreshHudVisibility(); } 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; showGameplayHud(); if (locationName == "uni_exterior" || locationName == "uni_interior") { if (gameState_.isDarklands) { audioPlayer_.crossFadeMusicAsync("music/bishkek fight calm.ogg"); lastMusicTrack = "music/bishkek fight calm.ogg"; } else if (gameState_.isNight) { audioPlayer_.crossFadeMusicAsync("music/bishkek night.ogg"); lastMusicTrack = "music/bishkek night.ogg"; } else { audioPlayer_.crossFadeMusicAsync("music/bishkek univer day.ogg"); lastMusicTrack = "music/bishkek univer day.ogg"; } } else { if (gameState_.isNight) { audioPlayer_.crossFadeMusicAsync("music/bishkek night.ogg"); lastMusicTrack = "music/bishkek night.ogg"; } else { audioPlayer_.crossFadeMusicAsync("music/obshaga.ogg"); lastMusicTrack = "music/obshaga.ogg"; } } } void MenuManager::onItemPickedUp(const std::string& itemId) { // The new item's HUD button appears through refreshHudVisibility(). if (itemId == "phone" || itemId == "journal") gameState_.completeTutorialStep(TutorialStep::PickUpItem); refreshHudVisibility(); } 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(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); if (isPortraitMode()) { node->height = numLines * 48.0f + 48.0f; } else { 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; } if (isPortraitMode() && journalMenuStackDepth_ == 1) { openPortraitQuestJournalItem(index); 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 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(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 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::openPortraitInventoryItem(int index) { const auto& items = inventory->getItems(); inventorySelectedIndex_ = index; inventoryMenuStackDepth_ = 2; uiManager.pushMenuFromSavedRoot(portraitInventoryItemRoot); uiManager.setButtonCallback("inventoryExitButton", [this](const std::string&) { closePortraitInventoryItem(); audioPlayer_.playSoundAsync("resources/audio/sound_bag001.ogg"); }); uiManager.setButtonCallback("inventoryExitButton2", [this](const std::string&) { closePortraitInventoryItem(); audioPlayer_.playSoundAsync("resources/audio/sound_bag001.ogg"); }); uiManager.setButtonCallback("inventoryMain", [this](const std::string&) {}); const auto& item = items[index]; 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); uiManager.updateAllLayouts(); } void MenuManager::closePortraitInventoryItem() { inventoryMenuStackDepth_ = 1; uiManager.popMenu(); uiManager.updateAllLayouts(); } void MenuManager::openPortraitQuestJournalItem(int index) { selectedQuestIndex = index; journalMenuStackDepth_ = 2; uiManager.pushMenuFromSavedRoot(portraitQuestJournalItemRoot); uiManager.setButtonCallback("journalItemExitButton", [this](const std::string&) { closePortraitJournalItem(); audioPlayer_.playSoundAsync("resources/audio/255571__stradie__flipping-paper.ogg"); }); uiManager.setButtonCallback("journalItemExitButton2", [this](const std::string&) { closeQuestJournal(); audioPlayer_.playSoundAsync("resources/audio/656126__itsthegoodstuff__paging-through-book.ogg"); }); uiManager.setButtonCallback("journalMain", [this](const std::string&) {}); 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" }; static const char* kObjNameLayouts[3] = { "objective1nameLayout", "objective2nameLayout", "objective3nameLayout" }; std::vector 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(visibleObjs.size())) { const auto& obj = *visibleObjs[i]; const bool isActive = (&obj - def.objectives.data() == quest->activeObjectiveIndex); auto img = uiManager.findStaticImage(kCheckboxes[i]); auto textView = uiManager.findTextView(kObjNames[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 }; } const int numLines = countWrappedLines(obj.text, *textView->textRenderer, 540); if (numLines == 1) { uiManager.findNode(kObjNameLayouts[i])->height = 80.0f; } else { uiManager.findNode(kObjNameLayouts[i])->height = 60.0f; } 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); uiManager.updateAllLayouts(); } void MenuManager::closePortraitJournalItem() { journalMenuStackDepth_ = 1; uiManager.popMenu(); uiManager.updateAllLayouts(); refreshQuestJournalUi(); } /* void MenuManager::closePortraitJournalItemCompletely() { journalMenuStackDepth_ = 1; uiManager.popMenu(); uiManager.updateAllLayouts(); 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 fboH = CHAT_FBO_OFFSET_UP_L + CHAT_FBO_OFFSET_DOWN_L; const float topY = (totalHeight <= fboH) ? fboH : totalHeight; float cursor = topY; for (auto& bubble : phoneChatVisibleBubbles_) { auto node = chatUiManager.findNode(bubble.nodeName); if (!node) continue; node->localY = cursor - bubble.height; cursor -= bubble.height + CHAT_SPACING; } chatUiManager.updateAllLayouts(); } void MenuManager::recomputePhoneChatPositionsPortrait() { 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 fboH = CHAT_FBO_OFFSET_UP_P + CHAT_FBO_OFFSET_DOWN_P; const float topY = (totalHeight <= fboH) ? fboH : totalHeight; float cursor = topY; for (auto& bubble : phoneChatVisibleBubbles_) { auto node = chatUiManager.findNode(bubble.nodeName); if (!node) continue; node->localY = cursor - bubble.height; cursor -= bubble.height + CHAT_SPACING; } chatUiManager.updateAllLayouts(); } void MenuManager::rebuildChatBubblesFromHistory(int chatIndex, bool isPortrait) { chatUiManager.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 = chatUiManager.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", isPortrait ? 32 : 20, isPortrait, CONST_ZIP_FILE); if (!nodeName.empty()) { auto n = chatUiManager.findNode(nodeName); phoneChatVisibleBubbles_.push_back({ nodeName, n ? n->height : 60.0f }); } } if (isPortrait) { recomputePhoneChatPositionsPortrait(); } else { recomputePhoneChatPositions(); } } void MenuManager::onChatBubbleReady(const std::string& text, bool incoming) { if (activeChatIndex_ < 0) return; auto& history = gameState_.chatHistory[activeChatIndex_]; if (static_cast(history.size()) >= 5) { history.erase(history.begin()); } history.push_back({ text, incoming }); if (uiState_ != GameUiState::PhoneScreen) return; bool isPortrait = isPortraitMode(); const std::string nodeName = chatUiManager.addChatBubble( "chatMessagesContainer", text, incoming, incoming ? texBubbleInCenter_ : texBubbleOutCenter_, incoming ? texBubbleInLT_ : texBubbleOutLT_, incoming ? texBubbleInLB_ : texBubbleOutLB_, incoming ? texBubbleInRT_ : texBubbleOutRT_, incoming ? texBubbleInRB_ : texBubbleOutRB_, renderer, "resources/fonts/DroidSans.ttf", isPortrait ? 32 : 20, isPortrait, CONST_ZIP_FILE); if (nodeName.empty()) return; auto node = chatUiManager.findNode(nodeName); if (node) { node->scaleX = 0.0f; node->scaleY = 0.0f; phoneChatVisibleBubbles_.push_back({ nodeName, node->height }); } if (isPortrait) { recomputePhoneChatPositionsPortrait(); } else { recomputePhoneChatPositions(); } chatUiManager.startPopIn(nodeName, 300.0f); audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg"); } void MenuManager::setDarklandsMode(bool enabled) { logger() << "MenuManager::setDarklandsMode called" << std::endl; if (enabled && gameState_.currentLocationName == "uni_interior" && inventory->hasItem("note_spell")) gameState_.completeTutorialStep(TutorialStep::EnterDarklands); if (uiState_ == GameUiState::Gameplay) showGameplayHud(); // darklands has its own HUD layouts } void MenuManager::onPlayerStartedWalking() { if (gameState_.currentLocationName == "uni_interior" && gameState_.isDarklands) gameState_.completeTutorialStep(TutorialStep::DarklandsHealth); } void MenuManager::advanceUniIntDarklandsHud() { // The ghost zone can be reached without a floor tap (DarklandsHealth never completed). if (gameState_.tutorialStep == TutorialStep::DarklandsHealth || gameState_.tutorialStep == TutorialStep::DarklandsExplore) gameState_.tutorialStep = TutorialStep::DarklandsAttack; } void MenuManager::onEnemyKilledInUniInterior() { // Every location's enemies report here; only the university interior drives the tutorial. if (gameState_.currentLocationName != "uni_interior") return; // The last tutorial step; like the attack hint, also ends the earlier darklands hints. if (gameState_.tutorialStep >= TutorialStep::DarklandsHealth) gameState_.tutorialStep = TutorialStep::Completed; } 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() { logger() << "MenuManager::applyCurrentHealthBar called step 1" << std::endl; logger() << "currentPlayerMaxHp_ is " << gameState_.playerMaxHp << std::endl; if (gameState_.playerMaxHp <= 0.f) return; logger() << "MenuManager::applyCurrentHealthBar called step 2" << std::endl; logger() << "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(gameState_.playerHp)) + "/" + std::to_string(static_cast(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(std::shared_ptr toastTexture, const std::string& text) { if (activeToasts_.size() < 3) { ToastEntry e; e.toastTexture = toastTexture; e.text = text; activeToasts_.push_back(std::move(e)); } else { toastQueue_.push_back({ toastTexture, 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.toastTexture = toastQueue_.front().toastTexture; 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(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 = e.toastTexture; 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); chatUiManager.update(deltaMs); if (uiState_ != GameUiState::MainMenu && uiState_ != GameUiState::About) refreshHudVisibility(); } void MenuManager::drawChatFbo(Renderer& renderer) { if (!chatFrameBuffer_) return; const bool portrait = isPortraitMode(); const float fboW = portrait ? CHAT_FBO_WIDTH_P : CHAT_FBO_WIDTH_L; const float offsetUp = portrait ? CHAT_FBO_OFFSET_UP_P : CHAT_FBO_OFFSET_UP_L; const float offsetDown= portrait ? CHAT_FBO_OFFSET_DOWN_P : CHAT_FBO_OFFSET_DOWN_L; const float fboH = offsetUp + offsetDown; const float projW = Environment::projectionWidth; const float projH = Environment::projectionHeight; // Pass 1: render chatUiManager into FBO chatFrameBuffer_->Bind(); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); chatUiManager.draw(renderer, fboW, fboH); chatFrameBuffer_->Unbind(); // Pass 2: blit FBO texture onto screen, centered with up/down offsets from screen center if (chatFboQuadLastProjW_ != projW || chatFboQuadLastPortrait_ != portrait || chatFboQuadLastProjH_ != projH) { const float x0 = (projW - fboW) * 0.5f; const float y0 = projH * 0.5f - offsetDown; const float x1 = x0 + fboW; const float y1 = y0 + fboH; VertexDataStruct vd; vd.PositionData = { {x0,y0,0},{x0,y1,0},{x1,y1,0}, {x0,y0,0},{x1,y1,0},{x1,y0,0} }; vd.TexCoordData = { {0,0},{0,1},{1,1}, {0,0},{1,1},{1,0} }; chatFboQuad_.AssignFrom(vd); chatFboQuadLastProjW_ = projW; chatFboQuadLastProjH_ = projH; chatFboQuadLastPortrait_ = portrait; } glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); renderer.PushProjectionMatrix(projW, projH, -1, 1); renderer.PushMatrix(); renderer.LoadIdentity(); renderer.RenderUniform1f("uAlpha", 1.0f); glBindTexture(GL_TEXTURE_2D, chatFrameBuffer_->getTextureID()); renderer.DrawVertexRenderStruct(chatFboQuad_); renderer.PopMatrix(); renderer.PopProjectionMatrix(); } void MenuManager::saveSettings() { nlohmann::json root; root["musicVolume"] = audioPlayer_.getMusicVolume(); root["soundVolume"] = audioPlayer_.getSoundVolume(); root["musicEnabled"] = audioPlayer_.isMusicEnabled(); root["soundEnabled"] = audioPlayer_.isSoundEnabled(); root["fullscreen"] = Environment::isFullscreen; root["language"] = FRG::languageToCode(FRG::g_currentLanguage); root["shadow"] = shadowsEnabled; root["tutorial"] = tutorialEnabledSetting; saveJsonToFile(root, "settings.json"); } void MenuManager::loadSettings() { // Note: on desktop, main() already reads settings.json and creates the window // directly in the right fullscreen state (see WIN32 main() in main.cpp) to avoid // a windowed -> fullscreen flash. The block below just keeps Environment::isFullscreen // in sync in case this is ever called from a path that didn't pre-apply it. // Language is loaded separately, even earlier (see FRG::loadLanguageSetting() // callers), because some location scripts start a dialogue as soon as they're // loaded, before this function ever runs. FRG::loadLanguageSetting(); const std::string content = FRG::readSavedTextFile("settings.json"); if (content.empty()) return; try { const nlohmann::json root = nlohmann::json::parse(content); if (root.contains("musicVolume")) audioPlayer_.setMusicVolume(root["musicVolume"].get()); if (root.contains("soundVolume")) audioPlayer_.setSoundVolume(root["soundVolume"].get()); if (root.contains("musicEnabled")) audioPlayer_.setMusicEnabled(root["musicEnabled"].get()); if (root.contains("soundEnabled")) audioPlayer_.setSoundEnabled(root["soundEnabled"].get()); if (root.contains("fullscreen")) { #ifndef EMSCRIPTEN Environment::setFullscreen(root["fullscreen"].get()); #endif } if (root.contains("tutorial")) tutorialEnabledSetting = root["tutorial"].get(); if (root.contains("shadow")) { if (shadowMapSettingsChangedFunc) { shadowMapSettingsChangedFunc(root["shadow"].get()); } } else { if (shadowMapSettingsChangedFunc) { shadowMapSettingsChangedFunc(shadowsEnabled); } } } catch (const std::exception& e) { std::cerr << "[settings] Failed to parse settings.json: " << e.what() << std::endl; } } bool MenuManager::isPortraitMode() const { return Environment::projectionWidth < Environment::projectionHeight; } void MenuManager::onOrientationChanged() { if (isPlayerInCreditsMenu) { uiManager.popMenu(); showCreditsScreen(); uiManager.updateAllLayouts(); return; } if (uiState_ == GameUiState::Inventory) { const int savedIndex = inventorySelectedIndex_; const bool wasOnItemDetail = (inventoryMenuStackDepth_ == 2); for (int i = 0; i < inventoryMenuStackDepth_; ++i) uiManager.popMenu(); inventoryMenuStackDepth_ = 0; uiState_ = GameUiState::Gameplay; inventorySelectedIndex_ = -1; openInventory(); const int itemCount = static_cast(inventory->getItems().size()); if (isPortraitMode() && wasOnItemDetail && savedIndex >= 0 && savedIndex < itemCount) { selectInventoryItem(savedIndex); } else if (!isPortraitMode() && savedIndex > 0 && savedIndex < itemCount) { selectInventoryItem(savedIndex); } uiManager.updateAllLayouts(); return; } if (uiState_ == GameUiState::QuestJournal) { const int savedQuestIndex = selectedQuestIndex; const bool wasOnItemDetail = (journalMenuStackDepth_ == 2); for (int i = 0; i < journalMenuStackDepth_; ++i) uiManager.popMenu(); journalMenuStackDepth_ = 0; uiState_ = GameUiState::Gameplay; selectedQuestIndex = -1; visibleQuestIds.clear(); openQuestJournal(); if (isPortraitMode() && wasOnItemDetail && savedQuestIndex >= 0 && savedQuestIndex < static_cast(visibleQuestIds.size())) { selectQuestByIndex(savedQuestIndex); } else if (!isPortraitMode() && savedQuestIndex > 0 && savedQuestIndex < static_cast(visibleQuestIds.size())) { selectQuestByIndex(savedQuestIndex); } uiManager.updateAllLayouts(); return; } if (uiState_ != GameUiState::PhoneScreen) return; const PhoneSubScreen savedSubScreen = currentPhoneSubScreen_; const int savedChatIndex = activeChatIndex_; // Collapse all stacked phone menus and reopen in the new orientation. activeChatIndex_ = -1; phoneChatVisibleBubbles_.clear(); const int depth = uiManager.menuStackSize(); for (int i = 0; i < depth; ++i) uiManager.popMenu(); uiState_ = GameUiState::Gameplay; switch (savedSubScreen) { case PhoneSubScreen::Bank: openPhoneScreen(); openPhoneBank(); break; case PhoneSubScreen::Video: openPhoneScreen(); openPhoneVideo(); break; case PhoneSubScreen::MapDorm: openPhoneScreen(); openPhoneMapScreen(isPortraitMode() ? verticalPhoneMapDormRoot : phoneMapDormRoot); currentPhoneSubScreen_ = PhoneSubScreen::MapDorm; break; case PhoneSubScreen::MapUni: openPhoneScreen(); openPhoneMapScreen(isPortraitMode() ? verticalPhoneMapUniRoot : phoneMapUniRoot); currentPhoneSubScreen_ = PhoneSubScreen::MapUni; break; case PhoneSubScreen::ChatList: openPhoneScreen(); openPhoneMessenger(); break; case PhoneSubScreen::Chat: { openPhoneScreen(); openPhoneMessenger(); std::shared_ptr chatRoot; if (savedChatIndex == 0) chatRoot = isPortraitMode() ? verticalPhoneChat1Root : phoneChat1Root; else if (savedChatIndex == 1) chatRoot = isPortraitMode() ? verticalPhoneChat2Root : phoneChat2Root; else if (savedChatIndex == 2) chatRoot = isPortraitMode() ? verticalPhoneChat3Root : phoneChat3Root; if (chatRoot) openPhoneChatFromList(savedChatIndex, chatRoot); break; } default: openPhoneScreen(); break; } uiManager.updateAllLayouts(); } } // namespace FRG