Working on tutorial refactoring
This commit is contained in:
1 parent
ea8f64c940
commit
9fa120d4ad
82 files changed
+1877
-7563
No files matched your search
+2
-3
@@ -16,14 +16,13 @@
|
||||
namespace FRG {
|
||||
|
||||
|
||||
|
||||
int Environment::windowHeaderHeight = 0;
|
||||
int Environment::width = CONST_DEFAULT_WIDTH;
|
||||
int Environment::height = CONST_DEFAULT_HEIGHT;
|
||||
float Environment::zoom = DEFAULT_ZOOM;
|
||||
|
||||
bool Environment::enableLogging = false;
|
||||
//bool Environment::enableLogging = true;
|
||||
//bool Environment::enableLogging = false;
|
||||
bool Environment::enableLogging = true;
|
||||
|
||||
SDL_Window* Environment::window = nullptr;
|
||||
|
||||
|
||||
+26
-31
@@ -394,7 +394,9 @@ namespace FRG
|
||||
menuManager.startGameFunc = [this]() {
|
||||
mainThreadHandler.EnqueueMainThreadTask([this]() {
|
||||
performResetToInitialState();
|
||||
applyTutorialSetting();
|
||||
gameState.currentLocationName = "location_dorm";
|
||||
menuManager.showGameplayHud(); // first HUD frame drawn from the fresh state
|
||||
currentLocation()->scriptEngine.callLocationEnterCallback();
|
||||
this->audioPlayer->crossFadeMusicAsync("music/obshaga.ogg");
|
||||
menuManager.lastMusicTrack = "music/obshaga.ogg";
|
||||
@@ -471,14 +473,6 @@ namespace FRG
|
||||
it->second->onPlayerTaxiRequired = [this]() {
|
||||
menuManager.tutorialShowTaxiHint();
|
||||
};
|
||||
|
||||
menuManager.tutorialUnlockInteractiveObjectsFunc = [this]()
|
||||
{
|
||||
if (gameState.locations["location_dorm"])
|
||||
{
|
||||
gameState.locations["location_dorm"]->state.tutorialInteractiveObjectsLocked = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void Game::performFullReset() {
|
||||
@@ -491,10 +485,7 @@ namespace FRG
|
||||
gameState.money = 5500;
|
||||
gameState.globalInts.clear();
|
||||
gameState.globalFloats.clear();
|
||||
gameState.tutorialStep = TutorialStep::Step0;
|
||||
gameState.uniIntTutorialState = UniIntTutorialState::Step10;
|
||||
gameState.tutorialPhonePickedUp = false;
|
||||
gameState.tutorialJournalPickedUp = false;
|
||||
gameState.tutorialStep = TutorialStep::AdvanceDialogue;
|
||||
gameState.tutorialPhoneChatScreenOpened = false;
|
||||
gameState.tutorialJournalScreenOpened = false;
|
||||
gameState.tutorialNeedOpenTaxiScreen = false;
|
||||
@@ -547,7 +538,7 @@ namespace FRG
|
||||
// sol2 state (GameState::load only touches map entries
|
||||
// that already exist).
|
||||
// Locations survive this reset, so any one-shot callbacks they consumed
|
||||
// during the previous playthrough (tutorial unlock, taxi request) must be
|
||||
// during the previous playthrough (taxi request) must be
|
||||
// re-armed here -- createLocations() normally does this, but doesn't run
|
||||
// on this fast path.
|
||||
rearmDormOneShotCallbacks();
|
||||
@@ -564,6 +555,13 @@ namespace FRG
|
||||
performFullReset();
|
||||
}
|
||||
|
||||
// Runs after either reset path. start_state.json always holds the start of the
|
||||
// tutorial; a disabled tutorial is simply one that is already completed.
|
||||
void Game::applyTutorialSetting() {
|
||||
if (!menuManager.tutorialEnabledSetting)
|
||||
gameState.tutorialStep = TutorialStep::Completed;
|
||||
}
|
||||
|
||||
void Game::saveInitialState() {
|
||||
nlohmann::json root;
|
||||
gameState.save(root);
|
||||
@@ -837,7 +835,10 @@ namespace FRG
|
||||
if (gameState.locations["location_dorm"]->player)
|
||||
gameState.locations["location_dorm"]->player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
||||
|
||||
gameState.locations["location_dorm"]->state.tutorialInteractiveObjectsLocked = true;
|
||||
// Objects can be picked up once the tutorial reaches "pick up an item".
|
||||
gameState.locations["location_dorm"]->areInteractiveObjectsLocked = [this]() {
|
||||
return gameState.tutorialStep < TutorialStep::PickUpItem;
|
||||
};
|
||||
|
||||
//Dorm set up taxi:
|
||||
|
||||
@@ -928,15 +929,13 @@ namespace FRG
|
||||
}
|
||||
|
||||
// Wire tutorial advance callbacks for all locations.
|
||||
// advanceTutorialStep() guards against double-advancing, so sharing is safe.
|
||||
// completeTutorialStep() only acts on the current step, so sharing is safe.
|
||||
for (auto& [name, loc] : gameState.locations) {
|
||||
loc->dialogueSystem.setOnDialogueAdvanced([this]() {
|
||||
menuManager.advanceTutorialStep();
|
||||
gameState.completeTutorialStep(TutorialStep::AdvanceDialogue);
|
||||
});
|
||||
loc->onPlayerFloorWalk = [this]() {
|
||||
if (gameState.tutorialStep == TutorialStep::Step2) {
|
||||
menuManager.advanceTutorialStep();
|
||||
}
|
||||
gameState.completeTutorialStep(TutorialStep::Walk);
|
||||
menuManager.onPlayerStartedWalking();
|
||||
};
|
||||
}
|
||||
@@ -1434,10 +1433,8 @@ namespace FRG
|
||||
Environment::zoom = zoomMax;
|
||||
}
|
||||
//logger() << "Current zoom: " << Environment::zoom << std::endl;
|
||||
// Tutorial step3 → step4: any mouse-wheel scroll counts as "zoom gesture".
|
||||
if (gameState.tutorialStep == TutorialStep::Step3) {
|
||||
menuManager.advanceTutorialStep();
|
||||
}
|
||||
// Tutorial: any mouse-wheel scroll counts as the "zoom gesture".
|
||||
gameState.completeTutorialStep(TutorialStep::Zoom);
|
||||
}
|
||||
|
||||
if (event.type == SDL_KEYDOWN && event.key.repeat == 0) {
|
||||
@@ -1595,11 +1592,9 @@ namespace FRG
|
||||
if (newZoom > zoomMax) newZoom = zoomMax;
|
||||
Environment::zoom = newZoom;
|
||||
|
||||
// Tutorial step3 → step4: detect a significant pinch-zoom (≥ 2 zoom units).
|
||||
if (gameState.tutorialStep == TutorialStep::Step3) {
|
||||
if (std::abs(Environment::zoom - pinchStartZoom) >= 2.0f) {
|
||||
menuManager.advanceTutorialStep();
|
||||
}
|
||||
// Tutorial: detect a significant pinch-zoom (≥ 2 zoom units).
|
||||
if (std::abs(Environment::zoom - pinchStartZoom) >= 2.0f) {
|
||||
gameState.completeTutorialStep(TutorialStep::Zoom);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1738,16 +1733,16 @@ namespace FRG
|
||||
// Forwarded for dialogue hover and (when cameraDragging) camera rotation.
|
||||
currentLocation()->handleMotion(fingerId, eventX, eventY, mx, my);
|
||||
|
||||
// Tutorial step1 → step2: detect a significant camera rotation on BOTH axes.
|
||||
// Tutorial: detect a significant camera rotation on BOTH axes.
|
||||
// ~0.15 rad (≈8.6°) per axis ensures the user intentionally panned in 2D,
|
||||
// not just nudged a single axis by accident.
|
||||
static constexpr float TUTORIAL_ROTATION_THRESHOLD = 0.15f;
|
||||
if (cameraDragging
|
||||
&& gameState.tutorialStep == TutorialStep::Step1) {
|
||||
&& gameState.tutorialStep == TutorialStep::RotateCamera) {
|
||||
float deltaAz = std::abs(currentLocation()->state.cameraAzimuth - dragStartAzimuth);
|
||||
float deltaInc = std::abs(currentLocation()->state.cameraInclination - dragStartInclination);
|
||||
if (deltaAz >= TUTORIAL_ROTATION_THRESHOLD && deltaInc >= TUTORIAL_ROTATION_THRESHOLD) {
|
||||
menuManager.advanceTutorialStep();
|
||||
gameState.completeTutorialStep(TutorialStep::RotateCamera);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -137,9 +137,11 @@ namespace FRG {
|
||||
// never synchronously from a Lua/cutscene call stack.
|
||||
void performResetToInitialState();
|
||||
void performFullReset();
|
||||
// Applies MenuManager::tutorialEnabledSetting to the freshly reset state (new games only).
|
||||
void applyTutorialSetting();
|
||||
void saveInitialState();
|
||||
|
||||
// (Re-)arms one-shot gameplay callbacks (tutorial unlock, taxi request) that
|
||||
// (Re-)arms one-shot gameplay callbacks (taxi request) that
|
||||
// createLocations() wires up but that get consumed (set to nullptr) once used
|
||||
// during play. Must be called after gameState.load() on the fast reset path,
|
||||
// since that path keeps the existing Location objects instead of recreating them.
|
||||
|
||||
+27
-8
@@ -5,6 +5,27 @@
|
||||
|
||||
namespace FRG {
|
||||
|
||||
// Saves made before the tutorial rework kept the dorm and the university tutorials
|
||||
// as two state machines (tutorialStep 0..6 + uniIntTutorialState 0..4); map them
|
||||
// onto the single TutorialStep sequence.
|
||||
static TutorialStep migrateLegacyTutorialStep(int dormStep, int uniIntState)
|
||||
{
|
||||
if (dormStep <= 4) return static_cast<TutorialStep>(dormStep); // Step0..Step4 kept their values
|
||||
switch (uniIntState) {
|
||||
case 2: return TutorialStep::DarklandsHealth; // DarklandsActive
|
||||
case 3: return TutorialStep::DarklandsAttack; // DarklandsStep13
|
||||
case 4: return TutorialStep::Completed; // DarklandsFull
|
||||
default: return TutorialStep::EnterDarklands; // Step10 / Step11
|
||||
}
|
||||
}
|
||||
|
||||
bool GameState::completeTutorialStep(TutorialStep step)
|
||||
{
|
||||
if (tutorialStep != step || step == TutorialStep::Completed) return false;
|
||||
tutorialStep = static_cast<TutorialStep>(static_cast<int>(step) + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
void GameState::save(nlohmann::json& out) const
|
||||
{
|
||||
// Timestamp
|
||||
@@ -35,10 +56,7 @@ void GameState::save(nlohmann::json& out) const
|
||||
out["globalFloats"] = std::move(gf);
|
||||
|
||||
// --- Tutorial ---
|
||||
out["tutorialStep"] = static_cast<int>(tutorialStep);
|
||||
out["uniIntTutorialState"] = static_cast<int>(uniIntTutorialState);
|
||||
out["tutorialPhonePickedUp"] = tutorialPhonePickedUp;
|
||||
out["tutorialJournalPickedUp"] = tutorialJournalPickedUp;
|
||||
out["tutorialStep"] = static_cast<int>(tutorialStep);
|
||||
out["tutorialPhoneChatScreenOpened"] = tutorialPhoneChatScreenOpened;
|
||||
out["tutorialJournalScreenOpened"] = tutorialJournalScreenOpened;
|
||||
out["tutorialNeedOpenTaxiScreen"] = tutorialNeedOpenTaxiScreen;
|
||||
@@ -118,10 +136,11 @@ void GameState::load(const nlohmann::json& in)
|
||||
//logger() << "GameState::load step 5" << std::endl;
|
||||
|
||||
// --- Tutorial ---
|
||||
tutorialStep = static_cast<TutorialStep>(in.value("tutorialStep", static_cast<int>(tutorialStep)));
|
||||
uniIntTutorialState = static_cast<UniIntTutorialState>(in.value("uniIntTutorialState", static_cast<int>(uniIntTutorialState)));
|
||||
tutorialPhonePickedUp = in.value("tutorialPhonePickedUp", tutorialPhonePickedUp);
|
||||
tutorialJournalPickedUp = in.value("tutorialJournalPickedUp", tutorialJournalPickedUp);
|
||||
if (in.contains("uniIntTutorialState")) {
|
||||
tutorialStep = migrateLegacyTutorialStep(in.value("tutorialStep", 0), in["uniIntTutorialState"].get<int>());
|
||||
} else {
|
||||
tutorialStep = static_cast<TutorialStep>(in.value("tutorialStep", static_cast<int>(tutorialStep)));
|
||||
}
|
||||
tutorialPhoneChatScreenOpened = in.value("tutorialPhoneChatScreenOpened", tutorialPhoneChatScreenOpened);
|
||||
tutorialJournalScreenOpened = in.value("tutorialJournalScreenOpened", tutorialJournalScreenOpened);
|
||||
tutorialNeedOpenTaxiScreen = in.value("tutorialNeedOpenTaxiScreen", tutorialNeedOpenTaxiScreen);
|
||||
|
||||
+22
-22
@@ -11,22 +11,20 @@
|
||||
|
||||
namespace FRG {
|
||||
|
||||
// The tutorial is one linear sequence: each step shows its hint until the player
|
||||
// performs the action, then moves on to the next step (GameState::completeTutorialStep).
|
||||
// Values are saved; AdvanceDialogue..PickUpItem keep the numbers of the old Step0..Step4.
|
||||
enum class TutorialStep {
|
||||
Step0, // Dialogue hint: "click to advance"
|
||||
Step1, // Camera rotation hint
|
||||
Step2, // Floor tap / walk hint
|
||||
Step3, // Pinch-zoom hint
|
||||
Step4, // Pick-up item hint
|
||||
Step5, // Post-pickup reaction
|
||||
Step6, // Tutorial complete
|
||||
};
|
||||
|
||||
enum class UniIntTutorialState {
|
||||
Step10,
|
||||
Step11,
|
||||
DarklandsActive,
|
||||
DarklandsStep13,
|
||||
DarklandsFull
|
||||
AdvanceDialogue, // dorm: "click to advance the dialogue"
|
||||
RotateCamera, // dorm
|
||||
Walk, // dorm: tap the floor
|
||||
Zoom, // dorm; interactive objects stay locked until this is done
|
||||
PickUpItem, // dorm: take the phone or the journal
|
||||
EnterDarklands, // uni_interior: press the darklands button (once the spell note is picked up)
|
||||
DarklandsHealth, // uni_interior darklands: health bar hint, until the player walks
|
||||
DarklandsExplore, // uni_interior darklands: no hint, until the ghost zone is reached
|
||||
DarklandsAttack, // uni_interior darklands: attack hint, until an enemy is killed
|
||||
Completed // no more hints; a new game starts here when the tutorial is disabled
|
||||
};
|
||||
|
||||
struct StoredChatMessage {
|
||||
@@ -56,13 +54,15 @@ struct GameState : public ISaveable {
|
||||
Quest::QuestJournal questJournal;
|
||||
|
||||
// --- Tutorial ---
|
||||
TutorialStep tutorialStep = TutorialStep::Step0;
|
||||
UniIntTutorialState uniIntTutorialState = UniIntTutorialState::Step10;
|
||||
bool tutorialPhonePickedUp = false;
|
||||
bool tutorialJournalPickedUp = false;
|
||||
bool tutorialPhoneChatScreenOpened = false;
|
||||
bool tutorialJournalScreenOpened = false;
|
||||
bool tutorialNeedOpenTaxiScreen = false;
|
||||
TutorialStep tutorialStep = TutorialStep::AdvanceDialogue;
|
||||
// Hints that run alongside the linear steps, until the tutorial is Completed.
|
||||
bool tutorialPhoneChatScreenOpened = false; // "open the messenger" hint is done
|
||||
bool tutorialJournalScreenOpened = false; // "open the journal" hint is done
|
||||
bool tutorialNeedOpenTaxiScreen = false; // "call a taxi" hint is showing
|
||||
|
||||
bool isTutorialActive() const { return tutorialStep != TutorialStep::Completed; }
|
||||
// Moves to the next step if `step` is the current one; returns whether it did.
|
||||
bool completeTutorialStep(TutorialStep step);
|
||||
|
||||
// --- Phone / chat ---
|
||||
bool chatUnread[3] = { true, true, true };
|
||||
|
||||
+1
-1
@@ -1631,7 +1631,7 @@ namespace FRG
|
||||
|
||||
// Check if player reached target interactive object
|
||||
if (auto* obj = getTargetInteractiveObject();
|
||||
!state.tutorialInteractiveObjectsLocked && obj && player && !obj->state.isAnimating)
|
||||
!(areInteractiveObjectsLocked && areInteractiveObjectsLocked()) && obj && player && !obj->state.isAnimating)
|
||||
{
|
||||
const Eigen::Vector3f& approachTarget = obj->state.hasInteractionPosition
|
||||
? obj->state.interactionPosition
|
||||
|
||||
+6
-2
@@ -101,14 +101,18 @@ namespace FRG
|
||||
|
||||
std::function<void()> onPlayerTaxiRequired;
|
||||
|
||||
// Optional, set by Game. While it returns true the player can't pick up /
|
||||
// use interactive objects (the dorm tutorial unlocks them step by step).
|
||||
std::function<bool()> areInteractiveObjectsLocked;
|
||||
|
||||
// Set by Game after setup(). Lua and onDeathAnimComplete call this to
|
||||
// request the transition without coupling Location to Game directly.
|
||||
std::function<bool()> requestDarklandsTransition;
|
||||
|
||||
std::function<void(bool, bool)> requestNightDayTransition;
|
||||
|
||||
// Set by Game after setup(). Lua calls this to advance the uni_interior HUD
|
||||
// from step12 to step13 (trigger-zone encounter hint).
|
||||
// Set by Game after setup(). Lua calls this to advance the uni_interior darklands
|
||||
// tutorial to the attack hint (trigger-zone encounter).
|
||||
std::function<void()> requestAdvanceDarklandsHud;
|
||||
|
||||
std::function<void()> requestClosePhone;
|
||||
|
||||
@@ -17,7 +17,6 @@ void LocationState::save(nlohmann::json& out) const
|
||||
out["isNight"] = isNight;
|
||||
out["isDawn"] = isDawn;
|
||||
|
||||
out["tutorialInteractiveObjectsLocked"] = tutorialInteractiveObjectsLocked;
|
||||
|
||||
|
||||
// Taxi stuff:
|
||||
@@ -122,7 +121,6 @@ void LocationState::load(const nlohmann::json& in)
|
||||
isNight = in.value("isNight", false);
|
||||
isDawn = in.value("isDawn", false);
|
||||
|
||||
tutorialInteractiveObjectsLocked = in.value("tutorialInteractiveObjectsLocked", false);
|
||||
|
||||
//logger() << "LocationState::load step 2" << std::endl;
|
||||
|
||||
|
||||
@@ -24,9 +24,6 @@ struct LocationState : public ISaveable {
|
||||
bool isNight = false;
|
||||
bool isDawn = false;
|
||||
|
||||
// ---- Tutorial ----
|
||||
bool tutorialInteractiveObjectsLocked = false;
|
||||
|
||||
|
||||
NpcCar taxiCarInitialState;
|
||||
NpcCar taxiCar;
|
||||
|
||||
+133
-398
@@ -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)
|
||||
{
|
||||
|
||||
+23
-29
@@ -63,6 +63,10 @@ namespace FRG {
|
||||
|
||||
void tutorialShowTaxiHint();
|
||||
|
||||
// Shows the HUD for the current location with its callbacks wired and visibility
|
||||
// applied. Game calls it once a new game's state has been reset.
|
||||
void showGameplayHud();
|
||||
|
||||
void setChatUnread(int chatIndex, bool unread, const std::string& previewMsg = "");
|
||||
|
||||
void spendMoney(int amount);
|
||||
@@ -75,7 +79,6 @@ namespace FRG {
|
||||
std::function<void(int)> chatOpenCallback;
|
||||
std::function<void()> skipCutsceneFunc;
|
||||
std::function<void()> callTaxiFunc;
|
||||
std::function<void()> tutorialUnlockInteractiveObjectsFunc;
|
||||
std::function<void(int)> onSaveGame;
|
||||
std::function<void(int)> onLoadGame;
|
||||
std::function<SaveSlotInfo(int)> getSlotInfoFunc;
|
||||
@@ -93,7 +96,6 @@ namespace FRG {
|
||||
void onChatBubbleReady(const std::string& text, bool incoming);
|
||||
|
||||
void setDarklandsMode(bool enabled);
|
||||
void advanceTutorialStep();
|
||||
void onItemPickedUp(const std::string& itemId);
|
||||
void onLocationChanged(const std::string& locationName);
|
||||
void onEnemyKilledInUniInterior();
|
||||
@@ -137,6 +139,9 @@ namespace FRG {
|
||||
#else
|
||||
bool shadowsEnabled = true;
|
||||
#endif
|
||||
// Applied only when a new game starts (off = start with the tutorial Completed);
|
||||
// saved games keep their own tutorial progress.
|
||||
bool tutorialEnabledSetting = true;
|
||||
std::string lastMusicTrack;
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
@@ -160,15 +165,18 @@ namespace FRG {
|
||||
AudioPlayerAsync& audioPlayer_;
|
||||
|
||||
void enterGameplay(bool isFromLoad = false);
|
||||
void applyHudForCurrentState();
|
||||
// HUD layout for the current location and darklands mode.
|
||||
std::shared_ptr<UiNode> gameplayHudRoot() const;
|
||||
// Single place that decides which HUD buttons and tutorial hints are visible:
|
||||
// buttons follow the inventory, hints follow gameState_.tutorialStep and the
|
||||
// tutorial flags. Runs every frame; names absent from the shown screen are skipped.
|
||||
void refreshHudVisibility();
|
||||
void refreshQuestJournalUi();
|
||||
void reloadLocalizedGameContent();
|
||||
std::shared_ptr<UiNode> loadLocalizedUi(const std::string& basePath, const std::string& zipFile);
|
||||
std::shared_ptr<Texture> loadLocalizedTexture(const std::string& basePath, const std::string& zipFile);
|
||||
void loadUiRoots(const std::string& zipFile);
|
||||
void selectQuestByIndex(int index);
|
||||
void refreshItemPickupHud();
|
||||
void setupStep5Callbacks();
|
||||
void setupGameplayHudCallbacks();
|
||||
void openPhoneMessenger();
|
||||
void openPhoneBank();
|
||||
@@ -182,7 +190,6 @@ namespace FRG {
|
||||
void openPhoneChatFromList(int chatIndex, std::shared_ptr<UiNode> chatRoot);
|
||||
void returnToPhoneChatList();
|
||||
void closePhoneScreenFromChat();
|
||||
void applyUniIntHud();
|
||||
void rebuildChatBubblesFromHistory(int chatIndex, bool isPortrait);
|
||||
void showModalMenuScreen();
|
||||
|
||||
@@ -196,7 +203,8 @@ namespace FRG {
|
||||
void showCreditsScreen();
|
||||
void showSettingsScreen();
|
||||
void showSettingsGraphicsScreen();
|
||||
|
||||
void setupTutorialToggle();
|
||||
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
void showSettingsNoMusicWebScreen();
|
||||
@@ -235,30 +243,19 @@ namespace FRG {
|
||||
int journalMenuStackDepth_ = 0;
|
||||
std::vector<std::string> visibleQuestIds;
|
||||
|
||||
std::shared_ptr<UiNode> hudRoot;
|
||||
std::shared_ptr<UiNode> hudStep1Root;
|
||||
std::shared_ptr<UiNode> hudStep2Root;
|
||||
std::shared_ptr<UiNode> hudStep3Root;
|
||||
std::shared_ptr<UiNode> hudStep4Root;
|
||||
std::shared_ptr<UiNode> hudStep5aRoot;
|
||||
std::shared_ptr<UiNode> hudStep5bRoot;
|
||||
std::shared_ptr<UiNode> hudStep5abRoot;
|
||||
// One HUD per location (+ darklands variant); each holds all of its buttons and
|
||||
// tutorial hints, refreshHudVisibility() shows the relevant ones.
|
||||
std::shared_ptr<UiNode> hudDormRoot;
|
||||
std::shared_ptr<UiNode> hudUniExtRoot;
|
||||
std::shared_ptr<UiNode> hudUniIntStep10Root;
|
||||
std::shared_ptr<UiNode> hudUniIntStep11Root;
|
||||
std::shared_ptr<UiNode> hudUniIntStep12Root;
|
||||
std::shared_ptr<UiNode> hudUniIntFullRoot;
|
||||
std::shared_ptr<UiNode> hudUniIntStep13Root;
|
||||
std::shared_ptr<UiNode> hudUniIntDarkFullRoot;
|
||||
std::shared_ptr<UiNode> hudUniExtDarkRoot;
|
||||
std::shared_ptr<UiNode> hudUniIntRoot;
|
||||
std::shared_ptr<UiNode> hudUniIntDarkRoot;
|
||||
|
||||
std::shared_ptr<UiNode> hudCutsceneRoot_;
|
||||
// "Click to advance the dialogue" hint; drawn by topUiManager, above the dialogue.
|
||||
std::shared_ptr<UiNode> hudTopHintRoot_;
|
||||
|
||||
std::shared_ptr<UiNode> phoneMainRoot;
|
||||
std::shared_ptr<UiNode> phoneMainHintARoot;
|
||||
std::shared_ptr<UiNode> phoneMainHintBRoot;
|
||||
std::shared_ptr<UiNode> phoneMainHintABRoot;
|
||||
|
||||
std::shared_ptr<UiNode> phoneBankRoot;
|
||||
std::shared_ptr<UiNode> phoneVideoRoot;
|
||||
@@ -267,17 +264,12 @@ namespace FRG {
|
||||
|
||||
// Portrait-mode (vertical) equivalents for screens that have a portrait layout
|
||||
std::shared_ptr<UiNode> verticalPhoneMainRoot;
|
||||
std::shared_ptr<UiNode> verticalPhoneMainHintARoot;
|
||||
std::shared_ptr<UiNode> verticalPhoneMainHintBRoot;
|
||||
std::shared_ptr<UiNode> verticalPhoneMainHintABRoot;
|
||||
std::shared_ptr<UiNode> verticalPhoneBankRoot;
|
||||
std::shared_ptr<UiNode> verticalPhoneVideoRoot;
|
||||
std::shared_ptr<UiNode> verticalPhoneMapDormRoot;
|
||||
std::shared_ptr<UiNode> verticalPhoneMapUniRoot;
|
||||
std::shared_ptr<UiNode> phoneChatListRoot;
|
||||
std::shared_ptr<UiNode> phoneChatListHintRoot;
|
||||
std::shared_ptr<UiNode> verticalPhoneChatListRoot;
|
||||
std::shared_ptr<UiNode> verticalPhoneChatListHintRoot;
|
||||
std::shared_ptr<UiNode> phoneChat1Root;
|
||||
std::shared_ptr<UiNode> phoneChat2Root;
|
||||
std::shared_ptr<UiNode> phoneChat3Root;
|
||||
@@ -367,6 +359,8 @@ namespace FRG {
|
||||
std::string musicToggleOff;
|
||||
std::string soundToggleOn;
|
||||
std::string soundToggleOff;
|
||||
std::string tutorialToggleOn;
|
||||
std::string tutorialToggleOff;
|
||||
std::string fullScreenToggleOn;
|
||||
std::string fullScreenToggleOff;
|
||||
std::string shadowToggleOn;
|
||||
|
||||
+16
-2
@@ -5,6 +5,7 @@
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <unordered_set>
|
||||
#include "GameConstants.h"
|
||||
|
||||
namespace FRG {
|
||||
@@ -1535,6 +1536,14 @@ namespace FRG {
|
||||
renderer.PopProjectionMatrix();
|
||||
}
|
||||
|
||||
// Buttons under a hidden node (the node itself or any ancestor) must not react to touches.
|
||||
static void collectVisibleInteractives(const std::shared_ptr<UiNode>& node, std::unordered_set<const void*>& out) {
|
||||
if (!node || !node->visible) return;
|
||||
if (node->button) out.insert(node->button.get());
|
||||
if (node->textButton) out.insert(node->textButton.get());
|
||||
for (auto& c : node->children) collectVisibleInteractives(c, out);
|
||||
}
|
||||
|
||||
static std::shared_ptr<UiNode> findNodeByName(const std::shared_ptr<UiNode>& node, const std::string& name) {
|
||||
if (!node) return nullptr;
|
||||
if (!name.empty() && node->name == name) return node;
|
||||
@@ -1847,11 +1856,13 @@ namespace FRG {
|
||||
void UiManager::onTouchDown(int64_t fingerId, int x, int y) {
|
||||
// Iterate allInteractives in reverse DFS order: later-declared elements have higher priority.
|
||||
// At most one button or textButton is pressed per touch.
|
||||
std::unordered_set<const void*> visibleInteractives;
|
||||
collectVisibleInteractives(root, visibleInteractives);
|
||||
for (auto it = allInteractives.rbegin(); it != allInteractives.rend(); ++it) {
|
||||
if (std::holds_alternative<std::shared_ptr<UiButton>>(*it)) {
|
||||
auto& b = std::get<std::shared_ptr<UiButton>>(*it);
|
||||
//logger() << "onTouchDown found the button: " << b->name << std::endl;
|
||||
if (b->state != ButtonState::Disabled &&
|
||||
if (visibleInteractives.count(b.get()) && b->state != ButtonState::Disabled &&
|
||||
b->getClickZoneRect().containsConsideringBorder((float)x, (float)y, b->border)) {
|
||||
b->state = ButtonState::Pressed;
|
||||
pressedButtons[fingerId] = b;
|
||||
@@ -1861,7 +1872,7 @@ namespace FRG {
|
||||
}
|
||||
} else {
|
||||
auto& tb = std::get<std::shared_ptr<UiTextButton>>(*it);
|
||||
if (tb->state != ButtonState::Disabled &&
|
||||
if (visibleInteractives.count(tb.get()) && tb->state != ButtonState::Disabled &&
|
||||
tb->getClickZoneRect().containsConsideringBorder((float)x, (float)y, tb->border)) {
|
||||
tb->state = ButtonState::Pressed;
|
||||
pressedTextButtons[fingerId] = tb;
|
||||
@@ -2197,6 +2208,9 @@ namespace FRG {
|
||||
if (!root) return false;
|
||||
auto node = findNodeByName(root, nodeName);
|
||||
if (!node) return false;
|
||||
// Replay the fade-in when a hidden image is revealed, as if its screen was just shown.
|
||||
if (visible && !node->visible && node->staticImage && node->staticImage->fadeInEnabled)
|
||||
node->staticImage->fadeInElapsedMs = 0.0f;
|
||||
node->visible = visible;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -370,6 +370,7 @@ namespace FRG {
|
||||
static constexpr int64_t MOUSE_FINGER_ID = -1LL;
|
||||
|
||||
void replaceRoot(std::shared_ptr<UiNode> newRoot);
|
||||
std::shared_ptr<UiNode> getRoot() const { return root; }
|
||||
void loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
|
||||
void appendFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
|
||||
|
||||
|
||||
Reference in new issue
Block a user