working on refactoring
This commit is contained in:
1 parent
9b71ee447a
commit
d5ef126264
12 files changed
+309
-219
No files matched your search
@@ -44,6 +44,7 @@ namespace FRG {
|
|||||||
out.lightsJsonPath = j.value("lights", "");
|
out.lightsJsonPath = j.value("lights", "");
|
||||||
if (j.contains("playerPosition")) out.playerPosition = readVec3(j["playerPosition"]);
|
if (j.contains("playerPosition")) out.playerPosition = readVec3(j["playerPosition"]);
|
||||||
out.lockInteractiveObjectsDuringTutorial = j.value("lockInteractiveObjectsDuringTutorial", false);
|
out.lockInteractiveObjectsDuringTutorial = j.value("lockInteractiveObjectsDuringTutorial", false);
|
||||||
|
out.loadingScreenPath = j.value("loadingScreen", "");
|
||||||
|
|
||||||
if (j.contains("taxi") && j["taxi"].is_object()) {
|
if (j.contains("taxi") && j["taxi"].is_object()) {
|
||||||
const json& t = j["taxi"];
|
const json& t = j["taxi"];
|
||||||
@@ -71,6 +72,18 @@ namespace FRG {
|
|||||||
return titles.empty() ? id : titles.begin()->second;
|
return titles.empty() ? id : titles.begin()->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LocationConfig* CampaignConfig::findLocation(const std::string& locationId) const {
|
||||||
|
for (const auto& loc : locations) {
|
||||||
|
if (loc.id == locationId) return &loc;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string CampaignConfig::loadingScreenFor(const std::string& locationId) const {
|
||||||
|
const LocationConfig* loc = findLocation(locationId);
|
||||||
|
return (loc && !loc->loadingScreenPath.empty()) ? loc->loadingScreenPath : loadingScreenPath;
|
||||||
|
}
|
||||||
|
|
||||||
CampaignRegistry& CampaignRegistry::instance() {
|
CampaignRegistry& CampaignRegistry::instance() {
|
||||||
static CampaignRegistry reg;
|
static CampaignRegistry reg;
|
||||||
return reg;
|
return reg;
|
||||||
@@ -97,6 +110,10 @@ namespace FRG {
|
|||||||
c.questsPath = entry.value("quests", "");
|
c.questsPath = entry.value("quests", "");
|
||||||
c.tutorial = entry.value("tutorial", false);
|
c.tutorial = entry.value("tutorial", false);
|
||||||
c.loadingScreenPath = entry.value("loadingScreen", std::string(CampaignConfig::kDefaultLoadingScreen));
|
c.loadingScreenPath = entry.value("loadingScreen", std::string(CampaignConfig::kDefaultLoadingScreen));
|
||||||
|
for (const std::string& path : c.locationConfigPaths) {
|
||||||
|
LocationConfig loc;
|
||||||
|
if (LocationConfig::fromFile(path, zipFile, loc)) c.locations.push_back(std::move(loc));
|
||||||
|
}
|
||||||
campaigns_.push_back(std::move(c));
|
campaigns_.push_back(std::move(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ namespace FRG {
|
|||||||
// Interactive objects stay locked until the tutorial reaches "pick up an item".
|
// Interactive objects stay locked until the tutorial reaches "pick up an item".
|
||||||
bool lockInteractiveObjectsDuringTutorial = false;
|
bool lockInteractiveObjectsDuringTutorial = false;
|
||||||
|
|
||||||
|
// UI layout shown while this location loads; empty = the campaign's loading screen.
|
||||||
|
std::string loadingScreenPath;
|
||||||
|
|
||||||
// Returns false (and logs) when the file can't be read or parsed.
|
// Returns false (and logs) when the file can't be read or parsed.
|
||||||
static bool fromFile(const std::string& path, const std::string& zipFile, LocationConfig& out);
|
static bool fromFile(const std::string& path, const std::string& zipFile, LocationConfig& out);
|
||||||
};
|
};
|
||||||
@@ -45,6 +48,8 @@ namespace FRG {
|
|||||||
std::string id;
|
std::string id;
|
||||||
std::map<std::string, std::string> titles; // language code -> title
|
std::map<std::string, std::string> titles; // language code -> title
|
||||||
std::vector<std::string> locationConfigPaths;
|
std::vector<std::string> locationConfigPaths;
|
||||||
|
// Parsed from locationConfigPaths when the registry loads.
|
||||||
|
std::vector<LocationConfig> locations;
|
||||||
std::string startLocation;
|
std::string startLocation;
|
||||||
std::string startMusic;
|
std::string startMusic;
|
||||||
std::string startStatePath;
|
std::string startStatePath;
|
||||||
@@ -56,6 +61,10 @@ namespace FRG {
|
|||||||
static constexpr const char* kDefaultLoadingScreen = "resources/w/ui/screen_loading.json";
|
static constexpr const char* kDefaultLoadingScreen = "resources/w/ui/screen_loading.json";
|
||||||
|
|
||||||
std::string title(Language lang) const;
|
std::string title(Language lang) const;
|
||||||
|
// Returns nullptr if the campaign has no such location.
|
||||||
|
const LocationConfig* findLocation(const std::string& locationId) const;
|
||||||
|
// The location's own loading screen, else the campaign's.
|
||||||
|
std::string loadingScreenFor(const std::string& locationId) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Campaigns listed in resources/config/campaigns.json, in display order.
|
// Campaigns listed in resources/config/campaigns.json, in display order.
|
||||||
|
|||||||
+149
-139
@@ -328,7 +328,7 @@ namespace FRG
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Campaigns are only listed here; their locations are built on demand when the
|
// 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.
|
// the list to preload each campaign's loading screen.
|
||||||
CampaignRegistry::instance().loadFromJson("resources/config/campaigns.json", CONST_ZIP_FILE);
|
CampaignRegistry::instance().loadFromJson("resources/config/campaigns.json", CONST_ZIP_FILE);
|
||||||
|
|
||||||
@@ -369,8 +369,6 @@ namespace FRG
|
|||||||
};
|
};
|
||||||
|
|
||||||
menuManager.callTaxiFunc = [this]() {
|
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();
|
if (currentLocation()) currentLocation()->onTaxiCalled();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -385,8 +383,10 @@ namespace FRG
|
|||||||
}
|
}
|
||||||
|
|
||||||
menuManager.startGameFunc = [this](const std::string& campaignId) {
|
menuManager.startGameFunc = [this](const std::string& campaignId) {
|
||||||
auto start = [this, campaignId]() {
|
const CampaignConfig* config = CampaignRegistry::instance().find(campaignId);
|
||||||
loadCampaignThen(campaignId, [this]() {
|
const std::string startLocation = config ? config->startLocation : std::string();
|
||||||
|
auto start = [this, campaignId, startLocation]() {
|
||||||
|
enterLocationThen(campaignId, startLocation, [this]() {
|
||||||
performResetToInitialState();
|
performResetToInitialState();
|
||||||
applyTutorialSetting();
|
applyTutorialSetting();
|
||||||
const CampaignConfig& campaign = activeCampaign();
|
const CampaignConfig& campaign = activeCampaign();
|
||||||
@@ -397,12 +397,12 @@ namespace FRG
|
|||||||
menuManager.lastMusicTrack = campaign.startMusic;
|
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
|
// Already loaded: the reset runs right away, so keep it off the menu
|
||||||
// button's call stack.
|
// button's call stack.
|
||||||
mainThreadHandler.EnqueueMainThreadTask(start);
|
mainThreadHandler.EnqueueMainThreadTask(start);
|
||||||
} else {
|
} 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
|
// steps, so start it now -- deferring it would draw one more frame of
|
||||||
// the main menu before the loading screen appears.
|
// the main menu before the loading screen appears.
|
||||||
start();
|
start();
|
||||||
@@ -480,20 +480,6 @@ namespace FRG
|
|||||||
loadStepsDone = 0;
|
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& Game::activeCampaign() const {
|
||||||
const CampaignConfig* campaign = CampaignRegistry::instance().find(gameState.campaignId);
|
const CampaignConfig* campaign = CampaignRegistry::instance().find(gameState.campaignId);
|
||||||
if (!campaign) campaign = CampaignRegistry::instance().find(CampaignRegistry::kDefaultCampaignId);
|
if (!campaign) campaign = CampaignRegistry::instance().find(CampaignRegistry::kDefaultCampaignId);
|
||||||
@@ -504,55 +490,130 @@ namespace FRG
|
|||||||
return *campaign;
|
return *campaign;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::loadCampaignThen(const std::string& requestedId, std::function<void()> then) {
|
bool Game::isLocationLoaded(const std::string& campaignId, const std::string& locationId) const {
|
||||||
const CampaignConfig* campaign = CampaignRegistry::instance().find(requestedId);
|
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) {
|
if (!campaign) {
|
||||||
std::cerr << "[campaign] Unknown campaign '" << requestedId << "', using '"
|
std::cerr << "[campaign] Unknown campaign '" << requestedCampaignId << "', using '"
|
||||||
<< CampaignRegistry::kDefaultCampaignId << "'" << std::endl;
|
<< CampaignRegistry::kDefaultCampaignId << "'" << std::endl;
|
||||||
campaign = CampaignRegistry::instance().find(CampaignRegistry::kDefaultCampaignId);
|
campaign = CampaignRegistry::instance().find(CampaignRegistry::kDefaultCampaignId);
|
||||||
if (!campaign) return;
|
if (!campaign) return;
|
||||||
}
|
}
|
||||||
const std::string campaignId = campaign->id;
|
const std::string campaignId = campaign->id;
|
||||||
|
const std::string locationId = campaign->findLocation(requestedLocationId)
|
||||||
|
? requestedLocationId : campaign->startLocation;
|
||||||
|
|
||||||
if (campaignLocations.count(campaignId)) {
|
if (isLocationLoaded(campaignId, locationId)) {
|
||||||
activateCampaign(campaignId);
|
|
||||||
then();
|
then();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build it behind the campaign's loading screen, one location per loading step.
|
// Behind the location's loading screen: release what's loaded first, then load the
|
||||||
// The screen is hidden when the sequence completes (see runAsLoadingSequence).
|
// new data, then continue. The screen is hidden when the sequence completes (see
|
||||||
menuManager.showLoadingScreen(campaign->loadingScreenPath);
|
// runAsLoadingSequence).
|
||||||
|
menuManager.showLoadingScreen(campaign->loadingScreenFor(locationId));
|
||||||
std::vector<std::function<void()>> steps;
|
std::vector<std::function<void()>> steps;
|
||||||
steps.push_back([this, campaignId]() {
|
if (loadedCampaignId != campaignId) {
|
||||||
unloadCampaigns(); // only one campaign is kept in memory
|
steps.push_back([this]() { unloadCampaign(); });
|
||||||
campaignLocations[campaignId];
|
steps.push_back([this, campaignId]() { loadCampaign(campaignId); });
|
||||||
gameState.globalFloats["player_hp"] = 200; // read by location scripts as they load
|
} else {
|
||||||
});
|
steps.push_back([this]() { unloadLocation(true); });
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
steps.push_back([this, campaignId, then]() {
|
steps.push_back([this, locationId]() { loadLocation(locationId); });
|
||||||
LocationMap& locations = campaignLocations[campaignId];
|
steps.push_back([then]() { then(); });
|
||||||
wireCampaignLocations(locations);
|
|
||||||
if (shadowMap) {
|
|
||||||
for (auto& [name, loc] : locations) loc->shadowMap = shadowMap;
|
|
||||||
}
|
|
||||||
activateCampaign(campaignId);
|
|
||||||
then();
|
|
||||||
});
|
|
||||||
menuManager.runAsLoadingSequence(std::move(steps));
|
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();
|
gameState.locations.clear();
|
||||||
campaignLocations.clear();
|
// Textures only the location's models and characters used are freed now.
|
||||||
// Drop the cache entries of the textures the campaign loaded; UI images stay.
|
renderer.textureManager.UnloadUnused();
|
||||||
renderer.textureManager.UnloadNonResident("resources/w/ui");
|
}
|
||||||
|
|
||||||
|
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() {
|
void Game::performFullReset() {
|
||||||
@@ -570,28 +631,22 @@ namespace FRG
|
|||||||
gameState.tutorialPhoneChatScreenOpened = false;
|
gameState.tutorialPhoneChatScreenOpened = false;
|
||||||
gameState.tutorialJournalScreenOpened = false;
|
gameState.tutorialJournalScreenOpened = false;
|
||||||
gameState.tutorialNeedOpenTaxiScreen = false;
|
gameState.tutorialNeedOpenTaxiScreen = false;
|
||||||
|
gameState.tutorialTaxiHintDone = false;
|
||||||
for (int i = 0; i < 3; ++i) {
|
for (int i = 0; i < 3; ++i) {
|
||||||
gameState.chatUnread[i] = true;
|
gameState.chatUnread[i] = true;
|
||||||
gameState.chatPreviewMsg[i].clear();
|
gameState.chatPreviewMsg[i].clear();
|
||||||
gameState.chatHistory[i].clear();
|
gameState.chatHistory[i].clear();
|
||||||
}
|
}
|
||||||
gameState.inventory.clear();
|
gameState.inventory.clear();
|
||||||
|
gameState.questJournal.loadFromFile(activeCampaign().questsPath, CONST_ZIP_FILE);
|
||||||
|
|
||||||
const CampaignConfig& campaign = activeCampaign();
|
// Rebuild the loaded location from its config; the others have no stored state
|
||||||
gameState.questJournal.loadFromFile(campaign.questsPath, CONST_ZIP_FILE);
|
// any more, so they are built fresh when the player first goes there.
|
||||||
|
gameState.storedLocationStates.clear();
|
||||||
LocationMap& locations = campaignLocations[campaign.id];
|
std::vector<std::string> loadedIds;
|
||||||
locations.clear();
|
for (auto& [name, loc] : gameState.locations) loadedIds.push_back(name);
|
||||||
for (const std::string& configPath : campaign.locationConfigPaths) {
|
unloadLocation(false);
|
||||||
LocationConfig config;
|
for (const std::string& id : loadedIds) loadLocation(id);
|
||||||
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
|
// Developer workflow: uncomment to (re)generate the campaign's start-state JSON
|
||||||
// from this freshly-rebuilt state after changing gameplay/content JSON or Lua.
|
// 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.
|
// must be cleared explicitly before loading it.
|
||||||
gameState.globalInts.clear();
|
gameState.globalInts.clear();
|
||||||
gameState.globalFloats.clear();
|
gameState.globalFloats.clear();
|
||||||
gameState.load(root); // updates existing Location objects in place -- no
|
gameState.load(root); // applies to the loaded location in place and stores
|
||||||
// destruction, no re-parsing JSON/nav files, no new
|
// the other locations' states for when they load.
|
||||||
// sol2 state (GameState::load only touches map entries
|
|
||||||
// that already exist).
|
|
||||||
gameState.campaignId = campaignId; // the snapshot may predate campaigns
|
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;
|
return;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
logger() << "[reset] Failed to parse " << path << ": " << e.what()
|
logger() << "[reset] Failed to parse " << path << ": " << e.what()
|
||||||
@@ -686,6 +734,7 @@ namespace FRG
|
|||||||
params.triggerZonesJsonPath = config.triggerZonesJsonPath;
|
params.triggerZonesJsonPath = config.triggerZonesJsonPath;
|
||||||
params.lightsJsonPath = config.lightsJsonPath;
|
params.lightsJsonPath = config.lightsJsonPath;
|
||||||
params.playerPosition = config.playerPosition;
|
params.playerPosition = config.playerPosition;
|
||||||
|
params.player = player;
|
||||||
|
|
||||||
auto loc = std::make_shared<Location>(renderer, gameState.inventory);
|
auto loc = std::make_shared<Location>(renderer, gameState.inventory);
|
||||||
loc->setup(params, &gameState.questJournal, audioPlayer.get());
|
loc->setup(params, &gameState.questJournal, audioPlayer.get());
|
||||||
@@ -705,8 +754,6 @@ namespace FRG
|
|||||||
loc->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);
|
menuManager.setChatUnread(idx, unread, msg);
|
||||||
};
|
};
|
||||||
if (loc->player)
|
|
||||||
loc->player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
|
||||||
for (auto& npc : loc->npcs) {
|
for (auto& npc : loc->npcs) {
|
||||||
if (npc && npc->state.canAttack) {
|
if (npc && npc->state.canAttack) {
|
||||||
npc->state.onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); };
|
npc->state.onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); };
|
||||||
@@ -744,59 +791,26 @@ namespace FRG
|
|||||||
return loc;
|
return loc;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::wireCampaignLocations(LocationMap& locations)
|
void Game::wireLocation(Location* loc)
|
||||||
{
|
{
|
||||||
rearmOneShotCallbacks(locations);
|
loc->onPlayerTaxiRequired = [this]() {
|
||||||
|
menuManager.tutorialShowTaxiHint();
|
||||||
// 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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (auto& [name, loc] : locations) {
|
// Teleport zones: destination name and position come from the teleport zone data.
|
||||||
loc->onTeleport = teleportCallback;
|
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
|
// 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.
|
// dialogue JSON and via Lua set_dialogue_flag all see the same state.
|
||||||
for (auto& [name, loc] : locations) {
|
{
|
||||||
loc->dialogueSystem.setGlobalFlagStore(&gameState.globalInts);
|
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.
|
// completeTutorialStep() only acts on the current step, so sharing is safe.
|
||||||
for (auto& [name, loc] : locations) {
|
{
|
||||||
loc->dialogueSystem.setOnDialogueAdvanced([this]() {
|
loc->dialogueSystem.setOnDialogueAdvanced([this]() {
|
||||||
gameState.completeTutorialStep(TutorialStep::AdvanceDialogue);
|
gameState.completeTutorialStep(TutorialStep::AdvanceDialogue);
|
||||||
});
|
});
|
||||||
@@ -807,14 +821,14 @@ namespace FRG
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wire chat-bubble callback so dynamic bubbles appear as dialogue lines are shown.
|
// 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) {
|
loc->dialogueSystem.setOnChatBubbleReady([this](const std::string& text, bool incoming) {
|
||||||
menuManager.onChatBubbleReady(text, incoming);
|
menuManager.onChatBubbleReady(text, incoming);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wire cutscene HUD: show skip button when cutscene starts, restore HUD when it ends.
|
// Wire cutscene HUD: show skip button when cutscene starts, restore HUD when it ends.
|
||||||
for (auto& [name, loc] : locations) {
|
{
|
||||||
loc->dialogueSystem.setOnCutsceneStarted([this]() {
|
loc->dialogueSystem.setOnCutsceneStarted([this]() {
|
||||||
menuManager.onCutsceneStarted();
|
menuManager.onCutsceneStarted();
|
||||||
});
|
});
|
||||||
@@ -823,12 +837,11 @@ namespace FRG
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wire HP-change callbacks so all player instances update the health bar HUD.
|
// Wire HP-change callbacks so the player updates the health bar HUD.
|
||||||
for (auto& [name, locPtr] : locations) {
|
{
|
||||||
if (locPtr->player) {
|
if (loc->player) {
|
||||||
// Raw pointer: the location owns this player, so a shared_ptr here would be
|
// Raw pointer: the player belongs to the location (and outlives it), so a
|
||||||
// a reference cycle that keeps unloaded locations alive.
|
// shared_ptr here would keep unloaded locations alive. Rewired on every load.
|
||||||
Location* loc = locPtr.get();
|
|
||||||
loc->player->state.onHpChanged = [this, loc](float hp, float maxHp, int attackerIndex) {
|
loc->player->state.onHpChanged = [this, loc](float hp, float maxHp, int attackerIndex) {
|
||||||
menuManager.updateHealthBar(hp, maxHp);
|
menuManager.updateHealthBar(hp, maxHp);
|
||||||
|
|
||||||
@@ -868,9 +881,7 @@ namespace FRG
|
|||||||
CheckGlError(__FILE__, __LINE__);
|
CheckGlError(__FILE__, __LINE__);
|
||||||
logger() << "Shadow map initialized" << std::endl;
|
logger() << "Shadow map initialized" << std::endl;
|
||||||
|
|
||||||
for (auto& [campaignId, locations] : campaignLocations) {
|
for (auto& [name, loc] : gameState.locations) loc->shadowMap = shadowMap;
|
||||||
for (auto& [name, loc] : locations) loc->shadowMap = shadowMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
void Game::destroyShadowMapAndProparateToLocations()
|
void Game::destroyShadowMapAndProparateToLocations()
|
||||||
@@ -880,9 +891,7 @@ namespace FRG
|
|||||||
shadowMap.reset();
|
shadowMap.reset();
|
||||||
CheckGlError(__FILE__, __LINE__);
|
CheckGlError(__FILE__, __LINE__);
|
||||||
|
|
||||||
for (auto& [campaignId, locations] : campaignLocations) {
|
for (auto& [name, loc] : gameState.locations) loc->shadowMap.reset();
|
||||||
for (auto& [name, loc] : locations) loc->shadowMap.reset();
|
|
||||||
}
|
|
||||||
CheckGlError(__FILE__, __LINE__);
|
CheckGlError(__FILE__, __LINE__);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1142,7 +1151,7 @@ namespace FRG
|
|||||||
if (!menuManager.isMainMenuOpen())
|
if (!menuManager.isMainMenuOpen())
|
||||||
menuManager.update(static_cast<float>(delta));
|
menuManager.update(static_cast<float>(delta));
|
||||||
|
|
||||||
if (currentLocation())
|
if (loadingCompleted && currentLocation())
|
||||||
{
|
{
|
||||||
currentLocation()->update(delta);
|
currentLocation()->update(delta);
|
||||||
}
|
}
|
||||||
@@ -1825,9 +1834,10 @@ namespace FRG
|
|||||||
return;
|
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));
|
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);
|
gameState.load(root);
|
||||||
playMusicForLoadedGame();
|
playMusicForLoadedGame();
|
||||||
menuManager.enterGameplay(true);
|
menuManager.enterGameplay(true);
|
||||||
|
|||||||
+26
-22
@@ -128,46 +128,50 @@ namespace FRG {
|
|||||||
void loadGame(int slot);
|
void loadGame(int slot);
|
||||||
void playMusicForLoadedGame();
|
void playMusicForLoadedGame();
|
||||||
SaveSlotInfo readSlotInfo(int slot) const;
|
SaveSlotInfo readSlotInfo(int slot) const;
|
||||||
// Locations of the loaded campaign (campaign id -> locations); at most one entry,
|
// Loading model: one campaign at a time, and within it only the location the player
|
||||||
// empty until the player starts or loads a game. gameState.locations is a copy
|
// is in (gameState.locations). Leaving a location stores its state in
|
||||||
// of the active campaign's entry.
|
// gameState.storedLocationStates and releases it; the player is created once per
|
||||||
std::unordered_map<std::string, LocationMap> campaignLocations;
|
// campaign and shared by its locations.
|
||||||
|
std::string loadedCampaignId;
|
||||||
|
std::shared_ptr<Character> player;
|
||||||
|
|
||||||
|
bool isLocationLoaded(const std::string& campaignId, const std::string& locationId) const;
|
||||||
|
// Runs `then` once `locationId` of `campaignId` is the loaded location. If it isn't,
|
||||||
|
// it is loaded first behind its loading screen, releasing the previous location
|
||||||
|
// (or, for another campaign, the whole previous campaign).
|
||||||
|
void enterLocationThen(const std::string& campaignId, const std::string& locationId,
|
||||||
|
std::function<void()> then);
|
||||||
|
// Starts the campaign: its quests and the player. No location is loaded yet.
|
||||||
|
void loadCampaign(const std::string& campaignId);
|
||||||
|
void unloadCampaign();
|
||||||
|
// Builds the campaign's location and restores its stored state, if any.
|
||||||
|
void loadLocation(const std::string& locationId);
|
||||||
|
// Releases the loaded location, keeping its state in storedLocationStates if asked.
|
||||||
|
void unloadLocation(bool storeState);
|
||||||
|
// Teleport zones: moves the player to another location of the campaign.
|
||||||
|
void teleportPlayer(const std::string& destName, const Eigen::Vector3f& destPos, float destRotY);
|
||||||
|
|
||||||
// Builds one location from its config and wires the callbacks every location gets.
|
// Builds one location from its config and wires the callbacks every location gets.
|
||||||
std::shared_ptr<Location> createLocation(const LocationConfig& config);
|
std::shared_ptr<Location> createLocation(const LocationConfig& config);
|
||||||
// Wires the callbacks that span a campaign's locations (teleports, dialogue, HUD...).
|
// Wires the location's callbacks into the game (teleports, dialogue, HUD...).
|
||||||
void wireCampaignLocations(LocationMap& locations);
|
void wireLocation(Location* loc);
|
||||||
// Runs `then` with the campaign active. If its locations aren't loaded, they are
|
|
||||||
// built first behind the campaign's loading screen (one loading step per
|
|
||||||
// location) and any other loaded campaign is released.
|
|
||||||
void loadCampaignThen(const std::string& campaignId, std::function<void()> then);
|
|
||||||
// Releases every loaded campaign's locations and the textures only they used.
|
|
||||||
void unloadCampaigns();
|
|
||||||
// Makes a loaded campaign the played one: its locations and its quest definitions.
|
|
||||||
void activateCampaign(const std::string& campaignId);
|
|
||||||
const CampaignConfig& activeCampaign() const;
|
const CampaignConfig& activeCampaign() const;
|
||||||
|
|
||||||
void createShadowMapAndProparateToLocations();
|
void createShadowMapAndProparateToLocations();
|
||||||
void destroyShadowMapAndProparateToLocations();
|
void destroyShadowMapAndProparateToLocations();
|
||||||
|
|
||||||
// Reset entry point used by "Start New Game": loads the active campaign's start-state
|
// Reset entry point used by "Start New Game": loads the active campaign's start-state
|
||||||
// JSON into its existing Location objects, else falls back to performFullReset().
|
// JSON (into the loaded location, storing the rest), else falls back to performFullReset().
|
||||||
// Must only ever run from a deferred task (see mainThreadHandler.EnqueueMainThreadTask),
|
// Must only ever run from a deferred task (see mainThreadHandler.EnqueueMainThreadTask),
|
||||||
// never synchronously from a Lua/cutscene call stack.
|
// never synchronously from a Lua/cutscene call stack.
|
||||||
void performResetToInitialState();
|
void performResetToInitialState();
|
||||||
// Rebuilds the active campaign's locations from scratch.
|
// Rebuilds the loaded location from scratch and forgets the other locations' states.
|
||||||
void performFullReset();
|
void performFullReset();
|
||||||
// Applies the campaign's tutorial flag and MenuManager::tutorialEnabledSetting to
|
// Applies the campaign's tutorial flag and MenuManager::tutorialEnabledSetting to
|
||||||
// the freshly reset state (new games only).
|
// the freshly reset state (new games only).
|
||||||
void applyTutorialSetting();
|
void applyTutorialSetting();
|
||||||
void saveInitialState();
|
void saveInitialState();
|
||||||
|
|
||||||
// (Re-)arms one-shot gameplay callbacks (taxi request) that
|
|
||||||
// wireCampaignLocations() sets up but that get consumed (set to nullptr) once used
|
|
||||||
// during play. Must be called after gameState.load() on the fast reset path,
|
|
||||||
// since that path keeps the existing Location objects instead of recreating them.
|
|
||||||
void rearmOneShotCallbacks(LocationMap& locations);
|
|
||||||
|
|
||||||
int64_t getSyncTimeMs();
|
int64_t getSyncTimeMs();
|
||||||
void processTickCount();
|
void processTickCount();
|
||||||
void drawScene();
|
void drawScene();
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ void GameState::save(nlohmann::json& out) const
|
|||||||
out["tutorialPhoneChatScreenOpened"] = tutorialPhoneChatScreenOpened;
|
out["tutorialPhoneChatScreenOpened"] = tutorialPhoneChatScreenOpened;
|
||||||
out["tutorialJournalScreenOpened"] = tutorialJournalScreenOpened;
|
out["tutorialJournalScreenOpened"] = tutorialJournalScreenOpened;
|
||||||
out["tutorialNeedOpenTaxiScreen"] = tutorialNeedOpenTaxiScreen;
|
out["tutorialNeedOpenTaxiScreen"] = tutorialNeedOpenTaxiScreen;
|
||||||
|
out["tutorialTaxiHintDone"] = tutorialTaxiHintDone;
|
||||||
|
|
||||||
// --- Phone / chat ---
|
// --- Phone / chat ---
|
||||||
out["chatUnread"] = { chatUnread[0], chatUnread[1], chatUnread[2] };
|
out["chatUnread"] = { chatUnread[0], chatUnread[1], chatUnread[2] };
|
||||||
@@ -87,7 +88,9 @@ void GameState::save(nlohmann::json& out) const
|
|||||||
out["questJournal"] = std::move(questJson);
|
out["questJournal"] = std::move(questJson);
|
||||||
|
|
||||||
// --- Locations ---
|
// --- Locations ---
|
||||||
|
// Unloaded locations keep their last stored state; loaded ones are saved live.
|
||||||
nlohmann::json locsJson = nlohmann::json::object();
|
nlohmann::json locsJson = nlohmann::json::object();
|
||||||
|
for (const auto& [name, stored] : storedLocationStates) locsJson[name] = stored;
|
||||||
for (const auto& [name, loc] : locations) {
|
for (const auto& [name, loc] : locations) {
|
||||||
if (loc) {
|
if (loc) {
|
||||||
nlohmann::json locJson;
|
nlohmann::json locJson;
|
||||||
@@ -147,6 +150,7 @@ void GameState::load(const nlohmann::json& in)
|
|||||||
tutorialPhoneChatScreenOpened = in.value("tutorialPhoneChatScreenOpened", tutorialPhoneChatScreenOpened);
|
tutorialPhoneChatScreenOpened = in.value("tutorialPhoneChatScreenOpened", tutorialPhoneChatScreenOpened);
|
||||||
tutorialJournalScreenOpened = in.value("tutorialJournalScreenOpened", tutorialJournalScreenOpened);
|
tutorialJournalScreenOpened = in.value("tutorialJournalScreenOpened", tutorialJournalScreenOpened);
|
||||||
tutorialNeedOpenTaxiScreen = in.value("tutorialNeedOpenTaxiScreen", tutorialNeedOpenTaxiScreen);
|
tutorialNeedOpenTaxiScreen = in.value("tutorialNeedOpenTaxiScreen", tutorialNeedOpenTaxiScreen);
|
||||||
|
tutorialTaxiHintDone = in.value("tutorialTaxiHintDone", false);
|
||||||
|
|
||||||
//logger() << "GameState::load step 6" << std::endl;
|
//logger() << "GameState::load step 6" << std::endl;
|
||||||
|
|
||||||
@@ -190,9 +194,13 @@ void GameState::load(const nlohmann::json& in)
|
|||||||
logger() << "GameState::load step 10" << std::endl;
|
logger() << "GameState::load step 10" << std::endl;
|
||||||
|
|
||||||
// --- Locations ---
|
// --- Locations ---
|
||||||
|
// Every location's state is stored (applied when the location loads); the ones
|
||||||
|
// already loaded get it right away.
|
||||||
|
storedLocationStates.clear();
|
||||||
if (in.contains("locations") && in["locations"].is_object()) {
|
if (in.contains("locations") && in["locations"].is_object()) {
|
||||||
for (const auto& [name, locJson] : in["locations"].items()) {
|
for (const auto& [name, locJson] : in["locations"].items()) {
|
||||||
//logger() << "GameState::load step 10.1: " << name << std::endl;
|
//logger() << "GameState::load step 10.1: " << name << std::endl;
|
||||||
|
storedLocationStates[name] = locJson;
|
||||||
|
|
||||||
auto it = locations.find(name);
|
auto it = locations.find(name);
|
||||||
if (it != locations.end() && it->second) {
|
if (it != locations.end() && it->second) {
|
||||||
|
|||||||
+6
-1
@@ -36,9 +36,13 @@ using LocationMap = std::unordered_map<std::string, std::shared_ptr<Location>>;
|
|||||||
|
|
||||||
struct GameState : public ISaveable {
|
struct GameState : public ISaveable {
|
||||||
// --- World ---
|
// --- World ---
|
||||||
// Campaign being played (see CampaignRegistry); `locations` holds that campaign's locations.
|
// Campaign being played (see CampaignRegistry).
|
||||||
std::string campaignId = "main";
|
std::string campaignId = "main";
|
||||||
|
// The campaign's loaded location(s); only the one the player is in is kept loaded.
|
||||||
LocationMap locations;
|
LocationMap locations;
|
||||||
|
// Saved state (Location::save) of the campaign's locations that are not loaded,
|
||||||
|
// applied when one is loaded again. save()/load() cover both maps.
|
||||||
|
std::unordered_map<std::string, nlohmann::json> storedLocationStates;
|
||||||
std::string currentLocationName;
|
std::string currentLocationName;
|
||||||
bool isDarklands = false;
|
bool isDarklands = false;
|
||||||
bool isNight = false;
|
bool isNight = false;
|
||||||
@@ -63,6 +67,7 @@ struct GameState : public ISaveable {
|
|||||||
bool tutorialPhoneChatScreenOpened = false; // "open the messenger" hint is done
|
bool tutorialPhoneChatScreenOpened = false; // "open the messenger" hint is done
|
||||||
bool tutorialJournalScreenOpened = false; // "open the journal" hint is done
|
bool tutorialJournalScreenOpened = false; // "open the journal" hint is done
|
||||||
bool tutorialNeedOpenTaxiScreen = false; // "call a taxi" hint is showing
|
bool tutorialNeedOpenTaxiScreen = false; // "call a taxi" hint is showing
|
||||||
|
bool tutorialTaxiHintDone = false; // a taxi was ordered; never show the hint again
|
||||||
|
|
||||||
bool isTutorialActive() const { return tutorialStep != TutorialStep::Completed; }
|
bool isTutorialActive() const { return tutorialStep != TutorialStep::Completed; }
|
||||||
// Moves to the next step if `step` is the current one; returns whether it did.
|
// Moves to the next step if `step` is the current one; returns whether it did.
|
||||||
|
|||||||
+67
-43
@@ -56,6 +56,18 @@ namespace FRG
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Location::~Location()
|
||||||
|
{
|
||||||
|
// The player is shared with the next location and outlives this one: drop
|
||||||
|
// everything in him that points into this location: its path planner, the HP
|
||||||
|
// callback Game wires with this location's pointer, and a pending walk-to callback.
|
||||||
|
if (player) {
|
||||||
|
player->setPathPlanner(nullptr);
|
||||||
|
player->state.onHpChanged = nullptr;
|
||||||
|
player->state.onArrivedCallback = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Pointer-to-index helpers ----
|
// ---- Pointer-to-index helpers ----
|
||||||
|
|
||||||
InteractiveObject* Location::getTargetInteractiveObject()
|
InteractiveObject* Location::getTargetInteractiveObject()
|
||||||
@@ -101,53 +113,14 @@ namespace FRG
|
|||||||
interactiveObjects = GameObjectLoader::loadAndCreateInteractiveObjects(params.interactiveObjectsJsonPath, renderer, CONST_ZIP_FILE);
|
interactiveObjects = GameObjectLoader::loadAndCreateInteractiveObjects(params.interactiveObjectsJsonPath, renderer, CONST_ZIP_FILE);
|
||||||
SDL_PumpEvents();
|
SDL_PumpEvents();
|
||||||
|
|
||||||
//auto playerTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/gg/IMG_20260413_182354_992.png", CONST_ZIP_FILE));
|
|
||||||
//auto playerTexture = renderer.textureManager.LoadFromPng("resources/w/gg/UniV_Grid_2K_Base_color.png", CONST_ZIP_FILE);
|
|
||||||
auto playerTexture = renderer.textureManager.LoadFromPng("resources/w/gg/UniV_Grid_2K_Base_color001.png", CONST_ZIP_FILE);
|
|
||||||
|
|
||||||
auto sparkTexture = renderer.textureManager.LoadFromPng("resources/w/spark.png", CONST_ZIP_FILE);
|
auto sparkTexture = renderer.textureManager.LoadFromPng("resources/w/spark.png", CONST_ZIP_FILE);
|
||||||
|
|
||||||
player = std::make_unique<Character>();
|
player = params.player ? params.player : createPlayer(renderer, audioPlayer);
|
||||||
|
// A shared player arrives from another location; until setupNavigation() below
|
||||||
player->loadBinaryAnimation(AnimationState::STAND, "resources/w/gg/new2/gg_stand_idle001_small.anim", CONST_ZIP_FILE);
|
// gives him this location's planner, he must not plan with anything else.
|
||||||
player->loadBinaryAnimation(AnimationState::WALK, "resources/w/gg/new2/gg_walk001_small.anim", CONST_ZIP_FILE);
|
player->setPathPlanner(nullptr);
|
||||||
player->loadBinaryAnimation(AnimationState::STAND_TO_ACTION, "resources/w/gg/new2/gg_stand_to_action001_small.anim", CONST_ZIP_FILE);
|
|
||||||
|
|
||||||
player->loadBinaryAnimation(AnimationState::ACTION_ATTACK, "resources/w/gg/new2/gg_action_chop001_small.anim", CONST_ZIP_FILE);
|
|
||||||
player->loadBinaryAnimation(AnimationState::ACTION_ATTACK_2, "resources/w/gg/new2/gg_action_stab001_small.anim", CONST_ZIP_FILE);
|
|
||||||
player->loadBinaryAnimation(AnimationState::ACTION_IDLE, "resources/w/gg/new2/gg_action_idle002_small.anim", CONST_ZIP_FILE);
|
|
||||||
player->loadBinaryAnimation(AnimationState::ACTION_TO_STAND, "resources/w/gg/new2/gg_action_to_stand001_small.anim", CONST_ZIP_FILE);
|
|
||||||
player->loadBinaryAnimation(AnimationState::ACTION_TO_DEATH, "resources/w/gg/new2/gg_die001_small.anim", CONST_ZIP_FILE);
|
|
||||||
player->loadBinaryAnimation(AnimationState::DEATH_IDLE, "resources/w/gg/new2/gg_die_idle001_small.anim", CONST_ZIP_FILE);
|
|
||||||
|
|
||||||
|
|
||||||
player->weaponTexture = renderer.textureManager.LoadFromPng("resources/w/gg/Props.png", CONST_ZIP_FILE);
|
|
||||||
player->weaponMesh.data = LoadModelFromBinFile("resources/w/gg/knife002.txt.bin", CONST_ZIP_FILE);
|
|
||||||
player->weaponMesh.data.Move(Vector3f(0, -0.3, 0));
|
|
||||||
player->weaponMesh.data.Scale(0.2f);
|
|
||||||
player->weaponMesh.RefreshVBO();
|
|
||||||
player->weaponAttachBoneName = "RightHand";
|
|
||||||
player->weaponInitialRotation = Eigen::AngleAxisf(-M_PI * 0.5, Eigen::Vector3f::UnitZ()).toRotationMatrix();
|
|
||||||
player->weaponInitialPosition = Eigen::Vector3f(0, 0.09, 0.016);
|
|
||||||
|
|
||||||
player->setTexture(playerTexture);
|
|
||||||
player->state.walkSpeed = 3.0f;
|
|
||||||
player->state.rotationSpeed = 8.0f;
|
|
||||||
|
|
||||||
player->state.modelScale = 1.f;
|
|
||||||
player->state.modelCorrectionRotation = Eigen::Quaternionf(
|
|
||||||
Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())
|
|
||||||
*
|
|
||||||
Eigen::AngleAxisf(-M_PI*0.5, Eigen::Vector3f::UnitX())
|
|
||||||
|
|
||||||
);
|
|
||||||
|
|
||||||
player->state.canAttack = true;
|
|
||||||
player->state.isPlayer = true;
|
|
||||||
player->state.position = params.playerPosition;
|
player->state.position = params.playerPosition;
|
||||||
player->setTarget(params.playerPosition);
|
player->setTarget(params.playerPosition);
|
||||||
player->setupHitSparks(sparkTexture);
|
|
||||||
player->audioPlayer = audioPlayer;
|
|
||||||
logger() << "Load resurces step 9" << std::endl;
|
logger() << "Load resurces step 9" << std::endl;
|
||||||
SDL_PumpEvents();
|
SDL_PumpEvents();
|
||||||
|
|
||||||
@@ -229,6 +202,57 @@ namespace FRG
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<Character> Location::createPlayer(Renderer& renderer, AudioPlayerAsync* audioPlayer)
|
||||||
|
{
|
||||||
|
//auto playerTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/gg/IMG_20260413_182354_992.png", CONST_ZIP_FILE));
|
||||||
|
//auto playerTexture = renderer.textureManager.LoadFromPng("resources/w/gg/UniV_Grid_2K_Base_color.png", CONST_ZIP_FILE);
|
||||||
|
auto playerTexture = renderer.textureManager.LoadFromPng("resources/w/gg/UniV_Grid_2K_Base_color001.png", CONST_ZIP_FILE);
|
||||||
|
|
||||||
|
auto sparkTexture = renderer.textureManager.LoadFromPng("resources/w/spark.png", CONST_ZIP_FILE);
|
||||||
|
|
||||||
|
auto player = std::make_shared<Character>();
|
||||||
|
|
||||||
|
player->loadBinaryAnimation(AnimationState::STAND, "resources/w/gg/new2/gg_stand_idle001_small.anim", CONST_ZIP_FILE);
|
||||||
|
player->loadBinaryAnimation(AnimationState::WALK, "resources/w/gg/new2/gg_walk001_small.anim", CONST_ZIP_FILE);
|
||||||
|
player->loadBinaryAnimation(AnimationState::STAND_TO_ACTION, "resources/w/gg/new2/gg_stand_to_action001_small.anim", CONST_ZIP_FILE);
|
||||||
|
|
||||||
|
player->loadBinaryAnimation(AnimationState::ACTION_ATTACK, "resources/w/gg/new2/gg_action_chop001_small.anim", CONST_ZIP_FILE);
|
||||||
|
player->loadBinaryAnimation(AnimationState::ACTION_ATTACK_2, "resources/w/gg/new2/gg_action_stab001_small.anim", CONST_ZIP_FILE);
|
||||||
|
player->loadBinaryAnimation(AnimationState::ACTION_IDLE, "resources/w/gg/new2/gg_action_idle002_small.anim", CONST_ZIP_FILE);
|
||||||
|
player->loadBinaryAnimation(AnimationState::ACTION_TO_STAND, "resources/w/gg/new2/gg_action_to_stand001_small.anim", CONST_ZIP_FILE);
|
||||||
|
player->loadBinaryAnimation(AnimationState::ACTION_TO_DEATH, "resources/w/gg/new2/gg_die001_small.anim", CONST_ZIP_FILE);
|
||||||
|
player->loadBinaryAnimation(AnimationState::DEATH_IDLE, "resources/w/gg/new2/gg_die_idle001_small.anim", CONST_ZIP_FILE);
|
||||||
|
|
||||||
|
|
||||||
|
player->weaponTexture = renderer.textureManager.LoadFromPng("resources/w/gg/Props.png", CONST_ZIP_FILE);
|
||||||
|
player->weaponMesh.data = LoadModelFromBinFile("resources/w/gg/knife002.txt.bin", CONST_ZIP_FILE);
|
||||||
|
player->weaponMesh.data.Move(Vector3f(0, -0.3, 0));
|
||||||
|
player->weaponMesh.data.Scale(0.2f);
|
||||||
|
player->weaponMesh.RefreshVBO();
|
||||||
|
player->weaponAttachBoneName = "RightHand";
|
||||||
|
player->weaponInitialRotation = Eigen::AngleAxisf(-M_PI * 0.5, Eigen::Vector3f::UnitZ()).toRotationMatrix();
|
||||||
|
player->weaponInitialPosition = Eigen::Vector3f(0, 0.09, 0.016);
|
||||||
|
|
||||||
|
player->setTexture(playerTexture);
|
||||||
|
player->state.walkSpeed = 3.0f;
|
||||||
|
player->state.rotationSpeed = 8.0f;
|
||||||
|
|
||||||
|
player->state.modelScale = 1.f;
|
||||||
|
player->state.modelCorrectionRotation = Eigen::Quaternionf(
|
||||||
|
Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())
|
||||||
|
*
|
||||||
|
Eigen::AngleAxisf(-M_PI*0.5, Eigen::Vector3f::UnitX())
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
player->state.canAttack = true;
|
||||||
|
player->state.isPlayer = true;
|
||||||
|
player->setupHitSparks(sparkTexture);
|
||||||
|
player->audioPlayer = audioPlayer;
|
||||||
|
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
void Location::loadTeleportZones(const std::string& jsonPath, const char* zipFile)
|
void Location::loadTeleportZones(const std::string& jsonPath, const char* zipFile)
|
||||||
{
|
{
|
||||||
if (jsonPath.empty()) return;
|
if (jsonPath.empty()) return;
|
||||||
|
|||||||
+10
-1
@@ -60,12 +60,17 @@ namespace FRG
|
|||||||
std::string triggerZonesJsonPath;
|
std::string triggerZonesJsonPath;
|
||||||
std::string lightsJsonPath;
|
std::string lightsJsonPath;
|
||||||
Eigen::Vector3f playerPosition = Eigen::Vector3f::Zero();
|
Eigen::Vector3f playerPosition = Eigen::Vector3f::Zero();
|
||||||
|
// Player shared by all locations of a campaign (see Location::createPlayer);
|
||||||
|
// setup() creates a private one when this is null.
|
||||||
|
std::shared_ptr<Character> player;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Location : public ISaveable
|
class Location : public ISaveable
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Location(Renderer& iRenderer, Inventory& iInventory);
|
Location(Renderer& iRenderer, Inventory& iInventory);
|
||||||
|
// Detaches the (shared, longer-lived) player from this location.
|
||||||
|
~Location();
|
||||||
|
|
||||||
// ---- All serialisable runtime state ----
|
// ---- All serialisable runtime state ----
|
||||||
LocationState state;
|
LocationState state;
|
||||||
@@ -74,9 +79,13 @@ namespace FRG
|
|||||||
|
|
||||||
std::vector<InteractiveObject> interactiveObjects;
|
std::vector<InteractiveObject> interactiveObjects;
|
||||||
|
|
||||||
std::unique_ptr<Character> player;
|
// Shared: the same player moves from location to location and outlives them.
|
||||||
|
std::shared_ptr<Character> player;
|
||||||
std::vector<std::unique_ptr<Character>> npcs;
|
std::vector<std::unique_ptr<Character>> npcs;
|
||||||
|
|
||||||
|
// Builds the player character (models, animations, weapon, textures).
|
||||||
|
static std::shared_ptr<Character> createPlayer(Renderer& renderer, AudioPlayerAsync* audioPlayer);
|
||||||
|
|
||||||
std::vector<PathFinder> navigationMaps;
|
std::vector<PathFinder> navigationMaps;
|
||||||
PathFinder* navigation = nullptr;
|
PathFinder* navigation = nullptr;
|
||||||
|
|
||||||
|
|||||||
+9
-3
@@ -326,9 +326,14 @@ namespace FRG {
|
|||||||
loadSavedGameScreenRoot = loadLocalizedUi("resources/w/ui/screen_load.json", zipFile);
|
loadSavedGameScreenRoot = loadLocalizedUi("resources/w/ui/screen_load.json", zipFile);
|
||||||
campaignScreenRoot = loadLocalizedUi("resources/w/ui/screen_campaign.json", zipFile);
|
campaignScreenRoot = loadLocalizedUi("resources/w/ui/screen_campaign.json", zipFile);
|
||||||
loadingScreenRoots_.clear();
|
loadingScreenRoots_.clear();
|
||||||
|
auto preloadLoadingScreen = [this, &zipFile](const std::string& path) {
|
||||||
|
if (!path.empty() && !loadingScreenRoots_.count(path))
|
||||||
|
loadingScreenRoots_[path] = loadLocalizedUi(path, zipFile);
|
||||||
|
};
|
||||||
for (const CampaignConfig& campaign : CampaignRegistry::instance().all()) {
|
for (const CampaignConfig& campaign : CampaignRegistry::instance().all()) {
|
||||||
if (!loadingScreenRoots_.count(campaign.loadingScreenPath))
|
preloadLoadingScreen(campaign.loadingScreenPath);
|
||||||
loadingScreenRoots_[campaign.loadingScreenPath] = loadLocalizedUi(campaign.loadingScreenPath, zipFile);
|
for (const LocationConfig& location : campaign.locations)
|
||||||
|
preloadLoadingScreen(location.loadingScreenPath);
|
||||||
}
|
}
|
||||||
saveGameScreenRoot = loadLocalizedUi("resources/w/ui/screen_save.json", zipFile);
|
saveGameScreenRoot = loadLocalizedUi("resources/w/ui/screen_save.json", zipFile);
|
||||||
}
|
}
|
||||||
@@ -1535,6 +1540,7 @@ namespace FRG {
|
|||||||
});
|
});
|
||||||
uiManager.setButtonCallback("mapGo", [this](const std::string&) {
|
uiManager.setButtonCallback("mapGo", [this](const std::string&) {
|
||||||
gameState_.tutorialNeedOpenTaxiScreen = false;
|
gameState_.tutorialNeedOpenTaxiScreen = false;
|
||||||
|
gameState_.tutorialTaxiHintDone = true;
|
||||||
|
|
||||||
if (gameState_.taxiIsCalled == false)
|
if (gameState_.taxiIsCalled == false)
|
||||||
{
|
{
|
||||||
@@ -1693,7 +1699,7 @@ namespace FRG {
|
|||||||
void MenuManager::tutorialShowTaxiHint()
|
void MenuManager::tutorialShowTaxiHint()
|
||||||
{
|
{
|
||||||
logger() << "tutorialShowTaxiHint" << std::endl;
|
logger() << "tutorialShowTaxiHint" << std::endl;
|
||||||
if (!gameState_.isTutorialActive()) return;
|
if (!gameState_.isTutorialActive() || gameState_.tutorialTaxiHintDone) return;
|
||||||
gameState_.tutorialNeedOpenTaxiScreen = true; // cleared when the taxi is ordered
|
gameState_.tutorialNeedOpenTaxiScreen = true; // cleared when the taxi is ordered
|
||||||
refreshHudVisibility();
|
refreshHudVisibility();
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -71,8 +71,8 @@ namespace FRG {
|
|||||||
// Called by Game once a save has been applied.
|
// Called by Game once a save has been applied.
|
||||||
void enterGameplay(bool isFromLoad = false);
|
void enterGameplay(bool isFromLoad = false);
|
||||||
|
|
||||||
// Campaign loading screen (layout from CampaignConfig::loadingScreenPath, preloaded
|
// Loading screen (a campaign's or a location's layout, all preloaded with the UI).
|
||||||
// with the UI). Game shows it before building a campaign and hides it when done.
|
// Game shows it before loading a campaign or location and hides it when done.
|
||||||
void showLoadingScreen(const std::string& layoutPath);
|
void showLoadingScreen(const std::string& layoutPath);
|
||||||
void hideLoadingScreen();
|
void hideLoadingScreen();
|
||||||
bool isLoadingScreenShown() const { return loadingScreenShown_; }
|
bool isLoadingScreenShown() const { return loadingScreenShown_; }
|
||||||
|
|||||||
@@ -558,12 +558,11 @@ namespace FRG
|
|||||||
for (const auto& [key, tex] : textureMap) residentKeys.insert(key);
|
for (const auto& [key, tex] : textureMap) residentKeys.insert(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextureManager::UnloadNonResident(const std::string& residentPrefix)
|
void TextureManager::UnloadUnused()
|
||||||
{
|
{
|
||||||
size_t unloaded = 0;
|
size_t unloaded = 0;
|
||||||
for (auto it = textureMap.begin(); it != textureMap.end(); ) {
|
for (auto it = textureMap.begin(); it != textureMap.end(); ) {
|
||||||
const bool keep = residentKeys.count(it->first)
|
const bool keep = residentKeys.count(it->first) || it->second.use_count() > 1;
|
||||||
|| (!residentPrefix.empty() && it->first.compare(0, residentPrefix.size(), residentPrefix) == 0);
|
|
||||||
if (keep) {
|
if (keep) {
|
||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
@@ -572,7 +571,7 @@ namespace FRG
|
|||||||
++unloaded;
|
++unloaded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger() << "Unloaded " << unloaded << " non-resident textures" << std::endl;
|
logger() << "Unloaded " << unloaded << " unused textures" << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -82,10 +82,9 @@ namespace FRG
|
|||||||
|
|
||||||
// Marks every texture cached so far as resident, so UnloadNonResident() keeps it.
|
// Marks every texture cached so far as resident, so UnloadNonResident() keeps it.
|
||||||
void MarkAllResident();
|
void MarkAllResident();
|
||||||
// Drops cached textures that aren't resident and whose key doesn't start with
|
// Frees cached textures nothing else uses any more (only the cache holds them),
|
||||||
// residentPrefix (e.g. UI images, which are reloaded under the same prefix). A
|
// except resident ones. Call after releasing the objects that used them.
|
||||||
// texture is freed once nothing else holds it.
|
void UnloadUnused();
|
||||||
void UnloadNonResident(const std::string& residentPrefix = "");
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unordered_map<std::string, std::shared_ptr<Texture>> textureMap;
|
std::unordered_map<std::string, std::shared_ptr<Texture>> textureMap;
|
||||||
|
|||||||
Reference in new issue
Block a user