working on refactoring
This commit is contained in:
1 parent
9b71ee447a
commit
d5ef126264
12 files changed
+309
-219
No files matched your search
+149
-139
@@ -328,7 +328,7 @@ namespace FRG
|
||||
});
|
||||
|
||||
// Campaigns are only listed here; their locations are built on demand when the
|
||||
// player starts or loads one (see loadCampaignThen). MenuManager::setup() needs
|
||||
// player starts or loads one (see enterLocationThen). MenuManager::setup() needs
|
||||
// the list to preload each campaign's loading screen.
|
||||
CampaignRegistry::instance().loadFromJson("resources/config/campaigns.json", CONST_ZIP_FILE);
|
||||
|
||||
@@ -369,8 +369,6 @@ namespace FRG
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
||||
});
|
||||
@@ -385,8 +383,10 @@ namespace FRG
|
||||
}
|
||||
|
||||
menuManager.startGameFunc = [this](const std::string& campaignId) {
|
||||
auto start = [this, campaignId]() {
|
||||
loadCampaignThen(campaignId, [this]() {
|
||||
const CampaignConfig* config = CampaignRegistry::instance().find(campaignId);
|
||||
const std::string startLocation = config ? config->startLocation : std::string();
|
||||
auto start = [this, campaignId, startLocation]() {
|
||||
enterLocationThen(campaignId, startLocation, [this]() {
|
||||
performResetToInitialState();
|
||||
applyTutorialSetting();
|
||||
const CampaignConfig& campaign = activeCampaign();
|
||||
@@ -397,12 +397,12 @@ namespace FRG
|
||||
menuManager.lastMusicTrack = campaign.startMusic;
|
||||
});
|
||||
};
|
||||
if (campaignLocations.count(campaignId)) {
|
||||
if (isLocationLoaded(campaignId, startLocation)) {
|
||||
// Already loaded: the reset runs right away, so keep it off the menu
|
||||
// button's call stack.
|
||||
mainThreadHandler.EnqueueMainThreadTask(start);
|
||||
} else {
|
||||
// Not loaded: this only shows the loading screen and queues the build
|
||||
// Not loaded: this only shows the loading screen and queues the load
|
||||
// steps, so start it now -- deferring it would draw one more frame of
|
||||
// the main menu before the loading screen appears.
|
||||
start();
|
||||
@@ -480,20 +480,6 @@ namespace FRG
|
||||
loadStepsDone = 0;
|
||||
}
|
||||
|
||||
void Game::rearmOneShotCallbacks(LocationMap& locations) {
|
||||
for (auto& [name, loc] : locations) {
|
||||
loc->onPlayerTaxiRequired = [this]() {
|
||||
menuManager.tutorialShowTaxiHint();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void Game::activateCampaign(const std::string& campaignId) {
|
||||
gameState.campaignId = campaignId;
|
||||
gameState.locations = campaignLocations[campaignId];
|
||||
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);
|
||||
@@ -504,55 +490,130 @@ namespace FRG
|
||||
return *campaign;
|
||||
}
|
||||
|
||||
void Game::loadCampaignThen(const std::string& requestedId, std::function<void()> then) {
|
||||
const CampaignConfig* campaign = CampaignRegistry::instance().find(requestedId);
|
||||
bool Game::isLocationLoaded(const std::string& campaignId, const std::string& locationId) const {
|
||||
return player && loadedCampaignId == campaignId && gameState.locations.count(locationId) > 0;
|
||||
}
|
||||
|
||||
void Game::enterLocationThen(const std::string& requestedCampaignId, const std::string& requestedLocationId,
|
||||
std::function<void()> then)
|
||||
{
|
||||
const CampaignConfig* campaign = CampaignRegistry::instance().find(requestedCampaignId);
|
||||
if (!campaign) {
|
||||
std::cerr << "[campaign] Unknown campaign '" << requestedId << "', using '"
|
||||
std::cerr << "[campaign] Unknown campaign '" << requestedCampaignId << "', using '"
|
||||
<< CampaignRegistry::kDefaultCampaignId << "'" << std::endl;
|
||||
campaign = CampaignRegistry::instance().find(CampaignRegistry::kDefaultCampaignId);
|
||||
if (!campaign) return;
|
||||
}
|
||||
const std::string campaignId = campaign->id;
|
||||
const std::string locationId = campaign->findLocation(requestedLocationId)
|
||||
? requestedLocationId : campaign->startLocation;
|
||||
|
||||
if (campaignLocations.count(campaignId)) {
|
||||
activateCampaign(campaignId);
|
||||
if (isLocationLoaded(campaignId, locationId)) {
|
||||
then();
|
||||
return;
|
||||
}
|
||||
|
||||
// Build it behind the campaign's loading screen, one location per loading step.
|
||||
// The screen is hidden when the sequence completes (see runAsLoadingSequence).
|
||||
menuManager.showLoadingScreen(campaign->loadingScreenPath);
|
||||
// Behind the location's loading screen: release what's loaded first, then load the
|
||||
// new data, then continue. The screen is hidden when the sequence completes (see
|
||||
// runAsLoadingSequence).
|
||||
menuManager.showLoadingScreen(campaign->loadingScreenFor(locationId));
|
||||
std::vector<std::function<void()>> steps;
|
||||
steps.push_back([this, campaignId]() {
|
||||
unloadCampaigns(); // only one campaign is kept in memory
|
||||
campaignLocations[campaignId];
|
||||
gameState.globalFloats["player_hp"] = 200; // read by location scripts as they load
|
||||
});
|
||||
for (const std::string& configPath : campaign->locationConfigPaths) {
|
||||
steps.push_back([this, campaignId, configPath]() {
|
||||
LocationConfig config;
|
||||
if (LocationConfig::fromFile(configPath, CONST_ZIP_FILE, config))
|
||||
campaignLocations[campaignId][config.id] = createLocation(config);
|
||||
});
|
||||
if (loadedCampaignId != campaignId) {
|
||||
steps.push_back([this]() { unloadCampaign(); });
|
||||
steps.push_back([this, campaignId]() { loadCampaign(campaignId); });
|
||||
} else {
|
||||
steps.push_back([this]() { unloadLocation(true); });
|
||||
}
|
||||
steps.push_back([this, campaignId, then]() {
|
||||
LocationMap& locations = campaignLocations[campaignId];
|
||||
wireCampaignLocations(locations);
|
||||
if (shadowMap) {
|
||||
for (auto& [name, loc] : locations) loc->shadowMap = shadowMap;
|
||||
}
|
||||
activateCampaign(campaignId);
|
||||
then();
|
||||
});
|
||||
steps.push_back([this, locationId]() { loadLocation(locationId); });
|
||||
steps.push_back([then]() { then(); });
|
||||
menuManager.runAsLoadingSequence(std::move(steps));
|
||||
}
|
||||
|
||||
void Game::unloadCampaigns() {
|
||||
void Game::loadCampaign(const std::string& campaignId) {
|
||||
loadedCampaignId = campaignId;
|
||||
gameState.campaignId = campaignId;
|
||||
gameState.storedLocationStates.clear();
|
||||
gameState.questJournal.loadFromFile(activeCampaign().questsPath, CONST_ZIP_FILE);
|
||||
gameState.globalFloats["player_hp"] = 200; // read by location scripts as they load
|
||||
// The player stays loaded for the whole campaign; locations come and go around him.
|
||||
player = Location::createPlayer(renderer, audioPlayer.get());
|
||||
if (player)
|
||||
player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
||||
}
|
||||
|
||||
void Game::unloadCampaign() {
|
||||
unloadLocation(false);
|
||||
player.reset();
|
||||
loadedCampaignId.clear();
|
||||
gameState.storedLocationStates.clear();
|
||||
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 '"
|
||||
<< locationId << "'" << std::endl;
|
||||
return;
|
||||
}
|
||||
std::shared_ptr<Location> loc = createLocation(*config);
|
||||
wireLocation(loc.get());
|
||||
if (shadowMap) loc->shadowMap = shadowMap;
|
||||
// Back to how the player left it (or to the start state's snapshot of it).
|
||||
auto stored = gameState.storedLocationStates.find(locationId);
|
||||
if (stored != gameState.storedLocationStates.end()) loc->load(stored->second);
|
||||
gameState.locations[locationId] = loc;
|
||||
}
|
||||
|
||||
void Game::unloadLocation(bool storeState) {
|
||||
for (auto& [name, loc] : gameState.locations) {
|
||||
if (!storeState) continue;
|
||||
nlohmann::json locJson;
|
||||
loc->save(locJson);
|
||||
gameState.storedLocationStates[name] = std::move(locJson);
|
||||
}
|
||||
gameState.locations.clear();
|
||||
campaignLocations.clear();
|
||||
// Drop the cache entries of the textures the campaign loaded; UI images stay.
|
||||
renderer.textureManager.UnloadNonResident("resources/w/ui");
|
||||
// Textures only the location's models and characters used are freed now.
|
||||
renderer.textureManager.UnloadUnused();
|
||||
}
|
||||
|
||||
void Game::teleportPlayer(const std::string& destName, const Eigen::Vector3f& destPos, float destRotY) {
|
||||
logger() << "[TELEPORT] " << " -> " << destName << std::endl;
|
||||
if (!activeCampaign().findLocation(destName)) {
|
||||
std::cerr << "[TELEPORT] Unknown destination location: " << destName << std::endl;
|
||||
return;
|
||||
}
|
||||
if (currentLocation())
|
||||
{
|
||||
currentLocation()->scriptEngine.callLocationExitCallback();
|
||||
currentLocation()->dialogueSystem.stopDialogue();
|
||||
}
|
||||
// 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;
|
||||
Location* loc = currentLocation();
|
||||
if (!loc) return;
|
||||
if (loc->player) {
|
||||
loc->player->state.position = destPos;
|
||||
loc->player->setTarget(destPos);
|
||||
loc->player->stopInPlace();
|
||||
loc->player->state.facingAngle = destRotY;
|
||||
loc->player->state.targetFacingAngle = destRotY;
|
||||
}
|
||||
loc->state.cameraAzimuth = destRotY;
|
||||
loc->state.isDarklands = gameState.isDarklands;
|
||||
loc->state.isNight = gameState.isNight;
|
||||
loc->state.isDawn = gameState.isDawn;
|
||||
loc->scriptEngine.callLocationEnterCallback();
|
||||
|
||||
//Reset taxi
|
||||
loc->state.taxiCar = loc->state.taxiCarInitialState;
|
||||
loc->state.taxiArrivedCallbackCalled = false;
|
||||
gameState.taxiIsCalled = false;
|
||||
|
||||
menuManager.onLocationChanged(destName);
|
||||
});
|
||||
}
|
||||
|
||||
void Game::performFullReset() {
|
||||
@@ -570,28 +631,22 @@ namespace FRG
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
// 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.storedLocationStates.clear();
|
||||
std::vector<std::string> loadedIds;
|
||||
for (auto& [name, loc] : gameState.locations) loadedIds.push_back(name);
|
||||
unloadLocation(false);
|
||||
for (const std::string& id : loadedIds) loadLocation(id);
|
||||
|
||||
// Developer workflow: uncomment to (re)generate the campaign's start-state JSON
|
||||
// from this freshly-rebuilt state after changing gameplay/content JSON or Lua.
|
||||
@@ -625,16 +680,9 @@ namespace FRG
|
||||
// must be cleared explicitly before loading it.
|
||||
gameState.globalInts.clear();
|
||||
gameState.globalFloats.clear();
|
||||
gameState.load(root); // updates existing Location objects in place -- no
|
||||
// destruction, no re-parsing JSON/nav files, no new
|
||||
// sol2 state (GameState::load only touches map entries
|
||||
// that already exist).
|
||||
gameState.load(root); // applies to the loaded location in place and stores
|
||||
// the other locations' states for when they load.
|
||||
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 -- 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()
|
||||
@@ -686,6 +734,7 @@ namespace FRG
|
||||
params.triggerZonesJsonPath = config.triggerZonesJsonPath;
|
||||
params.lightsJsonPath = config.lightsJsonPath;
|
||||
params.playerPosition = config.playerPosition;
|
||||
params.player = player;
|
||||
|
||||
auto loc = std::make_shared<Location>(renderer, gameState.inventory);
|
||||
loc->setup(params, &gameState.questJournal, audioPlayer.get());
|
||||
@@ -705,8 +754,6 @@ namespace FRG
|
||||
loc->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) {
|
||||
menuManager.setChatUnread(idx, unread, msg);
|
||||
};
|
||||
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(); };
|
||||
@@ -744,59 +791,26 @@ namespace FRG
|
||||
return loc;
|
||||
}
|
||||
|
||||
void Game::wireCampaignLocations(LocationMap& locations)
|
||||
void Game::wireLocation(Location* loc)
|
||||
{
|
||||
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) {
|
||||
logger() << "[TELEPORT] " << " -> " << destName << std::endl;
|
||||
auto it = gameState.locations.find(destName);
|
||||
if (it == gameState.locations.end()) {
|
||||
std::cerr << "[TELEPORT] Unknown destination location: " << destName << std::endl;
|
||||
return;
|
||||
}
|
||||
if (currentLocation())
|
||||
{
|
||||
currentLocation()->scriptEngine.callLocationExitCallback();
|
||||
currentLocation()->dialogueSystem.stopDialogue();
|
||||
}
|
||||
gameState.currentLocationName = destName;
|
||||
Location* loc = currentLocation();
|
||||
if (loc->player) {
|
||||
loc->player->state.position = destPos;
|
||||
loc->player->setTarget(destPos);
|
||||
loc->player->stopInPlace();
|
||||
loc->player->state.facingAngle = destRotY;
|
||||
loc->player->state.targetFacingAngle = destRotY;
|
||||
}
|
||||
loc->state.cameraAzimuth = destRotY;
|
||||
loc->state.isDarklands = gameState.isDarklands;
|
||||
loc->state.isNight = gameState.isNight;
|
||||
loc->state.isDawn = gameState.isDawn;
|
||||
loc->scriptEngine.callLocationEnterCallback();
|
||||
|
||||
//Reset taxi
|
||||
loc->state.taxiCar = loc->state.taxiCarInitialState;
|
||||
loc->state.taxiArrivedCallbackCalled = false;
|
||||
gameState.taxiIsCalled = false;
|
||||
|
||||
menuManager.onLocationChanged(destName);
|
||||
loc->onPlayerTaxiRequired = [this]() {
|
||||
menuManager.tutorialShowTaxiHint();
|
||||
};
|
||||
|
||||
for (auto& [name, loc] : locations) {
|
||||
loc->onTeleport = teleportCallback;
|
||||
}
|
||||
// Teleport zones: destination name and position come from the teleport zone data.
|
||||
loc->onTeleport = [this](const std::string& destName, const Eigen::Vector3f& destPos, float destRotY) {
|
||||
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.
|
||||
for (auto& [name, loc] : locations) {
|
||||
{
|
||||
loc->dialogueSystem.setGlobalFlagStore(&gameState.globalInts);
|
||||
}
|
||||
|
||||
// Wire tutorial advance callbacks for all locations.
|
||||
// Wire tutorial advance callbacks.
|
||||
// completeTutorialStep() only acts on the current step, so sharing is safe.
|
||||
for (auto& [name, loc] : locations) {
|
||||
{
|
||||
loc->dialogueSystem.setOnDialogueAdvanced([this]() {
|
||||
gameState.completeTutorialStep(TutorialStep::AdvanceDialogue);
|
||||
});
|
||||
@@ -807,14 +821,14 @@ namespace FRG
|
||||
}
|
||||
|
||||
// Wire chat-bubble callback so dynamic bubbles appear as dialogue lines are shown.
|
||||
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] : locations) {
|
||||
{
|
||||
loc->dialogueSystem.setOnCutsceneStarted([this]() {
|
||||
menuManager.onCutsceneStarted();
|
||||
});
|
||||
@@ -823,12 +837,11 @@ namespace FRG
|
||||
});
|
||||
}
|
||||
|
||||
// Wire HP-change callbacks so all player instances update the health bar HUD.
|
||||
for (auto& [name, locPtr] : locations) {
|
||||
if (locPtr->player) {
|
||||
// Raw pointer: the location owns this player, so a shared_ptr here would be
|
||||
// a reference cycle that keeps unloaded locations alive.
|
||||
Location* loc = locPtr.get();
|
||||
// Wire HP-change callbacks so the player updates the health bar HUD.
|
||||
{
|
||||
if (loc->player) {
|
||||
// Raw pointer: the player belongs to the location (and outlives it), so a
|
||||
// shared_ptr here would keep unloaded locations alive. Rewired on every load.
|
||||
loc->player->state.onHpChanged = [this, loc](float hp, float maxHp, int attackerIndex) {
|
||||
menuManager.updateHealthBar(hp, maxHp);
|
||||
|
||||
@@ -868,9 +881,7 @@ namespace FRG
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
logger() << "Shadow map initialized" << std::endl;
|
||||
|
||||
for (auto& [campaignId, locations] : campaignLocations) {
|
||||
for (auto& [name, loc] : locations) loc->shadowMap = shadowMap;
|
||||
}
|
||||
for (auto& [name, loc] : gameState.locations) loc->shadowMap = shadowMap;
|
||||
|
||||
}
|
||||
void Game::destroyShadowMapAndProparateToLocations()
|
||||
@@ -880,9 +891,7 @@ namespace FRG
|
||||
shadowMap.reset();
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
|
||||
for (auto& [campaignId, locations] : campaignLocations) {
|
||||
for (auto& [name, loc] : locations) loc->shadowMap.reset();
|
||||
}
|
||||
for (auto& [name, loc] : gameState.locations) loc->shadowMap.reset();
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
|
||||
}
|
||||
@@ -1142,7 +1151,7 @@ namespace FRG
|
||||
if (!menuManager.isMainMenuOpen())
|
||||
menuManager.update(static_cast<float>(delta));
|
||||
|
||||
if (currentLocation())
|
||||
if (loadingCompleted && currentLocation())
|
||||
{
|
||||
currentLocation()->update(delta);
|
||||
}
|
||||
@@ -1825,9 +1834,10 @@ namespace FRG
|
||||
return;
|
||||
}
|
||||
|
||||
// The save's campaign may not be loaded yet; the state is applied once it is.
|
||||
// The save's campaign and location may not be loaded yet; the state is applied once they are.
|
||||
const std::string campaignId = root.value("campaignId", std::string(CampaignRegistry::kDefaultCampaignId));
|
||||
loadCampaignThen(campaignId, [this, root]() {
|
||||
const std::string locationId = root.value("currentLocationName", std::string());
|
||||
enterLocationThen(campaignId, locationId, [this, root]() {
|
||||
gameState.load(root);
|
||||
playMusicForLoadedGame();
|
||||
menuManager.enterGameplay(true);
|
||||
|
||||
Reference in new issue
Block a user