More work on states, added bot

This commit is contained in:
Vladislav Khorev committed 2026-10-29 22:37:30 +06:00
1 parent 66e93d46f4
commit 5e9a663812
25 files changed
+1595 -740

No files matched your search

+168 -171
View File
@@ -13,6 +13,7 @@
#include <cmath>
#include <algorithm>
#include <functional>
#include <type_traits>
#ifdef __ANDROID__
#include <android/log.h>
#endif
@@ -76,16 +77,14 @@ namespace FRG
#endif
Location* Game::currentLocation() const {
if (gameState.currentLocationName.empty()) return nullptr;
const auto& locations = internalGameServer.runtimeLocations();
auto it = locations.find(gameState.currentLocationName);
return (it != locations.end()) ? it->second.get() : nullptr;
const auto state = stateSnapshot();
if (!state || state->currentLocationId.empty()) return nullptr;
return internalGameServer.runtimeLocation(state->currentLocationId);
}
Game::Game()
: audioPlayer(std::make_unique<AudioPlayerAsync>())
, internalGameServer()
, gameState(internalGameServer.hostState())
, menuManager(renderer, internalGameServer, *audioPlayer)
, newTickCount(0)
, lastTickCount(0)
@@ -360,19 +359,6 @@ namespace FRG
CampaignRegistry::instance().loadFromJson("resources/config/campaigns.json", CONST_ZIP_FILE);
loadSteps.push([this]() {
// Wire inventory callbacks: tutorial tracking + toast notifications.
gameState.inventory.onItemAdded = [this](const std::string& itemId) {
menuManager.onItemPickedUp(itemId);
const Item* item = ItemRegistry::instance().findById(itemId);
if (item) menuManager.showToast(menuManager.toastTextureItemAdded, item->name);
audioPlayer->playSoundAsync("resources/audio/831642__ienba__generic-game-pick-up.ogg");
};
gameState.inventory.onItemRemoved = [this](const std::string& itemId) {
const Item* item = ItemRegistry::instance().findById(itemId);
if (item) menuManager.showToast(menuManager.toastTextureItemRemoved, item->name);
audioPlayer->playSoundAsync("resources/audio/831642__ienba__generic-game-pick-up.ogg");
};
// Wire phone dialogue start function so MenuManager can trigger dialogues.
menuManager.startDialogueFunc = [this](const std::string& id) {
if (currentLocation()) currentLocation()->dialogueSystem.startDialogue(id);
@@ -399,18 +385,6 @@ namespace FRG
loadSteps.push([this]() {
try {
menuManager.setup(CONST_ZIP_FILE);
gameState.questJournal.onQuestUnlocked = [this](const std::string& id) {
const Quest::QuestState* quest = gameState.questJournal.findQuest(id);
if (quest) menuManager.showToast(menuManager.toastTextureQuestNew, quest->definition.title);
};
gameState.questJournal.onQuestCompleted = [this](const std::string& id) {
const Quest::QuestState* quest = gameState.questJournal.findQuest(id);
if (quest) menuManager.showToast(menuManager.toastTextureQuestCompleted, quest->definition.title);
};
gameState.questJournal.onQuestFailed = [this](const std::string& id) {
const Quest::QuestState* quest = gameState.questJournal.findQuest(id);
if (quest) menuManager.showToast(menuManager.toastTextureQuestFailed, quest->definition.title);
};
logger() << "UI loaded successfully" << std::endl;
}
catch (const std::exception& e) {
@@ -425,7 +399,8 @@ namespace FRG
performResetToInitialState();
applyTutorialSetting();
const CampaignConfig& campaign = activeCampaign();
gameState.currentLocationName = campaign.startLocation;
internalGameServer.submit(
SetCurrentLocationCommand{campaign.startLocation});
menuManager.showGameplayHud(); // first HUD frame drawn from the fresh state
if (currentLocation()) currentLocation()->scriptEngine.callLocationEnterCallback();
audioPlayer->crossFadeMusicAsync(campaign.startMusic);
@@ -516,7 +491,10 @@ namespace FRG
}
const CampaignConfig& Game::activeCampaign() const {
const CampaignConfig* campaign = CampaignRegistry::instance().find(gameState.campaignId);
const auto state = stateSnapshot();
const CampaignConfig* campaign = state
? CampaignRegistry::instance().find(state->campaignId)
: nullptr;
if (!campaign) campaign = CampaignRegistry::instance().find(CampaignRegistry::kDefaultCampaignId);
if (!campaign) {
static const CampaignConfig empty;
@@ -527,7 +505,7 @@ namespace FRG
bool Game::isLocationLoaded(const std::string& campaignId, const std::string& locationId) const {
return player && loadedCampaignId == campaignId
&& internalGameServer.runtimeLocations().count(locationId) > 0;
&& internalGameServer.hasRuntimeLocation(locationId);
}
void Game::enterLocationThen(const std::string& requestedCampaignId, const std::string& requestedLocationId,
@@ -567,15 +545,15 @@ namespace FRG
void Game::loadCampaign(const std::string& campaignId) {
loadedCampaignId = campaignId;
gameState.campaignId = campaignId;
gameState.locations.clear();
gameState.players.clear();
gameState.questJournal.loadFromFile(activeCampaign().questsPath, CONST_ZIP_FILE);
gameState.globalFloats["player_hp"] = 200; // read by location scripts as they load
const CampaignConfig* campaign =
CampaignRegistry::instance().find(campaignId);
if (!campaign) return;
internalGameServer.initializeCampaign(
campaignId, campaign->questsPath, CONST_ZIP_FILE);
// The player stays loaded for the whole campaign; locations come and go around him.
auto& playerState = gameState.players["player"];
if (!playerState) playerState = std::make_shared<CharacterState>();
player = Location::createPlayer(renderer, audioPlayer.get(), *playerState, internalGameServer);
player = Location::createPlayer(
renderer, audioPlayer.get(), internalGameServer,
internalGameServer.localPlayerId());
if (player)
player->onDeathAnimComplete = [this]() { startDarklandsTransition(); };
}
@@ -583,21 +561,21 @@ namespace FRG
void Game::unloadCampaign() {
unloadLocation(false);
player.reset();
gameState.players.clear();
loadedCampaignId.clear();
gameState.locations.clear();
internalGameServer.clearCampaign();
renderer.textureManager.UnloadUnused(); // the player's textures
}
void Game::loadLocation(const std::string& locationId) {
const LocationConfig* config = activeCampaign().findLocation(locationId);
if (!config) {
std::cerr << "[campaign] Campaign '" << gameState.campaignId << "' has no location '"
const auto state = stateSnapshot();
std::cerr << "[campaign] Campaign '"
<< (state ? state->campaignId : std::string()) << "' has no location '"
<< locationId << "'" << std::endl;
return;
}
const bool initializeLocation =
!gameState.locations[locationId].hasPersistentState;
const bool initializeLocation = internalGameServer.prepareLocation(*config);
std::shared_ptr<Location> loc = createLocation(*config);
wireLocation(loc.get());
if (shadowMap) loc->shadowMap = shadowMap;
@@ -608,12 +586,7 @@ namespace FRG
}
void Game::unloadLocation(bool storeState) {
if (storeState) internalGameServer.synchronizeRuntimeAuxiliaryState();
else {
for (const auto& [name, loc] : internalGameServer.runtimeLocations())
gameState.locations.erase(name);
}
internalGameServer.runtimeLocations().clear();
internalGameServer.clearRuntimeLocations(storeState);
// Textures only the location's models and characters used are freed now.
renderer.textureManager.UnloadUnused();
}
@@ -631,8 +604,10 @@ namespace FRG
}
// Runs from inside the current location's update(); the location itself is
// released by a later loading step.
enterLocationThen(gameState.campaignId, destName, [this, destName, destPos, destRotY]() {
gameState.currentLocationName = destName;
const auto state = stateSnapshot();
if (!state) return;
enterLocationThen(state->campaignId, destName, [this, destName, destPos, destRotY]() {
internalGameServer.submit(SetCurrentLocationCommand{destName});
Location* loc = currentLocation();
if (!loc) return;
if (loc->player) {
@@ -643,17 +618,17 @@ namespace FRG
});
loc->stopCharacter(*loc->player);
}
loc->state.cameraAzimuth = destRotY;
loc->state.isDarklands = gameState.isDarklands;
loc->state.isNight = gameState.isNight;
loc->state.isDawn = gameState.isDawn;
auto locationState = loc->stateSnapshot();
const float inclination = locationState
? locationState->cameraInclination
: 1.1036f;
internalGameServer.submit(SetLocationCameraCommand{
destName, destRotY, inclination});
loc->scriptEngine.callLocationEnterCallback();
//Reset taxi
auto locationState = gameState.locations.find(destName);
if (locationState != gameState.locations.end())
locationState->second.resetTaxi();
gameState.taxiIsCalled = false;
internalGameServer.submit(ResetTaxiCommand{destName});
internalGameServer.submit(SetTaxiCalledCommand{false});
menuManager.onLocationChanged(destName);
});
@@ -661,34 +636,12 @@ namespace FRG
void Game::performFullReset() {
gameState.currentLocationName.clear();
gameState.isDarklands = false;
gameState.isNight = false;
gameState.isDawn = false;
gameState.playerHp = 200.f;
gameState.playerMaxHp = 200.f;
gameState.money = 5500;
gameState.globalInts.clear();
gameState.globalFloats.clear();
gameState.globalFloats["player_hp"] = 200;
gameState.tutorialStep = TutorialStep::AdvanceDialogue;
gameState.tutorialPhoneChatScreenOpened = false;
gameState.tutorialJournalScreenOpened = false;
gameState.tutorialNeedOpenTaxiScreen = false;
gameState.tutorialTaxiHintDone = false;
for (int i = 0; i < 3; ++i) {
gameState.chatUnread[i] = true;
gameState.chatPreviewMsg[i].clear();
gameState.chatHistory[i].clear();
}
gameState.inventory.clear();
gameState.questJournal.loadFromFile(activeCampaign().questsPath, CONST_ZIP_FILE);
// Rebuild the loaded location from its config; the others have no stored state
// any more, so they are built fresh when the player first goes there.
gameState.locations.clear();
std::vector<std::string> loadedIds;
for (auto& [name, loc] : internalGameServer.runtimeLocations()) loadedIds.push_back(name);
std::vector<std::string> loadedIds =
internalGameServer.loadedRuntimeLocationIds();
internalGameServer.resetCampaign(
activeCampaign().questsPath, CONST_ZIP_FILE);
unloadLocation(false);
for (const std::string& id : loadedIds) loadLocation(id);
@@ -737,7 +690,8 @@ namespace FRG
// (if the campaign has one); a disabled tutorial is simply one that is already completed.
void Game::applyTutorialSetting() {
if (!activeCampaign().tutorial || !menuManager.tutorialEnabledSetting)
gameState.tutorialStep = TutorialStep::Completed;
internalGameServer.submit(
SetTutorialStepCommand{TutorialStep::Completed});
}
void Game::saveInitialState() {
@@ -760,12 +714,6 @@ namespace FRG
std::shared_ptr<Location> Game::createLocation(const LocationConfig& config)
{
LocationGameState& authoritative = gameState.locations[config.id];
if (!authoritative.location) authoritative.location = std::make_shared<LocationState>();
if (authoritative.location->navigationMaps.empty())
authoritative.location->initNavigation(config.navigationJsonPaths);
LocationSetup params;
params.gameObjectsJsonPath = config.gameObjectsJsonPath;
params.interactiveObjectsJsonPath = config.interactiveObjectsJsonPath;
@@ -778,29 +726,30 @@ namespace FRG
params.lightsJsonPath = config.lightsJsonPath;
params.playerPosition = config.playerPosition;
params.player = player;
params.gameState = &authoritative;
auto loc = std::make_shared<Location>(
renderer, gameState.inventory, *authoritative.location, internalGameServer, config.id);
loc->setup(params, &gameState.questJournal, audioPlayer.get());
loc->scriptEngine.setGlobalStore(&gameState.globalInts);
loc->scriptEngine.setGlobalFloatStore(&gameState.globalFloats);
renderer, internalGameServer, config.id);
internalGameServer.setupRuntimeLocation(
config.id, loc, std::move(params), audioPlayer.get());
loc->requestNightDayTransition = [this](bool isNight, bool isDawn) {
gameState.isNight = isNight;
gameState.isDawn = isDawn;
const auto state = stateSnapshot();
if (!state) return;
internalGameServer.submit(SetWorldModeCommand{
state->isDarklands, isNight, isDawn});
};
loc->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
loc->requestAdvanceDarklandsHud = [this]() { menuManager.advanceUniIntDarklandsHud(); };
loc->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); };
loc->requestReturnToMainMenu = [this]() {
gameState.currentLocationName.clear();
internalGameServer.submit(SetCurrentLocationCommand{std::string()});
menuManager.showMenuAfterGameCompleted();
};
loc->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) {
menuManager.setChatUnread(idx, unread, msg);
};
for (auto& npc : loc->npcs) {
if (npc && npc->characterState.canAttack) {
auto state = npc ? npc->stateSnapshot() : nullptr;
if (state && state->canAttack) {
npc->onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); };
}
}
@@ -813,25 +762,6 @@ namespace FRG
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
};
authoritative.lockInteractiveObjectsDuringTutorial =
config.lockInteractiveObjectsDuringTutorial;
if (config.taxi) {
const LocationConfig::Taxi& taxi = *config.taxi;
LocationState& locationState = *authoritative.location;
locationState.taxiCarTriggerPosition = taxi.triggerPosition;
locationState.taxiCarWaypoints = taxi.waypoints;
locationState.taxiCarInitialState.position = taxi.initialPosition;
locationState.taxiCarInitialState.rotation = taxi.initialRotation;
locationState.taxiCarInitialState.mode = NpcCar::Mode::NONE;
locationState.taxiCarInitialState.velocity = 0.f;
locationState.taxiCarInitialState.steeringAngle = 0.f;
locationState.taxiCarInitialState.waypointReachRadius = taxi.waypointReachRadius;
locationState.taxiCar.waypointReachRadius = taxi.waypointReachRadius;
if (!authoritative.hasPersistentState)
authoritative.resetTaxi();
}
return loc;
}
@@ -846,20 +776,16 @@ namespace FRG
teleportPlayer(destName, destPos, destRotY);
};
// Share the global int store with all dialogue runtimes so flags set via
// dialogue JSON and via Lua set_dialogue_flag all see the same state.
{
loc->dialogueSystem.setGlobalFlagStore(&gameState.globalInts);
}
// Wire tutorial advance callbacks.
// completeTutorialStep() only acts on the current step, so sharing is safe.
{
loc->dialogueSystem.setOnDialogueAdvanced([this]() {
gameState.completeTutorialStep(TutorialStep::AdvanceDialogue);
internalGameServer.submit(
CompleteTutorialStepCommand{TutorialStep::AdvanceDialogue});
});
loc->onPlayerFloorWalk = [this]() {
gameState.completeTutorialStep(TutorialStep::Walk);
internalGameServer.submit(
CompleteTutorialStepCommand{TutorialStep::Walk});
menuManager.onPlayerStartedWalking();
};
}
@@ -897,19 +823,23 @@ namespace FRG
}
bool currentTargetDead = false;
auto playerState = loc->player->stateSnapshot();
if (!playerState) return;
for (const auto& npc : loc->npcs) {
if (npc && npc->entityId() == loc->player->characterState.attackTargetId) {
currentTargetDead = npc->characterState.hp <= 0.f;
if (npc && npc->entityId() == playerState->attackTargetId) {
auto npcState = npc->stateSnapshot();
currentTargetDead = !npcState || npcState->hp <= 0.f;
break;
}
}
if ((loc->player->characterState.attackTargetId.empty() || currentTargetDead)
if ((playerState->attackTargetId.empty() || currentTargetDead)
&&
loc->player->isMoving() == false
&&
loc->player->getHp() > 0) {
internalGameServer.submit(SetCharacterTargetCommand{
loc->locationId(), "player", attackerId});
loc->locationId(),
internalGameServer.localPlayerId(), attackerId});
}
}
@@ -931,7 +861,9 @@ namespace FRG
CheckGlError(__FILE__, __LINE__);
logger() << "Shadow map initialized" << std::endl;
for (auto& [name, loc] : internalGameServer.runtimeLocations()) loc->shadowMap = shadowMap;
for (const std::string& id : internalGameServer.loadedRuntimeLocationIds())
if (Location* loc = internalGameServer.runtimeLocation(id))
loc->shadowMap = shadowMap;
}
void Game::destroyShadowMapAndProparateToLocations()
@@ -941,7 +873,9 @@ namespace FRG
shadowMap.reset();
CheckGlError(__FILE__, __LINE__);
for (auto& [name, loc] : internalGameServer.runtimeLocations()) loc->shadowMap.reset();
for (const std::string& id : internalGameServer.loadedRuntimeLocationIds())
if (Location* loc = internalGameServer.runtimeLocation(id))
loc->shadowMap.reset();
CheckGlError(__FILE__, __LINE__);
}
@@ -1014,16 +948,12 @@ namespace FRG
Location* loc = currentLocation();
if (loc)
{
// Sync global flags so Location's draw functions see them.
loc->state.isDarklands = gameState.isDarklands;
loc->state.isNight = gameState.isNight;
loc->state.isDawn = gameState.isDawn;
if (gameState.isDarklands) {
const auto state = stateSnapshot();
if (state && state->isDarklands) {
loc->drawGameDarklands();
CheckGlError(__FILE__, __LINE__);
}
else if (gameState.isNight) {
else if (state && state->isNight) {
loc->drawGameNight();
CheckGlError(__FILE__, __LINE__);
}
@@ -1206,11 +1136,67 @@ namespace FRG
// The authoritative implementation advances simulation; a future
// network client can use the same call to poll/apply server updates.
internalGameServer.update(delta);
processServerEvents();
}
}
}
void Game::processServerEvents()
{
for (GameServerEvent& event : internalGameServer.drainEvents()) {
std::visit([this](const auto& value) {
using T = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<T, InventoryItemAddedEvent>) {
menuManager.onItemPickedUp(value.itemId);
const Item* item =
ItemRegistry::instance().findById(value.itemId);
if (item)
menuManager.showToast(
menuManager.toastTextureItemAdded, item->name);
audioPlayer->playSoundAsync(
"resources/audio/831642__ienba__generic-game-pick-up.ogg");
} else if constexpr (std::is_same_v<T, InventoryItemRemovedEvent>) {
const Item* item =
ItemRegistry::instance().findById(value.itemId);
if (item)
menuManager.showToast(
menuManager.toastTextureItemRemoved, item->name);
audioPlayer->playSoundAsync(
"resources/audio/831642__ienba__generic-game-pick-up.ogg");
} else if constexpr (std::is_same_v<T, Quest::QuestEvent>) {
const auto state = stateSnapshot();
if (!state) return;
auto quest = std::find_if(
state->quests.begin(), state->quests.end(),
[&value](const Quest::QuestState& candidate) {
return candidate.definition.id == value.questId;
});
if (quest == state->quests.end()) return;
switch (value.type) {
case Quest::QuestEventType::Unlocked:
menuManager.showToast(
menuManager.toastTextureQuestNew,
quest->definition.title);
break;
case Quest::QuestEventType::Completed:
menuManager.showToast(
menuManager.toastTextureQuestCompleted,
quest->definition.title);
break;
case Quest::QuestEventType::Failed:
menuManager.showToast(
menuManager.toastTextureQuestFailed,
quest->definition.title);
break;
case Quest::QuestEventType::ObjectiveCompleted:
break;
}
}
}, event);
}
}
void Game::render() {
FRG::CheckGlError(__FILE__, __LINE__);
@@ -1256,10 +1242,7 @@ namespace FRG
}
void Game::update() {
// Host systems (locations, scripts and loading) still execute in-process.
// Publish any changes they made since the previous frame as a new immutable
// revision before presentation clients query the server.
internalGameServer.markHostMutation();
processServerEvents();
SDL_Event event;
const bool mobileIsPortrait = loadingCompleted && (Environment::width < Environment::height);
while (SDL_PollEvent(&event)) {
@@ -1394,7 +1377,8 @@ namespace FRG
}
//logger() << "Current zoom: " << Environment::zoom << std::endl;
// Tutorial: any mouse-wheel scroll counts as the "zoom gesture".
gameState.completeTutorialStep(TutorialStep::Zoom);
internalGameServer.submit(
CompleteTutorialStepCommand{TutorialStep::Zoom});
}
if (event.type == SDL_KEYDOWN && event.key.repeat == 0) {
@@ -1422,7 +1406,6 @@ namespace FRG
if (event.key.keysym.sym == SDLK_3)
{
currentLocation()->switchNavigation(2);
//logger() << "Player position: " << currentLocation()->player->characterState.position.transpose() << std::endl;
}
if (event.key.keysym.sym == SDLK_n)
{
@@ -1435,8 +1418,6 @@ namespace FRG
{
currentLocation()->editorMode = EditorMode::None;
}
//logger() << "Player position: " << currentLocation()->player->characterState.position.transpose() << std::endl;
}
}
@@ -1516,8 +1497,11 @@ namespace FRG
currentLocation()->lastMouseY = eventY;
// Snapshot current angles so we can measure how far the user rotates.
dragStartAzimuth = currentLocation()->state.cameraAzimuth;
dragStartInclination = currentLocation()->state.cameraInclination;
auto state = currentLocation()->stateSnapshot();
if (state) {
dragStartAzimuth = state->cameraAzimuth;
dragStartInclination = state->cameraInclination;
}
}
}
@@ -1577,7 +1561,8 @@ namespace FRG
// Tutorial: detect a significant pinch-zoom (≥ 2 zoom units).
if (std::abs(Environment::zoom - pinchStartZoom) >= 2.0f) {
gameState.completeTutorialStep(TutorialStep::Zoom);
internalGameServer.submit(
CompleteTutorialStepCommand{TutorialStep::Zoom});
}
}
@@ -1720,12 +1705,17 @@ namespace FRG
// ~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;
const auto game = stateSnapshot();
if (cameraDragging
&& gameState.tutorialStep == TutorialStep::RotateCamera) {
float deltaAz = std::abs(currentLocation()->state.cameraAzimuth - dragStartAzimuth);
float deltaInc = std::abs(currentLocation()->state.cameraInclination - dragStartInclination);
&& game
&& game->tutorialStep == TutorialStep::RotateCamera) {
auto state = currentLocation()->stateSnapshot();
if (!state) return;
float deltaAz = std::abs(state->cameraAzimuth - dragStartAzimuth);
float deltaInc = std::abs(state->cameraInclination - dragStartInclination);
if (deltaAz >= TUTORIAL_ROTATION_THRESHOLD && deltaInc >= TUTORIAL_ROTATION_THRESHOLD) {
gameState.completeTutorialStep(TutorialStep::RotateCamera);
internalGameServer.submit(
CompleteTutorialStepCommand{TutorialStep::RotateCamera});
}
}
}
@@ -1733,13 +1723,14 @@ namespace FRG
bool Game::startDarklandsTransition()
{
if (!gameState.isNight)
const auto state = stateSnapshot();
if (!state || !state->isNight)
{
currentLocation()->dialogueSystem.startDialogue("darklands_day_dialog001");
return false;
}
if (gameState.isDawn)
if (state->isDawn)
{
currentLocation()->dialogueSystem.startDialogue("darklands_morning_dialog001");
return false;
@@ -1773,11 +1764,13 @@ namespace FRG
if (darklandsFlashFadingIn) {
darklandsFlashAlpha = min(darklandsFlashAlpha + step, 1.0f);
if (darklandsFlashAlpha >= 1.0f) {
const auto before = stateSnapshot();
if (!before) return;
if (isNightTransition) {
gameState.isNight = !gameState.isNight;
internalGameServer.submit(SetWorldModeCommand{
before->isDarklands, !before->isNight, before->isDawn});
if (currentLocation())
{
currentLocation()->state.isNight = gameState.isNight;
currentLocation()->scriptEngine.callTriggerNightEnterCallback();
}
@@ -1785,12 +1778,15 @@ namespace FRG
menuManager.lastMusicTrack = "music/bishkek night.ogg";
} else {
gameState.isDarklands = !gameState.isDarklands;
internalGameServer.submit(SetWorldModeCommand{
!before->isDarklands, before->isNight, before->isDawn});
const auto after = stateSnapshot();
if (!after) return;
// Change HUD
menuManager.setDarklandsMode(gameState.isDarklands);
menuManager.setDarklandsMode(after->isDarklands);
if (gameState.isDarklands)
if (after->isDarklands)
{
audioPlayer->crossFadeMusicAsync("music/bishkek fight calm.ogg");
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
@@ -1802,8 +1798,7 @@ namespace FRG
}
if (currentLocation()) {
currentLocation()->state.isDarklands = gameState.isDarklands;
if (gameState.isDarklands)
if (after->isDarklands)
currentLocation()->scriptEngine.callDarklandsEnterCallback();
else
currentLocation()->scriptEngine.callDarklandsExitCallback();
@@ -1935,19 +1930,21 @@ namespace FRG
void Game::playMusicForLoadedGame()
{
if (gameState.isDarklands)
const auto state = stateSnapshot();
if (!state) return;
if (state->isDarklands)
{
audioPlayer->crossFadeMusicAsync("music/bishkek fight calm.ogg");
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
}
else if (gameState.isNight)
else if (state->isNight)
{
audioPlayer->crossFadeMusicAsync("music/bishkek night.ogg");
menuManager.lastMusicTrack = "music/bishkek night.ogg";
}
else
{
if (gameState.currentLocationName == "location_dorm") {
if (state->currentLocationId == "location_dorm") {
audioPlayer->crossFadeMusicAsync("music/obshaga.ogg");
menuManager.lastMusicTrack = "music/obshaga.ogg";
}