More work on states, added bot
This commit is contained in:
1 parent
66e93d46f4
commit
5e9a663812
25 files changed
+1595
-740
No files matched your search
+168
-171
@@ -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";
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user