Working on tutorial refactoring

This commit is contained in:
Vladislav Khorev committed 2026-10-23 22:03:55 +06:00
1 parent ea8f64c940
commit 9fa120d4ad
82 files changed
+1877 -7563

No files matched your search

+133 -398
View File
@@ -247,49 +247,17 @@ namespace FRG {
}
void MenuManager::loadUiRoots(const std::string& zipFile) {
hudRoot = loadLocalizedUi("resources/w/ui/hud_step0.json", zipFile);
#if defined(__ANDROID__)
hudStep1Root = loadLocalizedUi("resources/w/ui/hud_step1android.json", zipFile);
hudStep2Root = loadLocalizedUi("resources/w/ui/hud_step2android.json", zipFile);
#else
hudStep1Root = loadLocalizedUi("resources/w/ui/hud_step1.json", zipFile);
hudStep2Root = loadLocalizedUi("resources/w/ui/hud_step2.json", zipFile);
#endif
#ifdef __ANDROID__
hudStep3Root = loadLocalizedUi("resources/w/ui/hud_step3android.json", zipFile);
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
hudStep3Root = loadLocalizedUi("resources/w/ui/hud_step3.json", zipFile);
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
hudStep4Root = loadLocalizedUi("resources/w/ui/hud_step4.json", zipFile);
hudStep5aRoot = loadLocalizedUi("resources/w/ui/hud_step5a.json", zipFile);
hudStep5bRoot = loadLocalizedUi("resources/w/ui/hud_step5b.json", zipFile);
#ifdef __ANDROID__
hudStep5abRoot = loadLocalizedUi("resources/w/ui/hud_step5abandroid.json", zipFile);
#else
hudStep5abRoot = loadLocalizedUi("resources/w/ui/hud_step5ab.json", zipFile);
#endif
hudUniExtRoot = loadLocalizedUi("resources/w/ui/hud_uni_ext.json", zipFile);
hudUniIntStep10Root = loadLocalizedUi("resources/w/ui/hud_uni_int_step10.json", zipFile);
#ifdef __ANDROID__
hudUniIntStep11Root = loadLocalizedUi("resources/w/ui/hud_uni_int_step11android.json", zipFile);
#else
hudUniIntStep11Root = loadLocalizedUi("resources/w/ui/hud_uni_int_step11.json", zipFile);
#endif
hudUniIntStep12Root = loadLocalizedUi("resources/w/ui/hud_uni_int_step12.json", zipFile);
#ifdef __ANDROID__
hudUniIntStep13Root = loadLocalizedUi("resources/w/ui/hud_uni_int_step13android.json", zipFile);
#else
hudUniIntStep13Root = loadLocalizedUi("resources/w/ui/hud_uni_int_step13.json", zipFile);
#endif
hudUniIntFullRoot = loadLocalizedUi("resources/w/ui/hud_uni_int_full.json", zipFile);
hudUniIntDarkFullRoot = loadLocalizedUi("resources/w/ui/hud_uni_int_dark_full.json", zipFile);
hudUniExtDarkRoot = loadLocalizedUi("resources/w/ui/hud_uni_ext_dark.json", zipFile);
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);
@@ -298,9 +266,6 @@ namespace FRG {
hudTopHintRoot_ = loadLocalizedUi("resources/w/ui/hud_top_hint01.json", zipFile);
phoneMainRoot = loadLocalizedUi("resources/w/ui/screen_phone.json", zipFile);
phoneMainHintARoot = loadLocalizedUi("resources/w/ui/screen_phone_hint001.json", zipFile);
phoneMainHintBRoot = loadLocalizedUi("resources/w/ui/screen_phone_hint002.json", zipFile);
phoneMainHintABRoot = loadLocalizedUi("resources/w/ui/screen_phone_hint001_002.json", zipFile);
phoneBankRoot = loadLocalizedUi("resources/w/ui/screen_phone_bank.json", zipFile);
phoneVideoRoot = loadLocalizedUi("resources/w/ui/screen_phone_video.json", zipFile);
@@ -308,18 +273,13 @@ namespace FRG {
phoneMapUniRoot = loadLocalizedUi("resources/w/ui/screen_phone_map_uni.json", zipFile);
verticalPhoneMainRoot = loadLocalizedUi("resources/w/ui/portrait_screen_phone.json", zipFile);
verticalPhoneMainHintARoot = loadLocalizedUi("resources/w/ui/portrait_screen_phone_hint001.json", zipFile);
verticalPhoneMainHintBRoot = loadLocalizedUi("resources/w/ui/portrait_screen_phone_hint002.json", zipFile);
verticalPhoneMainHintABRoot= loadLocalizedUi("resources/w/ui/portrait_screen_phone_hint001_002.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);
phoneChatListHintRoot = loadLocalizedUi("resources/w/ui/screen_phone_chat_list_hint001.json", zipFile);
verticalPhoneChatListRoot = loadLocalizedUi("resources/w/ui/portrait_screen_phone_chat_list.json", zipFile);
verticalPhoneChatListHintRoot = loadLocalizedUi("resources/w/ui/portrait_screen_phone_chat_list_hint001.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);
@@ -417,64 +377,83 @@ namespace FRG {
}
void MenuManager::enterGameplay(bool isFromLoad) {
if (!isFromLoad && uiState_ == GameUiState::MainMenu && startGameFunc) startGameFunc();
uiState_ = GameUiState::Gameplay;
uiManager.replaceRoot(hudRoot);
topUiManager.replaceRoot(hudTopHintRoot_);
if (isFromLoad) {
uiManager.clearMenuStack();
topUiManager.clearMenuStack();
}
applyCurrentHealthBar();
uiManager.setButtonCallback("inventoryButton", [this](const std::string&) {
openInventory();
});
if (uiManager.findButton("settingsButton")) {
uiManager.setButtonCallback("settingsButton", [this](const std::string&) {
showModalMenuScreen();
});
}
if (isFromLoad)
applyHudForCurrentState();
// A new game resets the state 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 (!isFromLoad && uiState_ == GameUiState::MainMenu && startGameFunc)
startGameFunc();
else
showGameplayHud();
}
void MenuManager::applyHudForCurrentState() {
// Clear the step-0 dialogue hint unless we're actually at step 0.
if (gameState_.tutorialStep != TutorialStep::Step0)
topUiManager.replaceRoot(nullptr);
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
}
// Non-dorm locations: reuse the existing location-change logic.
if (loc == "uni_exterior" || loc == "uni_interior") {
onLocationChanged(loc);
return;
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 (or pre-first-teleport): select HUD root by tutorial step.
std::shared_ptr<UiNode> nextRoot;
switch (gameState_.tutorialStep) {
case TutorialStep::Step0: nextRoot = hudRoot; break;
case TutorialStep::Step1: nextRoot = hudStep1Root; break;
case TutorialStep::Step2: nextRoot = hudStep2Root; break;
case TutorialStep::Step3: nextRoot = hudStep3Root; break;
case TutorialStep::Step4: nextRoot = hudStep4Root; break;
default: break; // Step5/6 handled below
}
// 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);
if (nextRoot) {
uiManager.replaceRoot(nextRoot);
applyCurrentHealthBar();
uiManager.setButtonCallback("inventoryButton", [this](const std::string&) { openInventory(); });
if (uiManager.findButton("settingsButton"))
uiManager.setButtonCallback("settingsButton", [this](const std::string&) { showModalMenuScreen(); });
} else {
// Step5 or Step6: pick the right variant and apply hint visibility.
refreshItemPickupHud();
}
// 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() {
@@ -762,12 +741,35 @@ namespace FRG {
uiManager.setTextButtonCallback("soundToggleButton", [this](const std::string&) {
audioPlayer_.setSoundEnabled(!audioPlayer_.isSoundEnabled());
const bool on = audioPlayer_.isSoundEnabled();
uiManager.setTextButtonText("soundToggleButton", on ? soundToggleOn : soundToggleOff);
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() {
@@ -956,6 +958,8 @@ namespace FRG {
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
@@ -1264,9 +1268,6 @@ namespace FRG {
gameState_.tutorialJournalScreenOpened = true;
journalMenuStackDepth_ = 1;
uiManager.setNodeVisible("hint6b", false);
uiManager.setNodeVisible("hint6barrow", false);
const bool portrait = isPortraitMode();
uiManager.pushMenuFromSavedRoot(portrait ? portraitQuestJournalListRoot : questJournalRoot);
@@ -1330,24 +1331,8 @@ namespace FRG {
uiState_ = GameUiState::PhoneScreen;
currentPhoneSubScreen_ = PhoneSubScreen::Main;
if (isPortraitMode()) {
if (gameState_.tutorialNeedOpenTaxiScreen && (gameState_.tutorialPhoneChatScreenOpened == false))
uiManager.pushMenuFromSavedRoot(verticalPhoneMainHintABRoot);
else if (gameState_.tutorialNeedOpenTaxiScreen)
uiManager.pushMenuFromSavedRoot(verticalPhoneMainHintBRoot);
else if (gameState_.tutorialPhoneChatScreenOpened == false)
uiManager.pushMenuFromSavedRoot(verticalPhoneMainHintARoot);
else
uiManager.pushMenuFromSavedRoot(verticalPhoneMainRoot);
} else if (gameState_.tutorialNeedOpenTaxiScreen && (gameState_.tutorialPhoneChatScreenOpened == false)) {
uiManager.pushMenuFromSavedRoot(phoneMainHintABRoot);
} else if (gameState_.tutorialNeedOpenTaxiScreen) {
uiManager.pushMenuFromSavedRoot(phoneMainHintBRoot);
} else if (gameState_.tutorialPhoneChatScreenOpened == false) {
uiManager.pushMenuFromSavedRoot(phoneMainHintARoot);
} else {
uiManager.pushMenuFromSavedRoot(phoneMainRoot);
}
uiManager.pushMenuFromSavedRoot(isPortraitMode() ? verticalPhoneMainRoot : phoneMainRoot);
refreshHudVisibility();
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
@@ -1400,14 +1385,8 @@ namespace FRG {
void MenuManager::openPhoneMessenger() {
currentPhoneSubScreen_ = PhoneSubScreen::ChatList;
if (gameState_.tutorialPhoneChatScreenOpened)
{
uiManager.pushMenuFromSavedRoot(isPortraitMode() ? verticalPhoneChatListRoot : phoneChatListRoot);
}
else
{
uiManager.pushMenuFromSavedRoot(isPortraitMode() ? verticalPhoneChatListHintRoot : phoneChatListHintRoot);
}
uiManager.pushMenuFromSavedRoot(isPortraitMode() ? verticalPhoneChatListRoot : phoneChatListRoot);
refreshHudVisibility();
refreshChatUnreadIndicators();
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
@@ -1625,14 +1604,7 @@ namespace FRG {
chatFrameBuffer_.reset();
uiManager.popMenu();
uiManager.updateAllLayouts();
if (gameState_.tutorialPhoneChatScreenOpened)
{
uiManager.setNodeVisible("hint_m002", false);
}
else
{
uiManager.setNodeVisible("hint_m002", true);
}
refreshHudVisibility();
refreshChatUnreadIndicators();
}
@@ -1652,31 +1624,7 @@ namespace FRG {
const int depth = uiManager.menuStackSize();
for (int i = 0; i < depth; ++i) uiManager.popMenu();
uiManager.updateAllLayouts();
if (gameState_.tutorialNeedOpenTaxiScreen)
{
uiManager.setNodeVisible("hint7", true);
uiManager.setNodeVisible("hint7arrow", true);
uiManager.setNodeVisible("hint6a", false);
uiManager.setNodeVisible("hint6aarrow", false);
}
else
{
uiManager.setNodeVisible("hint7", false);
uiManager.setNodeVisible("hint7arrow", false);
if (gameState_.tutorialPhoneChatScreenOpened)
{
uiManager.setNodeVisible("hint6a", false);
uiManager.setNodeVisible("hint6aarrow", false);
}
else
{
uiManager.setNodeVisible("hint6a", true);
uiManager.setNodeVisible("hint6aarrow", true);
}
}
refreshHudVisibility();
}
void MenuManager::closePhoneScreenFromChat() {
@@ -1727,80 +1675,9 @@ namespace FRG {
void MenuManager::tutorialShowTaxiHint()
{
logger() << "tutorialShowTaxiHint" << std::endl;
gameState_.tutorialNeedOpenTaxiScreen = true;
if (uiState_ == GameUiState::Gameplay)
{
uiManager.setNodeVisible("hint6a", false);
uiManager.setNodeVisible("hint6aarrow", false);
uiManager.setNodeVisible("hint7", true);
uiManager.setNodeVisible("hint7arrow", true);
}
}
// Registers phoneButton / journalButton callbacks on the current HUD root
// and hides any hints that have already been completed.
// Called after every replaceRoot during step 5.
void MenuManager::setupStep5Callbacks() {
if (uiManager.findButton("phoneButton")) {
uiManager.setButtonCallback("phoneButton", [this](const std::string&) {
openPhoneScreen();
});
}
if (uiManager.findButton("inventoryButton")) {
uiManager.setButtonCallback("inventoryButton", [this](const std::string&) {
openInventory();
});
}
if (uiManager.findButton("journalButton")) {
uiManager.setButtonCallback("journalButton", [this](const std::string&) {
openQuestJournal();
});
}
if (uiManager.findButton("settingsButton")) {
uiManager.setButtonCallback("settingsButton", [this](const std::string&) {
showModalMenuScreen();
});
}
if (gameState_.tutorialNeedOpenTaxiScreen)
{
uiManager.setNodeVisible("hint6a", false);
uiManager.setNodeVisible("hint6aarrow", false);
uiManager.setNodeVisible("hint7", true);
uiManager.setNodeVisible("hint7arrow", true);
}
else
{
uiManager.setNodeVisible("hint7", false);
uiManager.setNodeVisible("hint7arrow", false);
}
if (gameState_.tutorialPhoneChatScreenOpened) {
uiManager.setNodeVisible("hint6a", false);
uiManager.setNodeVisible("hint6aarrow", false);
}
else
{
if (!gameState_.tutorialNeedOpenTaxiScreen)
{
uiManager.setNodeVisible("hint6a", true);
uiManager.setNodeVisible("hint6aarrow", true);
}
}
if (gameState_.tutorialJournalScreenOpened)
{
uiManager.setNodeVisible("hint6b", false);
uiManager.setNodeVisible("hint6barrow", false);
}
else
{
uiManager.setNodeVisible("hint6b", true);
uiManager.setNodeVisible("hint6barrow", true);
}
if (gameState_.tutorialPhoneChatScreenOpened && gameState_.tutorialJournalScreenOpened) {
gameState_.tutorialStep = TutorialStep::Step6;
}
if (!gameState_.isTutorialActive()) return;
gameState_.tutorialNeedOpenTaxiScreen = true; // cleared when the taxi is ordered
refreshHudVisibility();
}
void MenuManager::setupGameplayHudCallbacks() {
@@ -1823,28 +1700,9 @@ namespace FRG {
void MenuManager::onLocationChanged(const std::string& locationName) {
if (uiState_ != GameUiState::Gameplay) return;
gameState_.currentLocationName = locationName;
showGameplayHud();
if (locationName == "uni_exterior") {
uiManager.replaceRoot(gameState_.isDarklands ? hudUniExtDarkRoot : hudUniExtRoot);
applyCurrentHealthBar();
setupGameplayHudCallbacks();
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 (locationName == "uni_interior") {
applyUniIntHud();
if (locationName == "uni_exterior" || locationName == "uni_interior") {
if (gameState_.isDarklands)
{
audioPlayer_.crossFadeMusicAsync("music/bishkek fight calm.ogg");
@@ -1861,11 +1719,6 @@ namespace FRG {
lastMusicTrack = "music/bishkek univer day.ogg";
}
} else {
// Returning to dorm: reuse step5ab, suppress already-completed hints
uiManager.replaceRoot(hudStep5abRoot);
applyCurrentHealthBar();
setupStep5Callbacks();
if (gameState_.isNight)
{
audioPlayer_.crossFadeMusicAsync("music/bishkek night.ogg");
@@ -1879,96 +1732,11 @@ namespace FRG {
}
}
void MenuManager::advanceTutorialStep() {
std::shared_ptr<UiNode> nextRoot;
switch (gameState_.tutorialStep) {
case TutorialStep::Step0:
gameState_.tutorialStep = TutorialStep::Step1;
nextRoot = hudStep1Root;
topUiManager.replaceRoot(nullptr);
break;
case TutorialStep::Step1:
gameState_.tutorialStep = TutorialStep::Step2;
nextRoot = hudStep2Root;
break;
case TutorialStep::Step2:
gameState_.tutorialStep = TutorialStep::Step3;
nextRoot = hudStep3Root;
break;
case TutorialStep::Step3:
gameState_.tutorialStep = TutorialStep::Step4;
nextRoot = hudStep4Root;
if (tutorialUnlockInteractiveObjectsFunc)
{
tutorialUnlockInteractiveObjectsFunc();
tutorialUnlockInteractiveObjectsFunc = nullptr;
}
break;
default:
return; // Step4/Step5 transitions are driven by onItemPickedUp
}
if (uiState_ == GameUiState::Gameplay && nextRoot) {
uiManager.replaceRoot(nextRoot);
applyCurrentHealthBar();
uiManager.setButtonCallback("inventoryButton", [this](const std::string&) {
openInventory();
});
if (uiManager.findButton("settingsButton")) {
uiManager.setButtonCallback("settingsButton", [this](const std::string&) {
showModalMenuScreen();
});
}
}
}
void MenuManager::onItemPickedUp(const std::string& itemId) {
if (itemId == "note_spell" && gameState_.uniIntTutorialState == UniIntTutorialState::Step10) {
gameState_.uniIntTutorialState = UniIntTutorialState::Step11;
if (gameState_.currentLocationName == "uni_interior" && uiState_ == GameUiState::Gameplay)
applyUniIntHud();
}
if (gameState_.tutorialStep != TutorialStep::Step4 && gameState_.tutorialStep != TutorialStep::Step5) {
return;
}
// Dorm tutorial HUD logic must not run in other locations.
// currentLocationName is empty only before the first teleport (still in dorm).
if (!gameState_.currentLocationName.empty() && gameState_.currentLocationName != "location_dorm") {
return;
}
if (itemId == "phone") gameState_.tutorialPhonePickedUp = true;
if (itemId == "journal") gameState_.tutorialJournalPickedUp = true;
if (gameState_.tutorialStep == TutorialStep::Step4) {
gameState_.tutorialStep = TutorialStep::Step5;
}
refreshItemPickupHud();
}
void MenuManager::refreshItemPickupHud() {
if (uiState_ != GameUiState::Gameplay) return;
std::shared_ptr<UiNode> nextRoot;
if (gameState_.tutorialPhonePickedUp && gameState_.tutorialJournalPickedUp) {
nextRoot = hudStep5abRoot;
} else if (gameState_.tutorialPhonePickedUp) {
nextRoot = hudStep5aRoot;
} else if (gameState_.tutorialJournalPickedUp) {
nextRoot = hudStep5bRoot;
}
if (nextRoot) {
uiManager.replaceRoot(nextRoot);
applyCurrentHealthBar();
// Register item-screen buttons and re-apply any already-completed hint visibility.
setupStep5Callbacks();
}
// The new item's HUD button appears through refreshHudVisibility().
if (itemId == "phone" || itemId == "journal")
gameState_.completeTutorialStep(TutorialStep::PickUpItem);
refreshHudVisibility();
}
void MenuManager::refreshQuestJournalUi() {
@@ -2351,70 +2119,32 @@ namespace FRG {
{
logger() << "MenuManager::setDarklandsMode called" << std::endl;
if (gameState_.currentLocationName == "uni_interior") {
if (enabled && gameState_.uniIntTutorialState == UniIntTutorialState::Step11) {
gameState_.uniIntTutorialState = UniIntTutorialState::DarklandsActive;
}
applyUniIntHud();
} else if (gameState_.currentLocationName == "uni_exterior") {
if (uiState_ == GameUiState::Gameplay) {
uiManager.replaceRoot(enabled ? hudUniExtDarkRoot : hudUniExtRoot);
applyCurrentHealthBar();
setupGameplayHudCallbacks();
}
} else {
uiManager.setNodeVisible("darklandsButton", !enabled);
uiManager.setNodeVisible("phoneButton", !enabled);
}
}
if (enabled && gameState_.currentLocationName == "uni_interior" && inventory->hasItem("note_spell"))
gameState_.completeTutorialStep(TutorialStep::EnterDarklands);
void MenuManager::applyUniIntHud()
{
if (uiState_ != GameUiState::Gameplay) return;
std::shared_ptr<UiNode> root;
if (gameState_.isDarklands) {
switch (gameState_.uniIntTutorialState) {
case UniIntTutorialState::DarklandsStep13: root = hudUniIntStep13Root; break;
case UniIntTutorialState::DarklandsFull: root = hudUniIntDarkFullRoot; break;
default: root = hudUniIntStep12Root; break;
}
} else {
switch (gameState_.uniIntTutorialState) {
case UniIntTutorialState::Step10: root = hudUniIntStep10Root; break;
case UniIntTutorialState::Step11: root = hudUniIntStep11Root; break;
default: root = hudUniIntFullRoot; break;
}
}
uiManager.replaceRoot(root);
applyCurrentHealthBar();
setupGameplayHudCallbacks();
if (uiState_ == GameUiState::Gameplay)
showGameplayHud(); // darklands has its own HUD layouts
}
void MenuManager::onPlayerStartedWalking()
{
if (gameState_.currentLocationName == "uni_interior"
&& gameState_.isDarklands
&& gameState_.uniIntTutorialState == UniIntTutorialState::DarklandsActive) {
uiManager.setNodeVisible("hint_darklands003", false);
uiManager.setNodeVisible("hint_darklands003_arrow", false);
}
if (gameState_.currentLocationName == "uni_interior" && gameState_.isDarklands)
gameState_.completeTutorialStep(TutorialStep::DarklandsHealth);
}
void MenuManager::advanceUniIntDarklandsHud()
{
if (gameState_.uniIntTutorialState != UniIntTutorialState::DarklandsActive) return;
gameState_.uniIntTutorialState = UniIntTutorialState::DarklandsStep13;
if (gameState_.currentLocationName == "uni_interior" && gameState_.isDarklands && uiState_ == GameUiState::Gameplay)
applyUniIntHud();
// 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()
{
if (gameState_.uniIntTutorialState != UniIntTutorialState::DarklandsActive
&& gameState_.uniIntTutorialState != UniIntTutorialState::DarklandsStep13) return;
gameState_.uniIntTutorialState = UniIntTutorialState::DarklandsFull;
if (gameState_.currentLocationName == "uni_interior" && gameState_.isDarklands && uiState_ == GameUiState::Gameplay)
applyUniIntHud();
// 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) {
@@ -2554,6 +2284,8 @@ namespace FRG {
void MenuManager::update(float deltaMs) {
updateToasts(deltaMs);
chatUiManager.update(deltaMs);
if (uiState_ != GameUiState::MainMenu && uiState_ != GameUiState::About)
refreshHudVisibility();
}
void MenuManager::drawChatFbo(Renderer& renderer) {
@@ -2615,6 +2347,7 @@ namespace FRG {
root["fullscreen"] = Environment::isFullscreen;
root["language"] = FRG::languageToCode(FRG::g_currentLanguage);
root["shadow"] = shadowsEnabled;
root["tutorial"] = tutorialEnabledSetting;
saveJsonToFile(root, "settings.json");
}
@@ -2646,6 +2379,8 @@ namespace FRG {
Environment::setFullscreen(root["fullscreen"].get<bool>());
#endif
}
if (root.contains("tutorial"))
tutorialEnabledSetting = root["tutorial"].get<bool>();
if (root.contains("shadow")) {
if (shadowMapSettingsChangedFunc)
{