working on campaign
This commit is contained in:
1 parent
9fa120d4ad
commit
d38a75ea72
24 files changed
+919
-400
No files matched your search
@@ -112,6 +112,7 @@ add_library(main SHARED
|
|||||||
../../../../src/items/Item.cpp
|
../../../../src/items/Item.cpp
|
||||||
../../../../src/items/ItemRegistry.h
|
../../../../src/items/ItemRegistry.h
|
||||||
../../../../src/items/ItemRegistry.cpp
|
../../../../src/items/ItemRegistry.cpp
|
||||||
|
../../../../src/Campaign.cpp
|
||||||
../../../../src/items/InteractiveObject.h
|
../../../../src/items/InteractiveObject.h
|
||||||
../../../../src/items/InteractiveObject.cpp
|
../../../../src/items/InteractiveObject.cpp
|
||||||
../../../../src/items/InteractiveObjectState.h
|
../../../../src/items/InteractiveObjectState.h
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ add_executable(ShadowOverBishkekDemo
|
|||||||
../src/items/Item.cpp
|
../src/items/Item.cpp
|
||||||
../src/items/ItemRegistry.h
|
../src/items/ItemRegistry.h
|
||||||
../src/items/ItemRegistry.cpp
|
../src/items/ItemRegistry.cpp
|
||||||
|
../src/Campaign.cpp
|
||||||
../src/items/InteractiveObject.h
|
../src/items/InteractiveObject.h
|
||||||
../src/items/InteractiveObject.cpp
|
../src/items/InteractiveObject.cpp
|
||||||
../src/items/InteractiveObjectState.h
|
../src/items/InteractiveObjectState.h
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ set(SOURCES
|
|||||||
../src/items/Item.cpp
|
../src/items/Item.cpp
|
||||||
../src/items/ItemRegistry.h
|
../src/items/ItemRegistry.h
|
||||||
../src/items/ItemRegistry.cpp
|
../src/items/ItemRegistry.cpp
|
||||||
|
../src/Campaign.cpp
|
||||||
../src/items/InteractiveObject.h
|
../src/items/InteractiveObject.h
|
||||||
../src/items/InteractiveObject.cpp
|
../src/items/InteractiveObject.cpp
|
||||||
../src/items/InteractiveObjectState.h
|
../src/items/InteractiveObjectState.h
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ add_executable(ShadowOverBishkekDemo WIN32
|
|||||||
../src/items/Item.cpp
|
../src/items/Item.cpp
|
||||||
../src/items/ItemRegistry.h
|
../src/items/ItemRegistry.h
|
||||||
../src/items/ItemRegistry.cpp
|
../src/items/ItemRegistry.cpp
|
||||||
|
../src/Campaign.cpp
|
||||||
../src/items/InteractiveObject.h
|
../src/items/InteractiveObject.h
|
||||||
../src/items/InteractiveObject.cpp
|
../src/items/InteractiveObject.cpp
|
||||||
../src/items/InteractiveObjectState.h
|
../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
|
||||||
+157
-328
@@ -4,6 +4,7 @@
|
|||||||
#include "utils/Utils.h"
|
#include "utils/Utils.h"
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include "items/ItemRegistry.h"
|
#include "items/ItemRegistry.h"
|
||||||
|
#include "Campaign.h"
|
||||||
#include "render/OpenGlExtensions.h"
|
#include "render/OpenGlExtensions.h"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include "render/TextureManager.h"
|
#include "render/TextureManager.h"
|
||||||
@@ -326,23 +327,17 @@ namespace FRG
|
|||||||
Character::nameTranslations.loadFromFile("resources/config/translate_names.json");
|
Character::nameTranslations.loadFromFile("resources/config/translate_names.json");
|
||||||
});
|
});
|
||||||
|
|
||||||
// createLocations() is also used by the full-reset path (see performFullReset()),
|
// Every campaign's locations are built up front (one loading step per location),
|
||||||
// so it stays a single synchronous function rather than being split into
|
// so starting or loading any campaign later only switches the active set.
|
||||||
// per-location queue steps; it's queued here as one coarse step.
|
CampaignRegistry::instance().loadFromJson("resources/config/campaigns.json", CONST_ZIP_FILE);
|
||||||
loadSteps.push([this]() {
|
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]() {
|
loadSteps.push([this]() {
|
||||||
createLocationsStep2();
|
activateCampaign(CampaignRegistry::kDefaultCampaignId);
|
||||||
});
|
|
||||||
loadSteps.push([this]() {
|
|
||||||
createLocationsStep3();
|
|
||||||
});
|
|
||||||
loadSteps.push([this]() {
|
|
||||||
createLocationsStep4();
|
|
||||||
});
|
|
||||||
loadSteps.push([this]() {
|
|
||||||
createLocationsStep5();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
loadSteps.push([this]() {
|
loadSteps.push([this]() {
|
||||||
@@ -380,6 +375,12 @@ namespace FRG
|
|||||||
menuManager.skipCutsceneFunc = [this]() {
|
menuManager.skipCutsceneFunc = [this]() {
|
||||||
if (currentLocation()) currentLocation()->dialogueSystem.skipCutscene();
|
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]() {
|
loadSteps.push([this]() {
|
||||||
@@ -391,15 +392,17 @@ namespace FRG
|
|||||||
std::cerr << "Failed to load UI: " << e.what() << std::endl;
|
std::cerr << "Failed to load UI: " << e.what() << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
menuManager.startGameFunc = [this]() {
|
menuManager.startGameFunc = [this](const std::string& campaignId) {
|
||||||
mainThreadHandler.EnqueueMainThreadTask([this]() {
|
mainThreadHandler.EnqueueMainThreadTask([this, campaignId]() {
|
||||||
|
activateCampaign(campaignId);
|
||||||
performResetToInitialState();
|
performResetToInitialState();
|
||||||
applyTutorialSetting();
|
applyTutorialSetting();
|
||||||
gameState.currentLocationName = "location_dorm";
|
const CampaignConfig& campaign = activeCampaign();
|
||||||
|
gameState.currentLocationName = campaign.startLocation;
|
||||||
menuManager.showGameplayHud(); // first HUD frame drawn from the fresh state
|
menuManager.showGameplayHud(); // first HUD frame drawn from the fresh state
|
||||||
currentLocation()->scriptEngine.callLocationEnterCallback();
|
if (currentLocation()) currentLocation()->scriptEngine.callLocationEnterCallback();
|
||||||
this->audioPlayer->crossFadeMusicAsync("music/obshaga.ogg");
|
audioPlayer->crossFadeMusicAsync(campaign.startMusic);
|
||||||
menuManager.lastMusicTrack = "music/obshaga.ogg";
|
menuManager.lastMusicTrack = campaign.startMusic;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -466,14 +469,50 @@ namespace FRG
|
|||||||
loadStepsDone = 0;
|
loadStepsDone = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::rearmDormOneShotCallbacks() {
|
void Game::rearmOneShotCallbacks(LocationMap& locations) {
|
||||||
auto it = gameState.locations.find("location_dorm");
|
for (auto& [name, loc] : locations) {
|
||||||
if (it == gameState.locations.end() || !it->second) return;
|
loc->onPlayerTaxiRequired = [this]() {
|
||||||
|
|
||||||
it->second->onPlayerTaxiRequired = [this]() {
|
|
||||||
menuManager.tutorialShowTaxiHint();
|
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() {
|
void Game::performFullReset() {
|
||||||
gameState.currentLocationName.clear();
|
gameState.currentLocationName.clear();
|
||||||
@@ -485,6 +524,7 @@ namespace FRG
|
|||||||
gameState.money = 5500;
|
gameState.money = 5500;
|
||||||
gameState.globalInts.clear();
|
gameState.globalInts.clear();
|
||||||
gameState.globalFloats.clear();
|
gameState.globalFloats.clear();
|
||||||
|
gameState.globalFloats["player_hp"] = 200;
|
||||||
gameState.tutorialStep = TutorialStep::AdvanceDialogue;
|
gameState.tutorialStep = TutorialStep::AdvanceDialogue;
|
||||||
gameState.tutorialPhoneChatScreenOpened = false;
|
gameState.tutorialPhoneChatScreenOpened = false;
|
||||||
gameState.tutorialJournalScreenOpened = false;
|
gameState.tutorialJournalScreenOpened = false;
|
||||||
@@ -495,21 +535,31 @@ namespace FRG
|
|||||||
gameState.chatHistory[i].clear();
|
gameState.chatHistory[i].clear();
|
||||||
}
|
}
|
||||||
gameState.inventory.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.
|
// from this freshly-rebuilt state after changing gameplay/content JSON or Lua.
|
||||||
// Re-comment before shipping.
|
// Re-comment before shipping.
|
||||||
//saveInitialState();
|
//saveInitialState();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::performResetToInitialState() {
|
void Game::performResetToInitialState() {
|
||||||
const std::string path = "resources/config/start_state.json";
|
const std::string path = activeCampaign().startStatePath;
|
||||||
|
|
||||||
std::vector<char> fileData;
|
std::vector<char> fileData;
|
||||||
if (strlen(CONST_ZIP_FILE) > 0)
|
if (strlen(CONST_ZIP_FILE) > 0)
|
||||||
@@ -528,6 +578,7 @@ namespace FRG
|
|||||||
if (!content.empty()) {
|
if (!content.empty()) {
|
||||||
try {
|
try {
|
||||||
nlohmann::json root = nlohmann::json::parse(content);
|
nlohmann::json root = nlohmann::json::parse(content);
|
||||||
|
const std::string campaignId = gameState.campaignId;
|
||||||
// GameState::load() merges into existing maps rather than replacing them,
|
// GameState::load() merges into existing maps rather than replacing them,
|
||||||
// so any Lua globals set during play that aren't in the pristine snapshot
|
// so any Lua globals set during play that aren't in the pristine snapshot
|
||||||
// must be cleared explicitly before loading it.
|
// must be cleared explicitly before loading it.
|
||||||
@@ -537,11 +588,12 @@ namespace FRG
|
|||||||
// destruction, no re-parsing JSON/nav files, no new
|
// destruction, no re-parsing JSON/nav files, no new
|
||||||
// sol2 state (GameState::load only touches map entries
|
// sol2 state (GameState::load only touches map entries
|
||||||
// that already exist).
|
// that already exist).
|
||||||
|
gameState.campaignId = campaignId; // the snapshot may predate campaigns
|
||||||
// Locations survive this reset, so any one-shot callbacks they consumed
|
// Locations survive this reset, so any one-shot callbacks they consumed
|
||||||
// during the previous playthrough (taxi request) must be
|
// during the previous playthrough (taxi request) must be
|
||||||
// re-armed here -- createLocations() normally does this, but doesn't run
|
// re-armed here -- wireCampaignLocations() normally does this, but doesn't
|
||||||
// on this fast path.
|
// run on this fast path.
|
||||||
rearmDormOneShotCallbacks();
|
rearmOneShotCallbacks(gameState.locations);
|
||||||
return;
|
return;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
logger() << "[reset] Failed to parse " << path << ": " << e.what()
|
logger() << "[reset] Failed to parse " << path << ": " << e.what()
|
||||||
@@ -555,17 +607,19 @@ namespace FRG
|
|||||||
performFullReset();
|
performFullReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs after either reset path. start_state.json always holds the start of the
|
// Runs after either reset path. Start states always hold the start of the tutorial
|
||||||
// tutorial; a disabled tutorial is simply one that is already completed.
|
// (if the campaign has one); a disabled tutorial is simply one that is already completed.
|
||||||
void Game::applyTutorialSetting() {
|
void Game::applyTutorialSetting() {
|
||||||
if (!menuManager.tutorialEnabledSetting)
|
if (!activeCampaign().tutorial || !menuManager.tutorialEnabledSetting)
|
||||||
gameState.tutorialStep = TutorialStep::Completed;
|
gameState.tutorialStep = TutorialStep::Completed;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::saveInitialState() {
|
void Game::saveInitialState() {
|
||||||
nlohmann::json root;
|
nlohmann::json root;
|
||||||
gameState.save(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;
|
logger() << "[reset] saveInitialState called, to " << path << std::endl;
|
||||||
|
|
||||||
std::ofstream file(path);
|
std::ofstream file(path);
|
||||||
@@ -578,309 +632,80 @@ namespace FRG
|
|||||||
saveJsonToFile(root, path);
|
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;
|
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) {
|
||||||
void Game::createLocationsStep2()
|
gameState.isNight = isNight;
|
||||||
{
|
gameState.isDawn = isDawn;
|
||||||
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
|
|
||||||
};
|
};
|
||||||
|
loc->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
|
||||||
|
loc->requestAdvanceDarklandsHud = [this]() { menuManager.advanceUniIntDarklandsHud(); };
|
||||||
|
loc->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); };
|
||||||
/*
|
loc->requestReturnToMainMenu = [this]() {
|
||||||
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]() {
|
|
||||||
gameState.currentLocationName.clear();
|
gameState.currentLocationName.clear();
|
||||||
menuManager.showMenuAfterGameCompleted();
|
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);
|
menuManager.setChatUnread(idx, unread, msg);
|
||||||
};
|
};
|
||||||
if (gameState.locations["uni_interior"]->player)
|
if (loc->player)
|
||||||
gameState.locations["uni_interior"]->player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
loc->player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
||||||
for (auto& npc : gameState.locations["uni_interior"]->npcs) {
|
for (auto& npc : loc->npcs) {
|
||||||
if (npc && npc->state.canAttack) {
|
if (npc && npc->state.canAttack) {
|
||||||
npc->state.onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); };
|
npc->state.onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
loc->requestDarklandsPlayBattleMusic = [this]() {
|
||||||
gameState.locations["uni_interior"]->requestDarklandsPlayBattleMusic = [this]() {
|
|
||||||
audioPlayer->crossFadeMusicFromPositionAsync("music/bishkek fight.ogg");
|
audioPlayer->crossFadeMusicFromPositionAsync("music/bishkek fight.ogg");
|
||||||
menuManager.lastMusicTrack = "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");
|
audioPlayer->crossFadeMusicFromPositionAsync("music/bishkek fight calm.ogg");
|
||||||
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
|
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
if (config.lockInteractiveObjectsDuringTutorial) {
|
||||||
|
|
||||||
|
|
||||||
void Game::createLocationsStep3()
|
|
||||||
{
|
|
||||||
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".
|
// Objects can be picked up once the tutorial reaches "pick up an item".
|
||||||
gameState.locations["location_dorm"]->areInteractiveObjectsLocked = [this]() {
|
loc->areInteractiveObjectsLocked = [this]() {
|
||||||
return gameState.tutorialStep < TutorialStep::PickUpItem;
|
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()
|
if (config.taxi) {
|
||||||
{
|
const LocationConfig::Taxi& taxi = *config.taxi;
|
||||||
|
loc->state.taxiCarTriggerPosition = taxi.triggerPosition;
|
||||||
rearmDormOneShotCallbacks();
|
loc->state.taxiCarWaypoints = taxi.waypoints;
|
||||||
|
loc->state.taxiCarInitialState.position = taxi.initialPosition;
|
||||||
menuManager.callTaxiFunc = [this]()
|
loc->state.taxiCarInitialState.rotation = taxi.initialRotation;
|
||||||
{
|
loc->state.taxiCarInitialState.mode = NpcCar::Mode::NONE;
|
||||||
if (gameState.locations["location_dorm"])
|
loc->state.taxiCarInitialState.velocity = 0.f;
|
||||||
{
|
loc->state.taxiCarInitialState.steeringAngle = 0.f;
|
||||||
gameState.locations["location_dorm"]->onPlayerTaxiRequired = nullptr;
|
loc->state.taxiCarInitialState.waypointReachRadius = taxi.waypointReachRadius;
|
||||||
|
loc->state.taxiCar = loc->state.taxiCarInitialState;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentLocation())
|
return loc;
|
||||||
{
|
|
||||||
currentLocation()->onTaxiCalled();
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
void Game::wireCampaignLocations(LocationMap& locations)
|
||||||
|
{
|
||||||
|
rearmOneShotCallbacks(locations);
|
||||||
|
|
||||||
// Teleport callbacks: destination name and position come from the teleport zone data.
|
// 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) {
|
auto teleportCallback = [this](const std::string& destName, const Eigen::Vector3f& destPos, float destRotY) {
|
||||||
@@ -918,19 +743,19 @@ namespace FRG
|
|||||||
menuManager.onLocationChanged(destName);
|
menuManager.onLocationChanged(destName);
|
||||||
};
|
};
|
||||||
|
|
||||||
gameState.locations["uni_exterior"]->onTeleport = teleportCallback;
|
for (auto& [name, loc] : locations) {
|
||||||
gameState.locations["uni_interior"]->onTeleport = teleportCallback;
|
loc->onTeleport = teleportCallback;
|
||||||
gameState.locations["location_dorm"]->onTeleport = teleportCallback;
|
}
|
||||||
|
|
||||||
// Share the global int store with all dialogue runtimes so flags set via
|
// Share the global int store with all dialogue runtimes so flags set via
|
||||||
// dialogue JSON and via Lua set_dialogue_flag all see the same state.
|
// dialogue JSON and via Lua set_dialogue_flag all see the same state.
|
||||||
for (auto& [name, loc] : gameState.locations) {
|
for (auto& [name, loc] : locations) {
|
||||||
loc->dialogueSystem.setGlobalFlagStore(&gameState.globalInts);
|
loc->dialogueSystem.setGlobalFlagStore(&gameState.globalInts);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wire tutorial advance callbacks for all locations.
|
// Wire tutorial advance callbacks for all locations.
|
||||||
// completeTutorialStep() only acts on the current step, so sharing is safe.
|
// completeTutorialStep() only acts on the current step, so sharing is safe.
|
||||||
for (auto& [name, loc] : gameState.locations) {
|
for (auto& [name, loc] : locations) {
|
||||||
loc->dialogueSystem.setOnDialogueAdvanced([this]() {
|
loc->dialogueSystem.setOnDialogueAdvanced([this]() {
|
||||||
gameState.completeTutorialStep(TutorialStep::AdvanceDialogue);
|
gameState.completeTutorialStep(TutorialStep::AdvanceDialogue);
|
||||||
});
|
});
|
||||||
@@ -941,14 +766,14 @@ namespace FRG
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wire chat-bubble callback so dynamic bubbles appear as dialogue lines are shown.
|
// Wire chat-bubble callback so dynamic bubbles appear as dialogue lines are shown.
|
||||||
for (auto& [name, loc] : gameState.locations) {
|
for (auto& [name, loc] : locations) {
|
||||||
loc->dialogueSystem.setOnChatBubbleReady([this](const std::string& text, bool incoming) {
|
loc->dialogueSystem.setOnChatBubbleReady([this](const std::string& text, bool incoming) {
|
||||||
menuManager.onChatBubbleReady(text, incoming);
|
menuManager.onChatBubbleReady(text, incoming);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wire cutscene HUD: show skip button when cutscene starts, restore HUD when it ends.
|
// Wire cutscene HUD: show skip button when cutscene starts, restore HUD when it ends.
|
||||||
for (auto& [name, loc] : gameState.locations) {
|
for (auto& [name, loc] : locations) {
|
||||||
loc->dialogueSystem.setOnCutsceneStarted([this]() {
|
loc->dialogueSystem.setOnCutsceneStarted([this]() {
|
||||||
menuManager.onCutsceneStarted();
|
menuManager.onCutsceneStarted();
|
||||||
});
|
});
|
||||||
@@ -958,7 +783,7 @@ namespace FRG
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wire HP-change callbacks so all player instances update the health bar HUD.
|
// 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) {
|
if (loc->player) {
|
||||||
loc->player->state.onHpChanged = [this, loc](float hp, float maxHp, int attackerIndex) {
|
loc->player->state.onHpChanged = [this, loc](float hp, float maxHp, int attackerIndex) {
|
||||||
menuManager.updateHealthBar(hp, maxHp);
|
menuManager.updateHealthBar(hp, maxHp);
|
||||||
@@ -999,8 +824,8 @@ namespace FRG
|
|||||||
CheckGlError(__FILE__, __LINE__);
|
CheckGlError(__FILE__, __LINE__);
|
||||||
logger() << "Shadow map initialized" << std::endl;
|
logger() << "Shadow map initialized" << std::endl;
|
||||||
|
|
||||||
for (auto& [name, loc] : gameState.locations) {
|
for (auto& [campaignId, locations] : campaignLocations) {
|
||||||
loc->shadowMap = shadowMap;
|
for (auto& [name, loc] : locations) loc->shadowMap = shadowMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1011,8 +836,8 @@ namespace FRG
|
|||||||
shadowMap.reset();
|
shadowMap.reset();
|
||||||
CheckGlError(__FILE__, __LINE__);
|
CheckGlError(__FILE__, __LINE__);
|
||||||
|
|
||||||
for (auto& [name, loc] : gameState.locations) {
|
for (auto& [campaignId, locations] : campaignLocations) {
|
||||||
loc->shadowMap.reset();
|
for (auto& [name, loc] : locations) loc->shadowMap.reset();
|
||||||
}
|
}
|
||||||
CheckGlError(__FILE__, __LINE__);
|
CheckGlError(__FILE__, __LINE__);
|
||||||
|
|
||||||
@@ -1891,6 +1716,7 @@ namespace FRG
|
|||||||
nlohmann::json root = nlohmann::json::parse(content);
|
nlohmann::json root = nlohmann::json::parse(content);
|
||||||
SaveSlotInfo info;
|
SaveSlotInfo info;
|
||||||
info.empty = false;
|
info.empty = false;
|
||||||
|
info.campaignId = root.value("campaignId", std::string(CampaignRegistry::kDefaultCampaignId));
|
||||||
info.locationName = root.value("currentLocationName", "");
|
info.locationName = root.value("currentLocationName", "");
|
||||||
info.savedAt = root.value("savedAt", "");
|
info.savedAt = root.value("savedAt", "");
|
||||||
return info;
|
return info;
|
||||||
@@ -1924,6 +1750,9 @@ namespace FRG
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
nlohmann::json root = nlohmann::json::parse(content);
|
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);
|
gameState.load(root);
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cerr << "[save] Failed to parse save file " << path << ": " << e.what() << std::endl;
|
std::cerr << "[save] Failed to parse save file " << path << ": " << e.what() << std::endl;
|
||||||
|
|||||||
+25
-10
@@ -24,7 +24,11 @@
|
|||||||
#include "GameState.h"
|
#include "GameState.h"
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
|
|
||||||
|
struct LocationConfig;
|
||||||
|
struct CampaignConfig;
|
||||||
|
|
||||||
struct SaveSlotInfo {
|
struct SaveSlotInfo {
|
||||||
|
std::string campaignId;
|
||||||
std::string locationName;
|
std::string locationName;
|
||||||
std::string savedAt;
|
std::string savedAt;
|
||||||
bool empty = true;
|
bool empty = true;
|
||||||
@@ -123,29 +127,40 @@ namespace FRG {
|
|||||||
void saveGame(int slot);
|
void saveGame(int slot);
|
||||||
void loadGame(int slot);
|
void loadGame(int slot);
|
||||||
SaveSlotInfo readSlotInfo(int slot) const;
|
SaveSlotInfo readSlotInfo(int slot) const;
|
||||||
void createLocationsStep1();
|
// Every campaign's locations, all built at startup (campaign id -> locations).
|
||||||
void createLocationsStep2();
|
// gameState.locations is a copy of the active campaign's entry.
|
||||||
void createLocationsStep3();
|
std::unordered_map<std::string, LocationMap> campaignLocations;
|
||||||
void createLocationsStep4();
|
|
||||||
void createLocationsStep5();
|
// 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 createShadowMapAndProparateToLocations();
|
||||||
void destroyShadowMapAndProparateToLocations();
|
void destroyShadowMapAndProparateToLocations();
|
||||||
|
|
||||||
// Reset entry point used by "Start New Game": loads resources/config/start_state.json
|
// Reset entry point used by "Start New Game": loads the active campaign's start-state
|
||||||
// into the existing Location objects if present, else falls back to performFullReset().
|
// JSON into its existing Location objects, else falls back to performFullReset().
|
||||||
// Must only ever run from a deferred task (see mainThreadHandler.EnqueueMainThreadTask),
|
// Must only ever run from a deferred task (see mainThreadHandler.EnqueueMainThreadTask),
|
||||||
// never synchronously from a Lua/cutscene call stack.
|
// never synchronously from a Lua/cutscene call stack.
|
||||||
void performResetToInitialState();
|
void performResetToInitialState();
|
||||||
|
// Rebuilds the active campaign's locations from scratch.
|
||||||
void performFullReset();
|
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 applyTutorialSetting();
|
||||||
void saveInitialState();
|
void saveInitialState();
|
||||||
|
|
||||||
// (Re-)arms one-shot gameplay callbacks (taxi request) that
|
// (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,
|
// 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.
|
// since that path keeps the existing Location objects instead of recreating them.
|
||||||
void rearmDormOneShotCallbacks();
|
void rearmOneShotCallbacks(LocationMap& locations);
|
||||||
|
|
||||||
int64_t getSyncTimeMs();
|
int64_t getSyncTimeMs();
|
||||||
void processTickCount();
|
void processTickCount();
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ void GameState::save(nlohmann::json& out) const
|
|||||||
out["savedAt"] = ts.str();
|
out["savedAt"] = ts.str();
|
||||||
|
|
||||||
// --- World ---
|
// --- World ---
|
||||||
|
out["campaignId"] = campaignId;
|
||||||
out["currentLocationName"] = currentLocationName;
|
out["currentLocationName"] = currentLocationName;
|
||||||
out["isDarklands"] = isDarklands;
|
out["isDarklands"] = isDarklands;
|
||||||
out["isNight"] = isNight;
|
out["isNight"] = isNight;
|
||||||
@@ -104,6 +105,8 @@ void GameState::load(const nlohmann::json& in)
|
|||||||
//logger() << "GameState::load step 1" << std::endl;
|
//logger() << "GameState::load step 1" << std::endl;
|
||||||
|
|
||||||
// --- World ---
|
// --- 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);
|
currentLocationName = in.value("currentLocationName", currentLocationName);
|
||||||
isDarklands = in.value("isDarklands", isDarklands);
|
isDarklands = in.value("isDarklands", isDarklands);
|
||||||
isNight = in.value("isNight", isNight);
|
isNight = in.value("isNight", isNight);
|
||||||
|
|||||||
+5
-1
@@ -32,9 +32,13 @@ struct StoredChatMessage {
|
|||||||
bool incoming;
|
bool incoming;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
using LocationMap = std::unordered_map<std::string, std::shared_ptr<Location>>;
|
||||||
|
|
||||||
struct GameState : public ISaveable {
|
struct GameState : public ISaveable {
|
||||||
// --- World ---
|
// --- 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;
|
std::string currentLocationName;
|
||||||
bool isDarklands = false;
|
bool isDarklands = false;
|
||||||
bool isNight = false;
|
bool isNight = false;
|
||||||
|
|||||||
+60
-60
@@ -2,6 +2,7 @@
|
|||||||
#include "Game.h"
|
#include "Game.h"
|
||||||
#include "Localization.h"
|
#include "Localization.h"
|
||||||
#include "items/ItemRegistry.h"
|
#include "items/ItemRegistry.h"
|
||||||
|
#include "Campaign.h"
|
||||||
#include "render/TextRenderer.h"
|
#include "render/TextRenderer.h"
|
||||||
#include "utils/Utils.h"
|
#include "utils/Utils.h"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@@ -323,6 +324,7 @@ namespace FRG {
|
|||||||
settingsGraphicsScreenRoot = loadLocalizedUi("resources/w/ui/screen_settings_graphics.json", zipFile);
|
settingsGraphicsScreenRoot = loadLocalizedUi("resources/w/ui/screen_settings_graphics.json", zipFile);
|
||||||
languageScreenRoot = loadLocalizedUi("resources/w/ui/screen_language.json", zipFile);
|
languageScreenRoot = loadLocalizedUi("resources/w/ui/screen_language.json", zipFile);
|
||||||
loadSavedGameScreenRoot = loadLocalizedUi("resources/w/ui/screen_load.json", zipFile);
|
loadSavedGameScreenRoot = loadLocalizedUi("resources/w/ui/screen_load.json", zipFile);
|
||||||
|
campaignScreenRoot = loadLocalizedUi("resources/w/ui/screen_campaign.json", zipFile);
|
||||||
saveGameScreenRoot = loadLocalizedUi("resources/w/ui/screen_save.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);
|
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);
|
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);
|
toastTextureQuestNew = loadLocalizedTexture("resources/w/ui/img/toast/quest_new001.png", zipFile);
|
||||||
toastTextureQuestCompleted = loadLocalizedTexture("resources/w/ui/img/toast/quest_completed001.png", zipFile);
|
toastTextureQuestCompleted = loadLocalizedTexture("resources/w/ui/img/toast/quest_completed001.png", zipFile);
|
||||||
@@ -376,17 +378,18 @@ namespace FRG {
|
|||||||
showMainMenu();
|
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) {
|
void MenuManager::enterGameplay(bool isFromLoad) {
|
||||||
if (isFromLoad) {
|
if (isFromLoad) {
|
||||||
uiManager.clearMenuStack();
|
uiManager.clearMenuStack();
|
||||||
topUiManager.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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,8 +475,7 @@ namespace FRG {
|
|||||||
|
|
||||||
uiManager.setTextButtonCallback("menuStartButton", [this](const std::string&) {
|
uiManager.setTextButtonCallback("menuStartButton", [this](const std::string&) {
|
||||||
audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg");
|
audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg");
|
||||||
enterGameplay();
|
showCampaignScreen();
|
||||||
|
|
||||||
});
|
});
|
||||||
uiManager.setTextButtonCallback("menuAboutButton", [this](const std::string&) {
|
uiManager.setTextButtonCallback("menuAboutButton", [this](const std::string&) {
|
||||||
showAboutScreen();
|
showAboutScreen();
|
||||||
@@ -534,8 +536,7 @@ namespace FRG {
|
|||||||
|
|
||||||
uiManager.setTextButtonCallback("menuStartButton", [this](const std::string&) {
|
uiManager.setTextButtonCallback("menuStartButton", [this](const std::string&) {
|
||||||
audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg");
|
audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg");
|
||||||
enterGameplay();
|
showCampaignScreen();
|
||||||
|
|
||||||
});
|
});
|
||||||
uiManager.setTextButtonCallback("menuAboutButton", [this](const std::string&) {
|
uiManager.setTextButtonCallback("menuAboutButton", [this](const std::string&) {
|
||||||
showAboutScreen();
|
showAboutScreen();
|
||||||
@@ -1034,7 +1035,8 @@ namespace FRG {
|
|||||||
|
|
||||||
steps.push_back([this]() {
|
steps.push_back([this]() {
|
||||||
ItemRegistry::instance().loadFromJson("resources/config/items.json", CONST_ZIP_FILE);
|
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]() {
|
steps.push_back([this]() {
|
||||||
@@ -1074,22 +1076,7 @@ namespace FRG {
|
|||||||
for (int i = 0; i < 4; ++i) {
|
for (int i = 0; i < 4; ++i) {
|
||||||
int slot = i + 1;
|
int slot = i + 1;
|
||||||
if (getSlotInfoFunc) {
|
if (getSlotInfoFunc) {
|
||||||
SaveSlotInfo info = getSlotInfoFunc(slot);
|
uiManager.setTextButtonText(kSlotButtons[i], slotLabel(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.setTextButtonCallback(kSlotButtons[i], [this, slot](const std::string&) {
|
uiManager.setTextButtonCallback(kSlotButtons[i], [this, slot](const std::string&) {
|
||||||
audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg");
|
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()
|
void MenuManager::showSaveGameScreen()
|
||||||
{
|
{
|
||||||
uiManager.pushMenuFromSavedRoot(saveGameScreenRoot);
|
uiManager.pushMenuFromSavedRoot(saveGameScreenRoot);
|
||||||
@@ -1115,42 +1143,12 @@ namespace FRG {
|
|||||||
int slot = i + 1;
|
int slot = i + 1;
|
||||||
const char* buttonName = kSlotButtons[i];
|
const char* buttonName = kSlotButtons[i];
|
||||||
if (getSlotInfoFunc) {
|
if (getSlotInfoFunc) {
|
||||||
SaveSlotInfo info = getSlotInfoFunc(slot);
|
uiManager.setTextButtonText(buttonName, slotLabel(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.setTextButtonCallback(buttonName, [this, slot, buttonName](const std::string&) {
|
uiManager.setTextButtonCallback(buttonName, [this, slot, buttonName](const std::string&) {
|
||||||
if (onSaveGame) onSaveGame(slot);
|
if (onSaveGame) onSaveGame(slot);
|
||||||
if (getSlotInfoFunc) {
|
if (getSlotInfoFunc) {
|
||||||
SaveSlotInfo info = getSlotInfoFunc(slot);
|
uiManager.setTextButtonText(buttonName, slotLabel(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);
|
|
||||||
audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg");
|
audioPlayer_.playSoundAsync("resources/audio/751089__smallconfusion__mechanical-plastic-click-11.ogg");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2142,6 +2140,8 @@ namespace FRG {
|
|||||||
|
|
||||||
void MenuManager::onEnemyKilledInUniInterior()
|
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.
|
// The last tutorial step; like the attack hint, also ends the earlier darklands hints.
|
||||||
if (gameState_.tutorialStep >= TutorialStep::DarklandsHealth)
|
if (gameState_.tutorialStep >= TutorialStep::DarklandsHealth)
|
||||||
gameState_.tutorialStep = TutorialStep::Completed;
|
gameState_.tutorialStep = TutorialStep::Completed;
|
||||||
|
|||||||
+8
-1
@@ -72,7 +72,8 @@ namespace FRG {
|
|||||||
void spendMoney(int amount);
|
void spendMoney(int amount);
|
||||||
int getMoney() const { return gameState_.money; }
|
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(const std::string&)> startDialogueFunc;
|
||||||
std::function<void()> startDarklandsTransitionFunc;
|
std::function<void()> startDarklandsTransitionFunc;
|
||||||
std::function<void()> startNightTransitionFunc;
|
std::function<void()> startNightTransitionFunc;
|
||||||
@@ -164,6 +165,7 @@ namespace FRG {
|
|||||||
GameState& gameState_;
|
GameState& gameState_;
|
||||||
AudioPlayerAsync& audioPlayer_;
|
AudioPlayerAsync& audioPlayer_;
|
||||||
|
|
||||||
|
void startNewGame(const std::string& campaignId);
|
||||||
void enterGameplay(bool isFromLoad = false);
|
void enterGameplay(bool isFromLoad = false);
|
||||||
// HUD layout for the current location and darklands mode.
|
// HUD layout for the current location and darklands mode.
|
||||||
std::shared_ptr<UiNode> gameplayHudRoot() const;
|
std::shared_ptr<UiNode> gameplayHudRoot() const;
|
||||||
@@ -212,6 +214,10 @@ namespace FRG {
|
|||||||
void showLanguageScreen();
|
void showLanguageScreen();
|
||||||
void showLoadGameScreen();
|
void showLoadGameScreen();
|
||||||
void showSaveGameScreen();
|
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
|
// Toast internals
|
||||||
struct ToastEntry {
|
struct ToastEntry {
|
||||||
@@ -292,6 +298,7 @@ namespace FRG {
|
|||||||
std::shared_ptr<UiNode> settingsGraphicsScreenRoot;
|
std::shared_ptr<UiNode> settingsGraphicsScreenRoot;
|
||||||
std::shared_ptr<UiNode> languageScreenRoot;
|
std::shared_ptr<UiNode> languageScreenRoot;
|
||||||
std::shared_ptr<UiNode> loadSavedGameScreenRoot;
|
std::shared_ptr<UiNode> loadSavedGameScreenRoot;
|
||||||
|
std::shared_ptr<UiNode> campaignScreenRoot;
|
||||||
std::shared_ptr<UiNode> saveGameScreenRoot;
|
std::shared_ptr<UiNode> saveGameScreenRoot;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user