working on campaign
This commit is contained in:
1 parent
9fa120d4ad
commit
d38a75ea72
24 files changed
+929
-410
No files matched your search
@@ -112,6 +112,7 @@ add_library(main SHARED
|
||||
../../../../src/items/Item.cpp
|
||||
../../../../src/items/ItemRegistry.h
|
||||
../../../../src/items/ItemRegistry.cpp
|
||||
../../../../src/Campaign.cpp
|
||||
../../../../src/items/InteractiveObject.h
|
||||
../../../../src/items/InteractiveObject.cpp
|
||||
../../../../src/items/InteractiveObjectState.h
|
||||
|
||||
@@ -137,6 +137,7 @@ add_executable(ShadowOverBishkekDemo
|
||||
../src/items/Item.cpp
|
||||
../src/items/ItemRegistry.h
|
||||
../src/items/ItemRegistry.cpp
|
||||
../src/Campaign.cpp
|
||||
../src/items/InteractiveObject.h
|
||||
../src/items/InteractiveObject.cpp
|
||||
../src/items/InteractiveObjectState.h
|
||||
|
||||
@@ -131,6 +131,7 @@ set(SOURCES
|
||||
../src/items/Item.cpp
|
||||
../src/items/ItemRegistry.h
|
||||
../src/items/ItemRegistry.cpp
|
||||
../src/Campaign.cpp
|
||||
../src/items/InteractiveObject.h
|
||||
../src/items/InteractiveObject.cpp
|
||||
../src/items/InteractiveObjectState.h
|
||||
|
||||
@@ -79,6 +79,7 @@ add_executable(ShadowOverBishkekDemo WIN32
|
||||
../src/items/Item.cpp
|
||||
../src/items/ItemRegistry.h
|
||||
../src/items/ItemRegistry.cpp
|
||||
../src/Campaign.cpp
|
||||
../src/items/InteractiveObject.h
|
||||
../src/items/InteractiveObject.cpp
|
||||
../src/items/InteractiveObjectState.h
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"campaigns": [
|
||||
{
|
||||
"id": "main",
|
||||
"title": { "ru": "Основная игра", "en": "Main Game" },
|
||||
"locations": [
|
||||
"resources/config/locations/location_dorm.json",
|
||||
"resources/config/locations/uni_exterior.json",
|
||||
"resources/config/locations/uni_interior.json"
|
||||
],
|
||||
"startLocation": "location_dorm",
|
||||
"startMusic": "music/obshaga.ogg",
|
||||
"startState": "resources/config/start_state.json",
|
||||
"quests": "resources/config/quests.json",
|
||||
"tutorial": true
|
||||
},
|
||||
{
|
||||
"id": "battle_zone",
|
||||
"title": { "ru": "Зона боя", "en": "Battle Zone" },
|
||||
"locations": [
|
||||
"resources/config/locations/battle_zone_uni_exterior.json"
|
||||
],
|
||||
"startLocation": "uni_exterior",
|
||||
"startMusic": "music/bishkek univer day.ogg",
|
||||
"startState": "resources/config/start_state_battle_zone.json",
|
||||
"quests": "resources/config/quests_battle_zone.json",
|
||||
"tutorial": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": "uni_exterior",
|
||||
"gameObjects": "resources/config/gameobjects_uni_exterior_x.json",
|
||||
"npcs": "resources/config/npcs_battle_zone.json",
|
||||
"dialogues": "resources/dialogue/battle_zone_dialogues.json",
|
||||
"navigation": [
|
||||
"resources/navigation/uni_exterior3_trees_car.txt"
|
||||
],
|
||||
"script": "resources/start_battle_zone.lua",
|
||||
"playerPosition": [5, 0, -18.4]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"id": "location_dorm",
|
||||
"gameObjects": "resources/config/gameobjects_dorm_new_x.json",
|
||||
"interactiveObjects": "resources/config/interactive_objects_dorm_x.json",
|
||||
"npcs": "resources/config/npcs_dorm.json",
|
||||
"dialogues": "resources/dialogue/dorm_dialogues004.json",
|
||||
"navigation": [
|
||||
"resources/navigation/dorm3_bca.txt",
|
||||
"resources/navigation/dorm3_ca.txt",
|
||||
"resources/navigation/dorm3_ba.txt",
|
||||
"resources/navigation/dorm3_a.txt",
|
||||
"resources/navigation/dorm3_b.txt",
|
||||
"resources/navigation/dorm3_all_open.txt"
|
||||
],
|
||||
"script": "resources/start_dorm.lua",
|
||||
"teleports": "resources/config/teleports_dorm.json",
|
||||
"triggerZones": "resources/config/trigger_zones_dorm.json",
|
||||
"lights": "resources/config/lights_dorm.json",
|
||||
"playerPosition": [6.76345, 0, -14.6022],
|
||||
"taxi": {
|
||||
"triggerPosition": [-19.0, 0, 0],
|
||||
"waypoints": [
|
||||
[-20.85, 0, 30.0],
|
||||
[-19.35, 0, 24.0],
|
||||
[-19.35, 0, 14.0]
|
||||
],
|
||||
"initialPosition": [-20.85, 0, 64],
|
||||
"initialRotation": 0,
|
||||
"waypointReachRadius": 5
|
||||
},
|
||||
"lockInteractiveObjectsDuringTutorial": true
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"id": "uni_exterior",
|
||||
"gameObjects": "resources/config/gameobjects_uni_exterior_x.json",
|
||||
"interactiveObjects": "resources/config/interactive_objects_uni_exterior_x.json",
|
||||
"npcs": "resources/config/npcs_uni_exterior.json",
|
||||
"dialogues": "resources/dialogue/uni_exterior_dialogues003.json",
|
||||
"navigation": [
|
||||
"resources/navigation/uni_exterior3_trees_car.txt",
|
||||
"resources/navigation/uni_exterior3_trees.txt"
|
||||
],
|
||||
"script": "resources/start_uni_exterior.lua",
|
||||
"teleports": "resources/config/teleports_uni_exterior.json",
|
||||
"triggerZones": "resources/config/trigger_zones_uni_exterior.json",
|
||||
"lights": "resources/config/lights_uni_exterior.json",
|
||||
"playerPosition": [5, 0, -18.4],
|
||||
"taxi": {
|
||||
"triggerPosition": [-17.6, 0.05, -45],
|
||||
"waypoints": [
|
||||
[-17.6, 0.05, -90.0],
|
||||
[-17.6, 0.05, -75.0],
|
||||
[-17.6, 0.05, -55.0]
|
||||
],
|
||||
"initialPosition": [-17.6, 0.05, -100],
|
||||
"initialRotation": 180,
|
||||
"waypointReachRadius": 5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"id": "uni_interior",
|
||||
"gameObjects": "resources/config/gameobjects_uni_interior_x.json",
|
||||
"interactiveObjects": "resources/config/interactive_objects_uni_interior_x.json",
|
||||
"npcs": "resources/config/npcs_uni_interior.json",
|
||||
"dialogues": "resources/dialogue/uni_interior_dialogues_010.json",
|
||||
"navigation": [
|
||||
"resources/navigation/uni_interior4_unlocked_n2.txt",
|
||||
"resources/navigation/uni_interior3_n2_hall.txt",
|
||||
"resources/navigation/uni_interior4_n2_lr_hall.txt",
|
||||
"resources/navigation/uni_interior4_n2_lr_tr_hall.txt",
|
||||
"resources/navigation/uni_interior3_n2_lr_tr_hall_aiperi.txt",
|
||||
"resources/navigation/uni_interior3_darklands_all_open.txt",
|
||||
"resources/navigation/uni_interior4_unlocked_hall.txt",
|
||||
"resources/navigation/uni_interior4_unlocked_n3.txt",
|
||||
"resources/navigation/uni_interior4_unlocked_s1.txt",
|
||||
"resources/navigation/uni_interior4_unlocked_s2.txt",
|
||||
"resources/navigation/uni_interior4_unlocked_s3.txt",
|
||||
"resources/navigation/uni_interior4_unlocked_s3_s1.txt",
|
||||
"resources/navigation/uni_interior4_unlocked_s3_hall.txt",
|
||||
"resources/navigation/uni_interior4_unlocked_s3_n3.txt",
|
||||
"resources/navigation/uni_interior4_unlocked_s3_n2.txt",
|
||||
"resources/navigation/uni_interior4_unlocked_s3_s2.txt",
|
||||
"resources/navigation/uni_interior4_locked.txt"
|
||||
],
|
||||
"script": "resources/start_uni_interior.lua",
|
||||
"teleports": "resources/config/teleports_uni_interior.json",
|
||||
"triggerZones": "resources/config/trigger_zones_uni_interior.json",
|
||||
"lights": "resources/config/lights_uni_interior.json",
|
||||
"playerPosition": [0.942694, 0, -9.63104]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"npcs": [
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"quests": [
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"campaignId": "battle_zone",
|
||||
"chatHistory": [
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"chatPreviewMsg": [
|
||||
"",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"chatUnread": [
|
||||
true,
|
||||
true,
|
||||
true
|
||||
],
|
||||
"currentLocationName": "uni_exterior",
|
||||
"globalFloats": {
|
||||
"player_hp": 200.0
|
||||
},
|
||||
"globalInts": {},
|
||||
"inventory": {
|
||||
"items": []
|
||||
},
|
||||
"isDarklands": false,
|
||||
"isDawn": false,
|
||||
"isNight": false,
|
||||
"locations": {
|
||||
"uni_exterior": {
|
||||
"dialogueState": {
|
||||
"active": false
|
||||
},
|
||||
"interactiveObjectStates": [],
|
||||
"locationState": {
|
||||
"activeNavigationIndex": 0,
|
||||
"cameraAzimuth": -2.3499999046325684,
|
||||
"cameraInclination": 1.103600025177002,
|
||||
"isDarklands": false,
|
||||
"isDawn": false,
|
||||
"isNight": false,
|
||||
"targetInteractNpcIndex": -1,
|
||||
"targetInteractiveObjectIndex": -1,
|
||||
"targetTeleportZoneIndex": -1,
|
||||
"taxi": {
|
||||
"taxiArrivedCallbackCalled": false,
|
||||
"taxiCar": {
|
||||
"currentWaypoint": 0,
|
||||
"mode": 1,
|
||||
"positionX": -100.0,
|
||||
"positionY": -100.0,
|
||||
"positionZ": -100.0,
|
||||
"rotation": 0.0,
|
||||
"steeringAngle": 0.0,
|
||||
"velocity": 0.0,
|
||||
"waypointReachRadius": 3.0,
|
||||
"waypoints": []
|
||||
},
|
||||
"taxiCarInitialState": {
|
||||
"currentWaypoint": 0,
|
||||
"mode": 1,
|
||||
"positionX": -100.0,
|
||||
"positionY": -100.0,
|
||||
"positionZ": -100.0,
|
||||
"rotation": 0.0,
|
||||
"steeringAngle": 0.0,
|
||||
"velocity": 0.0,
|
||||
"waypointReachRadius": 3.0,
|
||||
"waypoints": []
|
||||
},
|
||||
"taxiCarTriggerPositionX": 0.0,
|
||||
"taxiCarTriggerPositionY": 0.0,
|
||||
"taxiCarTriggerPositionZ": 0.0,
|
||||
"taxiDefaultWaypoints": []
|
||||
}
|
||||
},
|
||||
"npcStates": [],
|
||||
"playerState": {
|
||||
"attackTargetIndex": -1,
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": true,
|
||||
"faceTargetIndex": -1,
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"hp": 200.0,
|
||||
"pathWaypoints": [],
|
||||
"position": [
|
||||
5.0,
|
||||
0.0,
|
||||
-18.399999618530273
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
5.0,
|
||||
0.0,
|
||||
-18.399999618530273
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.0,
|
||||
"walkTarget": [
|
||||
5.0,
|
||||
0.0,
|
||||
-18.399999618530273
|
||||
]
|
||||
},
|
||||
"scriptGlobals": null,
|
||||
"teleportZoneState": {},
|
||||
"triggerZoneState": {}
|
||||
}
|
||||
},
|
||||
"money": 5500,
|
||||
"playerHp": 200.0,
|
||||
"playerMaxHp": 200.0,
|
||||
"questJournal": {
|
||||
"quests": []
|
||||
},
|
||||
"savedAt": "2026-07-24 23:40",
|
||||
"taxiIsCalled": false,
|
||||
"tutorialJournalScreenOpened": false,
|
||||
"tutorialNeedOpenTaxiScreen": false,
|
||||
"tutorialPhoneChatScreenOpened": false,
|
||||
"tutorialStep": 9
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"dialogues": [
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
-- ============================================
|
||||
-- BATTLE ZONE (campaign "battle_zone")
|
||||
-- uni_exterior level with static objects and navigation only.
|
||||
-- ============================================
|
||||
|
||||
function callback_enter_location()
|
||||
end
|
||||
|
||||
function callback_exit_location()
|
||||
end
|
||||
|
||||
game_api.set_location_callbacks(
|
||||
callback_enter_location,
|
||||
callback_exit_location
|
||||
)
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"root": {
|
||||
"type": "FrameLayout",
|
||||
"width": "match_parent",
|
||||
"height": "match_parent",
|
||||
"children": [
|
||||
{
|
||||
"type": "StaticImage",
|
||||
"name": "campaignBkg",
|
||||
"width": 2992,
|
||||
"height": 1632,
|
||||
"horizontal_gravity": "center",
|
||||
"vertical_gravity": "center",
|
||||
"texture": "resources/w/ui/img/main/MenuBkg001_v001.png"
|
||||
},
|
||||
{
|
||||
"type": "TextView",
|
||||
"name": "campaignTitle",
|
||||
"horizontal_gravity": "center",
|
||||
"vertical_gravity": "center",
|
||||
"y": 200,
|
||||
"width": 300,
|
||||
"height": 70,
|
||||
"text": "Выберите кампанию:",
|
||||
"fontSize": 48,
|
||||
"textCentered": true
|
||||
},
|
||||
{
|
||||
"type": "TextButton",
|
||||
"name": "campaignSlot1Button",
|
||||
"horizontal_gravity": "center",
|
||||
"vertical_gravity": "center",
|
||||
"y": 130,
|
||||
"width": 400,
|
||||
"height": 70,
|
||||
"text": "",
|
||||
"fontSize": 32,
|
||||
"textCentered": true
|
||||
},
|
||||
{
|
||||
"type": "TextButton",
|
||||
"name": "campaignSlot2Button",
|
||||
"horizontal_gravity": "center",
|
||||
"vertical_gravity": "center",
|
||||
"y": 60,
|
||||
"width": 400,
|
||||
"height": 70,
|
||||
"text": "",
|
||||
"fontSize": 32,
|
||||
"textCentered": true
|
||||
},
|
||||
{
|
||||
"type": "TextButton",
|
||||
"name": "campaignSlot3Button",
|
||||
"horizontal_gravity": "center",
|
||||
"vertical_gravity": "center",
|
||||
"y": -10,
|
||||
"width": 400,
|
||||
"height": 70,
|
||||
"text": "",
|
||||
"fontSize": 32,
|
||||
"textCentered": true
|
||||
},
|
||||
{
|
||||
"type": "TextButton",
|
||||
"name": "campaignSlot4Button",
|
||||
"horizontal_gravity": "center",
|
||||
"vertical_gravity": "center",
|
||||
"y": -80,
|
||||
"width": 400,
|
||||
"height": 70,
|
||||
"text": "",
|
||||
"fontSize": 32,
|
||||
"textCentered": true
|
||||
},
|
||||
{
|
||||
"type": "TextButton",
|
||||
"name": "campaignBackButton",
|
||||
"horizontal_gravity": "center",
|
||||
"vertical_gravity": "bottom",
|
||||
"y": 40,
|
||||
"width": 200,
|
||||
"height": 60,
|
||||
"text": "Назад",
|
||||
"fontSize": 40,
|
||||
"textCentered": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"root": {
|
||||
"type": "FrameLayout",
|
||||
"width": "match_parent",
|
||||
"height": "match_parent",
|
||||
"children": [
|
||||
{
|
||||
"type": "StaticImage",
|
||||
"name": "campaignBkg",
|
||||
"width": 2992,
|
||||
"height": 1632,
|
||||
"horizontal_gravity": "center",
|
||||
"vertical_gravity": "center",
|
||||
"texture": "resources/w/ui/img/main/MenuBkg001_v001.png"
|
||||
},
|
||||
{
|
||||
"type": "TextView",
|
||||
"name": "campaignTitle",
|
||||
"horizontal_gravity": "center",
|
||||
"vertical_gravity": "center",
|
||||
"y": 200,
|
||||
"width": 300,
|
||||
"height": 70,
|
||||
"text": "Choose a campaign:",
|
||||
"fontSize": 48,
|
||||
"textCentered": true
|
||||
},
|
||||
{
|
||||
"type": "TextButton",
|
||||
"name": "campaignSlot1Button",
|
||||
"horizontal_gravity": "center",
|
||||
"vertical_gravity": "center",
|
||||
"y": 130,
|
||||
"width": 400,
|
||||
"height": 70,
|
||||
"text": "",
|
||||
"fontSize": 32,
|
||||
"textCentered": true
|
||||
},
|
||||
{
|
||||
"type": "TextButton",
|
||||
"name": "campaignSlot2Button",
|
||||
"horizontal_gravity": "center",
|
||||
"vertical_gravity": "center",
|
||||
"y": 60,
|
||||
"width": 400,
|
||||
"height": 70,
|
||||
"text": "",
|
||||
"fontSize": 32,
|
||||
"textCentered": true
|
||||
},
|
||||
{
|
||||
"type": "TextButton",
|
||||
"name": "campaignSlot3Button",
|
||||
"horizontal_gravity": "center",
|
||||
"vertical_gravity": "center",
|
||||
"y": -10,
|
||||
"width": 400,
|
||||
"height": 70,
|
||||
"text": "",
|
||||
"fontSize": 32,
|
||||
"textCentered": true
|
||||
},
|
||||
{
|
||||
"type": "TextButton",
|
||||
"name": "campaignSlot4Button",
|
||||
"horizontal_gravity": "center",
|
||||
"vertical_gravity": "center",
|
||||
"y": -80,
|
||||
"width": 400,
|
||||
"height": 70,
|
||||
"text": "",
|
||||
"fontSize": 32,
|
||||
"textCentered": true
|
||||
},
|
||||
{
|
||||
"type": "TextButton",
|
||||
"name": "campaignBackButton",
|
||||
"horizontal_gravity": "center",
|
||||
"vertical_gravity": "bottom",
|
||||
"y": 40,
|
||||
"width": 200,
|
||||
"height": 60,
|
||||
"text": "Back",
|
||||
"fontSize": 40,
|
||||
"textCentered": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
#include "Campaign.h"
|
||||
#include "utils/Utils.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include <iostream>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
static bool parseJsonFile(const std::string& path, const std::string& zipFile, json& out) {
|
||||
const std::string content = readLocalizedConfigFile(path, zipFile);
|
||||
if (content.empty()) {
|
||||
std::cerr << "[Campaign] Failed to read " << path << "\n";
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
out = json::parse(content);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[Campaign] JSON parse error in " << path << ": " << e.what() << "\n";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static Eigen::Vector3f readVec3(const json& j) {
|
||||
if (!j.is_array() || j.size() != 3) return Eigen::Vector3f::Zero();
|
||||
return Eigen::Vector3f(j[0].get<float>(), j[1].get<float>(), j[2].get<float>());
|
||||
}
|
||||
|
||||
bool LocationConfig::fromFile(const std::string& path, const std::string& zipFile, LocationConfig& out) {
|
||||
json j;
|
||||
if (!parseJsonFile(path, zipFile, j)) return false;
|
||||
|
||||
out = LocationConfig{};
|
||||
out.id = j.value("id", "");
|
||||
out.gameObjectsJsonPath = j.value("gameObjects", "");
|
||||
out.interactiveObjectsJsonPath = j.value("interactiveObjects", "");
|
||||
out.npcsJsonPath = j.value("npcs", "");
|
||||
out.dialoguesJsonPath = j.value("dialogues", "");
|
||||
out.navigationJsonPaths = j.value("navigation", std::vector<std::string>{});
|
||||
out.scriptPath = j.value("script", "");
|
||||
out.teleportsJsonPath = j.value("teleports", "");
|
||||
out.triggerZonesJsonPath = j.value("triggerZones", "");
|
||||
out.lightsJsonPath = j.value("lights", "");
|
||||
if (j.contains("playerPosition")) out.playerPosition = readVec3(j["playerPosition"]);
|
||||
out.lockInteractiveObjectsDuringTutorial = j.value("lockInteractiveObjectsDuringTutorial", false);
|
||||
|
||||
if (j.contains("taxi") && j["taxi"].is_object()) {
|
||||
const json& t = j["taxi"];
|
||||
Taxi taxi;
|
||||
if (t.contains("triggerPosition")) taxi.triggerPosition = readVec3(t["triggerPosition"]);
|
||||
if (t.contains("initialPosition")) taxi.initialPosition = readVec3(t["initialPosition"]);
|
||||
if (t.contains("waypoints") && t["waypoints"].is_array()) {
|
||||
for (const auto& w : t["waypoints"]) taxi.waypoints.push_back(readVec3(w));
|
||||
}
|
||||
taxi.initialRotation = t.value("initialRotation", taxi.initialRotation);
|
||||
taxi.waypointReachRadius = t.value("waypointReachRadius", taxi.waypointReachRadius);
|
||||
out.taxi = taxi;
|
||||
}
|
||||
|
||||
if (out.id.empty()) {
|
||||
std::cerr << "[Campaign] Location config without an id: " << path << "\n";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string CampaignConfig::title(Language lang) const {
|
||||
auto it = titles.find(languageToCode(lang));
|
||||
if (it != titles.end()) return it->second;
|
||||
return titles.empty() ? id : titles.begin()->second;
|
||||
}
|
||||
|
||||
CampaignRegistry& CampaignRegistry::instance() {
|
||||
static CampaignRegistry reg;
|
||||
return reg;
|
||||
}
|
||||
|
||||
void CampaignRegistry::loadFromJson(const std::string& jsonPath, const std::string& zipFile) {
|
||||
json j;
|
||||
if (!parseJsonFile(jsonPath, zipFile, j)) return;
|
||||
if (!j.contains("campaigns") || !j["campaigns"].is_array()) {
|
||||
std::cerr << "[Campaign] No 'campaigns' array in " << jsonPath << "\n";
|
||||
return;
|
||||
}
|
||||
|
||||
campaigns_.clear();
|
||||
for (const auto& entry : j["campaigns"]) {
|
||||
CampaignConfig c;
|
||||
c.id = entry.value("id", "");
|
||||
if (c.id.empty()) continue;
|
||||
c.titles = entry.value("title", std::map<std::string, std::string>{});
|
||||
c.locationConfigPaths = entry.value("locations", std::vector<std::string>{});
|
||||
c.startLocation = entry.value("startLocation", "");
|
||||
c.startMusic = entry.value("startMusic", "");
|
||||
c.startStatePath = entry.value("startState", "");
|
||||
c.questsPath = entry.value("quests", "");
|
||||
c.tutorial = entry.value("tutorial", false);
|
||||
campaigns_.push_back(std::move(c));
|
||||
}
|
||||
|
||||
logger() << "[Campaign] Loaded " << campaigns_.size() << " campaigns from " << jsonPath << "\n";
|
||||
}
|
||||
|
||||
const CampaignConfig* CampaignRegistry::find(const std::string& id) const {
|
||||
for (const auto& c : campaigns_) {
|
||||
if (c.id == id) return &c;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
#include "Localization.h"
|
||||
#include <Eigen/Dense>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
// One location of a campaign, loaded from resources/config/locations/<name>.json.
|
||||
// Optional paths may be left empty; Location::setup skips them.
|
||||
struct LocationConfig {
|
||||
std::string id;
|
||||
std::string gameObjectsJsonPath;
|
||||
std::string interactiveObjectsJsonPath;
|
||||
std::string npcsJsonPath;
|
||||
std::string dialoguesJsonPath;
|
||||
std::vector<std::string> navigationJsonPaths;
|
||||
std::string scriptPath;
|
||||
std::string teleportsJsonPath;
|
||||
std::string triggerZonesJsonPath;
|
||||
std::string lightsJsonPath;
|
||||
Eigen::Vector3f playerPosition = Eigen::Vector3f::Zero();
|
||||
|
||||
// Where the taxi drives in when the player calls it. No taxi when absent.
|
||||
struct Taxi {
|
||||
Eigen::Vector3f triggerPosition = Eigen::Vector3f::Zero();
|
||||
std::vector<Eigen::Vector3f> waypoints;
|
||||
Eigen::Vector3f initialPosition = Eigen::Vector3f::Zero();
|
||||
float initialRotation = 0.f;
|
||||
float waypointReachRadius = 5.f;
|
||||
};
|
||||
std::optional<Taxi> taxi;
|
||||
|
||||
// Interactive objects stay locked until the tutorial reaches "pick up an item".
|
||||
bool lockInteractiveObjectsDuringTutorial = false;
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
// A playable campaign: its own set of locations, quests and starting state.
|
||||
struct CampaignConfig {
|
||||
std::string id;
|
||||
std::map<std::string, std::string> titles; // language code -> title
|
||||
std::vector<std::string> locationConfigPaths;
|
||||
std::string startLocation;
|
||||
std::string startMusic;
|
||||
std::string startStatePath;
|
||||
std::string questsPath;
|
||||
bool tutorial = false;
|
||||
|
||||
std::string title(Language lang) const;
|
||||
};
|
||||
|
||||
// Campaigns listed in resources/config/campaigns.json, in display order.
|
||||
class CampaignRegistry {
|
||||
public:
|
||||
static constexpr const char* kDefaultCampaignId = "main";
|
||||
|
||||
static CampaignRegistry& instance();
|
||||
|
||||
void loadFromJson(const std::string& jsonPath, const std::string& zipFile = "");
|
||||
|
||||
const std::vector<CampaignConfig>& all() const { return campaigns_; }
|
||||
// Returns nullptr if not found.
|
||||
const CampaignConfig* find(const std::string& id) const;
|
||||
|
||||
private:
|
||||
CampaignRegistry() = default;
|
||||
|
||||
std::vector<CampaignConfig> campaigns_;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
+166
-337
@@ -4,6 +4,7 @@
|
||||
#include "utils/Utils.h"
|
||||
#include <fstream>
|
||||
#include "items/ItemRegistry.h"
|
||||
#include "Campaign.h"
|
||||
#include "render/OpenGlExtensions.h"
|
||||
#include <iostream>
|
||||
#include "render/TextureManager.h"
|
||||
@@ -326,24 +327,18 @@ namespace FRG
|
||||
Character::nameTranslations.loadFromFile("resources/config/translate_names.json");
|
||||
});
|
||||
|
||||
// createLocations() is also used by the full-reset path (see performFullReset()),
|
||||
// so it stays a single synchronous function rather than being split into
|
||||
// per-location queue steps; it's queued here as one coarse step.
|
||||
// Every campaign's locations are built up front (one loading step per location),
|
||||
// so starting or loading any campaign later only switches the active set.
|
||||
CampaignRegistry::instance().loadFromJson("resources/config/campaigns.json", CONST_ZIP_FILE);
|
||||
loadSteps.push([this]() {
|
||||
createLocationsStep1();
|
||||
gameState.globalFloats["player_hp"] = 200; // read by location scripts as they load
|
||||
});
|
||||
for (const CampaignConfig& campaign : CampaignRegistry::instance().all()) {
|
||||
queueCampaignBuildSteps(campaign);
|
||||
}
|
||||
loadSteps.push([this]() {
|
||||
createLocationsStep2();
|
||||
});
|
||||
loadSteps.push([this]() {
|
||||
createLocationsStep3();
|
||||
});
|
||||
loadSteps.push([this]() {
|
||||
createLocationsStep4();
|
||||
});
|
||||
loadSteps.push([this]() {
|
||||
createLocationsStep5();
|
||||
});
|
||||
activateCampaign(CampaignRegistry::kDefaultCampaignId);
|
||||
});
|
||||
|
||||
loadSteps.push([this]() {
|
||||
// Wire inventory callbacks: tutorial tracking + toast notifications.
|
||||
@@ -380,6 +375,12 @@ namespace FRG
|
||||
menuManager.skipCutsceneFunc = [this]() {
|
||||
if (currentLocation()) currentLocation()->dialogueSystem.skipCutscene();
|
||||
};
|
||||
|
||||
menuManager.callTaxiFunc = [this]() {
|
||||
// The taxi hint has served its purpose once a taxi is on its way.
|
||||
for (auto& [name, loc] : gameState.locations) loc->onPlayerTaxiRequired = nullptr;
|
||||
if (currentLocation()) currentLocation()->onTaxiCalled();
|
||||
};
|
||||
});
|
||||
|
||||
loadSteps.push([this]() {
|
||||
@@ -391,15 +392,17 @@ namespace FRG
|
||||
std::cerr << "Failed to load UI: " << e.what() << std::endl;
|
||||
}
|
||||
|
||||
menuManager.startGameFunc = [this]() {
|
||||
mainThreadHandler.EnqueueMainThreadTask([this]() {
|
||||
menuManager.startGameFunc = [this](const std::string& campaignId) {
|
||||
mainThreadHandler.EnqueueMainThreadTask([this, campaignId]() {
|
||||
activateCampaign(campaignId);
|
||||
performResetToInitialState();
|
||||
applyTutorialSetting();
|
||||
gameState.currentLocationName = "location_dorm";
|
||||
const CampaignConfig& campaign = activeCampaign();
|
||||
gameState.currentLocationName = campaign.startLocation;
|
||||
menuManager.showGameplayHud(); // first HUD frame drawn from the fresh state
|
||||
currentLocation()->scriptEngine.callLocationEnterCallback();
|
||||
this->audioPlayer->crossFadeMusicAsync("music/obshaga.ogg");
|
||||
menuManager.lastMusicTrack = "music/obshaga.ogg";
|
||||
if (currentLocation()) currentLocation()->scriptEngine.callLocationEnterCallback();
|
||||
audioPlayer->crossFadeMusicAsync(campaign.startMusic);
|
||||
menuManager.lastMusicTrack = campaign.startMusic;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -466,13 +469,49 @@ namespace FRG
|
||||
loadStepsDone = 0;
|
||||
}
|
||||
|
||||
void Game::rearmDormOneShotCallbacks() {
|
||||
auto it = gameState.locations.find("location_dorm");
|
||||
if (it == gameState.locations.end() || !it->second) return;
|
||||
void Game::rearmOneShotCallbacks(LocationMap& locations) {
|
||||
for (auto& [name, loc] : locations) {
|
||||
loc->onPlayerTaxiRequired = [this]() {
|
||||
menuManager.tutorialShowTaxiHint();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
it->second->onPlayerTaxiRequired = [this]() {
|
||||
menuManager.tutorialShowTaxiHint();
|
||||
};
|
||||
void Game::activateCampaign(const std::string& campaignId) {
|
||||
auto it = campaignLocations.find(campaignId);
|
||||
if (it == campaignLocations.end()) {
|
||||
std::cerr << "[campaign] Unknown campaign '" << campaignId << "', using '"
|
||||
<< CampaignRegistry::kDefaultCampaignId << "'" << std::endl;
|
||||
it = campaignLocations.find(CampaignRegistry::kDefaultCampaignId);
|
||||
if (it == campaignLocations.end()) return;
|
||||
}
|
||||
gameState.campaignId = it->first;
|
||||
gameState.locations = it->second;
|
||||
gameState.questJournal.loadFromFile(activeCampaign().questsPath, CONST_ZIP_FILE);
|
||||
}
|
||||
|
||||
const CampaignConfig& Game::activeCampaign() const {
|
||||
const CampaignConfig* campaign = CampaignRegistry::instance().find(gameState.campaignId);
|
||||
if (!campaign) campaign = CampaignRegistry::instance().find(CampaignRegistry::kDefaultCampaignId);
|
||||
if (!campaign) {
|
||||
static const CampaignConfig empty;
|
||||
return empty;
|
||||
}
|
||||
return *campaign;
|
||||
}
|
||||
|
||||
void Game::queueCampaignBuildSteps(const CampaignConfig& campaign) {
|
||||
const std::string campaignId = campaign.id;
|
||||
for (const std::string& configPath : campaign.locationConfigPaths) {
|
||||
loadSteps.push([this, campaignId, configPath]() {
|
||||
LocationConfig config;
|
||||
if (LocationConfig::fromFile(configPath, CONST_ZIP_FILE, config))
|
||||
campaignLocations[campaignId][config.id] = createLocation(config);
|
||||
});
|
||||
}
|
||||
loadSteps.push([this, campaignId]() {
|
||||
wireCampaignLocations(campaignLocations[campaignId]);
|
||||
});
|
||||
}
|
||||
|
||||
void Game::performFullReset() {
|
||||
@@ -485,6 +524,7 @@ namespace FRG
|
||||
gameState.money = 5500;
|
||||
gameState.globalInts.clear();
|
||||
gameState.globalFloats.clear();
|
||||
gameState.globalFloats["player_hp"] = 200;
|
||||
gameState.tutorialStep = TutorialStep::AdvanceDialogue;
|
||||
gameState.tutorialPhoneChatScreenOpened = false;
|
||||
gameState.tutorialJournalScreenOpened = false;
|
||||
@@ -495,21 +535,31 @@ namespace FRG
|
||||
gameState.chatHistory[i].clear();
|
||||
}
|
||||
gameState.inventory.clear();
|
||||
gameState.questJournal.loadFromFile(localizedConfigPath("resources/config/quests.json", g_currentLanguage), CONST_ZIP_FILE);
|
||||
createLocationsStep1();
|
||||
createLocationsStep2();
|
||||
createLocationsStep3();
|
||||
createLocationsStep4();
|
||||
createLocationsStep5();
|
||||
|
||||
// Developer workflow: uncomment to (re)generate resources/config/start_state.json
|
||||
const CampaignConfig& campaign = activeCampaign();
|
||||
gameState.questJournal.loadFromFile(campaign.questsPath, CONST_ZIP_FILE);
|
||||
|
||||
LocationMap& locations = campaignLocations[campaign.id];
|
||||
locations.clear();
|
||||
for (const std::string& configPath : campaign.locationConfigPaths) {
|
||||
LocationConfig config;
|
||||
if (LocationConfig::fromFile(configPath, CONST_ZIP_FILE, config))
|
||||
locations[config.id] = createLocation(config);
|
||||
}
|
||||
wireCampaignLocations(locations);
|
||||
gameState.locations = locations;
|
||||
if (shadowMap) {
|
||||
for (auto& [name, loc] : locations) loc->shadowMap = shadowMap;
|
||||
}
|
||||
|
||||
// Developer workflow: uncomment to (re)generate the campaign's start-state JSON
|
||||
// from this freshly-rebuilt state after changing gameplay/content JSON or Lua.
|
||||
// Re-comment before shipping.
|
||||
//saveInitialState();
|
||||
}
|
||||
|
||||
void Game::performResetToInitialState() {
|
||||
const std::string path = "resources/config/start_state.json";
|
||||
const std::string path = activeCampaign().startStatePath;
|
||||
|
||||
std::vector<char> fileData;
|
||||
if (strlen(CONST_ZIP_FILE) > 0)
|
||||
@@ -528,6 +578,7 @@ namespace FRG
|
||||
if (!content.empty()) {
|
||||
try {
|
||||
nlohmann::json root = nlohmann::json::parse(content);
|
||||
const std::string campaignId = gameState.campaignId;
|
||||
// GameState::load() merges into existing maps rather than replacing them,
|
||||
// so any Lua globals set during play that aren't in the pristine snapshot
|
||||
// must be cleared explicitly before loading it.
|
||||
@@ -537,11 +588,12 @@ namespace FRG
|
||||
// destruction, no re-parsing JSON/nav files, no new
|
||||
// sol2 state (GameState::load only touches map entries
|
||||
// that already exist).
|
||||
gameState.campaignId = campaignId; // the snapshot may predate campaigns
|
||||
// Locations survive this reset, so any one-shot callbacks they consumed
|
||||
// during the previous playthrough (taxi request) must be
|
||||
// re-armed here -- createLocations() normally does this, but doesn't run
|
||||
// on this fast path.
|
||||
rearmDormOneShotCallbacks();
|
||||
// re-armed here -- wireCampaignLocations() normally does this, but doesn't
|
||||
// run on this fast path.
|
||||
rearmOneShotCallbacks(gameState.locations);
|
||||
return;
|
||||
} catch (const std::exception& e) {
|
||||
logger() << "[reset] Failed to parse " << path << ": " << e.what()
|
||||
@@ -555,19 +607,21 @@ namespace FRG
|
||||
performFullReset();
|
||||
}
|
||||
|
||||
// Runs after either reset path. start_state.json always holds the start of the
|
||||
// tutorial; a disabled tutorial is simply one that is already completed.
|
||||
// Runs after either reset path. Start states always hold the start of the tutorial
|
||||
// (if the campaign has one); a disabled tutorial is simply one that is already completed.
|
||||
void Game::applyTutorialSetting() {
|
||||
if (!menuManager.tutorialEnabledSetting)
|
||||
if (!activeCampaign().tutorial || !menuManager.tutorialEnabledSetting)
|
||||
gameState.tutorialStep = TutorialStep::Completed;
|
||||
}
|
||||
|
||||
void Game::saveInitialState() {
|
||||
nlohmann::json root;
|
||||
gameState.save(root);
|
||||
const std::string path = "start_state.json";
|
||||
// Written next to the executable; copy it over the campaign's start-state file.
|
||||
const std::string startStatePath = activeCampaign().startStatePath;
|
||||
const std::string path = startStatePath.substr(startStatePath.find_last_of('/') + 1);
|
||||
logger() << "[reset] saveInitialState called, to " << path << std::endl;
|
||||
|
||||
|
||||
std::ofstream file(path);
|
||||
if (file.is_open()) {
|
||||
file << root.dump(2);
|
||||
@@ -578,309 +632,80 @@ namespace FRG
|
||||
saveJsonToFile(root, path);
|
||||
}
|
||||
|
||||
void Game::createLocationsStep1()
|
||||
std::shared_ptr<Location> Game::createLocation(const LocationConfig& config)
|
||||
{
|
||||
gameState.locations.clear();
|
||||
LocationSetup params;
|
||||
params.gameObjectsJsonPath = config.gameObjectsJsonPath;
|
||||
params.interactiveObjectsJsonPath = config.interactiveObjectsJsonPath;
|
||||
params.npcsJsonPath = config.npcsJsonPath;
|
||||
params.dialoguesJsonPath = config.dialoguesJsonPath;
|
||||
params.navigationJsonPaths = config.navigationJsonPaths;
|
||||
params.scriptPath = config.scriptPath;
|
||||
params.teleportsJsonPath = config.teleportsJsonPath;
|
||||
params.triggerZonesJsonPath = config.triggerZonesJsonPath;
|
||||
params.lightsJsonPath = config.lightsJsonPath;
|
||||
params.playerPosition = config.playerPosition;
|
||||
|
||||
gameState.globalFloats["player_hp"] = 200;
|
||||
|
||||
}
|
||||
|
||||
|
||||
void Game::createLocationsStep2()
|
||||
{
|
||||
LocationSetup uniInteriorParams;
|
||||
uniInteriorParams.gameObjectsJsonPath = "resources/config/gameobjects_uni_interior_x.json";
|
||||
uniInteriorParams.npcsJsonPath = "resources/config/npcs_uni_interior.json";
|
||||
uniInteriorParams.dialoguesJsonPath = "resources/dialogue/uni_interior_dialogues_010.json";
|
||||
|
||||
uniInteriorParams.navigationJsonPaths = {
|
||||
"resources/navigation/uni_interior4_unlocked_n2.txt", //0
|
||||
"resources/navigation/uni_interior3_n2_hall.txt",
|
||||
"resources/navigation/uni_interior4_n2_lr_hall.txt",
|
||||
"resources/navigation/uni_interior4_n2_lr_tr_hall.txt",
|
||||
"resources/navigation/uni_interior3_n2_lr_tr_hall_aiperi.txt",
|
||||
"resources/navigation/uni_interior3_darklands_all_open.txt", //5
|
||||
"resources/navigation/uni_interior4_unlocked_hall.txt",
|
||||
"resources/navigation/uni_interior4_unlocked_n3.txt",
|
||||
"resources/navigation/uni_interior4_unlocked_s1.txt",
|
||||
"resources/navigation/uni_interior4_unlocked_s2.txt",
|
||||
"resources/navigation/uni_interior4_unlocked_s3.txt",
|
||||
"resources/navigation/uni_interior4_unlocked_s3_s1.txt",
|
||||
"resources/navigation/uni_interior4_unlocked_s3_hall.txt",
|
||||
"resources/navigation/uni_interior4_unlocked_s3_n3.txt",
|
||||
"resources/navigation/uni_interior4_unlocked_s3_n2.txt",
|
||||
"resources/navigation/uni_interior4_unlocked_s3_s2.txt",
|
||||
"resources/navigation/uni_interior4_locked.txt" //16
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*
|
||||
uniInteriorParams.navigationJsonPaths = {
|
||||
"resources/navigation/uni_interior4_unlocked_n2.json", //0
|
||||
"resources/navigation/uni_interior3_n2_hall.json",
|
||||
"resources/navigation/uni_interior4_n2_lr_hall.json",
|
||||
"resources/navigation/uni_interior4_n2_lr_tr_hall.json",
|
||||
"resources/navigation/uni_interior3_n2_lr_tr_hall_aiperi.json",
|
||||
"resources/navigation/uni_interior3_darklands_all_open.json", //5
|
||||
"resources/navigation/uni_interior4_unlocked_hall.json",
|
||||
"resources/navigation/uni_interior4_unlocked_n3.json",
|
||||
"resources/navigation/uni_interior4_unlocked_s1.json",
|
||||
"resources/navigation/uni_interior4_unlocked_s2.json",
|
||||
"resources/navigation/uni_interior4_unlocked_s3.json",
|
||||
"resources/navigation/uni_interior4_unlocked_s3_s1.json", //11
|
||||
"resources/navigation/uni_interior4_unlocked_s3_hall.json",
|
||||
"resources/navigation/uni_interior4_unlocked_s3_n3.json",
|
||||
"resources/navigation/uni_interior4_unlocked_s3_n2.json",
|
||||
"resources/navigation/uni_interior4_unlocked_s3_s2.json",
|
||||
"resources/navigation/uni_interior4_locked.json" //16
|
||||
};*/
|
||||
|
||||
/*
|
||||
uniInteriorParams.navigationJsonPaths = {
|
||||
"resources/navigation/uni_interior3_darklands_all_open.json",
|
||||
"resources/navigation/uni_interior3_darklands_all_open.json",
|
||||
"resources/navigation/uni_interior3_darklands_all_open.json",
|
||||
"resources/navigation/uni_interior3_darklands_all_open.json",
|
||||
"resources/navigation/uni_interior3_darklands_all_open.json",
|
||||
"resources/navigation/uni_interior3_darklands_all_open.json",
|
||||
"resources/navigation/uni_interior3_darklands_all_open.json", //6
|
||||
"resources/navigation/uni_interior3_darklands_all_open.json",
|
||||
"resources/navigation/uni_interior3_darklands_all_open.json",
|
||||
"resources/navigation/uni_interior3_darklands_all_open.json",
|
||||
"resources/navigation/uni_interior3_darklands_all_open.json",
|
||||
"resources/navigation/uni_interior3_darklands_all_open.json",
|
||||
"resources/navigation/uni_interior3_darklands_all_open.json",
|
||||
"resources/navigation/uni_interior3_darklands_all_open.json"
|
||||
};*/
|
||||
|
||||
uniInteriorParams.teleportsJsonPath = "resources/config/teleports_uni_interior.json";
|
||||
uniInteriorParams.triggerZonesJsonPath = "resources/config/trigger_zones_uni_interior.json";
|
||||
uniInteriorParams.lightsJsonPath = "resources/config/lights_uni_interior.json";
|
||||
uniInteriorParams.scriptPath = "resources/start_uni_interior.lua";
|
||||
uniInteriorParams.interactiveObjectsJsonPath = "resources/config/interactive_objects_uni_interior_x.json";
|
||||
uniInteriorParams.playerPosition = Eigen::Vector3f(0.942694, 0, -9.63104);
|
||||
|
||||
gameState.locations["uni_interior"] = std::make_shared<Location>(renderer, gameState.inventory);
|
||||
gameState.locations["uni_interior"]->setup(uniInteriorParams, &gameState.questJournal, audioPlayer.get());
|
||||
gameState.locations["uni_interior"]->scriptEngine.setGlobalStore(&gameState.globalInts);
|
||||
gameState.locations["uni_interior"]->scriptEngine.setGlobalFloatStore(&gameState.globalFloats);
|
||||
gameState.locations["uni_interior"]->requestNightDayTransition = [this](bool isNight, bool isDawn) { gameState.isNight = isNight; gameState.isDawn = isDawn; };
|
||||
gameState.locations["uni_interior"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
|
||||
gameState.locations["uni_interior"]->requestAdvanceDarklandsHud = [this]() { menuManager.advanceUniIntDarklandsHud(); };
|
||||
gameState.locations["uni_interior"]->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); };
|
||||
gameState.locations["uni_interior"]->requestReturnToMainMenu = [this]() {
|
||||
auto loc = std::make_shared<Location>(renderer, gameState.inventory);
|
||||
loc->setup(params, &gameState.questJournal, audioPlayer.get());
|
||||
loc->scriptEngine.setGlobalStore(&gameState.globalInts);
|
||||
loc->scriptEngine.setGlobalFloatStore(&gameState.globalFloats);
|
||||
loc->requestNightDayTransition = [this](bool isNight, bool isDawn) {
|
||||
gameState.isNight = isNight;
|
||||
gameState.isDawn = isDawn;
|
||||
};
|
||||
loc->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
|
||||
loc->requestAdvanceDarklandsHud = [this]() { menuManager.advanceUniIntDarklandsHud(); };
|
||||
loc->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); };
|
||||
loc->requestReturnToMainMenu = [this]() {
|
||||
gameState.currentLocationName.clear();
|
||||
menuManager.showMenuAfterGameCompleted();
|
||||
};
|
||||
gameState.locations["uni_interior"]->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) {
|
||||
loc->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) {
|
||||
menuManager.setChatUnread(idx, unread, msg);
|
||||
};
|
||||
if (gameState.locations["uni_interior"]->player)
|
||||
gameState.locations["uni_interior"]->player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
||||
for (auto& npc : gameState.locations["uni_interior"]->npcs) {
|
||||
if (loc->player)
|
||||
loc->player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
||||
for (auto& npc : loc->npcs) {
|
||||
if (npc && npc->state.canAttack) {
|
||||
npc->state.onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); };
|
||||
}
|
||||
}
|
||||
|
||||
gameState.locations["uni_interior"]->requestDarklandsPlayBattleMusic = [this]() {
|
||||
loc->requestDarklandsPlayBattleMusic = [this]() {
|
||||
audioPlayer->crossFadeMusicFromPositionAsync("music/bishkek fight.ogg");
|
||||
menuManager.lastMusicTrack = "music/bishkek fight.ogg";
|
||||
};
|
||||
gameState.locations["uni_interior"]->requestDarklandsPlayNormalMusic = [this]() {
|
||||
loc->requestDarklandsPlayNormalMusic = [this]() {
|
||||
audioPlayer->crossFadeMusicFromPositionAsync("music/bishkek fight calm.ogg");
|
||||
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
|
||||
};
|
||||
|
||||
if (config.lockInteractiveObjectsDuringTutorial) {
|
||||
// Objects can be picked up once the tutorial reaches "pick up an item".
|
||||
loc->areInteractiveObjectsLocked = [this]() {
|
||||
return gameState.tutorialStep < TutorialStep::PickUpItem;
|
||||
};
|
||||
}
|
||||
|
||||
if (config.taxi) {
|
||||
const LocationConfig::Taxi& taxi = *config.taxi;
|
||||
loc->state.taxiCarTriggerPosition = taxi.triggerPosition;
|
||||
loc->state.taxiCarWaypoints = taxi.waypoints;
|
||||
loc->state.taxiCarInitialState.position = taxi.initialPosition;
|
||||
loc->state.taxiCarInitialState.rotation = taxi.initialRotation;
|
||||
loc->state.taxiCarInitialState.mode = NpcCar::Mode::NONE;
|
||||
loc->state.taxiCarInitialState.velocity = 0.f;
|
||||
loc->state.taxiCarInitialState.steeringAngle = 0.f;
|
||||
loc->state.taxiCarInitialState.waypointReachRadius = taxi.waypointReachRadius;
|
||||
loc->state.taxiCar = loc->state.taxiCarInitialState;
|
||||
}
|
||||
|
||||
return loc;
|
||||
}
|
||||
|
||||
|
||||
void Game::createLocationsStep3()
|
||||
void Game::wireCampaignLocations(LocationMap& locations)
|
||||
{
|
||||
LocationSetup uniInteriorParams;
|
||||
|
||||
LocationSetup uniExteriorParams = uniInteriorParams;
|
||||
uniExteriorParams.gameObjectsJsonPath = "resources/config/gameobjects_uni_exterior_x.json";
|
||||
uniExteriorParams.interactiveObjectsJsonPath = "resources/config/interactive_objects_uni_exterior_x.json";
|
||||
/*
|
||||
uniExteriorParams.navigationJsonPaths = {
|
||||
"resources/navigation/uni_exterior0.json",
|
||||
"resources/navigation/uni_exterior0.json"
|
||||
};*/
|
||||
//uni_exterior0
|
||||
/*uniExteriorParams.navigationJsonPaths = {
|
||||
"resources/navigation/uni_exterior3_all.json",
|
||||
"resources/navigation/uni_exterior3_all.json"
|
||||
};*/
|
||||
/*
|
||||
uniExteriorParams.navigationJsonPaths = {
|
||||
"resources/navigation/uni_exterior3_trees_car.json",
|
||||
"resources/navigation/uni_exterior3_trees.json",
|
||||
};*/
|
||||
|
||||
uniExteriorParams.navigationJsonPaths = {
|
||||
"resources/navigation/uni_exterior3_trees_car.txt",
|
||||
"resources/navigation/uni_exterior3_trees.txt",
|
||||
};
|
||||
|
||||
uniExteriorParams.teleportsJsonPath = "resources/config/teleports_uni_exterior.json";
|
||||
uniExteriorParams.triggerZonesJsonPath = "resources/config/trigger_zones_uni_exterior.json";
|
||||
uniExteriorParams.lightsJsonPath = "resources/config/lights_uni_exterior.json";
|
||||
uniExteriorParams.scriptPath = "resources/start_uni_exterior.lua";
|
||||
uniExteriorParams.playerPosition = Eigen::Vector3f(5, 0, -18.4);
|
||||
uniExteriorParams.npcsJsonPath = "resources/config/npcs_uni_exterior.json";
|
||||
uniExteriorParams.dialoguesJsonPath = "resources/dialogue/uni_exterior_dialogues003.json";
|
||||
|
||||
|
||||
gameState.locations["uni_exterior"] = std::make_shared<Location>(renderer, gameState.inventory);
|
||||
gameState.locations["uni_exterior"]->setup(uniExteriorParams, &gameState.questJournal, audioPlayer.get());
|
||||
gameState.locations["uni_exterior"]->scriptEngine.setGlobalStore(&gameState.globalInts);
|
||||
gameState.locations["uni_exterior"]->scriptEngine.setGlobalFloatStore(&gameState.globalFloats);
|
||||
gameState.locations["uni_exterior"]->requestNightDayTransition = [this](bool isNight, bool isDawn) {
|
||||
gameState.isNight = isNight;
|
||||
gameState.isDawn = isDawn;
|
||||
};
|
||||
gameState.locations["uni_exterior"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
|
||||
gameState.locations["uni_exterior"]->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); };
|
||||
//gameState.locations["uni_exterior"]->requestReturnToMainMenu = [this]() { menuManager.showMenuAfterGameCompleted(); };
|
||||
gameState.locations["uni_exterior"]->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) {
|
||||
menuManager.setChatUnread(idx, unread, msg);
|
||||
};
|
||||
if (gameState.locations["uni_exterior"]->player)
|
||||
gameState.locations["uni_exterior"]->player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
||||
|
||||
|
||||
//Exterior set up taxi:
|
||||
|
||||
gameState.locations["uni_exterior"]->state.taxiCarTriggerPosition = Eigen::Vector3f(-17.6, 0.05, -45);
|
||||
|
||||
gameState.locations["uni_exterior"]->state.taxiCarWaypoints = {
|
||||
Vector3f(-17.6, 0.05, -90.0),
|
||||
Vector3f(-17.6, 0.05, -75.0),
|
||||
Vector3f(-17.6, 0.05, -55.0)
|
||||
};
|
||||
|
||||
gameState.locations["uni_exterior"]->state.taxiCarInitialState.position = /*Vector3f(-17.6, 0.05, -40);*/ Vector3f(-17.6, 0.05, -100);
|
||||
gameState.locations["uni_exterior"]->state.taxiCarInitialState.rotation = 180.f;
|
||||
gameState.locations["uni_exterior"]->state.taxiCarInitialState.mode = NpcCar::Mode::NONE;
|
||||
gameState.locations["uni_exterior"]->state.taxiCarInitialState.velocity = 0.f;
|
||||
gameState.locations["uni_exterior"]->state.taxiCarInitialState.steeringAngle = 0.f;
|
||||
gameState.locations["uni_exterior"]->state.taxiCarInitialState.waypointReachRadius = 5.f;
|
||||
|
||||
gameState.locations["uni_exterior"]->state.taxiCar = gameState.locations["uni_exterior"]->state.taxiCarInitialState;
|
||||
|
||||
//Exterior set up taxi end
|
||||
|
||||
}
|
||||
|
||||
|
||||
void Game::createLocationsStep4()
|
||||
{
|
||||
LocationSetup params_dorm;
|
||||
//params_dorm.gameObjectsJsonPath = "resources/config/gameobjects_dorm.json";
|
||||
//params_dorm.gameObjectsJsonPath = "resources/config/gameobjects_dorm_trees001.json";
|
||||
params_dorm.gameObjectsJsonPath = "resources/config/gameobjects_dorm_new_x.json";
|
||||
|
||||
params_dorm.npcsJsonPath = "resources/config/npcs_dorm.json";
|
||||
params_dorm.dialoguesJsonPath = "resources/dialogue/dorm_dialogues004.json";
|
||||
|
||||
/*
|
||||
params_dorm.navigationJsonPaths = {
|
||||
"resources/navigation/dorm3_bca.json",
|
||||
"resources/navigation/dorm3_ca.json",
|
||||
"resources/navigation/dorm3_ba.json",
|
||||
"resources/navigation/dorm3_a.json",
|
||||
"resources/navigation/dorm3_b.json",
|
||||
"resources/navigation/dorm3_all_open.json",
|
||||
};*/
|
||||
|
||||
params_dorm.navigationJsonPaths = {
|
||||
"resources/navigation/dorm3_bca.txt", //0
|
||||
"resources/navigation/dorm3_ca.txt", //1
|
||||
"resources/navigation/dorm3_ba.txt",
|
||||
"resources/navigation/dorm3_a.txt",
|
||||
"resources/navigation/dorm3_b.txt",
|
||||
"resources/navigation/dorm3_all_open.txt", //5
|
||||
};
|
||||
|
||||
/*
|
||||
params_dorm.navigationJsonPaths = {
|
||||
"resources/navigation/dorm0_large.json",
|
||||
"resources/navigation/dorm0_large.json",
|
||||
"resources/navigation/dorm0_large.json",
|
||||
"resources/navigation/dorm0_large.json",
|
||||
"resources/navigation/dorm0_large.json",
|
||||
"resources/navigation/dorm0_large.json",
|
||||
};*/
|
||||
params_dorm.teleportsJsonPath = "resources/config/teleports_dorm.json";
|
||||
params_dorm.triggerZonesJsonPath = "resources/config/trigger_zones_dorm.json";
|
||||
params_dorm.lightsJsonPath = "resources/config/lights_dorm.json";
|
||||
params_dorm.scriptPath = "resources/start_dorm.lua";
|
||||
params_dorm.interactiveObjectsJsonPath = "resources/config/interactive_objects_dorm_x.json";
|
||||
params_dorm.playerPosition = Eigen::Vector3f(6.76345, 0, -14.6022);
|
||||
|
||||
gameState.locations["location_dorm"] = std::make_shared<Location>(renderer, gameState.inventory);
|
||||
gameState.locations["location_dorm"]->setup(params_dorm, &gameState.questJournal, audioPlayer.get());
|
||||
gameState.locations["location_dorm"]->scriptEngine.setGlobalStore(&gameState.globalInts);
|
||||
gameState.locations["location_dorm"]->scriptEngine.setGlobalFloatStore(&gameState.globalFloats);
|
||||
gameState.locations["location_dorm"]->requestNightDayTransition = [this](bool isNight, bool isDawn) { gameState.isNight = isNight; gameState.isDawn = isDawn; };
|
||||
gameState.locations["location_dorm"]->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
|
||||
gameState.locations["location_dorm"]->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); };
|
||||
//gameState.locations["location_dorm"]->requestReturnToMainMenu = [this]() { menuManager.showMenuAfterGameCompleted(); };
|
||||
gameState.locations["location_dorm"]->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) {
|
||||
menuManager.setChatUnread(idx, unread, msg);
|
||||
};
|
||||
if (gameState.locations["location_dorm"]->player)
|
||||
gameState.locations["location_dorm"]->player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
||||
|
||||
// Objects can be picked up once the tutorial reaches "pick up an item".
|
||||
gameState.locations["location_dorm"]->areInteractiveObjectsLocked = [this]() {
|
||||
return gameState.tutorialStep < TutorialStep::PickUpItem;
|
||||
};
|
||||
|
||||
//Dorm set up taxi:
|
||||
|
||||
gameState.locations["location_dorm"]->state.taxiCarTriggerPosition = Eigen::Vector3f(-19.0, 0, 0);
|
||||
|
||||
gameState.locations["location_dorm"]->state.taxiCarWaypoints = {
|
||||
Vector3f(-20.85, 0, 30.0),
|
||||
Vector3f(-19.35, 0, 24.0),
|
||||
Vector3f(-19.35, 0, 14.0)
|
||||
};
|
||||
|
||||
gameState.locations["location_dorm"]->state.taxiCarInitialState.position = Vector3f(-20.85, 0, 64);
|
||||
gameState.locations["location_dorm"]->state.taxiCarInitialState.rotation = 0.f;
|
||||
gameState.locations["location_dorm"]->state.taxiCarInitialState.mode = NpcCar::Mode::NONE;
|
||||
gameState.locations["location_dorm"]->state.taxiCarInitialState.velocity = 0.f;
|
||||
gameState.locations["location_dorm"]->state.taxiCarInitialState.steeringAngle = 0.f;
|
||||
gameState.locations["location_dorm"]->state.taxiCarInitialState.waypointReachRadius = 5.f;
|
||||
|
||||
gameState.locations["location_dorm"]->state.taxiCar = gameState.locations["location_dorm"]->state.taxiCarInitialState;
|
||||
|
||||
//Dorm set up taxi end
|
||||
|
||||
|
||||
}
|
||||
|
||||
void Game::createLocationsStep5()
|
||||
{
|
||||
|
||||
rearmDormOneShotCallbacks();
|
||||
|
||||
menuManager.callTaxiFunc = [this]()
|
||||
{
|
||||
if (gameState.locations["location_dorm"])
|
||||
{
|
||||
gameState.locations["location_dorm"]->onPlayerTaxiRequired = nullptr;
|
||||
}
|
||||
|
||||
if (currentLocation())
|
||||
{
|
||||
currentLocation()->onTaxiCalled();
|
||||
}
|
||||
};
|
||||
rearmOneShotCallbacks(locations);
|
||||
|
||||
// Teleport callbacks: destination name and position come from the teleport zone data.
|
||||
auto teleportCallback = [this](const std::string& destName, const Eigen::Vector3f& destPos, float destRotY) {
|
||||
@@ -918,19 +743,19 @@ namespace FRG
|
||||
menuManager.onLocationChanged(destName);
|
||||
};
|
||||
|
||||
gameState.locations["uni_exterior"]->onTeleport = teleportCallback;
|
||||
gameState.locations["uni_interior"]->onTeleport = teleportCallback;
|
||||
gameState.locations["location_dorm"]->onTeleport = teleportCallback;
|
||||
for (auto& [name, loc] : locations) {
|
||||
loc->onTeleport = teleportCallback;
|
||||
}
|
||||
|
||||
// Share the global int store with all dialogue runtimes so flags set via
|
||||
// dialogue JSON and via Lua set_dialogue_flag all see the same state.
|
||||
for (auto& [name, loc] : gameState.locations) {
|
||||
for (auto& [name, loc] : locations) {
|
||||
loc->dialogueSystem.setGlobalFlagStore(&gameState.globalInts);
|
||||
}
|
||||
|
||||
// Wire tutorial advance callbacks for all locations.
|
||||
// completeTutorialStep() only acts on the current step, so sharing is safe.
|
||||
for (auto& [name, loc] : gameState.locations) {
|
||||
for (auto& [name, loc] : locations) {
|
||||
loc->dialogueSystem.setOnDialogueAdvanced([this]() {
|
||||
gameState.completeTutorialStep(TutorialStep::AdvanceDialogue);
|
||||
});
|
||||
@@ -941,14 +766,14 @@ namespace FRG
|
||||
}
|
||||
|
||||
// Wire chat-bubble callback so dynamic bubbles appear as dialogue lines are shown.
|
||||
for (auto& [name, loc] : gameState.locations) {
|
||||
for (auto& [name, loc] : locations) {
|
||||
loc->dialogueSystem.setOnChatBubbleReady([this](const std::string& text, bool incoming) {
|
||||
menuManager.onChatBubbleReady(text, incoming);
|
||||
});
|
||||
}
|
||||
|
||||
// Wire cutscene HUD: show skip button when cutscene starts, restore HUD when it ends.
|
||||
for (auto& [name, loc] : gameState.locations) {
|
||||
for (auto& [name, loc] : locations) {
|
||||
loc->dialogueSystem.setOnCutsceneStarted([this]() {
|
||||
menuManager.onCutsceneStarted();
|
||||
});
|
||||
@@ -958,7 +783,7 @@ namespace FRG
|
||||
}
|
||||
|
||||
// Wire HP-change callbacks so all player instances update the health bar HUD.
|
||||
for (auto& [name, loc] : gameState.locations) {
|
||||
for (auto& [name, loc] : locations) {
|
||||
if (loc->player) {
|
||||
loc->player->state.onHpChanged = [this, loc](float hp, float maxHp, int attackerIndex) {
|
||||
menuManager.updateHealthBar(hp, maxHp);
|
||||
@@ -999,8 +824,8 @@ namespace FRG
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
logger() << "Shadow map initialized" << std::endl;
|
||||
|
||||
for (auto& [name, loc] : gameState.locations) {
|
||||
loc->shadowMap = shadowMap;
|
||||
for (auto& [campaignId, locations] : campaignLocations) {
|
||||
for (auto& [name, loc] : locations) loc->shadowMap = shadowMap;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1011,8 +836,8 @@ namespace FRG
|
||||
shadowMap.reset();
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
|
||||
for (auto& [name, loc] : gameState.locations) {
|
||||
loc->shadowMap.reset();
|
||||
for (auto& [campaignId, locations] : campaignLocations) {
|
||||
for (auto& [name, loc] : locations) loc->shadowMap.reset();
|
||||
}
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
|
||||
@@ -1891,6 +1716,7 @@ namespace FRG
|
||||
nlohmann::json root = nlohmann::json::parse(content);
|
||||
SaveSlotInfo info;
|
||||
info.empty = false;
|
||||
info.campaignId = root.value("campaignId", std::string(CampaignRegistry::kDefaultCampaignId));
|
||||
info.locationName = root.value("currentLocationName", "");
|
||||
info.savedAt = root.value("savedAt", "");
|
||||
return info;
|
||||
@@ -1924,6 +1750,9 @@ namespace FRG
|
||||
}
|
||||
try {
|
||||
nlohmann::json root = nlohmann::json::parse(content);
|
||||
// Every campaign is built at startup, so loading only switches to the save's
|
||||
// campaign (its locations and quest definitions) before applying the state.
|
||||
activateCampaign(root.value("campaignId", std::string(CampaignRegistry::kDefaultCampaignId)));
|
||||
gameState.load(root);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[save] Failed to parse save file " << path << ": " << e.what() << std::endl;
|
||||
|
||||
+25
-10
@@ -24,7 +24,11 @@
|
||||
#include "GameState.h"
|
||||
namespace FRG {
|
||||
|
||||
struct LocationConfig;
|
||||
struct CampaignConfig;
|
||||
|
||||
struct SaveSlotInfo {
|
||||
std::string campaignId;
|
||||
std::string locationName;
|
||||
std::string savedAt;
|
||||
bool empty = true;
|
||||
@@ -123,29 +127,40 @@ namespace FRG {
|
||||
void saveGame(int slot);
|
||||
void loadGame(int slot);
|
||||
SaveSlotInfo readSlotInfo(int slot) const;
|
||||
void createLocationsStep1();
|
||||
void createLocationsStep2();
|
||||
void createLocationsStep3();
|
||||
void createLocationsStep4();
|
||||
void createLocationsStep5();
|
||||
// Every campaign's locations, all built at startup (campaign id -> locations).
|
||||
// gameState.locations is a copy of the active campaign's entry.
|
||||
std::unordered_map<std::string, LocationMap> campaignLocations;
|
||||
|
||||
// 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);
|
||||
// Queues one loading step per location of `campaign`, plus its wiring step.
|
||||
void queueCampaignBuildSteps(const CampaignConfig& campaign);
|
||||
// Makes `campaignId` the played campaign: its locations and its quest definitions.
|
||||
void activateCampaign(const std::string& campaignId);
|
||||
const CampaignConfig& activeCampaign() const;
|
||||
|
||||
void createShadowMapAndProparateToLocations();
|
||||
void destroyShadowMapAndProparateToLocations();
|
||||
|
||||
// Reset entry point used by "Start New Game": loads resources/config/start_state.json
|
||||
// into the existing Location objects if present, else falls back to performFullReset().
|
||||
// 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().
|
||||
// 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.
|
||||
void performFullReset();
|
||||
// Applies MenuManager::tutorialEnabledSetting to the freshly reset state (new games only).
|
||||
// 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
|
||||
// createLocations() wires up but that get consumed (set to nullptr) once used
|
||||
// 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 rearmDormOneShotCallbacks();
|
||||
void rearmOneShotCallbacks(LocationMap& locations);
|
||||
|
||||
int64_t getSyncTimeMs();
|
||||
void processTickCount();
|
||||
|
||||
@@ -36,6 +36,7 @@ void GameState::save(nlohmann::json& out) const
|
||||
out["savedAt"] = ts.str();
|
||||
|
||||
// --- World ---
|
||||
out["campaignId"] = campaignId;
|
||||
out["currentLocationName"] = currentLocationName;
|
||||
out["isDarklands"] = isDarklands;
|
||||
out["isNight"] = isNight;
|
||||
@@ -104,6 +105,8 @@ void GameState::load(const nlohmann::json& in)
|
||||
//logger() << "GameState::load step 1" << std::endl;
|
||||
|
||||
// --- World ---
|
||||
// Literal default: saves made before campaigns existed belong to the main game.
|
||||
campaignId = in.value("campaignId", std::string("main"));
|
||||
currentLocationName = in.value("currentLocationName", currentLocationName);
|
||||
isDarklands = in.value("isDarklands", isDarklands);
|
||||
isNight = in.value("isNight", isNight);
|
||||
|
||||
+5
-1
@@ -32,9 +32,13 @@ struct StoredChatMessage {
|
||||
bool incoming;
|
||||
};
|
||||
|
||||
using LocationMap = std::unordered_map<std::string, std::shared_ptr<Location>>;
|
||||
|
||||
struct GameState : public ISaveable {
|
||||
// --- World ---
|
||||
std::unordered_map<std::string, std::shared_ptr<Location>> locations;
|
||||
// Campaign being played (see CampaignRegistry); `locations` holds that campaign's locations.
|
||||
std::string campaignId = "main";
|
||||
LocationMap locations;
|
||||
std::string currentLocationName;
|
||||
bool isDarklands = false;
|
||||
bool isNight = false;
|
||||
|
||||
+61
-61
@@ -2,6 +2,7 @@
|
||||
#include "Game.h"
|
||||
#include "Localization.h"
|
||||
#include "items/ItemRegistry.h"
|
||||
#include "Campaign.h"
|
||||
#include "render/TextRenderer.h"
|
||||
#include "utils/Utils.h"
|
||||
#include <iostream>
|
||||
@@ -323,6 +324,7 @@ namespace FRG {
|
||||
settingsGraphicsScreenRoot = loadLocalizedUi("resources/w/ui/screen_settings_graphics.json", zipFile);
|
||||
languageScreenRoot = loadLocalizedUi("resources/w/ui/screen_language.json", zipFile);
|
||||
loadSavedGameScreenRoot = loadLocalizedUi("resources/w/ui/screen_load.json", zipFile);
|
||||
campaignScreenRoot = loadLocalizedUi("resources/w/ui/screen_campaign.json", zipFile);
|
||||
saveGameScreenRoot = loadLocalizedUi("resources/w/ui/screen_save.json", zipFile);
|
||||
}
|
||||
|
||||
@@ -337,7 +339,7 @@ namespace FRG {
|
||||
texItemSelected_ = renderer.textureManager.LoadFromPng("resources/w/ui/img/journal/ButtonBkg001.png", zipFile, true);
|
||||
texItemTransparent_ = renderer.textureManager.LoadFromPng("resources/w/ui/img/journal/ButtonBkgTransparent001.png", zipFile, true);
|
||||
|
||||
gameState_.questJournal.loadFromFile("resources/config/quests.json", zipFile);
|
||||
// Quest definitions are loaded per campaign by Game (see Game::activateCampaign).
|
||||
|
||||
toastTextureQuestNew = loadLocalizedTexture("resources/w/ui/img/toast/quest_new001.png", zipFile);
|
||||
toastTextureQuestCompleted = loadLocalizedTexture("resources/w/ui/img/toast/quest_completed001.png", zipFile);
|
||||
@@ -376,18 +378,19 @@ namespace FRG {
|
||||
showMainMenu();
|
||||
}
|
||||
|
||||
void MenuManager::startNewGame(const std::string& campaignId) {
|
||||
// The reset runs on a later main-thread task, which then shows the HUD itself;
|
||||
// the main menu stays up until then. Showing the HUD now would draw one frame
|
||||
// from the previous game's state (e.g. its phone/journal buttons).
|
||||
if (startGameFunc) startGameFunc(campaignId);
|
||||
}
|
||||
|
||||
void MenuManager::enterGameplay(bool isFromLoad) {
|
||||
if (isFromLoad) {
|
||||
uiManager.clearMenuStack();
|
||||
topUiManager.clearMenuStack();
|
||||
}
|
||||
// A new game resets the state on a later main-thread task, which then shows the
|
||||
// HUD itself; the main menu stays up until then. Showing the HUD now would draw
|
||||
// one frame from the previous game's state (e.g. its phone/journal buttons).
|
||||
if (!isFromLoad && uiState_ == GameUiState::MainMenu && startGameFunc)
|
||||
startGameFunc();
|
||||
else
|
||||
showGameplayHud();
|
||||
showGameplayHud();
|
||||
}
|
||||
|
||||
std::shared_ptr<UiNode> MenuManager::gameplayHudRoot() const {
|
||||
@@ -472,8 +475,7 @@ namespace FRG {
|
||||
|
||||
uiManager.setTextButtonCallback("menuStartButton", [this](const std::string&) {
|
||||
audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg");
|
||||
enterGameplay();
|
||||
|
||||
showCampaignScreen();
|
||||
});
|
||||
uiManager.setTextButtonCallback("menuAboutButton", [this](const std::string&) {
|
||||
showAboutScreen();
|
||||
@@ -534,8 +536,7 @@ namespace FRG {
|
||||
|
||||
uiManager.setTextButtonCallback("menuStartButton", [this](const std::string&) {
|
||||
audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg");
|
||||
enterGameplay();
|
||||
|
||||
showCampaignScreen();
|
||||
});
|
||||
uiManager.setTextButtonCallback("menuAboutButton", [this](const std::string&) {
|
||||
showAboutScreen();
|
||||
@@ -1034,7 +1035,8 @@ namespace FRG {
|
||||
|
||||
steps.push_back([this]() {
|
||||
ItemRegistry::instance().loadFromJson("resources/config/items.json", CONST_ZIP_FILE);
|
||||
gameState_.questJournal.reloadDefinitionsPreservingState("resources/config/quests.json", CONST_ZIP_FILE);
|
||||
if (const CampaignConfig* campaign = CampaignRegistry::instance().find(gameState_.campaignId))
|
||||
gameState_.questJournal.reloadDefinitionsPreservingState(campaign->questsPath, CONST_ZIP_FILE);
|
||||
});
|
||||
|
||||
steps.push_back([this]() {
|
||||
@@ -1074,22 +1076,7 @@ namespace FRG {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
int slot = i + 1;
|
||||
if (getSlotInfoFunc) {
|
||||
SaveSlotInfo info = getSlotInfoFunc(slot);
|
||||
|
||||
std::string emptyText;
|
||||
if (FRG::g_currentLanguage == FRG::Language::Russian)
|
||||
{
|
||||
emptyText = EMPTY_LANGUAGE_RU;
|
||||
}
|
||||
else if (FRG::g_currentLanguage == FRG::Language::English)
|
||||
{
|
||||
emptyText = EMPTY_LANGUAGE_EN;
|
||||
}
|
||||
|
||||
std::string label = info.empty
|
||||
? emptyText
|
||||
: localizedLocationName[info.locationName][FRG::g_currentLanguage] + " " + info.savedAt;
|
||||
uiManager.setTextButtonText(kSlotButtons[i], label);
|
||||
uiManager.setTextButtonText(kSlotButtons[i], slotLabel(getSlotInfoFunc(slot)));
|
||||
}
|
||||
uiManager.setTextButtonCallback(kSlotButtons[i], [this, slot](const std::string&) {
|
||||
audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg");
|
||||
@@ -1099,6 +1086,47 @@ namespace FRG {
|
||||
}
|
||||
}
|
||||
|
||||
std::string MenuManager::slotLabel(const SaveSlotInfo& info) {
|
||||
if (info.empty)
|
||||
return FRG::g_currentLanguage == FRG::Language::English ? EMPTY_LANGUAGE_EN : EMPTY_LANGUAGE_RU;
|
||||
|
||||
std::string label = localizedLocationName[info.locationName][FRG::g_currentLanguage] + " " + info.savedAt;
|
||||
// Name the campaign unless it's the main game, so saves of different campaigns are told apart.
|
||||
const CampaignConfig* campaign = CampaignRegistry::instance().find(info.campaignId);
|
||||
if (campaign && campaign->id != CampaignRegistry::kDefaultCampaignId)
|
||||
label = campaign->title(FRG::g_currentLanguage) + ": " + label;
|
||||
return label;
|
||||
}
|
||||
|
||||
void MenuManager::showCampaignScreen()
|
||||
{
|
||||
uiManager.pushMenuFromSavedRoot(campaignScreenRoot);
|
||||
uiManager.setTextButtonCallback("campaignBackButton", [this](const std::string&) {
|
||||
uiManager.popMenu();
|
||||
uiManager.updateAllLayouts();
|
||||
audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg");
|
||||
});
|
||||
|
||||
// One button per campaign in campaigns.json order; spare buttons are hidden.
|
||||
static const char* kCampaignButtons[4] = {
|
||||
"campaignSlot1Button", "campaignSlot2Button", "campaignSlot3Button", "campaignSlot4Button"
|
||||
};
|
||||
const auto& campaigns = CampaignRegistry::instance().all();
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const bool used = i < static_cast<int>(campaigns.size());
|
||||
uiManager.setNodeVisible(kCampaignButtons[i], used);
|
||||
if (!used) continue;
|
||||
|
||||
const std::string campaignId = campaigns[i].id;
|
||||
uiManager.setTextButtonText(kCampaignButtons[i], campaigns[i].title(FRG::g_currentLanguage));
|
||||
uiManager.setTextButtonCallback(kCampaignButtons[i], [this, campaignId](const std::string&) {
|
||||
audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg");
|
||||
uiManager.popMenu();
|
||||
startNewGame(campaignId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void MenuManager::showSaveGameScreen()
|
||||
{
|
||||
uiManager.pushMenuFromSavedRoot(saveGameScreenRoot);
|
||||
@@ -1115,42 +1143,12 @@ namespace FRG {
|
||||
int slot = i + 1;
|
||||
const char* buttonName = kSlotButtons[i];
|
||||
if (getSlotInfoFunc) {
|
||||
SaveSlotInfo info = getSlotInfoFunc(slot);
|
||||
|
||||
std::string emptyText;
|
||||
if (FRG::g_currentLanguage == FRG::Language::Russian)
|
||||
{
|
||||
emptyText = EMPTY_LANGUAGE_RU;
|
||||
}
|
||||
else if (FRG::g_currentLanguage == FRG::Language::English)
|
||||
{
|
||||
emptyText = EMPTY_LANGUAGE_EN;
|
||||
}
|
||||
|
||||
std::string label = info.empty
|
||||
? emptyText
|
||||
: localizedLocationName[info.locationName][FRG::g_currentLanguage] + " " + info.savedAt;
|
||||
uiManager.setTextButtonText(buttonName, label);
|
||||
uiManager.setTextButtonText(buttonName, slotLabel(getSlotInfoFunc(slot)));
|
||||
}
|
||||
uiManager.setTextButtonCallback(buttonName, [this, slot, buttonName](const std::string&) {
|
||||
if (onSaveGame) onSaveGame(slot);
|
||||
if (getSlotInfoFunc) {
|
||||
SaveSlotInfo info = getSlotInfoFunc(slot);
|
||||
|
||||
std::string emptyText;
|
||||
if (FRG::g_currentLanguage == FRG::Language::Russian)
|
||||
{
|
||||
emptyText = EMPTY_LANGUAGE_RU;
|
||||
}
|
||||
else if (FRG::g_currentLanguage == FRG::Language::English)
|
||||
{
|
||||
emptyText = EMPTY_LANGUAGE_EN;
|
||||
}
|
||||
|
||||
std::string label = info.empty
|
||||
? emptyText
|
||||
: localizedLocationName[info.locationName][FRG::g_currentLanguage] + " " + info.savedAt;
|
||||
uiManager.setTextButtonText(buttonName, label);
|
||||
uiManager.setTextButtonText(buttonName, slotLabel(getSlotInfoFunc(slot)));
|
||||
audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg");
|
||||
}
|
||||
});
|
||||
@@ -2142,6 +2140,8 @@ namespace FRG {
|
||||
|
||||
void MenuManager::onEnemyKilledInUniInterior()
|
||||
{
|
||||
// Every location's enemies report here; only the university interior drives the tutorial.
|
||||
if (gameState_.currentLocationName != "uni_interior") return;
|
||||
// The last tutorial step; like the attack hint, also ends the earlier darklands hints.
|
||||
if (gameState_.tutorialStep >= TutorialStep::DarklandsHealth)
|
||||
gameState_.tutorialStep = TutorialStep::Completed;
|
||||
|
||||
+8
-1
@@ -72,7 +72,8 @@ namespace FRG {
|
||||
void spendMoney(int amount);
|
||||
int getMoney() const { return gameState_.money; }
|
||||
|
||||
std::function<void()> startGameFunc;
|
||||
// Starts a new game of the given campaign (see CampaignRegistry).
|
||||
std::function<void(const std::string& campaignId)> startGameFunc;
|
||||
std::function<void(const std::string&)> startDialogueFunc;
|
||||
std::function<void()> startDarklandsTransitionFunc;
|
||||
std::function<void()> startNightTransitionFunc;
|
||||
@@ -164,6 +165,7 @@ namespace FRG {
|
||||
GameState& gameState_;
|
||||
AudioPlayerAsync& audioPlayer_;
|
||||
|
||||
void startNewGame(const std::string& campaignId);
|
||||
void enterGameplay(bool isFromLoad = false);
|
||||
// HUD layout for the current location and darklands mode.
|
||||
std::shared_ptr<UiNode> gameplayHudRoot() const;
|
||||
@@ -212,6 +214,10 @@ namespace FRG {
|
||||
void showLanguageScreen();
|
||||
void showLoadGameScreen();
|
||||
void showSaveGameScreen();
|
||||
// Campaign list opened by "Start game".
|
||||
void showCampaignScreen();
|
||||
// Text of a load/save slot button.
|
||||
std::string slotLabel(const SaveSlotInfo& info);
|
||||
|
||||
// Toast internals
|
||||
struct ToastEntry {
|
||||
@@ -292,6 +298,7 @@ namespace FRG {
|
||||
std::shared_ptr<UiNode> settingsGraphicsScreenRoot;
|
||||
std::shared_ptr<UiNode> languageScreenRoot;
|
||||
std::shared_ptr<UiNode> loadSavedGameScreenRoot;
|
||||
std::shared_ptr<UiNode> campaignScreenRoot;
|
||||
std::shared_ptr<UiNode> saveGameScreenRoot;
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user