2540 lines
96 KiB
C++
2540 lines
96 KiB
C++
#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 <iostream>
|
|
#include <fstream>
|
|
#include <algorithm>
|
|
#include <string>
|
|
#include <SDL.h>
|
|
|
|
#ifdef __EMSCRIPTEN__
|
|
#include <emscripten.h>
|
|
#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<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;
|
|
|
|
|
|
//Localization
|
|
if (g_currentLanguage == Language::English)
|
|
{
|
|
return result + " som";
|
|
}
|
|
return result + u8" сом";
|
|
}
|
|
|
|
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 };
|
|
}
|
|
}
|
|
|
|
#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<UiNode> 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<Texture> 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<UiNode> 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<const char*> 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<int>(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<int>(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<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 ? soundToggleOn : soundToggleOff);
|
|
/*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 ? musicToggleOn : musicToggleOff);
|
|
/*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 });*/
|
|
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<float, 4>{ 0.2f, 0.9f, 0.2f, 1.0f }
|
|
: std::array<float, 4>{ 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<int>(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<float, 4>{ 0.2f, 0.9f, 0.2f, 1.0f }
|
|
: std::array<float, 4>{ 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<float, 4>{ 0.2f, 0.9f, 0.2f, 1.0f }
|
|
: std::array<float, 4>{ 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<std::function<void()>> 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<int>(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<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;
|
|
}
|
|
//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<int>(items.size())) return;
|
|
|
|
if (isPortraitMode() && inventoryMenuStackDepth_ == 1) {
|
|
openPortraitInventoryItem(index);
|
|
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;
|
|
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<UiNode> 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<UiNode> 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<UiNode>();
|
|
fboRoot->name = "chatFboRoot";
|
|
fboRoot->width = fboW;
|
|
fboRoot->height = fboH;
|
|
fboRoot->screenRect = { 0.0f, 0.0f, fboW, fboH };
|
|
|
|
auto fboContainer = std::make_shared<UiNode>();
|
|
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<FrameBuffer>(
|
|
static_cast<int>(fboW), static_cast<int>(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<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);
|
|
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<int>(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<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::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<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]);
|
|
auto textView = uiManager.findTextView(kObjNames[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 };
|
|
}
|
|
|
|
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<int>(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<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(std::shared_ptr<Texture> 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<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 = 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<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>());
|
|
if (root.contains("fullscreen")) {
|
|
#ifndef EMSCRIPTEN
|
|
Environment::setFullscreen(root["fullscreen"].get<bool>());
|
|
#endif
|
|
}
|
|
if (root.contains("tutorial"))
|
|
tutorialEnabledSetting = root["tutorial"].get<bool>();
|
|
if (root.contains("shadow")) {
|
|
if (shadowMapSettingsChangedFunc)
|
|
{
|
|
shadowMapSettingsChangedFunc(root["shadow"].get<bool>());
|
|
}
|
|
}
|
|
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<int>(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<int>(visibleQuestIds.size())) {
|
|
selectQuestByIndex(savedQuestIndex);
|
|
} else if (!isPortraitMode() && savedQuestIndex > 0 &&
|
|
savedQuestIndex < static_cast<int>(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<UiNode> 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
|