add quest journal (QuestJournal + split UI view)
This commit is contained in:
parent
dbf0af2cb2
commit
d78286a9c2
@ -120,6 +120,9 @@ set(SOURCES
|
||||
../src/dialogue/DialogueOverlay.cpp
|
||||
../src/dialogue/DialogueSystem.h
|
||||
../src/dialogue/DialogueSystem.cpp
|
||||
../src/quest/QuestTypes.h
|
||||
../src/quest/QuestJournal.h
|
||||
../src/quest/QuestJournal.cpp
|
||||
)
|
||||
|
||||
add_executable(bishkek-witcher ${SOURCES})
|
||||
|
||||
@ -73,6 +73,9 @@ add_executable(space-game001
|
||||
../src/dialogue/DialogueOverlay.cpp
|
||||
../src/dialogue/DialogueSystem.h
|
||||
../src/dialogue/DialogueSystem.cpp
|
||||
../src/quest/QuestTypes.h
|
||||
../src/quest/QuestJournal.h
|
||||
../src/quest/QuestJournal.cpp
|
||||
)
|
||||
|
||||
# Установка проекта по умолчанию для Visual Studio
|
||||
|
||||
@ -28,47 +28,72 @@
|
||||
"name": "inventory_items_panel",
|
||||
"x": 50.0,
|
||||
"y": 150.0,
|
||||
"width": 250.0,
|
||||
"height": 300.0,
|
||||
"width": 320.0,
|
||||
"height": 420.0,
|
||||
"children": [
|
||||
{
|
||||
"type": "StaticImage",
|
||||
"name": "panel_background",
|
||||
"width": 200,
|
||||
"height": 400,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"width": 320.0,
|
||||
"height": 420.0,
|
||||
"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",
|
||||
"name": "inventory_items_text",
|
||||
"x": -100.0,
|
||||
"y": -100.0,
|
||||
"width": 250.0,
|
||||
"height": 300.0,
|
||||
"x": 20.0,
|
||||
"y": 70.0,
|
||||
"width": 280.0,
|
||||
"height": 320.0,
|
||||
"text": "Inventory (Empty)",
|
||||
"fontSize": 18,
|
||||
"fontPath": "resources/fonts/DroidSans.ttf",
|
||||
"centered": false,
|
||||
"topAligned": true,
|
||||
"wrap": true,
|
||||
"paddingX": 0.0,
|
||||
"paddingY": 0.0,
|
||||
"maxLines": 14,
|
||||
"color": [1.0, 1.0, 1.0, 1.0]
|
||||
},
|
||||
{
|
||||
"type": "TextButton",
|
||||
"name": "close_inventory_button",
|
||||
"x": 165.0,
|
||||
"y": 0.0,
|
||||
"width": 40.0,
|
||||
"height": 40.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"
|
||||
}
|
||||
"name": "close_inventory_button",
|
||||
"x": 266.0,
|
||||
"y": 16.0,
|
||||
"width": 40.0,
|
||||
"height": 40.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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
353
resources/config2/ui_quest_journal.json
Normal file
353
resources/config2/ui_quest_journal.json
Normal file
@ -0,0 +1,353 @@
|
||||
{
|
||||
"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]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
90
resources/quests/quests.json
Normal file
90
resources/quests/quests.json
Normal file
@ -0,0 +1,90 @@
|
||||
{
|
||||
"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 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
165
src/Game.cpp
165
src/Game.cpp
@ -173,6 +173,11 @@ namespace ZL
|
||||
// Load UI with inventory button
|
||||
try {
|
||||
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;
|
||||
|
||||
menuManager.uiManager.setNodeVisible("inventory_items_panel", false);
|
||||
@ -180,6 +185,9 @@ namespace ZL
|
||||
|
||||
menuManager.uiManager.setTextButtonCallback("inventory_button", [this](const std::string& name) {
|
||||
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("close_inventory_button", true);
|
||||
this->inventoryOpen = true;
|
||||
@ -250,6 +258,149 @@ namespace ZL
|
||||
|
||||
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() {
|
||||
glViewport(0, 0, Environment::width, Environment::height);
|
||||
@ -446,6 +597,14 @@ namespace ZL
|
||||
std::cout << "\n========== MOUSE DOWN EVENT ==========" << std::endl;
|
||||
handleDown(ZL::UiManager::MOUSE_FINGER_ID, mx, my);
|
||||
|
||||
// Inventory and quest journal are modal UI screens. While either one is
|
||||
// open, left-clicks must never reach Location/world movement, even if
|
||||
// the click is on an empty background area of the panel.
|
||||
if (inventoryOpen || questJournalOpen) {
|
||||
std::cout << "[CLICK] Menu open, skipping character movement" << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (menuManager.uiManager.isUiInteractionForFinger(ZL::UiManager::MOUSE_FINGER_ID)) {
|
||||
std::cout << "[CLICK] UI handled, skipping character movement" << std::endl;
|
||||
continue;
|
||||
@ -537,13 +696,17 @@ namespace ZL
|
||||
currentLocation->dialogueSystem.startDialogue("test_cutscene_images_silent_dialogue");
|
||||
break;
|
||||
case SDLK_f:
|
||||
currentLocation->dialogueSystem.startDialogue("test_choice_dialogue");
|
||||
currentLocation->dialogueSystem.startDialogue("test_line_dialogue");
|
||||
break;
|
||||
|
||||
case SDLK_e:
|
||||
currentLocation->dialogueSystem.startDialogue("test_cutscene_pan_dialogue");
|
||||
break;
|
||||
|
||||
case SDLK_j:
|
||||
toggleQuestJournal();
|
||||
break;
|
||||
|
||||
case SDLK_p:
|
||||
break;
|
||||
|
||||
|
||||
13
src/Game.h
13
src/Game.h
@ -20,7 +20,8 @@
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include "Location.h"
|
||||
#include "AudioPlayerAsync.h"
|
||||
#include "AudioPlayerAsync.h"
|
||||
#include "quest/QuestJournal.h"
|
||||
|
||||
namespace ZL {
|
||||
|
||||
@ -51,6 +52,11 @@ namespace ZL {
|
||||
|
||||
bool inventoryOpen = false;
|
||||
|
||||
ZL::Quest::QuestJournal questJournal;
|
||||
bool questJournalOpen = false;
|
||||
int selectedQuestIndex = -1;
|
||||
std::vector<std::string> visibleQuestIds;
|
||||
|
||||
MenuManager menuManager;
|
||||
|
||||
private:
|
||||
@ -69,6 +75,11 @@ namespace ZL {
|
||||
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
|
||||
static Game* s_instance;
|
||||
|
||||
@ -4,12 +4,90 @@
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include "GameConstants.h"
|
||||
|
||||
namespace ZL {
|
||||
|
||||
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) {
|
||||
if (easing == "easein") {
|
||||
return t * t;
|
||||
@ -130,14 +208,60 @@ namespace ZL {
|
||||
|
||||
|
||||
// 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()) {
|
||||
float cx = rect.x + rect.w / 2.0f + animOffsetX;
|
||||
float cy = rect.y + rect.h / 2.0f + animOffsetY;
|
||||
textRenderer->drawText(text, cx, cy, 1.0f, textCentered, color);
|
||||
float tx = rect.x + rect.w / 2.0f + animOffsetX;
|
||||
if (!textCentered) {
|
||||
tx = rect.x + textPaddingX + animOffsetX;
|
||||
}
|
||||
const float ty = rect.y + rect.h * 0.5f + textPaddingY + animOffsetY;
|
||||
textRenderer->drawText(text, tx, ty, 1.0f, textCentered, color);
|
||||
}
|
||||
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() {
|
||||
trackMesh.data.PositionData.clear();
|
||||
trackMesh.data.TexCoordData.clear();
|
||||
@ -490,6 +614,8 @@ namespace ZL {
|
||||
if (j.contains("fontPath")) tb->fontPath = j["fontPath"].get<std::string>();
|
||||
if (j.contains("fontSize")) tb->fontSize = j["fontSize"].get<int>();
|
||||
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) {
|
||||
for (int i = 0; i < 4; ++i) tb->color[i] = j["color"][i].get<float>();
|
||||
}
|
||||
@ -573,6 +699,11 @@ namespace ZL {
|
||||
}
|
||||
}
|
||||
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>();
|
||||
if (!tv->textRenderer->init(renderer, tv->fontPath, tv->fontSize, zipFile)) {
|
||||
@ -675,6 +806,25 @@ namespace ZL {
|
||||
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) {
|
||||
|
||||
@ -1640,6 +1790,15 @@ namespace ZL {
|
||||
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) {
|
||||
for (auto& tb : textButtons) if (tb->name == name) return tb;
|
||||
return nullptr;
|
||||
@ -1672,6 +1831,13 @@ namespace ZL {
|
||||
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) {
|
||||
if (!root) return nullptr;
|
||||
return findNodeByName(root, name);
|
||||
|
||||
@ -145,6 +145,8 @@ namespace ZL {
|
||||
int fontSize = 32;
|
||||
std::array<float, 4> color = { 1.f, 1.f, 1.f, 1.f };
|
||||
bool textCentered = true;
|
||||
float textPaddingX = 12.0f;
|
||||
float textPaddingY = 0.0f;
|
||||
|
||||
std::unique_ptr<TextRenderer> textRenderer;
|
||||
|
||||
@ -169,14 +171,15 @@ namespace ZL {
|
||||
int fontSize = 32;
|
||||
std::array<float, 4> color = { 1.f, 1.f, 1.f, 1.f }; // rgba
|
||||
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;
|
||||
|
||||
void draw(Renderer& renderer) const {
|
||||
if (textRenderer) {
|
||||
textRenderer->drawText(text, rect.x + rect.w / 2, rect.y + rect.h / 2, 1.0f, centered, color);
|
||||
}
|
||||
}
|
||||
void draw(Renderer& renderer) const;
|
||||
};
|
||||
|
||||
struct UiTextField {
|
||||
@ -273,6 +276,7 @@ namespace ZL {
|
||||
|
||||
void replaceRoot(std::shared_ptr<UiNode> newRoot);
|
||||
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);
|
||||
|
||||
@ -329,6 +333,7 @@ namespace ZL {
|
||||
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 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,
|
||||
const std::string& trackPath, const std::string& knobPath, float initialValue = 0.0f, bool vertical = true);
|
||||
@ -339,6 +344,7 @@ namespace ZL {
|
||||
|
||||
std::shared_ptr<UiTextView> findTextView(const std::string& name);
|
||||
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);
|
||||
bool setTextFieldCallback(const std::string& name, std::function<void(const std::string&, const std::string&)> cb);
|
||||
|
||||
211
src/quest/QuestJournal.cpp
Normal file
211
src/quest/QuestJournal.cpp
Normal file
@ -0,0 +1,211 @@
|
||||
#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
|
||||
35
src/quest/QuestJournal.h
Normal file
35
src/quest/QuestJournal.h
Normal file
@ -0,0 +1,35 @@
|
||||
#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
|
||||
49
src/quest/QuestTypes.h
Normal file
49
src/quest/QuestTypes.h
Normal file
@ -0,0 +1,49 @@
|
||||
#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
|
||||
@ -30,6 +30,7 @@ public:
|
||||
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)
|
||||
void ClearCache();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user