working on refactoring

This commit is contained in:
Vladislav Khorev committed 2026-10-24 22:37:20 +06:00
1 parent 9b71ee447a
commit d5ef126264
12 files changed
+309 -219

No files matched your search

+17
View File
@@ -44,6 +44,7 @@ namespace FRG {
out.lightsJsonPath = j.value("lights", "");
if (j.contains("playerPosition")) out.playerPosition = readVec3(j["playerPosition"]);
out.lockInteractiveObjectsDuringTutorial = j.value("lockInteractiveObjectsDuringTutorial", false);
out.loadingScreenPath = j.value("loadingScreen", "");
if (j.contains("taxi") && j["taxi"].is_object()) {
const json& t = j["taxi"];
@@ -71,6 +72,18 @@ namespace FRG {
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() {
static CampaignRegistry reg;
return reg;
@@ -97,6 +110,10 @@ namespace FRG {
c.questsPath = entry.value("quests", "");
c.tutorial = entry.value("tutorial", false);
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));
}
+9
View File
@@ -36,6 +36,9 @@ namespace FRG {
// Interactive objects stay locked until the tutorial reaches "pick up an item".
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.
static bool fromFile(const std::string& path, const std::string& zipFile, LocationConfig& out);
};
@@ -45,6 +48,8 @@ namespace FRG {
std::string id;
std::map<std::string, std::string> titles; // language code -> title
std::vector<std::string> locationConfigPaths;
// Parsed from locationConfigPaths when the registry loads.
std::vector<LocationConfig> locations;
std::string startLocation;
std::string startMusic;
std::string startStatePath;
@@ -56,6 +61,10 @@ namespace FRG {
static constexpr const char* kDefaultLoadingScreen = "resources/w/ui/screen_loading.json";
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.
+149 -139
View File
@@ -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);
+26 -22
View File
@@ -128,46 +128,50 @@ namespace FRG {
void loadGame(int slot);
void playMusicForLoadedGame();
SaveSlotInfo readSlotInfo(int slot) const;
// Locations of the loaded campaign (campaign id -> locations); at most one entry,
// empty until the player starts or loads a game. gameState.locations is a copy
// of the active campaign's entry.
std::unordered_map<std::string, LocationMap> campaignLocations;
// Loading model: one campaign at a time, and within it only the location the player
// is in (gameState.locations). Leaving a location stores its state in
// gameState.storedLocationStates and releases it; the player is created once per
// 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.
std::shared_ptr<Location> createLocation(const LocationConfig& config);
// Wires the callbacks that span a campaign's locations (teleports, dialogue, HUD...).
void wireCampaignLocations(LocationMap& locations);
// 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);
// Wires the location's callbacks into the game (teleports, dialogue, HUD...).
void wireLocation(Location* loc);
const CampaignConfig& activeCampaign() const;
void createShadowMapAndProparateToLocations();
void destroyShadowMapAndProparateToLocations();
// 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),
// never synchronously from a Lua/cutscene call stack.
void performResetToInitialState();
// Rebuilds the active campaign's locations from scratch.
// Rebuilds the loaded location from scratch and forgets the other locations' states.
void performFullReset();
// Applies the campaign's tutorial flag and MenuManager::tutorialEnabledSetting to
// the freshly reset state (new games only).
void applyTutorialSetting();
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();
void processTickCount();
void drawScene();
+8
View File
@@ -61,6 +61,7 @@ void GameState::save(nlohmann::json& out) const
out["tutorialPhoneChatScreenOpened"] = tutorialPhoneChatScreenOpened;
out["tutorialJournalScreenOpened"] = tutorialJournalScreenOpened;
out["tutorialNeedOpenTaxiScreen"] = tutorialNeedOpenTaxiScreen;
out["tutorialTaxiHintDone"] = tutorialTaxiHintDone;
// --- Phone / chat ---
out["chatUnread"] = { chatUnread[0], chatUnread[1], chatUnread[2] };
@@ -87,7 +88,9 @@ void GameState::save(nlohmann::json& out) const
out["questJournal"] = std::move(questJson);
// --- Locations ---
// Unloaded locations keep their last stored state; loaded ones are saved live.
nlohmann::json locsJson = nlohmann::json::object();
for (const auto& [name, stored] : storedLocationStates) locsJson[name] = stored;
for (const auto& [name, loc] : locations) {
if (loc) {
nlohmann::json locJson;
@@ -147,6 +150,7 @@ void GameState::load(const nlohmann::json& in)
tutorialPhoneChatScreenOpened = in.value("tutorialPhoneChatScreenOpened", tutorialPhoneChatScreenOpened);
tutorialJournalScreenOpened = in.value("tutorialJournalScreenOpened", tutorialJournalScreenOpened);
tutorialNeedOpenTaxiScreen = in.value("tutorialNeedOpenTaxiScreen", tutorialNeedOpenTaxiScreen);
tutorialTaxiHintDone = in.value("tutorialTaxiHintDone", false);
//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;
// --- 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()) {
for (const auto& [name, locJson] : in["locations"].items()) {
//logger() << "GameState::load step 10.1: " << name << std::endl;
storedLocationStates[name] = locJson;
auto it = locations.find(name);
if (it != locations.end() && it->second) {
+6 -1
View File
@@ -36,9 +36,13 @@ using LocationMap = std::unordered_map<std::string, std::shared_ptr<Location>>;
struct GameState : public ISaveable {
// --- World ---
// Campaign being played (see CampaignRegistry); `locations` holds that campaign's locations.
// Campaign being played (see CampaignRegistry).
std::string campaignId = "main";
// The campaign's loaded location(s); only the one the player is in is kept loaded.
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;
bool isDarklands = false;
bool isNight = false;
@@ -63,6 +67,7 @@ struct GameState : public ISaveable {
bool tutorialPhoneChatScreenOpened = false; // "open the messenger" hint is done
bool tutorialJournalScreenOpened = false; // "open the journal" hint is done
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; }
// Moves to the next step if `step` is the current one; returns whether it did.
+67 -43
View File
@@ -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 ----
InteractiveObject* Location::getTargetInteractiveObject()
@@ -101,53 +113,14 @@ namespace FRG
interactiveObjects = GameObjectLoader::loadAndCreateInteractiveObjects(params.interactiveObjectsJsonPath, renderer, CONST_ZIP_FILE);
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);
player = std::make_unique<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 = params.player ? params.player : createPlayer(renderer, audioPlayer);
// A shared player arrives from another location; until setupNavigation() below
// gives him this location's planner, he must not plan with anything else.
player->setPathPlanner(nullptr);
player->state.position = params.playerPosition;
player->setTarget(params.playerPosition);
player->setupHitSparks(sparkTexture);
player->audioPlayer = audioPlayer;
logger() << "Load resurces step 9" << std::endl;
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)
{
if (jsonPath.empty()) return;
+10 -1
View File
@@ -60,12 +60,17 @@ namespace FRG
std::string triggerZonesJsonPath;
std::string lightsJsonPath;
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
{
public:
Location(Renderer& iRenderer, Inventory& iInventory);
// Detaches the (shared, longer-lived) player from this location.
~Location();
// ---- All serialisable runtime state ----
LocationState state;
@@ -74,9 +79,13 @@ namespace FRG
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;
// Builds the player character (models, animations, weapon, textures).
static std::shared_ptr<Character> createPlayer(Renderer& renderer, AudioPlayerAsync* audioPlayer);
std::vector<PathFinder> navigationMaps;
PathFinder* navigation = nullptr;
+9 -3
View File
@@ -326,9 +326,14 @@ namespace FRG {
loadSavedGameScreenRoot = loadLocalizedUi("resources/w/ui/screen_load.json", zipFile);
campaignScreenRoot = loadLocalizedUi("resources/w/ui/screen_campaign.json", zipFile);
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()) {
if (!loadingScreenRoots_.count(campaign.loadingScreenPath))
loadingScreenRoots_[campaign.loadingScreenPath] = loadLocalizedUi(campaign.loadingScreenPath, zipFile);
preloadLoadingScreen(campaign.loadingScreenPath);
for (const LocationConfig& location : campaign.locations)
preloadLoadingScreen(location.loadingScreenPath);
}
saveGameScreenRoot = loadLocalizedUi("resources/w/ui/screen_save.json", zipFile);
}
@@ -1535,6 +1540,7 @@ namespace FRG {
});
uiManager.setButtonCallback("mapGo", [this](const std::string&) {
gameState_.tutorialNeedOpenTaxiScreen = false;
gameState_.tutorialTaxiHintDone = true;
if (gameState_.taxiIsCalled == false)
{
@@ -1693,7 +1699,7 @@ namespace FRG {
void MenuManager::tutorialShowTaxiHint()
{
logger() << "tutorialShowTaxiHint" << std::endl;
if (!gameState_.isTutorialActive()) return;
if (!gameState_.isTutorialActive() || gameState_.tutorialTaxiHintDone) return;
gameState_.tutorialNeedOpenTaxiScreen = true; // cleared when the taxi is ordered
refreshHudVisibility();
}
+2 -2
View File
@@ -71,8 +71,8 @@ namespace FRG {
// Called by Game once a save has been applied.
void enterGameplay(bool isFromLoad = false);
// Campaign loading screen (layout from CampaignConfig::loadingScreenPath, preloaded
// with the UI). Game shows it before building a campaign and hides it when done.
// Loading screen (a campaign's or a location's layout, all preloaded with the UI).
// Game shows it before loading a campaign or location and hides it when done.
void showLoadingScreen(const std::string& layoutPath);
void hideLoadingScreen();
bool isLoadingScreenShown() const { return loadingScreenShown_; }
+3 -4
View File
@@ -558,12 +558,11 @@ namespace FRG
for (const auto& [key, tex] : textureMap) residentKeys.insert(key);
}
void TextureManager::UnloadNonResident(const std::string& residentPrefix)
void TextureManager::UnloadUnused()
{
size_t unloaded = 0;
for (auto it = textureMap.begin(); it != textureMap.end(); ) {
const bool keep = residentKeys.count(it->first)
|| (!residentPrefix.empty() && it->first.compare(0, residentPrefix.size(), residentPrefix) == 0);
const bool keep = residentKeys.count(it->first) || it->second.use_count() > 1;
if (keep) {
++it;
}
@@ -572,7 +571,7 @@ namespace FRG
++unloaded;
}
}
logger() << "Unloaded " << unloaded << " non-resident textures" << std::endl;
logger() << "Unloaded " << unloaded << " unused textures" << std::endl;
}
}
+3 -4
View File
@@ -82,10 +82,9 @@ namespace FRG
// Marks every texture cached so far as resident, so UnloadNonResident() keeps it.
void MarkAllResident();
// Drops cached textures that aren't resident and whose key doesn't start with
// residentPrefix (e.g. UI images, which are reloaded under the same prefix). A
// texture is freed once nothing else holds it.
void UnloadNonResident(const std::string& residentPrefix = "");
// Frees cached textures nothing else uses any more (only the cache holds them),
// except resident ones. Call after releasing the objects that used them.
void UnloadUnused();
private:
std::unordered_map<std::string, std::shared_ptr<Texture>> textureMap;