working on campaign

This commit is contained in:
Vladislav Khorev committed 2026-10-23 22:47:55 +06:00
1 parent 9fa120d4ad
commit d38a75ea72
24 files changed
+929 -410

No files matched your search

+166 -337
View File
@@ -4,6 +4,7 @@
#include "utils/Utils.h"
#include <fstream>
#include "items/ItemRegistry.h"
#include "Campaign.h"
#include "render/OpenGlExtensions.h"
#include <iostream>
#include "render/TextureManager.h"
@@ -326,24 +327,18 @@ namespace FRG
Character::nameTranslations.loadFromFile("resources/config/translate_names.json");
});
// createLocations() is also used by the full-reset path (see performFullReset()),
// so it stays a single synchronous function rather than being split into
// per-location queue steps; it's queued here as one coarse step.
// Every campaign's locations are built up front (one loading step per location),
// so starting or loading any campaign later only switches the active set.
CampaignRegistry::instance().loadFromJson("resources/config/campaigns.json", CONST_ZIP_FILE);
loadSteps.push([this]() {
createLocationsStep1();
gameState.globalFloats["player_hp"] = 200; // read by location scripts as they load
});
for (const CampaignConfig& campaign : CampaignRegistry::instance().all()) {
queueCampaignBuildSteps(campaign);
}
loadSteps.push([this]() {
createLocationsStep2();
});
loadSteps.push([this]() {
createLocationsStep3();
});
loadSteps.push([this]() {
createLocationsStep4();
});
loadSteps.push([this]() {
createLocationsStep5();
});
activateCampaign(CampaignRegistry::kDefaultCampaignId);
});
loadSteps.push([this]() {
// Wire inventory callbacks: tutorial tracking + toast notifications.
@@ -380,6 +375,12 @@ namespace FRG
menuManager.skipCutsceneFunc = [this]() {
if (currentLocation()) currentLocation()->dialogueSystem.skipCutscene();
};
menuManager.callTaxiFunc = [this]() {
// The taxi hint has served its purpose once a taxi is on its way.
for (auto& [name, loc] : gameState.locations) loc->onPlayerTaxiRequired = nullptr;
if (currentLocation()) currentLocation()->onTaxiCalled();
};
});
loadSteps.push([this]() {
@@ -391,15 +392,17 @@ namespace FRG
std::cerr << "Failed to load UI: " << e.what() << std::endl;
}
menuManager.startGameFunc = [this]() {
mainThreadHandler.EnqueueMainThreadTask([this]() {
menuManager.startGameFunc = [this](const std::string& campaignId) {
mainThreadHandler.EnqueueMainThreadTask([this, campaignId]() {
activateCampaign(campaignId);
performResetToInitialState();
applyTutorialSetting();
gameState.currentLocationName = "location_dorm";
const CampaignConfig& campaign = activeCampaign();
gameState.currentLocationName = campaign.startLocation;
menuManager.showGameplayHud(); // first HUD frame drawn from the fresh state
currentLocation()->scriptEngine.callLocationEnterCallback();
this->audioPlayer->crossFadeMusicAsync("music/obshaga.ogg");
menuManager.lastMusicTrack = "music/obshaga.ogg";
if (currentLocation()) currentLocation()->scriptEngine.callLocationEnterCallback();
audioPlayer->crossFadeMusicAsync(campaign.startMusic);
menuManager.lastMusicTrack = campaign.startMusic;
});
};
@@ -466,13 +469,49 @@ namespace FRG
loadStepsDone = 0;
}
void Game::rearmDormOneShotCallbacks() {
auto it = gameState.locations.find("location_dorm");
if (it == gameState.locations.end() || !it->second) return;
void Game::rearmOneShotCallbacks(LocationMap& locations) {
for (auto& [name, loc] : locations) {
loc->onPlayerTaxiRequired = [this]() {
menuManager.tutorialShowTaxiHint();
};
}
}
it->second->onPlayerTaxiRequired = [this]() {
menuManager.tutorialShowTaxiHint();
};
void Game::activateCampaign(const std::string& campaignId) {
auto it = campaignLocations.find(campaignId);
if (it == campaignLocations.end()) {
std::cerr << "[campaign] Unknown campaign '" << campaignId << "', using '"
<< CampaignRegistry::kDefaultCampaignId << "'" << std::endl;
it = campaignLocations.find(CampaignRegistry::kDefaultCampaignId);
if (it == campaignLocations.end()) return;
}
gameState.campaignId = it->first;
gameState.locations = it->second;
gameState.questJournal.loadFromFile(activeCampaign().questsPath, CONST_ZIP_FILE);
}
const CampaignConfig& Game::activeCampaign() const {
const CampaignConfig* campaign = CampaignRegistry::instance().find(gameState.campaignId);
if (!campaign) campaign = CampaignRegistry::instance().find(CampaignRegistry::kDefaultCampaignId);
if (!campaign) {
static const CampaignConfig empty;
return empty;
}
return *campaign;
}
void Game::queueCampaignBuildSteps(const CampaignConfig& campaign) {
const std::string campaignId = campaign.id;
for (const std::string& configPath : campaign.locationConfigPaths) {
loadSteps.push([this, campaignId, configPath]() {
LocationConfig config;
if (LocationConfig::fromFile(configPath, CONST_ZIP_FILE, config))
campaignLocations[campaignId][config.id] = createLocation(config);
});
}
loadSteps.push([this, campaignId]() {
wireCampaignLocations(campaignLocations[campaignId]);
});
}
void Game::performFullReset() {
@@ -485,6 +524,7 @@ namespace FRG
gameState.money = 5500;
gameState.globalInts.clear();
gameState.globalFloats.clear();
gameState.globalFloats["player_hp"] = 200;
gameState.tutorialStep = TutorialStep::AdvanceDialogue;
gameState.tutorialPhoneChatScreenOpened = false;
gameState.tutorialJournalScreenOpened = false;
@@ -495,21 +535,31 @@ namespace FRG
gameState.chatHistory[i].clear();
}
gameState.inventory.clear();
gameState.questJournal.loadFromFile(localizedConfigPath("resources/config/quests.json", g_currentLanguage), CONST_ZIP_FILE);
createLocationsStep1();
createLocationsStep2();
createLocationsStep3();
createLocationsStep4();
createLocationsStep5();
// Developer workflow: uncomment to (re)generate resources/config/start_state.json
const CampaignConfig& campaign = activeCampaign();
gameState.questJournal.loadFromFile(campaign.questsPath, CONST_ZIP_FILE);
LocationMap& locations = campaignLocations[campaign.id];
locations.clear();
for (const std::string& configPath : campaign.locationConfigPaths) {
LocationConfig config;
if (LocationConfig::fromFile(configPath, CONST_ZIP_FILE, config))
locations[config.id] = createLocation(config);
}
wireCampaignLocations(locations);
gameState.locations = locations;
if (shadowMap) {
for (auto& [name, loc] : locations) loc->shadowMap = shadowMap;
}
// Developer workflow: uncomment to (re)generate the campaign's start-state JSON
// from this freshly-rebuilt state after changing gameplay/content JSON or Lua.
// Re-comment before shipping.
//saveInitialState();
}
void Game::performResetToInitialState() {
const std::string path = "resources/config/start_state.json";
const std::string path = activeCampaign().startStatePath;
std::vector<char> fileData;
if (strlen(CONST_ZIP_FILE) > 0)
@@ -528,6 +578,7 @@ namespace FRG
if (!content.empty()) {
try {
nlohmann::json root = nlohmann::json::parse(content);
const std::string campaignId = gameState.campaignId;
// GameState::load() merges into existing maps rather than replacing them,
// so any Lua globals set during play that aren't in the pristine snapshot
// must be cleared explicitly before loading it.
@@ -537,11 +588,12 @@ namespace FRG
// destruction, no re-parsing JSON/nav files, no new
// sol2 state (GameState::load only touches map entries
// that already exist).
gameState.campaignId = campaignId; // the snapshot may predate campaigns
// Locations survive this reset, so any one-shot callbacks they consumed
// during the previous playthrough (taxi request) must be
// re-armed here -- createLocations() normally does this, but doesn't run
// on this fast path.
rearmDormOneShotCallbacks();
// re-armed here -- wireCampaignLocations() normally does this, but doesn't
// run on this fast path.
rearmOneShotCallbacks(gameState.locations);
return;
} catch (const std::exception& e) {
logger() << "[reset] Failed to parse " << path << ": " << e.what()
@@ -555,19 +607,21 @@ 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.
// Runs after either reset path. Start states always hold the start of the tutorial
// (if the campaign has one); a disabled tutorial is simply one that is already completed.
void Game::applyTutorialSetting() {
if (!menuManager.tutorialEnabledSetting)
if (!activeCampaign().tutorial || !menuManager.tutorialEnabledSetting)
gameState.tutorialStep = TutorialStep::Completed;
}
void Game::saveInitialState() {
nlohmann::json root;
gameState.save(root);
const std::string path = "start_state.json";
// Written next to the executable; copy it over the campaign's start-state file.
const std::string startStatePath = activeCampaign().startStatePath;
const std::string path = startStatePath.substr(startStatePath.find_last_of('/') + 1);
logger() << "[reset] saveInitialState called, to " << path << std::endl;
std::ofstream file(path);
if (file.is_open()) {
file << root.dump(2);
@@ -578,309 +632,80 @@ namespace FRG
saveJsonToFile(root, path);
}
void Game::createLocationsStep1()
std::shared_ptr<Location> Game::createLocation(const LocationConfig& config)
{
gameState.locations.clear();
LocationSetup params;
params.gameObjectsJsonPath = config.gameObjectsJsonPath;
params.interactiveObjectsJsonPath = config.interactiveObjectsJsonPath;
params.npcsJsonPath = config.npcsJsonPath;
params.dialoguesJsonPath = config.dialoguesJsonPath;
params.navigationJsonPaths = config.navigationJsonPaths;
params.scriptPath = config.scriptPath;
params.teleportsJsonPath = config.teleportsJsonPath;
params.triggerZonesJsonPath = config.triggerZonesJsonPath;
params.lightsJsonPath = config.lightsJsonPath;
params.playerPosition = config.playerPosition;
gameState.globalFloats["player_hp"] = 200;
}
void Game::createLocationsStep2()
{
LocationSetup uniInteriorParams;
uniInteriorParams.gameObjectsJsonPath = "resources/config/gameobjects_uni_interior_x.json";
uniInteriorParams.npcsJsonPath = "resources/config/npcs_uni_interior.json";
uniInteriorParams.dialoguesJsonPath = "resources/dialogue/uni_interior_dialogues_010.json";
uniInteriorParams.navigationJsonPaths = {
"resources/navigation/uni_interior4_unlocked_n2.txt", //0
"resources/navigation/uni_interior3_n2_hall.txt",
"resources/navigation/uni_interior4_n2_lr_hall.txt",
"resources/navigation/uni_interior4_n2_lr_tr_hall.txt",
"resources/navigation/uni_interior3_n2_lr_tr_hall_aiperi.txt",
"resources/navigation/uni_interior3_darklands_all_open.txt", //5
"resources/navigation/uni_interior4_unlocked_hall.txt",
"resources/navigation/uni_interior4_unlocked_n3.txt",
"resources/navigation/uni_interior4_unlocked_s1.txt",
"resources/navigation/uni_interior4_unlocked_s2.txt",
"resources/navigation/uni_interior4_unlocked_s3.txt",
"resources/navigation/uni_interior4_unlocked_s3_s1.txt",
"resources/navigation/uni_interior4_unlocked_s3_hall.txt",
"resources/navigation/uni_interior4_unlocked_s3_n3.txt",
"resources/navigation/uni_interior4_unlocked_s3_n2.txt",
"resources/navigation/uni_interior4_unlocked_s3_s2.txt",
"resources/navigation/uni_interior4_locked.txt" //16
};
/*
uniInteriorParams.navigationJsonPaths = {
"resources/navigation/uni_interior4_unlocked_n2.json", //0
"resources/navigation/uni_interior3_n2_hall.json",
"resources/navigation/uni_interior4_n2_lr_hall.json",
"resources/navigation/uni_interior4_n2_lr_tr_hall.json",
"resources/navigation/uni_interior3_n2_lr_tr_hall_aiperi.json",
"resources/navigation/uni_interior3_darklands_all_open.json", //5
"resources/navigation/uni_interior4_unlocked_hall.json",
"resources/navigation/uni_interior4_unlocked_n3.json",
"resources/navigation/uni_interior4_unlocked_s1.json",
"resources/navigation/uni_interior4_unlocked_s2.json",
"resources/navigation/uni_interior4_unlocked_s3.json",
"resources/navigation/uni_interior4_unlocked_s3_s1.json", //11
"resources/navigation/uni_interior4_unlocked_s3_hall.json",
"resources/navigation/uni_interior4_unlocked_s3_n3.json",
"resources/navigation/uni_interior4_unlocked_s3_n2.json",
"resources/navigation/uni_interior4_unlocked_s3_s2.json",
"resources/navigation/uni_interior4_locked.json" //16
};*/
/*
uniInteriorParams.navigationJsonPaths = {
"resources/navigation/uni_interior3_darklands_all_open.json",
"resources/navigation/uni_interior3_darklands_all_open.json",
"resources/navigation/uni_interior3_darklands_all_open.json",
"resources/navigation/uni_interior3_darklands_all_open.json",
"resources/navigation/uni_interior3_darklands_all_open.json",
"resources/navigation/uni_interior3_darklands_all_open.json",
"resources/navigation/uni_interior3_darklands_all_open.json", //6
"resources/navigation/uni_interior3_darklands_all_open.json",
"resources/navigation/uni_interior3_darklands_all_open.json",
"resources/navigation/uni_interior3_darklands_all_open.json",
"resources/navigation/uni_interior3_darklands_all_open.json",
"resources/navigation/uni_interior3_darklands_all_open.json",
"resources/navigation/uni_interior3_darklands_all_open.json",
"resources/navigation/uni_interior3_darklands_all_open.json"
};*/
uniInteriorParams.teleportsJsonPath = "resources/config/teleports_uni_interior.json";
uniInteriorParams.triggerZonesJsonPath = "resources/config/trigger_zones_uni_interior.json";
uniInteriorParams.lightsJsonPath = "resources/config/lights_uni_interior.json";
uniInteriorParams.scriptPath = "resources/start_uni_interior.lua";
uniInteriorParams.interactiveObjectsJsonPath = "resources/config/interactive_objects_uni_interior_x.json";
uniInteriorParams.playerPosition = Eigen::Vector3f(0.942694, 0, -9.63104);
gameState.locations["uni_interior"] = std::make_shared<Location>(renderer, gameState.inventory);
gameState.locations["uni_interior"]->setup(uniInteriorParams, &gameState.questJournal, audioPlayer.get());
gameState.locations["uni_interior"]->scriptEngine.setGlobalStore(&gameState.globalInts);
gameState.locations["uni_interior"]->scriptEngine.setGlobalFloatStore(&gameState.globalFloats);
gameState.locations["uni_interior"]->requestNightDayTransition = [this](bool isNight, bool isDawn) { gameState.isNight = isNight; gameState.isDawn = isDawn; };
gameState.locations["uni_interior"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
gameState.locations["uni_interior"]->requestAdvanceDarklandsHud = [this]() { menuManager.advanceUniIntDarklandsHud(); };
gameState.locations["uni_interior"]->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); };
gameState.locations["uni_interior"]->requestReturnToMainMenu = [this]() {
auto loc = std::make_shared<Location>(renderer, gameState.inventory);
loc->setup(params, &gameState.questJournal, audioPlayer.get());
loc->scriptEngine.setGlobalStore(&gameState.globalInts);
loc->scriptEngine.setGlobalFloatStore(&gameState.globalFloats);
loc->requestNightDayTransition = [this](bool isNight, bool isDawn) {
gameState.isNight = isNight;
gameState.isDawn = isDawn;
};
loc->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
loc->requestAdvanceDarklandsHud = [this]() { menuManager.advanceUniIntDarklandsHud(); };
loc->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); };
loc->requestReturnToMainMenu = [this]() {
gameState.currentLocationName.clear();
menuManager.showMenuAfterGameCompleted();
};
gameState.locations["uni_interior"]->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) {
loc->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) {
menuManager.setChatUnread(idx, unread, msg);
};
if (gameState.locations["uni_interior"]->player)
gameState.locations["uni_interior"]->player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); };
for (auto& npc : gameState.locations["uni_interior"]->npcs) {
if (loc->player)
loc->player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); };
for (auto& npc : loc->npcs) {
if (npc && npc->state.canAttack) {
npc->state.onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); };
}
}
gameState.locations["uni_interior"]->requestDarklandsPlayBattleMusic = [this]() {
loc->requestDarklandsPlayBattleMusic = [this]() {
audioPlayer->crossFadeMusicFromPositionAsync("music/bishkek fight.ogg");
menuManager.lastMusicTrack = "music/bishkek fight.ogg";
};
gameState.locations["uni_interior"]->requestDarklandsPlayNormalMusic = [this]() {
loc->requestDarklandsPlayNormalMusic = [this]() {
audioPlayer->crossFadeMusicFromPositionAsync("music/bishkek fight calm.ogg");
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
};
if (config.lockInteractiveObjectsDuringTutorial) {
// Objects can be picked up once the tutorial reaches "pick up an item".
loc->areInteractiveObjectsLocked = [this]() {
return gameState.tutorialStep < TutorialStep::PickUpItem;
};
}
if (config.taxi) {
const LocationConfig::Taxi& taxi = *config.taxi;
loc->state.taxiCarTriggerPosition = taxi.triggerPosition;
loc->state.taxiCarWaypoints = taxi.waypoints;
loc->state.taxiCarInitialState.position = taxi.initialPosition;
loc->state.taxiCarInitialState.rotation = taxi.initialRotation;
loc->state.taxiCarInitialState.mode = NpcCar::Mode::NONE;
loc->state.taxiCarInitialState.velocity = 0.f;
loc->state.taxiCarInitialState.steeringAngle = 0.f;
loc->state.taxiCarInitialState.waypointReachRadius = taxi.waypointReachRadius;
loc->state.taxiCar = loc->state.taxiCarInitialState;
}
return loc;
}
void Game::createLocationsStep3()
void Game::wireCampaignLocations(LocationMap& locations)
{
LocationSetup uniInteriorParams;
LocationSetup uniExteriorParams = uniInteriorParams;
uniExteriorParams.gameObjectsJsonPath = "resources/config/gameobjects_uni_exterior_x.json";
uniExteriorParams.interactiveObjectsJsonPath = "resources/config/interactive_objects_uni_exterior_x.json";
/*
uniExteriorParams.navigationJsonPaths = {
"resources/navigation/uni_exterior0.json",
"resources/navigation/uni_exterior0.json"
};*/
//uni_exterior0
/*uniExteriorParams.navigationJsonPaths = {
"resources/navigation/uni_exterior3_all.json",
"resources/navigation/uni_exterior3_all.json"
};*/
/*
uniExteriorParams.navigationJsonPaths = {
"resources/navigation/uni_exterior3_trees_car.json",
"resources/navigation/uni_exterior3_trees.json",
};*/
uniExteriorParams.navigationJsonPaths = {
"resources/navigation/uni_exterior3_trees_car.txt",
"resources/navigation/uni_exterior3_trees.txt",
};
uniExteriorParams.teleportsJsonPath = "resources/config/teleports_uni_exterior.json";
uniExteriorParams.triggerZonesJsonPath = "resources/config/trigger_zones_uni_exterior.json";
uniExteriorParams.lightsJsonPath = "resources/config/lights_uni_exterior.json";
uniExteriorParams.scriptPath = "resources/start_uni_exterior.lua";
uniExteriorParams.playerPosition = Eigen::Vector3f(5, 0, -18.4);
uniExteriorParams.npcsJsonPath = "resources/config/npcs_uni_exterior.json";
uniExteriorParams.dialoguesJsonPath = "resources/dialogue/uni_exterior_dialogues003.json";
gameState.locations["uni_exterior"] = std::make_shared<Location>(renderer, gameState.inventory);
gameState.locations["uni_exterior"]->setup(uniExteriorParams, &gameState.questJournal, audioPlayer.get());
gameState.locations["uni_exterior"]->scriptEngine.setGlobalStore(&gameState.globalInts);
gameState.locations["uni_exterior"]->scriptEngine.setGlobalFloatStore(&gameState.globalFloats);
gameState.locations["uni_exterior"]->requestNightDayTransition = [this](bool isNight, bool isDawn) {
gameState.isNight = isNight;
gameState.isDawn = isDawn;
};
gameState.locations["uni_exterior"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
gameState.locations["uni_exterior"]->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); };
//gameState.locations["uni_exterior"]->requestReturnToMainMenu = [this]() { menuManager.showMenuAfterGameCompleted(); };
gameState.locations["uni_exterior"]->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) {
menuManager.setChatUnread(idx, unread, msg);
};
if (gameState.locations["uni_exterior"]->player)
gameState.locations["uni_exterior"]->player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); };
//Exterior set up taxi:
gameState.locations["uni_exterior"]->state.taxiCarTriggerPosition = Eigen::Vector3f(-17.6, 0.05, -45);
gameState.locations["uni_exterior"]->state.taxiCarWaypoints = {
Vector3f(-17.6, 0.05, -90.0),
Vector3f(-17.6, 0.05, -75.0),
Vector3f(-17.6, 0.05, -55.0)
};
gameState.locations["uni_exterior"]->state.taxiCarInitialState.position = /*Vector3f(-17.6, 0.05, -40);*/ Vector3f(-17.6, 0.05, -100);
gameState.locations["uni_exterior"]->state.taxiCarInitialState.rotation = 180.f;
gameState.locations["uni_exterior"]->state.taxiCarInitialState.mode = NpcCar::Mode::NONE;
gameState.locations["uni_exterior"]->state.taxiCarInitialState.velocity = 0.f;
gameState.locations["uni_exterior"]->state.taxiCarInitialState.steeringAngle = 0.f;
gameState.locations["uni_exterior"]->state.taxiCarInitialState.waypointReachRadius = 5.f;
gameState.locations["uni_exterior"]->state.taxiCar = gameState.locations["uni_exterior"]->state.taxiCarInitialState;
//Exterior set up taxi end
}
void Game::createLocationsStep4()
{
LocationSetup params_dorm;
//params_dorm.gameObjectsJsonPath = "resources/config/gameobjects_dorm.json";
//params_dorm.gameObjectsJsonPath = "resources/config/gameobjects_dorm_trees001.json";
params_dorm.gameObjectsJsonPath = "resources/config/gameobjects_dorm_new_x.json";
params_dorm.npcsJsonPath = "resources/config/npcs_dorm.json";
params_dorm.dialoguesJsonPath = "resources/dialogue/dorm_dialogues004.json";
/*
params_dorm.navigationJsonPaths = {
"resources/navigation/dorm3_bca.json",
"resources/navigation/dorm3_ca.json",
"resources/navigation/dorm3_ba.json",
"resources/navigation/dorm3_a.json",
"resources/navigation/dorm3_b.json",
"resources/navigation/dorm3_all_open.json",
};*/
params_dorm.navigationJsonPaths = {
"resources/navigation/dorm3_bca.txt", //0
"resources/navigation/dorm3_ca.txt", //1
"resources/navigation/dorm3_ba.txt",
"resources/navigation/dorm3_a.txt",
"resources/navigation/dorm3_b.txt",
"resources/navigation/dorm3_all_open.txt", //5
};
/*
params_dorm.navigationJsonPaths = {
"resources/navigation/dorm0_large.json",
"resources/navigation/dorm0_large.json",
"resources/navigation/dorm0_large.json",
"resources/navigation/dorm0_large.json",
"resources/navigation/dorm0_large.json",
"resources/navigation/dorm0_large.json",
};*/
params_dorm.teleportsJsonPath = "resources/config/teleports_dorm.json";
params_dorm.triggerZonesJsonPath = "resources/config/trigger_zones_dorm.json";
params_dorm.lightsJsonPath = "resources/config/lights_dorm.json";
params_dorm.scriptPath = "resources/start_dorm.lua";
params_dorm.interactiveObjectsJsonPath = "resources/config/interactive_objects_dorm_x.json";
params_dorm.playerPosition = Eigen::Vector3f(6.76345, 0, -14.6022);
gameState.locations["location_dorm"] = std::make_shared<Location>(renderer, gameState.inventory);
gameState.locations["location_dorm"]->setup(params_dorm, &gameState.questJournal, audioPlayer.get());
gameState.locations["location_dorm"]->scriptEngine.setGlobalStore(&gameState.globalInts);
gameState.locations["location_dorm"]->scriptEngine.setGlobalFloatStore(&gameState.globalFloats);
gameState.locations["location_dorm"]->requestNightDayTransition = [this](bool isNight, bool isDawn) { gameState.isNight = isNight; gameState.isDawn = isDawn; };
gameState.locations["location_dorm"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
gameState.locations["location_dorm"]->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); };
//gameState.locations["location_dorm"]->requestReturnToMainMenu = [this]() { menuManager.showMenuAfterGameCompleted(); };
gameState.locations["location_dorm"]->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) {
menuManager.setChatUnread(idx, unread, msg);
};
if (gameState.locations["location_dorm"]->player)
gameState.locations["location_dorm"]->player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); };
// 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:
gameState.locations["location_dorm"]->state.taxiCarTriggerPosition = Eigen::Vector3f(-19.0, 0, 0);
gameState.locations["location_dorm"]->state.taxiCarWaypoints = {
Vector3f(-20.85, 0, 30.0),
Vector3f(-19.35, 0, 24.0),
Vector3f(-19.35, 0, 14.0)
};
gameState.locations["location_dorm"]->state.taxiCarInitialState.position = Vector3f(-20.85, 0, 64);
gameState.locations["location_dorm"]->state.taxiCarInitialState.rotation = 0.f;
gameState.locations["location_dorm"]->state.taxiCarInitialState.mode = NpcCar::Mode::NONE;
gameState.locations["location_dorm"]->state.taxiCarInitialState.velocity = 0.f;
gameState.locations["location_dorm"]->state.taxiCarInitialState.steeringAngle = 0.f;
gameState.locations["location_dorm"]->state.taxiCarInitialState.waypointReachRadius = 5.f;
gameState.locations["location_dorm"]->state.taxiCar = gameState.locations["location_dorm"]->state.taxiCarInitialState;
//Dorm set up taxi end
}
void Game::createLocationsStep5()
{
rearmDormOneShotCallbacks();
menuManager.callTaxiFunc = [this]()
{
if (gameState.locations["location_dorm"])
{
gameState.locations["location_dorm"]->onPlayerTaxiRequired = nullptr;
}
if (currentLocation())
{
currentLocation()->onTaxiCalled();
}
};
rearmOneShotCallbacks(locations);
// Teleport callbacks: destination name and position come from the teleport zone data.
auto teleportCallback = [this](const std::string& destName, const Eigen::Vector3f& destPos, float destRotY) {
@@ -918,19 +743,19 @@ namespace FRG
menuManager.onLocationChanged(destName);
};
gameState.locations["uni_exterior"]->onTeleport = teleportCallback;
gameState.locations["uni_interior"]->onTeleport = teleportCallback;
gameState.locations["location_dorm"]->onTeleport = teleportCallback;
for (auto& [name, loc] : locations) {
loc->onTeleport = teleportCallback;
}
// 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.
for (auto& [name, loc] : gameState.locations) {
for (auto& [name, loc] : locations) {
loc->dialogueSystem.setGlobalFlagStore(&gameState.globalInts);
}
// Wire tutorial advance callbacks for all locations.
// completeTutorialStep() only acts on the current step, so sharing is safe.
for (auto& [name, loc] : gameState.locations) {
for (auto& [name, loc] : locations) {
loc->dialogueSystem.setOnDialogueAdvanced([this]() {
gameState.completeTutorialStep(TutorialStep::AdvanceDialogue);
});
@@ -941,14 +766,14 @@ namespace FRG
}
// Wire chat-bubble callback so dynamic bubbles appear as dialogue lines are shown.
for (auto& [name, loc] : gameState.locations) {
for (auto& [name, loc] : locations) {
loc->dialogueSystem.setOnChatBubbleReady([this](const std::string& text, bool incoming) {
menuManager.onChatBubbleReady(text, incoming);
});
}
// Wire cutscene HUD: show skip button when cutscene starts, restore HUD when it ends.
for (auto& [name, loc] : gameState.locations) {
for (auto& [name, loc] : locations) {
loc->dialogueSystem.setOnCutsceneStarted([this]() {
menuManager.onCutsceneStarted();
});
@@ -958,7 +783,7 @@ namespace FRG
}
// Wire HP-change callbacks so all player instances update the health bar HUD.
for (auto& [name, loc] : gameState.locations) {
for (auto& [name, loc] : locations) {
if (loc->player) {
loc->player->state.onHpChanged = [this, loc](float hp, float maxHp, int attackerIndex) {
menuManager.updateHealthBar(hp, maxHp);
@@ -999,8 +824,8 @@ namespace FRG
CheckGlError(__FILE__, __LINE__);
logger() << "Shadow map initialized" << std::endl;
for (auto& [name, loc] : gameState.locations) {
loc->shadowMap = shadowMap;
for (auto& [campaignId, locations] : campaignLocations) {
for (auto& [name, loc] : locations) loc->shadowMap = shadowMap;
}
}
@@ -1011,8 +836,8 @@ namespace FRG
shadowMap.reset();
CheckGlError(__FILE__, __LINE__);
for (auto& [name, loc] : gameState.locations) {
loc->shadowMap.reset();
for (auto& [campaignId, locations] : campaignLocations) {
for (auto& [name, loc] : locations) loc->shadowMap.reset();
}
CheckGlError(__FILE__, __LINE__);
@@ -1891,6 +1716,7 @@ namespace FRG
nlohmann::json root = nlohmann::json::parse(content);
SaveSlotInfo info;
info.empty = false;
info.campaignId = root.value("campaignId", std::string(CampaignRegistry::kDefaultCampaignId));
info.locationName = root.value("currentLocationName", "");
info.savedAt = root.value("savedAt", "");
return info;
@@ -1924,6 +1750,9 @@ namespace FRG
}
try {
nlohmann::json root = nlohmann::json::parse(content);
// Every campaign is built at startup, so loading only switches to the save's
// campaign (its locations and quest definitions) before applying the state.
activateCampaign(root.value("campaignId", std::string(CampaignRegistry::kDefaultCampaignId)));
gameState.load(root);
} catch (const std::exception& e) {
std::cerr << "[save] Failed to parse save file " << path << ": " << e.what() << std::endl;