Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b0a18a6cf | ||
|
|
cebbb8bb5d |
No files matched your search
@@ -122,9 +122,6 @@ set(SOURCES
|
|||||||
../src/dialogue/DialogueOverlay.cpp
|
../src/dialogue/DialogueOverlay.cpp
|
||||||
../src/dialogue/DialogueSystem.h
|
../src/dialogue/DialogueSystem.h
|
||||||
../src/dialogue/DialogueSystem.cpp
|
../src/dialogue/DialogueSystem.cpp
|
||||||
../src/quest/QuestTypes.h
|
|
||||||
../src/quest/QuestJournal.h
|
|
||||||
../src/quest/QuestJournal.cpp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(bishkek-witcher ${SOURCES})
|
add_executable(bishkek-witcher ${SOURCES})
|
||||||
|
|||||||
@@ -79,9 +79,6 @@ add_executable(space-game001
|
|||||||
../src/dialogue/DialogueOverlay.cpp
|
../src/dialogue/DialogueOverlay.cpp
|
||||||
../src/dialogue/DialogueSystem.h
|
../src/dialogue/DialogueSystem.h
|
||||||
../src/dialogue/DialogueSystem.cpp
|
../src/dialogue/DialogueSystem.cpp
|
||||||
../src/quest/QuestTypes.h
|
|
||||||
../src/quest/QuestJournal.h
|
|
||||||
../src/quest/QuestJournal.cpp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Установка проекта по умолчанию для Visual Studio
|
# Установка проекта по умолчанию для Visual Studio
|
||||||
|
|||||||
@@ -3,175 +3,472 @@
|
|||||||
"agentRadius": 0.45,
|
"agentRadius": 0.45,
|
||||||
"floorY": 0.0,
|
"floorY": 0.0,
|
||||||
"objectPadding": 0.25,
|
"objectPadding": 0.25,
|
||||||
|
"boundaryPadding": 0.35,
|
||||||
"areas": [
|
"areas": [
|
||||||
{
|
{
|
||||||
"name": "main_corridor",
|
"name": "main_corridor",
|
||||||
"available": true,
|
"available": true,
|
||||||
"polygon": [
|
"polygon": [
|
||||||
[-2.2, 0.8],
|
[
|
||||||
[2.2, 0.8],
|
-2.2,
|
||||||
[2.2, -40.8],
|
0.8
|
||||||
[-2.2, -40.8]
|
],
|
||||||
|
[
|
||||||
|
2.2,
|
||||||
|
0.8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.2,
|
||||||
|
-40.8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2.2,
|
||||||
|
-40.8
|
||||||
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "left_door_05",
|
"name": "left_door_05",
|
||||||
"available": true,
|
"available": true,
|
||||||
"polygon": [
|
"polygon": [
|
||||||
[-3.4, -4.2],
|
[
|
||||||
[-2.0, -4.2],
|
-3.4,
|
||||||
[-2.0, -5.8],
|
-4.2
|
||||||
[-3.4, -5.8]
|
],
|
||||||
|
[
|
||||||
|
-2.0,
|
||||||
|
-4.2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2.0,
|
||||||
|
-5.8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-3.4,
|
||||||
|
-5.8
|
||||||
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "right_door_05",
|
"name": "right_door_05",
|
||||||
"available": true,
|
"available": true,
|
||||||
"polygon": [
|
"polygon": [
|
||||||
[2.0, -4.2],
|
[
|
||||||
[3.4, -4.2],
|
2.0,
|
||||||
[3.4, -5.8],
|
-4.2
|
||||||
[2.0, -5.8]
|
],
|
||||||
|
[
|
||||||
|
3.4,
|
||||||
|
-4.2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.4,
|
||||||
|
-5.8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.0,
|
||||||
|
-5.8
|
||||||
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "left_room_05",
|
"name": "left_room_05",
|
||||||
"available": true,
|
"available": true,
|
||||||
"polygon": [
|
"polygon": [
|
||||||
[-8.8, -1.0],
|
[
|
||||||
[-3.0, -1.0],
|
-8.8,
|
||||||
[-3.0, -9.0],
|
-1.0
|
||||||
[-8.8, -9.0]
|
],
|
||||||
|
[
|
||||||
|
-3.0,
|
||||||
|
-1.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-3.0,
|
||||||
|
-9.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-8.8,
|
||||||
|
-9.0
|
||||||
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "right_room_05",
|
"name": "right_room_05",
|
||||||
"available": true,
|
"available": true,
|
||||||
"polygon": [
|
"polygon": [
|
||||||
[3.0, -1.0],
|
[
|
||||||
[8.8, -1.0],
|
3.0,
|
||||||
[8.8, -9.0],
|
-1.0
|
||||||
[3.0, -9.0]
|
],
|
||||||
|
[
|
||||||
|
8.8,
|
||||||
|
-1.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8.8,
|
||||||
|
-9.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.0,
|
||||||
|
-9.0
|
||||||
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "left_door_15",
|
"name": "left_door_15",
|
||||||
"available": true,
|
"available": true,
|
||||||
"polygon": [
|
"polygon": [
|
||||||
[-3.4, -14.2],
|
[
|
||||||
[-2.0, -14.2],
|
-3.4,
|
||||||
[-2.0, -15.8],
|
-14.2
|
||||||
[-3.4, -15.8]
|
],
|
||||||
|
[
|
||||||
|
-2.0,
|
||||||
|
-14.2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2.0,
|
||||||
|
-15.8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-3.4,
|
||||||
|
-15.8
|
||||||
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "right_door_15",
|
"name": "right_door_15",
|
||||||
"available": true,
|
"available": true,
|
||||||
"polygon": [
|
"polygon": [
|
||||||
[2.0, -14.2],
|
[
|
||||||
[3.4, -14.2],
|
2.0,
|
||||||
[3.4, -15.8],
|
-14.2
|
||||||
[2.0, -15.8]
|
],
|
||||||
|
[
|
||||||
|
3.4,
|
||||||
|
-14.2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.4,
|
||||||
|
-15.8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.0,
|
||||||
|
-15.8
|
||||||
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "left_room_15",
|
"name": "left_room_15",
|
||||||
"available": true,
|
"available": true,
|
||||||
"polygon": [
|
"polygon": [
|
||||||
[-8.8, -11.0],
|
[
|
||||||
[-3.0, -11.0],
|
-8.8,
|
||||||
[-3.0, -19.0],
|
-11.0
|
||||||
[-8.8, -19.0]
|
],
|
||||||
|
[
|
||||||
|
-3.0,
|
||||||
|
-11.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-3.0,
|
||||||
|
-19.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-8.8,
|
||||||
|
-19.0
|
||||||
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "right_room_15",
|
"name": "right_room_15",
|
||||||
"available": true,
|
"available": true,
|
||||||
"polygon": [
|
"polygon": [
|
||||||
[3.0, -11.0],
|
[
|
||||||
[8.8, -11.0],
|
3.0,
|
||||||
[8.8, -19.0],
|
-11.0
|
||||||
[3.0, -19.0]
|
],
|
||||||
|
[
|
||||||
|
8.8,
|
||||||
|
-11.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8.8,
|
||||||
|
-19.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.0,
|
||||||
|
-19.0
|
||||||
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "left_door_25",
|
"name": "left_door_25",
|
||||||
"available": true,
|
"available": true,
|
||||||
"polygon": [
|
"polygon": [
|
||||||
[-3.4, -24.2],
|
[
|
||||||
[-2.0, -24.2],
|
-3.4,
|
||||||
[-2.0, -25.8],
|
-24.2
|
||||||
[-3.4, -25.8]
|
],
|
||||||
|
[
|
||||||
|
-2.0,
|
||||||
|
-24.2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2.0,
|
||||||
|
-25.8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-3.4,
|
||||||
|
-25.8
|
||||||
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "right_door_25",
|
"name": "right_door_25",
|
||||||
"available": true,
|
"available": true,
|
||||||
"polygon": [
|
"polygon": [
|
||||||
[2.0, -24.2],
|
[
|
||||||
[3.4, -24.2],
|
2.0,
|
||||||
[3.4, -25.8],
|
-24.2
|
||||||
[2.0, -25.8]
|
],
|
||||||
|
[
|
||||||
|
3.4,
|
||||||
|
-24.2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.4,
|
||||||
|
-25.8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.0,
|
||||||
|
-25.8
|
||||||
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "left_room_25",
|
"name": "left_room_25",
|
||||||
"available": true,
|
"available": true,
|
||||||
"polygon": [
|
"polygon": [
|
||||||
[-8.8, -21.0],
|
[
|
||||||
[-3.0, -21.0],
|
-8.8,
|
||||||
[-3.0, -29.0],
|
-21.0
|
||||||
[-8.8, -29.0]
|
],
|
||||||
|
[
|
||||||
|
-3.0,
|
||||||
|
-21.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-3.0,
|
||||||
|
-29.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-8.8,
|
||||||
|
-29.0
|
||||||
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "right_room_25",
|
"name": "right_room_25",
|
||||||
"available": true,
|
"available": true,
|
||||||
"polygon": [
|
"polygon": [
|
||||||
[3.0, -21.0],
|
[
|
||||||
[8.8, -21.0],
|
3.0,
|
||||||
[8.8, -29.0],
|
-21.0
|
||||||
[3.0, -29.0]
|
],
|
||||||
|
[
|
||||||
|
8.8,
|
||||||
|
-21.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8.8,
|
||||||
|
-29.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.0,
|
||||||
|
-29.0
|
||||||
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "left_door_35",
|
"name": "left_door_35",
|
||||||
"available": true,
|
"available": true,
|
||||||
"polygon": [
|
"polygon": [
|
||||||
[-3.4, -34.2],
|
[
|
||||||
[-2.0, -34.2],
|
-3.4,
|
||||||
[-2.0, -35.8],
|
-34.2
|
||||||
[-3.4, -35.8]
|
],
|
||||||
|
[
|
||||||
|
-2.0,
|
||||||
|
-34.2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2.0,
|
||||||
|
-35.8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-3.4,
|
||||||
|
-35.8
|
||||||
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "right_door_35",
|
"name": "right_door_35",
|
||||||
"available": true,
|
"available": true,
|
||||||
"polygon": [
|
"polygon": [
|
||||||
[2.0, -34.2],
|
[
|
||||||
[3.4, -34.2],
|
2.0,
|
||||||
[3.4, -35.8],
|
-34.2
|
||||||
[2.0, -35.8]
|
],
|
||||||
|
[
|
||||||
|
3.4,
|
||||||
|
-34.2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.4,
|
||||||
|
-35.8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.0,
|
||||||
|
-35.8
|
||||||
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "left_room_35",
|
"name": "left_room_35",
|
||||||
"available": true,
|
"available": true,
|
||||||
"polygon": [
|
"polygon": [
|
||||||
[-8.8, -31.0],
|
[
|
||||||
[-3.0, -31.0],
|
-8.8,
|
||||||
[-3.0, -39.0],
|
-31.0
|
||||||
[-8.8, -39.0]
|
],
|
||||||
|
[
|
||||||
|
-3.0,
|
||||||
|
-31.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-3.0,
|
||||||
|
-39.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-8.8,
|
||||||
|
-39.0
|
||||||
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "right_room_35",
|
"name": "right_room_35",
|
||||||
"available": true,
|
"available": true,
|
||||||
"polygon": [
|
"polygon": [
|
||||||
[3.0, -31.0],
|
[
|
||||||
[8.8, -31.0],
|
3.0,
|
||||||
[8.8, -39.0],
|
-31.0
|
||||||
[3.0, -39.0]
|
],
|
||||||
|
[
|
||||||
|
8.8,
|
||||||
|
-31.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8.8,
|
||||||
|
-39.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.0,
|
||||||
|
-39.0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"obstacles": [
|
||||||
|
{
|
||||||
|
"name": "firebox",
|
||||||
|
"polygon": [
|
||||||
|
[
|
||||||
|
-2.49468,
|
||||||
|
-32.87888
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2.05776,
|
||||||
|
-32.87888
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2.05776,
|
||||||
|
-31.51618
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2.49468,
|
||||||
|
-31.51618
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bench",
|
||||||
|
"polygon": [
|
||||||
|
[
|
||||||
|
-2.54485,
|
||||||
|
-9.31999
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2.51786,
|
||||||
|
-9.3557
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2.48369,
|
||||||
|
-9.37177
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2.45574,
|
||||||
|
-9.38357
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2.17907,
|
||||||
|
-9.39566
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1.80829,
|
||||||
|
-9.40749
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1.7521,
|
||||||
|
-9.36413
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1.66462,
|
||||||
|
-9.28728
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1.65801,
|
||||||
|
-9.14512
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1.65114,
|
||||||
|
-7.22169
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1.65535,
|
||||||
|
-6.51944
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1.72626,
|
||||||
|
-6.42306
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-1.76837,
|
||||||
|
-6.39498
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2.47166,
|
||||||
|
-6.39746
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2.5309,
|
||||||
|
-6.44135
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2.53691,
|
||||||
|
-7.18464
|
||||||
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,10 +8,668 @@
|
|||||||
"name": "main_corridor",
|
"name": "main_corridor",
|
||||||
"available": true,
|
"available": true,
|
||||||
"polygon": [
|
"polygon": [
|
||||||
[-200, 200],
|
[
|
||||||
[200, 200],
|
-200,
|
||||||
[200, -200],
|
200
|
||||||
[-200, -200]
|
],
|
||||||
|
[
|
||||||
|
200,
|
||||||
|
200
|
||||||
|
],
|
||||||
|
[
|
||||||
|
200,
|
||||||
|
-200
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-200,
|
||||||
|
-200
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"obstacles": [
|
||||||
|
{
|
||||||
|
"name": "door",
|
||||||
|
"polygon": [
|
||||||
|
[
|
||||||
|
-5.2,
|
||||||
|
7.08001
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-5.19904,
|
||||||
|
7.07025
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-5.19619,
|
||||||
|
7.06087
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-5.19157,
|
||||||
|
7.05223
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-5.18536,
|
||||||
|
7.04465
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-5.17778,
|
||||||
|
7.03843
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-5.16913,
|
||||||
|
7.03381
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-5.15975,
|
||||||
|
7.03097
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-5.15,
|
||||||
|
7.03001
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-3.75,
|
||||||
|
7.13001
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-3.625,
|
||||||
|
7.43001
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-3.625,
|
||||||
|
8.83001
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-3.75,
|
||||||
|
9.13001
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-5.15,
|
||||||
|
9.23001
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-5.15975,
|
||||||
|
9.22905
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-5.16913,
|
||||||
|
9.2262
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-5.17778,
|
||||||
|
9.22158
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-5.18536,
|
||||||
|
9.21536
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-5.19157,
|
||||||
|
9.20778
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-5.19619,
|
||||||
|
9.19914
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-5.19904,
|
||||||
|
9.18976
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-5.2,
|
||||||
|
9.18001
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "inai",
|
||||||
|
"polygon": [
|
||||||
|
[
|
||||||
|
-3.75,
|
||||||
|
-15.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.75,
|
||||||
|
-15.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3.75,
|
||||||
|
15.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-3.75,
|
||||||
|
15.0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tree001",
|
||||||
|
"polygon": [
|
||||||
|
[
|
||||||
|
10.45,
|
||||||
|
12.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
10.38971,
|
||||||
|
12.225
|
||||||
|
],
|
||||||
|
[
|
||||||
|
10.225,
|
||||||
|
12.38971
|
||||||
|
],
|
||||||
|
[
|
||||||
|
10.0,
|
||||||
|
12.45
|
||||||
|
],
|
||||||
|
[
|
||||||
|
9.775,
|
||||||
|
12.38971
|
||||||
|
],
|
||||||
|
[
|
||||||
|
9.61029,
|
||||||
|
12.225
|
||||||
|
],
|
||||||
|
[
|
||||||
|
9.55,
|
||||||
|
12.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
9.61029,
|
||||||
|
11.775
|
||||||
|
],
|
||||||
|
[
|
||||||
|
9.775,
|
||||||
|
11.61029
|
||||||
|
],
|
||||||
|
[
|
||||||
|
10.0,
|
||||||
|
11.55
|
||||||
|
],
|
||||||
|
[
|
||||||
|
10.225,
|
||||||
|
11.61029
|
||||||
|
],
|
||||||
|
[
|
||||||
|
10.38971,
|
||||||
|
11.775
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tree002",
|
||||||
|
"polygon": [
|
||||||
|
[
|
||||||
|
-11.55,
|
||||||
|
19.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11.61029,
|
||||||
|
19.225
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11.775,
|
||||||
|
19.38971
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.0,
|
||||||
|
19.45
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.225,
|
||||||
|
19.38971
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.38971,
|
||||||
|
19.225
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.45,
|
||||||
|
19.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.38971,
|
||||||
|
18.775
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.225,
|
||||||
|
18.61029
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.0,
|
||||||
|
18.55
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11.775,
|
||||||
|
18.61029
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11.61029,
|
||||||
|
18.775
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tree003",
|
||||||
|
"polygon": [
|
||||||
|
[
|
||||||
|
-11.55,
|
||||||
|
8.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11.61029,
|
||||||
|
8.225
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11.775,
|
||||||
|
8.38971
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.0,
|
||||||
|
8.45
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.225,
|
||||||
|
8.38971
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.38971,
|
||||||
|
8.225
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.45,
|
||||||
|
8.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.38971,
|
||||||
|
7.775
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.225,
|
||||||
|
7.61029
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.0,
|
||||||
|
7.55
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11.775,
|
||||||
|
7.61029
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11.61029,
|
||||||
|
7.775
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tree004",
|
||||||
|
"polygon": [
|
||||||
|
[
|
||||||
|
-11.55,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11.61029,
|
||||||
|
0.225
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11.775,
|
||||||
|
0.38971
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.0,
|
||||||
|
0.45
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.225,
|
||||||
|
0.38971
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.38971,
|
||||||
|
0.225
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.45,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.38971,
|
||||||
|
-0.225
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.225,
|
||||||
|
-0.38971
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.0,
|
||||||
|
-0.45
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11.775,
|
||||||
|
-0.38971
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11.61029,
|
||||||
|
-0.225
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tree005",
|
||||||
|
"polygon": [
|
||||||
|
[
|
||||||
|
-11.55,
|
||||||
|
-8.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11.61029,
|
||||||
|
-7.775
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11.775,
|
||||||
|
-7.61029
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.0,
|
||||||
|
-7.55
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.225,
|
||||||
|
-7.61029
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.38971,
|
||||||
|
-7.775
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.45,
|
||||||
|
-8.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.38971,
|
||||||
|
-8.225
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.225,
|
||||||
|
-8.38971
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-12.0,
|
||||||
|
-8.45
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11.775,
|
||||||
|
-8.38971
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-11.61029,
|
||||||
|
-8.225
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tree006",
|
||||||
|
"polygon": [
|
||||||
|
[
|
||||||
|
8.94915,
|
||||||
|
-2.59884
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8.88886,
|
||||||
|
-2.37384
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8.72415,
|
||||||
|
-2.20913
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8.49915,
|
||||||
|
-2.14884
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8.27415,
|
||||||
|
-2.20913
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8.10944,
|
||||||
|
-2.37384
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8.04915,
|
||||||
|
-2.59884
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8.10944,
|
||||||
|
-2.82384
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8.27415,
|
||||||
|
-2.98855
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8.49915,
|
||||||
|
-3.04884
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8.72415,
|
||||||
|
-2.98855
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8.88886,
|
||||||
|
-2.82384
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tree007",
|
||||||
|
"polygon": [
|
||||||
|
[
|
||||||
|
15.0436,
|
||||||
|
5.3401
|
||||||
|
],
|
||||||
|
[
|
||||||
|
14.98331,
|
||||||
|
5.5651
|
||||||
|
],
|
||||||
|
[
|
||||||
|
14.8186,
|
||||||
|
5.72981
|
||||||
|
],
|
||||||
|
[
|
||||||
|
14.5936,
|
||||||
|
5.7901
|
||||||
|
],
|
||||||
|
[
|
||||||
|
14.3686,
|
||||||
|
5.72981
|
||||||
|
],
|
||||||
|
[
|
||||||
|
14.20389,
|
||||||
|
5.5651
|
||||||
|
],
|
||||||
|
[
|
||||||
|
14.1436,
|
||||||
|
5.3401
|
||||||
|
],
|
||||||
|
[
|
||||||
|
14.20389,
|
||||||
|
5.1151
|
||||||
|
],
|
||||||
|
[
|
||||||
|
14.3686,
|
||||||
|
4.95039
|
||||||
|
],
|
||||||
|
[
|
||||||
|
14.5936,
|
||||||
|
4.8901
|
||||||
|
],
|
||||||
|
[
|
||||||
|
14.8186,
|
||||||
|
4.95039
|
||||||
|
],
|
||||||
|
[
|
||||||
|
14.98331,
|
||||||
|
5.1151
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tree008",
|
||||||
|
"polygon": [
|
||||||
|
[
|
||||||
|
24.3795,
|
||||||
|
9.00583
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.31921,
|
||||||
|
9.23083
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.1545,
|
||||||
|
9.39554
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.9295,
|
||||||
|
9.45583
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.7045,
|
||||||
|
9.39554
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.53979,
|
||||||
|
9.23083
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.4795,
|
||||||
|
9.00583
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.53979,
|
||||||
|
8.78083
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.7045,
|
||||||
|
8.61612
|
||||||
|
],
|
||||||
|
[
|
||||||
|
23.9295,
|
||||||
|
8.55583
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.1545,
|
||||||
|
8.61612
|
||||||
|
],
|
||||||
|
[
|
||||||
|
24.31921,
|
||||||
|
8.78083
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tree009",
|
||||||
|
"polygon": [
|
||||||
|
[
|
||||||
|
30.2628,
|
||||||
|
-1.45278
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.20251,
|
||||||
|
-1.22778
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.0378,
|
||||||
|
-1.06307
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.8128,
|
||||||
|
-1.00278
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.5878,
|
||||||
|
-1.06307
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.42309,
|
||||||
|
-1.22778
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.3628,
|
||||||
|
-1.45278
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.42309,
|
||||||
|
-1.67778
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.5878,
|
||||||
|
-1.84249
|
||||||
|
],
|
||||||
|
[
|
||||||
|
29.8128,
|
||||||
|
-1.90278
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.0378,
|
||||||
|
-1.84249
|
||||||
|
],
|
||||||
|
[
|
||||||
|
30.20251,
|
||||||
|
-1.67778
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tree010",
|
||||||
|
"polygon": [
|
||||||
|
[
|
||||||
|
33.6271,
|
||||||
|
14.609
|
||||||
|
],
|
||||||
|
[
|
||||||
|
33.56681,
|
||||||
|
14.834
|
||||||
|
],
|
||||||
|
[
|
||||||
|
33.4021,
|
||||||
|
14.99871
|
||||||
|
],
|
||||||
|
[
|
||||||
|
33.1771,
|
||||||
|
15.059
|
||||||
|
],
|
||||||
|
[
|
||||||
|
32.9521,
|
||||||
|
14.99871
|
||||||
|
],
|
||||||
|
[
|
||||||
|
32.78739,
|
||||||
|
14.834
|
||||||
|
],
|
||||||
|
[
|
||||||
|
32.7271,
|
||||||
|
14.609
|
||||||
|
],
|
||||||
|
[
|
||||||
|
32.78739,
|
||||||
|
14.384
|
||||||
|
],
|
||||||
|
[
|
||||||
|
32.9521,
|
||||||
|
14.21929
|
||||||
|
],
|
||||||
|
[
|
||||||
|
33.1771,
|
||||||
|
14.159
|
||||||
|
],
|
||||||
|
[
|
||||||
|
33.4021,
|
||||||
|
14.21929
|
||||||
|
],
|
||||||
|
[
|
||||||
|
33.56681,
|
||||||
|
14.384
|
||||||
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -28,57 +28,34 @@
|
|||||||
"name": "inventory_items_panel",
|
"name": "inventory_items_panel",
|
||||||
"x": 50.0,
|
"x": 50.0,
|
||||||
"y": 150.0,
|
"y": 150.0,
|
||||||
"width": 320.0,
|
"width": 250.0,
|
||||||
"height": 420.0,
|
"height": 300.0,
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"type": "StaticImage",
|
"type": "StaticImage",
|
||||||
"name": "panel_background",
|
"name": "panel_background",
|
||||||
"x": 0.0,
|
"width": 200,
|
||||||
"y": 0.0,
|
"height": 400,
|
||||||
"width": 320.0,
|
|
||||||
"height": 420.0,
|
|
||||||
"texture": "resources/w/red.png"
|
"texture": "resources/w/red.png"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "TextView",
|
|
||||||
"name": "inventory_title_text",
|
|
||||||
"x": 20.0,
|
|
||||||
"y": 18.0,
|
|
||||||
"width": 230.0,
|
|
||||||
"height": 34.0,
|
|
||||||
"text": "Inventory",
|
|
||||||
"fontSize": 24,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"centered": false,
|
|
||||||
"topAligned": true,
|
|
||||||
"paddingX": 0.0,
|
|
||||||
"paddingY": 0.0,
|
|
||||||
"color": [1.0, 1.0, 1.0, 1.0]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "TextView",
|
"type": "TextView",
|
||||||
"name": "inventory_items_text",
|
"name": "inventory_items_text",
|
||||||
"x": 20.0,
|
"x": -100.0,
|
||||||
"y": 70.0,
|
"y": -100.0,
|
||||||
"width": 280.0,
|
"width": 250.0,
|
||||||
"height": 320.0,
|
"height": 300.0,
|
||||||
"text": "Inventory (Empty)",
|
"text": "Inventory (Empty)",
|
||||||
"fontSize": 18,
|
"fontSize": 18,
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
"fontPath": "resources/fonts/DroidSans.ttf",
|
||||||
"centered": false,
|
"centered": false,
|
||||||
"topAligned": true,
|
|
||||||
"wrap": true,
|
|
||||||
"paddingX": 0.0,
|
|
||||||
"paddingY": 0.0,
|
|
||||||
"maxLines": 14,
|
|
||||||
"color": [1.0, 1.0, 1.0, 1.0]
|
"color": [1.0, 1.0, 1.0, 1.0]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "TextButton",
|
"type": "TextButton",
|
||||||
"name": "close_inventory_button",
|
"name": "close_inventory_button",
|
||||||
"x": 266.0,
|
"x": 165.0,
|
||||||
"y": 16.0,
|
"y": 0.0,
|
||||||
"width": 40.0,
|
"width": 40.0,
|
||||||
"height": 40.0,
|
"height": 40.0,
|
||||||
"text": "X",
|
"text": "X",
|
||||||
@@ -87,9 +64,7 @@
|
|||||||
"textCentered": true,
|
"textCentered": true,
|
||||||
"color": [1.0, 1.0, 1.0, 1.0],
|
"color": [1.0, 1.0, 1.0, 1.0],
|
||||||
"textures": {
|
"textures": {
|
||||||
"normal": "resources/w/blue.png",
|
"normal": "resources/w/blue.png"
|
||||||
"hover": "resources/w/blue.png",
|
|
||||||
"pressed": "resources/w/blue.png"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,353 +0,0 @@
|
|||||||
{
|
|
||||||
"root": {
|
|
||||||
"type": "FrameLayout",
|
|
||||||
"name": "quest_journal_root",
|
|
||||||
"width": "match_parent",
|
|
||||||
"height": "match_parent",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "TextButton",
|
|
||||||
"name": "quest_journal_button",
|
|
||||||
"x": 220.0,
|
|
||||||
"y": 50.0,
|
|
||||||
"width": 170.0,
|
|
||||||
"height": 60.0,
|
|
||||||
"text": "Quests",
|
|
||||||
"fontSize": 24,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"textCentered": true,
|
|
||||||
"color": [1.0, 1.0, 1.0, 1.0],
|
|
||||||
"textures": {
|
|
||||||
"normal": "resources/w/red.png",
|
|
||||||
"hover": "resources/w/red.png",
|
|
||||||
"pressed": "resources/w/red.png"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "FrameLayout",
|
|
||||||
"name": "quest_journal_panel",
|
|
||||||
"x": 55.0,
|
|
||||||
"y": 85.0,
|
|
||||||
"width": 1170.0,
|
|
||||||
"height": 590.0,
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "StaticImage",
|
|
||||||
"name": "quest_panel_background",
|
|
||||||
"x": 0.0,
|
|
||||||
"y": 0.0,
|
|
||||||
"width": 1170.0,
|
|
||||||
"height": 590.0,
|
|
||||||
"texture": "resources/black.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "TextView",
|
|
||||||
"name": "quest_journal_title_text",
|
|
||||||
"x": 0.0,
|
|
||||||
"y": 18.0,
|
|
||||||
"width": 1170.0,
|
|
||||||
"height": 44.0,
|
|
||||||
"text": "QUESTS",
|
|
||||||
"fontSize": 32,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"centered": true,
|
|
||||||
"topAligned": true,
|
|
||||||
"paddingY": 2.0,
|
|
||||||
"color": [1.0, 1.0, 1.0, 1.0]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "TextButton",
|
|
||||||
"name": "quest_close_button",
|
|
||||||
"x": 1110.0,
|
|
||||||
"y": 20.0,
|
|
||||||
"width": 38.0,
|
|
||||||
"height": 38.0,
|
|
||||||
"text": "X",
|
|
||||||
"fontSize": 20,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"textCentered": true,
|
|
||||||
"color": [1.0, 1.0, 1.0, 1.0],
|
|
||||||
"textures": {
|
|
||||||
"normal": "resources/w/blue.png",
|
|
||||||
"hover": "resources/w/blue.png",
|
|
||||||
"pressed": "resources/w/blue.png"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "StaticImage",
|
|
||||||
"name": "quest_separator_left",
|
|
||||||
"x": 390.0,
|
|
||||||
"y": 88.0,
|
|
||||||
"width": 2.0,
|
|
||||||
"height": 470.0,
|
|
||||||
"texture": "resources/w/red.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "StaticImage",
|
|
||||||
"name": "quest_separator_right",
|
|
||||||
"x": 770.0,
|
|
||||||
"y": 88.0,
|
|
||||||
"width": 2.0,
|
|
||||||
"height": 470.0,
|
|
||||||
"texture": "resources/w/red.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "TextView",
|
|
||||||
"name": "quest_list_header_text",
|
|
||||||
"x": 35.0,
|
|
||||||
"y": 90.0,
|
|
||||||
"width": 330.0,
|
|
||||||
"height": 32.0,
|
|
||||||
"text": "ЗАДАНИЯ",
|
|
||||||
"fontSize": 22,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"centered": false,
|
|
||||||
"topAligned": true,
|
|
||||||
"paddingX": 4.0,
|
|
||||||
"paddingY": 0.0,
|
|
||||||
"color": [1.0, 0.88, 0.45, 1.0]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "TextButton",
|
|
||||||
"name": "quest_slot_0",
|
|
||||||
"x": 35.0,
|
|
||||||
"y": 130.0,
|
|
||||||
"width": 330.0,
|
|
||||||
"height": 42.0,
|
|
||||||
"text": "",
|
|
||||||
"fontSize": 18,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"textCentered": false,
|
|
||||||
"textPaddingX": 12.0,
|
|
||||||
"color": [1.0, 1.0, 1.0, 1.0],
|
|
||||||
"textures": { "normal": "resources/transparent.png", "hover": "resources/w/blue.png", "pressed": "resources/w/blue.png" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "TextButton",
|
|
||||||
"name": "quest_slot_1",
|
|
||||||
"x": 35.0,
|
|
||||||
"y": 178.0,
|
|
||||||
"width": 330.0,
|
|
||||||
"height": 42.0,
|
|
||||||
"text": "",
|
|
||||||
"fontSize": 18,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"textCentered": false,
|
|
||||||
"textPaddingX": 12.0,
|
|
||||||
"color": [1.0, 1.0, 1.0, 1.0],
|
|
||||||
"textures": { "normal": "resources/transparent.png", "hover": "resources/w/blue.png", "pressed": "resources/w/blue.png" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "TextButton",
|
|
||||||
"name": "quest_slot_2",
|
|
||||||
"x": 35.0,
|
|
||||||
"y": 226.0,
|
|
||||||
"width": 330.0,
|
|
||||||
"height": 42.0,
|
|
||||||
"text": "",
|
|
||||||
"fontSize": 18,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"textCentered": false,
|
|
||||||
"textPaddingX": 12.0,
|
|
||||||
"color": [1.0, 1.0, 1.0, 1.0],
|
|
||||||
"textures": { "normal": "resources/transparent.png", "hover": "resources/w/blue.png", "pressed": "resources/w/blue.png" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "TextButton",
|
|
||||||
"name": "quest_slot_3",
|
|
||||||
"x": 35.0,
|
|
||||||
"y": 274.0,
|
|
||||||
"width": 330.0,
|
|
||||||
"height": 42.0,
|
|
||||||
"text": "",
|
|
||||||
"fontSize": 18,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"textCentered": false,
|
|
||||||
"textPaddingX": 12.0,
|
|
||||||
"color": [1.0, 1.0, 1.0, 1.0],
|
|
||||||
"textures": { "normal": "resources/transparent.png", "hover": "resources/w/blue.png", "pressed": "resources/w/blue.png" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "TextButton",
|
|
||||||
"name": "quest_slot_4",
|
|
||||||
"x": 35.0,
|
|
||||||
"y": 322.0,
|
|
||||||
"width": 330.0,
|
|
||||||
"height": 42.0,
|
|
||||||
"text": "",
|
|
||||||
"fontSize": 18,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"textCentered": false,
|
|
||||||
"textPaddingX": 12.0,
|
|
||||||
"color": [1.0, 1.0, 1.0, 1.0],
|
|
||||||
"textures": { "normal": "resources/transparent.png", "hover": "resources/w/blue.png", "pressed": "resources/w/blue.png" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "TextButton",
|
|
||||||
"name": "quest_slot_5",
|
|
||||||
"x": 35.0,
|
|
||||||
"y": 370.0,
|
|
||||||
"width": 330.0,
|
|
||||||
"height": 42.0,
|
|
||||||
"text": "",
|
|
||||||
"fontSize": 18,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"textCentered": false,
|
|
||||||
"textPaddingX": 12.0,
|
|
||||||
"color": [1.0, 1.0, 1.0, 1.0],
|
|
||||||
"textures": { "normal": "resources/transparent.png", "hover": "resources/w/blue.png", "pressed": "resources/w/blue.png" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "TextButton",
|
|
||||||
"name": "quest_slot_6",
|
|
||||||
"x": 35.0,
|
|
||||||
"y": 418.0,
|
|
||||||
"width": 330.0,
|
|
||||||
"height": 42.0,
|
|
||||||
"text": "",
|
|
||||||
"fontSize": 18,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"textCentered": false,
|
|
||||||
"textPaddingX": 12.0,
|
|
||||||
"color": [1.0, 1.0, 1.0, 1.0],
|
|
||||||
"textures": { "normal": "resources/transparent.png", "hover": "resources/w/blue.png", "pressed": "resources/w/blue.png" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "TextButton",
|
|
||||||
"name": "quest_slot_7",
|
|
||||||
"x": 35.0,
|
|
||||||
"y": 466.0,
|
|
||||||
"width": 330.0,
|
|
||||||
"height": 42.0,
|
|
||||||
"text": "",
|
|
||||||
"fontSize": 18,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"textCentered": false,
|
|
||||||
"textPaddingX": 12.0,
|
|
||||||
"color": [1.0, 1.0, 1.0, 1.0],
|
|
||||||
"textures": { "normal": "resources/transparent.png", "hover": "resources/w/blue.png", "pressed": "resources/w/blue.png" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "TextButton",
|
|
||||||
"name": "quest_slot_8",
|
|
||||||
"x": 35.0,
|
|
||||||
"y": 514.0,
|
|
||||||
"width": 330.0,
|
|
||||||
"height": 42.0,
|
|
||||||
"text": "",
|
|
||||||
"fontSize": 18,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"textCentered": false,
|
|
||||||
"textPaddingX": 12.0,
|
|
||||||
"color": [1.0, 1.0, 1.0, 1.0],
|
|
||||||
"textures": { "normal": "resources/transparent.png", "hover": "resources/w/blue.png", "pressed": "resources/w/blue.png" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "TextView",
|
|
||||||
"name": "quest_middle_title_text",
|
|
||||||
"x": 415.0,
|
|
||||||
"y": 94.0,
|
|
||||||
"width": 330.0,
|
|
||||||
"height": 72.0,
|
|
||||||
"text": "Выберите задание",
|
|
||||||
"fontSize": 22,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"centered": false,
|
|
||||||
"topAligned": true,
|
|
||||||
"wrap": true,
|
|
||||||
"paddingX": 8.0,
|
|
||||||
"paddingY": 4.0,
|
|
||||||
"maxLines": 2,
|
|
||||||
"color": [1.0, 0.88, 0.45, 1.0]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "TextView",
|
|
||||||
"name": "quest_meta_text",
|
|
||||||
"x": 415.0,
|
|
||||||
"y": 168.0,
|
|
||||||
"width": 330.0,
|
|
||||||
"height": 48.0,
|
|
||||||
"text": "",
|
|
||||||
"fontSize": 16,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"centered": false,
|
|
||||||
"topAligned": true,
|
|
||||||
"wrap": true,
|
|
||||||
"paddingX": 8.0,
|
|
||||||
"paddingY": 2.0,
|
|
||||||
"maxLines": 2,
|
|
||||||
"color": [0.72, 0.72, 0.72, 1.0]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "TextView",
|
|
||||||
"name": "quest_objectives_header_text",
|
|
||||||
"x": 415.0,
|
|
||||||
"y": 232.0,
|
|
||||||
"width": 330.0,
|
|
||||||
"height": 32.0,
|
|
||||||
"text": "ЦЕЛИ",
|
|
||||||
"fontSize": 20,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"centered": false,
|
|
||||||
"topAligned": true,
|
|
||||||
"paddingX": 8.0,
|
|
||||||
"paddingY": 0.0,
|
|
||||||
"color": [1.0, 0.88, 0.45, 1.0]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "TextView",
|
|
||||||
"name": "quest_objectives_text",
|
|
||||||
"x": 415.0,
|
|
||||||
"y": 272.0,
|
|
||||||
"width": 330.0,
|
|
||||||
"height": 285.0,
|
|
||||||
"text": "",
|
|
||||||
"fontSize": 17,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"centered": false,
|
|
||||||
"topAligned": true,
|
|
||||||
"wrap": true,
|
|
||||||
"paddingX": 8.0,
|
|
||||||
"paddingY": 4.0,
|
|
||||||
"maxLines": 10,
|
|
||||||
"color": [0.88, 0.88, 0.88, 1.0]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "TextView",
|
|
||||||
"name": "quest_lore_title_text",
|
|
||||||
"x": 795.0,
|
|
||||||
"y": 94.0,
|
|
||||||
"width": 330.0,
|
|
||||||
"height": 38.0,
|
|
||||||
"text": "Описание задания",
|
|
||||||
"fontSize": 22,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"centered": false,
|
|
||||||
"topAligned": true,
|
|
||||||
"paddingX": 8.0,
|
|
||||||
"paddingY": 0.0,
|
|
||||||
"color": [1.0, 0.88, 0.45, 1.0]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "TextView",
|
|
||||||
"name": "quest_description_text",
|
|
||||||
"x": 795.0,
|
|
||||||
"y": 145.0,
|
|
||||||
"width": 330.0,
|
|
||||||
"height": 410.0,
|
|
||||||
"text": "",
|
|
||||||
"fontSize": 18,
|
|
||||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
|
||||||
"centered": false,
|
|
||||||
"topAligned": true,
|
|
||||||
"wrap": true,
|
|
||||||
"paddingX": 8.0,
|
|
||||||
"paddingY": 6.0,
|
|
||||||
"maxLines": 15,
|
|
||||||
"color": [0.92, 0.92, 0.92, 1.0]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
{
|
|
||||||
"dialogues": [
|
|
||||||
{
|
|
||||||
"id": "test_cutscene_skip_hold_dialogue",
|
|
||||||
"start": "cutscene_start",
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"id": "cutscene_start",
|
|
||||||
"type": "CutsceneStart",
|
|
||||||
"cutsceneId": "test_cutscene_skip_hold_01",
|
|
||||||
"next": "end_1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "end_1",
|
|
||||||
"type": "End"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "test_cutscene_images_hardcut_dialogue",
|
|
||||||
"start": "cutscene_start",
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"id": "cutscene_start",
|
|
||||||
"type": "CutsceneStart",
|
|
||||||
"cutsceneId": "test_cutscene_images_hardcut_01",
|
|
||||||
"next": "end_1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "end_1",
|
|
||||||
"type": "End"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "test_cutscene_images_crossfade_dialogue",
|
|
||||||
"start": "cutscene_start",
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"id": "cutscene_start",
|
|
||||||
"type": "CutsceneStart",
|
|
||||||
"cutsceneId": "test_cutscene_images_crossfade_01",
|
|
||||||
"next": "end_1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "end_1",
|
|
||||||
"type": "End"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "test_cutscene_images_silent_dialogue",
|
|
||||||
"start": "cutscene_start",
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"id": "cutscene_start",
|
|
||||||
"type": "CutsceneStart",
|
|
||||||
"cutsceneId": "test_cutscene_images_silent_01",
|
|
||||||
"next": "end_1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "end_1",
|
|
||||||
"type": "End"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cutscenes": [
|
|
||||||
{
|
|
||||||
"id": "test_cutscene_skip_hold_01",
|
|
||||||
"background": "resources/first_cutscene.png",
|
|
||||||
"skippable": true,
|
|
||||||
"durationMs": 12000,
|
|
||||||
"cameraTrack": [
|
|
||||||
{
|
|
||||||
"durationMs": 3000,
|
|
||||||
"from": { "anchor": "Center", "zoom": 1.0, "rotationDeg": 0.0 },
|
|
||||||
"to": { "anchor": "TopLeft", "zoom": 1.45, "rotationDeg": 0.0 },
|
|
||||||
"easing": "EaseInOutSine"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"durationMs": 3000,
|
|
||||||
"from": { "anchor": "TopLeft", "zoom": 1.45, "rotationDeg": 0.0 },
|
|
||||||
"to": { "anchor": "TopRight", "zoom": 1.45, "rotationDeg": 0.0 },
|
|
||||||
"easing": "EaseInOutSine"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"durationMs": 3000,
|
|
||||||
"from": { "anchor": "TopRight", "zoom": 1.45, "rotationDeg": 0.0 },
|
|
||||||
"to": { "anchor": "BottomRight", "zoom": 1.65, "rotationDeg": 0.0 },
|
|
||||||
"easing": "EaseInCubic"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"durationMs": 3000,
|
|
||||||
"from": { "anchor": "BottomRight", "zoom": 1.65, "rotationDeg": 0.0 },
|
|
||||||
"to": { "anchor": "BottomLeft", "zoom": 1.45, "rotationDeg": 0.0 },
|
|
||||||
"easing": "EaseInOutSine"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lines": [
|
|
||||||
{
|
|
||||||
"speaker": "Narrator",
|
|
||||||
"portrait": "",
|
|
||||||
"text": "This cutscene is long enough to test hold-to-skip.",
|
|
||||||
"durationMs": 2600
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"speaker": "Narrator",
|
|
||||||
"portrait": "",
|
|
||||||
"text": "A normal click must not skip it.",
|
|
||||||
"durationMs": 2600
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"speaker": "Ghost",
|
|
||||||
"portrait": "resources/ghost_avatar.png",
|
|
||||||
"text": "Only the skip button with hold should work.",
|
|
||||||
"durationMs": 2600
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "test_cutscene_images_hardcut_01",
|
|
||||||
"background": "resources/first_cutscene.png",
|
|
||||||
"skippable": true,
|
|
||||||
"durationMs": 9000,
|
|
||||||
"cameraTrack": [
|
|
||||||
{
|
|
||||||
"durationMs": 4500,
|
|
||||||
"from": { "anchor": "Center", "zoom": 1.0, "rotationDeg": 0.0 },
|
|
||||||
"to": { "anchor": "TopLeft", "zoom": 1.35, "rotationDeg": 0.0 },
|
|
||||||
"easing": "EaseInOutSine"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"durationMs": 4500,
|
|
||||||
"from": { "anchor": "TopLeft", "zoom": 1.35, "rotationDeg": 0.0 },
|
|
||||||
"to": { "anchor": "BottomRight", "zoom": 1.55, "rotationDeg": 0.0 },
|
|
||||||
"easing": "EaseInOutSine"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"images": [
|
|
||||||
{
|
|
||||||
"path": "resources/first_cutscene.png",
|
|
||||||
"startMs": 0,
|
|
||||||
"endMs": 4500,
|
|
||||||
"fadeInMs": 0,
|
|
||||||
"fadeOutMs": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "resources/second_cutscene.png",
|
|
||||||
"startMs": 4500,
|
|
||||||
"endMs": 9000,
|
|
||||||
"fadeInMs": 0,
|
|
||||||
"fadeOutMs": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lines": [
|
|
||||||
{
|
|
||||||
"speaker": "Narrator",
|
|
||||||
"portrait": "",
|
|
||||||
"text": "First image should switch sharply to the second one.",
|
|
||||||
"durationMs": 2800
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"speaker": "Narrator",
|
|
||||||
"portrait": "",
|
|
||||||
"text": "No fade should be visible here.",
|
|
||||||
"durationMs": 2800
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "test_cutscene_images_crossfade_01",
|
|
||||||
"background": "resources/first_cutscene.png",
|
|
||||||
"skippable": true,
|
|
||||||
"durationMs": 10000,
|
|
||||||
"cameraTrack": [
|
|
||||||
{
|
|
||||||
"durationMs": 2500,
|
|
||||||
"from": { "anchor": "Center", "zoom": 1.0, "rotationDeg": 0.0 },
|
|
||||||
"to": { "anchor": "Custom", "centerX": 0.35, "centerY": 0.30, "zoom": 1.45, "rotationDeg": 0.0 },
|
|
||||||
"easing": "EaseInOutQuad"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"durationMs": 2500,
|
|
||||||
"from": { "anchor": "Custom", "centerX": 0.35, "centerY": 0.30, "zoom": 1.45, "rotationDeg": 0.0 },
|
|
||||||
"to": { "anchor": "Custom", "centerX": 0.70, "centerY": 0.32, "zoom": 1.45, "rotationDeg": 0.0 },
|
|
||||||
"easing": "EaseOutCubic"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"durationMs": 2500,
|
|
||||||
"from": { "anchor": "Custom", "centerX": 0.70, "centerY": 0.32, "zoom": 1.45, "rotationDeg": 0.0 },
|
|
||||||
"to": { "anchor": "BottomRight", "zoom": 1.70, "rotationDeg": 0.0 },
|
|
||||||
"easing": "EaseInCubic"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"durationMs": 2500,
|
|
||||||
"from": { "anchor": "BottomRight", "zoom": 1.70, "rotationDeg": 0.0 },
|
|
||||||
"to": { "anchor": "BottomLeft", "zoom": 1.55, "rotationDeg": 0.0 },
|
|
||||||
"easing": "EaseInOutSine"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"images": [
|
|
||||||
{
|
|
||||||
"path": "resources/first_cutscene.png",
|
|
||||||
"startMs": 0,
|
|
||||||
"endMs": 6000,
|
|
||||||
"fadeInMs": 0,
|
|
||||||
"fadeOutMs": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "resources/second_cutscene.png",
|
|
||||||
"startMs": 4500,
|
|
||||||
"endMs": 10000,
|
|
||||||
"fadeInMs": 1500,
|
|
||||||
"fadeOutMs": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lines": [
|
|
||||||
{
|
|
||||||
"speaker": "Narrator",
|
|
||||||
"portrait": "",
|
|
||||||
"text": "The second image should fade over the first one.",
|
|
||||||
"durationMs": 2600
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"speaker": "Ghost",
|
|
||||||
"portrait": "resources/ghost_avatar.png",
|
|
||||||
"text": "This test checks overlap and alpha blending.",
|
|
||||||
"durationMs": 2600
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "test_cutscene_images_silent_01",
|
|
||||||
"background": "resources/first_cutscene.png",
|
|
||||||
"skippable": true,
|
|
||||||
"durationMs": 11000,
|
|
||||||
"cameraTrack": [
|
|
||||||
{
|
|
||||||
"durationMs": 2500,
|
|
||||||
"from": { "anchor": "Center", "zoom": 1.0, "rotationDeg": 0.0 },
|
|
||||||
"to": { "anchor": "TopLeft", "zoom": 1.35, "rotationDeg": 0.0 },
|
|
||||||
"easing": "EaseInOutSine"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"durationMs": 3000,
|
|
||||||
"from": { "anchor": "TopLeft", "zoom": 1.35, "rotationDeg": 0.0 },
|
|
||||||
"to": { "anchor": "TopRight", "zoom": 1.35, "rotationDeg": 0.0 },
|
|
||||||
"easing": "EaseInOutSine"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"durationMs": 3000,
|
|
||||||
"from": { "anchor": "TopRight", "zoom": 1.35, "rotationDeg": 0.0 },
|
|
||||||
"to": { "anchor": "BottomRight", "zoom": 1.55, "rotationDeg": 0.0 },
|
|
||||||
"easing": "EaseOutCubic"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"durationMs": 2500,
|
|
||||||
"from": { "anchor": "BottomRight", "zoom": 1.55, "rotationDeg": 0.0 },
|
|
||||||
"to": { "anchor": "BottomLeft", "zoom": 1.45, "rotationDeg": 0.0 },
|
|
||||||
"easing": "EaseInOutQuad"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"images": [
|
|
||||||
{
|
|
||||||
"path": "resources/first_cutscene.png",
|
|
||||||
"startMs": 0,
|
|
||||||
"endMs": 3500,
|
|
||||||
"fadeInMs": 0,
|
|
||||||
"fadeOutMs": 800
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "resources/second_cutscene.png",
|
|
||||||
"startMs": 3000,
|
|
||||||
"endMs": 7500,
|
|
||||||
"fadeInMs": 800,
|
|
||||||
"fadeOutMs": 1000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "resources/loading.png",
|
|
||||||
"startMs": 7000,
|
|
||||||
"endMs": 11000,
|
|
||||||
"fadeInMs": 1000,
|
|
||||||
"fadeOutMs": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lines": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
{
|
|
||||||
"quests": [
|
|
||||||
{
|
|
||||||
"id": "tutorial_open_journal",
|
|
||||||
"title": "Журнал заданий",
|
|
||||||
"category": "Tutorial",
|
|
||||||
"status": "Completed",
|
|
||||||
"recommendedLevel": 0,
|
|
||||||
"description": "Теперь у героя есть журнал заданий. В нём можно просматривать текущие поручения, цели и подробные записи о событиях, которые уже произошли в мире игры.",
|
|
||||||
"objectives": [
|
|
||||||
{ "id": "open_journal", "text": "Открыть журнал заданий", "completed": true }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "side_lost_bag",
|
|
||||||
"title": "Потерянная сумка",
|
|
||||||
"category": "Side",
|
|
||||||
"status": "Completed",
|
|
||||||
"recommendedLevel": 1,
|
|
||||||
"description": "Путник потерял сумку недалеко от лесной тропы. Внутри оказались вещи, важные для его дальнейшего пути. Возвращение сумки укрепило доверие местных жителей к герою.",
|
|
||||||
"objectives": [
|
|
||||||
{ "id": "find_bag", "text": "Найти сумку у лесной тропы", "completed": true },
|
|
||||||
{ "id": "bring_bag", "text": "Вернуть сумку путнику", "completed": true }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "failed_missing_courier",
|
|
||||||
"title": "След курьера",
|
|
||||||
"category": "Side",
|
|
||||||
"status": "Failed",
|
|
||||||
"recommendedLevel": 2,
|
|
||||||
"description": "Курьер исчез до того, как герой смог выйти на его след. Местные говорят, что на дороге остались только следы копыт и обрывок ремня. Возможность узнать, кто забрал посылку, была упущена.",
|
|
||||||
"objectives": [
|
|
||||||
{ "id": "ask_innkeeper", "text": "Расспросить трактирщика", "completed": true },
|
|
||||||
{ "id": "find_courier", "text": "Найти курьера до наступления ночи", "completed": false }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "contract_old_well",
|
|
||||||
"title": "Старый колодец",
|
|
||||||
"category": "Contract",
|
|
||||||
"status": "Active",
|
|
||||||
"recommendedLevel": 2,
|
|
||||||
"description": "На окраине деревни стоит старый колодец. Ночью из него слышны голоса, а утром вокруг находят мокрые следы. Местные боятся подходить к нему, но староста считает, что причина происходящего связана с давним проклятием.",
|
|
||||||
"objectives": [
|
|
||||||
{ "id": "inspect_well", "text": "Осмотреть старый колодец", "completed": false },
|
|
||||||
{ "id": "search_tracks", "text": "Найти следы возле каменной ограды", "completed": false },
|
|
||||||
{ "id": "report_elder", "text": "Вернуться к старосте", "completed": false }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "main_find_ghost",
|
|
||||||
"title": "Следы призрака",
|
|
||||||
"category": "Main",
|
|
||||||
"status": "Active",
|
|
||||||
"recommendedLevel": 1,
|
|
||||||
"description": "В заброшенной части деревни появился странный призрак. Местные жители говорят, что он появляется только ночью и исчезает возле старого колодца. Похоже, дух пытается привести кого-то к месту, где много лет назад случилась трагедия.",
|
|
||||||
"objectives": [
|
|
||||||
{ "id": "talk_to_ghost", "text": "Поговорить с призраком возле старого дома", "completed": false },
|
|
||||||
{ "id": "inspect_well", "text": "Осмотреть старый колодец", "completed": false },
|
|
||||||
{ "id": "return_to_elder", "text": "Вернуться к старосте", "completed": false }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "main_black_letter",
|
|
||||||
"title": "Письмо с чёрной печатью",
|
|
||||||
"category": "Main",
|
|
||||||
"status": "Active",
|
|
||||||
"recommendedLevel": 3,
|
|
||||||
"description": "В руки героя попало письмо с чёрной восковой печатью. Имя отправителя стёрто, но на бумаге остался запах дыма и дорогих трав. Письмо упоминает встречу у северных ворот и человека, который знает правду о призраке.",
|
|
||||||
"objectives": [
|
|
||||||
{ "id": "read_letter", "text": "Прочитать письмо", "completed": true },
|
|
||||||
{ "id": "go_north_gate", "text": "Добраться до северных ворот", "completed": false },
|
|
||||||
{ "id": "find_contact", "text": "Найти человека с серебряным кольцом", "completed": false }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "side_herbalist_help",
|
|
||||||
"title": "Травы для лекаря",
|
|
||||||
"category": "Side",
|
|
||||||
"status": "Available",
|
|
||||||
"recommendedLevel": 1,
|
|
||||||
"description": "Лекарю нужны редкие болотные травы, чтобы приготовить настой для больных детей. Он предупредил, что растение раскрывается только на рассвете, а рядом часто появляются дикие звери.",
|
|
||||||
"objectives": [
|
|
||||||
{ "id": "collect_herbs", "text": "Собрать болотные травы на рассвете", "completed": false },
|
|
||||||
{ "id": "bring_herbs", "text": "Отнести травы лекарю", "completed": false }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -1,11 +0,0 @@
|
|||||||
//precision mediump float;
|
|
||||||
uniform sampler2D Texture;
|
|
||||||
uniform float uAlpha;
|
|
||||||
varying vec2 texCoord;
|
|
||||||
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
vec4 color = texture2D(Texture, texCoord).rgba;
|
|
||||||
color.a *= uAlpha;
|
|
||||||
gl_FragColor = color;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
precision mediump float;
|
|
||||||
uniform sampler2D Texture;
|
|
||||||
uniform float uAlpha;
|
|
||||||
varying vec2 texCoord;
|
|
||||||
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
vec4 color = texture2D(Texture, texCoord).rgba;
|
|
||||||
color.a *= uAlpha;
|
|
||||||
gl_FragColor = color;
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
#version 120
|
|
||||||
|
|
||||||
attribute vec3 vPosition;
|
attribute vec3 vPosition;
|
||||||
attribute vec2 vTexCoord;
|
attribute vec2 vTexCoord;
|
||||||
attribute vec3 vNormal;
|
attribute vec3 vNormal;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#version 120
|
|
||||||
|
|
||||||
attribute vec3 vPosition;
|
attribute vec3 vPosition;
|
||||||
attribute vec2 vTexCoord;
|
attribute vec2 vTexCoord;
|
||||||
attribute vec3 vNormal;
|
attribute vec3 vNormal;
|
||||||
|
|||||||
+37
-29
@@ -78,6 +78,43 @@ void Character::setPathPlanner(PathPlanner planner) {
|
|||||||
pathPlanner = std::move(planner);
|
pathPlanner = std::move(planner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Character::isMoving() const
|
||||||
|
{
|
||||||
|
Eigen::Vector3f toTarget = walkTarget - position;
|
||||||
|
toTarget.y() = 0.f;
|
||||||
|
return !pathWaypoints.empty() || toTarget.norm() > WALK_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
Eigen::Vector3f Character::getCurrentNavigationTarget() const
|
||||||
|
{
|
||||||
|
if (!pathWaypoints.empty() && currentWaypointIndex < pathWaypoints.size()) {
|
||||||
|
return pathWaypoints[currentWaypointIndex];
|
||||||
|
}
|
||||||
|
return walkTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Character::forceReplan()
|
||||||
|
{
|
||||||
|
if (!pathPlanner) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Eigen::Vector3f normalizedTarget(requestedWalkTarget.x(), 0.f, requestedWalkTarget.z());
|
||||||
|
pathWaypoints = pathPlanner(position, normalizedTarget);
|
||||||
|
currentWaypointIndex = 0;
|
||||||
|
|
||||||
|
if (!pathWaypoints.empty()) {
|
||||||
|
for (Eigen::Vector3f& waypoint : pathWaypoints) {
|
||||||
|
waypoint.y() = 0.f;
|
||||||
|
}
|
||||||
|
walkTarget = pathWaypoints.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
walkTarget = Eigen::Vector3f(position.x(), 0.f, position.z());
|
||||||
|
onArrivedCallback = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
void Character::setTexture(std::shared_ptr<ZL::Texture> texture)
|
void Character::setTexture(std::shared_ptr<ZL::Texture> texture)
|
||||||
{
|
{
|
||||||
for (auto& animEntry : animations) {
|
for (auto& animEntry : animations) {
|
||||||
@@ -105,38 +142,9 @@ AnimationState Character::resolveActiveState() const {
|
|||||||
return AnimationState::STAND;
|
return AnimationState::STAND;
|
||||||
}
|
}
|
||||||
|
|
||||||
float Character::getSpeedMultiplier(float transitionProgress) const {
|
|
||||||
|
|
||||||
if (isPlayer) {
|
|
||||||
return 1.0f + (1.5f - 1.0f) * transitionProgress;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return 1.0f - (1.0f - 0.1f) * transitionProgress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Character::update(int64_t deltaMs) {
|
void Character::update(int64_t deltaMs) {
|
||||||
if (initialHp <= 0.f) initialHp = hp;
|
if (initialHp <= 0.f) initialHp = hp;
|
||||||
|
|
||||||
if (slowMoTimeRemaining > 0.0f) {
|
|
||||||
slowMoTimeRemaining -= static_cast<float>(deltaMs) / 1000.0f;
|
|
||||||
|
|
||||||
float transitionProgress;
|
|
||||||
if (slowMoTimeRemaining > slowMoActiveTime - slowMoTransitionTime) {
|
|
||||||
float timeInTransition = slowMoActiveTime - slowMoTimeRemaining;
|
|
||||||
transitionProgress = (std::min)(1.0f, timeInTransition / slowMoTransitionTime);
|
|
||||||
}
|
|
||||||
else if (slowMoTimeRemaining < slowMoTransitionTime) {
|
|
||||||
transitionProgress = (std::max)(0.0f, slowMoTimeRemaining / slowMoTransitionTime);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
transitionProgress = 1.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
float speedMultiplier = getSpeedMultiplier(transitionProgress);
|
|
||||||
deltaMs = static_cast<int64_t>(static_cast<float>(deltaMs) * speedMultiplier);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//weaponInitialRotation = Eigen::AngleAxisf(x/180.0, Eigen::Vector3f::UnitZ()).toRotationMatrix();
|
//weaponInitialRotation = Eigen::AngleAxisf(x/180.0, Eigen::Vector3f::UnitZ()).toRotationMatrix();
|
||||||
//weaponInitialRotation = Eigen::AngleAxisf(-M_PI*0.5, Eigen::Vector3f::UnitZ()).toRotationMatrix();
|
//weaponInitialRotation = Eigen::AngleAxisf(-M_PI*0.5, Eigen::Vector3f::UnitZ()).toRotationMatrix();
|
||||||
|
|||||||
+10
-4
@@ -53,6 +53,16 @@ public:
|
|||||||
void drawShadowDepth(Renderer& renderer);
|
void drawShadowDepth(Renderer& renderer);
|
||||||
void drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera);
|
void drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera);
|
||||||
|
|
||||||
|
// Character-to-character collision (XZ-plane). Used by Location to keep
|
||||||
|
// player/NPCs from walking through each other.
|
||||||
|
float collisionRadius = 0.45f;
|
||||||
|
|
||||||
|
// Movement/path introspection used for dynamic replanning.
|
||||||
|
bool isMoving() const;
|
||||||
|
const Eigen::Vector3f& getRequestedWalkTarget() const { return requestedWalkTarget; }
|
||||||
|
Eigen::Vector3f getCurrentNavigationTarget() const;
|
||||||
|
void forceReplan();
|
||||||
|
|
||||||
|
|
||||||
// attackDirection is a world-space horizontal vector pointing from the
|
// attackDirection is a world-space horizontal vector pointing from the
|
||||||
// attacker toward this character — i.e. the direction the hit pushes
|
// attacker toward this character — i.e. the direction the hit pushes
|
||||||
@@ -132,10 +142,6 @@ public:
|
|||||||
|
|
||||||
SparkEmitter hitSparkEmitter;
|
SparkEmitter hitSparkEmitter;
|
||||||
|
|
||||||
float slowMoTimeRemaining = 0.0f;
|
|
||||||
float slowMoTransitionTime = 0.5f;
|
|
||||||
float slowMoActiveTime = 6.0f;
|
|
||||||
float getSpeedMultiplier(float transitionProgress) const;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
|||||||
-174
@@ -240,10 +240,6 @@ namespace ZL
|
|||||||
// Load UI with inventory button
|
// Load UI with inventory button
|
||||||
try {
|
try {
|
||||||
menuManager.uiManager.loadFromFile("resources/config2/ui_inventory.json", renderer, CONST_ZIP_FILE);
|
menuManager.uiManager.loadFromFile("resources/config2/ui_inventory.json", renderer, CONST_ZIP_FILE);
|
||||||
menuManager.uiManager.appendFromFile("resources/config2/ui_quest_journal.json", renderer, CONST_ZIP_FILE);
|
|
||||||
|
|
||||||
questJournal.loadFromFile("resources/quests/quests.json", CONST_ZIP_FILE);
|
|
||||||
setupQuestJournalUi();
|
|
||||||
std::cout << "UI loaded successfully" << std::endl;
|
std::cout << "UI loaded successfully" << std::endl;
|
||||||
|
|
||||||
menuManager.uiManager.setNodeVisible("inventory_items_panel", false);
|
menuManager.uiManager.setNodeVisible("inventory_items_panel", false);
|
||||||
@@ -251,9 +247,6 @@ namespace ZL
|
|||||||
|
|
||||||
menuManager.uiManager.setTextButtonCallback("inventory_button", [this](const std::string& name) {
|
menuManager.uiManager.setTextButtonCallback("inventory_button", [this](const std::string& name) {
|
||||||
std::cout << "[UI] Inventory button clicked" << std::endl;
|
std::cout << "[UI] Inventory button clicked" << std::endl;
|
||||||
if (this->questJournalOpen) {
|
|
||||||
this->toggleQuestJournal();
|
|
||||||
}
|
|
||||||
this->menuManager.uiManager.setNodeVisible("inventory_items_panel", true);
|
this->menuManager.uiManager.setNodeVisible("inventory_items_panel", true);
|
||||||
this->menuManager.uiManager.setNodeVisible("close_inventory_button", true);
|
this->menuManager.uiManager.setNodeVisible("close_inventory_button", true);
|
||||||
this->inventoryOpen = true;
|
this->inventoryOpen = true;
|
||||||
@@ -326,149 +319,6 @@ namespace ZL
|
|||||||
CheckGlError(__FILE__, __LINE__);
|
CheckGlError(__FILE__, __LINE__);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static int questStatusPriority(Quest::QuestStatus status) {
|
|
||||||
switch (status) {
|
|
||||||
case Quest::QuestStatus::Active: return 0;
|
|
||||||
case Quest::QuestStatus::Available: return 1;
|
|
||||||
case Quest::QuestStatus::Completed: return 2;
|
|
||||||
case Quest::QuestStatus::Failed: return 3;
|
|
||||||
default: return 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::array<float, 4> questStatusColor(Quest::QuestStatus status) {
|
|
||||||
switch (status) {
|
|
||||||
case Quest::QuestStatus::Completed: return { 0.25f, 0.95f, 0.35f, 1.0f };
|
|
||||||
case Quest::QuestStatus::Failed: return { 1.0f, 0.25f, 0.25f, 1.0f };
|
|
||||||
case Quest::QuestStatus::Active: return { 1.0f, 1.0f, 1.0f, 1.0f };
|
|
||||||
case Quest::QuestStatus::Available: return { 0.86f, 0.86f, 0.86f, 1.0f };
|
|
||||||
default: return { 0.45f, 0.45f, 0.45f, 1.0f };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Game::setupQuestJournalUi() {
|
|
||||||
questJournalOpen = false;
|
|
||||||
selectedQuestIndex = -1;
|
|
||||||
visibleQuestIds.clear();
|
|
||||||
|
|
||||||
menuManager.uiManager.setNodeVisible("quest_journal_panel", false);
|
|
||||||
menuManager.uiManager.setNodeVisible("quest_close_button", false);
|
|
||||||
|
|
||||||
menuManager.uiManager.setTextButtonCallback("quest_journal_button", [this](const std::string&) {
|
|
||||||
toggleQuestJournal();
|
|
||||||
});
|
|
||||||
|
|
||||||
menuManager.uiManager.setTextButtonCallback("quest_close_button", [this](const std::string&) {
|
|
||||||
if (questJournalOpen) {
|
|
||||||
toggleQuestJournal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for (int i = 0; i < 9; ++i) {
|
|
||||||
const std::string slotName = "quest_slot_" + std::to_string(i);
|
|
||||||
menuManager.uiManager.setTextButtonCallback(slotName, [this, i](const std::string&) {
|
|
||||||
selectQuestByIndex(i);
|
|
||||||
});
|
|
||||||
menuManager.uiManager.setNodeVisible(slotName, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Game::toggleQuestJournal() {
|
|
||||||
questJournalOpen = !questJournalOpen;
|
|
||||||
std::cout << "[quest] toggleQuestJournal: " << (questJournalOpen ? "open" : "closed") << std::endl;
|
|
||||||
|
|
||||||
if (questJournalOpen) {
|
|
||||||
if (inventoryOpen) {
|
|
||||||
menuManager.uiManager.setNodeVisible("inventory_items_panel", false);
|
|
||||||
menuManager.uiManager.setNodeVisible("close_inventory_button", false);
|
|
||||||
inventoryOpen = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
menuManager.uiManager.setNodeVisible("quest_journal_panel", questJournalOpen);
|
|
||||||
menuManager.uiManager.setNodeVisible("quest_close_button", questJournalOpen);
|
|
||||||
|
|
||||||
if (questJournalOpen) {
|
|
||||||
refreshQuestJournalUi();
|
|
||||||
if (!visibleQuestIds.empty()) {
|
|
||||||
selectQuestByIndex(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Game::refreshQuestJournalUi() {
|
|
||||||
visibleQuestIds.clear();
|
|
||||||
auto quests = questJournal.getVisibleQuests();
|
|
||||||
|
|
||||||
std::sort(quests.begin(), quests.end(), [](const Quest::QuestState* a, const Quest::QuestState* b) {
|
|
||||||
const int pa = questStatusPriority(a->status);
|
|
||||||
const int pb = questStatusPriority(b->status);
|
|
||||||
if (pa != pb) return pa < pb;
|
|
||||||
// Newer quests are shown above older quests inside the same status bucket.
|
|
||||||
return a->orderIndex > b->orderIndex;
|
|
||||||
});
|
|
||||||
|
|
||||||
for (int i = 0; i < 9; ++i) {
|
|
||||||
const std::string slotName = "quest_slot_" + std::to_string(i);
|
|
||||||
|
|
||||||
if (i < static_cast<int>(quests.size())) {
|
|
||||||
const auto* quest = quests[i];
|
|
||||||
visibleQuestIds.push_back(quest->definition.id);
|
|
||||||
|
|
||||||
const bool selected = (i == selectedQuestIndex);
|
|
||||||
const std::string prefix = selected ? "> " : " ";
|
|
||||||
menuManager.uiManager.setTextButtonText(slotName, prefix + quest->definition.title);
|
|
||||||
menuManager.uiManager.setTextButtonColor(slotName, questStatusColor(quest->status));
|
|
||||||
menuManager.uiManager.setNodeVisible(slotName, true);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
menuManager.uiManager.setTextButtonText(slotName, "");
|
|
||||||
menuManager.uiManager.setNodeVisible(slotName, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Game::selectQuestByIndex(int index) {
|
|
||||||
if (index < 0 || index >= static_cast<int>(visibleQuestIds.size())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedQuestIndex = index;
|
|
||||||
Quest::QuestState* quest = questJournal.findQuest(visibleQuestIds[index]);
|
|
||||||
if (!quest) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto& def = quest->definition;
|
|
||||||
|
|
||||||
menuManager.uiManager.setText("quest_middle_title_text", def.title);
|
|
||||||
menuManager.uiManager.setTextColor("quest_middle_title_text", questStatusColor(quest->status));
|
|
||||||
|
|
||||||
const std::string meta = std::string("Category: ") + Quest::toString(def.category)
|
|
||||||
+ " | Status: " + Quest::toString(quest->status)
|
|
||||||
+ " | Level: " + std::to_string(def.recommendedLevel);
|
|
||||||
menuManager.uiManager.setText("quest_meta_text", meta);
|
|
||||||
|
|
||||||
std::string objectivesText;
|
|
||||||
for (size_t i = 0; i < def.objectives.size(); ++i) {
|
|
||||||
const auto& obj = def.objectives[i];
|
|
||||||
const bool isActive = static_cast<int>(i) == quest->activeObjectiveIndex;
|
|
||||||
const std::string mark = obj.completed ? "[x] " : (isActive ? "> [ ] " : "[ ] ");
|
|
||||||
objectivesText += mark + obj.text;
|
|
||||||
if (i + 1 < def.objectives.size()) {
|
|
||||||
objectivesText += "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
menuManager.uiManager.setText("quest_objectives_text", objectivesText);
|
|
||||||
|
|
||||||
menuManager.uiManager.setText("quest_lore_title_text", "Описание задания");
|
|
||||||
menuManager.uiManager.setText("quest_description_text", def.description);
|
|
||||||
|
|
||||||
refreshQuestJournalUi();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Game::drawScene() {
|
void Game::drawScene() {
|
||||||
glViewport(0, 0, Environment::width, Environment::height);
|
glViewport(0, 0, Environment::width, Environment::height);
|
||||||
if (!loadingCompleted) {
|
if (!loadingCompleted) {
|
||||||
@@ -696,16 +546,6 @@ namespace ZL
|
|||||||
case SDLK_l:
|
case SDLK_l:
|
||||||
x = x - 0.002;
|
x = x - 0.002;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SDLK_c:
|
|
||||||
std::cout << "SLOW-MO activated!" << std::endl;
|
|
||||||
activateSlowMoEffect();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SDLK_j:
|
|
||||||
toggleQuestJournal();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SDLK_RETURN:
|
case SDLK_RETURN:
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -752,20 +592,6 @@ namespace ZL
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::activateSlowMoEffect() {
|
|
||||||
if (!currentLocation) return;
|
|
||||||
|
|
||||||
if (currentLocation->player) {
|
|
||||||
currentLocation->player->slowMoTimeRemaining = currentLocation->player->slowMoActiveTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto& npc : currentLocation->npcs) {
|
|
||||||
if (npc) {
|
|
||||||
npc->slowMoTimeRemaining = npc->slowMoActiveTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Game::enterCameraDragMode(int eventX, int eventY)
|
void Game::enterCameraDragMode(int eventX, int eventY)
|
||||||
{
|
{
|
||||||
cameraDragging = true;
|
cameraDragging = true;
|
||||||
|
|||||||
-16
@@ -21,7 +21,6 @@
|
|||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include "Location.h"
|
#include "Location.h"
|
||||||
#include "AudioPlayerAsync.h"
|
#include "AudioPlayerAsync.h"
|
||||||
#include "quest/QuestJournal.h"
|
|
||||||
|
|
||||||
namespace ZL {
|
namespace ZL {
|
||||||
|
|
||||||
@@ -54,15 +53,8 @@ namespace ZL {
|
|||||||
|
|
||||||
bool inventoryOpen = false;
|
bool inventoryOpen = false;
|
||||||
|
|
||||||
ZL::Quest::QuestJournal questJournal;
|
|
||||||
bool questJournalOpen = false;
|
|
||||||
int selectedQuestIndex = -1;
|
|
||||||
std::vector<std::string> visibleQuestIds;
|
|
||||||
|
|
||||||
MenuManager menuManager;
|
MenuManager menuManager;
|
||||||
|
|
||||||
void activateSlowMoEffect();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Unified pointer handling: mouse-left and a single touch share one path.
|
// Unified pointer handling: mouse-left and a single touch share one path.
|
||||||
// A press becomes a tap (interact / walk-to) on release if it never crossed
|
// A press becomes a tap (interact / walk-to) on release if it never crossed
|
||||||
@@ -107,14 +99,6 @@ namespace ZL {
|
|||||||
void updatePinchZoom();
|
void updatePinchZoom();
|
||||||
void endPinch();
|
void endPinch();
|
||||||
int countNonUiPointers() const;
|
int countNonUiPointers() const;
|
||||||
void handleDown(int64_t fingerId, int mx, int my);
|
|
||||||
void handleUp(int64_t fingerId, int mx, int my);
|
|
||||||
void handleMotion(int64_t fingerId, int mx, int my);
|
|
||||||
|
|
||||||
void setupQuestJournalUi();
|
|
||||||
void toggleQuestJournal();
|
|
||||||
void refreshQuestJournalUi();
|
|
||||||
void selectQuestByIndex(int index);
|
|
||||||
|
|
||||||
#ifdef EMSCRIPTEN
|
#ifdef EMSCRIPTEN
|
||||||
static Game* s_instance;
|
static Game* s_instance;
|
||||||
|
|||||||
+203
-25
@@ -23,6 +23,23 @@ namespace ZL
|
|||||||
// on_npc_interact callback fires and the conversation begins.
|
// on_npc_interact callback fires and the conversation begins.
|
||||||
static constexpr float NPC_TALK_DISTANCE = 1.25f;
|
static constexpr float NPC_TALK_DISTANCE = 1.25f;
|
||||||
|
|
||||||
|
static float distancePointToSegmentXZ(const Eigen::Vector3f& p,
|
||||||
|
const Eigen::Vector3f& a,
|
||||||
|
const Eigen::Vector3f& b)
|
||||||
|
{
|
||||||
|
const Eigen::Vector2f p2(p.x(), p.z());
|
||||||
|
const Eigen::Vector2f a2(a.x(), a.z());
|
||||||
|
const Eigen::Vector2f b2(b.x(), b.z());
|
||||||
|
const Eigen::Vector2f ab = b2 - a2;
|
||||||
|
const float abLenSq = ab.squaredNorm();
|
||||||
|
if (abLenSq <= 1e-8f) {
|
||||||
|
return (p2 - a2).norm();
|
||||||
|
}
|
||||||
|
const float t = std::clamp((p2 - a2).dot(ab) / abLenSq, 0.0f, 1.0f);
|
||||||
|
const Eigen::Vector2f closest = a2 + ab * t;
|
||||||
|
return (p2 - closest).norm();
|
||||||
|
}
|
||||||
|
|
||||||
Location::Location(Renderer& iRenderer, Inventory& iInventory)
|
Location::Location(Renderer& iRenderer, Inventory& iInventory)
|
||||||
: renderer(iRenderer)
|
: renderer(iRenderer)
|
||||||
, inventory(iInventory)
|
, inventory(iInventory)
|
||||||
@@ -166,7 +183,6 @@ namespace ZL
|
|||||||
std::cerr << "Failed to init NPC name TextRenderer" << std::endl;
|
std::cerr << "Failed to init NPC name TextRenderer" << std::endl;
|
||||||
npcNameText.reset();
|
npcNameText.reset();
|
||||||
}
|
}
|
||||||
dialogueSystem.loadDatabase("resources/dialogue/sample_dialogues.json");
|
|
||||||
/*dialogueSystem.addTriggerZone({
|
/*dialogueSystem.addTriggerZone({
|
||||||
"ghost_room_trigger",
|
"ghost_room_trigger",
|
||||||
"test_line_dialogue",
|
"test_line_dialogue",
|
||||||
@@ -181,38 +197,51 @@ namespace ZL
|
|||||||
|
|
||||||
void Location::setupNavigation(const std::string& navigationJsonPath)
|
void Location::setupNavigation(const std::string& navigationJsonPath)
|
||||||
{
|
{
|
||||||
std::vector<PathFinder::ObstacleMesh> obstacles;
|
// Static navigation blockers are defined in the navigation JSON as polygons.
|
||||||
obstacles.reserve(gameObjects.size() + interactiveObjects.size());
|
// NPC + player are handled as dynamic obstacles, so they are intentionally NOT in JSON.
|
||||||
|
navigation.build({}, navigationJsonPath, CONST_ZIP_FILE);
|
||||||
for (const auto& item : gameObjects) {
|
|
||||||
const LoadedGameObject& gameObj = item.second;
|
|
||||||
obstacles.push_back({ &gameObj.mesh.data, Eigen::Vector3f::Zero() });
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const InteractiveObject& intObj : interactiveObjects) {
|
|
||||||
if (!intObj.isActive) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
obstacles.push_back({ &intObj.mesh.data, intObj.position });
|
|
||||||
}
|
|
||||||
|
|
||||||
navigation.build(obstacles, navigationJsonPath, CONST_ZIP_FILE);
|
|
||||||
|
|
||||||
#ifdef SHOW_PATH
|
#ifdef SHOW_PATH
|
||||||
buildDebugNavMeshes();
|
buildDebugNavMeshes();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
auto planner = [this](const Eigen::Vector3f& start, const Eigen::Vector3f& end) {
|
static constexpr float kDynamicObstacleInfluenceDist = 6.0f;
|
||||||
return navigation.findPath(start, end);
|
|
||||||
|
auto makePlanner = [this](const Character* self) {
|
||||||
|
return [this, self](const Eigen::Vector3f& start, const Eigen::Vector3f& end) {
|
||||||
|
std::vector<PathFinder::DynamicObstacle> dynamicObstacles;
|
||||||
|
dynamicObstacles.reserve(npcs.size() + 1);
|
||||||
|
|
||||||
|
const auto addCharacter = [&](const Character* other) {
|
||||||
|
if (!other || other == self) return;
|
||||||
|
if (other->hp <= 0.f) return;
|
||||||
|
|
||||||
|
if (distancePointToSegmentXZ(other->position, start, end) > kDynamicObstacleInfluenceDist) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PathFinder::DynamicObstacle obs;
|
||||||
|
obs.position = Eigen::Vector3f(other->position.x(), navigation.getFloorY(), other->position.z());
|
||||||
|
obs.radius = (std::max)(0.0f, other->collisionRadius);
|
||||||
|
dynamicObstacles.push_back(obs);
|
||||||
|
};
|
||||||
|
|
||||||
|
addCharacter(player.get());
|
||||||
|
for (const auto& npc : npcs) {
|
||||||
|
addCharacter(npc.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
return navigation.findPath(start, end, dynamicObstacles);
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
if (player) {
|
if (player) {
|
||||||
player->setPathPlanner(planner);
|
player->setPathPlanner(makePlanner(player.get()));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto& npc : npcs) {
|
for (auto& npc : npcs) {
|
||||||
if (npc) {
|
if (npc) {
|
||||||
npc->setPathPlanner(planner);
|
npc->setPathPlanner(makePlanner(npc.get()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -641,6 +670,156 @@ namespace ZL
|
|||||||
return navigation.setAreaAvailable(areaName, available);
|
return navigation.setAreaAvailable(areaName, available);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Location::resolveCharacterCollisions()
|
||||||
|
{
|
||||||
|
std::vector<Character*> characters;
|
||||||
|
characters.reserve(npcs.size() + 1);
|
||||||
|
|
||||||
|
if (player) {
|
||||||
|
characters.push_back(player.get());
|
||||||
|
}
|
||||||
|
for (auto& npc : npcs) {
|
||||||
|
if (npc) {
|
||||||
|
characters.push_back(npc.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (characters.size() < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr int kIterations = 3;
|
||||||
|
static constexpr float kMinSeparationEps = 1e-4f;
|
||||||
|
|
||||||
|
for (int iter = 0; iter < kIterations; ++iter) {
|
||||||
|
for (size_t i = 0; i < characters.size(); ++i) {
|
||||||
|
for (size_t j = i + 1; j < characters.size(); ++j) {
|
||||||
|
Character* a = characters[i];
|
||||||
|
Character* b = characters[j];
|
||||||
|
if (!a || !b) continue;
|
||||||
|
if (a->hp <= 0.f || b->hp <= 0.f) continue;
|
||||||
|
|
||||||
|
const float minDist = a->collisionRadius + b->collisionRadius;
|
||||||
|
if (minDist <= 0.f) continue;
|
||||||
|
|
||||||
|
const Eigen::Vector2f pa(a->position.x(), a->position.z());
|
||||||
|
const Eigen::Vector2f pb(b->position.x(), b->position.z());
|
||||||
|
const Eigen::Vector2f delta = pb - pa;
|
||||||
|
const float dist = delta.norm();
|
||||||
|
|
||||||
|
if (dist >= minDist) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Eigen::Vector2f normal(1.f, 0.f);
|
||||||
|
if (dist > kMinSeparationEps) {
|
||||||
|
normal = delta / dist;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float penetration = (minDist - dist);
|
||||||
|
const float push = penetration * 0.5f;
|
||||||
|
|
||||||
|
Eigen::Vector3f newA = a->position;
|
||||||
|
Eigen::Vector3f newB = b->position;
|
||||||
|
newA.x() -= normal.x() * push;
|
||||||
|
newA.z() -= normal.y() * push;
|
||||||
|
newA.y() = 0.f;
|
||||||
|
newB.x() += normal.x() * push;
|
||||||
|
newB.z() += normal.y() * push;
|
||||||
|
newB.y() = 0.f;
|
||||||
|
|
||||||
|
if (navigation.isReady()) {
|
||||||
|
const bool aOk = navigation.isWalkable(newA);
|
||||||
|
const bool bOk = navigation.isWalkable(newB);
|
||||||
|
|
||||||
|
if (aOk && bOk) {
|
||||||
|
a->position = newA;
|
||||||
|
b->position = newB;
|
||||||
|
}
|
||||||
|
else if (aOk && !bOk) {
|
||||||
|
a->position = newA;
|
||||||
|
}
|
||||||
|
else if (!aOk && bOk) {
|
||||||
|
b->position = newB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
a->position = newA;
|
||||||
|
b->position = newB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Location::updateDynamicReplans(int64_t deltaMs)
|
||||||
|
{
|
||||||
|
static constexpr float kMovedEps = 0.05f;
|
||||||
|
static constexpr float kReplanTriggerDist = 1.1f;
|
||||||
|
static constexpr int64_t kReplanCooldownMs = 300;
|
||||||
|
|
||||||
|
for (auto it = replanCooldownRemainingMs.begin(); it != replanCooldownRemainingMs.end();) {
|
||||||
|
it->second -= deltaMs;
|
||||||
|
if (it->second <= 0) {
|
||||||
|
it = replanCooldownRemainingMs.erase(it);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Character*> characters;
|
||||||
|
characters.reserve(npcs.size() + 1);
|
||||||
|
if (player) characters.push_back(player.get());
|
||||||
|
for (auto& npc : npcs) if (npc) characters.push_back(npc.get());
|
||||||
|
|
||||||
|
std::vector<Character*> movers;
|
||||||
|
movers.reserve(characters.size());
|
||||||
|
for (Character* c : characters) {
|
||||||
|
if (!c) continue;
|
||||||
|
auto it = lastCharacterPositions.find(c);
|
||||||
|
if (it == lastCharacterPositions.end()) {
|
||||||
|
lastCharacterPositions[c] = c->position;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Eigen::Vector3f prev = it->second;
|
||||||
|
const float moved = (c->position - prev).norm();
|
||||||
|
it->second = c->position;
|
||||||
|
|
||||||
|
if (moved > kMovedEps) {
|
||||||
|
movers.push_back(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movers.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Character* mover : movers) {
|
||||||
|
if (!mover) continue;
|
||||||
|
|
||||||
|
for (Character* walker : characters) {
|
||||||
|
if (!walker || walker == mover) continue;
|
||||||
|
if (walker->hp <= 0.f) continue;
|
||||||
|
if (!walker->isMoving()) continue;
|
||||||
|
|
||||||
|
if (replanCooldownRemainingMs.find(walker) != replanCooldownRemainingMs.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Eigen::Vector3f nextTarget = walker->getCurrentNavigationTarget();
|
||||||
|
const float distToSegment = distancePointToSegmentXZ(mover->position, walker->position, nextTarget);
|
||||||
|
if (distToSegment > kReplanTriggerDist) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
walker->forceReplan();
|
||||||
|
replanCooldownRemainingMs[walker] = kReplanCooldownMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Location::update(int64_t delta)
|
void Location::update(int64_t delta)
|
||||||
{
|
{
|
||||||
if (player) {
|
if (player) {
|
||||||
@@ -652,6 +831,9 @@ namespace ZL
|
|||||||
npc->update(delta);
|
npc->update(delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolveCharacterCollisions();
|
||||||
|
updateDynamicReplans(delta);
|
||||||
|
|
||||||
// Check if player reached target interactive object
|
// Check if player reached target interactive object
|
||||||
if (targetInteractiveObject && player) {
|
if (targetInteractiveObject && player) {
|
||||||
float distToObject = (player->position - targetInteractiveObject->position).norm();
|
float distToObject = (player->position - targetInteractiveObject->position).norm();
|
||||||
@@ -841,10 +1023,6 @@ namespace ZL
|
|||||||
}
|
}
|
||||||
void Location::handleUp(int64_t fingerId, int mx, int my)
|
void Location::handleUp(int64_t fingerId, int mx, int my)
|
||||||
{
|
{
|
||||||
if (dialogueSystem.blocksGameplayInput()) {
|
|
||||||
dialogueSystem.handlePointerReleased(static_cast<float>(mx), Environment::projectionHeight - static_cast<float>(my));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
void Location::handleMotion(int64_t fingerId, int eventX, int eventY, int mx, int my)
|
void Location::handleMotion(int64_t fingerId, int eventX, int eventY, int mx, int my)
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
#include "dialogue/DialogueSystem.h"
|
#include "dialogue/DialogueSystem.h"
|
||||||
#include "SparkEmitter.h"
|
#include "SparkEmitter.h"
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <unordered_map>
|
||||||
namespace ZL
|
namespace ZL
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -112,6 +114,13 @@ namespace ZL
|
|||||||
protected:
|
protected:
|
||||||
Renderer& renderer;
|
Renderer& renderer;
|
||||||
Inventory& inventory;
|
Inventory& inventory;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void resolveCharacterCollisions();
|
||||||
|
void updateDynamicReplans(int64_t deltaMs);
|
||||||
|
|
||||||
|
std::unordered_map<Character*, Eigen::Vector3f> lastCharacterPositions;
|
||||||
|
std::unordered_map<Character*, int64_t> replanCooldownRemainingMs;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZL
|
} // namespace ZL
|
||||||
+3
-169
@@ -4,90 +4,12 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <sstream>
|
|
||||||
#include "GameConstants.h"
|
#include "GameConstants.h"
|
||||||
|
|
||||||
namespace ZL {
|
namespace ZL {
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
|
||||||
static int countWrappedLines(const std::string& text) {
|
|
||||||
if (text.empty()) return 0;
|
|
||||||
int lines = 1;
|
|
||||||
for (char c : text) {
|
|
||||||
if (c == '\n') ++lines;
|
|
||||||
}
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::string limitLines(const std::string& text, int maxLines) {
|
|
||||||
if (maxLines <= 0) return text;
|
|
||||||
std::string out;
|
|
||||||
int lines = 1;
|
|
||||||
for (char c : text) {
|
|
||||||
if (c == '\n') {
|
|
||||||
if (lines >= maxLines) {
|
|
||||||
out += "...";
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
++lines;
|
|
||||||
}
|
|
||||||
out.push_back(c);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::string wrapTextByPixels(const std::string& input, const TextRenderer& textRenderer, float maxWidthPx, float scale, int maxLines = 0) {
|
|
||||||
if (input.empty() || maxWidthPx <= 1.0f) return input;
|
|
||||||
|
|
||||||
std::string output;
|
|
||||||
std::string currentLine;
|
|
||||||
std::string currentWord;
|
|
||||||
auto flushLine = [&]() {
|
|
||||||
if (!currentLine.empty()) {
|
|
||||||
if (!output.empty()) output.push_back('\n');
|
|
||||||
output += currentLine;
|
|
||||||
currentLine.clear();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
auto pushWord = [&](const std::string& word) {
|
|
||||||
if (word.empty()) return;
|
|
||||||
if (currentLine.empty()) {
|
|
||||||
currentLine = word;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const std::string candidate = currentLine + " " + word;
|
|
||||||
if (textRenderer.measureTextWidth(candidate, scale) <= maxWidthPx) {
|
|
||||||
currentLine = candidate;
|
|
||||||
} else {
|
|
||||||
flushLine();
|
|
||||||
currentLine = word;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (size_t i = 0; i < input.size(); ++i) {
|
|
||||||
const char ch = input[i];
|
|
||||||
if (ch == '\n') {
|
|
||||||
pushWord(currentWord);
|
|
||||||
currentWord.clear();
|
|
||||||
flushLine();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (ch == ' ' || ch == '\t' || ch == '\r') {
|
|
||||||
pushWord(currentWord);
|
|
||||||
currentWord.clear();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
currentWord.push_back(ch);
|
|
||||||
}
|
|
||||||
|
|
||||||
pushWord(currentWord);
|
|
||||||
flushLine();
|
|
||||||
|
|
||||||
return limitLines(output, maxLines);
|
|
||||||
}
|
|
||||||
|
|
||||||
static float applyEasing(const std::string& easing, float t) {
|
static float applyEasing(const std::string& easing, float t) {
|
||||||
if (easing == "easein") {
|
if (easing == "easein") {
|
||||||
return t * t;
|
return t * t;
|
||||||
@@ -208,60 +130,14 @@ namespace ZL {
|
|||||||
|
|
||||||
|
|
||||||
// Draw text on top (uses absolute coords, add anim offset manually)
|
// Draw text on top (uses absolute coords, add anim offset manually)
|
||||||
// use left padding, which is required for inventory/quest lists.
|
|
||||||
if (textRenderer && !text.empty()) {
|
if (textRenderer && !text.empty()) {
|
||||||
float tx = rect.x + rect.w / 2.0f + animOffsetX;
|
float cx = rect.x + rect.w / 2.0f + animOffsetX;
|
||||||
if (!textCentered) {
|
float cy = rect.y + rect.h / 2.0f + animOffsetY;
|
||||||
tx = rect.x + textPaddingX + animOffsetX;
|
textRenderer->drawText(text, cx, cy, 1.0f, textCentered, color);
|
||||||
}
|
|
||||||
const float ty = rect.y + rect.h * 0.5f + textPaddingY + animOffsetY;
|
|
||||||
textRenderer->drawText(text, tx, ty, 1.0f, textCentered, color);
|
|
||||||
}
|
}
|
||||||
glEnable(GL_DEPTH_TEST);
|
glEnable(GL_DEPTH_TEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UiTextView::draw(Renderer& renderer) const {
|
|
||||||
(void)renderer;
|
|
||||||
if (!textRenderer || text.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const float scale = 1.0f;
|
|
||||||
|
|
||||||
// Backward compatibility:
|
|
||||||
// Old UI files, including the original inventory panel, positioned TextView
|
|
||||||
// around the rect center. If a TextView does not explicitly request wrapping,
|
|
||||||
// top alignment, padding or line limiting, keep that old behavior.
|
|
||||||
const bool usesModernRectText = wrap || topAligned || paddingX != 0.0f || paddingY != 0.0f || maxLines > 0;
|
|
||||||
if (!usesModernRectText) {
|
|
||||||
textRenderer->drawText(
|
|
||||||
text,
|
|
||||||
rect.x + rect.w * 0.5f,
|
|
||||||
rect.y + rect.h * 0.5f,
|
|
||||||
scale,
|
|
||||||
centered,
|
|
||||||
color
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const float availableWidth = max(1.0f, rect.w - paddingX * 2.0f);
|
|
||||||
const std::string finalText = wrap
|
|
||||||
? wrapTextByPixels(text, *textRenderer, availableWidth, scale, maxLines)
|
|
||||||
: limitLines(text, maxLines);
|
|
||||||
|
|
||||||
float tx = centered ? rect.x + rect.w * 0.5f : rect.x + paddingX;
|
|
||||||
float ty = rect.y + rect.h * 0.5f;
|
|
||||||
|
|
||||||
if (topAligned) {
|
|
||||||
// TextRenderer expects a baseline position. This offset places the first
|
|
||||||
// visible line close to the top inside the TextView rectangle.
|
|
||||||
ty = rect.y + rect.h - paddingY - static_cast<float>(fontSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
textRenderer->drawText(finalText, tx, ty, scale, centered, color);
|
|
||||||
}
|
|
||||||
|
|
||||||
void UiSlider::buildTrackMesh() {
|
void UiSlider::buildTrackMesh() {
|
||||||
trackMesh.data.PositionData.clear();
|
trackMesh.data.PositionData.clear();
|
||||||
trackMesh.data.TexCoordData.clear();
|
trackMesh.data.TexCoordData.clear();
|
||||||
@@ -614,8 +490,6 @@ namespace ZL {
|
|||||||
if (j.contains("fontPath")) tb->fontPath = j["fontPath"].get<std::string>();
|
if (j.contains("fontPath")) tb->fontPath = j["fontPath"].get<std::string>();
|
||||||
if (j.contains("fontSize")) tb->fontSize = j["fontSize"].get<int>();
|
if (j.contains("fontSize")) tb->fontSize = j["fontSize"].get<int>();
|
||||||
if (j.contains("textCentered")) tb->textCentered = j["textCentered"].get<bool>();
|
if (j.contains("textCentered")) tb->textCentered = j["textCentered"].get<bool>();
|
||||||
if (j.contains("textPaddingX")) tb->textPaddingX = j["textPaddingX"].get<float>();
|
|
||||||
if (j.contains("textPaddingY")) tb->textPaddingY = j["textPaddingY"].get<float>();
|
|
||||||
if (j.contains("color") && j["color"].is_array() && j["color"].size() == 4) {
|
if (j.contains("color") && j["color"].is_array() && j["color"].size() == 4) {
|
||||||
for (int i = 0; i < 4; ++i) tb->color[i] = j["color"][i].get<float>();
|
for (int i = 0; i < 4; ++i) tb->color[i] = j["color"][i].get<float>();
|
||||||
}
|
}
|
||||||
@@ -699,11 +573,6 @@ namespace ZL {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (j.contains("centered")) tv->centered = j["centered"].get<bool>();
|
if (j.contains("centered")) tv->centered = j["centered"].get<bool>();
|
||||||
if (j.contains("wrap")) tv->wrap = j["wrap"].get<bool>();
|
|
||||||
if (j.contains("topAligned")) tv->topAligned = j["topAligned"].get<bool>();
|
|
||||||
if (j.contains("paddingX")) tv->paddingX = j["paddingX"].get<float>();
|
|
||||||
if (j.contains("paddingY")) tv->paddingY = j["paddingY"].get<float>();
|
|
||||||
if (j.contains("maxLines")) tv->maxLines = j["maxLines"].get<int>();
|
|
||||||
|
|
||||||
tv->textRenderer = std::make_unique<TextRenderer>();
|
tv->textRenderer = std::make_unique<TextRenderer>();
|
||||||
if (!tv->textRenderer->init(renderer, tv->fontPath, tv->fontSize, zipFile)) {
|
if (!tv->textRenderer->init(renderer, tv->fontPath, tv->fontSize, zipFile)) {
|
||||||
@@ -806,25 +675,6 @@ namespace ZL {
|
|||||||
replaceRoot(newRoot);
|
replaceRoot(newRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UiManager::appendFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) {
|
|
||||||
std::shared_ptr<UiNode> extraRoot = loadUiFromFile(path, renderer, zipFile);
|
|
||||||
if (!extraRoot) {
|
|
||||||
std::cerr << "UiManager: appendFromFile failed: " << path << std::endl;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!root) {
|
|
||||||
replaceRoot(extraRoot);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto& child : extraRoot->children) {
|
|
||||||
root->children.push_back(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
replaceRoot(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void UiManager::layoutNode(const std::shared_ptr<UiNode>& node, float parentX, float parentY, float parentW, float parentH, float finalLocalX, float finalLocalY) {
|
void UiManager::layoutNode(const std::shared_ptr<UiNode>& node, float parentX, float parentY, float parentW, float parentH, float finalLocalX, float finalLocalY) {
|
||||||
|
|
||||||
@@ -1790,15 +1640,6 @@ namespace ZL {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UiManager::setTextColor(const std::string& name, const std::array<float, 4>& color) {
|
|
||||||
auto tv = findTextView(name);
|
|
||||||
if (!tv) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
tv->color = color;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<UiTextButton> UiManager::findTextButton(const std::string& name) {
|
std::shared_ptr<UiTextButton> UiManager::findTextButton(const std::string& name) {
|
||||||
for (auto& tb : textButtons) if (tb->name == name) return tb;
|
for (auto& tb : textButtons) if (tb->name == name) return tb;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -1831,13 +1672,6 @@ namespace ZL {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UiManager::setTextButtonColor(const std::string& name, const std::array<float, 4>& color) {
|
|
||||||
auto tb = findTextButton(name);
|
|
||||||
if (!tb) return false;
|
|
||||||
tb->color = color;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<UiNode> UiManager::findNode(const std::string& name) {
|
std::shared_ptr<UiNode> UiManager::findNode(const std::string& name) {
|
||||||
if (!root) return nullptr;
|
if (!root) return nullptr;
|
||||||
return findNodeByName(root, name);
|
return findNodeByName(root, name);
|
||||||
|
|||||||
+5
-11
@@ -145,8 +145,6 @@ namespace ZL {
|
|||||||
int fontSize = 32;
|
int fontSize = 32;
|
||||||
std::array<float, 4> color = { 1.f, 1.f, 1.f, 1.f };
|
std::array<float, 4> color = { 1.f, 1.f, 1.f, 1.f };
|
||||||
bool textCentered = true;
|
bool textCentered = true;
|
||||||
float textPaddingX = 12.0f;
|
|
||||||
float textPaddingY = 0.0f;
|
|
||||||
|
|
||||||
std::unique_ptr<TextRenderer> textRenderer;
|
std::unique_ptr<TextRenderer> textRenderer;
|
||||||
|
|
||||||
@@ -171,15 +169,14 @@ namespace ZL {
|
|||||||
int fontSize = 32;
|
int fontSize = 32;
|
||||||
std::array<float, 4> color = { 1.f, 1.f, 1.f, 1.f }; // rgba
|
std::array<float, 4> color = { 1.f, 1.f, 1.f, 1.f }; // rgba
|
||||||
bool centered = true;
|
bool centered = true;
|
||||||
bool wrap = false;
|
|
||||||
bool topAligned = true;
|
|
||||||
float paddingX = 0.0f;
|
|
||||||
float paddingY = 0.0f;
|
|
||||||
int maxLines = 0; // 0 = no line limit
|
|
||||||
|
|
||||||
std::unique_ptr<TextRenderer> textRenderer;
|
std::unique_ptr<TextRenderer> textRenderer;
|
||||||
|
|
||||||
void draw(Renderer& renderer) const;
|
void draw(Renderer& renderer) const {
|
||||||
|
if (textRenderer) {
|
||||||
|
textRenderer->drawText(text, rect.x + rect.w / 2, rect.y + rect.h / 2, 1.0f, centered, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct UiTextField {
|
struct UiTextField {
|
||||||
@@ -276,7 +273,6 @@ namespace ZL {
|
|||||||
|
|
||||||
void replaceRoot(std::shared_ptr<UiNode> newRoot);
|
void replaceRoot(std::shared_ptr<UiNode> newRoot);
|
||||||
void loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
|
void loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
|
||||||
void appendFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
|
|
||||||
|
|
||||||
void draw(Renderer& renderer);
|
void draw(Renderer& renderer);
|
||||||
|
|
||||||
@@ -333,7 +329,6 @@ namespace ZL {
|
|||||||
bool setTextButtonCallback(const std::string& name, std::function<void(const std::string&)> cb);
|
bool setTextButtonCallback(const std::string& name, std::function<void(const std::string&)> cb);
|
||||||
bool setTextButtonPressCallback(const std::string& name, std::function<void(const std::string&)> cb);
|
bool setTextButtonPressCallback(const std::string& name, std::function<void(const std::string&)> cb);
|
||||||
bool setTextButtonText(const std::string& name, const std::string& newText);
|
bool setTextButtonText(const std::string& name, const std::string& newText);
|
||||||
bool setTextButtonColor(const std::string& name, const std::array<float, 4>& color);
|
|
||||||
|
|
||||||
bool addSlider(const std::string& name, const UiRect& rect, Renderer& renderer, const std::string& zipFile,
|
bool addSlider(const std::string& name, const UiRect& rect, Renderer& renderer, const std::string& zipFile,
|
||||||
const std::string& trackPath, const std::string& knobPath, float initialValue = 0.0f, bool vertical = true);
|
const std::string& trackPath, const std::string& knobPath, float initialValue = 0.0f, bool vertical = true);
|
||||||
@@ -344,7 +339,6 @@ namespace ZL {
|
|||||||
|
|
||||||
std::shared_ptr<UiTextView> findTextView(const std::string& name);
|
std::shared_ptr<UiTextView> findTextView(const std::string& name);
|
||||||
bool setText(const std::string& name, const std::string& newText);
|
bool setText(const std::string& name, const std::string& newText);
|
||||||
bool setTextColor(const std::string& name, const std::array<float, 4>& color);
|
|
||||||
|
|
||||||
std::shared_ptr<UiTextField> findTextField(const std::string& name);
|
std::shared_ptr<UiTextField> findTextField(const std::string& name);
|
||||||
bool setTextFieldCallback(const std::string& name, std::function<void(const std::string&, const std::string&)> cb);
|
bool setTextFieldCallback(const std::string& name, std::function<void(const std::string&, const std::string&)> cb);
|
||||||
|
|||||||
@@ -180,16 +180,6 @@ CutsceneCameraSegment DialogueDatabase::parseCutsceneCameraSegment(const json& j
|
|||||||
return segment;
|
return segment;
|
||||||
}
|
}
|
||||||
|
|
||||||
CutsceneImageCue DialogueDatabase::parseCutsceneImageCue(const json& j) {
|
|
||||||
CutsceneImageCue cue;
|
|
||||||
cue.path = j.value("path", "");
|
|
||||||
cue.startMs = j.value("startMs", 0);
|
|
||||||
cue.endMs = j.value("endMs", 0);
|
|
||||||
cue.fadeInMs = j.value("fadeInMs", 0);
|
|
||||||
cue.fadeOutMs = j.value("fadeOutMs", 0);
|
|
||||||
return cue;
|
|
||||||
}
|
|
||||||
|
|
||||||
StaticCutsceneDefinition DialogueDatabase::parseCutscene(const json& j) {
|
StaticCutsceneDefinition DialogueDatabase::parseCutscene(const json& j) {
|
||||||
StaticCutsceneDefinition cutscene;
|
StaticCutsceneDefinition cutscene;
|
||||||
cutscene.id = j.value("id", "");
|
cutscene.id = j.value("id", "");
|
||||||
@@ -204,12 +194,6 @@ StaticCutsceneDefinition DialogueDatabase::parseCutscene(const json& j) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (j.contains("images") && j["images"].is_array()) {
|
|
||||||
for (const auto& item : j["images"]) {
|
|
||||||
cutscene.images.push_back(parseCutsceneImageCue(item));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (j.contains("lines") && j["lines"].is_array()) {
|
if (j.contains("lines") && j["lines"].is_array()) {
|
||||||
for (const auto& item : j["lines"]) {
|
for (const auto& item : j["lines"]) {
|
||||||
cutscene.lines.push_back(parseCutsceneLine(item));
|
cutscene.lines.push_back(parseCutsceneLine(item));
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ private:
|
|||||||
static CutsceneLine parseCutsceneLine(const json& j);
|
static CutsceneLine parseCutsceneLine(const json& j);
|
||||||
static CutsceneCameraPose parseCutsceneCameraPose(const json& j);
|
static CutsceneCameraPose parseCutsceneCameraPose(const json& j);
|
||||||
static CutsceneCameraSegment parseCutsceneCameraSegment(const json& j);
|
static CutsceneCameraSegment parseCutsceneCameraSegment(const json& j);
|
||||||
static CutsceneImageCue parseCutsceneImageCue(const json& j);
|
|
||||||
static StaticCutsceneDefinition parseCutscene(const json& j);
|
static StaticCutsceneDefinition parseCutscene(const json& j);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -85,55 +85,9 @@ bool DialogueOverlay::init(Renderer& renderer, const std::string& zipFile) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DialogueOverlay::update(const PresentationModel& model, int deltaMs) {
|
void DialogueOverlay::update(const PresentationModel& model, int deltaMs) {
|
||||||
if (model.mode == PresentationMode::Hidden) {
|
|
||||||
hoveredChoiceIndex = -1;
|
|
||||||
cutsceneSkipHintVisible = false;
|
|
||||||
cutsceneSkipArmed = false;
|
|
||||||
cutsceneSkipHolding = false;
|
|
||||||
cutsceneSkipTriggered = false;
|
|
||||||
cutsceneSkipHintRemainingMs = 0;
|
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
lastChoiceRects.clear();
|
|
||||||
lastDialogueAdvanceRect = {};
|
|
||||||
lastCutsceneAdvanceRect = {};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.mode != PresentationMode::Choice) {
|
if (model.mode != PresentationMode::Choice) {
|
||||||
hoveredChoiceIndex = -1;
|
hoveredChoiceIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.mode != PresentationMode::Cutscene || !model.cutsceneSkippable) {
|
|
||||||
cutsceneSkipHintVisible = false;
|
|
||||||
cutsceneSkipArmed = false;
|
|
||||||
cutsceneSkipHolding = false;
|
|
||||||
cutsceneSkipTriggered = false;
|
|
||||||
cutsceneSkipHintRemainingMs = 0;
|
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int safeDeltaMs = max(deltaMs, 0);
|
|
||||||
|
|
||||||
if (cutsceneSkipHintVisible) {
|
|
||||||
cutsceneSkipHintRemainingMs -= safeDeltaMs;
|
|
||||||
if (cutsceneSkipHintRemainingMs <= 0) {
|
|
||||||
cutsceneSkipHintVisible = false;
|
|
||||||
cutsceneSkipArmed = false;
|
|
||||||
cutsceneSkipHolding = false;
|
|
||||||
cutsceneSkipHintRemainingMs = 0;
|
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cutsceneSkipHolding && cutsceneSkipArmed) {
|
|
||||||
cutsceneSkipHoldElapsedMs += safeDeltaMs;
|
|
||||||
if (cutsceneSkipHoldElapsedMs >= CutsceneSkipHoldDurationMs) {
|
|
||||||
cutsceneSkipTriggered = true;
|
|
||||||
cutsceneSkipHolding = false;
|
|
||||||
cutsceneSkipHoldElapsedMs = CutsceneSkipHoldDurationMs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueOverlay::draw(Renderer& renderer, const PresentationModel& model) {
|
void DialogueOverlay::draw(Renderer& renderer, const PresentationModel& model) {
|
||||||
@@ -141,11 +95,7 @@ void DialogueOverlay::draw(Renderer& renderer, const PresentationModel& model) {
|
|||||||
lastChoiceRects.clear();
|
lastChoiceRects.clear();
|
||||||
lastDialogueAdvanceRect = {};
|
lastDialogueAdvanceRect = {};
|
||||||
lastCutsceneAdvanceRect = {};
|
lastCutsceneAdvanceRect = {};
|
||||||
cutsceneSkipHintVisible = false;
|
cutsceneAdvanceEnabled = false;
|
||||||
cutsceneSkipArmed = false;
|
|
||||||
cutsceneSkipHolding = false;
|
|
||||||
cutsceneSkipHintRemainingMs = 0;
|
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,11 +116,7 @@ void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel&
|
|||||||
|
|
||||||
lastDialogueAdvanceRect = { portraitRect.x, portraitRect.y, textboxRect.x + textboxRect.w - portraitRect.x, textboxRect.h };
|
lastDialogueAdvanceRect = { portraitRect.x, portraitRect.y, textboxRect.x + textboxRect.w - portraitRect.x, textboxRect.h };
|
||||||
lastCutsceneAdvanceRect = {};
|
lastCutsceneAdvanceRect = {};
|
||||||
cutsceneSkipHintVisible = false;
|
cutsceneAdvanceEnabled = false;
|
||||||
cutsceneSkipArmed = false;
|
|
||||||
cutsceneSkipHolding = false;
|
|
||||||
cutsceneSkipHintRemainingMs = 0;
|
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
|
|
||||||
if (!portraitQuad.initialized || portraitQuad.rect.w != portraitRect.w || portraitQuad.rect.h != portraitRect.h ||
|
if (!portraitQuad.initialized || portraitQuad.rect.w != portraitRect.w || portraitQuad.rect.h != portraitRect.h ||
|
||||||
portraitQuad.rect.x != portraitRect.x || portraitQuad.rect.y != portraitRect.y) {
|
portraitQuad.rect.x != portraitRect.x || portraitQuad.rect.y != portraitRect.y) {
|
||||||
@@ -205,16 +151,8 @@ void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel&
|
|||||||
nameRenderer->drawText(model.speaker, nameX, nameY, 1.0f, false, { 1.0f, 0.88f, 0.45f, 1.0f });
|
nameRenderer->drawText(model.speaker, nameX, nameY, 1.0f, false, { 1.0f, 0.88f, 0.45f, 1.0f });
|
||||||
}
|
}
|
||||||
|
|
||||||
// const std::string wrappedBody = wrapText(model.visibleText, 90);
|
const std::string wrappedBody = wrapText(model.visibleText, 90);
|
||||||
// bodyRenderer->drawText(wrappedBody, bodyX, bodyY, 1.0f, false, { 1.0f, 1.0f, 1.0f, 1.0f });
|
bodyRenderer->drawText(wrappedBody, bodyX, bodyY, 1.0f, false, { 1.0f, 1.0f, 1.0f, 1.0f });
|
||||||
// const std::string wrappedBody = wrapText(model.visibleText, 56);
|
|
||||||
// bodyRenderer->drawText(wrappedBody, bodyX, bodyY, 1.0f, false, { 1.0f, 1.0f, 1.0f, 1.0f });
|
|
||||||
|
|
||||||
const float bodyTextScale = 1.0f;
|
|
||||||
const float bodyMaxWidthPx = textboxRect.w - 48.0f;
|
|
||||||
|
|
||||||
const std::string wrappedBody = wrapTextToWidth(model.visibleText, *bodyRenderer, bodyMaxWidthPx, bodyTextScale);
|
|
||||||
bodyRenderer->drawText(wrappedBody, bodyX, bodyY, bodyTextScale, false, { 1.0f, 1.0f, 1.0f, 1.0f });
|
|
||||||
|
|
||||||
lastChoiceRects.clear();
|
lastChoiceRects.clear();
|
||||||
if (model.mode == PresentationMode::Choice) {
|
if (model.mode == PresentationMode::Choice) {
|
||||||
@@ -258,21 +196,11 @@ void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel&
|
|||||||
? std::array<float, 4>{0.82f, 0.82f, 0.82f, 1.0f}
|
? std::array<float, 4>{0.82f, 0.82f, 0.82f, 1.0f}
|
||||||
: std::array<float, 4>{ 1.0f, 0.93f, 0.65f, 1.0f };
|
: std::array<float, 4>{ 1.0f, 0.93f, 0.65f, 1.0f };
|
||||||
|
|
||||||
const float choiceTextScale = 1.0f;
|
|
||||||
const float choiceMaxWidthPx = rect.w - 28.0f;
|
|
||||||
|
|
||||||
const std::string wrappedChoiceText = wrapTextToWidth(
|
|
||||||
model.choices[i].text,
|
|
||||||
*choiceRenderer,
|
|
||||||
choiceMaxWidthPx,
|
|
||||||
choiceTextScale
|
|
||||||
);
|
|
||||||
|
|
||||||
choiceRenderer->drawText(
|
choiceRenderer->drawText(
|
||||||
wrappedChoiceText,
|
wrapText(model.choices[i].text, 52),
|
||||||
rect.x + 14.0f,
|
rect.x + 14.0f,
|
||||||
rect.y + 9.0f,
|
rect.y + 9.0f,
|
||||||
choiceTextScale,
|
1.0f,
|
||||||
false,
|
false,
|
||||||
isHighlighted ? std::array<float, 4>{1.0f, 1.0f, 1.0f, 1.0f} : color
|
isHighlighted ? std::array<float, 4>{1.0f, 1.0f, 1.0f, 1.0f} : color
|
||||||
);
|
);
|
||||||
@@ -390,57 +318,48 @@ void DialogueOverlay::drawCutscene(Renderer& renderer, const PresentationModel&
|
|||||||
|
|
||||||
lastDialogueAdvanceRect = {};
|
lastDialogueAdvanceRect = {};
|
||||||
lastCutsceneAdvanceRect = subtitleRect;
|
lastCutsceneAdvanceRect = subtitleRect;
|
||||||
|
cutsceneAdvanceEnabled = model.showCutsceneSubtitle;
|
||||||
|
|
||||||
|
std::shared_ptr<Texture> bgTexture = model.backgroundPath.empty() ? nullptr : loadTextureCached(model.backgroundPath);
|
||||||
|
|
||||||
glEnable(GL_BLEND);
|
glEnable(GL_BLEND);
|
||||||
|
renderer.shaderManager.PushShader(defaultShaderName);
|
||||||
renderer.shaderManager.PushShader("cutsceneFade");
|
|
||||||
renderer.RenderUniform1i(textureUniformName, 0);
|
renderer.RenderUniform1i(textureUniformName, 0);
|
||||||
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
|
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
|
||||||
renderer.PushMatrix();
|
renderer.PushMatrix();
|
||||||
renderer.LoadIdentity();
|
renderer.LoadIdentity();
|
||||||
|
|
||||||
const UiRect screenRect{ 0.0f, 0.0f, W, H };
|
if (bgTexture) {
|
||||||
|
const float texW = static_cast<float>(bgTexture->getWidth());
|
||||||
|
const float texH = static_cast<float>(bgTexture->getHeight());
|
||||||
|
|
||||||
std::vector<PresentedCutsceneImage> imageLayers = model.cutsceneImages;
|
ResolvedViewport currentViewport{};
|
||||||
if (imageLayers.empty() && !model.backgroundPath.empty()) {
|
|
||||||
imageLayers.push_back({ model.backgroundPath, 1.0f });
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const PresentedCutsceneImage& layer : imageLayers) {
|
|
||||||
const auto texture = loadTextureCached(layer.path);
|
|
||||||
if (!texture) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const float texW = static_cast<float>(texture->getWidth());
|
|
||||||
const float texH = static_cast<float>(texture->getHeight());
|
|
||||||
|
|
||||||
ResolvedViewport layerViewport{};
|
|
||||||
|
|
||||||
if (model.cutsceneCamera.active) {
|
if (model.cutsceneCamera.active) {
|
||||||
const ResolvedViewport fromViewport = resolveViewportPose(model.cutsceneCamera.from, texW, texH, W, H);
|
const ResolvedViewport fromViewport = resolveViewportPose(model.cutsceneCamera.from, texW, texH, W, H);
|
||||||
const ResolvedViewport toViewport = resolveViewportPose(model.cutsceneCamera.to, texW, texH, W, H);
|
const ResolvedViewport toViewport = resolveViewportPose(model.cutsceneCamera.to, texW, texH, W, H);
|
||||||
layerViewport = blendViewport(
|
|
||||||
|
currentViewport = blendViewport(
|
||||||
fromViewport,
|
fromViewport,
|
||||||
toViewport,
|
toViewport,
|
||||||
std::clamp(model.cutsceneCamera.t, 0.0f, 1.0f)
|
std::clamp(model.cutsceneCamera.t, 0.0f, 1.0f)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
layerViewport = resolveViewportPose(CutsceneCameraPose{}, texW, texH, W, H);
|
currentViewport = resolveViewportPose(CutsceneCameraPose{}, texW, texH, W, H);
|
||||||
}
|
}
|
||||||
|
|
||||||
const float halfW = layerViewport.widthPx * 0.5f;
|
const float halfW = currentViewport.widthPx * 0.5f;
|
||||||
const float halfH = layerViewport.heightPx * 0.5f;
|
const float halfH = currentViewport.heightPx * 0.5f;
|
||||||
const float rotationRad = layerViewport.rotationDeg * 3.14159265358979323846f / 180.0f;
|
const float rotationRad = currentViewport.rotationDeg * 3.14159265358979323846f / 180.0f;
|
||||||
|
|
||||||
const float c = std::cos(rotationRad);
|
const float c = std::cos(rotationRad);
|
||||||
const float s = std::sin(rotationRad);
|
const float s = std::sin(rotationRad);
|
||||||
|
|
||||||
auto rotatePoint = [&](float x, float y) -> Eigen::Vector2f {
|
auto rotatePoint = [&](float x, float y) -> Eigen::Vector2f {
|
||||||
return {
|
return {
|
||||||
layerViewport.centerXPx + x * c - y * s,
|
currentViewport.centerXPx + x * c - y * s,
|
||||||
layerViewport.centerYPx + x * s + y * c
|
currentViewport.centerYPx + x * s + y * c
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -457,6 +376,7 @@ void DialogueOverlay::drawCutscene(Renderer& renderer, const PresentationModel&
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const UiRect screenRect{ 0.0f, 0.0f, W, H };
|
||||||
backgroundQuad.rebuildWithUV(
|
backgroundQuad.rebuildWithUV(
|
||||||
screenRect,
|
screenRect,
|
||||||
toUV(srcBL),
|
toUV(srcBL),
|
||||||
@@ -465,47 +385,14 @@ void DialogueOverlay::drawCutscene(Renderer& renderer, const PresentationModel&
|
|||||||
toUV(srcBR)
|
toUV(srcBR)
|
||||||
);
|
);
|
||||||
|
|
||||||
renderer.RenderUniform1f("uAlpha", std::clamp(layer.alpha, 0.0f, 1.0f));
|
drawQuad(renderer, backgroundQuad, bgTexture);
|
||||||
drawQuad(renderer, backgroundQuad, texture);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.PopMatrix();
|
|
||||||
renderer.PopProjectionMatrix();
|
|
||||||
renderer.shaderManager.PopShader();
|
|
||||||
|
|
||||||
// UI quads over the image: subtitle panel and skip progress hint background.
|
|
||||||
renderer.shaderManager.PushShader(defaultShaderName);
|
|
||||||
renderer.RenderUniform1i(textureUniformName, 0);
|
|
||||||
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
|
|
||||||
renderer.PushMatrix();
|
|
||||||
renderer.LoadIdentity();
|
|
||||||
|
|
||||||
if (model.showCutsceneSubtitle) {
|
if (model.showCutsceneSubtitle) {
|
||||||
subtitleQuad.rebuild(subtitleRect);
|
subtitleQuad.rebuild(subtitleRect);
|
||||||
drawQuad(renderer, subtitleQuad, cutsceneSubtitleTexture);
|
drawQuad(renderer, subtitleQuad, cutsceneSubtitleTexture);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.cutsceneSkippable && cutsceneSkipHintVisible) {
|
|
||||||
const UiRect hintBg{ W - 250.0f, H - 62.0f, 226.0f, 42.0f };
|
|
||||||
skipHintBgQuad.rebuild(hintBg);
|
|
||||||
drawQuad(renderer, skipHintBgQuad, choiceOptionalTexture);
|
|
||||||
|
|
||||||
const UiRect progressBg{ W - 232.0f, H - 34.0f, 190.0f, 7.0f };
|
|
||||||
skipProgressBgQuad.rebuild(progressBg);
|
|
||||||
drawQuad(renderer, skipProgressBgQuad, choiceOptionalTexture);
|
|
||||||
|
|
||||||
if (cutsceneSkipHolding) {
|
|
||||||
const float progress = std::clamp(
|
|
||||||
static_cast<float>(cutsceneSkipHoldElapsedMs) / static_cast<float>(CutsceneSkipHoldDurationMs),
|
|
||||||
0.0f,
|
|
||||||
1.0f
|
|
||||||
);
|
|
||||||
const UiRect progressFill{ progressBg.x, progressBg.y, progressBg.w * progress, progressBg.h };
|
|
||||||
skipProgressFillQuad.rebuild(progressFill);
|
|
||||||
drawQuad(renderer, skipProgressFillQuad, choiceMainTexture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.PopMatrix();
|
renderer.PopMatrix();
|
||||||
renderer.PopProjectionMatrix();
|
renderer.PopProjectionMatrix();
|
||||||
renderer.shaderManager.PopShader();
|
renderer.shaderManager.PopShader();
|
||||||
@@ -521,85 +408,24 @@ void DialogueOverlay::drawCutscene(Renderer& renderer, const PresentationModel&
|
|||||||
{ 1.0f, 0.88f, 0.45f, 1.0f }
|
{ 1.0f, 0.88f, 0.45f, 1.0f }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const float subtitleTextScale = 1.0f;
|
|
||||||
const float subtitleMaxWidthPx = subtitleRect.w - 48.0f;
|
|
||||||
|
|
||||||
const std::string wrappedSubtitle = wrapTextToWidth(
|
|
||||||
model.visibleText,
|
|
||||||
*cutsceneRenderer,
|
|
||||||
subtitleMaxWidthPx,
|
|
||||||
subtitleTextScale
|
|
||||||
);
|
|
||||||
|
|
||||||
cutsceneRenderer->drawText(
|
cutsceneRenderer->drawText(
|
||||||
wrappedSubtitle,
|
wrapText(model.visibleText, 62),
|
||||||
subtitleRect.x + 24.0f,
|
subtitleRect.x + 24.0f,
|
||||||
subtitleRect.y + 30.0f,
|
subtitleRect.y + 30.0f,
|
||||||
subtitleTextScale,
|
1.0f,
|
||||||
false,
|
false,
|
||||||
{ 1.0f, 1.0f, 1.0f, 1.0f }
|
{ 1.0f, 1.0f, 1.0f, 1.0f }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.cutsceneSkippable && cutsceneSkipHintVisible) {
|
|
||||||
choiceRenderer->drawText(
|
|
||||||
cutsceneSkipHolding ? "Hold to skip..." : "Hold LMB to skip",
|
|
||||||
W - 232.0f,
|
|
||||||
H - 50.0f,
|
|
||||||
0.85f,
|
|
||||||
false,
|
|
||||||
{ 1.0f, 1.0f, 1.0f, 0.95f }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
glDisable(GL_BLEND);
|
glDisable(GL_BLEND);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DialogueOverlay::consumeSkipRequested() {
|
|
||||||
const bool result = cutsceneSkipTriggered;
|
|
||||||
cutsceneSkipTriggered = false;
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
cutsceneSkipHintVisible = false;
|
|
||||||
cutsceneSkipArmed = false;
|
|
||||||
cutsceneSkipHolding = false;
|
|
||||||
cutsceneSkipHintRemainingMs = 0;
|
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DialogueOverlay::handlePointerDown(float x, float y, const PresentationModel& model) {
|
void DialogueOverlay::handlePointerDown(float x, float y, const PresentationModel& model) {
|
||||||
(void)x;
|
|
||||||
(void)y;
|
|
||||||
|
|
||||||
if (model.mode == PresentationMode::Choice) {
|
if (model.mode == PresentationMode::Choice) {
|
||||||
handlePointerMoved(x, y, model);
|
handlePointerMoved(x, y, model);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.mode == PresentationMode::Cutscene && model.cutsceneSkippable) {
|
|
||||||
// First click/tap only arms skip and shows the hint for a short time.
|
|
||||||
// It does not immediately start skipping, to avoid accidental skip.
|
|
||||||
if (!cutsceneSkipArmed) {
|
|
||||||
cutsceneSkipHintVisible = true;
|
|
||||||
cutsceneSkipArmed = true;
|
|
||||||
cutsceneSkipHolding = false;
|
|
||||||
cutsceneSkipTriggered = false;
|
|
||||||
cutsceneSkipHintRemainingMs = CutsceneSkipHintDurationMs;
|
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Once armed, holding anywhere on the screen starts skip progress.
|
|
||||||
cutsceneSkipHintVisible = true;
|
|
||||||
cutsceneSkipHintRemainingMs = CutsceneSkipHintDurationMs;
|
|
||||||
cutsceneSkipHolding = true;
|
|
||||||
cutsceneSkipTriggered = false;
|
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueOverlay::handlePointerMoved(float x, float y, const PresentationModel& model) {
|
void DialogueOverlay::handlePointerMoved(float x, float y, const PresentationModel& model) {
|
||||||
@@ -632,16 +458,15 @@ bool DialogueOverlay::handlePointerReleased(float x, float y, const Presentation
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (model.mode == PresentationMode::Dialogue) {
|
if (model.mode == PresentationMode::Dialogue) {
|
||||||
outAdvanceDialogue = rectContains(lastDialogueAdvanceRect, x, y);
|
if (lastDialogueAdvanceRect.contains(x, y)) {
|
||||||
return outAdvanceDialogue;
|
outAdvanceDialogue = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.mode == PresentationMode::Cutscene) {
|
if (model.mode == PresentationMode::Cutscene) {
|
||||||
if (cutsceneSkipHolding && cutsceneSkipHoldElapsedMs < CutsceneSkipHoldDurationMs) {
|
return cutsceneAdvanceEnabled && lastCutsceneAdvanceRect.contains(x, y);
|
||||||
cutsceneSkipHolding = false;
|
|
||||||
cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -700,71 +525,6 @@ std::string DialogueOverlay::wrapText(const std::string& input, size_t maxLineLe
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string DialogueOverlay::wrapTextToWidth(const std::string& input, const TextRenderer& textRenderer, float maxWidthPx, float scale)
|
|
||||||
{
|
|
||||||
if (input.empty() || maxWidthPx <= 1.0f) {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string output;
|
|
||||||
std::string currentLine;
|
|
||||||
std::string currentWord;
|
|
||||||
|
|
||||||
auto flushLine = [&]() {
|
|
||||||
if (!currentLine.empty()) {
|
|
||||||
if (!output.empty()) {
|
|
||||||
output.push_back('\n');
|
|
||||||
}
|
|
||||||
output += currentLine;
|
|
||||||
currentLine.clear();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
auto pushWord = [&](const std::string& word) {
|
|
||||||
if (word.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentLine.empty()) {
|
|
||||||
currentLine = word;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string candidate = currentLine + " " + word;
|
|
||||||
if (textRenderer.measureTextWidth(candidate, scale) <= maxWidthPx) {
|
|
||||||
currentLine = candidate;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
flushLine();
|
|
||||||
currentLine = word;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (size_t i = 0; i < input.size(); ++i) {
|
|
||||||
const char ch = input[i];
|
|
||||||
|
|
||||||
if (ch == '\n') {
|
|
||||||
pushWord(currentWord);
|
|
||||||
currentWord.clear();
|
|
||||||
flushLine();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ch == ' ' || ch == '\t' || ch == '\r') {
|
|
||||||
pushWord(currentWord);
|
|
||||||
currentWord.clear();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentWord.push_back(ch);
|
|
||||||
}
|
|
||||||
|
|
||||||
pushWord(currentWord);
|
|
||||||
flushLine();
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DialogueOverlay::rectContains(const UiRect& rect, float x, float y) {
|
bool DialogueOverlay::rectContains(const UiRect& rect, float x, float y) {
|
||||||
return x >= rect.x && x <= rect.x + rect.w && y >= rect.y && y <= rect.y + rect.h;
|
return x >= rect.x && x <= rect.x + rect.w && y >= rect.y && y <= rect.y + rect.h;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ public:
|
|||||||
void handlePointerDown(float x, float y, const PresentationModel& model);
|
void handlePointerDown(float x, float y, const PresentationModel& model);
|
||||||
void handlePointerMoved(float x, float y, const PresentationModel& model);
|
void handlePointerMoved(float x, float y, const PresentationModel& model);
|
||||||
bool handlePointerReleased(float x, float y, const PresentationModel& model, int& outChoiceIndex, bool& outAdvanceDialogue);
|
bool handlePointerReleased(float x, float y, const PresentationModel& model, int& outChoiceIndex, bool& outAdvanceDialogue);
|
||||||
bool consumeSkipRequested();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct TexturedQuad {
|
struct TexturedQuad {
|
||||||
@@ -60,22 +59,10 @@ private:
|
|||||||
mutable std::vector<UiRect> lastChoiceRects;
|
mutable std::vector<UiRect> lastChoiceRects;
|
||||||
mutable UiRect lastDialogueAdvanceRect{};
|
mutable UiRect lastDialogueAdvanceRect{};
|
||||||
mutable UiRect lastCutsceneAdvanceRect{};
|
mutable UiRect lastCutsceneAdvanceRect{};
|
||||||
mutable UiRect lastCutsceneSkipRect{};
|
mutable bool cutsceneAdvanceEnabled = false;
|
||||||
|
|
||||||
int hoveredChoiceIndex = -1;
|
int hoveredChoiceIndex = -1;
|
||||||
|
|
||||||
// Cutscene skip UX:
|
|
||||||
// First LMB/tap anywhere arms skip and shows a hint for 5 seconds.
|
|
||||||
// While armed, holding LMB/touch anywhere for 3.5 seconds requests skip.
|
|
||||||
bool cutsceneSkipHintVisible = false;
|
|
||||||
bool cutsceneSkipArmed = false;
|
|
||||||
bool cutsceneSkipHolding = false;
|
|
||||||
bool cutsceneSkipTriggered = false;
|
|
||||||
int cutsceneSkipHintRemainingMs = 0;
|
|
||||||
int cutsceneSkipHoldElapsedMs = 0;
|
|
||||||
static constexpr int CutsceneSkipHintDurationMs = 5000;
|
|
||||||
static constexpr int CutsceneSkipHoldDurationMs = 3500;
|
|
||||||
|
|
||||||
std::unique_ptr<TextRenderer> nameRenderer;
|
std::unique_ptr<TextRenderer> nameRenderer;
|
||||||
std::unique_ptr<TextRenderer> bodyRenderer;
|
std::unique_ptr<TextRenderer> bodyRenderer;
|
||||||
std::unique_ptr<TextRenderer> choiceRenderer;
|
std::unique_ptr<TextRenderer> choiceRenderer;
|
||||||
@@ -85,9 +72,6 @@ private:
|
|||||||
TexturedQuad textboxQuad;
|
TexturedQuad textboxQuad;
|
||||||
TexturedQuad subtitleQuad;
|
TexturedQuad subtitleQuad;
|
||||||
TexturedQuad backgroundQuad;
|
TexturedQuad backgroundQuad;
|
||||||
TexturedQuad skipHintBgQuad;
|
|
||||||
TexturedQuad skipProgressBgQuad;
|
|
||||||
TexturedQuad skipProgressFillQuad;
|
|
||||||
mutable std::vector<TexturedQuad> choiceQuads;
|
mutable std::vector<TexturedQuad> choiceQuads;
|
||||||
|
|
||||||
std::unordered_map<std::string, std::shared_ptr<Texture>> textureCache;
|
std::unordered_map<std::string, std::shared_ptr<Texture>> textureCache;
|
||||||
@@ -99,7 +83,6 @@ private:
|
|||||||
void drawQuad(Renderer& renderer, const TexturedQuad& quad, const std::shared_ptr<Texture>& texture) const;
|
void drawQuad(Renderer& renderer, const TexturedQuad& quad, const std::shared_ptr<Texture>& texture) const;
|
||||||
|
|
||||||
static std::string wrapText(const std::string& input, size_t maxLineLength);
|
static std::string wrapText(const std::string& input, size_t maxLineLength);
|
||||||
static std::string wrapTextToWidth(const std::string& input, const TextRenderer& textRenderer, float maxWidthPx, float scale);
|
|
||||||
static bool rectContains(const UiRect& rect, float x, float y);
|
static bool rectContains(const UiRect& rect, float x, float y);
|
||||||
|
|
||||||
static float lerpFloat(float a, float b, float t);
|
static float lerpFloat(float a, float b, float t);
|
||||||
|
|||||||
@@ -27,13 +27,12 @@ bool DialogueRuntime::startDialogue(const std::string& dialogueId) {
|
|||||||
currentNodeId.clear();
|
currentNodeId.clear();
|
||||||
pendingNodeAfterCutscene.clear();
|
pendingNodeAfterCutscene.clear();
|
||||||
visibleChoices.clear();
|
visibleChoices.clear();
|
||||||
selectedChoice = -1;
|
selectedChoice = 0;
|
||||||
revealCharacters = 0.0f;
|
revealCharacters = 0.0f;
|
||||||
currentCutsceneLine = -1;
|
currentCutsceneLine = -1;
|
||||||
cutsceneTimerMs = 0;
|
cutsceneTimerMs = 0;
|
||||||
cutsceneElapsedMs = 0;
|
cutsceneElapsedMs = 0;
|
||||||
cutsceneTotalDurationMs = 0;
|
cutsceneTotalDurationMs = 0;
|
||||||
currentCutsceneBackground.clear();
|
|
||||||
presentation = {};
|
presentation = {};
|
||||||
presentation.dialogueId = dialogue->id;
|
presentation.dialogueId = dialogue->id;
|
||||||
|
|
||||||
@@ -46,13 +45,12 @@ void DialogueRuntime::stop() {
|
|||||||
currentNodeId.clear();
|
currentNodeId.clear();
|
||||||
pendingNodeAfterCutscene.clear();
|
pendingNodeAfterCutscene.clear();
|
||||||
visibleChoices.clear();
|
visibleChoices.clear();
|
||||||
selectedChoice = -1;
|
selectedChoice = 0;
|
||||||
revealCharacters = 0.0f;
|
revealCharacters = 0.0f;
|
||||||
currentCutsceneLine = -1;
|
currentCutsceneLine = -1;
|
||||||
cutsceneTimerMs = 0;
|
cutsceneTimerMs = 0;
|
||||||
cutsceneElapsedMs = 0;
|
cutsceneElapsedMs = 0;
|
||||||
cutsceneTotalDurationMs = 0;
|
cutsceneTotalDurationMs = 0;
|
||||||
currentCutsceneBackground.clear();
|
|
||||||
mode = Mode::Inactive;
|
mode = Mode::Inactive;
|
||||||
presentation = {};
|
presentation = {};
|
||||||
}
|
}
|
||||||
@@ -157,11 +155,11 @@ void DialogueRuntime::confirmAdvance() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mode == Mode::WaitingForChoice) {
|
if (mode == Mode::WaitingForChoice) {
|
||||||
if (visibleChoices.empty() || selectedChoice < 0 || selectedChoice >= static_cast<int>(visibleChoices.size())) {
|
if (visibleChoices.empty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Choice& choice = visibleChoices[selectedChoice];
|
const Choice& choice = visibleChoices[std::clamp(selectedChoice, 0, static_cast<int>(visibleChoices.size()) - 1)];
|
||||||
if (choice.consumeOnce && !choice.id.empty()) {
|
if (choice.consumeOnce && !choice.id.empty()) {
|
||||||
consumedChoices.insert(choice.id);
|
consumedChoices.insert(choice.id);
|
||||||
}
|
}
|
||||||
@@ -171,8 +169,14 @@ void DialogueRuntime::confirmAdvance() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mode == Mode::PlayingCutscene) {
|
if (mode == Mode::PlayingCutscene) {
|
||||||
|
if (!activeCutscene || activeCutscene->lines.empty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentCutsceneLine >= 0 && currentCutsceneLine < static_cast<int>(activeCutscene->lines.size())) {
|
||||||
|
advanceCutsceneLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueRuntime::moveSelection(int delta) {
|
void DialogueRuntime::moveSelection(int delta) {
|
||||||
@@ -204,43 +208,6 @@ void DialogueRuntime::selectChoice(int index) {
|
|||||||
presentation.selectedChoice = selectedChoice;
|
presentation.selectedChoice = selectedChoice;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DialogueRuntime::canSkipCurrentCutscene() const {
|
|
||||||
return mode == Mode::PlayingCutscene && activeCutscene && activeCutscene->skippable;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DialogueRuntime::skipCurrentCutscene() {
|
|
||||||
if (!canSkipCurrentCutscene()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multi-image cutscenes skip to the next image cue first.
|
|
||||||
// This matches the desired behavior: skip advances the visual chapter,
|
|
||||||
// not necessarily the whole cutscene immediately.
|
|
||||||
if (!activeCutscene->images.empty()) {
|
|
||||||
int nextImageStartMs = -1;
|
|
||||||
|
|
||||||
for (const CutsceneImageCue& cue : activeCutscene->images) {
|
|
||||||
if (cue.path.empty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (cue.startMs > cutsceneElapsedMs) {
|
|
||||||
if (nextImageStartMs < 0 || cue.startMs < nextImageStartMs) {
|
|
||||||
nextImageStartMs = cue.startMs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextImageStartMs >= 0) {
|
|
||||||
cutsceneElapsedMs = nextImageStartMs;
|
|
||||||
syncCutsceneLineToElapsedTime();
|
|
||||||
refreshCutscenePresentation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finishCutscene();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DialogueRuntime::setFlag(const std::string& name, int value) {
|
void DialogueRuntime::setFlag(const std::string& name, int value) {
|
||||||
flags[name] = value;
|
flags[name] = value;
|
||||||
}
|
}
|
||||||
@@ -358,12 +325,10 @@ void DialogueRuntime::presentLine(const Node& node) {
|
|||||||
presentation.portraitPath = node.portrait;
|
presentation.portraitPath = node.portrait;
|
||||||
presentation.backgroundPath.clear();
|
presentation.backgroundPath.clear();
|
||||||
presentation.choices.clear();
|
presentation.choices.clear();
|
||||||
presentation.selectedChoice = -1;
|
presentation.selectedChoice = 0;
|
||||||
presentation.revealCompleted = node.text.empty();
|
presentation.revealCompleted = node.text.empty();
|
||||||
presentation.showCutsceneSubtitle = false;
|
presentation.showCutsceneSubtitle = false;
|
||||||
presentation.cutsceneSkippable = false;
|
|
||||||
presentation.cutsceneCamera = {};
|
presentation.cutsceneCamera = {};
|
||||||
presentation.cutsceneImages.clear();
|
|
||||||
|
|
||||||
if (presentation.revealCompleted) {
|
if (presentation.revealCompleted) {
|
||||||
presentation.visibleText = node.text;
|
presentation.visibleText = node.text;
|
||||||
@@ -407,9 +372,7 @@ void DialogueRuntime::presentChoices(const Node& node) {
|
|||||||
presentation.selectedChoice = -1;
|
presentation.selectedChoice = -1;
|
||||||
presentation.revealCompleted = true;
|
presentation.revealCompleted = true;
|
||||||
presentation.showCutsceneSubtitle = false;
|
presentation.showCutsceneSubtitle = false;
|
||||||
presentation.cutsceneSkippable = false;
|
|
||||||
presentation.cutsceneCamera = {};
|
presentation.cutsceneCamera = {};
|
||||||
presentation.cutsceneImages.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::string& nextNodeAfterCutscene) {
|
void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::string& nextNodeAfterCutscene) {
|
||||||
@@ -437,21 +400,7 @@ void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::st
|
|||||||
cutsceneElapsedMs = 0;
|
cutsceneElapsedMs = 0;
|
||||||
cutsceneTimerMs = 0;
|
cutsceneTimerMs = 0;
|
||||||
currentCutsceneLine = activeCutscene->lines.empty() ? -1 : 0;
|
currentCutsceneLine = activeCutscene->lines.empty() ? -1 : 0;
|
||||||
int imageTrackDurationMs = 0;
|
cutsceneTotalDurationMs = std::max(activeCutscene->durationMs, computeCameraTrackDurationMs(*activeCutscene));
|
||||||
for (size_t i = 0; i < activeCutscene->images.size(); ++i) {
|
|
||||||
const CutsceneImageCue& cue = activeCutscene->images[i];
|
|
||||||
int cueEnd = cue.endMs;
|
|
||||||
if (cueEnd <= cue.startMs) {
|
|
||||||
if (i + 1 < activeCutscene->images.size()) {
|
|
||||||
cueEnd = std::max(activeCutscene->images[i + 1].startMs, cue.startMs);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
cueEnd = cue.startMs + std::max(cue.fadeInMs, 0) + std::max(cue.fadeOutMs, 0) + 1000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
imageTrackDurationMs = std::max(imageTrackDurationMs, cueEnd);
|
|
||||||
}
|
|
||||||
cutsceneTotalDurationMs = std::max({ activeCutscene->durationMs, computeCameraTrackDurationMs(*activeCutscene), imageTrackDurationMs });
|
|
||||||
if (cutsceneTotalDurationMs <= 0 && activeCutscene->lines.empty()) {
|
if (cutsceneTotalDurationMs <= 0 && activeCutscene->lines.empty()) {
|
||||||
cutsceneTotalDurationMs = 3000;
|
cutsceneTotalDurationMs = 3000;
|
||||||
}
|
}
|
||||||
@@ -471,7 +420,6 @@ void DialogueRuntime::finishCutscene() {
|
|||||||
cutsceneTimerMs = 0;
|
cutsceneTimerMs = 0;
|
||||||
cutsceneElapsedMs = 0;
|
cutsceneElapsedMs = 0;
|
||||||
cutsceneTotalDurationMs = 0;
|
cutsceneTotalDurationMs = 0;
|
||||||
currentCutsceneBackground.clear();
|
|
||||||
if (!pendingNodeAfterCutscene.empty()) {
|
if (!pendingNodeAfterCutscene.empty()) {
|
||||||
const std::string nextNode = pendingNodeAfterCutscene;
|
const std::string nextNode = pendingNodeAfterCutscene;
|
||||||
pendingNodeAfterCutscene.clear();
|
pendingNodeAfterCutscene.clear();
|
||||||
@@ -482,35 +430,6 @@ void DialogueRuntime::finishCutscene() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueRuntime::syncCutsceneLineToElapsedTime() {
|
|
||||||
if (!activeCutscene || activeCutscene->lines.empty()) {
|
|
||||||
currentCutsceneLine = -1;
|
|
||||||
cutsceneTimerMs = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int elapsed = std::max(cutsceneElapsedMs, 0);
|
|
||||||
int accumulatedMs = 0;
|
|
||||||
|
|
||||||
for (size_t i = 0; i < activeCutscene->lines.size(); ++i) {
|
|
||||||
const CutsceneLine& line = activeCutscene->lines[i];
|
|
||||||
const int durationMs = (line.durationMs > 0)
|
|
||||||
? line.durationMs
|
|
||||||
: computeFallbackCutsceneDurationMs(line.text);
|
|
||||||
|
|
||||||
if (elapsed < accumulatedMs + durationMs) {
|
|
||||||
currentCutsceneLine = static_cast<int>(i);
|
|
||||||
cutsceneTimerMs = std::max(0, elapsed - accumulatedMs);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
accumulatedMs += durationMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentCutsceneLine = -1;
|
|
||||||
cutsceneTimerMs = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DialogueRuntime::advanceCutsceneLine() {
|
void DialogueRuntime::advanceCutsceneLine() {
|
||||||
if (!activeCutscene) {
|
if (!activeCutscene) {
|
||||||
stop();
|
stop();
|
||||||
@@ -572,78 +491,6 @@ CutsceneCameraBlendState DialogueRuntime::evaluateCutsceneCameraBlend() const {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<PresentedCutsceneImage> DialogueRuntime::evaluateCutsceneImages() const {
|
|
||||||
std::vector<PresentedCutsceneImage> result;
|
|
||||||
if (!activeCutscene) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& fallbackPath = !currentCutsceneBackground.empty()
|
|
||||||
? currentCutsceneBackground
|
|
||||||
: activeCutscene->background;
|
|
||||||
|
|
||||||
if (activeCutscene->images.empty()) {
|
|
||||||
if (!fallbackPath.empty()) {
|
|
||||||
result.push_back({ fallbackPath, 1.0f });
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int effectiveTotalDuration = (cutsceneTotalDurationMs > 0) ? cutsceneTotalDurationMs : std::max(activeCutscene->durationMs, 1);
|
|
||||||
const int now = std::max(cutsceneElapsedMs, 0);
|
|
||||||
|
|
||||||
for (size_t i = 0; i < activeCutscene->images.size(); ++i) {
|
|
||||||
const CutsceneImageCue& cue = activeCutscene->images[i];
|
|
||||||
if (cue.path.empty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int startMs = std::max(cue.startMs, 0);
|
|
||||||
int endMs = cue.endMs;
|
|
||||||
if (endMs <= startMs) {
|
|
||||||
if (i + 1 < activeCutscene->images.size()) {
|
|
||||||
endMs = std::max(activeCutscene->images[i + 1].startMs, startMs + 1);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
endMs = effectiveTotalDuration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (endMs <= startMs) {
|
|
||||||
endMs = startMs + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (now < startMs || now > endMs) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
float alpha = 1.0f;
|
|
||||||
if (cue.fadeInMs > 0 && now < startMs + cue.fadeInMs) {
|
|
||||||
alpha = std::clamp(
|
|
||||||
static_cast<float>(now - startMs) / static_cast<float>(cue.fadeInMs),
|
|
||||||
0.0f,
|
|
||||||
1.0f
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (alpha > 0.0f) {
|
|
||||||
result.push_back({ cue.path, alpha });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Safety fallback: never leave the cutscene without an opaque image layer.
|
|
||||||
if (result.empty() && !fallbackPath.empty()) {
|
|
||||||
result.push_back({ fallbackPath, 1.0f });
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the first active layer is still fading in, put an opaque fallback/base below it.
|
|
||||||
// This prevents the world from becoming visible behind the cutscene.
|
|
||||||
if (!result.empty() && result.front().alpha < 0.999f && !fallbackPath.empty() && result.front().path != fallbackPath) {
|
|
||||||
result.insert(result.begin(), { fallbackPath, 1.0f });
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DialogueRuntime::refreshCutscenePresentation() {
|
void DialogueRuntime::refreshCutscenePresentation() {
|
||||||
if (!activeCutscene) {
|
if (!activeCutscene) {
|
||||||
return;
|
return;
|
||||||
@@ -652,11 +499,9 @@ void DialogueRuntime::refreshCutscenePresentation() {
|
|||||||
presentation.mode = PresentationMode::Cutscene;
|
presentation.mode = PresentationMode::Cutscene;
|
||||||
presentation.backgroundPath = activeCutscene->background;
|
presentation.backgroundPath = activeCutscene->background;
|
||||||
presentation.cutsceneCamera = evaluateCutsceneCameraBlend();
|
presentation.cutsceneCamera = evaluateCutsceneCameraBlend();
|
||||||
presentation.cutsceneImages = evaluateCutsceneImages();
|
|
||||||
presentation.cutsceneSkippable = activeCutscene->skippable;
|
|
||||||
|
|
||||||
presentation.choices.clear();
|
presentation.choices.clear();
|
||||||
presentation.selectedChoice = -1;
|
presentation.selectedChoice = 0;
|
||||||
presentation.revealCompleted = true;
|
presentation.revealCompleted = true;
|
||||||
|
|
||||||
const bool hasSubtitle = currentCutsceneLine >= 0 && currentCutsceneLine < static_cast<int>(activeCutscene->lines.size());
|
const bool hasSubtitle = currentCutsceneLine >= 0 && currentCutsceneLine < static_cast<int>(activeCutscene->lines.size());
|
||||||
@@ -671,24 +516,26 @@ void DialogueRuntime::refreshCutscenePresentation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
|
const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
|
||||||
|
/*<<<<<<< HEAD
|
||||||
|
|
||||||
if (!line.background.empty()) {
|
if (!line.background.empty()) {
|
||||||
currentCutsceneBackground = line.background;
|
currentCutsceneBackground = line.background;
|
||||||
}
|
}
|
||||||
|
|
||||||
presentation.mode = PresentationMode::Cutscene;
|
presentation.mode = PresentationMode::Cutscene;
|
||||||
|
=======
|
||||||
|
>>>>>>> witcher001-cutscene*/
|
||||||
presentation.speaker = line.speaker;
|
presentation.speaker = line.speaker;
|
||||||
presentation.fullText = line.text;
|
presentation.fullText = line.text;
|
||||||
presentation.visibleText = line.text;
|
presentation.visibleText = line.text;
|
||||||
presentation.portraitPath = line.portrait;
|
presentation.portraitPath = line.portrait;
|
||||||
|
/*<<<<<<< HEAD
|
||||||
//presentation.backgroundPath = activeCutscene->background;
|
//presentation.backgroundPath = activeCutscene->background;
|
||||||
presentation.backgroundPath = currentCutsceneBackground;
|
presentation.backgroundPath = currentCutsceneBackground;
|
||||||
presentation.choices.clear();
|
presentation.choices.clear();
|
||||||
presentation.selectedChoice = 0;
|
presentation.selectedChoice = 0;
|
||||||
presentation.revealCompleted = true;
|
presentation.revealCompleted = true;
|
||||||
|
=======*/
|
||||||
|
|
||||||
std::cout << "[CUTSCENE] lines=" << activeCutscene->lines.size()
|
std::cout << "[CUTSCENE] lines=" << activeCutscene->lines.size()
|
||||||
<< " current=" << currentCutsceneLine
|
<< " current=" << currentCutsceneLine
|
||||||
@@ -722,6 +569,7 @@ float DialogueRuntime::applyEasing(EasingType easing, float t) {
|
|||||||
default:
|
default:
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
//>>>>>>> witcher001-cutscene
|
||||||
}
|
}
|
||||||
|
|
||||||
int DialogueRuntime::computeFallbackCutsceneDurationMs(const std::string& text) {
|
int DialogueRuntime::computeFallbackCutsceneDurationMs(const std::string& text) {
|
||||||
@@ -782,15 +630,13 @@ bool DialogueRuntime::restoreSaveState(const json& state) {
|
|||||||
|
|
||||||
const std::string nodeId = state.value("currentNodeId", "");
|
const std::string nodeId = state.value("currentNodeId", "");
|
||||||
pendingNodeAfterCutscene = state.value("pendingNodeAfterCutscene", "");
|
pendingNodeAfterCutscene = state.value("pendingNodeAfterCutscene", "");
|
||||||
selectedChoice = state.value("selectedChoice", -1);
|
selectedChoice = state.value("selectedChoice", 0);
|
||||||
currentCutsceneLine = state.value("currentCutsceneLine", -1);
|
currentCutsceneLine = state.value("currentCutsceneLine", -1);
|
||||||
cutsceneTimerMs = state.value("cutsceneTimerMs", 0);
|
cutsceneTimerMs = state.value("cutsceneTimerMs", 0);
|
||||||
|
|
||||||
const bool ok = nodeId.empty() ? true : enterNode(nodeId);
|
const bool ok = nodeId.empty() ? true : enterNode(nodeId);
|
||||||
if (mode == Mode::WaitingForChoice && !visibleChoices.empty()) {
|
if (mode == Mode::WaitingForChoice && !visibleChoices.empty()) {
|
||||||
if (selectedChoice >= 0) {
|
|
||||||
selectedChoice = std::clamp(selectedChoice, 0, static_cast<int>(visibleChoices.size()) - 1);
|
selectedChoice = std::clamp(selectedChoice, 0, static_cast<int>(visibleChoices.size()) - 1);
|
||||||
}
|
|
||||||
presentation.selectedChoice = selectedChoice;
|
presentation.selectedChoice = selectedChoice;
|
||||||
}
|
}
|
||||||
return ok;
|
return ok;
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ public:
|
|||||||
void confirmAdvance();
|
void confirmAdvance();
|
||||||
void moveSelection(int delta);
|
void moveSelection(int delta);
|
||||||
void selectChoice(int index);
|
void selectChoice(int index);
|
||||||
bool canSkipCurrentCutscene() const;
|
|
||||||
void skipCurrentCutscene();
|
|
||||||
|
|
||||||
const PresentationModel& getPresentation() const { return presentation; }
|
const PresentationModel& getPresentation() const { return presentation; }
|
||||||
|
|
||||||
@@ -60,7 +58,7 @@ private:
|
|||||||
PresentationModel presentation;
|
PresentationModel presentation;
|
||||||
Mode mode = Mode::Inactive;
|
Mode mode = Mode::Inactive;
|
||||||
|
|
||||||
int selectedChoice = -1;
|
int selectedChoice = 0;
|
||||||
float revealCharacters = 0.0f;
|
float revealCharacters = 0.0f;
|
||||||
float revealSpeedCharsPerSecond = 52.0f;
|
float revealSpeedCharsPerSecond = 52.0f;
|
||||||
|
|
||||||
@@ -79,12 +77,10 @@ private:
|
|||||||
void presentChoices(const Node& node);
|
void presentChoices(const Node& node);
|
||||||
void startCutscene(const std::string& cutsceneId, const std::string& nextNodeAfterCutscene);
|
void startCutscene(const std::string& cutsceneId, const std::string& nextNodeAfterCutscene);
|
||||||
void finishCutscene();
|
void finishCutscene();
|
||||||
void syncCutsceneLineToElapsedTime();
|
|
||||||
|
|
||||||
void advanceCutsceneLine();
|
void advanceCutsceneLine();
|
||||||
void refreshCutscenePresentation();
|
void refreshCutscenePresentation();
|
||||||
CutsceneCameraBlendState evaluateCutsceneCameraBlend() const;
|
CutsceneCameraBlendState evaluateCutsceneCameraBlend() const;
|
||||||
std::vector<PresentedCutsceneImage> evaluateCutsceneImages() const;
|
|
||||||
|
|
||||||
static float applyEasing(EasingType easing, float t);
|
static float applyEasing(EasingType easing, float t);
|
||||||
static int computeFallbackCutsceneDurationMs(const std::string& text);
|
static int computeFallbackCutsceneDurationMs(const std::string& text);
|
||||||
|
|||||||
@@ -29,10 +29,6 @@ void DialogueSystem::update(int deltaMs, const Eigen::Vector3f& playerPosition)
|
|||||||
}
|
}
|
||||||
|
|
||||||
runtime.update(deltaMs);
|
runtime.update(deltaMs);
|
||||||
overlay.update(runtime.getPresentation(), deltaMs);
|
|
||||||
if (overlay.consumeSkipRequested()) {
|
|
||||||
runtime.skipCurrentCutscene();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueSystem::draw(Renderer& renderer) {
|
void DialogueSystem::draw(Renderer& renderer) {
|
||||||
@@ -44,18 +40,6 @@ bool DialogueSystem::handleKeyDown(SDL_Keycode key) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (runtime.isPlayingCutscene()) {
|
|
||||||
switch (key) {
|
|
||||||
case SDLK_RETURN:
|
|
||||||
case SDLK_SPACE:
|
|
||||||
case SDLK_e:
|
|
||||||
case SDLK_ESCAPE:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case SDLK_RETURN:
|
case SDLK_RETURN:
|
||||||
case SDLK_SPACE:
|
case SDLK_SPACE:
|
||||||
@@ -105,11 +89,7 @@ bool DialogueSystem::handlePointerReleased(float x, float y) {
|
|||||||
bool advanceDialogue = false;
|
bool advanceDialogue = false;
|
||||||
const PresentationModel& model = runtime.getPresentation();
|
const PresentationModel& model = runtime.getPresentation();
|
||||||
if (!overlay.handlePointerReleased(x, y, model, choiceIndex, advanceDialogue)) {
|
if (!overlay.handlePointerReleased(x, y, model, choiceIndex, advanceDialogue)) {
|
||||||
if (overlay.consumeSkipRequested()) {
|
return false;
|
||||||
runtime.skipCurrentCutscene();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return runtime.isPlayingCutscene();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (choiceIndex >= 0) {
|
if (choiceIndex >= 0) {
|
||||||
@@ -123,11 +103,6 @@ bool DialogueSystem::handlePointerReleased(float x, float y) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (overlay.consumeSkipRequested()) {
|
|
||||||
runtime.skipCurrentCutscene();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -135,14 +135,6 @@ struct CutsceneCameraSegment {
|
|||||||
EasingType easing = EasingType::EaseInOutSine;
|
EasingType easing = EasingType::EaseInOutSine;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct CutsceneImageCue {
|
|
||||||
std::string path;
|
|
||||||
int startMs = 0;
|
|
||||||
int endMs = 0;
|
|
||||||
int fadeInMs = 0;
|
|
||||||
int fadeOutMs = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct StaticCutsceneDefinition {
|
struct StaticCutsceneDefinition {
|
||||||
std::string id;
|
std::string id;
|
||||||
std::string background;
|
std::string background;
|
||||||
@@ -150,7 +142,6 @@ struct StaticCutsceneDefinition {
|
|||||||
bool skippable = true;
|
bool skippable = true;
|
||||||
int durationMs = 0;
|
int durationMs = 0;
|
||||||
std::vector<CutsceneCameraSegment> cameraTrack;
|
std::vector<CutsceneCameraSegment> cameraTrack;
|
||||||
std::vector<CutsceneImageCue> images;
|
|
||||||
std::vector<CutsceneLine> lines;
|
std::vector<CutsceneLine> lines;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -160,11 +151,6 @@ struct PresentedChoice {
|
|||||||
ChoiceKind kind = ChoiceKind::Main;
|
ChoiceKind kind = ChoiceKind::Main;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PresentedCutsceneImage {
|
|
||||||
std::string path;
|
|
||||||
float alpha = 1.0f;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class PresentationMode {
|
enum class PresentationMode {
|
||||||
Hidden,
|
Hidden,
|
||||||
Dialogue,
|
Dialogue,
|
||||||
@@ -191,19 +177,17 @@ struct PresentationModel {
|
|||||||
int selectedChoice = -1;
|
int selectedChoice = -1;
|
||||||
bool revealCompleted = true;
|
bool revealCompleted = true;
|
||||||
bool showCutsceneSubtitle = false;
|
bool showCutsceneSubtitle = false;
|
||||||
bool cutsceneSkippable = false;
|
|
||||||
|
|
||||||
CutsceneCameraBlendState cutsceneCamera;
|
CutsceneCameraBlendState cutsceneCamera;
|
||||||
std::vector<PresentedCutsceneImage> cutsceneImages;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SaveState {
|
struct SaveState {
|
||||||
std::string dialogueId;
|
std::string dialogueId;
|
||||||
std::string currentNodeId;
|
std::string currentNodeId;
|
||||||
std::string pendingNodeAfterCutscene;
|
std::string pendingNodeAfterCutscene;
|
||||||
std::unordered_map<std::string, int> flags;
|
std::unordered_set<std::string, int> flags;
|
||||||
std::unordered_set<std::string> consumedChoices;
|
std::unordered_set<std::string> consumedChoices;
|
||||||
int selectedChoice = -1;
|
int selectedChoice = 0;
|
||||||
int currentCutsceneLine = -1;
|
int currentCutsceneLine = -1;
|
||||||
int cutsceneTimerMs = 0;
|
int cutsceneTimerMs = 0;
|
||||||
bool active = false;
|
bool active = false;
|
||||||
|
|||||||
+355
-11
@@ -38,6 +38,52 @@ namespace {
|
|||||||
|
|
||||||
return polygon;
|
return polygon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float distancePointToSegment2D(const Eigen::Vector2f& p,
|
||||||
|
const Eigen::Vector2f& a,
|
||||||
|
const Eigen::Vector2f& b)
|
||||||
|
{
|
||||||
|
const Eigen::Vector2f ab = b - a;
|
||||||
|
const float abLenSq = ab.squaredNorm();
|
||||||
|
if (abLenSq <= 1e-8f) {
|
||||||
|
return (p - a).norm();
|
||||||
|
}
|
||||||
|
float t = (p - a).dot(ab) / abLenSq;
|
||||||
|
t = (std::max)(0.0f, (std::min)(1.0f, t));
|
||||||
|
const Eigen::Vector2f proj = a + t * ab;
|
||||||
|
return (p - proj).norm();
|
||||||
|
}
|
||||||
|
|
||||||
|
Eigen::Vector2f closestPointOnSegment2D(const Eigen::Vector2f& p,
|
||||||
|
const Eigen::Vector2f& a,
|
||||||
|
const Eigen::Vector2f& b)
|
||||||
|
{
|
||||||
|
const Eigen::Vector2f ab = b - a;
|
||||||
|
const float abLenSq = ab.squaredNorm();
|
||||||
|
if (abLenSq <= 1e-8f) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
float t = (p - a).dot(ab) / abLenSq;
|
||||||
|
t = (std::max)(0.0f, (std::min)(1.0f, t));
|
||||||
|
return a + t * ab;
|
||||||
|
}
|
||||||
|
|
||||||
|
float distancePointToPolygonEdges(const Eigen::Vector2f& p,
|
||||||
|
const std::vector<Eigen::Vector2f>& polygon)
|
||||||
|
{
|
||||||
|
if (polygon.size() < 2) {
|
||||||
|
return (std::numeric_limits<float>::max)();
|
||||||
|
}
|
||||||
|
|
||||||
|
float best = (std::numeric_limits<float>::max)();
|
||||||
|
for (size_t i = 0; i < polygon.size(); ++i) {
|
||||||
|
const Eigen::Vector2f& a = polygon[i];
|
||||||
|
const Eigen::Vector2f& b = polygon[(i + 1) % polygon.size()];
|
||||||
|
best = (std::min)(best, distancePointToSegment2D(p, a, b));
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PathFinder::build(const std::vector<ObstacleMesh>& obstacleMeshes,
|
void PathFinder::build(const std::vector<ObstacleMesh>& obstacleMeshes,
|
||||||
@@ -74,8 +120,8 @@ std::vector<Eigen::Vector3f> PathFinder::findPath(const Eigen::Vector3f& start,
|
|||||||
|
|
||||||
Cell startCell;
|
Cell startCell;
|
||||||
Cell endCell;
|
Cell endCell;
|
||||||
if (!findNearestWalkableCell(start, startCell) ||
|
if (!findNearestWalkableCell(start, startCell, walkable) ||
|
||||||
!findNearestWalkableCell(end, endCell)) {
|
!findNearestWalkableCell(end, endCell, walkable)) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +167,7 @@ std::vector<Eigen::Vector3f> PathFinder::findPath(const Eigen::Vector3f& start,
|
|||||||
|
|
||||||
for (const auto& offset : offsets) {
|
for (const auto& offset : offsets) {
|
||||||
Cell next{ currentCell.x + offset[0], currentCell.z + offset[1] };
|
Cell next{ currentCell.x + offset[0], currentCell.z + offset[1] };
|
||||||
if (!isCellWalkable(next)) {
|
if (!isCellWalkable(next, walkable)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +175,7 @@ std::vector<Eigen::Vector3f> PathFinder::findPath(const Eigen::Vector3f& start,
|
|||||||
if (diagonal) {
|
if (diagonal) {
|
||||||
Cell horizontal{ currentCell.x + offset[0], currentCell.z };
|
Cell horizontal{ currentCell.x + offset[0], currentCell.z };
|
||||||
Cell vertical{ currentCell.x, currentCell.z + offset[1] };
|
Cell vertical{ currentCell.x, currentCell.z + offset[1] };
|
||||||
if (!isCellWalkable(horizontal) || !isCellWalkable(vertical)) {
|
if (!isCellWalkable(horizontal, walkable) || !isCellWalkable(vertical, walkable)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,7 +206,161 @@ std::vector<Eigen::Vector3f> PathFinder::findPath(const Eigen::Vector3f& start,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::reverse(cells.begin(), cells.end());
|
std::reverse(cells.begin(), cells.end());
|
||||||
cells = smoothCells(cells);
|
cells = smoothCells(cells, walkable);
|
||||||
|
|
||||||
|
std::vector<Eigen::Vector3f> path;
|
||||||
|
path.reserve(cells.size());
|
||||||
|
for (const Cell& cell : cells) {
|
||||||
|
path.push_back(cellCenter(cell));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path.empty() && (path.front() - Eigen::Vector3f(start.x(), floorY, start.z())).norm() < cellSize * 0.75f) {
|
||||||
|
path.erase(path.begin());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path.empty()) {
|
||||||
|
Cell requestedEndCell;
|
||||||
|
if (worldToCell(end, requestedEndCell) &&
|
||||||
|
requestedEndCell.x == endCell.x &&
|
||||||
|
requestedEndCell.z == endCell.z) {
|
||||||
|
path.back() = Eigen::Vector3f(end.x(), floorY, end.z());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Eigen::Vector3f> PathFinder::findPath(const Eigen::Vector3f& start,
|
||||||
|
const Eigen::Vector3f& end,
|
||||||
|
const std::vector<DynamicObstacle>& dynamicObstacles) const
|
||||||
|
{
|
||||||
|
if (!ready || walkable.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned char> walkableGrid = walkable;
|
||||||
|
if (!dynamicObstacles.empty()) {
|
||||||
|
for (const DynamicObstacle& obstacle : dynamicObstacles) {
|
||||||
|
const float radius = obstacle.radius + agentRadius;
|
||||||
|
if (radius <= 0.0f) continue;
|
||||||
|
|
||||||
|
const float minWorldX = obstacle.position.x() - radius;
|
||||||
|
const float maxWorldX = obstacle.position.x() + radius;
|
||||||
|
const float minWorldZ = obstacle.position.z() - radius;
|
||||||
|
const float maxWorldZ = obstacle.position.z() + radius;
|
||||||
|
|
||||||
|
const int minCellX = (std::max)(0, static_cast<int>(std::floor((minWorldX - minX) / cellSize)));
|
||||||
|
const int maxCellX = (std::min)(gridWidth - 1, static_cast<int>(std::floor((maxWorldX - minX) / cellSize)));
|
||||||
|
const int minCellZ = (std::max)(0, static_cast<int>(std::floor((minWorldZ - minZ) / cellSize)));
|
||||||
|
const int maxCellZ = (std::min)(gridDepth - 1, static_cast<int>(std::floor((maxWorldZ - minZ) / cellSize)));
|
||||||
|
|
||||||
|
const float radiusSq = radius * radius;
|
||||||
|
for (int z = minCellZ; z <= maxCellZ; ++z) {
|
||||||
|
for (int x = minCellX; x <= maxCellX; ++x) {
|
||||||
|
const Cell cell{ x, z };
|
||||||
|
const Eigen::Vector3f center = cellCenter(cell);
|
||||||
|
const float dx = center.x() - obstacle.position.x();
|
||||||
|
const float dz = center.z() - obstacle.position.z();
|
||||||
|
if (dx * dx + dz * dz <= radiusSq) {
|
||||||
|
const int idx = indexOf(cell);
|
||||||
|
if (idx >= 0 && idx < static_cast<int>(walkableGrid.size())) {
|
||||||
|
walkableGrid[static_cast<size_t>(idx)] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cell startCell;
|
||||||
|
Cell endCell;
|
||||||
|
if (!findNearestWalkableCell(start, startCell, walkableGrid) ||
|
||||||
|
!findNearestWalkableCell(end, endCell, walkableGrid)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startCell.x == endCell.x && startCell.z == endCell.z) {
|
||||||
|
return { cellCenter(endCell) };
|
||||||
|
}
|
||||||
|
|
||||||
|
struct QueueNode {
|
||||||
|
int index = 0;
|
||||||
|
float priority = 0.0f;
|
||||||
|
|
||||||
|
bool operator<(const QueueNode& other) const
|
||||||
|
{
|
||||||
|
return priority > other.priority;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const int cellCount = gridWidth * gridDepth;
|
||||||
|
std::vector<float> cost(static_cast<size_t>(cellCount), INF_COST);
|
||||||
|
std::vector<int> cameFrom(static_cast<size_t>(cellCount), -1);
|
||||||
|
std::priority_queue<QueueNode> open;
|
||||||
|
|
||||||
|
const int startIndex = indexOf(startCell);
|
||||||
|
const int endIndex = indexOf(endCell);
|
||||||
|
|
||||||
|
cost[static_cast<size_t>(startIndex)] = 0.0f;
|
||||||
|
open.push({ startIndex, 0.0f });
|
||||||
|
|
||||||
|
static const int offsets[8][2] = {
|
||||||
|
{ 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 },
|
||||||
|
{ 1, 1 }, { 1, -1 }, { -1, 1 }, { -1, -1 }
|
||||||
|
};
|
||||||
|
|
||||||
|
while (!open.empty()) {
|
||||||
|
const QueueNode current = open.top();
|
||||||
|
open.pop();
|
||||||
|
|
||||||
|
if (current.index == endIndex) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Cell currentCell{ current.index % gridWidth, current.index / gridWidth };
|
||||||
|
|
||||||
|
for (const auto& offset : offsets) {
|
||||||
|
Cell next{ currentCell.x + offset[0], currentCell.z + offset[1] };
|
||||||
|
if (!isCellWalkable(next, walkableGrid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool diagonal = offset[0] != 0 && offset[1] != 0;
|
||||||
|
if (diagonal) {
|
||||||
|
Cell horizontal{ currentCell.x + offset[0], currentCell.z };
|
||||||
|
Cell vertical{ currentCell.x, currentCell.z + offset[1] };
|
||||||
|
if (!isCellWalkable(horizontal, walkableGrid) || !isCellWalkable(vertical, walkableGrid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const int nextIndex = indexOf(next);
|
||||||
|
const float stepCost = diagonal ? 1.41421356f : 1.0f;
|
||||||
|
const float newCost = cost[static_cast<size_t>(current.index)] + stepCost;
|
||||||
|
if (newCost >= cost[static_cast<size_t>(nextIndex)]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cost[static_cast<size_t>(nextIndex)] = newCost;
|
||||||
|
cameFrom[static_cast<size_t>(nextIndex)] = current.index;
|
||||||
|
const float priority = newCost + distanceCells(next, endCell);
|
||||||
|
open.push({ nextIndex, priority });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cameFrom[static_cast<size_t>(endIndex)] == -1) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Cell> cells;
|
||||||
|
for (int current = endIndex; current != -1; current = cameFrom[static_cast<size_t>(current)]) {
|
||||||
|
cells.push_back({ current % gridWidth, current / gridWidth });
|
||||||
|
if (current == startIndex) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::reverse(cells.begin(), cells.end());
|
||||||
|
cells = smoothCells(cells, walkableGrid);
|
||||||
|
|
||||||
std::vector<Eigen::Vector3f> path;
|
std::vector<Eigen::Vector3f> path;
|
||||||
path.reserve(cells.size());
|
path.reserve(cells.size());
|
||||||
@@ -206,7 +406,7 @@ bool PathFinder::setAreaAvailable(const std::string& areaName, bool available)
|
|||||||
bool PathFinder::isWalkable(const Eigen::Vector3f& point) const
|
bool PathFinder::isWalkable(const Eigen::Vector3f& point) const
|
||||||
{
|
{
|
||||||
Cell cell;
|
Cell cell;
|
||||||
return worldToCell(point, cell) && isCellWalkable(cell);
|
return worldToCell(point, cell) && isCellWalkable(cell, walkable);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PathFinder::loadConfig(const std::string& configPath, const std::string& zipPath)
|
void PathFinder::loadConfig(const std::string& configPath, const std::string& zipPath)
|
||||||
@@ -215,7 +415,9 @@ void PathFinder::loadConfig(const std::string& configPath, const std::string& zi
|
|||||||
agentRadius = 0.45f;
|
agentRadius = 0.45f;
|
||||||
floorY = 0.0f;
|
floorY = 0.0f;
|
||||||
objectPadding = 0.25f;
|
objectPadding = 0.25f;
|
||||||
|
boundaryPadding = 0.0f;
|
||||||
areas.clear();
|
areas.clear();
|
||||||
|
obstaclePolygons.clear();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
std::string content;
|
std::string content;
|
||||||
@@ -236,6 +438,7 @@ void PathFinder::loadConfig(const std::string& configPath, const std::string& zi
|
|||||||
agentRadius = root.value("agentRadius", agentRadius);
|
agentRadius = root.value("agentRadius", agentRadius);
|
||||||
floorY = root.value("floorY", floorY);
|
floorY = root.value("floorY", floorY);
|
||||||
objectPadding = root.value("objectPadding", objectPadding);
|
objectPadding = root.value("objectPadding", objectPadding);
|
||||||
|
boundaryPadding = root.value("boundaryPadding", boundaryPadding);
|
||||||
|
|
||||||
if (!root.contains("areas") || !root["areas"].is_array()) {
|
if (!root.contains("areas") || !root["areas"].is_array()) {
|
||||||
std::cerr << "[nav] Navigation config has no 'areas' array: " << configPath << "\n";
|
std::cerr << "[nav] Navigation config has no 'areas' array: " << configPath << "\n";
|
||||||
@@ -259,6 +462,18 @@ void PathFinder::loadConfig(const std::string& configPath, const std::string& zi
|
|||||||
|
|
||||||
areas.push_back(area);
|
areas.push_back(area);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (root.contains("obstacles") && root["obstacles"].is_array()) {
|
||||||
|
for (const auto& item : root["obstacles"]) {
|
||||||
|
ObstaclePolygon obstacle;
|
||||||
|
obstacle.name = item.value("name", "");
|
||||||
|
obstacle.polygon = readPolygon(item);
|
||||||
|
if (obstacle.polygon.size() < 3) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
obstaclePolygons.push_back(std::move(obstacle));
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::cerr << "[nav] Failed to load config '" << configPath << "': " << e.what() << "\n";
|
std::cerr << "[nav] Failed to load config '" << configPath << "': " << e.what() << "\n";
|
||||||
}
|
}
|
||||||
@@ -266,6 +481,7 @@ void PathFinder::loadConfig(const std::string& configPath, const std::string& zi
|
|||||||
cellSize = (std::max)(cellSize, 0.1f);
|
cellSize = (std::max)(cellSize, 0.1f);
|
||||||
agentRadius = (std::max)(agentRadius, 0.0f);
|
agentRadius = (std::max)(agentRadius, 0.0f);
|
||||||
objectPadding = (std::max)(objectPadding, 0.0f);
|
objectPadding = (std::max)(objectPadding, 0.0f);
|
||||||
|
boundaryPadding = (std::max)(boundaryPadding, 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PathFinder::resetGridBounds()
|
void PathFinder::resetGridBounds()
|
||||||
@@ -304,6 +520,12 @@ void PathFinder::resetGridBounds()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const ObstaclePolygon& obstacle : obstaclePolygons) {
|
||||||
|
for (const Eigen::Vector2f& point : obstacle.polygon) {
|
||||||
|
includePoint(point.x(), point.y());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const float padding = cellSize * 2.0f + agentRadius + objectPadding;
|
const float padding = cellSize * 2.0f + agentRadius + objectPadding;
|
||||||
minX -= padding;
|
minX -= padding;
|
||||||
minZ -= padding;
|
minZ -= padding;
|
||||||
@@ -319,20 +541,75 @@ void PathFinder::rebuildWalkableGrid()
|
|||||||
walkable.assign(static_cast<size_t>(gridWidth * gridDepth), 0);
|
walkable.assign(static_cast<size_t>(gridWidth * gridDepth), 0);
|
||||||
markAvailableAreasWalkable();
|
markAvailableAreasWalkable();
|
||||||
markObstacleMeshesBlocked();
|
markObstacleMeshesBlocked();
|
||||||
|
markObstaclePolygonsBlocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PathFinder::markAvailableAreasWalkable()
|
void PathFinder::markAvailableAreasWalkable()
|
||||||
{
|
{
|
||||||
|
const auto insideAvailableArea = [this](const Eigen::Vector2f& point) {
|
||||||
|
for (const NavigationArea& area : areas) {
|
||||||
|
if (!area.available) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (pointInPolygon(point.x(), point.y(), area.polygon)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto externalBoundaryDistance = [this, &insideAvailableArea](const Eigen::Vector2f& point) {
|
||||||
|
if (boundaryPadding <= 0.0f) {
|
||||||
|
return (std::numeric_limits<float>::max)();
|
||||||
|
}
|
||||||
|
|
||||||
|
const float sampleOffset = (std::max)(cellSize * 0.25f, 0.05f);
|
||||||
|
float best = (std::numeric_limits<float>::max)();
|
||||||
|
|
||||||
|
for (const NavigationArea& area : areas) {
|
||||||
|
if (!area.available || area.polygon.size() < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < area.polygon.size(); ++i) {
|
||||||
|
const Eigen::Vector2f& a = area.polygon[i];
|
||||||
|
const Eigen::Vector2f& b = area.polygon[(i + 1) % area.polygon.size()];
|
||||||
|
const Eigen::Vector2f edge = b - a;
|
||||||
|
if (edge.squaredNorm() <= 1e-8f) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Eigen::Vector2f closest = closestPointOnSegment2D(point, a, b);
|
||||||
|
Eigen::Vector2f normal(-edge.y(), edge.x());
|
||||||
|
normal.normalize();
|
||||||
|
|
||||||
|
const bool sideAInside = insideAvailableArea(closest + normal * sampleOffset);
|
||||||
|
const bool sideBInside = insideAvailableArea(closest - normal * sampleOffset);
|
||||||
|
if (sideAInside && sideBInside) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
best = (std::min)(best, distancePointToSegment2D(point, a, b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best;
|
||||||
|
};
|
||||||
|
|
||||||
for (int z = 0; z < gridDepth; ++z) {
|
for (int z = 0; z < gridDepth; ++z) {
|
||||||
for (int x = 0; x < gridWidth; ++x) {
|
for (int x = 0; x < gridWidth; ++x) {
|
||||||
const Cell cell{ x, z };
|
const Cell cell{ x, z };
|
||||||
const Eigen::Vector3f center = cellCenter(cell);
|
const Eigen::Vector3f center = cellCenter(cell);
|
||||||
|
const Eigen::Vector2f point(center.x(), center.z());
|
||||||
|
|
||||||
for (const NavigationArea& area : areas) {
|
for (const NavigationArea& area : areas) {
|
||||||
if (!area.available) {
|
if (!area.available) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (pointInPolygon(center.x(), center.z(), area.polygon)) {
|
if (pointInPolygon(center.x(), center.z(), area.polygon)) {
|
||||||
|
if (boundaryPadding > 0.0f && externalBoundaryDistance(point) < boundaryPadding) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
walkable[static_cast<size_t>(indexOf(cell))] = 1;
|
walkable[static_cast<size_t>(indexOf(cell))] = 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -341,8 +618,51 @@ void PathFinder::markAvailableAreasWalkable()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PathFinder::markObstaclePolygonsBlocked()
|
||||||
|
{
|
||||||
|
if (obstaclePolygons.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float padding = agentRadius + objectPadding;
|
||||||
|
|
||||||
|
for (int z = 0; z < gridDepth; ++z) {
|
||||||
|
for (int x = 0; x < gridWidth; ++x) {
|
||||||
|
const Cell cell{ x, z };
|
||||||
|
const int idx = indexOf(cell);
|
||||||
|
if (idx < 0 || idx >= static_cast<int>(walkable.size())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!walkable[static_cast<size_t>(idx)]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Eigen::Vector3f center3 = cellCenter(cell);
|
||||||
|
const Eigen::Vector2f center(center3.x(), center3.z());
|
||||||
|
|
||||||
|
for (const ObstaclePolygon& obstacle : obstaclePolygons) {
|
||||||
|
if (pointInPolygon(center.x(), center.y(), obstacle.polygon)) {
|
||||||
|
walkable[static_cast<size_t>(idx)] = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (padding > 0.0f &&
|
||||||
|
distancePointToPolygonEdges(center, obstacle.polygon) <= padding) {
|
||||||
|
walkable[static_cast<size_t>(idx)] = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void PathFinder::markObstacleMeshesBlocked()
|
void PathFinder::markObstacleMeshesBlocked()
|
||||||
{
|
{
|
||||||
|
if (!obstaclePolygons.empty()) {
|
||||||
|
// Prefer JSON-defined obstacle polygons over mesh-derived blockers.
|
||||||
|
// Mesh blockers tend to over-block (e.g. tree crowns) and are harder to author/tune.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const float padding = agentRadius + objectPadding;
|
const float padding = agentRadius + objectPadding;
|
||||||
|
|
||||||
for (const ObstacleMesh& obstacle : obstacles) {
|
for (const ObstacleMesh& obstacle : obstacles) {
|
||||||
@@ -406,7 +726,16 @@ bool PathFinder::isInsideGrid(const Cell& cell) const
|
|||||||
|
|
||||||
bool PathFinder::isCellWalkable(const Cell& cell) const
|
bool PathFinder::isCellWalkable(const Cell& cell) const
|
||||||
{
|
{
|
||||||
return isInsideGrid(cell) && walkable[static_cast<size_t>(indexOf(cell))] != 0;
|
return isCellWalkable(cell, walkable);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PathFinder::isCellWalkable(const Cell& cell, const std::vector<unsigned char>& walkableGrid) const
|
||||||
|
{
|
||||||
|
const int idx = indexOf(cell);
|
||||||
|
return isInsideGrid(cell) &&
|
||||||
|
idx >= 0 &&
|
||||||
|
idx < static_cast<int>(walkableGrid.size()) &&
|
||||||
|
walkableGrid[static_cast<size_t>(idx)] != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int PathFinder::indexOf(const Cell& cell) const
|
int PathFinder::indexOf(const Cell& cell) const
|
||||||
@@ -415,6 +744,11 @@ int PathFinder::indexOf(const Cell& cell) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool PathFinder::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out) const
|
bool PathFinder::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out) const
|
||||||
|
{
|
||||||
|
return findNearestWalkableCell(point, out, walkable);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PathFinder::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out, const std::vector<unsigned char>& walkableGrid) const
|
||||||
{
|
{
|
||||||
Cell origin;
|
Cell origin;
|
||||||
if (!worldToCell(point, origin)) {
|
if (!worldToCell(point, origin)) {
|
||||||
@@ -424,7 +758,7 @@ bool PathFinder::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out
|
|||||||
origin.z = (std::min)((std::max)(z, 0), gridDepth - 1);
|
origin.z = (std::min)((std::max)(z, 0), gridDepth - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCellWalkable(origin)) {
|
if (isCellWalkable(origin, walkableGrid)) {
|
||||||
out = origin;
|
out = origin;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -438,7 +772,7 @@ bool PathFinder::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out
|
|||||||
}
|
}
|
||||||
|
|
||||||
Cell candidate{ origin.x + dx, origin.z + dz };
|
Cell candidate{ origin.x + dx, origin.z + dz };
|
||||||
if (isCellWalkable(candidate)) {
|
if (isCellWalkable(candidate, walkableGrid)) {
|
||||||
out = candidate;
|
out = candidate;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -450,6 +784,11 @@ bool PathFinder::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool PathFinder::hasLineOfSight(const Cell& from, const Cell& to) const
|
bool PathFinder::hasLineOfSight(const Cell& from, const Cell& to) const
|
||||||
|
{
|
||||||
|
return hasLineOfSight(from, to, walkable);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PathFinder::hasLineOfSight(const Cell& from, const Cell& to, const std::vector<unsigned char>& walkableGrid) const
|
||||||
{
|
{
|
||||||
const Eigen::Vector3f a = cellCenter(from);
|
const Eigen::Vector3f a = cellCenter(from);
|
||||||
const Eigen::Vector3f b = cellCenter(to);
|
const Eigen::Vector3f b = cellCenter(to);
|
||||||
@@ -460,7 +799,7 @@ bool PathFinder::hasLineOfSight(const Cell& from, const Cell& to) const
|
|||||||
const float t = static_cast<float>(i) / static_cast<float>(steps);
|
const float t = static_cast<float>(i) / static_cast<float>(steps);
|
||||||
const Eigen::Vector3f p = a * (1.0f - t) + b * t;
|
const Eigen::Vector3f p = a * (1.0f - t) + b * t;
|
||||||
Cell cell;
|
Cell cell;
|
||||||
if (!worldToCell(p, cell) || !isCellWalkable(cell)) {
|
if (!worldToCell(p, cell) || !isCellWalkable(cell, walkableGrid)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -469,6 +808,11 @@ bool PathFinder::hasLineOfSight(const Cell& from, const Cell& to) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<PathFinder::Cell> PathFinder::smoothCells(const std::vector<Cell>& cells) const
|
std::vector<PathFinder::Cell> PathFinder::smoothCells(const std::vector<Cell>& cells) const
|
||||||
|
{
|
||||||
|
return smoothCells(cells, walkable);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<PathFinder::Cell> PathFinder::smoothCells(const std::vector<Cell>& cells, const std::vector<unsigned char>& walkableGrid) const
|
||||||
{
|
{
|
||||||
if (cells.size() <= 2) {
|
if (cells.size() <= 2) {
|
||||||
return cells;
|
return cells;
|
||||||
@@ -480,7 +824,7 @@ std::vector<PathFinder::Cell> PathFinder::smoothCells(const std::vector<Cell>& c
|
|||||||
|
|
||||||
while (anchor < cells.size() - 1) {
|
while (anchor < cells.size() - 1) {
|
||||||
size_t next = cells.size() - 1;
|
size_t next = cells.size() - 1;
|
||||||
while (next > anchor + 1 && !hasLineOfSight(cells[anchor], cells[next])) {
|
while (next > anchor + 1 && !hasLineOfSight(cells[anchor], cells[next], walkableGrid)) {
|
||||||
--next;
|
--next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ public:
|
|||||||
Eigen::Vector3f offset = Eigen::Vector3f::Zero();
|
Eigen::Vector3f offset = Eigen::Vector3f::Zero();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct ObstaclePolygon {
|
||||||
|
std::string name;
|
||||||
|
std::vector<Eigen::Vector2f> polygon;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DynamicObstacle {
|
||||||
|
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||||
|
float radius = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
struct Cell {
|
struct Cell {
|
||||||
int x = 0;
|
int x = 0;
|
||||||
int z = 0;
|
int z = 0;
|
||||||
@@ -32,6 +42,10 @@ public:
|
|||||||
std::vector<Eigen::Vector3f> findPath(const Eigen::Vector3f& start,
|
std::vector<Eigen::Vector3f> findPath(const Eigen::Vector3f& start,
|
||||||
const Eigen::Vector3f& end) const;
|
const Eigen::Vector3f& end) const;
|
||||||
|
|
||||||
|
std::vector<Eigen::Vector3f> findPath(const Eigen::Vector3f& start,
|
||||||
|
const Eigen::Vector3f& end,
|
||||||
|
const std::vector<DynamicObstacle>& dynamicObstacles) const;
|
||||||
|
|
||||||
bool setAreaAvailable(const std::string& areaName, bool available);
|
bool setAreaAvailable(const std::string& areaName, bool available);
|
||||||
bool isReady() const { return ready; }
|
bool isReady() const { return ready; }
|
||||||
bool isWalkable(const Eigen::Vector3f& point) const;
|
bool isWalkable(const Eigen::Vector3f& point) const;
|
||||||
@@ -44,6 +58,7 @@ private:
|
|||||||
float agentRadius = 0.45f;
|
float agentRadius = 0.45f;
|
||||||
float floorY = 0.0f;
|
float floorY = 0.0f;
|
||||||
float objectPadding = 0.25f;
|
float objectPadding = 0.25f;
|
||||||
|
float boundaryPadding = 0.0f;
|
||||||
|
|
||||||
float minX = 0.0f;
|
float minX = 0.0f;
|
||||||
float minZ = 0.0f;
|
float minZ = 0.0f;
|
||||||
@@ -54,6 +69,7 @@ private:
|
|||||||
std::string loadedConfigPath;
|
std::string loadedConfigPath;
|
||||||
std::string loadedZipPath;
|
std::string loadedZipPath;
|
||||||
std::vector<ObstacleMesh> obstacles;
|
std::vector<ObstacleMesh> obstacles;
|
||||||
|
std::vector<ObstaclePolygon> obstaclePolygons;
|
||||||
std::vector<unsigned char> walkable;
|
std::vector<unsigned char> walkable;
|
||||||
std::vector<NavigationArea> areas;
|
std::vector<NavigationArea> areas;
|
||||||
|
|
||||||
@@ -62,6 +78,7 @@ private:
|
|||||||
void rebuildWalkableGrid();
|
void rebuildWalkableGrid();
|
||||||
void markAvailableAreasWalkable();
|
void markAvailableAreasWalkable();
|
||||||
void markObstacleMeshesBlocked();
|
void markObstacleMeshesBlocked();
|
||||||
|
void markObstaclePolygonsBlocked();
|
||||||
|
|
||||||
bool worldToCell(const Eigen::Vector3f& point, Cell& out) const;
|
bool worldToCell(const Eigen::Vector3f& point, Cell& out) const;
|
||||||
Eigen::Vector3f cellCenter(const Cell& cell) const;
|
Eigen::Vector3f cellCenter(const Cell& cell) const;
|
||||||
@@ -74,6 +91,11 @@ private:
|
|||||||
std::vector<Cell> smoothCells(const std::vector<Cell>& cells) const;
|
std::vector<Cell> smoothCells(const std::vector<Cell>& cells) const;
|
||||||
|
|
||||||
static bool pointInPolygon(float x, float z, const std::vector<Eigen::Vector2f>& polygon);
|
static bool pointInPolygon(float x, float z, const std::vector<Eigen::Vector2f>& polygon);
|
||||||
|
|
||||||
|
bool isCellWalkable(const Cell& cell, const std::vector<unsigned char>& walkableGrid) const;
|
||||||
|
bool findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out, const std::vector<unsigned char>& walkableGrid) const;
|
||||||
|
bool hasLineOfSight(const Cell& from, const Cell& to, const std::vector<unsigned char>& walkableGrid) const;
|
||||||
|
std::vector<Cell> smoothCells(const std::vector<Cell>& cells, const std::vector<unsigned char>& walkableGrid) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZL
|
} // namespace ZL
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
#include "quest/QuestJournal.h"
|
|
||||||
#include "external/nlohmann/json.hpp"
|
|
||||||
#include "utils/Utils.h"
|
|
||||||
#include <algorithm>
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
namespace ZL::Quest {
|
|
||||||
|
|
||||||
using json = nlohmann::json;
|
|
||||||
|
|
||||||
const char* toString(QuestStatus status) {
|
|
||||||
switch (status) {
|
|
||||||
case QuestStatus::Hidden: return "Hidden";
|
|
||||||
case QuestStatus::Available: return "Available";
|
|
||||||
case QuestStatus::Active: return "Active";
|
|
||||||
case QuestStatus::Completed: return "Completed";
|
|
||||||
case QuestStatus::Failed: return "Failed";
|
|
||||||
default: return "Unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* toString(QuestCategory category) {
|
|
||||||
switch (category) {
|
|
||||||
case QuestCategory::Main: return "Main";
|
|
||||||
case QuestCategory::Side: return "Side";
|
|
||||||
case QuestCategory::Contract: return "Contract";
|
|
||||||
case QuestCategory::Tutorial: return "Tutorial";
|
|
||||||
default: return "Unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static QuestStatus parseQuestStatus(const std::string& value) {
|
|
||||||
if (value == "Available") return QuestStatus::Available;
|
|
||||||
if (value == "Active") return QuestStatus::Active;
|
|
||||||
if (value == "Completed") return QuestStatus::Completed;
|
|
||||||
if (value == "Failed") return QuestStatus::Failed;
|
|
||||||
return QuestStatus::Hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
static QuestCategory parseQuestCategory(const std::string& value) {
|
|
||||||
if (value == "Main") return QuestCategory::Main;
|
|
||||||
if (value == "Contract") return QuestCategory::Contract;
|
|
||||||
if (value == "Tutorial") return QuestCategory::Tutorial;
|
|
||||||
return QuestCategory::Side;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool QuestJournal::loadFromFile(const std::string& path, const std::string& zipFile) {
|
|
||||||
quests.clear();
|
|
||||||
questOrder.clear();
|
|
||||||
|
|
||||||
std::string content;
|
|
||||||
try {
|
|
||||||
if (zipFile.empty()) {
|
|
||||||
content = ZL::readTextFile(path);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
auto data = ZL::readFileFromZIP(path, zipFile);
|
|
||||||
content.assign(data.begin(), data.end());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (const std::exception& e) {
|
|
||||||
std::cerr << "[quest] Failed to read " << path << ": " << e.what() << std::endl;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.empty()) {
|
|
||||||
std::cerr << "[quest] Empty quest file: " << path << std::endl;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
json root;
|
|
||||||
try {
|
|
||||||
root = json::parse(content);
|
|
||||||
}
|
|
||||||
catch (const std::exception& e) {
|
|
||||||
std::cerr << "[quest] JSON parse error in " << path << ": " << e.what() << std::endl;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!root.contains("quests") || !root["quests"].is_array()) {
|
|
||||||
std::cerr << "[quest] Missing quests array in " << path << std::endl;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& item : root["quests"]) {
|
|
||||||
QuestDefinition def;
|
|
||||||
def.id = item.value("id", "");
|
|
||||||
def.title = item.value("title", def.id);
|
|
||||||
def.description = item.value("description", "");
|
|
||||||
def.category = parseQuestCategory(item.value("category", "Side"));
|
|
||||||
def.initialStatus = parseQuestStatus(item.value("status", "Hidden"));
|
|
||||||
def.recommendedLevel = item.value("recommendedLevel", 0);
|
|
||||||
|
|
||||||
if (item.contains("objectives") && item["objectives"].is_array()) {
|
|
||||||
for (const auto& obj : item["objectives"]) {
|
|
||||||
QuestObjective objective;
|
|
||||||
objective.id = obj.value("id", "");
|
|
||||||
objective.text = obj.value("text", "");
|
|
||||||
objective.completed = obj.value("completed", false);
|
|
||||||
if (!objective.id.empty()) {
|
|
||||||
def.objectives.push_back(objective);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (def.id.empty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
QuestState state;
|
|
||||||
state.definition = def;
|
|
||||||
state.status = def.initialStatus;
|
|
||||||
state.activeObjectiveIndex = 0;
|
|
||||||
state.orderIndex = static_cast<int>(questOrder.size());
|
|
||||||
|
|
||||||
quests[def.id] = std::move(state);
|
|
||||||
questOrder.push_back(def.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool QuestJournal::setStatus(const std::string& questId, QuestStatus status) {
|
|
||||||
QuestState* quest = findQuest(questId);
|
|
||||||
if (!quest) return false;
|
|
||||||
quest->status = status;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool QuestJournal::unlockQuest(const std::string& questId) {
|
|
||||||
QuestState* quest = findQuest(questId);
|
|
||||||
if (!quest) return false;
|
|
||||||
if (quest->status == QuestStatus::Hidden) {
|
|
||||||
quest->status = QuestStatus::Available;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool QuestJournal::startQuest(const std::string& questId) {
|
|
||||||
return setStatus(questId, QuestStatus::Active);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool QuestJournal::completeQuest(const std::string& questId) {
|
|
||||||
return setStatus(questId, QuestStatus::Completed);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool QuestJournal::failQuest(const std::string& questId) {
|
|
||||||
return setStatus(questId, QuestStatus::Failed);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool QuestJournal::setObjectiveCompleted(const std::string& questId, const std::string& objectiveId, bool completed) {
|
|
||||||
QuestState* quest = findQuest(questId);
|
|
||||||
if (!quest) return false;
|
|
||||||
|
|
||||||
for (auto& objective : quest->definition.objectives) {
|
|
||||||
if (objective.id == objectiveId) {
|
|
||||||
objective.completed = completed;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool QuestJournal::setActiveObjective(const std::string& questId, int objectiveIndex) {
|
|
||||||
QuestState* quest = findQuest(questId);
|
|
||||||
if (!quest) return false;
|
|
||||||
|
|
||||||
if (objectiveIndex < 0 || objectiveIndex >= static_cast<int>(quest->definition.objectives.size())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
quest->activeObjectiveIndex = objectiveIndex;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
QuestState* QuestJournal::findQuest(const std::string& questId) {
|
|
||||||
auto it = quests.find(questId);
|
|
||||||
return (it != quests.end()) ? &it->second : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QuestState* QuestJournal::findQuest(const std::string& questId) const {
|
|
||||||
auto it = quests.find(questId);
|
|
||||||
return (it != quests.end()) ? &it->second : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<QuestState*> QuestJournal::getVisibleQuests() {
|
|
||||||
std::vector<QuestState*> result;
|
|
||||||
for (const std::string& id : questOrder) {
|
|
||||||
QuestState* quest = findQuest(id);
|
|
||||||
if (!quest) continue;
|
|
||||||
if (quest->status != QuestStatus::Hidden) {
|
|
||||||
result.push_back(quest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<const QuestState*> QuestJournal::getVisibleQuests() const {
|
|
||||||
std::vector<const QuestState*> result;
|
|
||||||
for (const std::string& id : questOrder) {
|
|
||||||
const QuestState* quest = findQuest(id);
|
|
||||||
if (!quest) continue;
|
|
||||||
if (quest->status != QuestStatus::Hidden) {
|
|
||||||
result.push_back(quest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace ZL::Quest
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "quest/QuestTypes.h"
|
|
||||||
#include <string>
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace ZL::Quest {
|
|
||||||
|
|
||||||
class QuestJournal {
|
|
||||||
public:
|
|
||||||
bool loadFromFile(const std::string& path, const std::string& zipFile = "");
|
|
||||||
|
|
||||||
bool unlockQuest(const std::string& questId);
|
|
||||||
bool startQuest(const std::string& questId);
|
|
||||||
bool completeQuest(const std::string& questId);
|
|
||||||
bool failQuest(const std::string& questId);
|
|
||||||
|
|
||||||
bool setObjectiveCompleted(const std::string& questId, const std::string& objectiveId, bool completed = true);
|
|
||||||
bool setActiveObjective(const std::string& questId, int objectiveIndex);
|
|
||||||
|
|
||||||
QuestState* findQuest(const std::string& questId);
|
|
||||||
const QuestState* findQuest(const std::string& questId) const;
|
|
||||||
|
|
||||||
std::vector<QuestState*> getVisibleQuests();
|
|
||||||
std::vector<const QuestState*> getVisibleQuests() const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::unordered_map<std::string, QuestState> quests;
|
|
||||||
std::vector<std::string> questOrder;
|
|
||||||
|
|
||||||
bool setStatus(const std::string& questId, QuestStatus status);
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace ZL::Quest
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace ZL::Quest {
|
|
||||||
|
|
||||||
enum class QuestStatus {
|
|
||||||
Hidden,
|
|
||||||
Available,
|
|
||||||
Active,
|
|
||||||
Completed,
|
|
||||||
Failed
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class QuestCategory {
|
|
||||||
Main,
|
|
||||||
Side,
|
|
||||||
Contract,
|
|
||||||
Tutorial
|
|
||||||
};
|
|
||||||
|
|
||||||
struct QuestObjective {
|
|
||||||
std::string id;
|
|
||||||
std::string text;
|
|
||||||
bool completed = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct QuestDefinition {
|
|
||||||
std::string id;
|
|
||||||
std::string title;
|
|
||||||
std::string description;
|
|
||||||
QuestCategory category = QuestCategory::Side;
|
|
||||||
QuestStatus initialStatus = QuestStatus::Hidden;
|
|
||||||
int recommendedLevel = 0;
|
|
||||||
std::vector<QuestObjective> objectives;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct QuestState {
|
|
||||||
QuestDefinition definition;
|
|
||||||
QuestStatus status = QuestStatus::Hidden;
|
|
||||||
int activeObjectiveIndex = 0;
|
|
||||||
int orderIndex = 0; // acquisition/load order; larger value means newer quest
|
|
||||||
};
|
|
||||||
|
|
||||||
const char* toString(QuestStatus status);
|
|
||||||
const char* toString(QuestCategory category);
|
|
||||||
|
|
||||||
} // namespace ZL::Quest
|
|
||||||
@@ -347,38 +347,6 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
float TextRenderer::measureTextWidth(const std::string& text, float scale) const
|
|
||||||
{
|
|
||||||
if (text.empty()) {
|
|
||||||
return 0.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
float penX = 0.0f;
|
|
||||||
float maxLineWidth = 0.0f;
|
|
||||||
|
|
||||||
size_t textPos = 0;
|
|
||||||
while (textPos < text.size()) {
|
|
||||||
uint32_t cp = nextUtf8Codepoint(text, textPos);
|
|
||||||
|
|
||||||
if (cp == '\n') {
|
|
||||||
maxLineWidth = max(maxLineWidth, penX);
|
|
||||||
penX = 0.0f;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto it = glyphs.find(cp);
|
|
||||||
if (it == glyphs.end()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GlyphInfo& g = it->second;
|
|
||||||
penX += static_cast<float>(g.advance >> 6) * scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
maxLineWidth = max(maxLineWidth, penX);
|
|
||||||
return maxLineWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextRenderer::drawText(const std::string& text, float x, float y, float scale, bool centered, std::array<float, 4> color)
|
void TextRenderer::drawText(const std::string& text, float x, float y, float scale, bool centered, std::array<float, 4> color)
|
||||||
{
|
{
|
||||||
if (!r || text.empty() || !atlasTexture) return;
|
if (!r || text.empty() || !atlasTexture) return;
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ public:
|
|||||||
bool init(Renderer& renderer, const std::string& ttfPath, int pixelSize, const std::string& zipfilename);
|
bool init(Renderer& renderer, const std::string& ttfPath, int pixelSize, const std::string& zipfilename);
|
||||||
void drawText(const std::string& text, float x, float y, float scale, bool centered, std::array<float, 4> color = { 1.f,1.f,1.f,1.f });
|
void drawText(const std::string& text, float x, float y, float scale, bool centered, std::array<float, 4> color = { 1.f,1.f,1.f,1.f });
|
||||||
|
|
||||||
float measureTextWidth(const std::string& text, float scale = 1.0f) const;
|
|
||||||
float getLineHeight(float scale = 1.0f) const { return lineHeight * scale; }
|
|
||||||
|
|
||||||
// Clear cached meshes (call on window resize / DPI change)
|
// Clear cached meshes (call on window resize / DPI change)
|
||||||
void ClearCache();
|
void ClearCache();
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user